skip to content

Why is java.io considered a textbook example of the Decorator design pattern? Explain how stream wrapping illustrates it.

level: middleimportance: should knowfreq 62%

answer

  1. same interface + holds wrapped + delegates
  2. FilterInputStream = abstract decorator base
  3. avoids 2^n subclass explosion
  4. open/closed principle
  5. read constructor inside-out

basics

~20 s

Each stream class wraps another stream of the same type and adds one behavior (buffering, data conversion, etc.). Because the wrapper has the same interface as what it wraps, you can stack wrappers in any order to combine features without subclassing every combination.

solid answer

~40 s

The Decorator pattern lets you add responsibilities to an object at runtime by wrapping it in another object that shares its interface and forwards calls to it. java.io is the canonical example: InputStream is the common interface; FileInputStream/ByteArrayInputStream are the concrete sources; and BufferedInputStream, DataInputStream, GZIPInputStream, etc. are decorators that each take an InputStream in their constructor, hold a reference to it, and override read() to add behavior (buffering, reading primitives, decompression) before delegating to the wrapped stream. Because every decorator is itself an InputStream, you can compose them: new DataInputStream(new BufferedInputStream(new FileInputStream(f))). This avoids a combinatorial explosion of subclasses (you do not need a BufferedGzipFileInputStream class); instead you mix small, single-responsibility layers. The downside is many small wrapper objects and sometimes confusing stack traces.

code

java · 9 lines
java
// Compose decorators; read the constructor inside-out.
try (DataInputStream in =
         new DataInputStream(          // adds readInt/readUTF
             new BufferedInputStream(  // adds buffering
                 new GZIPInputStream(  // adds decompression
                     new FileInputStream("data.gz"))))) {
    int n = in.readInt();             // works through all layers
    String s = in.readUTF();
}  // closing the outermost propagates close() down the whole chain

go deeper

for a junior

Recognizes that streams wrap other streams and that you can stack them, even if the formal pattern roles are fuzzy.

for a middle

Names the Component/Decorator/ConcreteDecorator roles, maps FilterInputStream and BufferedInputStream onto them, and explains the subclass-explosion motivation.

for a senior

Discusses open/closed, why wrap order matters, the closing-propagation contract, and contrasts Decorator with Adapter/Proxy.

for a principal

Critiques the design (object churn, debuggability) and can weigh it against alternatives like the NIO channel/ByteBuffer model when designing new I/O APIs.

## The Decorator pattern in plain terms The **Decorator pattern** is a way to add new behavior to an object *without* changing its class and *without* subclassing every variation. You do it by putting the object inside a 'wrapper' object that: 1. **implements the same interface** as the thing it wraps, and 2. **holds a reference** to that wrapped thing, and 3. **forwards** (delegates) calls to it, optionally doing extra work before or after. Because the wrapper has the same type, callers cannot tell the difference — they just see 'an InputStream' — and wrappers can wrap other wrappers, stacking behavior. ## The pattern's roles, mapped onto java.io - **Component (the common interface):** the abstract class `InputStream` (or `OutputStream`, `Reader`, `Writer`). It declares `read()`. - **Concrete component (the real source):** `FileInputStream`, `ByteArrayInputStream`, a socket's input stream — these actually produce bytes from somewhere. - **Decorator (abstract base for wrappers):** `FilterInputStream`. It holds a protected field `protected volatile InputStream in;` and its default `read()` just calls `in.read()` — pure delegation, no added behavior. It exists so concrete decorators only override what they change. - **Concrete decorators:** `BufferedInputStream` (adds an internal buffer), `DataInputStream` (adds `readInt()`, `readUTF()`, etc.), `GZIPInputStream` / `InflaterInputStream` (adds on-the-fly decompression), `CheckedInputStream` (computes a checksum as bytes flow through). Each extends `FilterInputStream`, takes an `InputStream` in its constructor, and overrides methods to add its one responsibility. ## Why this is the *textbook* example The combination of (a) a shared abstract type, (b) wrappers that take that type in their constructor and store it, and (c) wrappers that are themselves of that type so they nest — is the Decorator pattern's exact shape. Java's own designers acknowledge it; it appears in nearly every design-patterns course. ## What problem it actually solves: the subclass explosion Imagine you wanted buffering, decompression, and primitive-reading without decorators. With subclassing you would need a class for every combination: `BufferedFileInputStream`, `GzipFileInputStream`, `BufferedGzipFileInputStream`, `BufferedGzipDataFileInputStream`... For *n* independent features that is up to 2^n classes. Decorators give you *n* small classes that you compose at runtime in whatever order you need. This is the open/closed principle in action — open to new behavior (write a new decorator), closed to modification (existing classes untouched). ## Composition example, read inside-out ``` new DataInputStream( // 3) lets me call readInt(), readUTF() new BufferedInputStream( // 2) batches the syscalls new GZIPInputStream( // 1') decompresses new FileInputStream(f)))) // 0) the raw bytes on disk ``` Data flows from the innermost source outward; each layer transforms or augments. Order matters: buffering the *compressed* bytes vs the *decompressed* bytes is a real performance/semantics choice. ## Trade-offs and gotchas - **Many small objects** and an extra method call per layer (negligible vs I/O cost, but real). - **Stack traces and debugging** can be confusing because of the nesting. - **Closing:** you close the *outermost* stream; close() propagates down the chain, so you never close the inner ones yourself. - **Identity:** because the wrapper is a different object, `instanceof` checks against a specific concrete inner type fail — callers should depend on the interface. ## Relationship to other patterns Decorator has the same structure as the Proxy and Adapter patterns (all wrap an object) but a different *intent*: Decorator adds responsibilities, Adapter changes the interface, Proxy controls access. java.io decorators keep the interface and add behavior, so it is squarely Decorator.

  • Which java.io class plays the 'abstract decorator' role for input?
    FilterInputStream — it stores a protected InputStream 'in' and delegates every method to it by default, so concrete decorators override only what they add.
  • How is Decorator different from Adapter, given both wrap an object?
    Adapter changes the interface to make incompatible types work together; Decorator keeps the same interface and adds behavior. Intent differs even though both wrap.

saying these in an interview costs you the question

  • Confusing Decorator with Adapter (Adapter changes the interface; Decorator keeps it)
  • Saying you must subclass for each feature combination — that's exactly what decorators avoid
  • Closing inner streams manually instead of just the outermost
  • Claiming order of wrapping never matters (it affects what gets buffered/compressed)

context