NIO.2

NIO.2, introduced in Java 7, modernized Java's file I/O with the Path/Paths/Files trio, directory streams, rich file-attribute support, symbolic-link handling, and channel-based I/O.

NIO.2 (New I/O 2, delivered in Java 7 under JSR 203) is the modern file-system API that largely superseded the older java.io.File-centric approach. It centers on Path as the location abstraction, Files as the operations utility class, directory streams for efficient iteration, a pluggable file-attribute model that goes beyond basic metadata, first-class symbolic-link support, and channel-based I/O for both files and networking.

Why NIO.2 Replaced java.io.File

The legacy java.io.File class had long-standing design problems: many of its methods returned a boolean false on failure instead of throwing an exception with a useful reason (making error handling weak), it had no real support for symbolic links, no easy way to iterate large directories without loading every name into memory, and no way to walk a file tree with fine-grained control. NIO.2 addressed all of these with Path/Files, which favor exceptions over silent boolean failures and provide symbolic-link-aware and streaming-capable operations.

java.io.File is still present and still works, and File.toPath() converts it to a Path for use with the newer API — but new code should generally prefer Path/Files directly.

Path — The Core Abstraction

java.nio.file.Path represents a file-system location, and is the type nearly every NIO.2 operation accepts or returns. It supports rich manipulation independent of any actual file system access: resolving relative segments, normalizing ./.., comparing paths, and converting between relative and absolute forms.

import java.nio.file.Path;

Path p = Path.of("a", "b", "..", "c", "file.txt");
System.out.println(p);              // a/b/../c/file.txt
System.out.println(p.normalize());  // a/c/file.txt
System.out.println(p.toAbsolutePath()); // resolved against the current working directory
System.out.println(p.getNameCount()); // number of path elements

Paths — The Legacy Factory Class

java.nio.file.Paths is a small factory class predating Path.of(...): its static Paths.get(String, String...) method creates a Path, identically to what Path.of(...) (added in Java 11) does today. Path.of(...) is now the preferred, more concise form for new code, but Paths.get(...) remains common in code written for Java 7-10 or in existing codebases.

import java.nio.file.Paths;
import java.nio.file.Path;

Path viaOldFactory = Paths.get("data", "input.csv");  // pre-Java-11 idiom
Path viaNewFactory = Path.of("data", "input.csv");     // Java 11+, preferred
System.out.println(viaOldFactory.equals(viaNewFactory)); // true - same result

Files — Operations on Paths

Files remains the workhorse utility class: existence checks, create/read/write/delete, copy/move, and attribute access, as covered in depth in file-handling topics. NIO.2's design principle is that Path never performs I/O itself — every actual file-system operation goes through a static Files method that takes a Path argument.

Directory Streams

Files.newDirectoryStream(dir) returns a DirectoryStream<Path> for efficient, lazy iteration over a directory's entries — it does not read the whole directory listing into memory at once, which matters for very large directories. It implements Closeable, so it should be used in try-with-resources. An overload accepts a glob filter string to pre-filter entries by name pattern.

The glob "*.java" filters entries to only Java source files without loading the full directory listing first.

For most modern code, Files.list(dir) (a Stream<Path>) is more convenient than DirectoryStream and is generally preferred; DirectoryStream remains useful for its glob-filtering constructor and for very old code predating Files.list (added later than the original NIO.2 release).

import java.nio.file.DirectoryStream;
import java.nio.file.Files;

try (DirectoryStream<Path> stream = Files.newDirectoryStream(Path.of("."), "*.java")) {
    for (Path entry : stream) {
        System.out.println(entry.getFileName());
    }
}

File Attributes

NIO.2 models file metadata through 'file attribute views' — pluggable, file-system-specific bundles of metadata beyond the lowest-common-denominator basics. BasicFileAttributes (available on every file system) covers size, timestamps, and file type; PosixFileAttributes (POSIX-compliant systems like Linux/macOS) adds owner, group, and permission bits; DosFileAttributes (Windows) adds hidden/system/archive/read-only flags.

PosixFileAttributes/PosixFilePermission are available only on POSIX-compliant file systems and throw UnsupportedOperationException elsewhere (e.g. typical Windows setups).

import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFileAttributes;
import java.util.Set;

Path p = Path.of("script.sh");
// Reading POSIX attributes (Linux/macOS only)
PosixFileAttributes attrs = Files.readAttributes(p, PosixFileAttributes.class);
System.out.println(attrs.owner());
System.out.println(attrs.permissions());

