File I/O & NIO.2

Working with Directories

15 min Lesson 5 of 13

Working with Directories

Directories are the skeleton of any file system. Java's NIO.2 API — centred on java.nio.file.Files and java.nio.file.Path — gives you precise, expressive control over creating directories, listing their contents, and walking entire directory trees. In this lesson you will learn exactly which method to reach for and, equally important, why each design choice was made.

Creating Directories

NIO.2 provides two distinct factory methods, and choosing the wrong one is a classic mistake.

Files.createDirectory(Path) creates exactly one directory. The call fails with NoSuchFileException if any parent does not already exist, and with FileAlreadyExistsException if the target itself exists.

import java.nio.file.*; Path dir = Path.of("output/reports"); Files.createDirectory(dir); // throws if 'output' does not exist

Files.createDirectories(Path) creates the full path — parents included — and is idempotent: if the directory already exists, it returns silently instead of throwing.

Path nested = Path.of("output/2024/reports/q1"); Files.createDirectories(nested); // creates every missing segment; no-op if all exist
Prefer createDirectories in application code. Real applications rarely know in advance whether a parent exists. Using createDirectories removes the need for a prior existence check and is race-condition-safe — the check and creation happen atomically at the OS level.

You can also create a directory with specific POSIX permissions in one call:

import java.nio.file.attribute.*; import java.util.Set; Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rwxr-x---"); FileAttribute<Set<PosixFilePermission>> attr = PosixFilePermissions.asFileAttribute(perms); Files.createDirectories(Path.of("secure/vault"), attr); // Applies only on POSIX systems (Linux, macOS). Silently ignored on Windows.

Listing Directory Entries

Three methods exist for listing a directory's contents. They differ in depth, laziness, and the control they give you.

Files.list(Path) returns a lazy Stream<Path> of the direct children only (depth 1). Because the stream is backed by a directory iterator, you must close it — always use try-with-resources.

Path source = Path.of("src/main/java"); try (var entries = Files.list(source)) { entries .filter(p -> p.toString().endsWith(".java")) .map(Path::getFileName) .forEach(System.out::println); }
Unclosed directory streams leak OS file descriptors. On Linux the default per-process limit is 1 024. Always use try-with-resources around Files.list, Files.walk, and Files.find.

Files.newDirectoryStream(Path) and its glob overload give you a DirectoryStream<Path> — useful when you want glob-pattern filtering handled by the OS rather than Java:

try (var stream = Files.newDirectoryStream(Path.of("logs"), "*.log")) { for (Path logFile : stream) { System.out.println(logFile.getFileName()); } }

Walking a Directory Tree

Files.walk(Path, int maxDepth) returns a lazy Stream<Path> that performs a depth-first traversal. The root itself is always the first element emitted.

Path root = Path.of("project"); try (var tree = Files.walk(root)) { long javaFileCount = tree .filter(Files::isRegularFile) .filter(p -> p.toString().endsWith(".java")) .count(); System.out.println("Java files found: " + javaFileCount); }

Omitting maxDepth walks the entire subtree; supplying it caps the recursion. A depth of 1 is equivalent to Files.list.

Files.find(Path, int maxDepth, BiPredicate<Path, BasicFileAttributes>) is the more powerful cousin. The predicate receives both the path and its BasicFileAttributes snapshot, so you can filter on metadata (size, last-modified, whether it is a symlink) without a second system call per entry:

import java.nio.file.attribute.BasicFileAttributes; import java.time.Instant; import java.time.temporal.ChronoUnit; Instant cutoff = Instant.now().minus(7, ChronoUnit.DAYS); try (var recent = Files.find( Path.of("uploads"), Integer.MAX_VALUE, (path, attrs) -> attrs.isRegularFile() && attrs.lastModifiedTime().toInstant().isAfter(cutoff))) { recent.forEach(System.out::println); }
Why Files.find beats Files.walk + Files.readAttributes. Every call to Files.readAttributes is a separate OS syscall. Files.find captures the attributes in the same syscall that reads the directory entry, so you pay once, not twice.

Deleting a Directory Tree

Directories must be empty before Files.delete(Path) accepts them. To recursively delete a tree, walk it with BOTTOM_UP ordering by reversing a sorted stream, or use FileVisitor:

import java.util.Comparator; try (var tree = Files.walk(Path.of("tmp/scratch"))) { tree.sorted(Comparator.reverseOrder()) // deepest entries first .forEach(p -> { try { Files.delete(p); } catch (Exception e) { throw new RuntimeException(e); } }); }
There is no recycle bin. Files.delete is permanent. In production code, confirm the root path carefully before recursing. A misplaced variable has deleted entire project directories.

Copying and Moving Directory Trees

Files.copy copies a single entry; it does not recurse. To copy a whole tree, walk it and replicate the structure manually:

Path src = Path.of("templates/base"); Path dest = Path.of("projects/new-app"); try (var tree = Files.walk(src)) { tree.forEach(source -> { Path target = dest.resolve(src.relativize(source)); try { if (Files.isDirectory(source)) { Files.createDirectories(target); } else { Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING); } } catch (Exception e) { throw new RuntimeException(e); } }); }

The key idiom here is dest.resolve(src.relativize(source)): it strips the source root prefix and grafts each remaining segment onto the destination root — producing the mirror path without string manipulation.

Summary

  • Use Files.createDirectories in almost all cases — it is idempotent and creates parents.
  • Use Files.list for shallow, lazy directory listing; always close the stream.
  • Use Files.walk for recursive traversal and Files.find when you need attribute-based filtering in the same pass.
  • Delete trees bottom-up by reversing a sorted walk; copy trees by walking and resolving relative paths.