Skip to content

Java I/O — File Handling & NIO Guide

DodaTech Updated 2026-06-20 8 min read

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

  1. What is the difference between FileReader and BufferedReader?
  2. How does NIO's Files.walk() differ from Files.list()?
  3. What does the true parameter do in new FileWriter("log.txt", true)?
  4. Why should you use try-with-resources for I/O operations?
  5. What is the purpose of WatchService in NIO?

Answers:

  1. FileReader reads characters directly from the file (one at a time). BufferedReader wraps a reader with an internal buffer (default 8 KB) so data is read in chunks for much better performance.
  2. Files.list() returns only immediate children of a directory (non-recursive). Files.walk() traverses the entire subtree recursively. walk also supports depth limiting.
  3. 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.
  4. 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.
  5. WatchService monitors 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:

  1. Walks /var/log/ recursively
  2. Finds files modified in the last 24 hours
  3. Counts lines containing "ERROR" and "WARN"
  4. Writes a summary report to reports/summary.txt
  5. 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.

What is the difference between java.io and java.nio?

java.io uses stream-based I/O (bytes and characters flowing sequentially), while java.nio uses channel-buffer based I/O that supports non-blocking operations, memory-mapped files, and better performance for large datasets. Since Java 7, NIO is the preferred API for most file operations.

How do I handle very large files in Java?

Use NIO's Files.lines() which returns a lazy Stream<String>, or FileChannel.map() for memory-mapped access. Avoid Files.readAllLines() for files over 100 MB as it loads the entire file into heap memory, risking OutOfMemoryError.

What is a memory-mapped file in Java NIO?

A memory-mapped file uses FileChannel.map() to map a region of a file directly into virtual memory. Reading from this region is as fast as reading from RAM because the OS handles paging. Durga Antivirus Pro uses this to scan file signatures at near-zero overhead.

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