File I/O & NIO.2

The Path & Files APIs

15 min Lesson 4 of 13

The Path & Files APIs

Java 7 introduced the NIO.2 API — java.nio.file — to replace the old java.io.File class. Where File was inconsistent (silent failures, no exception details, poor symlink support), Path and Files are precise, composable, and exception-driven. If you are working with modern Java, these are the right tools for every file-system operation.

Path: Representing a Location

Path is an interface that represents a location in the file system — it carries no I/O itself. You create one with Path.of() (Java 11+) or the equivalent Paths.get():

import java.nio.file.Path; Path absolute = Path.of("/home/user/documents/report.txt"); Path relative = Path.of("data", "config.json"); // platform separator added automatically Path resolved = Path.of("/home/user").resolve("documents/report.txt"); System.out.println(absolute.getFileName()); // report.txt System.out.println(absolute.getParent()); // /home/user/documents System.out.println(absolute.getRoot()); // / System.out.println(relative.toAbsolutePath()); // anchors to the JVM working directory

Key navigation methods on Path:

  • resolve(other) — appends another path segment, returning a new Path.
  • relativize(other) — computes a relative path between two absolute paths.
  • normalize() — removes redundant . and .. segments.
  • toAbsolutePath() — makes a relative path absolute based on the JVM's working directory.
  • toRealPath() — like toAbsolutePath() but also resolves symlinks and throws IOException if the file does not exist.
Path base = Path.of("/var/app/logs"); Path target = Path.of("/var/app/logs/2024/march/error.log"); Path rel = base.relativize(target); System.out.println(rel); // 2024/march/error.log Path normalized = Path.of("/var/app/./logs/../logs/access.log").normalize(); System.out.println(normalized); // /var/app/logs/access.log
Path is immutable. Every method that looks like it modifies a path — resolve, normalize, relativize — returns a new Path object. The original is unchanged.

Files: The Static Utility Class

Files is a final class of static methods that perform actual I/O on a Path. Think of Path as the address and Files as the postal service that does something at that address.

Checking Existence and Metadata

import java.nio.file.Files; import java.nio.file.Path; Path p = Path.of("data/report.csv"); boolean exists = Files.exists(p); // true / false boolean isFile = Files.isRegularFile(p); boolean isDir = Files.isDirectory(p); boolean readable = Files.isReadable(p); boolean writable = Files.isWritable(p); long sizeBytes = Files.size(p); // throws IOException if missing System.out.printf("exists=%b size=%d bytes%n", exists, sizeBytes);
TOCTOU hazard. Checking Files.exists() and then acting on the result is a time-of-check / time-of-use race condition in multi-threaded or multi-process code. Prefer attempting the operation directly and handling the IOException — for example, open the file for writing and catch FileAlreadyExistsException if exclusive creation is needed.

Copying Files

Files.copy(source, target, options...) copies a file or directory entry. The optional CopyOption vararg controls behaviour:

import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; Path src = Path.of("originals/photo.jpg"); Path dest = Path.of("backups/photo.jpg"); // Default: throws FileAlreadyExistsException if dest exists Files.copy(src, dest); // Overwrite if destination already exists Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING); // Also copy file attributes (timestamps, permissions) Files.copy(src, dest, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
Copying a directory copies only the directory entry, not its children. To copy a directory tree recursively, walk the tree with Files.walkFileTree() and copy each entry individually — or use a library like Apache Commons IO that wraps this pattern.

Moving and Renaming Files

Files.move(source, target, options...) moves or renames. When source and target are on the same file-system partition this is typically an atomic rename (as cheap as mv in a shell); across partitions it copies then deletes.

Path oldPath = Path.of("inbox/draft.txt"); Path newPath = Path.of("archive/2024/draft.txt"); // Ensure the destination directory exists first Files.createDirectories(newPath.getParent()); Files.move(oldPath, newPath, StandardCopyOption.REPLACE_EXISTING); // oldPath no longer exists; newPath holds the file

ATOMIC_MOVE is a second option. When supported by the file system it guarantees the move is atomic — no other process can see a partial state:

Files.move(src, dest, StandardCopyOption.ATOMIC_MOVE); // throws AtomicMoveNotSupportedException if the OS/FS cannot honour it

Deleting Files

Two variants exist with different failure semantics:

Path file = Path.of("temp/cache.bin"); // Throws NoSuchFileException if the file does not exist Files.delete(file); // Returns false (no exception) if the file does not exist — "delete if present" boolean deleted = Files.deleteIfExists(file);
You cannot delete a non-empty directory with either call — both throw DirectoryNotEmptyException. To delete a tree, walk it with Files.walkFileTree() and delete files before their parent directories.

Creating Directories

// Creates ONE directory — fails if parent does not exist Files.createDirectory(Path.of("logs")); // Creates the full path including any missing parents (like mkdir -p) Files.createDirectories(Path.of("logs/2024/march")); // Create a temp file / temp directory Path tmpFile = Files.createTempFile("prefix-", ".log"); Path tmpDir = Files.createTempDirectory("work-");

Reading File Size Efficiently

Files.size(path) queries the file-system metadata and returns the size in bytes without reading the file content — it is an O(1) syscall, not an O(n) read. Use it for validation, progress reporting, or routing large files to a streaming path:

Path upload = Path.of("uploads/video.mp4"); long maxBytes = 100L * 1024 * 1024; // 100 MB if (Files.size(upload) > maxBytes) { throw new IllegalArgumentException("File exceeds the 100 MB limit"); }

Putting It All Together

import java.io.IOException; import java.nio.file.*; public class FileOps { public static void archiveLog(Path source, Path archiveDir) throws IOException { if (!Files.isRegularFile(source)) { throw new IllegalArgumentException("Not a regular file: " + source); } Files.createDirectories(archiveDir); Path dest = archiveDir.resolve(source.getFileName()); Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES); System.out.printf("Archived %s (%,d bytes) -> %s%n", source.getFileName(), Files.size(dest), dest); Files.deleteIfExists(source); } public static void main(String[] args) throws IOException { archiveLog( Path.of("logs/app.log"), Path.of("archive/2024/june") ); } }

Summary

Path is an immutable, composable representation of a file-system location; Files is the static utility that acts on it. Use Files.copy() with REPLACE_EXISTING or COPY_ATTRIBUTES, Files.move() with ATOMIC_MOVE for safe renames, Files.delete() vs Files.deleteIfExists() depending on whether a missing file is an error, and Files.size() for cheap metadata queries. Always let operations throw IOException and handle it at the call site rather than swallowing it silently.