Files, Paths & the File System
Files, Paths & the File System
Java has two generations of file system APIs. The original java.io.File class has existed since Java 1.0. The modern java.nio.file package — often called NIO.2 — arrived in Java 7 and completely supersedes it. Understanding both generations, and why the older one falls short, is the foundation for everything else in this tutorial.
The Legacy java.io.File Class
Before NIO.2, every file system operation went through java.io.File. A File object is essentially just a string wrapper around a path — it does not check whether the path actually exists when you construct it.
The API looks simple, but it has several serious design problems that trip up almost every developer who uses it long enough:
- Boolean-return error reporting. Most mutating methods (
mkdir(),delete(),renameTo()) returnfalseon failure without any reason. You have no idea whether the operation failed because of permissions, a missing parent directory, a race condition, or something else entirely. - Platform-dependent path separators. Using hard-coded
"/"or"\\"breaks portability.File.separatorhelps, but it is easy to forget. - No symbolic link awareness.
Filesilently follows symbolic links in ways that can surprise you. - Incomplete directory listing.
list()returns a plainString[]or aFile[]— fine for small directories, but it loads everything into memory at once, which is problematic for directories with millions of entries. - No metadata access. You cannot read file permissions, creation time, or owner in a portable way.
Files.move() instead.
The Modern NIO.2 API: Path and Files
NIO.2 splits the concept into two clean abstractions:
java.nio.file.Path— a pure value object representing a path string. It knows about the file system's syntax (separators, roots, relative vs absolute) but does not perform I/O itself.java.nio.file.Files— a utility class of static methods that perform the actual I/O operations, throwing checkedIOException(or its subtypes) on failure so you always know what went wrong.
You obtain a Path through the factory method Path.of() (Java 11+) or the older Paths.get():
Path.of() was added in Java 11 as a convenience method directly on the interface. In modern codebases (Java 11+) prefer Path.of() — it is more readable and does not require importing a separate class.
Path Navigation and Resolution
One of Path's biggest advantages over raw strings is its built-in path arithmetic. You can navigate the file system tree without string concatenation:
Checking Existence and File Attributes
The Files class provides predicate methods that correspond to the old File methods, plus richer attribute access:
Files.exists() first. Reserve IOException handling for truly exceptional I/O failures — disk full, permissions errors — not routine path-not-found checks.
Converting Between File and Path
Legacy code that uses java.io.File is everywhere — old libraries, third-party APIs, Android pre-NIO wrappers. You will frequently need to bridge both worlds:
The FileSystem and FileSystems
Behind both Path and Files sits a java.nio.file.FileSystem — an abstraction that lets you work with ZIP archives, in-memory file systems (for testing), and remote file systems through the same API. The default file system (your OS disk) is obtained via FileSystems.getDefault(). You will not need this directly in most application code, but it explains why Path.of() is actually a shorthand for FileSystems.getDefault().getPath().
Summary
The legacy java.io.File class is a fragile path wrapper with silent failure modes, no rich metadata, and no symbolic link control. The modern NIO.2 pair — Path (value object for paths) and Files (I/O operations with proper exceptions) — solves every one of those shortcomings. In all new code, use Path.of() and Files.*. When maintaining old code that exposes File, convert immediately with file.toPath() and work in NIO.2 from that point forward.