Free lesson · Java Basic To Advance
Introduction to File I/O
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):
Fileclass represents files and directoriesInputStream/OutputStreamfor binary dataReader/Writerfor character dataFileReader,FileWriter,BufferedReader, etc.- Many small specialized classes
java.nio.file (modern, Java 7+):
Pathinterface represents file system pathsFilesutility class for operationsFileSystemfor working with file systems- More consistent, fewer classes, more methods
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:
- Writing new code
- Doing common operations (read file, write file, list directory)
- You want concise, exception-safe code
- Working with file attributes, symlinks, file watching
Use legacy API (java.io) when:
- Maintaining existing code that uses it
- Working with frameworks that expect Streams/Readers (servlets, many libraries)
- You need fine-grained control over byte-level operations
- Streaming operations where memory matters
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.
- Legacy:
InputStream,OutputStream - Modern:
Files.readAllBytes(),Files.write()
2. Characters (text data).
Text files, log files, JSON, XML, source code — anything where you care about character encoding (UTF-8, ASCII, etc.).
- Legacy:
Reader,Writer - Modern:
Files.readString(),Files.writeString(),Files.lines()
3. Lines (text data, line-by-line).
Common for log files, CSV-like data, configuration files.
- Legacy:
BufferedReader.readLine() - Modern:
Files.readAllLines(),Files.lines()(Stream)
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:
- Topic 13.2 — The Path API (the foundation of modern file I/O)
- Topic 13.3 — The Files utility class
- Topic 13.4 — Reading files (modern approaches)
- Topic 13.5 — Writing files (modern approaches)
- Topic 13.6 — Streams: InputStream and OutputStream (legacy binary)
- Topic 13.7 — Readers and Writers (legacy character I/O)
- Topic 13.8 — Buffered I/O (performance optimization)
- Topic 13.9 — Working with directories
- Topic 13.10 — File attributes and metadata
- Topic 13.11 — Production I/O patterns (Emeka guest-authors this Topic)
- Topic 13.12 — Signature Interview Lens
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:
- Streaming with
Files.lines()(returns a Stream, processes lazily) - Buffered I/O explicitly with
BufferedReader/BufferedInputStream - Memory-mapped files with
FileChannel.map()for very large files - NIO Channels for high-performance I/O
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 withFiles.lines().Hardcoding path separators. Use
/(works on all platforms in Java) orPath.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:
- Read a small text file using
Files.readString(Paths.get("file.txt")). Note how short the code is.- Read the same file using the legacy
BufferedReader+FileReaderapproach. Compare the code.- Write a file using
Files.writeString(). Open it and verify.- Create a text file with non-ASCII characters (accents, emojis). Read it with UTF-8 vs default encoding. Note differences.
- List the files in a directory using
Files.list(Paths.get(".")).Each exercise builds intuition for what's ahead in this module.