You are currently viewing Working with Filesystems in Java

Working with Filesystems in Java

This entry is part 22 of 23 in the series Key Java APIs

Introduction

Modern Java applications interact with filesystems far more often than many developers realize: configuration loading, log rotation, file uploads, batch processing, backups, and more. Since Java 7, the platform provides a unified and expressive filesystem API through NIO.2, centered around Path, Files, and FileSystem.

This article explains how Java models filesystems, how paths differ from files, and how the Files utility class performs filesystem operations in a safe, portable way.
For a broader overview of Java I/O concepts, see Java IO API.

“A file is data; a path is a location.”

1. From File to Path: A Necessary Evolution

Before Java 7, filesystem access relied on java.io.File. While useful, it suffered from several limitations:

  • poor error reporting,
  • limited support for symbolic links,
  • weak portability guarantees.

The Path interface replaces File as a pure representation of a filesystem location.

Key improvements:

2. Understanding Path

A Path represents a location in a filesystem, not the file itself.
It describes where something is, not what it contains.

A Path:

  • may or may not exist,
  • may point to a file, a directory, or a symbolic link,
  • is platform-independent (the API adapts to the operating system).

2.1 Creating a Path

Absolute path
An absolute path starts from the filesystem root.

Path absolutePath = Path.of("/tmp/data.txt");
Path windowsPath = Path.of("C:/temp/data.txt");

Relative path
A relative path is resolved from the current working directory.

Path relativePath = Path.of("data/input.txt");

Path using multiple components
Using multiple components avoids hardcoded separators.

Path composedPath = Path.of("data", "logs", "app.log");//similar to Path.of("data/logs/app.log" on Linux)

Path relative to the user’s home directory
Java does not expand ~, but it can be resolved explicitly.

Path homePath = Path.of(
        System.getProperty("user.home"),
        "config",
        "app.properties"
);

2.2 What a Path Does Not Do

Creating a Path does not:

  • check whether the file exists,
  • open the file,
  • read or write any data,
  • validate permissions.
Path path = Path.of("missing.txt"); // perfectly valid

The following will only fail when actual I/O is attempted:

Files.readString(path); // may throw NoSuchFileException

3. The Files Utility Class

While Path only represents a location, all real filesystem work happens through the Files utility class.
It is the central entry point for interacting with the filesystem in modern Java.

In practice, you will almost always use Path together with Files.

Typical responsibilities:

  • creating files and directories,
  • reading and writing data,
  • copying and moving paths,
  • querying metadata.

3.1 Existence and type checks

Before performing I/O, it is common to check whether a path exists and what it represents.

Path path = Path.of("data/input.txt");

boolean exists = Files.exists(path);
boolean isFile = Files.isRegularFile(path);
boolean isDirectory = Files.isDirectory(path);

These checks are non-intrusive: they do not open the file or lock it.

3.2 Creating files and directories

Files provides safe, explicit methods for creation.

Path dir = Path.of("data/logs");
Files.createDirectories(dir); // creates parent directories if needed

Path file = dir.resolve("app.log");
Files.createFile(file);

If the file or directory already exists, a checked exception is thrown, making failures explicit.

3.3 Reading data

For text files, Files offers high-level convenience methods.

Path file = Path.of("data/input.txt");
String content = Files.readString(file);

For line-by-line processing:

Files.lines(file)
     .filter(line -> !line.isBlank())
        .forEach(System.out::println);

3.4 Writing data

Writing data is straightforward.

Path output = Path.of("data/output.txt");
Files.writeString(output, "Hello, world!");

Appending to an existing file:

Files.writeString(
    output,
    "Another line\n",
    StandardOpenOption.CREATE,
    StandardOpenOption.APPEND
);

3.5 Copying and moving files

Files also handles common filesystem operations.

Path source = Path.of("data/input.txt");
Path target = Path.of("backup/input.txt");

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(source, Path.of("data/archive/input.txt"));

