Files.walk and DirectoryStream: Traversing Directories
Java NIO.2 provides several complementary APIs for walking directory trees: Files.walk() returns a lazy Stream
Contents
Files.walk() basics
Files.walk()Stream<Path>maxDepth
import java.nio.file.*;
import java.io.IOException;
// Files.walk(start) — depth-first, includes the start path itself
// Returns a lazy Stream<Path>; MUST be closed (try-with-resources)
Path root = Path.of("/home/user/projects");
try (Stream<Path> stream = Files.walk(root)) {
stream.forEach(System.out::println);
}
// /home/user/projects
// /home/user/projects/myapp
// /home/user/projects/myapp/src
// /home/user/projects/myapp/src/Main.java
// ...
// walk with maxDepth — 1 = direct children only, 2 = children + grandchildren
try (Stream<Path> shallow = Files.walk(root, 2)) {
shallow.forEach(System.out::println);
}
// The start path (root) is ALWAYS the first element in the stream
// To exclude it: skip(1) or filter(p -> !p.equals(root))
try (Stream<Path> stream = Files.walk(root)) {
stream.skip(1).forEach(System.out::println); // skip root itself
}
// Count all files and directories
try (Stream<Path> stream = Files.walk(root)) {
long count = stream.count();
System.out.println("Total entries: " + count);
}
// Collect all entries into a List
List<Path> allPaths;
try (Stream<Path> stream = Files.walk(root)) {
allPaths = stream.collect(Collectors.toList());
}
// Total size of all files in a directory tree
long totalBytes;
try (Stream<Path> stream = Files.walk(root)) {
totalBytes = stream
.filter(Files::isRegularFile)
.mapToLong(p -> {
try { return Files.size(p); }
catch (IOException e) { return 0L; }
})
.sum();
}
System.out.printf("Total size: %.2f MB%n", totalBytes / 1_048_576.0);
Files.walk()Filtering with Stream operations
Because Files.walk()Stream<Path>filter()Files.isRegularFile()Files.isDirectory()p.toString()Files.find()BiPredicate<Path, BasicFileAttributes>Files.walk() + Files.size()
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
Path root = Path.of("/home/user/projects");
// Find all .java source files
try (Stream<Path> stream = Files.walk(root)) {
List<Path> javaFiles = stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.collect(Collectors.toList());
System.out.println("Java files: " + javaFiles.size());
}
// Find files modified in the last 24 hours
Instant oneDayAgo = Instant.now().minus(Duration.ofDays(1));
try (Stream<Path> stream = Files.walk(root)) {
stream.filter(Files::isRegularFile)
.filter(p -> {
try {
return Files.getLastModifiedTime(p).toInstant().isAfter(oneDayAgo);
} catch (IOException e) { return false; }
})
.forEach(p -> System.out.println("Recent: " + p));
}
// Files.find() — walk + BiPredicate, more efficient than walk + filter
// The predicate receives (Path, BasicFileAttributes) — attributes already read
try (Stream<Path> found = Files.find(root, Integer.MAX_VALUE,
(path, attrs) -> attrs.isRegularFile()
&& attrs.size() > 100_000 // files > 100 KB
&& path.getFileName().toString().endsWith(".log"))) {
found.forEach(p -> System.out.println("Large log: " + p));
}
// Delete all .class files in target/
Path target = Path.of("target");
try (Stream<Path> stream = Files.walk(target)) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".class"))
.forEach(p -> {
try { Files.delete(p); }
catch (IOException e) { System.err.println("Cannot delete: " + p); }
});
}
// Delete a directory tree (delete deepest files first)
try (Stream<Path> stream = Files.walk(target)) {
stream.sorted(Comparator.reverseOrder()) // directories after their children
.forEach(p -> {
try { Files.deleteIfExists(p); }
catch (IOException e) { System.err.println("Failed: " + p); }
});
}
Files.find()Files.walk() + filter()Files.find()Files.walk() + filter()Files.size()Files.getLastModifiedTime()Files.walkFileTree() with FileVisitor
Files.walkFileTree()FileVisitorpreVisitDirectoryvisitFilevisitFileFailedpostVisitDirectoryFileVisitResult
import java.nio.file.*;
import java.nio.file.attribute.*;
import java.io.IOException;
// FileVisitor has 4 callbacks:
// preVisitDirectory — called before entering a directory
// visitFile — called for each file
// visitFileFailed — called when a file cannot be visited (e.g., permission denied)
// postVisitDirectory — called after all entries in a directory have been visited
// SimpleFileVisitor provides default (CONTINUE) implementations
Files.walkFileTree(Path.of("/home/user/projects"), new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
String name = dir.getFileName() == null ? "" : dir.getFileName().toString();
// Skip .git, .svn, node_modules, build output
if (name.equals(".git") || name.equals("node_modules") || name.equals("target")) {
System.out.println("Skipping: " + dir);
return FileVisitResult.SKIP_SUBTREE;
}
System.out.println("Entering: " + dir);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.printf(" File: %-50s %,d bytes%n",
file.getFileName(), attrs.size());
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
System.err.println("Cannot access: " + file + " reason: " + exc.getMessage());
return FileVisitResult.CONTINUE; // continue despite access error
}
@Override
public FileVisitResult postVisitDirectory(Path dir, IOException exc) {
if (exc != null) System.err.println("Error after visiting: " + dir);
return FileVisitResult.CONTINUE;
}
});
// FileVisitResult values:
// CONTINUE — keep walking
// SKIP_SUBTREE — skip this directory's children (only valid in preVisitDirectory)
// SKIP_SIBLINGS — skip remaining entries in the current directory
// TERMINATE — stop the entire walk immediately
// Practical: find and return first matching file
class FileFinder extends SimpleFileVisitor<Path> {
private final String target;
Path found = null;
FileFinder(String target) { this.target = target; }
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
if (file.getFileName().toString().equals(target)) {
found = file;
return FileVisitResult.TERMINATE; // stop as soon as found
}
return FileVisitResult.CONTINUE;
}
}
FileFinder finder = new FileFinder("config.properties");
Files.walkFileTree(Path.of("."), finder);
System.out.println(finder.found != null ? "Found: " + finder.found : "Not found");
DirectoryStream for single-level listing
Files.newDirectoryStream()DirectoryStreamCloseable
import java.nio.file.*;
import java.io.IOException;
// DirectoryStream — efficient single-level directory iteration (not recursive)
// More lightweight than Stream<Path> from Files.list() for large directories
// Must be closed (implements Closeable)
Path dir = Path.of("/home/user/downloads");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}
// Order is NOT guaranteed — depends on the OS and filesystem
// Filter: only regular files
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path entry : stream) {
if (Files.isRegularFile(entry)) {
System.out.printf("File: %s (%,d bytes)%n",
entry.getFileName(), Files.size(entry));
}
}
}
// Collect to a sorted list
List<Path> sorted;
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
sorted = new ArrayList<>();
for (Path p : stream) sorted.add(p);
}
sorted.sort(Comparator.comparing(p -> p.getFileName().toString()));
sorted.forEach(System.out::println);
// DirectoryStream with a DirectoryStream.Filter — custom predicate
DirectoryStream.Filter<Path> filter = entry ->
Files.isRegularFile(entry) && Files.size(entry) > 1_000_000; // > 1 MB
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, filter)) {
for (Path entry : stream) {
System.out.println("Large file: " + entry.getFileName());
}
}
DirectoryStream with glob filter
An overload of Files.newDirectoryStream()*?[...]{a,b}"*.{txt,csv}"*PathMatcherFiles.walk()**
// Files.newDirectoryStream(dir, glob) — glob pattern for single-level listing
// Glob syntax (not regex!):
// * — matches any string not containing /
// ** — matches any string including / (for path matching)
// ? — matches any single character
// [ab] — matches 'a' or 'b'
// {a,b}— matches 'a' or 'b' (alternative)
Path dir = Path.of("/home/user/documents");
// All .pdf files in the directory
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.pdf")) {
for (Path p : stream) {
System.out.println(p.getFileName());
}
}
// All files matching multiple extensions
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "*.{jpg,jpeg,png,gif}")) {
for (Path p : stream) {
System.out.println("Image: " + p.getFileName());
}
}
// Files starting with "report_"
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir, "report_*")) {
for (Path p : stream) {
System.out.println(p.getFileName());
}
}
// Using PathMatcher with glob for deep matching (Files.walk + PathMatcher)
PathMatcher matcher = FileSystems.getDefault()
.getPathMatcher("glob:**/*.{java,kt}"); // any .java or .kt file, any depth
try (Stream<Path> walk = Files.walk(Path.of("src"))) {
walk.filter(matcher::matches)
.forEach(System.out::println);
}
// Glob vs regex — glob is simpler but less powerful
// Use glob for file-extension and name-pattern matching
// Use regex (Pattern) for complex content or path structure matching
// Check if a single path matches a glob
PathMatcher javaMatch = FileSystems.getDefault().getPathMatcher("glob:*.java");
System.out.println(javaMatch.matches(Path.of("Main.java"))); // true
System.out.println(javaMatch.matches(Path.of("Main.class"))); // false
Note: The glob delimiter */**glob:**/*.java.javaglob:*.java
Files.list() vs Files.walk()
Files.list()Files.walk()maxDepth=1Stream<Path>Files.list()lsFiles.walk()DirectoryStream
import java.nio.file.*;
Path dir = Path.of("/home/user/projects");
// Files.list(dir) — non-recursive, single level, returns Stream<Path>
// Equivalent to DirectoryStream but as a Stream
try (Stream<Path> stream = Files.list(dir)) {
stream.filter(Files::isDirectory)
.map(Path::getFileName)
.sorted()
.forEach(System.out::println);
}
// Files.walk(dir) — recursive, depth-first, all descendants
try (Stream<Path> stream = Files.walk(dir)) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// Comparison:
// ┌──────────────────┬──────────────┬──────────────────────────────────┐
// │ API │ Recursive? │ Best for │
// ├──────────────────┼──────────────┼──────────────────────────────────┤
// │ Files.list() │ No (1 level)│ ls equivalent, simple listing │
// │ DirectoryStream │ No (1 level)│ Memory-efficient iteration │
// │ Files.walk() │ Yes │ Stream pipeline over all files │
// │ Files.find() │ Yes │ Walk + attribute filter in one │
// │ walkFileTree() │ Yes │ Precise control, skip subtrees │
// └──────────────────┴──────────────┴──────────────────────────────────┘
// Practical: list only immediate subdirectories
try (Stream<Path> stream = Files.list(dir)) {
List<String> subdirs = stream
.filter(Files::isDirectory)
.map(p -> p.getFileName().toString())
.sorted()
.collect(Collectors.toList());
System.out.println("Subdirectories: " + subdirs);
}
// Practical: count files per extension recursively
try (Stream<Path> stream = Files.walk(dir)) {
Map<String, Long> byExt = stream
.filter(Files::isRegularFile)
.collect(Collectors.groupingBy(
p -> {
String name = p.getFileName().toString();
int dot = name.lastIndexOf('.');
return dot >= 0 ? name.substring(dot) : "(no ext)";
},
Collectors.counting()
));
byExt.entrySet().stream()
.sorted(Map.Entry.<String, Long>comparingByValue().reversed())
.forEach(e -> System.out.printf("%-15s %,d%n", e.getKey(), e.getValue()));
}
Handling symbolic links and cycles
By default, Files.walk()FileVisitOption.FOLLOW_LINKSFileSystemLoopExceptionFiles.walkFileTree()FOLLOW_LINKSFileSystemLoopExceptionvisitFileFailedFileVisitResult.CONTINUE
import java.nio.file.*;
// By default, Files.walk() does NOT follow symbolic links
// This prevents infinite loops from symlink cycles
Path root = Path.of("/home/user/projects");
// Default — symlinks are listed as symlinks but NOT followed
try (Stream<Path> stream = Files.walk(root)) {
stream.filter(Files::isSymbolicLink)
.forEach(p -> System.out.println("Symlink: " + p));
}
// Follow symlinks — use FileVisitOption.FOLLOW_LINKS
// If a cycle is detected, a FileSystemLoopException is reported via visitFileFailed
try (Stream<Path> stream = Files.walk(root, FileVisitOption.FOLLOW_LINKS)) {
stream.filter(Files::isRegularFile)
.forEach(System.out::println);
}
// May throw FileSystemLoopException if a cycle exists
// walkFileTree with FOLLOW_LINKS — handle cycle in visitFileFailed
Files.walkFileTree(root,
EnumSet.of(FileVisitOption.FOLLOW_LINKS),
Integer.MAX_VALUE,
new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
if (exc instanceof FileSystemLoopException) {
System.err.println("Cycle detected: " + file);
return FileVisitResult.CONTINUE; // skip the cycle
}
System.err.println("Cannot visit: " + file + " — " + exc);
return FileVisitResult.CONTINUE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
System.out.println(file);
return FileVisitResult.CONTINUE;
}
});
// Detect symlinks and resolve them
Path link = Path.of("/home/user/projects/current");
if (Files.isSymbolicLink(link)) {
Path target = Files.readSymbolicLink(link);
Path resolved = link.getParent().resolve(target).normalize();
System.out.println(link + " → " + resolved);
}
// Real path — resolves all symlinks in the path
Path real = link.toRealPath(); // throws NoSuchFileException if broken symlink
System.out.println("Real path: " + real);
// toRealPath without following links (Java has no direct option — use toAbsolutePath().normalize())
Path abs = link.toAbsolutePath().normalize(); // normalizes .. but does NOT resolve symlinks
FileVisitOption.FOLLOW_LINKSFileSystemLoopExceptionvisitFileFailed