NIO.2: The Path and Files API
Objective
NIO.2 (java.nio.file, added in JDK 7) is the modern replacement for java.io.File: it represents a location with the Path interface, performs file operations through the static Files class, and adds real filesystem features File never had — symbolic-link awareness and change notifications through WatchService. The core difference isn't cosmetic: File's methods report failure by returning false or doing nothing, while Files methods throw a specific checked exception that says exactly what went wrong.
Use Cases
- Deployment scripts that must know why a copy or move failed (target exists vs. permission denied vs. source missing) instead of just getting
falseback. - Build tooling that walks a directory of release artifacts where
currentis a symbolic link to the active version and the code needs to detect and follow (or not follow) that link deliberately. - One-shot, filtered listing of a directory's immediate entries (by glob or custom predicate) without pulling in a full recursive
Stream<Path>. - File-drop or hot-reload pipelines that react to files appearing in a folder instead of polling it on a timer.
- Migrating legacy code built on
java.io.Fileincrementally, usingFile.toPath()andPath.toFile()as the bridge between the two APIs.
Deep Dive
File's silent failures vs. Files' informative exceptions
java.io.File reports most failures as a boolean or simply does nothing — it never says why:
javaFile target = new File("/no/such/dir/report.txt");
boolean ok = target.createNewFile(); // false — parent doesn't exist, no message at all
File missing = new File("ghost.txt");
boolean deleted = missing.delete(); // false — file never existed, still no messageFiles performs the same operations but throws a specific, informative exception instead:
javaPath target = Path.of("/no/such/dir/report.txt");
Files.createFile(target);
// throws NoSuchFileException: /no/such/dir/report.txt
Path missing = Path.of("ghost.txt");
Files.delete(missing);
// throws NoSuchFileException: ghost.txt
Path notEmpty = Path.of("some-dir");
Files.delete(notEmpty);
// throws DirectoryNotEmptyException: some-dir (if it contains entries)deleteIfExists() is the one method that keeps the "safe, no exception" shape on purpose — it returns true/false for existence, but still throws if the directory isn't empty or an I/O error occurs, so it isn't a silent-failure method either.
DirectoryStream: a closeable, filterable, single-use iterator
Files.list()/Files.walk() (already familiar from the basic NIO topic) return a Stream<Path>. DirectoryStream<Path>, obtained from Files.newDirectoryStream(), is the lower-level NIO.2 primitive they're built on: it implements both AutoCloseable and Iterable<Path>, so it plugs into try-with-resources and a for-each loop directly:
javatry (DirectoryStream<Path> stream = Files.newDirectoryStream(Path.of("."))) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}It can filter by glob directly, without a separate Stream.filter() step:
javatry (DirectoryStream<Path> java = Files.newDirectoryStream(dir, "*.java")) {
for (Path p : java) System.out.println(p);
}Or by a custom DirectoryStream.Filter<Path> when the condition isn't name-based:
javaDirectoryStream.Filter<Path> writable = Files::isWritable;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, writable)) {
for (Path p : stream) System.out.println(p);
}Its iterator can only be obtained once — calling iterator() (or running the for-each) a second time on the same instance throws:
javaDirectoryStream<Path> stream = Files.newDirectoryStream(dir);
stream.iterator(); // fine
stream.iterator(); // throws IllegalStateException: iterator has already been returnedAn I/O error mid-iteration (e.g., the directory is removed underneath you) surfaces as DirectoryIteratorException wrapping the real IOException, thrown from hasNext()/next() rather than from newDirectoryStream() itself.
Symbolic links: a filesystem feature File can't see
java.io.File has no concept of a symbolic link at all — it follows one transparently and exposes no method to detect it existed. NIO.2 makes links a first-class, inspectable thing:
javaPath link = Path.of("current");
Files.createSymbolicLink(link, Path.of("release-2.3.0"));
Files.isSymbolicLink(link); // true
Files.readSymbolicLink(link); // release-2.3.0 (the link's target, not resolved)
Files.isDirectory(link); // follows the link by default → true, if target is a dir
Files.isDirectory(link, LinkOption.NOFOLLOW_LINKS); // false — link itself isn't a directoryreadSymbolicLink() throws NotLinkException if the path isn't actually a symbolic link — another example of a specific, named failure instead of a boolean. isSymbolicLink() itself doesn't follow that same "throw on trouble" pattern: it quietly returns false both when the path isn't a link and when the path doesn't exist at all, so a false result alone doesn't tell you which case you're in.
Most Files methods that touch link targets (copy, isDirectory, readAttributes, ...) accept LinkOption.NOFOLLOW_LINKS to operate on the link itself instead of transparently following it — there is no equivalent switch anywhere in File.
WatchService: reacting to filesystem changes instead of polling
Path implements Watchable, so any path can register with a WatchService for specific kinds of events instead of a program re-scanning a directory on a timer:
javatry (WatchService watcher = FileSystems.getDefault().newWatchService()) {
Path dir = Path.of("incoming");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_DELETE,
StandardWatchEventKinds.ENTRY_MODIFY);
while (true) {
WatchKey key = watcher.take(); // blocks until an event is queued
for (WatchEvent<?> event : key.pollEvents()) {
if (event.kind() == StandardWatchEventKinds.OVERFLOW) {
continue; // events may have been lost — consider re-scanning the directory
}
Path changed = (Path) event.context(); // name relative to the registered dir
System.out.println(event.kind() + ": " + changed);
}
if (!key.reset()) break; // the watched directory became inaccessible
}
}take() blocks until a WatchKey has events; poll()/poll(timeout, unit) give a non-blocking or timed alternative. key.reset() must be called after processing to put the key back in the ready state — forgetting it means the key never fires again even though events keep occurring. File has no equivalent: detecting changes with it means writing your own polling loop that compares timestamps or directory listings.
Trade-offs
- Exceptions demand handling, not just an
if—Filesmethods force atry/catch(or athrowsdeclaration) for each specific failure mode instead of a single boolean check, which is more verbose but removes the "it returned false, now guess why" debugging step. DirectoryStream's iterator is single-use — reusing an instance across two loops is a bug, not a style choice:
javaDirectoryStream<Path> ds = Files.newDirectoryStream(dir);
for (Path p : ds) { /* ok */ }
for (Path p : ds) { /* IllegalStateException at iterator() */ }- Symbolic link support is filesystem- and OS-dependent —
Files.createSymbolicLink()throwsUnsupportedOperationExceptionon filesystems without link support, and on Windows it typically requires elevated privileges or Developer Mode; code that creates links needs a fallback path or a documented requirement. WatchServiceis best-effort, not a real-time guarantee — events can coalesce, arrive with platform-dependent latency, and overflow into a singleOVERFLOWevent if too many queue up before you drain them; it's unreliable over network filesystems and isn't a substitute for verifying state withFilesafter an event fires.