skip to content

Write a custom Decorator over an existing interface (e.g. a List or an InputStream) that adds behavior, and explain the delegation mechanics.

level: middleimportance: should knowfreq 45%

answer

  1. Same type + held reference + delegate all + augment a few
  2. FilterInputStream = ready-made delegating base for streams
  3. Override both read() and read(byte[],int,int) for a byte counter
  4. Forward EVERY method or behavior leaks (use ForwardingList)
  5. Augment = code around the delegate call

basics

~20 s

Implement the same interface as the thing you wrap, store a reference to it, and forward each method to that reference, adding your extra behavior in the methods you care about. For example, a List wrapper that counts how many times add() is called before delegating to the real list.

solid answer

~50 s

To write a decorator you make a class that implements the component's interface, holds a final reference to the wrapped instance (injected via the constructor), and delegates every method to it. In the methods where you want extra behavior, you add code before or after the delegate call; everything else just forwards unchanged. For an InputStream decorator the JDK gives you FilterInputStream as a ready base whose default behavior is pure delegation, so you only override the methods you change (e.g. count bytes in read()). For a List you typically extend java.util's wrapping helpers or implement List directly. The key mechanic is delegation: the decorator never reimplements the real logic, it forwards to the wrappee and only augments. Because it implements the same interface, callers and other decorators can't tell it apart, so it stays substitutable and stackable. Watch out for forwarding completeness — miss an overload and behavior leaks through inconsistently.

code

java · 26 lines
java
import java.io.*;

// A Decorator over InputStream that counts bytes read.
final class CountingInputStream extends FilterInputStream {
    private long bytesRead = 0;

    CountingInputStream(InputStream in) { super(in); } // stores 'in' field

    long bytesRead() { return bytesRead; }

    @Override public int read() throws IOException {
        int b = super.read();            // delegate to wrapped stream
        if (b != -1) bytesRead++;        // augment
        return b;
    }

    @Override public int read(byte[] buf, int off, int len) throws IOException {
        int n = super.read(buf, off, len); // must also override the bulk read!
        if (n > 0) bytesRead += n;
        return n;
    }
}

// Stacks like any other stream:
// var in = new CountingInputStream(
//              new BufferedInputStream(new FileInputStream("f")));

go deeper

for a junior

Can describe the idea (implement the same interface, hold and forward to the wrapped object) even if not all forwarding details.

for a middle

Writes a working decorator, uses FilterInputStream for streams, and knows to forward all methods and augment the right ones.

for a senior

Handles overload pitfalls (read vs bulk read), recommends delegating base classes to avoid boilerplate bugs, and reasons about identity/equality of wrappers.

for a principal

Weighs hand-rolled decorators vs. dynamic proxies/ForwardingX, considers thread-safety and consistency of the forwarded surface, and sets conventions for composable wrappers.

## Goal Build a wrapper that *is* the same type as what it wraps, forwards work to it, and injects extra behavior — without modifying the wrapped class. ## The four ingredients 1. **Implement the same interface/type** as the component (e.g. `implements List<E>` or `extends InputStream`/`FilterInputStream`). 2. **Hold a reference** to the wrapped instance, usually a `final` field set in the constructor. 3. **Delegate every method** to the wrapped instance. 4. **Augment** the methods you care about (do work before/after the delegate call). ## Example A: counting List decorator ```java import java.util.*; final class CountingList<E> implements List<E> { private final List<E> delegate; // the wrapped component private int addCount = 0; CountingList(List<E> delegate) { this.delegate = Objects.requireNonNull(delegate); } public int addCount() { return addCount; } @Override public boolean add(E e) { // AUGMENTED addCount++; return delegate.add(e); // then delegate } // ----- pure forwarding for the rest ----- @Override public int size() { return delegate.size(); } @Override public boolean isEmpty() { return delegate.isEmpty(); } @Override public E get(int i) { return delegate.get(i); } @Override public Iterator<E> iterator(){ return delegate.iterator(); } // ... and so on for every List method } ``` Usage: `List<String> l = new CountingList<>(new ArrayList<>()); l.add("x");` — `l` is a perfectly normal `List` everywhere, but now counts adds. Note the tedium: `List` has many methods and you must forward them all (this is the classic argument for Guava's `ForwardingList`, which provides the boilerplate). ## Example B: InputStream decorator via FilterInputStream For streams the JDK hands you `FilterInputStream`, the base decorator whose every method already delegates to the wrapped `in`. You override only what you change: ```java import java.io.*; final class CountingInputStream extends FilterInputStream { private long bytesRead = 0; CountingInputStream(InputStream in) { super(in); } // stores 'in' public long bytesRead() { return bytesRead; } @Override public int read() throws IOException { int b = super.read(); // delegate to wrapped stream if (b != -1) bytesRead++; // augment return b; } @Override public int read(byte[] buf, int off, int len) throws IOException { int n = super.read(buf, off, len); if (n > 0) bytesRead += n; return n; } } ``` Because it `extends FilterInputStream` (which `extends InputStream`), it's a drop-in `InputStream` and stacks with the standard wrappers: `new CountingInputStream(new BufferedInputStream(new FileInputStream(f)))`. ## Delegation mechanics — what's actually happening - The decorator does **not** reimplement the real logic; it calls the wrapped object's method (`delegate.add(...)`, `super.read()` which forwards to `in.read()`). - 'Adding behavior' means running code **around** that delegate call. - Because the decorator implements the same type, the compiler and other decorators treat it identically — that's why it's *substitutable* and *stackable*. ## Common pitfalls - **Incomplete forwarding**: implementing `List` directly, you must forward *every* method (including default-overridden ones like `forEach`, `removeIf` if you need consistent behavior). Missing one means inconsistent state. Using a base like `FilterInputStream` or Guava `ForwardingList` avoids this. - **Augmenting only one of several overloads**: `read()` vs `read(byte[], int, int)` — bulk reads often bypass single-byte `read()`, so a byte counter must override both, or the count is wrong. - **Equality/identity**: a decorator is a different object than its delegate; `wrapper.equals(delegate)` is usually false. Be careful if identity matters. - **Final fields / immutability of the link**: keep the delegate reference final so the chain can't be mutated mid-flight. ## Takeaway A custom decorator = same type + held reference + delegate-everything + augment-the-few. Prefer a delegating base class (FilterInputStream, Guava ForwardingX) to avoid forwarding-boilerplate bugs.

  • Why is FilterInputStream helpful when writing an InputStream decorator?
    It's a base decorator whose methods already delegate to the wrapped stream (in), so you only override the few methods you want to change instead of forwarding all of them yourself.
  • What goes wrong if your CountingInputStream overrides only the no-arg read()?
    Code that calls the bulk read(byte[], int, int) — which most readers do for efficiency — bypasses your no-arg read(), so your byte count stays near zero. You must override the bulk overload too.

saying these in an interview costs you the question

  • Reimplementing the wrapped logic instead of delegating to it
  • Forwarding only some methods, leaving inconsistent behavior
  • Counting bytes by overriding only read() and missing the bulk read(byte[]) overload
  • Mutating the delegate reference after construction (keep it final)

context