// Setting POSIX permissions directly
Set<PosixFilePermission> perms = PosixFilePermission.fromString("rwxr-xr-x");
Files.setPosixFilePermissions(p, perms);

Symbolic Links

NIO.2 has first-class symbolic link support: Files.createSymbolicLink(link, target) creates one, Files.isSymbolicLink(path) checks for one, and Files.readSymbolicLink(link) returns the target it points to. Crucially, most Files methods follow symbolic links by default (operating on what the link points to), but accept LinkOption.NOFOLLOW_LINKS to instead operate on the link itself.

Creating symbolic links may require elevated privileges on some operating systems (notably Windows without Developer Mode enabled), throwing an exception if permission is lacking.

import java.nio.file.LinkOption;

Path link = Path.of("shortcut");
Path target = Path.of("/real/data/file.txt");
Files.createSymbolicLink(link, target);

System.out.println(Files.isSymbolicLink(link));       // true
System.out.println(Files.readSymbolicLink(link));      // /real/data/file.txt
System.out.println(Files.exists(link, LinkOption.NOFOLLOW_LINKS)); // checks the link itself, not its target

Recursive Directory Walking

Files.walk(start, maxDepth) returns a lazy Stream<Path> over an entire directory tree; Files.walkFileTree(start, visitor) offers finer control via a FileVisitor callback interface with separate hooks for entering a directory, visiting a file, failing to visit a file, and finishing a directory — useful for operations like recursive delete or recursive copy where you need to act at specific points in the traversal, not just get a flat list of paths.

A recursive-delete pattern: files are deleted as they're visited, and each directory is deleted only after all its contents (postVisitDirectory) have been removed.

import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;

Files.walkFileTree(Path.of("build"), 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 {
        Files.delete(dir); // delete the directory itself after its contents are gone
        return FileVisitResult.CONTINUE;
    }
});

Channels Overview

NIO (and NIO.2) also introduced channel-based I/O as an alternative to the classic stream-based java.io model. FileChannel provides efficient, position-based (seekable) reading and writing of files, often paired with ByteBuffer for buffer-oriented transfer, and supports operations like file locking (FileChannel.lock()) and memory-mapped file access (map()) that classic streams cannot do. Channels are lower-level than Files' convenience methods and are typically reached for only when you need random access, memory-mapping, or interoperability with non-blocking network channels.

FileChannel.open() gives seekable, buffer-based access; unlike an InputStream, you can also reposition the channel to read/write at arbitrary offsets.

import java.nio.channels.FileChannel;
import java.nio.ByteBuffer;
import java.nio.file.StandardOpenOption;

try (FileChannel channel = FileChannel.open(Path.of("data.bin"), StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    int bytesRead = channel.read(buffer);
    buffer.flip();
    // process buffer contents up to bytesRead
}

Practical Usage

Typical NIO.2 usage combines these pieces: use Path/Files for everyday file operations, Files.walk/walkFileTree for recursive tree operations (backup tools, build cleanup, recursive search), attribute views when you need platform-specific metadata (permissions, ownership), symbolic-link-aware calls when working in environments that use links (deployment 'latest' symlinks, for example), and drop down to FileChannel only for random-access or memory-mapped scenarios that Files' convenience methods don't cover.

Key Takeaways

NIO.2 (Java 7, JSR 203) modernized file I/O around Path and Files, replacing much of java.io.File's role for new code.

Paths.get(...) is the pre-Java-11 factory; Path.of(...) (Java 11+) does the same thing and is now preferred.

DirectoryStream and Files.list/Files.walk both provide lazy iteration; both must be closed via try-with-resources.

File attributes are modeled as pluggable views (BasicFileAttributes everywhere, PosixFileAttributes/DosFileAttributes on their respective platforms).

Most Files methods follow symbolic links by default; pass LinkOption.NOFOLLOW_LINKS to operate on the link itself.

FileChannel offers seekable, buffer-based, and memory-mapped file access beyond what the Files convenience methods provide.

PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); // run unconditionally, including on typical Windows setups try { PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); } catch (UnsupportedOperationException e) { // fall back to BasicFileAttributes on non-POSIX file systems }

PosixFileAttributes is only supported on POSIX-compliant file systems (typically Linux/macOS); requesting it on a file system that doesn't support it throws UnsupportedOperationException.

Files.delete(symlinkToDirectory); // intending to delete only the link, but symlink targets a large directory

