AlgoInsight

Free lesson · Java Basic To Advance

Introduction to File I/O

Medium6 min readjava

Two Eras of Java I/O

Olena Marchenko picking up Module 13. File I/O is one of the most-used features in any application — reading config files, writing logs, processing data files, handling user uploads. Java's file I/O has evolved significantly over time, and modern code looks very different from code written even ten years ago.

This module covers both eras. The legacy I/O (java.io package, since Java 1.0) — Streams, Readers, Writers — is still everywhere in existing codebases and frameworks. The modern I/O (java.nio.file, since Java 7) — Path, Files class, the NIO.2 API — is what you should write today.

Knowing both matters. Modern code uses the new APIs. Legacy code still uses the old ones. Real Java developers move between them daily.

The Problem File I/O Solves

Programs need to interact with the file system. Read configuration. Write logs. Process data files. Store state. Handle uploads. Cache results.

Without good I/O abstractions, every program would have to wrestle with platform-specific file handling, encoding issues, buffer management, and resource cleanup. Java's I/O APIs hide much of this complexity.

The trade-off: Java has TWO complete I/O frameworks. They overlap but have different strengths. Knowing when to use which is part of senior Java fluency.

The Two Frameworks at a Glance

java.io (the original, Java 1.0):

java.nio.file (modern, Java 7+):

The modern API isn't a replacement — both still work. But modern code starts with java.nio.file. Legacy classes are still around because changing existing code is expensive, and the legacy API works fine for simple cases.

A Quick Comparison

Reading a small text file the OLD way:

BufferedReader reader = null;
try {
    reader = new BufferedReader(new FileReader("data.txt"));
    StringBuilder content = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        content.append(line).append("\n");
    }
    return content.toString();
} catch (IOException e) {
    throw new RuntimeException("Read failed", e);
} finally {
    if (reader != null) {
        try { reader.close(); } catch (IOException e) {}
    }
}

The same operation the MODERN way:

return Files.readString(Paths.get("data.txt"));

One line vs sixteen. The modern API is dramatically more concise for common operations. Behind the scenes, Files.readString does the same kind of buffered reading, but the boilerplate is gone.

For most common operations, the modern API is the right answer. The legacy API is for specialized cases or when working with libraries that expect it.

When You Use Each

Use modern API (java.nio.file) when:

Use legacy API (java.io) when:

In practice, most application code uses the modern API for file operations and the legacy API only when interfacing with Stream-based libraries. Frameworks like Spring often have helper classes that bridge both worlds.

The Three Data Types

Java I/O distinguishes three kinds of data:

1. Bytes (binary data).

Images, executables, compressed files, network protocols — anything where the bytes are meaningful in themselves.

2. Characters (text data).

Text files, log files, JSON, XML, source code — anything where you care about character encoding (UTF-8, ASCII, etc.).

3. Lines (text data, line-by-line).

Common for log files, CSV-like data, configuration files.

The distinction matters because character data needs to know its encoding (the mapping between bytes and characters). Modern API defaults to UTF-8. Legacy API uses the platform default — which is unreliable across systems.

Character Encoding: A Subtle Trap

A common bug in Java I/O is encoding mismatch:

// Bad — uses platform default encoding (varies by system)
BufferedReader reader = new BufferedReader(new FileReader("data.txt"));

// Good — explicit UTF-8
BufferedReader reader = new BufferedReader(
    new FileReader("data.txt", StandardCharsets.UTF_8));

// Modern way — UTF-8 by default
String content = Files.readString(Paths.get("data.txt"));

A file written on a Mac (UTF-8 default) might be read incorrectly on Windows (Windows-1252 default). Characters with accents, currency symbols, emojis all break. The legacy default-encoding behavior is a famous source of "works on my machine" bugs.

Always specify UTF-8 explicitly for character I/O. The modern Files methods default to UTF-8, which is one of the reasons to prefer them.

What This Module Covers

Module 13 covers file I/O in depth:

After this module, you'll be able to read and write files of any type, navigate the file system, work with binary and character data, and handle the edge cases that catch out junior developers.

A Mental Model

Think of Java I/O as three layers:

Layer 3: High-level operations (Files.readString, Files.lines)
   ↓
Layer 2: Streams and Readers (InputStream, Reader)
   ↓
Layer 1: Bytes on disk

For common operations, work at Layer 3. The modern Files class handles details. When you need fine control (large files, custom processing, special encoding), drop to Layer 2. Layer 1 is what's actually happening but rarely what you write code against.

This is similar to how you use ArrayList for most collections work and only drop to arrays when performance matters. Pick the right abstraction level for the task.

graph TD
    A["Java File I/O"] --> B["java.io<br/>(legacy, Java 1.0+)"]
    A --> C["java.nio.file<br/>(modern, Java 7+)"]

    B --> B1["File class"]
    B --> B2["InputStream/OutputStream<br/>(binary)"]
    B --> B3["Reader/Writer<br/>(character)"]

    C --> C1["Path interface"]
    C --> C2["Files utility class"]
    C --> C3["FileSystem"]

    D["Recommendation"] --> E["Write new code<br/>with java.nio.file"]
    D --> F["Use java.io for<br/>existing code & specific cases"]

    style B fill:#f59e0b,color:#fff
    style C fill:#10b981,color:#fff
This diagram shows Java's two file I/O frameworks. The java.io package has been around since Java 1.0 — it uses the File class, InputStream and OutputStream for binary data, and Reader and Writer for character data. The java.nio.file package was introduced in Java 7 — it uses the Path interface for file paths, the Files utility class for operations, and FileSystem for working with different file systems. For new code, prefer java.nio.file — it's more concise, has better defaults like UTF-8 encoding, and exposes more features like file watching and attributes. The legacy java.io is still everywhere in existing code and frameworks like servlets, so you need to know both. They interoperate fine — you can convert between Path and File, and the modern Files class can return InputStreams when needed.

A Word on Performance

For most application code, the modern API's performance is fine. Files.readString() is buffered internally — you don't lose efficiency by using the convenient method.

When performance matters (large files, high throughput), you might want:

We cover buffered I/O in Topic 13.8 and production patterns in Topic 13.11. For typical code, start with the simple modern API and only optimize if measurement shows a real problem.

Common Pitfalls Before We Dive In

Warning: Mixing legacy and modern APIs unnecessarily. Pick one for new code. Mixing makes code harder to follow.

Using default encoding. Always specify UTF-8 explicitly (or use modern API which defaults to UTF-8).

Forgetting to close streams. Old API requires manual close. Always use try-with-resources.

Reading huge files into memory. Files.readString() loads the whole file. For large files, stream with Files.lines().

Hardcoding path separators. Use / (works on all platforms in Java) or Path.of("dir", "subdir", "file.txt").

Why This Matters in Interviews

Interview: File I/O questions appear in many Java interviews. "How do you read a file in Java?" Asks for fluency with current best practices. Senior questions probe edge cases: encoding, large files, exception handling.

Topic 13.12 covers the Interview Lens.

Try It

Exercise: Explore each:

  1. Read a small text file using Files.readString(Paths.get("file.txt")). Note how short the code is.
  2. Read the same file using the legacy BufferedReader + FileReader approach. Compare the code.
  3. Write a file using Files.writeString(). Open it and verify.
  4. Create a text file with non-ASCII characters (accents, emojis). Read it with UTF-8 vs default encoding. Note differences.
  5. List the files in a directory using Files.list(Paths.get(".")).

Each exercise builds intuition for what's ahead in this module.

Back to Java Basic To Advance