Java NIO.2 Path and Files API: Modern File I/O

The NIO.2 API (java.nio.file), introduced in Java 7, replaces the aging java.io.File class with two cleaner, more capable abstractions: Path (an immutable representation of a file system path) and Files (a utility class with static methods for all common file operations). Java 11 added Files.readString() and Files.writeString() for one-liner file I/O, making NIO.2 even more ergonomic.

Contents

Path: Creating, Resolving, Relativizing

Path.of() (Java 11+) and its predecessor Paths.get() create an immutable Path from a string or a sequence of components. resolve() appends a relative path to an existing path, acting like a cd operation; if the argument is absolute it replaces the base entirely. relativize() computes the relative path between two absolute paths. normalize() collapses redundant . and .. components without touching the filesystem. toAbsolutePath() makes a relative path absolute using the current working directory. None of these methods require the path to exist on disk.

Java
import java.nio.file.*;

// Creating paths
Path p1 = Path.of("/home/user/docs/report.txt");       // absolute
Path p2 = Path.of("src", "main", "java", "App.java");  // relative, OS separator
Path p3 = Path.of(".");                                  // current directory

// Path components
System.out.println(p1.getFileName());    // report.txt
System.out.println(p1.getParent());      // /home/user/docs
System.out.println(p1.getRoot());        // / (null on Windows relative paths)
System.out.println(p1.getNameCount());   // 4
System.out.println(p1.getName(2));       // docs

// resolve — append a path (like cd)
Path dir  = Path.of("/home/user");
Path file = dir.resolve("docs/report.txt"); // /home/user/docs/report.txt
Path abs  = dir.resolve(Path.of("/etc/hosts")); // /etc/hosts (absolute overrides)

// resolveSibling — resolve relative to parent
Path sibling = p1.resolveSibling("summary.txt"); // /home/user/docs/summary.txt

// relativize — compute relative path from one to another
Path from = Path.of("/home/user");
Path to   = Path.of("/home/user/docs/report.txt");
Path rel  = from.relativize(to); // docs/report.txt

// normalize — remove . and .. components
Path messy = Path.of("/home/user/../user/./docs/report.txt");
System.out.println(messy.normalize()); // /home/user/docs/report.txt

// toAbsolutePath — make relative path absolute (uses current directory)
Path abs2 = Path.of("config.properties").toAbsolutePath();

// toRealPath — resolve symlinks, check existence
// Path real = Path.of("link").toRealPath(); // throws NoSuchFileException if missing

// startsWith / endsWith
System.out.println(p1.startsWith("/home/user")); // true
System.out.println(p1.endsWith("report.txt"));   // true

Reading and Writing Files

Files.readString() (Java 11+) and Files.readAllBytes() load an entire file into memory in one call: appropriate for small configuration files and templates. Files.readAllLines() reads lines into a List<String>; for large files use Files.lines() instead, which returns a lazy stream that does not load all lines at once. On the write side, Files.writeString() and Files.write() are the simplest options; pass StandardOpenOption.APPEND to append rather than overwrite. All Files methods throw checked IOException on failure.

Java
import java.nio.file.*;
import java.nio.charset.StandardCharsets;

Path file = Path.of("data.txt");

// Read entire file as String (Java 11+) — simplest option
String content = Files.readString(file);
String contentUtf8 = Files.readString(file, StandardCharsets.UTF_8);

// Read all lines as List<String>
List<String> lines = Files.readAllLines(file);

// Read as bytes
byte[] bytes = Files.readAllBytes(file);

// Stream lines lazily (good for large files — doesn't load all at once)
try (Stream<String> stream = Files.lines(file)) {
    long count = stream.filter(l -> l.contains("ERROR")).count();
}

// Write entire file (Java 11+)
Files.writeString(file, "Hello, World!\n");
Files.writeString(file, "Append me\n", StandardOpenOption.APPEND);

// Write all lines
Files.write(file, List.of("line1", "line2", "line3"));
Files.write(file, List.of("more"), StandardOpenOption.APPEND);

// Buffered reader/writer for streaming I/O
try (var reader = Files.newBufferedReader(file)) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
}

try (var writer = Files.newBufferedWriter(file, StandardOpenOption.CREATE)) {
    writer.write("line 1");
    writer.newLine();
    writer.write("line 2");
}

// Temporary files
Path tempFile = Files.createTempFile("prefix-", ".tmp");
Path tempDir  = Files.createTempDirectory("myapp-");

Copy, Move, and Delete

Files.copy() copies a file or stream to a destination; it fails by default if the destination exists unless you pass REPLACE_EXISTING. Note that copying a directory only creates the directory entry itself: it does not recursively copy its contents. Files.move() is atomic on the same filesystem and is the preferred way to implement safe file writes (write to a temp file, then atomically replace the target). Files.delete() throws NoSuchFileException if the path does not exist; Files.deleteIfExists() returns a boolean and does not throw in that case.

Java
Path src  = Path.of("src/data.txt");
Path dest = Path.of("dest/data.txt");

// Copy — fails if destination exists by default
Files.copy(src, dest);

// Copy with options
Files.copy(src, dest,
    StandardCopyOption.REPLACE_EXISTING,  // overwrite if exists
    StandardCopyOption.COPY_ATTRIBUTES);  // preserve timestamps etc.

// Copy from/to InputStream (useful for resources, downloads)
try (InputStream in = SomeApi.getStream()) {
    Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);
}
try (OutputStream out = new FileOutputStream("out.bin")) {
    Files.copy(src, out);
}

