☕ Java

Path

Path is the central interface in java.nio.file, introduced in Java 7 as part of NIO.2, representing a location in a file system as a sequence of path elements separated by a delimiter. Unlike java.io.File, Path is an interface backed by a pluggable FileSystem provider — the default provider maps to the OS file system, but ZIP archives, in-memory file systems, and remote file systems can provide their own Path implementations. Path is immutable and does not represent an open file or an existing file system entry; it is purely a path string with structured access and manipulation methods. Path provides substantially better semantics than File: path component access, resolution, relativization, normalization, and iteration over path elements are all first-class operations. Combined with the Files utility class, Path enables all file system operations with proper IOException-throwing semantics. This entry covers all Path construction methods, every path manipulation method in depth, path resolution and relativization with edge cases, normalization and toRealPath(), path iteration and watching, conversion to URI and File, comparison semantics, and cross-platform path handling.

Path Construction, Components, and Basic Navigation

Path objects are created through three entry points. Path.of(String first, String... more) (Java 11+) is the modern factory method on the interface itself. Paths.get(String first, String... more) (Java 7+) is the older factory method in the Paths utility class — both call FileSystems.getDefault().getPath() internally and are equivalent. Path.of(URI) creates a Path from a file:// URI. The varargs form joins segments with the platform file separator: Path.of("/home", "user", "file.txt") produces /home/user/file.txt on Unix and /home/user ile.txt on Windows (then resolved to the appropriate form for the default filesystem). Path components are accessed through a set of methods that decompose the path string. getFileName() returns the last element of the path — the filename or last directory name — as a Path (not a String). getParent() returns the path without the last element, or null if the path has no parent (a root or a bare filename). getRoot() returns the root component, or null for relative paths — on Unix this is "/" for absolute paths; on Windows it is "C:" or similar. getNameCount() returns the number of name elements (non-root components). getName(int index) returns the name element at the given index. subpath(int beginIndex, int endIndex) returns a relative Path consisting of name elements from beginIndex (inclusive) to endIndex (exclusive). The structural nature of Path means that no file system access is required for any of these methods — they operate on the path string alone. This is an important difference from File.getCanonicalPath() (which accesses the file system to resolve symlinks) vs Path.normalize() (which operates purely on the path string). toRealPath() is the Path equivalent of getCanonicalPath() — it resolves the path to its canonical form by following symlinks and resolving ".." and "." components with actual file system access. Path.isAbsolute() tests whether the path has a root component. An absolute path starts at the file system root; a relative path is interpreted relative to a directory (typically the current working directory). toAbsolutePath() converts a relative path to absolute by prepending the current working directory. On Windows, the current working directory includes the drive, so a path starting with (drive-relative, without a drive letter) is made absolute by prepending the current drive.
Java
// ── Path construction ─────────────────────────────────────────────────
// Modern (Java 11+):
Path p1 = Path.of("/home/user/documents/report.txt");
Path p2 = Path.of("/home", "user", "documents", "report.txt");  // joined with /
Path p3 = Path.of("relative/path/file.txt");   // relative to CWD

// Older (Java 7+):
Path p4 = Paths.get("/home/user/documents/report.txt");

// From URI:
Path p5 = Path.of(URI.create("file:///home/user/report.txt"));

// From File (interop):
Path p6 = new File("/home/user/report.txt").toPath();

// ── Component access ──────────────────────────────────────────────────
Path p = Path.of("/home/user/documents/report.txt");

System.out.println(p.getFileName());     // report.txt   (Path, not String)
System.out.println(p.getParent());       // /home/user/documents
System.out.println(p.getRoot());         // /             (null for relative paths)
System.out.println(p.getNameCount());    // 4
System.out.println(p.getName(0));        // home
System.out.println(p.getName(1));        // user
System.out.println(p.getName(2));        // documents
System.out.println(p.getName(3));        // report.txt

// subpath — relative Path from index range:
System.out.println(p.subpath(1, 3));    // user/documents  (no leading /)

// isAbsolute:
System.out.println(Path.of("/absolute").isAbsolute());   // true
System.out.println(Path.of("relative").isAbsolute());    // false

// toAbsolutePath: prepends CWD for relative paths
Path rel = Path.of("data/file.txt");
System.out.println(rel.toAbsolutePath());  // /current/working/dir/data/file.txt

