Back to Structural: Adapter, Decorator, Proxy, Facade
Q3Question 3 of 6

How Java I/O streams exemplify the Decorator pattern and avoid subclass explosion

Key Concepts
  • Each Java stream decorator wraps another stream of the same type and adds one feature, so capabilities are stacked at runtime.
  • The is-a and has-a dual relationship lets a decorator be used anywhere the component is expected, including as the inner of another decorator.
  • `FilterInputStream` is the abstract decorator that holds a `protected volatile InputStream in` reference.
  • Three decorator classes give all combinations instead of eight-plus subclasses, avoiding subclass explosion.
  • Wrapping order matters: buffering a compressed stream differs from compressing a buffered stream.
  • Closing the outermost stream tears down the entire chain because each decorator's `close()` delegates inward.

Definition

Each Java stream decorator wraps another stream of the same type and adds one feature, so you stack capabilities at runtime instead of building a subclass for every combination.
  • Decorator pattern – attaches additional responsibilities to an object dynamically by wrapping it in another object that shares the same interface.
  • Component – the common type, InputStream, that both real sources and decorators implement.
  • Concrete component – a real source like FileInputStream or ByteArrayInputStream.
  • Abstract decorator – FilterInputStream, which extends InputStream and holds a reference to a wrapped InputStream.
  • Concrete decorators – BufferedInputStream, DataInputStream, GZIPInputStream, each adding exactly one capability.
  • Subclass explosion – without decorators, every combination of features requires a separate subclass, growing combinatorially.

Imagine National Bank of India sends a daily statement file. The raw file is plain data. Now you want it buffered for speed, compressed to save space, and read as structured numbers. Instead of inventing a special buffered-compressed-structured machine, you take the plain file and wrap it: first through a buffering layer, then through a decompression layer, then through a structured-reading layer. Each layer is a clip-on attachment that adds one capability and passes the data along. That snap-on-layers idea is the Decorator pattern.

Understanding the Concepts

What is the Decorator pattern in Java I/O?

The Decorator pattern has four roles in Java I/O. The component is InputStream. The concrete component is FileInputStream. The abstract decorator is FilterInputStream, which extends InputStream and holds a protected volatile InputStream in. The concrete decorators are BufferedInputStream, DataInputStream, and GZIPInputStream, each adding one capability.

JAVA
package com.bank.io;

import java.io.*;
import java.util.zip.GZIPInputStream;

public class StatementReader {
    public static void main(String[] args) throws Exception {
        // Stack decorators: file → decompress → buffer → structured read
        try (DataInputStream in = new DataInputStream(
                new BufferedInputStream(
                    new GZIPInputStream(
                        new FileInputStream("statement.gz"))))) {

            String accountId = in.readUTF();
            double balance = in.readDouble();
            System.out.println("Account: " + accountId
                             + ", Balance: " + balance);
        }
    }
}

Why does the is-a and has-a relationship matter?

Each decorator is-a InputStream (because it extends InputStream) and has-a InputStream (because it holds a reference to the wrapped stream). Because it extends InputStream, it can be used anywhere an InputStream is expected, including as the thing another decorator wraps. Because it holds an inner stream, it can delegate the real work and add its own behaviour.

JAVA
// is-a: DataInputStream IS an InputStream
InputStream stream = new DataInputStream(new FileInputStream("data.bin"));

// has-a: DataInputStream HAS an InputStream (the FileInputStream)
// This dual relationship enables unlimited stacking

How does each decorator add exactly one concern?

FileInputStream reads raw bytes from disk. GZIPInputStream transparently decompresses. BufferedInputStream reads a big chunk into a buffer so each small read does not hit the disk. DataInputStream adds methods to read primitive types.

JAVA
package com.bank.io;

import java.io.*;
import java.math.BigDecimal;

public class BalanceFileWriter {
    public static void main(String[] args) throws Exception {
        // Write structured banking data
        try (DataOutputStream out = new DataOutputStream(
                new BufferedOutputStream(
                    new FileOutputStream("balances.dat")))) {
            out.writeUTF("SAV2001");
            out.writeDouble(150000.75);
            out.writeUTF("CUR3002");
            out.writeDouble(89500.00);
        }

        // Read it back with the mirror stack
        try (DataInputStream in = new DataInputStream(
                new BufferedInputStream(
                    new FileInputStream("balances.dat")))) {
            System.out.println(in.readUTF() + ": " + in.readDouble());
            System.out.println(in.readUTF() + ": " + in.readDouble());
        }
    }
}
TEXT
SAV2001: 150000.75
CUR3002: 89500.0

How does the Decorator pattern avoid subclass explosion?

Without decorators, combining features with inheritance would require a subclass for every combination:

  • FileInputStream
  • BufferedFileInputStream
  • GzipFileInputStream
  • DataFileInputStream
  • BufferedGzipFileInputStream
  • BufferedDataFileInputStream
  • GzipDataFileInputStream
  • BufferedGzipDataFileInputStream

With n features the number of subclasses grows as 2^n. The Decorator pattern collapses that: one decorator class per feature, composed at runtime. Three decorator classes give all eight combinations, and adding a fourth feature is one more class, not a doubling of the hierarchy.

JAVA
// Without decorators: 8+ classes for 3 features (bad)
class BufferedGzipDataFileInputStream extends InputStream { ... }

// With decorators: 3 classes, composed freely (good)
new DataInputStream(new BufferedInputStream(new GZIPInputStream(
    new FileInputStream("file.gz"))))

Why does wrapping order matter?

