JAVA / INHERITANCE AND POLYMORPHISM
Composition as an alternative to inheritance
Replace fragile subclassing with a class that holds a collaborator in a private field, forwards calls to it, and adds behavior around them.
What you will learn
- Replace a regretted `extends` with a private final field plus forwarding methods.
- Implement the same interface as the delegate so callers accept the wrapper.
- Spot the self-use bug where a parent method calls an override you did not expect.
- Swap a collaborator at runtime with a setter, which `extends` can never do.
Understanding Composition as an alternative to inheritance
`extends` hands you the parent's whole public surface, but it also ties you to the parent's inside: when a parent method calls another method on the same object, your override is pulled into that internal call chain whether you wanted it or not. Composition takes the opposite deal. You keep a reference to the other object in a private field and call it like any other collaborator, so you depend only on what its public methods promise, never on the order in which it calls itself.
Delegation is the mechanical half of the pattern: your class implements the same interface as the object it holds, and each method forwards to the field with your own work added before or after the forwarding call. Because the wrapper satisfies that interface, callers pass it wherever the original was accepted and cannot tell which one they are holding. Because it depends on the interface rather than one concrete class, a single counting wrapper instruments every implementation, present and future, while inheritance would need one subclass per parent class.
The price is the forwarding methods you have to type, plus one genuine behavioral difference: the delegate holds no reference back to the wrapper, so any call the delegate makes on itself stays inside the delegate. That is precisely why a wrapper cannot double count, and also why a wrapper cannot hook into a delegate's internal steps the way an override can. Choose `extends` when you own the parent, the is-a relationship is real, and the parent documents which of its own methods it calls; wrap in every other case, especially for classes from a library you do not control.
import java.util.ArrayList;
import java.util.List;
interface Sink {
void write(String line);
}
class ConsoleSink implements Sink {
@Override
public void write(String line) {
System.out.println("out: " + line);
}
}
class MemorySink implements Sink {
private final List<String> lines = new ArrayList<>();
@Override
public void write(String line) {
lines.add(line);
}
List<String> lines() {
return lines;
}
}
// CountingSink HAS-A Sink. It does not extend one.
class CountingSink implements Sink {
private final Sink delegate;
private int writes;
CountingSink(Sink delegate) {
this.delegate = delegate;
}
@Override
public void write(String line) {
writes++;
delegate.write(line);
}
int writes() {
return writes;
}
}
public class Main {
public static void main(String[] args) {
CountingSink counted = new CountingSink(new ConsoleSink());
counted.write("a");
counted.write("b");
System.out.println("writes: " + counted.writes());
MemorySink memory = new MemorySink();
CountingSink countedMemory = new CountingSink(memory);
countedMemory.write("x");
System.out.println("stored: " + memory.lines() + " writes: " + countedMemory.writes());
}
}A class that holds another object and forwards work to it depends only on that object's public contract, whereas a subclass also depends on how the parent is written inside.
Worked examples
Why extends breaks: the parent calls your override
A counting subclass reports the wrong number because the parent's addAll is written in terms of add.
import java.util.ArrayList;
import java.util.List;
class Basket {
private final List<String> items = new ArrayList<>();
public void add(String item) {
items.add(item);
}
public void addAll(List<String> more) {
for (String m : more) {
add(m);
}
}
public int size() {
return items.size();
}
}
class CountingBasket extends Basket {
private int added;
@Override
public void add(String item) {
added++;
super.add(item);
}
@Override
public void addAll(List<String> more) {
added += more.size();
super.addAll(more);
}
public int added() {
return added;
}
}
public class Main {
public static void main(String[] args) {
CountingBasket basket = new CountingBasket();
basket.addAll(List.of("pen", "book", "cup"));
System.out.println("size: " + basket.size());
System.out.println("counted: " + basket.added());
}
}Example explained
Line 1`CountingBasket.addAll` adds 3 to the counter before delegating upward.
Line 2`super.addAll` loops and calls `add(m)`, and dynamic dispatch sends each call to the override, adding 3 more.
Line 3`size()` prints 3, so the storage is fine and only the subclass's bookkeeping is wrong.
Line 4Deleting the `addAll` override fixes the number today but breaks again if `Basket` ever stops calling `add` internally.
Swapping the collaborator at runtime
Holding a strategy in a field lets one object change behavior after construction, which extends cannot express.
interface Rate {
double fee(double amount);
}
class FlatRate implements Rate {
@Override
public double fee(double amount) {
return 2.0;
}
}
class PercentRate implements Rate {
private final double percent;
PercentRate(double percent) {
this.percent = percent;
}
@Override
public double fee(double amount) {
return amount * percent / 100;
}
}
class Account {
private Rate rate;
Account(Rate rate) {
this.rate = rate;
}
void setRate(Rate rate) {
this.rate = rate;
}
double charge(double amount) {
return amount + rate.fee(amount);
}
}
public class Main {
public static void main(String[] args) {
Account account = new Account(new FlatRate());
System.out.println("flat: " + account.charge(100));
account.setRate(new PercentRate(5));
System.out.println("percent: " + account.charge(100));
}
}Example explained
Line 1`Account` holds a `Rate` instead of extending one, so the fee rule is data the object carries, not part of its class identity.
Line 2`setRate` replaces the rule on a live object; an `Account` subclass with a built-in flat fee would be frozen at `new`.
Line 3`charge` is written once against the `Rate` interface, so adding a third fee rule needs no change inside `Account`.
Line 4`fee(100)` returns 2.0 and then 5.0, which is why the identical `charge(100)` call prints 102.0 and then 105.0.
Important notes
A wrapper is not a subtype of the delegate's class: code that demands `ArrayList` or tests `instanceof ArrayList` will reject it, so share an interface and type parameters and fields to that interface.
Forwarding looks like boilerplate, and the extra object and extra call are almost always inlined by the JIT, so decide between wrapping and extending on design grounds rather than on speed.
Common mistakes
Writing `class Stack extends ArrayList` just to reuse the storage: callers now legally call `clear()`, `remove(0)` and `add(index, x)` on your stack, and no invariant you write can stop them.
Keeping the delegate but publishing it with `getList()` or `getSink()`: callers mutate the inner object directly and the wrapper's counting or validation silently never runs.
Assuming the delegate's internal calls will reach your override after you convert a subclass into a wrapper: they will not, so per-element hooks that used to fire on `addAll` stop firing and the numbers quietly change.
Making the field non-final and reassigning it from several methods, which turns a simple wrapper into an object whose behavior depends on call order.
Try it yourself
Change, predict, then run
Extend the main example with an `UpperCaseSink` that implements `Sink`, holds another `Sink`, and forwards `line.toUpperCase()` to it. Wire `new CountingSink(new UpperCaseSink(new ConsoleSink()))`, then swap the two wrappers and check that the printed text is uppercase either way while each `write` call still counts exactly once.
Open the Java workspaceCheck your understanding
`LoggingList` holds a `List` in a private field, implements `List` itself, prints one line inside its own `add`, and implements `addAll` by forwarding straight to `delegate.addAll(items)`. A caller runs `loggingList.addAll(List.of("a", "b"))`. How many lines get printed?
- Two: addAll is implemented in terms of add, so the wrapper's add runs once per element
- One: the wrapper logs a single line for the whole addAll call
- Zero: any add calls made while the delegate runs are calls on the delegate itself, never on the wrapper
- Two, but only when the delegate is an ArrayList; other List implementations would log zero
Show answer
Forwarding hands the entire operation to the delegate, and the delegate has no reference back to the wrapper, so nothing it does internally can dispatch to `LoggingList.add`. The "two, one per element" option describes what inheritance would do: if `LoggingList` extended a list class whose `addAll` looped over `add`, `super.addAll` would run on the same object and re-enter the override. If you want one line per element from a wrapper, its `addAll` must loop and call its own `add`.