// ── Bare filename has null parent and root ────────────────────────────
Path bare = Path.of("file.txt");
System.out.println(bare.getParent());    // null — no parent component
System.out.println(bare.getRoot());      // null — no root (relative)
System.out.println(bare.getFileName());  // file.txt
System.out.println(bare.getNameCount()); // 1

// ── Root-only path ────────────────────────────────────────────────────
Path root = Path.of("/");
System.out.println(root.getParent());    // null — root has no parent
System.out.println(root.getRoot());      // /
System.out.println(root.getFileName());  // null — no filename component
System.out.println(root.getNameCount()); // 0

// ── Path iteration ────────────────────────────────────────────────────
Path longPath = Path.of("/home/user/documents/projects/java/report.txt");
for (Path element : longPath) {
    System.out.println(element);   // each name component as a relative Path
}
// home, user, documents, projects, java, report.txt

// getFileName() as String:
String filename = longPath.getFileName().toString();   // "report.txt"
// Extract extension:
int dot = filename.lastIndexOf('.');
String ext = dot > 0 ? filename.substring(dot + 1) : "";  // "txt"

resolve(), relativize(), normalize(), and toRealPath()

resolve(Path other) and resolve(String other) combine a base path with another path, analogous to URL resolution. If other is an absolute path, resolve returns other unchanged — the absolute path replaces the base. If other is a relative path, resolve appends it to the base. This makes resolve composable for path building: base.resolve("subdir").resolve("file.txt") builds paths by appending components, replacing the base if any component is absolute. resolveSibling(Path other) is a shortcut for getParent().resolve(other): it replaces the last component of the current path with other, which is useful for renaming files or changing extensions. relativize(Path other) computes the relative path from the current path to other. The result is a relative path such that resolve(result).equals(other) — that is, appending the result to the current path gives other. Both paths must be of the same type (both absolute or both relative) for relativize to work; mixing absolute and relative throws IllegalArgumentException. If the paths share no common ancestor in their string representation (after normalization), relativize introduces ".." components to traverse up the directory tree. normalize() removes redundant path elements: ".", "..", and redundant separators. /home/user/../user/./documents normalizes to /home/user/documents. normalize() does NOT access the file system — it operates on the path string only. The result may not exist, and it does not resolve symbolic links. This is the critical difference from toRealPath(): normalize() is string manipulation; toRealPath() is file system I/O. toRealPath(LinkOption... options) accesses the file system to fully resolve a path to its canonical form. It resolves all symbolic links, resolves ".." and "." components against the actual file system, and makes the path absolute. toRealPath() throws IOException if the path does not exist (because it must verify each component on the actual file system). Passing LinkOption.NOFOLLOW_LINKS makes it resolve ".." and "." components without following symbolic links. toRealPath() is the correct method when you need to compare two paths that may refer to the same file through different symlinks or "." and ".." components. startsWith(Path) and endsWith(Path) test prefix and suffix relationships in terms of path components, not string prefixes. Path.of("/home/user").startsWith(Path.of("/home")) is true because /home is a proper path prefix. Path.of("/home/user").startsWith(Path.of("/ho")) is false because /ho is not a complete path component match. Similarly, endsWith() matches from the last component, not from the last character.
Java
// ── resolve() — appending paths ──────────────────────────────────────
Path base = Path.of("/home/user");

// Relative resolution — append:
System.out.println(base.resolve("documents/report.txt"));
// /home/user/documents/report.txt

// Absolute resolution — other replaces base entirely:
System.out.println(base.resolve("/tmp/file.txt"));
// /tmp/file.txt   (absolute path ignores base)

// Empty resolution — returns base unchanged:
System.out.println(base.resolve(""));
// /home/user

// Building paths step by step:
Path config = Path.of("/etc")
    .resolve("myapp")
    .resolve("config")
    .resolve("settings.json");
System.out.println(config);  // /etc/myapp/config/settings.json

// ── resolveSibling() — replace last component ─────────────────────────
Path original = Path.of("/home/user/document.txt");
System.out.println(original.resolveSibling("backup.txt"));   // /home/user/backup.txt
System.out.println(original.resolveSibling("archive"));      // /home/user/archive

// Practical: change file extension
Path changeExt(Path p, String newExt) {
    String name = p.getFileName().toString();
    String baseName = name.contains(".")
        ? name.substring(0, name.lastIndexOf('.'))
        : name;
    return p.resolveSibling(baseName + "." + newExt);
}
System.out.println(changeExt(Path.of("/docs/report.txt"), "pdf"));
// /docs/report.pdf

// ── relativize() — computing relative paths ───────────────────────────
Path from = Path.of("/home/user");
Path to   = Path.of("/home/user/documents/report.txt");
System.out.println(from.relativize(to));   // documents/report.txt

// Moving up the tree:
Path from2 = Path.of("/home/user/project");
Path to2   = Path.of("/home/user/documents/report.txt");
System.out.println(from2.relativize(to2));  // ../documents/report.txt

// Completely different trees:
Path from3 = Path.of("/a/b/c");
Path to3   = Path.of("/x/y/z");
System.out.println(from3.relativize(to3));  // ../../../x/y/z

// Round-trip property: resolve(relativize(target)) == target
assert from.resolve(from.relativize(to)).normalize().equals(to.normalize());

// ── normalize() — string-level, no file system access ─────────────────
System.out.println(Path.of("/home/user/../user/./docs").normalize());
// /home/user/docs

System.out.println(Path.of("a/b/../../c").normalize());
// c

System.out.println(Path.of("./relative/../path").normalize());
// path

// normalize() doesn't resolve symlinks:
// /symlink-to-usr/../etc  after normalize: /etc
// But if /symlink-to-usr -> /usr, the canonical path would be /etc (correct)
// or /usr/../etc -> /etc (same result here but not always)

// ── toRealPath() — file system access, resolves symlinks ──────────────
try {
    Path real = Path.of("/home/user/./docs/../documents/../docs")
        .toRealPath();   // accesses file system; throws IOException if not found
    System.out.println("Real path: " + real);

    // NOFOLLOW_LINKS: resolve ".." and "." but don't follow symlinks
    Path noFollow = Path.of("/home/user/symlink/../other")
        .toRealPath(LinkOption.NOFOLLOW_LINKS);
} catch (IOException e) {
    System.err.println("Path doesn't exist: " + e.getMessage());
}

// ── startsWith() and endsWith() — component-based ─────────────────────
Path p = Path.of("/home/user/documents/report.txt");

System.out.println(p.startsWith("/home/user"));       // true
System.out.println(p.startsWith("/home/use"));        // false — not a component boundary
System.out.println(p.startsWith(Path.of("/home")));   // true

System.out.println(p.endsWith("report.txt"));                    // true
System.out.println(p.endsWith("documents/report.txt"));          // true
System.out.println(p.endsWith("ort.txt"));                       // false

Conversion, Comparison, Watching, and Cross-Platform Handling

Path provides conversion methods to and from other representations. toUri() converts to a file:// URI, which is always absolute and uses forward slashes regardless of platform. toFile() converts to a java.io.File object for interoperability with APIs that require File. toAbsolutePath() resolves against the current working directory. toRealPath() resolves against the actual file system. toString() returns the path string in the platform-specific format. Path comparison semantics follow the underlying file system. On case-sensitive file systems (most Unix file systems), path comparison is case-sensitive. On case-insensitive file systems (macOS HFS+ with default settings, NTFS), comparison is case-insensitive — but Path.equals() may still be case-sensitive because it compares path strings, not file system identities. Two paths that look different as strings but refer to the same file on a case-insensitive file system are not equal via equals() but will be detected as the same file by Files.isSameFile(). compareTo() provides a natural ordering that is platform-consistent. For genuine file identity comparison (independent of path string, following symlinks), always use Files.isSameFile(path1, path2), which compares the underlying file system inode. A Path from one FileSystem cannot be compared with or resolved against a Path from a different FileSystem. The FileSystem backing a Path is accessible via getFileSystem(). Path.of() always uses the default FileSystem. To work with ZIP archives or other custom file systems, use FileSystems.newFileSystem(zipPath) and then call newFileSystem.getPath() on it. The resulting Paths work with Files.copy(), Files.walk(), and other Files methods, enabling treating a ZIP file exactly like a directory. Cross-platform path handling requires care with separators and root components. The safest approach is always using Path.of() with multiple segments rather than hardcoding a path string with / or separators. Path.of("/home", "user", "file.txt") is correct on all platforms; Path.of("/home/user/file.txt") assumes Unix-style separators. For Windows-specific paths, Path.of("C:", "Users", "user", "file.txt") is the portable form. When paths come from configuration files or user input that may contain either / or , normalizing via Path.of(rawString) handles it correctly because the default FileSystem's path parsing accepts both on Windows.
Java
// ── Conversion methods ────────────────────────────────────────────────
Path p = Path.of("/home/user/documents/report.txt");