Files.delete(symlinkToDirectory); // Files.delete on a symlink itself removes only the link entry, not its target - verify with Files.isSymbolicLink first if unsure

It's worth explicitly confirming with Files.isSymbolicLink(path) before deleting when the intent is ambiguous, since accidentally deleting through a followed link versus deleting the link itself have very different consequences and default-follow behavior varies by operation.

DirectoryStream<Path> stream = Files.newDirectoryStream(dir); for (Path p: stream) { process(p); } // stream never closed try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) { for (Path p: stream) { process(p); } }

DirectoryStream implements Closeable and holds an open file-system resource; failing to close it (via try-with-resources) leaks that resource.

What core design problems in java.io.File did NIO.2 address?

java.io.File methods often returned a bare boolean false on failure instead of a descriptive exception, had no real symbolic-link support, and had no efficient way to lazily iterate large directories or walk a file tree with fine-grained control. NIO.2's Path/Files favor exceptions over silent failure and add symbolic-link-aware, streaming-capable operations.

What is the relationship between Paths.get(...) and Path.of(...)?

They produce equivalent Path objects. Paths.get(...) is the original NIO.2 (Java 7) factory method; Path.of(...), added in Java 11, does the same thing with a more concise call and is now the preferred idiom for new code.

Why must a DirectoryStream or a Files.list()/Files.walk() stream be closed?

Each holds an open underlying file-system resource (a directory handle) for its lazy iteration; failing to close it leaks that resource, which can eventually exhaust file-descriptor limits under heavy use.

What is the difference between BasicFileAttributes, PosixFileAttributes, and DosFileAttributes?

BasicFileAttributes provides the lowest-common-denominator metadata (size, timestamps, file type) available on every file system. PosixFileAttributes adds owner/group/permission-bit metadata available on POSIX-compliant systems (Linux/macOS). DosFileAttributes adds Windows-specific flags (hidden, system, archive, read-only). Requesting an unsupported view throws UnsupportedOperationException.

By default, do most Files methods operate on a symbolic link itself or on what it points to?

By default, most Files methods follow symbolic links and operate on their target. Passing LinkOption.NOFOLLOW_LINKS to methods that accept it makes them operate on the link itself instead.

When would you use FileChannel instead of the Files convenience methods?

When you need random/seekable access to specific byte offsets within a file, memory-mapped file access, file locking, or interoperability with non-blocking network channels — capabilities the simpler Files methods (readString, writeString, copy, etc.) do not provide.

What is the key structural difference between Files.walk() and Files.walkFileTree()?

Files.walk() returns a flat, lazy Stream<Path> over the tree for simple filtering/collecting use cases. Files.walkFileTree() takes a FileVisitor callback with distinct hooks (preVisitDirectory, visitFile, visitFileFailed, postVisitDirectory), giving finer control needed for operations like ordered recursive delete or copy.

Write a program that lists all .log files directly inside a directory using DirectoryStream with a glob filter.

import java.nio.file.*; public class LogLister { public static void main(String[] args) throws Exception { Path dir = Path.of("/var/log"); try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.log")) { for (Path entry : stream) { System.out.println(entry.getFileName()); } } } }

Files.newDirectoryStream(dir, "*.log") lazily iterates only entries matching the glob pattern *.log directly inside the directory (non-recursive), and the try-with-resources ensures the directory stream is closed after iteration.

What does this program print, assuming the symbolic link creation succeeds?

It prints 'true' (link is indeed a symbolic link) and then 'release-2.3' (readSymbolicLink returns the raw target path the link points to, without resolving it further).

Write a program using Files.walkFileTree to recursively delete a directory and all its contents.

import java.io.IOException; import java.nio.file.*; import java.nio.file.attribute.BasicFileAttributes; public class RecursiveDelete { public static void main(String[] args) throws IOException { Path target = Path.of("build/temp"); Files.walkFileTree(target, 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 { Files.delete(dir); return FileVisitResult.CONTINUE; } }); } } visitFile deletes each regular file as the walk encounters it; postVisitDirectory runs only after all of a directory's children have already been visited (and deleted), so each directory is deleted last, bottom-up, guaranteeing Files.delete never fails on a non-empty directory.

Path link = Path.of("current");
Files.createSymbolicLink(link, Path.of("release-2.3"));
System.out.println(Files.isSymbolicLink(link));
System.out.println(Files.readSymbolicLink(link));