Java I/O — File Handling & NIO Guide
In this tutorial, you'll learn about Java I/O. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Java I/O provides APIs for reading and writing files and streams, with the NIO package offering non-blocking and memory-mapped operations for high-performance apps.
Why Java I/O Matters
Every real-world application reads or writes data — configuration files, logs, user uploads, database files. Without solid I/O handling, your app is fragile. DodaTech's Durga Antivirus Pro scans millions of files daily using Java NIO to read file signatures at high speed. Understanding I/O lets you write tools that handle data reliably, whether it's a simple log parser or a security scanner that examines every byte of a suspicious file. This builds on Java Collections for data processing and leads into Java Concurrency for parallel file handling.
Learning Path
graph LR
A[Java Basics] --> B[Java Collections]
B --> C[Java I/O & NIO]
C --> D[Java Concurrency]
D --> E[Real-World File Scanners]
C --> F[Memory-Mapped Processing]
style C fill:#f59e0b,color:#fff,stroke-width:3px
The Two Pillars: Stream I/O and NIO
Java gives you two ways to work with files. Stream I/O (java.io) works with byte and character streams — think of it like a garden hose where water (data) flows one drop at a time. NIO (java.nio) works with channels and buffers, more like a pipeline where water flows in large batches. NIO is faster for big files and supports non-blocking operations.
Reading a File with Stream I/O
Let's start with the simplest approach — reading a text file using FileReader and BufferedReader.
import java.io.*;
public class SimpleFileReader {
public static void main(String[] args) {
File file = new File("config.properties");
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
System.err.println("Error reading file: " + e.getMessage());
}
}
}
Let's break down what's happening. File represents the file path. FileReader opens a character stream to the file. BufferedReader wraps it with an internal buffer (8 KB by default) so we read in chunks instead of one character at a time — much faster. The try-with-resources block (Java 7+) automatically closes the reader, even if an exception occurs. The while loop reads lines until readLine() returns null, which means end of file.
Expected output (given a file with two lines):
# Database configuration
db.host=localhost
Writing to a File with Stream I/O
Writing follows the same pattern but with FileWriter and BufferedWriter.
import java.io.*;
import java.time.LocalDateTime;
public class SimpleFileWriter {
public static void main(String[] args) {
String logEntry = LocalDateTime.now() + " [INFO] Application started\n";
try (BufferedWriter writer = new BufferedWriter(new FileWriter("app.log", true))) {
writer.write(logEntry);
System.out.println("Log entry written successfully");
} catch (IOException e) {
System.err.println("Error writing file: " + e.getMessage());
}
}
}
The second parameter true in FileWriter enables append mode — new data is added to the end of the file instead of overwriting it. This is how log files work: each new event appends to the growing log. Without true, the file is truncated on every write.
Expected output:
Log entry written successfully
The file app.log now contains something like 2026-06-20T10:30:00 [INFO] Application started.
Modern I/O with NIO — Path and Files
NIO's Path and Files classes make file operations simpler and more powerful. They were introduced in Java 7 and are now the recommended approach.
import java.nio.file.*;
import java.util.*;
import java.io.IOException;
public class NIOFileRead {
public static void main(String[] args) {
Path path = Paths.get("data", "users.csv");
try {
List<String> lines = Files.readAllLines(path);
for (String line : lines) {
System.out.println(line);
}
long byteCount = Files.size(path);
System.out.println("File size: " + byteCount + " bytes");
} catch (IOException e) {
System.err.println("Error: " + e.getMessage());
}
}
}
Files.readAllLines() reads the entire file into memory — perfect for small to medium files. For large files, use Files.lines() which returns a Stream<String> that processes lines lazily. Files.size() returns the file size without opening the file at all.
Expected output (CSV file with header + 2 rows):
id,name,email
1,Alice,alice@example.com
2,Bob,bob@example.com
File size: 52 bytes
Walking a Directory Tree with NIO
Security tools often need to scan entire directory structures. NIO's walk() method traverses a directory tree recursively.
import java.nio.file.*;
import java.io.IOException;
import java.util.stream.Stream;
public class DirectoryWalker {
public static void main(String[] args) {
Path startDir = Paths.get("/var/log");
try (Stream<Path> stream = Files.walk(startDir, 3)) {
stream.filter(Files::isRegularFile)
.filter(p -> p.toString().endsWith(".log"))
.forEach(p -> {
try {
long size = Files.size(p);
System.out.printf("%s — %d bytes%n", p, size);
} catch (IOException e) {
System.err.println("Can't access: " + p);
}
});
} catch (IOException e) {
System.err.println("Error walking directory: " + e.getMessage());
}
}
}
Files.walk() returns a lazy Stream<Path> that traverses the directory tree. The second parameter limits depth to 3 levels. We filter to only regular files (not directories) that end with .log. The stream is wrapped in try-with-resources because directory streams hold open file handles. This pattern is used by Durga Antivirus Pro to scan directories for suspicious file signatures.
Expected output (sample):
/var/log/syslog — 128340 bytes
/var/log/auth.log — 45210 bytes
/var/log/nginx/access.log — 890123 bytes
Common Errors in Java I/O
| Error | Cause | Fix |
|---|---|---|
FileNotFoundException |
File path is wrong or file doesn't exist | Check path with Files.exists() before opening |
IOException: Stream closed |
Reading from a closed stream | Ensure try-with-resources or proper close order |
java.nio.file.AccessDeniedException |
File permissions prevent read/write | Check file permissions with Files.isReadable() |
OutOfMemoryError |
Reading an entire huge file into memory | Use Files.lines() (lazy stream) instead of readAllLines() |
NoSuchFileException |
NIO path contains invalid characters or missing parent dirs | Create parent dirs with Files.createDirectories() first |
NotDirectoryException |
Called walk() on a file, not a directory |
Check Files.isDirectory() first |
ClosedWatchServiceException |
WatchService was closed while watching | Re-create WatchService or check isOpen() before events |
Java I/O vs NIO at a Glance
| Aspect | java.io (Stream) | java.nio |
|---|---|---|
| Core concept | Streams (byte/char flow) | Channels + Buffers |
| Blocking | Always blocking | Both blocking and non-blocking |
| Buffering | Optional (wrap in BufferedReader) | Built-in with Buffer classes |
| File operations | File class (basic) | Path + Files (rich API) |
| Directory walking | Manual Recursion | Files.walk() / Files.find() |
| Performance | Good for small files | Excellent for large files |
| Memory mapping | Not supported | FileChannel.map() for memory-mapped I/O |
Security Angle: File Scanning in Practice
Java I/O is central to security tools. Durga Antivirus Pro uses Files.walk() to traverse directories, FileChannel.map() to memory-map suspicious files for byte-level signature matching, and WatchService to monitor directories in real time for new files.
Here's how WatchService monitors a directory for new or modified files:
import java.nio.file.*;
import static java.nio.file.StandardWatchEventKinds.*;
public class DirectoryWatcher {
public static void main(String[] args) throws IOException, InterruptedException {
WatchService watcher = FileSystems.getDefault().newWatchService();
Path dir = Paths.get("/home/user/downloads");
dir.register(watcher, ENTRY_CREATE, ENTRY_MODIFY, ENTRY_DELETE);
System.out.println("Watching: " + dir);
while (true) {
WatchKey key = watcher.take();
for (WatchEvent<?> event : key.pollEvents()) {
WatchEvent.Kind<?> kind = event.kind();
Path fileName = (Path) event.context();
System.out.println(kind.name() + ": " + fileName);
if (kind == ENTRY_CREATE && fileName.toString().endsWith(".exe")) {
System.out.println(" >> Potential executable detected — scanning...");
}
}
if (!key.reset()) break;
}
}
}
WatchService registers interest in file events (create, modify, delete). The take() method blocks until an event occurs. pollEvents() returns all accumulated events. key.reset() must be called after processing to continue watching. This exact pattern powers DodaTech's real-time threat detection.
Expected output (when a file is added):
Watching: /home/user/downloads
ENTRY_CREATE: setup.exe
>> Potential executable detected — scanning...
ENTRY_MODIFY: setup.exe
Practice Questions
- What is the difference between
FileReaderandBufferedReader? - How does NIO's
Files.walk()differ fromFiles.list()? - What does the
trueparameter do innew FileWriter("log.txt", true)? - Why should you use try-with-resources for I/O operations?
- What is the purpose of
WatchServicein NIO?
Answers:
FileReaderreads characters directly from the file (one at a time).BufferedReaderwraps a reader with an internal buffer (default 8 KB) so data is read in chunks for much better performance.Files.list()returns only immediate children of a directory (non-recursive).Files.walk()traverses the entire subtree recursively.walkalso supports depth limiting.- It enables append mode — new writes go to the end of the file instead of overwriting it. Without it, the file is truncated on open.
- Try-with-resources automatically calls
close()on resources, even if an exception occurs. Without it, unclosed file handles cause resource leaks that crash the application. WatchServicemonitors directories for file system events (create, modify, delete). It's used for real-time file monitoring in security scanners and backup tools.
Challenge
Write a program that monitors a directory for new .csv files, reads them with Files.readAllLines(), and logs the row count and column headers. If a file exceeds 10 MB, skip it and log a warning. Handle AccessDeniedException gracefully — log the error and continue watching.
Real-World Task: Log File Analyzer
Build a log file analyzer that:
- Walks
/var/log/recursively - Finds files modified in the last 24 hours
- Counts lines containing "ERROR" and "WARN"
- Writes a summary report to
reports/summary.txt - Uses NIO throughout
This is the same pattern DodaTech uses for automated log monitoring across thousands of customer servers — processing terabytes of log data daily.
Related tutorials: Java Streams, Java Concurrency, Performance Testing
Next lesson: Java Testing — JUnit 5 Complete Guide
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro