Path.of() and resolve()

Build file paths from parts, join them with resolve, normalise them, and keep user-supplied names inside the directory you meant.

Signature
static Path of(String first, String... more)
  • Added in Java Java 11

Parameters

ParameterTypeDescription
$firstStringThe path string or its first part, such as "/srv/app" or "reports".
$moreoptionalString...Further parts joined with the separator. Empty strings are ignored.

Return value

PathA Path on the default file system, made by joining first and more with the platform separator. No file is opened or checked.

Errors

InvalidPathException if the string cannot be converted to a path, for example one containing a NUL character.

Changelog

  • PHP Java 22Path gained resolve(String first, String... more) and resolve(Path first, Path... more) for joining several parts in one call.

Path.of("/srv", "app", "data") builds a Path, an object that names a file. It does not touch the disk: the file does not have to exist, and nothing is checked until you pass the path to a Files method. Path.of was added in Java 11 and does the same as the older Paths.get, which is still fine to read in existing code.

resolve joins paths

base.resolve("uploads") appends a part to a path with the right separator, so code works on Linux and Windows without string concatenation. Since Java 22, resolve also takes several parts: base.resolve("2026", "10", "a.png"). Two rules matter:

  • If the argument is absolute, it is returned as it is and the base is discarded. Path.of("/srv/uploads").resolve("/etc/passwd") is /etc/passwd.
  • .. is kept as a name, not applied. normalize() removes . and folds name/.., purely by looking at the text.

Path traversal

Both rules matter as soon as part of a path comes from a request. A download endpoint that does root.resolve(filename) with filename set to ../../etc/passwd or /etc/passwd serves files from anywhere on the server. The fix is to resolve, normalise, and check the result is still under the root:

Path root = Path.of("/srv/uploads").toAbsolutePath().normalize();
Path target = root.resolve(userInput).normalize();
if (!target.startsWith(root)) {
    throw new IllegalArgumentException("invalid file name");
}

Path.startsWith compares whole name elements, so /srv/uploads-old/x does not start with /srv/uploads; a String prefix check gets this wrong. normalize does not follow symbolic links. If users can create links inside the root, compare toRealPath() results instead, which resolve links but need the file to exist.

Other useful methods

getFileName(), getParent(), resolveSibling(name) for renames, relativize(other) for display and for rebuilding trees elsewhere, and toAbsolutePath(). A relative path resolves against the working directory of the JVM process, which differs between your IDE, a container and a service manager. Build every path from a configured absolute base instead.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Serve user-requested downloads from one storage directory while rejecting ../ and absolute names.
  • Build the storage path for an upload from a configured base directory, a date and a generated id.
  • Derive the path of a thumbnail or converted file next to the original with resolveSibling.

Frequently asked questions

What is the difference between Path.of and Paths.get?

None in behaviour. Path.of, added in Java 11, is a static factory on the Path interface; Paths.get is the older Java 7 helper that now simply calls Path.of.

How do I join two paths in Java?

Use base.resolve("child") or base.resolve(otherPath). Since Java 22 you can pass several parts: base.resolve("a", "b", "c"). Remember that an absolute argument replaces the base.

How do I prevent path traversal in Java?

Resolve the user input against an absolute, normalised root, call normalize() on the result and reject it unless result.startsWith(root). Use toRealPath() if symbolic links inside the root are possible.

More examples

Keeping user input inside a directory

Normalising before the check folds the .. segments, and an absolute input replaces the root entirely, so both escape attempts fail startsWith. The last input becomes /srv/uploads-old/x.png, which a String prefix check would accept but Path.startsWith rejects.

Example · java
Path safeResolve(Path root, String name) {
    Path target = root.resolve(name).normalize();
    if (!target.startsWith(root)) {
        throw new IllegalArgumentException("rejected: " + name);
    }
    return target;
}

void main() {
    Path root = Path.of("/srv/uploads").toAbsolutePath().normalize();
    List<String> inputs = List.of(
        "avatar.png", "2026/report.pdf", "../../etc/passwd", "/etc/passwd", "../uploads-old/x.png");
    for (String input : inputs) {
        try {
            IO.println("ok: " + safeResolve(root, input));
        } catch (IllegalArgumentException e) {
            IO.println(e.getMessage());
        }
    }
}

// Output:
// ok: /srv/uploads/avatar.png
// ok: /srv/uploads/2026/report.pdf
// rejected: ../../etc/passwd
// rejected: /etc/passwd
// rejected: ../uploads-old/x.png

Siblings, relative paths and extensions

resolveSibling replaces the last element, which is the usual way to name a derived file next to the original. Path has no extension method, so the stem comes from the file name as a String.

Example · java
void main() {
    Path upload = Path.of("/srv/app/uploads/2026/photo.jpeg");

    Path thumb = upload.resolveSibling("photo-thumb.jpeg");
    IO.println(thumb);

    Path root = Path.of("/srv/app");
    IO.println(root.relativize(upload));

    String name = upload.getFileName().toString();
    String stem = name.substring(0, name.lastIndexOf('.'));
    IO.println(upload.resolveSibling(stem + ".webp").getFileName());

    IO.println(upload.getNameCount() + " names, root " + upload.getRoot());
}

// Output:
// /srv/app/uploads/2026/photo-thumb.jpeg
// uploads/2026/photo.jpeg
// photo.webp
// 5 names, root /

Path.of, Paths.get and file URIs

Path.of and Paths.get produce equal paths; Path.of is the newer name. Path.of(URI) converts a file: URI back to a path, and empty parts are skipped when joining.

Example · java
void main() {
    Path a = Path.of("/srv/data", "export.csv");
    Path b = Paths.get("/srv/data", "export.csv");
    IO.println("same path: " + a.equals(b));

    URI uri = a.toUri();
    IO.println(uri);
    IO.println(Path.of(uri));

    Path relative = Path.of("data", "", "export.csv");
    IO.println(relative + " absolute: " + relative.isAbsolute());
}

// Output:
// same path: true
// file:///srv/data/export.csv
// /srv/data/export.csv
// data/export.csv absolute: false

Discussion

  • Be the first to comment on this lesson.