Learning goals
By the end of this chapter you should be able to:
- Use
PathandPathsto represent file locations without hard-coding platform separators - Read and write text with
Fileshelpers and understand when to use streams vs. whole-file APIs - Close resources safely with try-with-resources and know why leaked handles cause production bugs
- Choose between
InputStream/OutputStream,Reader/Writer, and NIO.2Filesfor typical tasks
Why NIO.2 matters
Before Java 7, file code mixed java.io.File, manual streams, and easy-to-forget close() calls. NIO.2 (java.nio.file) gives you:
Path— immutable path value (replaces mostFileusage)Files— static helpers for copy, move, read, write, walk trees- Better errors —
IOExceptionwith clear messages instead of silentfalsereturns
For new code on JDK 21, default to Path + Files. Drop down to streams only when you need fine-grained buffering or binary protocols.
Path basics
Path home = Path.of(System.getProperty("user.home"));
Path config = home.resolve(".myapp").resolve("config.properties");
Path absolute = config.toAbsolutePath(); // resolve against default file system
Path normalized = config.normalize(); // collapse . and ..
boolean exists = Files.exists(config);
boolean isDir = Files.isDirectory(config);
Path is not a file. It is a location string with helpers. Creating a Path does not touch disk until you call Files.*.
Common operations:
String fileName = config.getFileName().toString(); // "config.properties"
Path parent = config.getParent(); // .../.myapp
Path sibling = config.resolveSibling("backup.properties");
Reading and writing text
Whole file (small, simple)
Since Java 11:
Path p = Path.of("notes.txt");
String content = Files.readString(p); // UTF-8 by default
Files.writeString(p, "updated\n", StandardOpenOption.TRUNCATE_EXISTING);
Specify charset when needed:
String latin = Files.readString(p, StandardCharsets.ISO_8859_1);
Line-by-line (large files)
try (Stream<String> lines = Files.lines(p)) {
long count = lines.filter(line -> !line.isBlank()).count();
}
The stream must be closed — hence try-with-resources. Loading a 2 GB log with readString will blow the heap.
Binary data
byte[] bytes = Files.readAllBytes(p);
Files.write(p, bytes, StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);
try-with-resources
Any type implementing AutoCloseable can sit in a try-with-resources header. The compiler inserts close() in a finally block — even if an exception is thrown.
public List<String> readLines(Path path) throws IOException {
try (BufferedReader reader = Files.newBufferedReader(path)) {
return reader.lines().toList();
}
}
Multiple resources, declared in order, close in reverse order:
try (InputStream in = Files.newInputStream(src);
OutputStream out = Files.newOutputStream(dst)) {
in.transferTo(out);
}
If both in and out throw on close, the primary exception from the try block is preserved; suppressed exceptions attach to it.
Creating directories and copying
Files.createDirectories(Path.of("data/cache")); // mkdir -p style
Files.copy(src, dst, StandardCopyOption.REPLACE_EXISTING);
Files.move(oldPath, newPath, StandardCopyOption.ATOMIC_MOVE); // when supported
Use CREATE_NEW when you want “fail if exists” semantics for idempotent setup scripts.
Walking a directory tree
try (Stream<Path> walk = Files.walk(Path.of("src"), 2)) {
walk.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".java"))
.forEach(System.out::println);
}
Depth limit 2 avoids scanning entire monorepos accidentally. For production file watchers, consider dedicated libraries — but Files.walk is fine for one-off tooling.
Common mistakes
- Using
Filein new code — preferPath;File.toPath()only when bridging legacy APIs. - Forgetting charset —
readString/writeStringuse UTF-8; Windows Notepad legacy files may need an explicit charset. - Not closing streams — leaks file handles; on Windows you may be unable to delete or overwrite the file until GC runs.
- Checking
Files.existsthen acting — race condition between check and open; catchNoSuchFileExceptionor useCREATEoptions instead. - Mixing text and binary — never wrap a binary
InputStreamwith aReaderunless you know the encoding.
Practice checkpoint
-
Write a method
long countNonBlankLines(Path p)usingFiles.lines. Answer: openFiles.lines(p)in try-with-resources, filter!line.isBlank(), count. -
What happens if you write
try (Files.lines(p))without assigning to a variable? Answer: invalid —lines()returns aStreamyou must bind; the try-with-resources variable must be the stream. -
Copy
a.txttob.txtonly ifb.txtdoes not exist. Answer:Files.copy(a, b, StandardCopyOption.COPY_ATTRIBUTES)withoutREPLACE_EXISTING; or check!Files.exists(b)knowing the TOCTOU race. -
Why prefer
Path.of("foo", "bar")over string concatenation with/? Answer: correct separator on all platforms; avoids double slashes and drive-letter bugs on Windows.
Interview angles
- NIO.2 vs legacy IO:
Path/Filesfor filesystem operations; streams for custom protocols and socket IO. - try-with-resources vs finally: compiler-generated suppression list; fewer boilerplate bugs.
- Memory:
readAllBytes/readStringload entire file — ask about size limits in production. - Atomic move:
ATOMIC_MOVEfor config swaps; falls back on some file systems — know your deployment OS.