The I/O stream model
Byte vs character streams, the decorator design, and why you wrap streams.
A stream is a one-way flow of data
Java models all input/output as streams: an ordered sequence of data flowing in one direction. An *input* stream is something you read FROM (a file, a socket, a byte array); an *output* stream is something you write TO. Every stream is read or written sequentially, one chunk at a time, from start to end.
The java.io package splits streams along two axes:
- Direction — input vs output.
- Unit — raw bytes vs Unicode characters.
That gives four abstract base classes you must keep straight:
| | Input | Output |
|---|---|---|
| Bytes (8-bit) | InputStream | OutputStream |
| Characters (16-bit Unicode) | Reader | Writer |
The byte classes (...Stream) move raw bytes — use them for images, serialized objects, or any binary format. The character classes (Reader/Writer) move chars and handle the byte↔character *encoding* (UTF-8, etc.) for you — use them for text.
The decorator pattern: wrap to add capability
A bare FileInputStream can only read bytes one at a time — slow, and with no convenience methods. Rather than build one giant class with every feature, java.io uses the Decorator pattern: you start with a *source* stream and wrap it in layers, each adding behaviour.
// source: bytes from a file
FileInputStream raw = new FileInputStream("data.txt");
// decorator 1: bytes -> characters (decode with the platform charset)
InputStreamReader chars = new InputStreamReader(raw);
// decorator 2: buffer + give a readLine() convenience
BufferedReader in = new BufferedReader(chars);
String firstLine = in.readLine();
Each constructor takes the stream it decorates. The outermost object is the one you actually use. The classic decorators:
- BufferedInputStream / BufferedReader — add an in-memory buffer so reads hit disk in big chunks, not one byte/char at a time. Always buffer file I/O.
- InputStreamReader / OutputStreamWriter — the *bridge* between the byte world and the character world; they apply a charset.
- DataInputStream / DataOutputStream — read/write Java primitives (readInt(), writeDouble()) in a portable binary format.
- ObjectInputStream / ObjectOutputStream — read/write whole objects (serialization — lesson 8.4).
Streams must be closed
An open stream holds an OS file handle. Leaking handles eventually exhausts the process. Worse, an output stream's buffer is only guaranteed to reach disk when you flush() or close() it — close late and you can lose the last buffered bytes. The modern rule: open streams in a try-with-resources block (lesson 3.3) so they close automatically, even on exception.