Buffering a compressed stream means you buffer the compressed bytes, reducing disk reads. Compressing a buffered stream means you buffer raw bytes and then compress them. The data flow is different, and performance characteristics change.

JAVA
// Order 1: buffer after decompression (buffer decompressed bytes)
new BufferedInputStream(new GZIPInputStream(new FileInputStream("f.gz")))

// Order 2: buffer before decompression (buffer compressed bytes)
new GZIPInputStream(new BufferedInputStream(new FileInputStream("f.gz")))
// Order 2 is usually better: fewer disk reads of compressed data

How does closing work in a decorator chain?

Closing the outermost stream closes the whole chain. Each decorator's close() method calls close() on its inner stream, which calls close() on its inner stream, and so on down to the source. You only need one try-with-resources on the outer stream.

JAVA
// One try-with-resources closes the entire chain
try (DataInputStream in = new DataInputStream(
        new BufferedInputStream(
            new FileInputStream("statement.dat")))) {
    // read data
} // close() cascades: DataInputStream → BufferedInputStream → FileInputStream

How do you apply the Decorator pattern in banking code?

In a banking notification system, Rajesh can stack logging, encryption, and SMS sending as decorators:

JAVA
package com.bank.decorator;

public interface Notifier {
    void send(String message);
}

public class SmsNotifier implements Notifier {
    @Override
    public void send(String message) {
        System.out.println("SMS to Priya: " + message);
    }
}

public class LoggingDecorator implements Notifier {
    private final Notifier inner;
    public LoggingDecorator(Notifier inner) { this.inner = inner; }

    @Override
    public void send(String message) {
        System.out.println("AUDIT LOG: " + message);
        inner.send(message);
    }
}

public class EncryptionDecorator implements Notifier {
    private final Notifier inner;
    public EncryptionDecorator(Notifier inner) { this.inner = inner; }

    @Override
    public void send(String message) {
        inner.send("ENC[" + message + "]");
    }
}
JAVA
Notifier notifier = new LoggingDecorator(
    new EncryptionDecorator(new SmsNotifier()));
notifier.send("Transfer of INR 50000 completed");
TEXT
AUDIT LOG: Transfer of INR 50000 completed
SMS to Priya: ENC[Transfer of INR 50000 completed]

What are the limitations?

The layered design is powerful but the class hierarchy is large and sometimes confusing for newcomers. Each layer adds a small indirection overhead. And debugging a deeply wrapped chain requires understanding which layer is responsible for what behaviour.

What should a senior engineer know?

The same Decorator pattern appears on the output side with DataOutputStream, BufferedOutputStream, and GZIPOutputStream, and in the character streams with BufferedReader wrapping InputStreamReader. Recognising this in Java I/O immediately tells an interviewer you understand both the library and the pattern.

At National Bank of India, Pankaj uses exactly this approach to read and write compressed, buffered, structured balance-snapshot files, stacking DataOutputStream over BufferedOutputStream over GZIPOutputStream to write, and the mirror stack to read.

JAVA
// Senior: recognise the pattern across all Java I/O
// Input side
new BufferedReader(new InputStreamReader(System.in))
// Output side
new PrintWriter(new BufferedWriter(new FileWriter("log.txt")))
// Both are Decorator chains

Visual Overview

DIAGRAM
  Reading through a Decorator chain
  ─────────────────────────────────
  DataInputStream   ←── reads typed values (readInt, readUTF)
       │
       ▼
  BufferedInputStream ←── buffers for performance
       │
       ▼
  GZIPInputStream    ←── decompresses
       │
       ▼
  FileInputStream    ←── reads raw bytes from disk
       │
       ▼
    [ file.gz ]
DIAGRAM
  Subclass explosion without Decorator
  ────────────────────────────────────
  3 features → 2^3 = 8 subclasses

  With Decorator:
  3 decorator classes → all 8 combinations at runtime

  FileInputStream ──→ GZIPInputStream ──→ BufferedInputStream
        │                                        │
        └────── any subset, any order ───────────┘

Key Takeaway

The Decorator pattern attaches extra responsibilities to an object dynamically by wrapping it in another object that shares the same interface, and Java's I/O stream library is the textbook example. On the input side the common type is InputStream. FileInputStream is the concrete source that reads raw bytes, and decorators like GZIPInputStream, BufferedInputStream, and DataInputStream each wrap another stream, add exactly one capability, and still expose the InputStream interface.

The structural trick is that each decorator is-an InputStream and also has-an InputStream. Because it extends InputStream it can be used anywhere a stream is expected, including as the thing another decorator wraps. Because it holds an inner stream it delegates the real work and adds its own behaviour. That dual relationship lets you compose a DataInputStream wrapping a BufferedInputStream wrapping a GZIPInputStream wrapping a FileInputStream, and reading from the outer stream pulls bytes through the whole chain transparently.

The reason this matters is subclass explosion. Without decorators, combining features with inheritance would require a separate subclass for every combination and ordering, which grows as 2^n. The Decorator pattern collapses that: one class per feature, composed at runtime in whatever order you need. Three decorator classes give every combination, and adding a fourth feature is one more class rather than doubling the hierarchy.

In the banking project at National Bank of India, Rajesh uses exactly this approach to read and write compressed, buffered, structured balance-snapshot files. The two nuances to mention are that wrapping order matters, since buffering a compressed stream differs from compressing a buffered one, and that closing the outermost stream tears down the whole chain because each decorator's close() delegates inward.

Remember one sentence: each Java I/O decorator wraps another stream, adds one feature, and avoids subclass explosion by composing capabilities at runtime.

---

Share
Use ← → arrow keys to navigate