14.4 Advanced NIO.2: Directory Walking, Streams (`Files.lines`, `walk`), and `WatchService`
Key Takeaways
Files.list, Files.walk, Files.find, and Files.lines return lazy, functional streams backed by operating system file descriptors that must be enclosed in try-with-resources to prevent resource leaks.
Files.list(Path) performs a shallow single-level directory listing, while Files.walk(Path, maxDepth) performs a recursive depth-first traversal without following symbolic links by default.
Files.lines(Path) reads file lines lazily as a
Stream<String>, avoiding the high memory footprint of Files.readAllLines(Path) on multi-gigabyte files.BasicFileAttributes provides a read-only bulk snapshot of file metadata (size, timestamps, type flags), while BasicFileAttributeView is required to modify file timestamps.
Files.walk(start, maxDepth) counts the start path at depth 0, so walk(dir, 1) returns the directory plus its immediate entries while Files.list(dir) returns only the entries.
14.4 Advanced NIO.2: Directory Walking, Streams (Files.lines, walk), and WatchService
NIO.2 integrates seamlessly with the Java Stream API, providing lazy, functional methods on java.nio.file.Files for directory listing, recursive tree traversal, targeted file searching, and line-by-line file reading. Additionally, NIO.2 provides high-performance file attribute inspection through BasicFileAttributes and native operating system change notifications via WatchService. Exam questions focus on the resource lifecycle of these streams, lazy evaluation, and depth limits; WatchService is covered at the end as background because it is not named in the current objectives.
Functional Stream Methods in Files
Files includes four core methods that return java.util.stream.Stream. Because these streams interact directly with underlying file systems, understanding their depth, traversal order, and filtering behavior is vital.
NIO.2 Stream Methods
|
+-----------------+--------+--------+-----------------+
| | | |
Files.list() Files.walk() Files.find() Files.lines()
(Depth = 1) (Depth-First) (Depth + Filter) (Lazy Lines)
1. Files.list(Path dir)
Returns a Stream<Path> of entries located directly in the specified directory:
- Shallow: Traverses only the immediate children (equivalent to depth 1). It does not recurse into subdirectories.
- Does not include
.or..entries. - Throws
NotDirectoryExceptionifdiris not a directory.
try (Stream<Path> stream = Files.list(Path.of("/var/log"))) {
stream.forEach(System.out::println);
}
2. Files.walk(Path start, int maxDepth, FileVisitOption... options)
Returns a Stream<Path> populated by depth-first recursive traversal starting at start:
- Overloaded version
Files.walk(Path start)defaultsmaxDepthtoInteger.MAX_VALUE. - A
maxDepthof0returns a stream containing only the starting path itself. AmaxDepthof1matches the behavior ofFiles.list()plus the root. - Symbolic Links: By default,
Files.walkdoes not follow symbolic links. To follow links, passFileVisitOption.FOLLOW_LINKS.
Warning
The Circular Link Loop Trap
When FileVisitOption.FOLLOW_LINKS is enabled, circular symbolic link cycles (a link pointing to an ancestor folder) can cause infinite traversal loops. If NIO.2 detects a cycle during Files.walk, the stream throws an UncheckedIOException whose cause is a java.nio.file.FileSystemLoopException.
3. Files.find(Path start, int maxDepth, BiPredicate<Path, BasicFileAttributes> matcher, FileVisitOption... options)
Performs recursive traversal while applying an attribute-aware filter in a single optimized pass:
- Takes a
BiPredicate<Path, BasicFileAttributes>allowing simultaneous filtering on thePathand its underlying file metadata. - Avoids the performance overhead of querying file attributes separately inside a
.filter()call.
Path rootDir = Path.of("/data/reports");
try (Stream<Path> stream = Files.find(rootDir, 5,
(path, attrs) -> attrs.isRegularFile() &&
attrs.size() > 1_000_000 &&
path.toString().endsWith(".pdf"))) {
stream.forEach(p -> System.out.println("Large report: " + p));
}
4. Files.lines(Path path)
Reads all lines from a file lazily as a Stream<String> using UTF-8 (or a specified Charset):
- Lazy streaming: Lines are read from disk only as demanded by downstream intermediate and terminal operations.
- Contrast with
Files.readAllLines(Path):Files.readAllLines(Path)reads the entire file eagerly into memory, returning aList<String>. Calling this on a 10 GB log file triggersOutOfMemoryError.Files.lines(Path)streams lines lazily, processing gigabyte-scale files with minimal, constant heap memory consumption.
Mandatory Resource Management: Closing I/O Streams
A critical exam topic is resource leaks in NIO.2 streams. Standard Java in-memory streams (such as List.stream()) do not hold external operating system handles and do not require closing.
However, all streams returned by Files.list(), Files.walk(), Files.find(), and Files.lines() implement AutoCloseable and hold open operating system file handles, directory iterators, or file channel locks.
- Terminal stream operations (such as
.count(),.findFirst(), or.collect()) DO NOT close the underlying stream! - If an application repeatedly opens streams via
Files.lines()orFiles.walk()without closing them, the operating system eventually exhausts its file descriptor table, resulting injava.io.IOException: Too many open files. - Always enclose NIO.2 streams in a try-with-resources statement:
// CORRECT: Try-with-resources guarantees file descriptor release
try (Stream<String> lines = Files.lines(Path.of("/var/log/app.log"))) {
long errorCount = lines.filter(l -> l.contains("ERROR")).count();
System.out.println("Errors: " + errorCount);
}
File Attributes: BasicFileAttributes vs. BasicFileAttributeView
Querying individual file properties using individual static methods (such as Files.size(), Files.isDirectory(), Files.getLastModifiedTime()) requires a separate round-trip system call for each property.
NIO.2 provides bulk attribute retrieval through attribute views in java.nio.file.attribute:
Attribute Architecture
|
+-----------------------+-----------------------+
| |
[Read-Only Snapshot] [Mutable View]
BasicFileAttributes BasicFileAttributeView
(size, creationTime, isDirectory) (setTimes(mod, acc, create))
BasicFileAttributes (Read-Only Bulk Snapshot)
Retrieves standard file system metadata in a single native system call via Files.readAttributes():
Path file = Path.of("data.txt");
BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
long size = attrs.size(); // File size in bytes
FileTime created = attrs.creationTime(); // Creation timestamp
FileTime modified = attrs.lastModifiedTime(); // Last modified timestamp
boolean isReg = attrs.isRegularFile(); // True if standard file
boolean isDir = attrs.isDirectory(); // True if directory
boolean isSym = attrs.isSymbolicLink(); // True if symbolic link
boolean isOther = attrs.isOther(); // True if device, pipe, socket
BasicFileAttributes is strictly read-only; it contains getter methods only.
BasicFileAttributeView (Mutable View)
To modify file system metadata, obtain a view instance via Files.getFileAttributeView():
BasicFileAttributeView view = Files.getFileAttributeView(file, BasicFileAttributeView.class);
FileTime newTime = FileTime.fromMillis(System.currentTimeMillis());
// Update timestamps: lastModifiedTime, lastAccessTime, createTime
// Passing null preserves the existing timestamp for that attribute
view.setTimes(newTime, null, null);
Other specialized attribute views include DosFileAttributeView (for DOS hidden, archive, readonly flags) and PosixFileAttributeView (for Unix POSIX permissions and ownership).
Asynchronous File Monitoring: WatchService (Background)
Note
WatchService is not named in the current 1Z0-829 objectives; treat this subsection as background knowledge.
The WatchService API provides asynchronous notifications when registered directories experience file system events. Where the platform supports it, the JDK uses native event hooks (such as Linux inotify or Windows ReadDirectoryChangesW); on other platforms, such as macOS, it falls back to polling.
Setting Up a WatchService
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
public class DirectoryMonitor {
public static void watchDirectory(Path dir) throws Exception {
// Step 1: Create WatchService from default FileSystem
try (WatchService watcher = FileSystems.getDefault().newWatchService()) {
// Step 2: Register directory with event kinds
dir.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
// Step 3: Event listening loop
while (true) {
// Step 4: Retrieve signaled key (take blocks until event arrives)
WatchKey key = watcher.take();
// Step 5: Process queued events
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
// StandardWatchEventKinds.OVERFLOW signals lost/dropped events
if (kind == OVERFLOW) continue;
// Context for entry events is a relative Path (filename)
WatchEvent<Path> ev = (WatchEvent<Path>) event;
Path filename = ev.context();
System.out.printf("Event %s on file %s%n", kind.name(), filename);
}
// Step 6: CRUCIAL - Reset key to receive further events
boolean valid = key.reset();
if (!valid) {
System.out.println("Monitored directory is no longer accessible");
break;
}
}
}
}
}
Key Methods for Event Retrieval
watcher.poll(): Non-blocking. Returns the next availableWatchKey, ornullif no events have occurred.watcher.poll(long timeout, TimeUnit unit): Blocks up to the specified timeout waiting for a key to be signaled.watcher.take(): Blocks indefinitely until a key is signaled or the thread is interrupted.
The key.reset() State Machine Rule
When events occur in a registered directory, the associated WatchKey transitions from the ready state to the signaled state and is queued in the WatchService.
Warning
The WatchKey.reset() Trap
Once a WatchKey is signaled, no further events will be queued for that key until key.reset() is explicitly called! If you process events but forget to invoke key.reset(), the directory remains in the signaled state, and your application will never receive another notification for that folder. If key.reset() returns false, the directory has been deleted or has become inaccessible, and the loop should terminate.
A developer needs to search for an error token across a 15 GB log file in an application running with a 2 GB JVM heap. Which NIO.2 approach avoids an OutOfMemoryError?
try (Stream<String> lines = Files.lines(path)) { return lines.filter(line -> line.contains("FATAL")).findFirst(); }
Files.walk(path).map(Path::toString).filter(s -> s.contains("FATAL")).findFirst()
Files.readString(path).lines().filter(line -> line.contains("FATAL")).findFirst()
Files.readAllLines(path).stream().filter(line -> line.contains("FATAL")).findFirst()
Consider the following method designed to count source code files in a project tree:
public long countJavaFiles(Path dir) throws IOException {
return Files.walk(dir)
.filter(p -> p.toString().endsWith(".java"))
.count();
}
What potential bug or production issue is present in this implementation?
Files.walk() only inspects the immediate child files of dir and fails to traverse nested subdirectories.
The Stream<Path> returned by Files.walk() is not closed using try-with-resources, risking operating system file descriptor leaks.
Terminal stream operations like count() cannot be called on directory streams.
Files.walk() throws NoSuchFileException if any discovered .java file is empty.
A directory named data contains the file a.txt and a subdirectory logs, which contains b.txt. What does this code print?
Path dir = Path.of("data");
try (Stream<Path> s0 = Files.walk(dir, 0);
Stream<Path> s1 = Files.walk(dir, 1);
Stream<Path> list = Files.list(dir)) {
System.out.println(s0.count() + " " + s1.count() + " " + list.count());
}
0 2 2
0 3 3
1 2 2
1 3 2
Which code snippet correctly updates the last modified timestamp of an existing file data.log using NIO.2 attribute views?
BasicFileAttributeView view = Files.readAttributes(path, BasicFileAttributeView.class); view.setTimes(newTime, null, null);
Files.readAttributes(path, BasicFileAttributes.class).setLastModifiedTime(newTime);
BasicFileAttributeView view = Files.getFileAttributeView(path, BasicFileAttributeView.class); view.setTimes(newTime, null, null);
Files.getLastModifiedTime(path).toInstant().plusSeconds(60);
Sections you finish are checked off in the contents.