Classic IO (java.io)
InputStream, OutputStream, Reader, Writer and buffering
Classic I/O is the old but still essential Java model for reading and writing bytes and characters. It teaches the difference between binary and text data, the importance of charset selection, and the lifecycle rules behind safe resource handling.
1. Definition
What is classic I/O?
Classic Java I/O is the stream-oriented programming model built around the java.io package.
Its main idea is that data flows through objects that read from a source or write to a destination.
This model is intentionally simple.
It separates the world into two major families:
- byte-oriented APIs for raw data
- character-oriented APIs for text
That distinction is one of the first real engineering boundaries in Java.
If you ignore it, files may be corrupted, characters may become unreadable, and bugs may appear only on some machines.
Why does it still matter?
Modern Java code often uses Path, Files, frameworks, and higher-level libraries.
But many of those abstractions still rely on stream-oriented I/O underneath.
Classic I/O remains important because it explains:
- why text and bytes must be modeled differently
- why explicit charset handling matters
- why buffering changes performance
- why cleanup is part of correctness, not only style
What should a strong interview answer include?
A strong answer connects the API names to real-world decisions.
For example:
- when to use
InputStreaminstead ofReader - why
InputStreamReaderexists - why
BufferedReaderorBufferedOutputStreamoften improves practical performance - when
flush()matters beforeclose() - why
try-with-resourcesis the default cleanup pattern
2. Core Concepts
2.1 Byte streams versus character streams
InputStream and OutputStream work with raw bytes.
They are the correct abstraction for:
- images
- ZIP files
- PDFs
- network payloads
- encrypted or compressed data
Reader and Writer work with characters.
They are the correct abstraction for:
- text files
- CSV
- logs
- configuration files
- console text
If the payload is conceptually text, character APIs are usually clearer.
If the payload is binary, character APIs are often wrong.
2.1.1 Keywords and contracts you should state explicitly here
This topic is incomplete unless the main terms and their contracts are named precisely:
InputStream— reads raw bytes from a sourceOutputStream— writes raw bytes to a destinationReader— reads decoded charactersWriter— writes encoded charactersInputStreamReader— bridges bytes to characters with a charsetOutputStreamWriter— bridges characters to bytes with a charsetBufferedInputStream/BufferedOutputStream— add byte bufferingBufferedReader/BufferedWriter— add character bufferingflush()— pushes buffered output toward the next layerclose()— releases the resource and usually flushes wrappers firstEOF— end-of-file or end-of-stream, often represented by-1IOException— checked failure used by many I/O operationstry-with-resources— automatic cleanup pattern forAutoCloseablecharset— the mapping between bytes and characters- default charset — environment-dependent encoding choice that should not be assumed casually
These are not just glossary words.
They define correctness boundaries.
Choosing the wrong family or the wrong charset can silently corrupt data.
2.2 Bridging bytes and text
Text is stored somewhere as bytes.
That means reading text requires decoding.
Writing text requires encoding.
This is why InputStreamReader and OutputStreamWriter exist.
They connect byte-oriented streams with character-oriented APIs.
The key decision is the charset.
In production-oriented code, that choice should usually be explicit.
Typical default:
StandardCharsets.UTF_8
2.3 Buffering
Buffering reduces the number of expensive I/O calls.
Instead of reading or writing tiny fragments one by one, a buffered wrapper gathers more data per interaction.
That often improves throughput significantly.
Buffering also affects visibility semantics.
On the write side, data may already exist in memory while still not being visible to the file, socket, or receiving process.
That is why flush() matters.
2.4 Lifecycle and failure handling
I/O resources are scarce operating-system-level handles.
If you forget to close them, the application may leak descriptors, buffers, or locks.
The preferred pattern is try-with-resources.
It makes cleanup deterministic.
It also preserves suppressed exceptions when the main block fails and cleanup fails too.
That detail matters in senior-level debugging and exception analysis.
3. Practical Usage
Default practical choices
For binary file copy, choose byte streams.
For text reading, choose Reader-based APIs with explicit UTF-8.
For repetitive I/O, add buffering.
For cleanup, use try-with-resources.
Common use cases
- reading a UTF-8 config file
- copying a PDF or ZIP file
- writing text output to a long-lived stream
- reading console text line by line
- exporting CSV with
BufferedWriter
Decision guide
Use byte streams when:
- the payload is binary
- raw bytes must be preserved exactly
- another layer will decode later
Use character streams when:
- the payload is text
- line-based reading is convenient
- charset handling belongs in this layer
Use buffering when:
- many small reads or writes would otherwise happen
- throughput matters
- you want line helpers like
readLine()
Be especially careful when:
- using
PrintWriter, because write failures are easy to miss - assuming
flush()implies durable persistence - relying on the platform default charset
Interview framing
An interview-ready explanation sounds like this:
“First I decide whether the data is binary or text.
Then I choose the correct stream family.
For text I specify the charset explicitly.
For repeated operations I add buffering.
And I use try-with-resources to make resource cleanup deterministic.”
4. Code Examples
Example 1: Reading UTF-8 text correctly
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadConfigExample {
public static void main(String[] args) throws IOException {
Path path = Path.of("config.txt");
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(Files.newInputStream(path), StandardCharsets.UTF_8))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
}
}
Why this is good:
- it is explicit about text
- it is explicit about UTF-8
- it closes resources safely
- it uses buffering for practical efficiency
Example 2: Copying binary data safely
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class BinaryCopyExample {
public static void main(String[] args) throws IOException {
Path source = Path.of("input.pdf");
Path target = Path.of("copy.pdf");
try (BufferedInputStream in = new BufferedInputStream(Files.newInputStream(source));
BufferedOutputStream out = new BufferedOutputStream(Files.newOutputStream(target))) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buffer)) != -1) {
out.write(buffer, 0, bytesRead);
}
}
}
}
Why this is good:
- it uses byte streams for binary payloads
- it avoids text-decoding mistakes
- it uses bulk operations instead of byte-by-byte loops
- it benefits from buffering
Example 3: Writing text with explicit visibility
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class WriteLogExample {
public static void main(String[] args) throws IOException {
Path path = Path.of("app.log");
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("started");
writer.newLine();
writer.flush();
}
}
}
Why flush() matters here:
- the data becomes visible before final close
- longer-lived writers often need intermediate visibility points
- waiting only for close is not always acceptable
Example 4: Wrong abstraction choice
Do not use Writer for PNG, ZIP, or other binary files.
That would push binary data through character encoding logic.
The result may be corruption.
This is not a small implementation bug.
It is the wrong abstraction.
5. Trade-offs
| Choice | Advantage | Cost or risk |
|---|---|---|
| Byte streams | Exact control over raw data | Text decoding must be handled separately |
| Character streams | Clearer for text | Wrong for binary payloads |
| Buffering | Fewer expensive I/O calls | Visibility may be delayed until flush or close |
| Explicit charset | Portable and deterministic text behavior | Slightly more verbose code |
PrintWriter |
Convenient formatted output | Error reporting can be overlooked |
Practical trade-off analysis
Classic I/O is highly composable.
That flexibility is useful.
It also makes bad combinations easy.
Typical bad choices:
- char APIs for binary data
- default charset in portable systems
- single-byte loops on hot paths
- swallowing
IOException - double-buffering where it adds little value
Another trade-off is abstraction level.
BufferedReader is easier for line-based text.
InputStream is more general and lower-level.
The right choice depends on the semantics of the data, not on habit.
6. Common Mistakes
Mistake 1: Blindly using the default charset
This often works on the developer machine.
It then breaks on another OS, locale, or container image.
Correct approach:
- specify UTF-8 or the required domain charset explicitly
Mistake 2: Treating binary data as text
This causes corruption.
Correct approach:
- keep binary payloads on byte streams
Mistake 3: Forgetting cleanup
This leads to resource leaks and unstable behavior.
Correct approach:
- use
try-with-resources
Mistake 4: Forgetting `flush()` for long-lived outputs
This matters for:
- sockets
- pipes
- interactive command processes
- long-lived writers
Correct approach:
- flush at message or visibility boundaries
Mistake 5: Reading one byte at a time in hot paths
This creates too many expensive calls.
Correct approach:
- use buffering and bulk reads
Mistake 6: Swallowing `IOException`
That kills diagnosability.
Correct approach:
- handle where recovery is meaningful
- otherwise propagate with context
7. Deep Dive
7.1 Why buffering helps
I/O often crosses the boundary between JVM code and operating-system resources.
That boundary is much more expensive than plain in-memory work.
Buffering reduces those crossings.
That is why repeated small reads or writes usually become much faster with buffering.
7.2 `flush()` is not the same as durable persistence
flush() usually means “push the data out of this Java-level buffer to the next layer”.
It does not mean the physical storage device has durably persisted the bytes.
That distinction matters in discussions about logging, networking, and storage guarantees.
7.3 Decorator-style design
Classic I/O is wrapper-oriented.
Examples:
BufferedInputStream(new FileInputStream(...))BufferedReader(new InputStreamReader(..., UTF_8))
This is decorator-style design.
Each layer adds behavior without changing the underlying source or destination concept.
7.4 `mark()` and `reset()`
Some input streams and readers support revisiting previously read data.
That is the role of mark() and reset().
Support is not universal.
If this matters, check markSupported().
7.5 Suppressed exceptions and cleanup
With try-with-resources, the primary exception from the main block stays primary.
Failures during cleanup become suppressed exceptions.
That preserves more diagnostic context than naive finally-based cleanup that overwrites the original failure.
8. Interview Questions
1. Why do `Reader` and `Writer` exist if `InputStream` and `OutputStream` already handle data?
Because text requires decoding and encoding.
Characters have meaning that raw bytes do not.
2. When is `flush()` mandatory?
When output visibility matters before close.
Typical examples are sockets, pipes, interactive protocols, and long-lived writers.
3. Why is the default charset dangerous?
Because it depends on the runtime environment.
The same file may decode differently on another machine.
4. Why is `BufferedReader.readLine()` useful?
It gives a convenient line-based text API combined with buffering.
5. Why not use `Reader` for binary files?
Because the character layer applies decoding rules and may corrupt non-text data.
6. Why is `try-with-resources` better than manual close in finally?
It is shorter, safer, and preserves suppressed exceptions correctly.
7. Does `close()` make `flush()` unnecessary?
Often close does flush wrappers.
But if you need earlier visibility while the resource stays open, you still need flush().
8. What is a common production bug in this area?
Charset mismatch is one of the most common subtle data-corruption bugs.
9. Why are buffered wrappers often the default practical choice?
They reduce expensive I/O calls and usually improve throughput with little code cost.
10. What contract does EOF communicate?
It tells the caller that normal reading has reached the end of the available data.
9. Glossary
| Term | Meaning |
|---|---|
| stream | An abstraction for sequential data flow |
| byte stream | I/O abstraction for raw bytes |
| character stream | I/O abstraction for decoded text |
| charset | Mapping between bytes and characters |
| decoder | Converts bytes to characters |
| encoder | Converts characters to bytes |
| buffer | Temporary memory used to batch operations |
| flush | Push buffered output onward |
| close | Release the resource and usually flush wrappers |
| EOF | End of file or end of stream |
| blocking I/O | The caller waits for the operation to progress or complete |
| decorator | Wrapper-based design that adds behavior layer by layer |
10. Cheatsheet
- Binary data →
InputStream/OutputStream - Text data →
Reader/Writer - Text over bytes →
InputStreamReader/OutputStreamWriter - Text should usually specify
StandardCharsets.UTF_8 - Repeated small operations should usually be buffered
read(...)returns a count or-1at EOFflush()is about visibility, not necessarily durabilitytry-with-resourcesis the default cleanup pattern- Avoid char APIs for binary payloads
- Avoid default charset in portable text handling
- Avoid swallowing
IOException - Mention buffering, charset, cleanup, and binary-vs-text split in interviews
🎮 Games
10 questions