Lab 11: File I/O — NIO.2

Objective

Read and write files using modern Java NIO.2 (java.nio.file), work with Paths, copy/move/delete files, walk directory trees, and process large files with streams.

Background

Java's NIO.2 API (java.nio.file.Files, Path, Paths) replaced the old java.io.File class in Java 7. It's more expressive, throws meaningful exceptions, supports symbolic links, and integrates cleanly with streams. Every Java backend developer works with the filesystem daily — config files, logs, uploads, reports.

Time

40 minutes

Prerequisites

  • Lab 09 (Collections)

  • Lab 10 (Exception Handling)

Tools

  • Java 21 (Eclipse Temurin)

  • Docker image: innozverse-java:latest


Lab Instructions

Step 1: Path Basics

💡 Path.of() replaced Paths.get() in Java 11. Paths are immutable value objects — operations like resolve() and normalize() return new Path instances. Think of Path as a "smart string" for file locations, not a reference to a physical file.

📸 Verified Output:


Step 2: Reading Files

💡 Files.readString() / readAllLines() are convenient for small files. For large files (logs, CSVs with millions of rows), use Files.lines() — it returns a lazy Stream<String> that reads one line at a time. Always use it in try-with-resources to close the underlying file handle.

📸 Verified Output:


Step 3: Writing Files

💡 StandardOpenOption controls write behavior: WRITE (default), APPEND, CREATE, CREATE_NEW, TRUNCATE_EXISTING. CREATE_NEW throws if the file already exists — useful for preventing accidental overwrites. BufferedWriter batches small writes into larger OS calls, dramatically improving performance.

📸 Verified Output:


Step 4: File Operations — Copy, Move, Delete

💡 REPLACE_EXISTING is the key option for copy() and move() — without it they throw if the destination exists. Files.createDirectories() creates the full path including parents (like mkdir -p). Prefer deleteIfExists() over delete() to avoid NoSuchFileException.

📸 Verified Output:


Step 5: Walking Directory Trees

💡 Files.walk() vs Files.find(): walk() returns all paths at all depths; find() takes a BiPredicate<Path, BasicFileAttributes> for filtering — more efficient since attributes are read once. Both return lazy streams — use try-with-resources or they may leak file handles on some platforms.

📸 Verified Output:


Step 6: Watching a Directory

💡 WatchService uses OS-level file system notifications (inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows) — it doesn't poll. This makes it efficient for build tools, config reloaders, and hot-reload systems. key.reset() is mandatory to receive future events.

📸 Verified Output:


Step 7: Working with Temp Files & System Properties

💡 Files.createTempFile(prefix, suffix) creates a file in the system temp directory with a guaranteed unique name. Use it for intermediate processing, test fixtures, or downloads. deleteOnExit() registers a JVM shutdown hook — but for long-running servers, explicitly delete temp files to avoid filling up /tmp.

📸 Verified Output:


Step 8: Complete Example — CSV Log Analyzer

💡 UncheckedIOException wraps IOException in a RuntimeException so it can be thrown inside lambdas (which can't declare checked exceptions). This is the standard Java pattern for using checked exceptions in streams. Alternatively, extract the lambda to a named method with throws.

📸 Verified Output:


Verification

Summary

You've covered Path operations, Files.readString/writeString, Files.lines() for streaming large files, copy/move/delete, directory walking with Files.walk(), WatchService for filesystem events, temp files, and the CSV log analyzer. NIO.2 is the modern standard for all Java file work.

Further Reading

Last updated