// Move (rename) — atomic on same filesystem
Files.move(src, dest, StandardCopyOption.REPLACE_EXISTING);
// ATOMIC_MOVE — platform-dependent, throws AtomicMoveNotSupportedException if unavailable
Files.move(src, dest, StandardCopyOption.ATOMIC_MOVE);

// Delete
Files.delete(dest);                  // throws NoSuchFileException if missing
Files.deleteIfExists(dest);          // no exception if missing

// Delete a directory tree
Files.walk(Path.of("target"))
    .sorted(Comparator.reverseOrder()) // delete children before parents
    .forEach(p -> {
        try { Files.delete(p); } catch (IOException e) { throw new RuntimeException(e); }
    });

// Create directories
Files.createDirectory(Path.of("newdir"));        // parent must exist
Files.createDirectories(Path.of("a/b/c/d"));    // creates all missing parents

Walking and Finding Files

Files.walk() returns a lazy Stream<Path> that traverses all descendants of a directory depth-first; always close it with try-with-resources. Files.find() combines the walk with a BiPredicate<Path, BasicFileAttributes> so you can filter on file attributes (size, modification time, type) without issuing separate stat calls per file. Both streams must be closed. For a non-recursive listing equivalent to ls, use Files.list().

Java
Path root = Path.of("/home/user/projects");

// Walk — depth-first stream of all paths (files + directories)
try (Stream<Path> walk = Files.walk(root)) {
    walk.filter(Files::isRegularFile)
        .filter(p -> p.toString().endsWith(".java"))
        .forEach(System.out::println);
}

// Walk with max depth
try (Stream<Path> shallow = Files.walk(root, 2)) {
    // only goes 2 levels deep
}

// find — walk with a BiPredicate (more efficient than walk + filter)
try (Stream<Path> found = Files.find(root, Integer.MAX_VALUE,
        (path, attrs) -> attrs.isRegularFile()
                      && path.toString().endsWith(".java")
                      && attrs.size() > 1024)) {
    found.forEach(System.out::println);
}

// list — non-recursive listing of a directory (like ls)
try (Stream<Path> entries = Files.list(root)) {
    entries.filter(Files::isDirectory)
           .forEach(System.out::println);
}

// FileVisitor — full control over traversal
Files.walkFileTree(root, new SimpleFileVisitor<Path>() {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        System.out.println("File: " + file + " (" + attrs.size() + " bytes)");
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) {
        if (dir.getFileName().toString().equals(".git")) {
            return FileVisitResult.SKIP_SUBTREE; // skip .git directories
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) {
        System.err.println("Failed to visit: " + file + " - " + exc);
        return FileVisitResult.CONTINUE; // continue despite error
    }
});

File Attributes and Metadata

The Files utility class provides individual helper methods for common checks: Files.size(), Files.isRegularFile(), Files.isDirectory(), Files.isReadable(), and Files.isWritable(). For multiple attributes at once, Files.readAttributes(path, BasicFileAttributes.class) fetches creation time, last-modified time, last-access time, size, and type in a single OS call. On POSIX systems (Linux, macOS), PosixFileAttributes additionally exposes owner, group, and permission bits. Use Files.getLastModifiedTime() as a shortcut when you only need the timestamp.

Java
import java.nio.file.attribute.*;

Path file = Path.of("report.txt");

// Quick checks
boolean exists   = Files.exists(file);
boolean isFile   = Files.isRegularFile(file);
boolean isDir    = Files.isDirectory(file);
boolean readable = Files.isReadable(file);
boolean writable = Files.isWritable(file);
boolean hidden   = Files.isHidden(file);
long size        = Files.size(file);             // throws if not exists

// Basic attributes — all at once (single OS call)
BasicFileAttributes attrs = Files.readAttributes(file, BasicFileAttributes.class);
System.out.println("Size:     " + attrs.size());
System.out.println("Created:  " + attrs.creationTime());
System.out.println("Modified: " + attrs.lastModifiedTime());
System.out.println("Accessed: " + attrs.lastAccessTime());
System.out.println("Is symlink: " + attrs.isSymbolicLink());

// Set modification time
Files.setLastModifiedTime(file, FileTime.fromMillis(System.currentTimeMillis()));

// POSIX attributes (Linux/macOS) — owner, group, permissions
if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) {
    PosixFileAttributes posix = Files.readAttributes(file, PosixFileAttributes.class);
    System.out.println("Owner:  " + posix.owner().getName());
    System.out.println("Group:  " + posix.group().getName());
    System.out.println("Perms:  " + PosixFilePermissions.toString(posix.permissions()));
    // e.g., "rw-r--r--"

    // Set permissions
    Set<PosixFilePermission> perms = PosixFilePermissions.fromString("rw-rw-r--");
    Files.setPosixFilePermissions(file, perms);
}

// User-defined attributes (extended attributes)
UserDefinedFileAttributeView view =
    Files.getFileAttributeView(file, UserDefinedFileAttributeView.class);
ByteBuffer buf = ByteBuffer.allocate(64);
view.write("myapp.checksum", ByteBuffer.wrap("sha256:abc123".getBytes()));
buf.clear();
view.read("myapp.checksum", buf);
buf.flip();
System.out.println(StandardCharsets.UTF_8.decode(buf)); // sha256:abc123
Prefer Path and Files over the legacy java.io.File for all new code. File has many design problems: methods return boolean instead of throwing exceptions on failure, no symbolic link support, and limited attribute access. Path.toFile() and File.toPath() bridge between the two APIs when needed.