Java NIO Path Class
Represent file system paths in modern Java with java.nio.file.Path and the Paths factory.
Java NIO Path Class
Path is the modern replacement for java.io.File. It represents a filesystem path — an ordered sequence of name components, optionally rooted at / or C:\ — and nothing else. It doesn't read the file. It doesn't check the file exists. It doesn't lock anything. The bytes-on-disk operations are in Files (the next chapter). Path is the noun; Files is the verb.
If you've used java.io.File, the difference is twofold: Path is immutable (every operation returns a new Path), and it cleanly separates "the path string" from "what's on disk at that path." Most modern APIs — Files, FileChannel, BufferedReader.lines() overloads — take Path, not File. New code reaches for Path.
Building a Path
Path p = Path.of("logs", "2025", "app.log"); // joins components with the platform separator
Path q = Path.of("/etc/hosts"); // absolute Unix path
Path r = Paths.get("C:", "Users", "vaz"); // older factory — same behaviour
Path s = Path.of(URI.create("file:///etc/hosts")); // from a URIPath.of(...) is the modern factory; Paths.get(...) is the older one and still works. Both build a path object without touching the filesystem. Path.of("nope/nope/nope") succeeds even if no such file exists.
Path.of joins varargs with the platform's File.separator — / on Unix, \ on Windows. That makes the literal-string path you write portable: Path.of("src", "main", "java") builds the right thing on both. The moment you write Path.of("src/main/java") with hardcoded slashes, you've made it Unix-only by accident.
Inspecting a path
The component-access methods, on Path.of("/var/log/app/today.log"):
| Method | Returns | Example |
|---|---|---|
getFileName() | last component as a Path | today.log |
getParent() | everything but the last | /var/log/app |
getRoot() | the root, or null if relative | / |
getNameCount() | number of name components | 4 |
getName(int i) | i-th component | getName(0) → var |
subpath(b, e) | name components [b, e) | subpath(1, 3) → log/app |
isAbsolute() | whether the path has a root | true |
toString() | the platform-formatted string | /var/log/app/today.log |
These are pure: they look at the path object's internal name list and return slices of it. None of them touches the disk.
resolve, resolveSibling, and the absolute-argument trap
resolve(other) is "join this and other":
Path base = Path.of("/var/log");
base.resolve("app.log"); // /var/log/app.log
base.resolve("app/today.log"); // /var/log/app/today.logThe trap: if other is absolute, resolve returns other unchanged:
base.resolve("/etc/hosts"); // /etc/hosts -- base is discarded
base.resolve(Path.of("/etc/hosts")); // same: /etc/hostsThis is the documented behaviour, and it bites every Java programmer once. If you accept a filename from user input and resolve it against a configured base directory, an attacker who supplies "/etc/passwd" lands their absolute path — escaping the base. Always validate or normalise external input before resolve-ing it.
resolveSibling(other) replaces the last component:
Path p = Path.of("/var/log/today.log");
p.resolveSibling("yesterday.log"); // /var/log/yesterday.logIt's getParent().resolve(other) with a null check baked in. Useful for "write the output next to the input."
relativize: the inverse
Given a base and a target, base.relativize(target) returns the relative path from base to target:
Path base = Path.of("/var/log");
Path target = Path.of("/var/log/app/today.log");
base.relativize(target); // app/today.log
target.relativize(base); // ../..The contract: base.resolve(base.relativize(target)) is equivalent to target (modulo normalize). It's how you turn an absolute target into a short, relative reference inside a base directory — useful for log lines, archive entries, and URLs.
Both paths must be the same type (both absolute or both relative) and both must come from the same FileSystem. Mixing throws IllegalArgumentException.
normalize: collapse . and ..
Path objects allow . and .. components — Path.of("/var/log/../tmp") is a valid Path. normalize() removes them syntactically:
Path.of("/var/log/../tmp").normalize(); // /var/tmp
Path.of("./a/./b/../c").normalize(); // a/c"Syntactically" matters: normalize does string-level work. It doesn't ask the filesystem whether .. actually points where the strings suggest. If /var/log is a symbolic link to /tmp/logs, then on disk /var/log/.. is /tmp, not /var. normalize() doesn't know that — it just deletes the ...
When you need the real path on disk (symlinks resolved, .. interpreted correctly), use toRealPath(), which is a filesystem-touching call:
Path real = Path.of("/var/log/../tmp").toRealPath(); // resolves symlinks, throws if the file doesn't existFor path-equality checks and string comparisons, normalize() is what you want. For "the canonical name of the file on disk right now," it's toRealPath().
Equality is string-based
path1.equals(path2) compares the paths as strings (component by component). It does not normalise, does not resolve symlinks, does not check the filesystem:
Path.of("/var/log").equals(Path.of("/var/log/.")); // false
Path.of("/var/log").equals(Path.of("/var/log").normalize()); // false (the second has the same string, but...)
Path.of("/var/log").equals(Path.of("/var/log")); // trueTo compare two paths as "do they point to the same file," normalise both and compare, or call Files.isSameFile(p1, p2) (filesystem-touching, the only fully-correct check). For sorting and HashSet keys, the string equality is what Path gives you; it's fine for most uses but isn't "same file on disk."
File interop
Path and File convert in both directions:
File f = Path.of("/etc/hosts").toFile();
Path p = new File("/etc/hosts").toPath();You'll need this when an old API takes File and a new API takes Path (or vice versa). Don't store paths as File; convert at the API boundary and keep Path in your code.
A worked example: join, resolve, relativize, normalize
The program below walks through every Path operation introduced in this chapter against concrete paths. The output makes the difference between resolve and resolveSibling, between normalize and toRealPath, and between absolute-argument and relative-argument resolve visible.
What to take from the run:
Path.of(\"/var\", \"log\", \"app\", \"today.log\")produced/var/log/app/today.logon Unix and\\var\\log\\app\\today.logon Windows. Letting the varargs factory join the components is the portable way; hardcoded/or\in the input string is the unportable way. Use the factory.- The
resolve(\"/etc/hosts\")line discardedbaseand returned/etc/hosts. That's the absolute-argument behaviour and the most-frequent source of "but I gave it a base directory, why is it writing to/etc/hosts?" Always validate user-supplied filenames beforeresolve. The defensive form isbase.resolve(other).normalize().startsWith(base)— and even that has subtle holes when symlinks are involved. base.relativize(target)returnedapp/today.log. Concatenating that back withbase.resolve(...)produced the original target — the round-trip identity. Use this when you're writing a log message or an archive entry that needs a short, relative form of a long absolute path.Path.of(\"/var/log/../tmp/./a/b/../c\").normalize()produced/var/tmp/a/c. The transformation was purely string-level: every.removed, everyname/..pair removed. The filesystem wasn't consulted.toRealPathon the next line did consult the filesystem — that's why the result was the canonical, symlink-resolved name of the actual file on disk.- The two
equalschecks:Path.of(\"/var/log\")andPath.of(\"/var\", \"log\")were equal (same internal name sequence, same string);Path.of(\"/var/log\")andPath.of(\"/var/log/.\")were not (one has a trailing.component, one doesn't). The take:equalsis a string comparison. For "do these two paths point to the same file?" useFiles.isSameFile(a, b)from the next chapter — it's the only check that asks the filesystem.
What's next
Path is the noun. The next chapter, Java Files Class, covers the verb — Files, the giant utility class of one-line operations on the file system: readString, writeString, createDirectories, copy, move, delete, walk, and the rest.
Practice
`base` is `Path.of('/srv/uploads')` and `userInput` is the string `'/etc/passwd'` (an attacker-controlled value). Your code does `base.resolve(userInput)` to compute the target path. What's the resulting path, and what's the security lesson?