// To URI: always absolute, always forward slashes
URI uri = p.toUri();
System.out.println(uri);   // file:///home/user/documents/report.txt

// Back to Path from URI:
Path fromUri = Path.of(uri);
System.out.println(fromUri.equals(p));  // true

// To File (for legacy APIs):
File file = p.toFile();

// From File (to use NIO.2):
Path fromFile = file.toPath();
System.out.println(p.equals(fromFile));  // true

// toString:
System.out.println(p.toString());  // /home/user/documents/report.txt (Unix)
                                   // home/userdocuments
eport.txt (Windows)

// ── Path comparison ────────────────────────────────────────────────────
Path a = Path.of("/home/user/file.txt");
Path b = Path.of("/home/user/file.txt");
Path c = Path.of("/HOME/USER/FILE.TXT");

System.out.println(a.equals(b));   // true — same path string
System.out.println(a.equals(c));   // false on Unix (case-sensitive string comparison)
                                   // may be false even on case-insensitive Windows FS

// Files.isSameFile — actual file system identity:
try {
    boolean same = Files.isSameFile(a, c);  // true on case-insensitive FS
    // Follows symlinks, checks inodes — the only correct identity comparison
} catch (IOException e) { /* path doesn't exist */ }

// Sorting paths:
List<Path> paths = Arrays.asList(
    Path.of("/home/user/z.txt"),
    Path.of("/home/user/a.txt"),
    Path.of("/home/user/m.txt")
);
Collections.sort(paths);   // compareTo() — platform-consistent ordering
// Result: /home/user/a.txt, /home/user/m.txt, /home/user/z.txt

// ── ZIP file system — treating ZIP as a directory ─────────────────────
Path zipFile = Path.of("/home/user/archive.zip");
try (FileSystem zipFs = FileSystems.newFileSystem(zipFile)) {
    Path zipRoot = zipFs.getPath("/");   // root of the ZIP
    // List ZIP contents:
    try (Stream<Path> walk = Files.walk(zipRoot)) {
        walk.forEach(System.out::println);   // /dir/, /dir/file.txt, etc.
    }

    // Read a file from ZIP:
    Path entry = zipFs.getPath("/config/settings.json");
    String content = Files.readString(entry, StandardCharsets.UTF_8);

    // Copy file from ZIP to real FS:
    Files.copy(entry, Path.of("/tmp/settings.json"),
        StandardCopyOption.REPLACE_EXISTING);

    // Add a file to ZIP:
    Path newEntry = zipFs.getPath("/newfile.txt");
    Files.writeString(newEntry, "Hello from ZIP!", StandardCharsets.UTF_8);
}

// ── Cross-platform path construction ─────────────────────────────────
// BEST: use multi-segment form, let FileSystem handle separators:
Path crossPlatform = Path.of("data", "config", "settings.json");
// On Unix: data/config/settings.json
// On Windows: dataconfigsettings.json

// From user input — handles both / and  on Windows:
String userInput = "data/config\settings.json";   // mixed separators
Path normalized = Path.of(userInput);   // FileSystem normalizes

// Joining user-provided path with base:
Path base2 = Path.of("/var/myapp");
String userRelative = "uploads/image.png";
Path safe = base2.resolve(userRelative).normalize();
// Security check: ensure result is under base
if (!safe.startsWith(base2)) {
    throw new SecurityException("Path traversal attempt: " + userRelative);
}

// ── Path as TreeMap key — ordering and equality ───────────────────────
TreeMap<Path, String> pathMap = new TreeMap<>();
pathMap.put(Path.of("/home/user/b.txt"), "B");
pathMap.put(Path.of("/home/user/a.txt"), "A");
pathMap.put(Path.of("/home/user/c.txt"), "C");
pathMap.forEach((k, v) -> System.out.println(k + " = " + v));
// /home/user/a.txt = A
// /home/user/b.txt = B
// /home/user/c.txt = C  (sorted by compareTo)

Related Topics in Java NIO

