CodeGym /Courses /JAVA 25 SELF /NIO2: Files, Paths, Files.walk: file system traversal

NIO2: Files, Paths, Files.walk: file system traversal

JAVA 25 SELF
Level 39 , Lesson 0
Available

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
file.exists()
Files.exists(path)
Get size
file.length()
Files.size(path)
List files in a directory
file.listFiles()
Files.list(path)
Recursive traversal Manual recursion
Files.walk(path)
Copy a file file.renameTo() (flaky)
Files.copy(src, dest)
Get extension Parse the string
path.getFileName().toString()
Get parent
file.getParentFile()
path.getParent()
Permissions handling Barely possible
Files.getPosixFilePermissions(path)

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.

1
Task
JAVA 25 SELF, level 39, lesson 0
Locked
Digital Inventory: Overview of Folder Contents
Digital Inventory: Overview of Folder Contents
1
Task
JAVA 25 SELF, level 39, lesson 0
Locked
Developer's treasure hunt: find all Java files
Developer's treasure hunt: find all Java files
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION