Working with Directories
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.
Files.createDirectories(Path) creates the full path — parents included — and is idempotent: if the directory already exists, it returns silently instead of throwing.
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:
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.
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:
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.
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:
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:
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:
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.createDirectoriesin almost all cases — it is idempotent and creates parents. - Use
Files.listfor shallow, lazy directory listing; always close the stream. - Use
Files.walkfor recursive traversal andFiles.findwhen 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.