Phase 3 · Core JavaModule 19~42 min read

File Handling & I/O

Read and write files with byte and character streams, buffering, and the modern NIO.2 API.

What you'll learn

Programs need to save and load data — settings, logs, reports, user files. This module covers Java's input/output (I/O): reading and writing files the modern, easy way with NIO.2, plus the stream classes underneath.

By the end you'll be able to:

  • Tell apart byte and character streams
  • Work with files using the modern Path and Files API
  • Read and write text and binary files
  • Use buffered streams for performance
  • Traverse directories and understand serialization

Byte vs character streams

Java models I/O as streams of data flowing in or out. There are two families, and picking the right one matters:

Two stream families

Byte streams

Raw 8-bit data — images, audio, PDFs, anything binary.

InputStream · OutputStream

Character streams

Text, decoded with a charset (UTF-8) into readable characters.

Reader · Writer

Note

Rule of thumb: text → character streams (Reader/Writer); everything else → byte streams (InputStream/OutputStream). For text, always be explicit about the charset (UTF-8) to avoid garbled characters across systems.

NIO.2: Path & Files

Modern Java (since Java 7) offers NIO.2 — the java.nio.file package — which is far cleaner than the old File class. A Path represents a file or folder location, and the Files utility class does the actual work with simple static methods. This is what you should reach for today.

Reading & writing text

With NIO.2, reading and writing a whole text file is a single line. Files.writeString and Files.readString handle opening, closing, and encoding for you:

TextFiles.java
import java.nio.file.*;
import java.util.List;

Path file = Path.of("notes.txt");

Files.writeString(file, "Line 1\nLine 2\n");   // write (creates/overwrites)
String content = Files.readString(file);          // read the whole file
System.out.print(content);

List<String> lines = Files.readAllLines(file);    // read as a list of lines
System.out.println("Lines: " + lines.size());

By default writing overwrites. To add to an existing file, pass the APPEND option:

Append.java
import java.nio.file.*;

Path file = Path.of("log.txt");

Files.writeString(file, "start\n");                              // fresh
Files.writeString(file, "more\n", StandardOpenOption.APPEND);   // append

System.out.println(Files.exists(file));       // true
System.out.print(Files.readString(file));
Class / methodUse for
Files.readString / writeStringread or write an entire text file
Files.readAllLines / writetext as a List of lines
Files.readAllBytes / writebinary data
Files.exists / delete / copyfile operations
Path.of(...)build a path

Buffered streams

For large files or line-by-line processing, use buffered streams. They read and write in big chunks instead of one character at a time, which is dramatically faster. Wrap a reader/writer in a Buffered version, and use try-with-resources so it always closes:

Buffered.java
import java.io.*;

// Buffered wrappers read in big chunks — far faster for large files.
// try-with-resources closes the reader automatically.
try (BufferedReader reader = new BufferedReader(new FileReader("data.txt"))) {
    String line;
    while ((line = reader.readLine()) != null) {
        System.out.println(line);
    }
} catch (IOException e) {
    System.out.println("Could not read the file");
}

Tip

For huge files, don't load everything into memory. Files.lines(path) returns a stream of lines you can filter and process lazily — combine it with everything you learned in Module 18.

Directories & file attributes

Files also handles folders: createDirectories, list (one level), and walk (recursive). Because these return streams, close them with try-with-resources:

Directory.java
import java.nio.file.*;
import java.util.stream.Stream;

// list only the regular files in the current directory
try (Stream<Path> files = Files.list(Path.of("."))) {
    files.filter(Files::isRegularFile)
         .forEach(System.out::println);
} catch (IOException e) {
    e.printStackTrace();
}

Serialization

Serialization converts an object into a byte stream (to save or send it), and deserialization rebuilds it. A class opts in by implementing the marker interface Serializable, then ObjectOutputStream/ObjectInputStream do the work.

Handle with care

Java's built-in serialization has real drawbacks: it's brittle across class changes and deserializing untrusted data is a serious security risk (Module 36). For saving and exchanging data, prefer a format like JSON (Module 25) instead.

Recap & quick check

Key takeaways

  • Byte streams (InputStream/OutputStream) for binary; character streams (Reader/Writer) for text.
  • Prefer the modern NIO.2 API: Path for locations, Files for operations.
  • Files.readString/writeString read or write a whole text file in one line; APPEND to add on.
  • Use buffered streams (or Files.lines) for large files and speed.
  • Serialization needs Serializable but is brittle and risky for untrusted data — prefer JSON.

Quick check

1. Which stream family should you use for reading an image file?

2. What is the modern way to read a whole text file?

3. Why use buffered streams?

4. What interface must a class implement to be serialized?

5. For very large files, how do you avoid loading everything into memory?

Great — your programs can now persist and load data. Next up: Module 20 — Regular Expressions.