3.6 Error handling with checked exceptions

Most Files methods throw checked exceptions such as IOException.

try {
    Files.readString(Path.of("missing.txt"));
} catch (IOException e) {
    // failure is explicit and must be handled
}

4. File Attributes and Metadata

Java exposes filesystem metadata through attribute views.

Common attributes:

  • size
  • last modified time
  • permissions
  • ownership
long size = Files.size(path);
FileTime lastModified = Files.getLastModifiedTime(path);

This design allows Java to adapt to different operating systems.

Symbolic links introduce ambiguity:

  • a path may not point to its real target,
  • deletion and traversal may behave differently.

Java provides:

  • Files.isSymbolicLink(path)
  • path.toRealPath()

6. FileSystem and Virtual Filesystems in Java

The FileSystem abstraction allows Java to work with:

  • default OS filesystems,
  • ZIP/JAR files,
  • in-memory or custom filesystems.
FileSystem zipFs = FileSystems.newFileSystem(zipPath);

This makes filesystem access uniform across storage types.

7. Traversing Directory Trees

Java provides two main APIs to traverse directory trees:

  • Files.walk
  • Files.walkFileTree

Both allow recursive traversal, but they serve different use cases.

Typical scenarios include:

  • recursive search,
  • cleanup jobs,
  • filesystem indexing,
  • batch processing.

Traversal is resource-sensitive: directories and file handles are opened during traversal and must be handled carefully.

7.1 Traversing with Files.walk

Files.walk returns a lazy stream of Path objects.

Path root = Path.of("data");

try (Stream<Path> paths = Files.walk(root)) {
    paths
        .filter(Files::isRegularFile)
        .forEach(System.out::println);
}

Key points:

  • traversal is recursive by default,
  • the stream must be closed (use try-with-resources),
  • suitable for simple, stream-oriented processing.

Limiting traversal depth:

Files.walk(root, 2)
     .forEach(System.out::println);

7.2 Traversing with Files.walkFileTree

Files.walkFileTree is callback-based and gives full control over traversal.

Path root = Path.of("data");

Files.walkFileTree(root, new SimpleFileVisitor<>() {

    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("File: " + file);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        System.out.println("Directory: " + dir);
        return FileVisitResult.CONTINUE;
    }
});

This approach is preferred when you need:

  • fine-grained control,
  • conditional traversal,
  • error handling per directory,
  • early termination.

7.3 Handling errors during traversal

With walkFileTree, errors can be handled explicitly.

@Override
public FileVisitResult visitFileFailed(Path file, IOException exc) {
    System.err.println("Failed to access: " + file);
    return FileVisitResult.CONTINUE;
}

7.4 Choosing between walk and walkFileTree

Use caseRecommended API
Simple filteringFiles.walk
Large treesFiles.walkFileTree
Error controlFiles.walkFileTree
Early terminationFiles.walkFileTree

Use Files.walk for simplicity, Files.walkFileTree for control.

8. Common Filesystem Pitfalls

Frequent mistakes include:

  • confusing paths with files,
  • ignoring symbolic links,
  • assuming atomic operations,
  • hardcoding path separators.

Deleting files and directories safely deserves special attention. Java provides powerful APIs that can be dangerous if misused. This is covered in detail in: Delete Paths Safely

Conclusion

Java’s filesystem API offers a robust, portable, and expressive model for interacting with storage. By separating paths, operations, and filesystems, Java enables safer and more predictable file handling across platforms.

Before deleting, moving, or manipulating paths, developers must understand this model clearly.

“Correct filesystem code begins with correct abstractions.”

You can find the complete code of this article here on GitHub.

Key Java APIs

Serialize an Object Graph: Understanding Java Serialization Deleting Paths Safely in Java

Noel Kamphoa

Experienced software engineer with expertise in Telecom, Payroll, and Banking. Now Senior Software Engineer at Societe Generale Paris.