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.