Directory Traversal with Files.walk()
Fast recall
Quick revision
Core answer
Files.walk(start, maxDepth, options) inside try-with-resources to obtain a lazy depth-first stream of Paths, filter or process it, and close it so open directory resources are released.Interview-ready guide
Interview answer
Files.walk recursively traverses a directory tree and returns a lazy Stream. The stream visits the starting path and then descendants in depth-first order. Because iteration can keep one or more directories open, the stream must be used in try-with-resources rather than left for garbage collection.
The simple overload walks to unlimited depth without following symbolic links. Another overload accepts maxDepth and FileVisitOption.FOLLOW_LINKS. A depth of zero yields only the start. Following links can turn the directory structure into a graph; Java attempts to detect cycles and can report FileSystemLoopException.
A typical pipeline filters with Files.isRegularFile(path), perhaps narrows by extension, and then maps or processes each path. For example, a cleanup report can walk a temporary tree, count regular .log files, and close the stream when the terminal operation completes.
Traversal is weakly consistent. It does not freeze the tree, so files may be added, removed, or replaced during iteration. If opening the start fails, the method throws IOException immediately. If a later lazy access fails after the stream has been returned, the operation can throw UncheckedIOException with the original IOException as its cause.
Files.walk is concise for stream-style queries, but it is not always the best control structure. Files.walkFileTree provides callbacks for pre-visit, file visit, failures, and post-visit, making it clearer for deletion, copying, permission handling, or recovery. Whichever API is chosen, define link behavior, depth, failure policy, concurrent-change expectations, and resource lifetime explicitly.
Directory Traversal with Files.walk()
Direct Answer
Use Files.walk(start, maxDepth, options) inside try-with-resources to obtain a lazy depth-first stream of Paths, filter or process it, and close it so open directory resources are released.
Deep Dive & Architecture
The Stream abstraction can make a walk look like an in-memory collection, but its elements are discovered as the terminal operation asks for them. Directories may remain open during that discovery. Closing the stream releases those resources, which is why assigning it to a long-lived field or returning it without an ownership rule is hazardous.
Weak consistency means there is no promise that a changing tree is represented at one instant. A file seen earlier may disappear before metadata is read, and a newly created entry may or may not be visited. Applications that need a stable input set require an immutable publication scheme, a snapshot-capable storage layer, or a separate inventory phase with its own consistency rules.
Symbolic-link handling changes both meaning and safety. Without FOLLOW_LINKS, a link is encountered as an entry but its directory target is not descended into. With it, traversal can leave the apparent tree and encounter cycles. Cycle detection itself may require comparing file keys or real paths and can fail with a loop exception.
For destructive algorithms, visitor callbacks are usually easier to reason about. A recursive delete needs post-order directory deletion and an explicit response to visit failures. walkFileTree names those lifecycle points, while a compact stream pipeline can hide important partial-failure behavior.
Code Example
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
class WalkDirectory {
public static void main(String[] args) throws Exception {
Path root = Files.createTempDirectory("walk-");
try {
Files.createDirectories(root.resolve("nested"));
Files.writeString(root.resolve("a.log"), "a", StandardCharsets.UTF_8);
Files.writeString(root.resolve("nested/b.log"), "b", StandardCharsets.UTF_8);
Files.writeString(root.resolve("nested/c.txt"), "c", StandardCharsets.UTF_8);
long logs;
try (var paths = Files.walk(root)) {
logs = paths.filter(Files::isRegularFile)
.filter(path -> path.getFileName().toString().endsWith(".log"))
.count();
}
if (logs != 2) throw new AssertionError(logs);
System.out.println(logs);
} finally {
try (var paths = Files.walk(root)) {
for (Path path : paths.sorted((a, b) -> b.compareTo(a)).toList()) {
Files.deleteIfExists(path);
}
}
}
}
}
Check your understanding
Answer these without looking back at the article.
- Why must the Stream from Files.walk be closed?
- What changes when FOLLOW_LINKS is enabled?
- When is walkFileTree clearer than Files.walk?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.