Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
94: NIO.2 and the Files API
A few years ago, I was reviewing a junior developer's code for a batch processing system. He had written a cleanup routine using the old java.io.File API. The logic was simple: find a directory of old logs and delete them. The problem was that he was calling file.delete(), which returns a boolean. One day, the process failed in production because of a permission change on the server. The code didn't crash; it just silently received false from the delete method and kept moving. We spent four hours hunting down a "ghost" bug because the API didn't tell us why it failed—it just didn't work.
That's the primary reason we moved to NIO.2 (introduced in Java 7). The old File class was an awkward hybrid of a path and a file handle, and its error handling was abysmal. NIO.2 separates the location of the file from the operations performed on it, providing a much more robust, exception-driven framework that actually tells you when things go wrong.
Paths as Addresses, Files as the Toolkit
In NIO.2, you'll stop using java.io.File and start using java.nio.file.Path and java.nio.file.Files. Think of a Path as just a coordinate or an address; it doesn't actually check if the file exists on the disk. To actually do something—like reading, writing, or deleting—you pass that path into the Files utility class.
I usually prefer Path.of() (in newer Java versions) or Paths.get() to initialize these. It handles the platform-specific separators for you, so you aren't hardcoding backslashes and breaking your code the moment it moves from Windows to Linux.
Path configPath = Path.of("etc", "app", "config.properties");
if (Files.exists(configPath)) {
// Now we actually do something with it
byte[] data = Files.readAllBytes(configPath);
}
The magic here is that if Files.readAllBytes fails, it throws an IOException with a specific message (like "Access Denied"), rather than just returning null or false. It forces you to handle the failure, which is exactly what you want when dealing with the unpredictability of a file system.
Walking the File Tree and Streamlined I/O
One of the most powerful additions in NIO.2 is the ability to treat the file system as a stream. If you've ever written a recursive function to find all .txt files in a folder and its subfolders, you know it's a recipe for a StackOverflowError if the directory structure is deep enough. NIO.2 solves this with Files.walk.
Files.walk returns a Stream<Path>, allowing you to use functional programming to filter and process files. I've found this incredibly useful for implementing custom search tools or cleanup scripts. Just remember: Files.walk opens a resource, so you must use a try-with-resources block to ensure the stream is closed.
try (Stream<Path> paths = Files.walk(Path.of("/var/logs"), 3)) {
paths.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".log"))
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
Beyond walking trees, NIO.2 simplified the "small file" problem. Instead of setting up a BufferedReader, a FileReader, and a while loop just to read a few lines of a config file, you can now use Files.readAllLines() or Files.write(). It's cleaner, faster to write, and much harder to mess up.
📋 Practical Task
Build a Recursive Log Archive Utility
Create a program that scans a specified root directory (and all its sub-directories) for files ending in .log. For every log file found that is older than 30 days, the program should move the file into a folder named /archive located at the root of the search path.
Requirements:
- Use
Files.walkto traverse the directory structure. - Use
Files.getLastModifiedTimeto check the age of the file. - Use
Files.moveto relocate the files. - Ensure the
/archivedirectory is created usingFiles.createDirectoriesif it doesn't already exist. - Properly handle
IOExceptionusing try-catch blocks to ensure the program doesn't crash if a single file is locked by another process.
There are no comments for now.