Contents
- Files.walk() basics
- Filtering with Stream operations
- Files.walkFileTree() with FileVisitor
- DirectoryStream for single-level listing
- DirectoryStream with glob filter
- Files.list() vs Files.walk()
- Handling symbolic links and cycles
Files.walk() returns a lazy Stream<Path> that traverses a directory tree depth-first, including the starting path itself as the first element. The stream must always be closed — it holds an open file-system handle — so wrap it in try-with-resources every time. An overload accepts a maxDepth argument to limit how deep the traversal goes: depth 1 yields only the immediate children, depth 2 adds grandchildren, and so on.
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);
Always close the Stream returned by Files.walk() — it holds an open file handle to the directory. Failing to close it leaks the handle and may exhaust the OS file descriptor limit. Use try-with-resources every time.
Because Files.walk() returns a standard Stream<Path>, any stream operation can be chained directly. Use filter() with Files.isRegularFile(), Files.isDirectory(), or extension checks on p.toString() to narrow results. When you need to filter on file attributes such as size or last-modified time, prefer Files.find() instead: it accepts a BiPredicate<Path, BasicFileAttributes> and reads attributes in a single OS call per file, avoiding the extra stat calls that Files.walk() + Files.size() would require.
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); }
});
}
Prefer Files.find() over Files.walk() + filter() when you need to inspect file attributes (size, timestamps, type) in the filter. Files.find() reads attributes once per file as part of the walk, whereas Files.walk() + filter() requires separate Files.size() or Files.getLastModifiedTime() calls — each of which is a separate OS stat call.
Files.walkFileTree() gives fine-grained control over directory traversal through the FileVisitor interface, which provides four callbacks: preVisitDirectory fires before entering a directory, visitFile fires for each file, visitFileFailed handles per-file errors such as permission denied, and postVisitDirectory fires after all entries have been visited. Each callback returns a FileVisitResult that controls whether to continue, skip a subtree, skip siblings, or stop entirely. This is the right tool when you need to skip specific directories, terminate early on a match, or respond differently to files versus directories.
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");
Files.newDirectoryStream() lists only the immediate children of a directory — it does not recurse into subdirectories. It returns entries lazily via an iterator, making it more memory-efficient than collecting all paths into a list up front, especially for directories with thousands of entries. The order of entries is filesystem-dependent and not guaranteed to be sorted. Like all NIO stream-based APIs, DirectoryStream implements Closeable and must be closed with try-with-resources.
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());
}
}
An overload of Files.newDirectoryStream() accepts a glob pattern string that filters entries before they are returned, avoiding the need for a separate filter step. Glob syntax uses * to match any sequence of characters within a single name component, ? to match exactly one character, [...] for character classes, and {a,b} for alternatives — for example "*.{txt,csv}" matches all text and CSV files. Note that * does not cross the path separator; use PathMatcher with Files.walk() and the ** glob segment when you need to match across multiple directory levels.
// 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
The glob delimiter * does not match the path separator /. To match across directory separators use **. For example, glob:**/*.java matches any .java file at any depth, while glob:*.java only matches in the directory being searched.
Files.list() is essentially Files.walk() with maxDepth=1 — it returns a Stream<Path> of the immediate children of a directory without recursing. Both return lazy streams that must be closed. Choose Files.list() when you only need one level (equivalent to ls), Files.walk() when you need all descendants, and DirectoryStream when you need the most memory-efficient single-level iteration without the stream API overhead.
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()));
}
By default, Files.walk() does not follow symbolic links — symlinked directories appear as entries but their contents are not traversed, making cycles impossible. To follow symlinks, pass FileVisitOption.FOLLOW_LINKS; if the walk encounters a directory it has already visited via a symlink, it throws a FileSystemLoopException. When using Files.walkFileTree() with FOLLOW_LINKS, handle FileSystemLoopException in visitFileFailed and return FileVisitResult.CONTINUE to skip the cycle rather than aborting the entire walk.
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
Only use FileVisitOption.FOLLOW_LINKS when you explicitly need to traverse into symlinked directories. Without it, directory cycles are impossible because the walk never follows symlinks into directories it has already visited. With it, always handle FileSystemLoopException in visitFileFailed, otherwise the walk throws and stops.