34. Java I/O Streams
Table of Contents
- 34.1 What Is an IO Stream in Java
- 34.2 Byte Streams vs Character Streams
- 34.3 Low-Level vs High-Level Streams
- 34.3.1 Low-Level Streams Node-Streams
- 34.3.2 Common Low-Level Streams
- 34.3.3 High-Level Streams Filter--Processing-Streams
- 34.3.4 Common High-Level Streams
- 34.3.5 Stream Chaining Rules and Common Errors
- 34.3.5.1 Fundamental Chaining Rule
- 34.3.5.2 Byte vs Character Stream Incompatibility
- 34.3.5.3 Invalid Chaining Compile-Time-Error
- 34.3.5.4 Bridging Byte Streams to Character Streams
- 34.3.5.5 Correct Conversion Pattern
- 34.3.5.6 Ordering Rules in Stream Chains
- 34.3.5.7 Correct Logical Order
- 34.3.5.8 Resource Management Rule
- 34.3.5.9 Common Pitfalls
- 34.4 Core javaio Base Classes and Key Methods
- 34.5 Buffered Streams and Performance
- 34.6 java io vs java nio and java nio file
- 34.7 When to Use Which API
- 34.8 Common Traps and Tips
This chapter provides a detailed explanation of Java I/O Streams.
It covers classic java.io streams, contrasts them with java.nio / java.nio.file, and explains design principles, APIs, edge cases, and relevant distinctions.
34.1 What Is an I/O Stream in Java?
An I/O Stream represents a flow of data between a Java program and an external source or destination.
The data flows sequentially, like water in a pipe.
- A stream is not a data structure; it does not store data permanently
- Streams are unidirectional (input OR output)
- Streams abstract the underlying source (file, network, memory, device)
- Streams operate in a blocking, synchronous manner (classic I/O)
In Java, streams are organized around two major dimensions:
Direction: Input vs OutputData type: Byte vs Character
34.2 Byte Streams vs Character Streams
Java distinguishes streams based on the unit of data they process.
34.2.1 Byte Streams
- Work with raw 8-bit bytes
- Used for binary data (images, audio, PDFs, ZIPs)
- Base classes:
InputStreamandOutputStream
34.2.2 Character Streams
- Work with 16-bit Unicode characters
- Handle character encoding automatically
- Base classes:
ReaderandWriter
34.2.3 Summary Table
| Aspect | Byte Streams | Character Streams |
|---|---|---|
Unit of data |
byte (8 bits) | char (16 bits) |
Encoding handling |
None | Yes (Charset aware) |
Base classes |
InputStream / OutputStream | Reader / Writer |
Typical usage |
Binary files | Text files |
Focus |
Low-level I/O | Text processing |
34.3 Low-Level vs High-Level Streams
Streams in java.io follow a decorator pattern. Streams are stacked to add functionality.
34.3.1 Low-Level Streams (Node Streams)
Low-level streams connect directly to a data source or sink.
- They know how to read/write bytes or characters
- They do NOT provide buffering, formatting, or object handling
34.3.2 Common Low-Level Streams
| Stream Class | Purpose |
|---|---|
FileInputStream |
Read bytes from file |
FileOutputStream |
Write bytes to file |
FileReader |
Read characters from file |
FileWriter |
Write characters to file |
- Example: Low-Level Byte Stream
try (InputStream in = new FileInputStream("data.bin")) {
int b;
while ((b = in.read()) != -1) {
System.out.println(b);
}
}
Note
Low-level streams are rarely used alone in real applications due to poor performance and limited features.
34.3.3 High-Level Streams (Filter / Processing Streams)
High-level streams wrap other streams to add functionality.
- Buffering
- Data type conversion
- Object serialization
- Primitive reading/writing
34.3.4 Common High-Level Streams
| Stream Class | Adds Functionality |
|---|---|
BufferedInputStream |
Buffering |
BufferedReader |
Line-based reading |
DataInputStream |
Primitive types |
ObjectInputStream |
Object serialization |
PrintWriter |
Formatted text output |
- Example: Stream Chaining
try (BufferedReader reader =
new BufferedReader(
new InputStreamReader(
new FileInputStream("text.txt")))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
34.3.5 Stream Chaining Rules and Common Errors
The previous example illustrates stream chaining, a core concept in java.io based on the decorator pattern.
Each stream wraps another stream, adding functionality while preserving a strict type hierarchy.
34.3.5.1 Fundamental Chaining Rule
A stream can only wrap another stream of a compatible abstraction level.
- Byte streams can only wrap byte streams
- Character streams can only wrap character streams
- High-level streams require an underlying low-level stream
Note
You cannot arbitrarily mix InputStream with Reader or OutputStream with Writer.
34.3.5.2 Byte vs Character Stream Incompatibility
A very common error is attempting to wrap a byte stream directly with a character-based class (or vice versa).
34.3.5.3 Invalid Chaining (Compile-Time Error)
BufferedReader reader =
new BufferedReader(new FileInputStream("text.txt"));
Note
This fails because BufferedReader expects a Reader, not an InputStream.
34.3.5.4 Bridging Byte Streams to Character Streams
To convert between byte-based and character-based streams, Java provides bridge classes that perform explicit charset decoding/encoding.
InputStreamReaderconverts bytes → charactersOutputStreamWriterconverts characters → bytes
34.3.5.5 Correct Conversion Pattern
BufferedReader reader =
new BufferedReader(
new InputStreamReader(new FileInputStream("text.txt")));
Note
The bridge handles character decoding using a charset (default or explicit).
34.3.5.6 Ordering Rules in Stream Chains
The order of wrapping is not arbitrary.
- Low-level stream must be innermost
- Bridges (if needed) come next
- Buffered or processing streams come last
34.3.5.7 Correct Logical Order
FileInputStream → InputStreamReader → BufferedReader
34.3.5.8 Resource Management Rule
Closing the outermost stream automatically closes all wrapped streams.
Note
This is why try-with-resources should reference only the highest-level stream.
34.3.5.9 Common Pitfalls
- Trying to buffer a stream of the wrong type
- Forgetting the bridge between byte and char streams
- Assuming
Readerworks with binary data - Using default charset unintentionally
- Closing inner streams manually (risking double-close):
close()on the outer wrapper is enough and recommended
34.4 Core java.io Base Classes and Key Methods
The java.io package is built around a small set of abstract base classes.
Understanding these classes and their contracts is essential, because all concrete I/O classes build on them.
34.4.1 InputStream
Abstract base class for byte-oriented input. All input streams read raw bytes (8-bit values) from a source such as a file, network socket, or memory buffer.
34.4.1.1 Key Methods
| Method | Description |
|---|---|
int read() |
Reads one byte (0–255); returns -1 at end of stream |
int read(byte[]) |
Reads bytes into buffer; returns number of bytes read or -1 |
int read(byte[], int, int) |
Reads up to length bytes into a buffer slice |
int available() |
Bytes readable without blocking (hint, not guarantee) |
void close() |
Releases the underlying resource |
Note
The read() methods are blocking by default.
They suspend the calling thread until data is available, end-of-stream is reached, or an I/O error occurs.
The single-byte read() method is primarily a low-level primitive.
In practice, reading one byte at a time is inefficient and should almost always be avoided in favor of buffered reads.
34.4.1.2 Typical Usage Example
try (InputStream in = new FileInputStream("data.bin")) {
byte[] buffer = new byte[1024];
int count;
while ((count = in.read(buffer)) != -1) {
// process buffer[0..count-1]
}
}
34.4.2 OutputStream
Abstract base class for byte-oriented output.
It represents a destination where raw bytes can be written.
34.4.2.1 Key Methods
| Method | Description |
|---|---|
void write(int b) |
Writes the low 8 bits of the integer |
void write(byte[]) |
Writes an entire byte array |
void write(byte[], int, int) |
Writes a slice of a byte array |
void flush() |
Forces buffered data to be written |
void close() |
Flushes and releases the resource |
Note
Calling close() implicitly calls flush().
Failing to flush or close an OutputStream may result in data loss.
34.4.2.2 Typical Usage Example
try (OutputStream out = new FileOutputStream("out.bin")) {
out.write(new byte[] {1, 2, 3, 4});
out.flush();
}
34.4.3 Reader and Writer
Reader and Writer are the character-oriented counterparts of InputStream and OutputStream.
They operate on 16-bit Unicode characters instead of raw bytes.
| Class | Direction | Character-based | Encoding aware |
|---|---|---|---|
Reader |
Input | Yes | Yes |
Writer |
Output | Yes | Yes |
Readers and Writers always involve a charset, either explicitly or implicitly.
This makes them the correct abstraction for text processing.
34.4.3.1 Charset Handling
Reader reader = new InputStreamReader(
new FileInputStream("file.txt"),
StandardCharsets.UTF_8
);
Note
InputStreamReader and OutputStreamWriter are bridge classes.
They convert between byte streams and character streams using a charset.
34.5 Buffered Streams and Performance
Buffered streams wrap another stream and add an in-memory buffer.
Instead of interacting with the operating system on every read or write, data is accumulated in memory and transferred in larger chunks.
BufferedInputStream/BufferedOutputStreamfor byte streamsBufferedReader/BufferedWriterfor character streams
Note
Buffered streams are decorators: they do not replace the underlying stream, they enhance it by adding buffering behavior.
34.5.1 Why Buffering Matters
| Aspect | Unbuffered | Buffered |
|---|---|---|
System calls |
Frequent | Reduced |
Performance |
Poor | High |
Memory usage |
Minimal | Slightly higher |
System calls are expensive operations.
Buffering minimizes them by grouping multiple logical reads or writes into fewer physical I/O operations.
34.5.2 How Unbuffered Reading Works
In an unbuffered stream, each call to read() may result in a native system call.
This is especially inefficient when reading large amounts of data.
try (InputStream in = new FileInputStream("data.bin")) {
int b;
while ((b = in.read()) != -1) {
// each read() may trigger a system call
}
}
Note
Reading byte-by-byte without buffering is almost always a performance anti-pattern.
34.5.3 How BufferedInputStream Works
BufferedInputStream internally reads a large block of bytes into a buffer.
Subsequent read() calls are served directly from memory until the buffer is empty.
try (InputStream in =
new BufferedInputStream(new FileInputStream("data.bin"))) {
int b;
while ((b = in.read()) != -1) {
// most reads are served from memory, not the OS
}
}
Note
The program still calls read() repeatedly, but the operating system is accessed only when the internal buffer needs refilling.
34.5.4 Buffered Output Example
Buffered output accumulates data in memory and writes it in larger chunks.
The flush() operation forces the buffer to be written immediately.
try (OutputStream out =
new BufferedOutputStream(new FileOutputStream("out.bin"))) {
for (int i = 0; i < 1_000; i++) {
out.write(i);
}
out.flush(); // forces buffered data to disk
}
Note
close() automatically calls flush().
Calling flush() explicitly is useful when data must be visible immediately.
34.5.5 BufferedReader vs Reader
BufferedReader adds efficient **line-based reading** on top of a Reader.
Without buffering, each character read may involve a system call.
try (BufferedReader reader =
new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
Note
The readLine() method is only available on BufferedReader (not Reader), because it relies on buffering to efficiently detect line boundaries.
34.5.6 BufferedWriter Example
try (BufferedWriter writer =
new BufferedWriter(new FileWriter("file.txt"))) {
writer.write("Hello");
writer.newLine();
writer.write("World");
}
BufferedWriter minimizes disk access and provides convenience methods such as newLine().
Note
Always wrap file streams with buffering unless there is a strong reason not to
Prefer BufferedReader / BufferedWriter for text
Prefer BufferedInputStream / BufferedOutputStream for binary data
34.6 java.io vs java.nio (and java.nio.file)
Modern Java applications increasingly favor NIO and NIO.2 APIs, but java.io remains fundamental and widely used.
34.6.1 Conceptual Differences
| Aspect | java.io | java.nio / nio.2 |
|---|---|---|
Programming model |
Stream-based | Buffer / Channel-based |
Blocking I/O |
blocking by default | Non-blocking capable |
File API |
File | Path + Files |
Scalability |
Limited | High |
Introduced |
Java 1.0 | Java 4 / Java 7 |
Note
java.nio does not replace java.io.
Many NIO classes internally rely on streams or coexist with them.
34.6.2 java.nio (Modern File I/O)
The java.nio.file package (NIO.2) provides a high-level, expressive, and safer file API.
It is the preferred approach for file operations in Java 11+.
Example: Reading a File (NIO)
Path path = Path.of("file.txt");
List<String> lines = Files.readAllLines(path);
Equivalent java.io Code
try (BufferedReader reader = new BufferedReader(new FileReader("file.txt"))) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
}
34.7 When to Use Which API
| Scenario | Recommended API |
|---|---|
Simple file read/write |
java.nio.file.Files |
Binary streaming |
InputStream / OutputStream |
Character text processing |
Reader / Writer |
High-performance servers |
java.nio.channels |
Legacy APIs |
java.io |
34.8 Common Traps and Tips
- End-of-file is indicated by -1, not by an exception
- Closing a wrapper stream closes the wrapped stream automatically
BufferedReader.readLine()strips line separatorsInputStreamReaderalways involves a charset- Files utility methods throw checked IOException
available()must not be used to detect EOF
Note
Most I/O bugs come from incorrect assumptions about blocking, buffering, or character encoding.