NIO Overview
Java NIO (New I/O), introduced in Java 1.4 as the java.nio package, is a collection of APIs that complement and in many cases supersede the original java.io stream model. NIO introduces three foundational abstractions absent from java.io: buffers (typed, fixed-capacity containers for data that support direct memory allocation outside the Java heap), channels (bidirectional, closeable connections to I/O entities that read and write buffers rather than individual bytes), and selectors (multiplexers that let a single thread monitor multiple channels for readiness, enabling non-blocking I/O at scale). Java 7 extended NIO with the java.nio.file package, known as NIO.2 or JSR-203, which provides the Path interface, the Files utility class, the FileSystem abstraction, WatchService for file system event monitoring, and the AsynchronousFileChannel API. Together, NIO and NIO.2 enable four I/O models unavailable in java.io: non-blocking I/O (channel reads and writes return immediately if data is unavailable), asynchronous I/O (I/O operations are submitted and a callback or Future signals completion), memory-mapped I/O (a file is mapped directly into the JVM's virtual address space for zero-copy access), and scatter/gather I/O (reading into or writing from multiple buffers in a single system call). This entry covers the conceptual architecture of NIO, the relationship between buffers, channels, and selectors, the four I/O models, the NIO.2 additions and how they relate to the core NIO abstractions, the difference between blocking and non-blocking channels, and which NIO API to use for which problem.
Files Class
java.nio.file.Files is a utility class introduced in Java 7 containing over 70 static methods that perform all file system operations on Path objects: reading and writing file content, querying and setting file metadata, creating and deleting files and directories, copying and moving, directory listing and recursive traversal, checking permissions and existence, managing symbolic links, and obtaining channels and streams. Files is the NIO.2 replacement for the operations previously handled by java.io.File's mutation methods (which return boolean on failure) and for the InputStream/OutputStream factory pattern used with java.io streams. Every Files method throws IOException (or a subclass like NoSuchFileException, FileAlreadyExistsException, DirectoryNotEmptyException, or AccessDeniedException) rather than returning boolean, making error handling explicit and unambiguous. This entry covers every major category of Files method in depth, the complete set of StandardOpenOption and StandardCopyOption values, atomic operations and their filesystem-level semantics, bulk read/write convenience methods, the directory traversal API including Files.walk, Files.find, and Files.walkFileTree, permission and attribute manipulation, and the interplay between Files methods and the underlying OS system calls.
Channels
A channel in Java NIO is an open connection to an I/O entity — a file, a socket, a pipe — that can be read from, written to, or both. Channels differ from streams in being bidirectional (a single FileChannel can both read and write), operating exclusively with ByteBuffers rather than individual bytes, supporting seeking and position-based access (FileChannel), and being capable of non-blocking mode (network channels). The channel hierarchy descends from java.nio.channels.Channel at the root; ReadableByteChannel and WritableByteChannel define the reading and writing contracts; ByteChannel combines both; SeekableByteChannel adds position and size access; FileChannel extends SeekableByteChannel with file-specific operations including memory mapping, file locking, and zero-copy transfers. For network I/O, SocketChannel, ServerSocketChannel, and DatagramChannel support both blocking and non-blocking modes and are registered with a Selector for multiplexed event-driven I/O. This entry covers the complete FileChannel API including map(), lock(), transferTo(), and transferFrom(); the blocking and non-blocking network channels; the Selector and SelectionKey model; Pipe channels for inter-thread communication; and the AsynchronousChannel group for callback-based or Future-based I/O.
Buffers
A Buffer in Java NIO is a fixed-capacity, typed container for data that serves as the intermediary between application code and channels. Every I/O operation in NIO reads data into a buffer or writes data from a buffer — there is no byte-at-a-time API. The Buffer class hierarchy provides typed buffers for every Java primitive: ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, and DoubleBuffer. ByteBuffer is the foundation type because channels always operate in bytes; the other types are views over a ByteBuffer or are used for in-memory data processing. Every buffer has three state variables — position, limit, and capacity — that control where reads and writes occur and how much data is available. The flip(), clear(), compact(), rewind(), and mark()/reset() operations manipulate these state variables to implement the write-then-read and read-then-write patterns that buffer-based I/O requires. ByteBuffer can be backed by a Java heap array (allocate()) or by off-heap native memory (allocateDirect()), with significant performance implications for I/O operations. This entry covers all buffer state variables and their transitions, every buffer method in depth, heap vs direct buffer performance model, view buffers and byte order, the slice() and duplicate() operations, and the common patterns for buffer-based channel I/O.