Java I/O Class Hierarchy
Fast recall
Quick revision
Core answer
InputStream and OutputStream for bytes, plus Reader and Writer for characters. Concrete sources and destinations are combined with wrapper classes that add buffering, conversion, or higher-level operations.Interview-ready guide
Interview answer
The classic java.io hierarchy has four main roots. InputStream reads bytes, OutputStream writes bytes, Reader reads characters, and Writer writes characters. These abstract classes provide common operations, while subclasses decide where the data comes from, where it goes, or what extra behavior is added.
Endpoint classes connect to real data. FileInputStream and FileOutputStream use files, ByteArrayInputStream and ByteArrayOutputStream use byte arrays, and StringReader and StringWriter use text in memory. Wrapper classes add features: buffered classes reduce small accesses, data streams handle primitive values, and PrintWriter adds formatted text output.
For example, a UTF-8 file can use the chain BufferedReader → InputStreamReader → InputStream. The input stream supplies bytes, InputStreamReader decodes them with UTF-8, and BufferedReader adds efficient character and line reading. OutputStreamWriter performs the opposite bridge from characters to bytes.
Inheritance tells whether the data is bytes or characters and whether it flows in or out. Composition chooses the endpoint, charset conversion, buffering, and formatting. Code normally reads through and closes the outermost owned wrapper because standard wrappers close the stream below them. If an API only lends a stream, however, the caller must follow that API's ownership rule instead of closing it automatically.
Java I/O Class Hierarchy
Direct Answer
The classic Java I/O hierarchy has four abstract roots: InputStream and OutputStream for bytes, plus Reader and Writer for characters. Concrete sources and destinations are combined with wrapper classes that add buffering, conversion, or higher-level operations.
Deep Dive & Architecture
A method that accepts InputStream can consume a file, socket, byte array, decompressor, or a test double because every source follows the same read contract. That polymorphism is the value of the abstract roots.
Wrappers let behavior be stacked without creating a class for every combination. A buffer can wrap a file source or a network source; a data decoder can sit above either. The ordering matters because each layer sees the representation supplied by the layer beneath it.
Convenience factories such as Files.newBufferedReader can assemble a common pipeline without exposing every object, but the same family and ownership rules still apply.
Code Example
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
class IoHierarchy {
public static void main(String[] args) throws Exception {
Path file = Files.createTempFile("hierarchy-", ".txt");
Files.writeString(file, "first line\nsecond line", StandardCharsets.UTF_8);
try (var input = Files.newInputStream(file);
var decoder = new InputStreamReader(input, StandardCharsets.UTF_8);
var reader = new BufferedReader(decoder)) {
System.out.println(reader.readLine());
} finally {
Files.deleteIfExists(file);
}
}
}
Check your understanding
Answer these without looking back at the article.
- What are the four abstract roots in classic Java I/O?
- Why are buffered streams wrappers rather than separate endpoints?
- Which classes bridge bytes and characters?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.