FileChannel Fundamentals
A FileChannel is a SeekableByteChannel connected to a file, facilitating reading, writing, mapping, and manipulation. Unlike network socket channels that can operate in non-blocking mode with a selector, FileChannel operates exclusively in blocking mode. It maintains a current position within the file that can be queried and modified.
Acquiring a FileChannel
Direct instantiation of a FileChannel is not permitted. Instances must be retrieved via the getChannel() method of one of the following I/O streams:
- FileInputStream: Returns a channel open only for reading.
- FileOutputStream: Returns a channel open only for writing.
- RandomAccessFile: The read/write capability is determined by the mode string (
"r","rw", etc.) used during construction.
Reading and Writing Data
Interaction with a channel typically involves a ByteBuffer.
To read data:
int bytesRead = fileChannel.read(buffer);
// Returns -1 if the end of the stream is reached.
Writing requires checking for remaining data in the buffer, as a single write operation may not consume the entire buffer content:
buffer.put(data);
buffer.flip(); // Switch buffer to read mode
while (buffer.hasRemaining()) {
channel.write(buffer);
}
Position and Size Management
The current position can be obtained or modified:
long currentPos = channel.position();
channel.position(1024); // Set position to byte 1024
If the position is set beyond the current file size and data is written, a "hole" (filled with zeros) is created between the old end and the new data. The file size is retrieved via channel.size().
Forcing Updates to Disk
Operating systems often cache file data for performance. To ensure both file content and metadata (permissions, etc.) are written to the storage device immediately, invoke:
channel.force(true);
Direct Data Transfer (Zero Copy)
The transferTo method allows efficient transfer of data between channels, often utilizing zero-copy techniques to transfer data directly from the filesystem cache to the target channel without passing through user space.
try (FileChannel sourceChannel = new FileInputStream("source.log").getChannel();
FileChannel destChannel = new FileOutputStream("dest.log").getChannel()) {
long size = sourceChannel.size();
long position = 0;
// transferTo has a limit (often 2GB), so loop for large files
while (position < size) {
long transferred = sourceChannel.transferTo(position, size - position, destChannel);
position += transferred;
}
} catch (IOException e) {
e.printStackTrace();
}
NIO Path and Files Utilities
Java NIO introduced the Path interface to represent file system paths and the Paths utility class to obtain Path instances. The Files class provides static methods for manipulating files and directories using these Path objects.
Working with Paths
Paths.get() accepts a string or a sequence of strings to construct a path. It handles both absolute and relative paths.
Path p1 = Paths.get("data/config.properties"); // Relative path
Path p2 = Paths.get("/usr/local/bin"); // Absolute path
// Normalizing removes redundancies like "." or ".."
Path messyPath = Paths.get("/home/user/../documents/./file.txt");
System.out.println(messyPath.normalize()); // Output: /home/documents/file.txt
Common Files Operations
Creating Directories
Path singleDir = Paths.get("data/newdir");
Files.createDirectory(singleDir); // Throws exception if parent doesn't exist or dir exists
Path multiDir = Paths.get("data/level1/level2");
Files.createDirectories(multiDir); // Creates all non-existent parent directories
Copying and Moving
By default, copy and move operations throw an exception if the target exists. StandardCopyOption can be used to modify this behavior.
Path source = Paths.get("source.txt");
Path target = Paths.get("target.txt");
// Copy and replace if exists
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
// Move atomically if supported
Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
Deleting
Files.delete(target); // Throws NoSuchFileException if missing
// Files.deleteIfExists(target); // Safer alternative
Traversing Directories (Visitor Pattern)
The Files.walkFileTree method implements the Visitor design pattern, allowing you to define behaviors for pre-visiting directories, visiting files, and post-visiting directories. This is useful for tasks like recursive deletion or counting specific file types.
AtomicInteger dirCount = new AtomicInteger();
AtomicInteger javaFileCount = new AtomicInteger();
Path startPath = Paths.get("C:\\Projects\\MyJavaApp");
Files.walkFileTree(startPath, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
dirCount.incrementAndGet();
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.toString().endsWith(".java")) {
javaFileCount.incrementAndGet();
}
return FileVisitResult.CONTINUE;
}
});
System.out.println("Directories: " + dirCount.get());
System.out.println("Java Files: " + javaFileCount.get());
Recursively Deleting a Directory
To delete a directory tree, delete files first, then the empty directories as the traversal backs out.
Path rootToDelete = Paths.get("/tmp/archive");
Files.walkFileTree(rootToDelete, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc == null) {
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
throw exc;
}
});
Copying a Directory Tree
Files.walk returns a Stream of paths, which can be used to perform operations recursively.
Path sourceFolder = Paths.get("D:\\Source");
Path targetFolder = Paths.get("D:\\Backup");
try (Stream<Path> stream = Files.walk(sourceFolder)) {
stream.forEach(source -> {
Path destination = targetFolder.resolve(sourceFolder.relativize(source));
try {
if (Files.isDirectory(source)) {
if (!Files.exists(destination)) {
Files.createDirectory(destination);
}
} else {
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e) {
System.err.println("Error copying " + source + ": " + e.getMessage());
}
});
}