NIO.2: Path & Files
Java 7 introduced NIO.2 (java.nio.file), a complete overhaul of file I/O that fixes the long-standing pain points of the old java.io.File class. If you are writing new code today, Path and Files should be your first choice for anything file-system related.
Tip: Already using
java.io.File? You can bridge the two worlds withfile.toPath()andpath.toFile(), so you can adopt NIO.2 incrementally.
Why NIO.2 Instead of java.io.File?
The classic File class works, but it has frustrating design quirks:
Pain point with java.io.File | NIO.2 solution |
|---|---|
delete(), mkdir() return boolean — no error detail | Methods throw IOException with a real message |
| No symbolic link support | Full symlink awareness |
| No file attributes (owner, POSIX permissions, etc.) | Rich attribute views |
| No directory tree walking API | Files.walk(), Files.walkFileTree() |
renameTo() is unreliable across file systems | Files.move() is atomic on the same FS |
| Slow directory listing | Files.newDirectoryStream() is lazy |
The Three Key Classes
Everything in NIO.2 revolves around three classes in java.nio.file:
Path— an object that represents a file or directory path (immutable, likeString).Paths— a factory class with static methods to createPathobjects. (Java 11+ can usePath.of()directly.)Files— a utility class with static methods that actually do things: copy, move, read, write, walk, check attributes, etc.
Creating a Path
import java.nio.file.Path;
import java.nio.file.Paths;
public class PathDemo {
public static void main(String[] args) {
// Java 7–10: use Paths.get()
Path p1 = Paths.get("data/config.txt");
// Java 11+: use Path.of() — cleaner
Path p2 = Path.of("data", "config.txt"); // varargs segments
// Absolute path
Path abs = Path.of("/home/user/docs/report.pdf");
System.out.println(p1); // data/config.txt
System.out.println(p2); // data/config.txt
System.out.println(abs.getFileName()); // report.pdf
System.out.println(abs.getParent()); // /home/user/docs
System.out.println(abs.getRoot()); // /
}
}
Output:
data/config.txt
data/config.txt
report.pdf
/home/user/docs
/
Useful Path Methods
Path itself is mostly about navigating and inspecting the path — it does not touch the file system.
Path p = Path.of("/home/user/projects/app/src/Main.java");
System.out.println(p.getNameCount()); // 6 (number of elements)
System.out.println(p.getName(0)); // home
System.out.println(p.getName(5)); // Main.java
System.out.println(p.subpath(3, 5)); // app/src
System.out.println(p.isAbsolute()); // true
System.out.println(p.normalize()); // cleans up any .. or . segments
System.out.println(p.resolve("test")); // /home/user/projects/app/src/Main.java/test
resolve() appends a relative path, and relativize() computes the relative path between two paths:
Path base = Path.of("/home/user");
Path target = Path.of("/home/user/docs/report.pdf");
System.out.println(base.relativize(target)); // docs/report.pdf
Checking Files with Files
Files provides all the I/O operations. These methods talk to the file system and throw IOException when something goes wrong.
import java.nio.file.Files;
import java.nio.file.Path;
public class FilesCheck {
public static void main(String[] args) throws Exception {
Path p = Path.of("data/config.txt");
System.out.println(Files.exists(p)); // true/false
System.out.println(Files.isDirectory(p)); // false
System.out.println(Files.isRegularFile(p)); // true
System.out.println(Files.isReadable(p)); // true
System.out.println(Files.isWritable(p)); // true
System.out.println(Files.isHidden(p)); // false
System.out.println(Files.size(p)); // file size in bytes
}
}
Reading and Writing Files
NIO.2 makes common read/write tasks one-liners.
Read All Lines into a List
import java.nio.file.*;
import java.util.List;
public class ReadLines {
public static void main(String[] args) throws Exception {
List<String> lines = Files.readAllLines(Path.of("notes.txt"));
lines.forEach(System.out::println);
}
}
Read All Bytes
byte[] bytes = Files.readAllBytes(Path.of("image.png"));
System.out.println("Bytes read: " + bytes.length);
Write Lines to a File
import java.nio.file.*;
import java.util.List;
public class WriteLines {
public static void main(String[] args) throws Exception {
List<String> content = List.of("Line 1", "Line 2", "Line 3");
// Creates the file if it does not exist; overwrites if it does
Files.write(Path.of("output.txt"), content);
// Append instead of overwrite
Files.write(Path.of("output.txt"), List.of("Line 4"),
StandardOpenOption.APPEND);
}
}
Tip:
Files.writeString()(Java 11+) is even more convenient when you have a singleStringto write.
Files.writeString(Path.of("hello.txt"), "Hello, NIO.2!");
String text = Files.readString(Path.of("hello.txt")); // Java 11+
System.out.println(text); // Hello, NIO.2!
Creating, Copying, Moving, and Deleting
Create Files and Directories
import java.nio.file.*;
public class CreateDemo {
public static void main(String[] args) throws Exception {
// Create a single directory
Files.createDirectory(Path.of("output"));
// Create full path (like mkdirs)
Files.createDirectories(Path.of("output/reports/2024"));
// Create an empty file
Files.createFile(Path.of("output/report.txt"));
}
}
Warning:
Files.createDirectory()throwsFileAlreadyExistsExceptionif the directory already exists. UseFiles.createDirectories()when you are not sure.
Copy a File
import java.nio.file.*;
public class CopyDemo {
public static void main(String[] args) throws Exception {
Path src = Path.of("original.txt");
Path dst = Path.of("backup/original_copy.txt");
// REPLACE_EXISTING overwrites the destination if it exists
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied successfully");
}
}
Move (Rename) a File
Files.move(
Path.of("draft.txt"),
Path.of("archive/final.txt"),
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.ATOMIC_MOVE // best-effort atomic; OS-dependent
);
ATOMIC_MOVE asks the OS to perform the move atomically so another process never sees a partial file. It falls back silently if the OS does not support it for that path.
Delete a File
// Throws NoSuchFileException if it does not exist
Files.delete(Path.of("temp.log"));
// Silent version — returns false if not found, never throws
Files.deleteIfExists(Path.of("temp.log"));
Walking a Directory Tree
Files.walk() — Stream-based Walking
import java.nio.file.*;
import java.util.stream.Stream;
public class WalkDemo {
public static void main(String[] args) throws Exception {
try (Stream<Path> stream = Files.walk(Path.of("src"))) {
stream
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
}
}
Output (example):
src/com/example/Main.java
src/com/example/Utils.java
Note: Always use
Files.walk()inside a try-with-resources block — it returns a lazyStreambacked by an open directory iterator that must be closed.
Delete a Non-Empty Directory
The old File.delete() fails on non-empty directories. With NIO.2 you walk the tree and delete bottom-up:
import java.nio.file.*;
import java.util.Comparator;
import java.util.stream.Stream;
public class DeleteDir {
public static void main(String[] args) throws Exception {
Path dir = Path.of("build");
try (Stream<Path> walk = Files.walk(dir)) {
walk.sorted(Comparator.reverseOrder()) // deepest files first
.forEach(p -> {
try { Files.delete(p); }
catch (Exception e) { throw new RuntimeException(e); }
});
}
System.out.println("Deleted: " + !Files.exists(dir));
}
}
File Attributes
NIO.2 gives you access to rich metadata beyond what java.io.File exposes.
import java.nio.file.*;
import java.nio.file.attribute.*;
public class AttributesDemo {
public static void main(String[] args) throws Exception {
Path p = Path.of("data.txt");
BasicFileAttributes attrs =
Files.readAttributes(p, BasicFileAttributes.class);
System.out.println("Size: " + attrs.size());
System.out.println("Created: " + attrs.creationTime());
System.out.println("Last modified: " + attrs.lastModifiedTime());
System.out.println("Is symlink: " + attrs.isSymbolicLink());
System.out.println("Is directory: " + attrs.isDirectory());
}
}
On POSIX systems (Linux, macOS) you can read POSIX permissions:
PosixFileAttributes posix =
Files.readAttributes(p, PosixFileAttributes.class);
System.out.println(PosixFilePermissions.toString(posix.permissions()));
// e.g. rw-r--r--
Watching a Directory for Changes
WatchService (also part of NIO.2) lets you monitor a directory for file-system events without polling.
import java.nio.file.*;
public class WatchDemo {
public static void main(String[] args) throws Exception {
WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Path.of("watched");
dir.register(watcher,
StandardWatchEventKinds.ENTRY_CREATE,
StandardWatchEventKinds.ENTRY_MODIFY,
StandardWatchEventKinds.ENTRY_DELETE);
System.out.println("Watching " + dir + " ...");
WatchKey key = watcher.take(); // blocks until an event occurs
for (WatchEvent<?> event : key.pollEvents()) {
System.out.println(event.kind() + ": " + event.context());
}
key.reset();
}
}
Under the Hood
Path Is an Interface
Path is actually an interface in java.nio.file. When you call Path.of() or Paths.get(), you get a platform-specific implementation: sun.nio.fs.UnixPath on Linux/macOS, or sun.nio.fs.WindowsPath on Windows. This is why path comparison with equals() is case-sensitive on Linux but case-insensitive on Windows — the implementation delegates to the OS.
Files and the Provider SPI
Files is a facade over a FileSystemProvider (a Service Provider Interface). The default provider maps to the native OS file system. Third-party providers (ZIP file systems, cloud file systems, in-memory file systems for testing) can be plugged in transparently — your Files.copy() call works the same regardless of the underlying provider.
Atomic Operations and OS Guarantees
Files.move() with ATOMIC_MOVE maps to rename(2) on Linux, which is atomic within the same mount point. Files.createTempFile() uses O_EXCL internally, so there is no race condition between checking existence and creating the file — unlike File.createNewFile() on some systems.
Memory-Efficient Directory Streaming
Files.newDirectoryStream() wraps the OS readdir() call and yields entries one at a time, so it does not load the whole directory listing into memory. This is far more efficient than file.listFiles() in very large directories (millions of entries).
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Path.of("."))) {
for (Path entry : stream) {
System.out.println(entry.getFileName());
}
}
Quick Reference: Files Method Cheat Sheet
| Method | Since | Description |
|---|---|---|
Files.exists(path) | 7 | Check if path exists |
Files.readAllLines(path) | 7 | Read lines into List<String> |
Files.readAllBytes(path) | 7 | Read raw bytes |
Files.readString(path) | 11 | Read entire file as String |
Files.write(path, lines) | 7 | Write lines to file |
Files.writeString(path, s) | 11 | Write a String to file |
Files.copy(src, dst) | 7 | Copy file |
Files.move(src, dst) | 7 | Move/rename file |
Files.delete(path) | 7 | Delete (throws if missing) |
Files.deleteIfExists(path) | 7 | Delete silently |
Files.createDirectories(path) | 7 | Create full directory tree |
Files.walk(path) | 7 | Lazy recursive stream of all paths |
Files.lines(path) | 8 | Lazy Stream<String> of lines |
Files.readAttributes(path, cls) | 7 | Read file metadata |
Related Topics
- File Class — the classic
java.io.FileAPI and when you might still reach for it - File Handling — overview of all file I/O approaches in Java
- Read a File Line by Line — comparing
BufferedReadervsFiles.readAllLines()vsFiles.lines() - Write to a File — all the ways to write text and binary data to disk
- Delete a File — deleting files and directories safely
- Serialization — persisting Java objects to a file with
ObjectOutputStream