W3docs

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 URI

Path.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"):

MethodReturnsExample
getFileName()last component as a Pathtoday.log
getParent()everything but the last/var/log/app
getRoot()the root, or null if relative/
getNameCount()number of name components4
getName(int i)i-th componentgetName(0)var
subpath(b, e)name components [b, e)subpath(1, 3)log/app
isAbsolute()whether the path has a roottrue
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.log

The 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/hosts

This 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.log

It'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 exist

For 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"));             // true

To 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.

java— editable, runs on the server

What to take from the run:

  • Path.of(\"/var\", \"log\", \"app\", \"today.log\") produced /var/log/app/today.log on Unix and \\var\\log\\app\\today.log on 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 discarded base and 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 before resolve. The defensive form is base.resolve(other).normalize().startsWith(base) — and even that has subtle holes when symlinks are involved.
  • base.relativize(target) returned app/today.log. Concatenating that back with base.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, every name/.. pair removed. The filesystem wasn't consulted. toRealPath on 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 equals checks: Path.of(\"/var/log\") and Path.of(\"/var\", \"log\") were equal (same internal name sequence, same string); Path.of(\"/var/log\") and Path.of(\"/var/log/.\") were not (one has a trailing . component, one doesn't). The take: equals is a string comparison. For "do these two paths point to the same file?" use Files.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

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?