java.nio.file API
Java's java.nio.file package (NIO.2, introduced in Java 7) provides a modern, expressive API for filesystem operations. It supersedes the error-prone java.io.File class.
Path and Paths
Path represents a path in the filesystem (file or directory). Create one with:
Path.of("dir/file.txt")(Java 11+) — preferredPaths.get("dir/file.txt")— equivalent, works from Java 7
Paths can be absolute or relative. Useful methods: resolve(), relativize(), normalize(), getParent(), getFileName(), toAbsolutePath().
Files utility class
The Files class provides static methods for virtually all common filesystem tasks:
Existence and metadata:
Files.exists(path)/Files.notExists(path)Files.isDirectory(path)/Files.isRegularFile(path)Files.size(path)— file size in bytesFiles.getLastModifiedTime(path)
Creating and deleting:
Files.createFile(path)— throws if file existsFiles.createDirectory(path)/Files.createDirectories(path)— create parent dirs tooFiles.delete(path)— throws if not found;Files.deleteIfExists(path)— silent
Reading and writing (small files):
Files.readString(path)(Java 11+) — entire file as StringFiles.writeString(path, content)(Java 11+) — write String to fileFiles.readAllLines(path)— returnsList<String>Files.readAllBytes(path)/Files.write(path, bytes)
Copying and moving:
Files.copy(src, dest, CopyOption...)Files.move(src, dest, CopyOption...)StandardCopyOption.REPLACE_EXISTING,COPY_ATTRIBUTES,ATOMIC_MOVE
Walking directory trees:
Files.walk(startPath)— returnsStream<Path>of all descendantsFiles.find(start, maxDepth, BiPredicate)— filtered walkFiles.list(dir)— non-recursive directory listing
StandardOpenOption
Used with write methods: CREATE, APPEND, TRUNCATE_EXISTING, CREATE_NEW (fail if exists).
Code Examples
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.List;
public class NioReadWriteDemo {
public static void main(String[] args) throws IOException {
Path file = Path.of("demo.txt");
// Write entire string (creates or replaces file)
Files.writeString(file, "Hello, NIO!\nLine two\nLine three");
System.out.println("Written: " + Files.size(file) + " bytes");
// Read back as a single String (Java 11+)
String content = Files.readString(file);
System.out.println("Content:\n" + content);
// Read as list of lines
List<String> lines = Files.readAllLines(file);
System.out.println("Lines : " + lines.size());
lines.forEach(l -> System.out.println(" > " + l));
// Append more content
Files.writeString(file, "\nAppended line",
StandardOpenOption.APPEND);
System.out.println("After append: " + Files.readAllLines(file).size() + " lines");
// Metadata
System.out.println("Exists : " + Files.exists(file));
System.out.println("Is file : " + Files.isRegularFile(file));
// Cleanup
Files.deleteIfExists(file);
System.out.println("Deleted : " + !Files.exists(file));
}
}Files.writeString and readString (Java 11+) handle the most common I/O tasks in one line each. StandardOpenOption.APPEND adds content without truncating. Always clean up temp files in examples.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class DirectoryWalkDemo {
public static void main(String[] args) throws IOException {
// Create a temporary directory tree for the demo
Path root = Files.createTempDirectory("demo-tree");
Path sub = Files.createDirectory(root.resolve("subdir"));
Files.writeString(root.resolve("a.txt"), "file a");
Files.writeString(root.resolve("b.java"), "file b");
Files.writeString(sub.resolve("c.txt"), "file c");
Files.writeString(sub.resolve("d.java"), "file d");
// Walk all entries (directories included)
System.out.println("All entries:");
try (Stream<Path> walk = Files.walk(root)) {
walk.forEach(p -> System.out.println(" " + root.relativize(p)));
}
// Non-recursive listing of root only
System.out.println("Root only:");
try (Stream<Path> list = Files.list(root)) {
list.forEach(p -> System.out.println(" " + p.getFileName()));
}
// Find only .java files (any depth)
System.out.println(".java files:");
try (Stream<Path> found = Files.find(root, 10,
(p, attrs) -> attrs.isRegularFile()
&& p.toString().endsWith(".java"))) {
found.map(Path::getFileName).forEach(p -> System.out.println(" " + p));
}
// Count files by extension using streams
try (Stream<Path> walk2 = Files.walk(root)) {
long txtCount = walk2
.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".txt"))
.count();
System.out.println(".txt count: " + txtCount);
}
// Cleanup
Files.walk(root).sorted(java.util.Comparator.reverseOrder())
.forEach(p -> { try { Files.delete(p); } catch (IOException e) { } });
}
}Files.walk returns a lazy Stream<Path> — always close it in a try-with-resources to release the underlying directory handle. Files.find is more efficient when you have a filter condition, as it can prune branches early.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class FileCopyDemo {
public static void main(String[] args) throws IOException {
// Setup: create source files
Path source = Files.createTempFile("source", ".txt");
Files.writeString(source, "Original content\nLine 2\nLine 3");
Path dest = source.resolveSibling("destination.txt");
Path moved = source.resolveSibling("moved.txt");
// Copy — fails if dest exists; use REPLACE_EXISTING to overwrite
Files.copy(source, dest, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Copied : " + Files.readString(dest));
// Verify source still exists after copy
System.out.println("Source exists: " + Files.exists(source));
// Move (rename) — ATOMIC_MOVE where supported
Files.move(source, moved, StandardCopyOption.REPLACE_EXISTING);
System.out.println("Source after move: " + Files.exists(source));
System.out.println("Moved exists : " + Files.exists(moved));
System.out.println("Moved content : " + Files.readString(moved));
// Copy with attributes preserved (timestamps etc.)
Path withAttrs = moved.resolveSibling("with-attrs.txt");
Files.copy(moved, withAttrs,
StandardCopyOption.REPLACE_EXISTING,
StandardCopyOption.COPY_ATTRIBUTES);
System.out.println("Attrs copy size: " + Files.size(withAttrs));
// Cleanup
Files.deleteIfExists(dest);
Files.deleteIfExists(moved);
Files.deleteIfExists(withAttrs);
System.out.println("Cleanup done");
}
}Files.copy does NOT delete the source — use Files.move for renaming/relocating. REPLACE_EXISTING silently overwrites the destination. ATOMIC_MOVE guarantees that the rename is an indivisible operation on platforms that support it.
Quick Quiz
1. Which method creates all missing parent directories in a single call?
2. What does `Files.walk()` return?
3. Which StandardCopyOption should you use to overwrite an existing destination file?
4. Which Java version introduced `Files.readString()` and `Files.writeString()`?
Was this lesson helpful?