1. NIO2: a closer look
We’ve already been introduced to NIO2, but now we’ll review and deepen our knowledge of this useful library for working with files and directories.
Previously, Java had only the File class. It could check if a file exists, create and delete files and folders, and get a list of files in a directory. But it had many limitations:
- it’s inconvenient to work with paths, especially if you need to account for different operating systems (C:\Users\user\file.txt on Windows and /home/user/file.txt on Linux);
- there’s no proper support for symbolic links, permissions, and file attributes;
- directory tree traversal capabilities are limited;
- error handling left much to be desired.
With the advent of NIO2 (New Input/Output, version 2) in Java 7, life for developers became simpler and more pleasant. Now you have:
- the Path class for convenient work with file and folder paths;
- the Files class, which provides all basic operations: reading, writing, copying, deleting, and retrieving file information;
- the FileVisitor interface and methods like Files.walk that let you traverse the file system easily and flexibly.
Why is this important?
- Cross-platform: The same code runs on Windows, Linux, and macOS without worrying about separators (/ or \).
- Safety and convenience: More informative errors, less magic and fewer unexpected surprises.
- Power: You can process huge directories and even traverse recursively with filtering and parallel processing.
2. Core classes: Path and Files
The Path class
Path is the modern representation of a file or folder path. It doesn’t have to point to an actually existing file—it’s simply a path that’s convenient to work with.
Getting a Path
import java.nio.file.Path;
import java.nio.file.Paths;
Path path1 = Paths.get("file.txt"); // relative path
Path path2 = Paths.get("/home/user/file.txt"); // absolute path
Path path3 = Path.of("mydir", "subdir", "file.txt"); // since Java 11+
Fact: Path is OS-independent. Forget about manually concatenating strings with / or \!
Convert to string
System.out.println(path1.toString());
Getting the parent directory and file name
Path parent = path1.getParent(); // may be null for relative paths
Path fileName = path1.getFileName(); // file name only
The Files class
Files is a collection of static methods for all file and directory operations:
- Existence check: Files.exists(path)
- Reading and writing files: Files.readAllBytes(path), Files.write(path, bytes)
- Retrieving information: Files.size(path), Files.getLastModifiedTime(path)
- Copying, deleting, moving: Files.copy, Files.delete, Files.move
Examples:
import java.nio.file.Files;
import java.nio.file.Path;
Path path = Path.of("file.txt");
if (Files.exists(path)) {
System.out.println("File exists!");
System.out.println("Size: " + Files.size(path) + " bytes");
System.out.println("Last modified: " + Files.getLastModifiedTime(path));
} else {
System.out.println("File not found.");
}
3. File system traversal: Files.walk and friends
The problem with the old approach
In the old API, to traverse all files in a folder and its subfolders, you had to write recursive functions, manually check what’s a file and what’s a directory, and ensure you didn’t end up in infinite recursion. This was not only tedious but also very error-prone.
Modern approach: Files.walk
Files.walk(Path start) returns a Stream<Path>—a stream of all files and directories starting from the specified path, including all subdirectories. Now traversing the file system is just working with streams!
Example: Print all files and directories
import java.nio.file.*;
try (var paths = Files.walk(Path.of("mydir"))) {
paths.forEach(System.out::println);
}
This will print all paths—both files and directories—starting at mydir.
Example: Files only (no directories)
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.forEach(System.out::println);
}
The Files.isRegularFile(path) method returns true only for regular files (not directories or symlinks).
Example: Find files by extension
Suppose we need to find all .txt files in a directory and its subdirectories:
try (var paths = Files.walk(Path.of("mydir"))) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(System.out::println);
}
Example: Calculate the total size of all files
long totalSize = 0;
try (var paths = Files.walk(Path.of("mydir"))) {
totalSize = paths.filter(Files::isRegularFile)
.mapToLong(path -> {
try {
return Files.size(path);
} catch (Exception e) {
System.err.println("Error reading size: " + path);
return 0L;
}
})
.sum();
}
System.out.println("Total size of files: " + totalSize + " bytes");
Important!
- The Files.walk method returns a stream that must be closed (it implements AutoCloseable). Therefore, use try-with-resources!
- By default, traversal depth is to the very bottom (all subdirectories). You can limit the depth: Files.walk(path, maxDepth)
4. Practical tasks
Task 1: Find all images in a directory
You need to find all files with the extensions .jpg, .png, .gif in the images folder and print their names.
import java.nio.file.*;
import java.util.Set;
Set<String> extensions = Set.of(".jpg", ".png", ".gif");
try (var paths = Files.walk(Path.of("images"))) {
paths.filter(Files::isRegularFile)
.filter(path -> {
String name = path.getFileName().toString().toLowerCase();
return extensions.stream().anyMatch(name::endsWith);
})
.forEach(System.out::println);
}
Task 2: Copy all .txt files to another folder
import java.nio.file.*;
Path sourceDir = Path.of("src");
Path destDir = Path.of("dest");
try (var paths = Files.walk(sourceDir)) {
paths.filter(Files::isRegularFile)
.filter(path -> path.toString().endsWith(".txt"))
.forEach(path -> {
try {
Path relative = sourceDir.relativize(path);
Path target = destDir.resolve(relative);
Files.createDirectories(target.getParent());
Files.copy(path, target, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied: " + path + " -> " + target);
} catch (Exception e) {
System.err.println("Copy error: " + path);
}
});
}
Here we preserve the subdirectory structure.
5. Useful details
Benefits of NIO2
Cross-platform
Path handles directory separators for you. Your code will run the same way on Windows, Linux, and macOS.
Stream-based processing
Methods like Files.walk return a stream (Stream<Path>) that you can filter, transform, and collect—everything the Stream API can do.
Working with large directories
The old API could “crash” if there were too many files (for example, 100,000 photos). NIO2 handles such cases easily because it doesn’t load everything into memory at once.
Support for symlinks, attributes, permissions
You can check whether a path is a symbolic link (Files.isSymbolicLink(path)), get permissions (Files.getPosixFilePermissions(path)), find the file owner, and much more.
Comparing the old and the new APIs
| Operation | Old API (File) | New API (Path, Files) |
|---|---|---|
| Check existence | |
|
| Get size | |
|
| List files in a directory | |
|
| Recursive traversal | Manual recursion | |
| Copy a file | file.renameTo() (flaky) | |
| Get extension | Parse the string | |
| Get parent | |
|
| Permissions handling | Barely possible | |
Important aspects
File type checks
- Files.isRegularFile(path) — regular file
- Files.isDirectory(path) — directory
- Files.isSymbolicLink(path) — symlink
Working with large directories
- Don’t collect all paths into a list: work with Stream<Path> and process items as they come.
- After finishing, always close the stream (try-with-resources).
Exceptions
- Almost all methods can throw IOException—remember to handle errors (or rethrow them).
Limiting traversal depth
try (var paths = Files.walk(Path.of("mydir"), 2)) { // only 2 levels
// ...
}
6. Common mistakes when working with NIO2
Mistake #1: forgot to close the walk stream. If you don’t use try-with-resources, you can leak resources—the file system stream will remain open. Always use the construct try (var paths = Files.walk(...)) { ... }.
Mistake #2: didn’t check that the path is a directory. If you pass a file path instead of a directory to Files.walk, you may get unexpected behavior or an error.
Mistake #3: didn’t handle exceptions. Almost all NIO2 methods can throw IOException. Don’t ignore these errors—at the very least print a message to the user or log it.
Mistake #4: confusion with path separators. If you manually concatenate paths with / or \, don’t do that! Use Path.of(...) or resolve(...)—they’ll handle it for you.
Mistake #5: trying to read a huge directory “into memory”. Don’t collect all paths into a list if there are many files—work with Stream<Path> and process them as they come.
Mistake #6: forgot about cross-platform concerns. Don’t hardcode absolute paths in Windows or Unix style. Use Path and operations like resolve/relativize—they’ll do the right thing on any OS.
GO TO FULL VERSION