Java 9: Composition over Inheritance
easy⏱ 5 mincoursejava
The fragile base class
Classic bug: CountingSet extends HashSet overrides add to increment a counter, also overrides addAll to iterate and add each element. It works — until HashSet's internal addAll already calls add per element. Now every addAll double-counts. The parent's implementation detail (not its contract) leaked into the child. You don't own the parent class — it can change.
Composition + delegation
Wrap instead of extend. CountingSet has a Set<E> inner field and implements Set<E> by delegating every method to inner, except add which also bumps the counter. Now the parent's internals can't leak — you only touch the contracted Set interface. This is also how Collections.unmodifiableXxx and Collections.synchronizedXxx are built.
public class CountingSet<E> implements Set<E> {
private final Set<E> inner;
private int addCount = 0;
public CountingSet(Set<E> inner) { this.inner = inner; }
@Override public boolean add(E e) { addCount++; return inner.add(e); }
@Override public boolean addAll(Collection<? extends E> c) {
for (E e : c) add(e); return true;
}
// ... delegate every other Set method to inner
}
When inheritance is still right
Use inheritance when: (1) You own the parent class, (2) There's a true 'is-a' relationship (a Square is a Rectangle — but even this breaks LSP if Rectangle has setWidth!), (3) The parent was designed for extension (documented hooks, protected extension points, sealed hierarchies).
Build a composition-based CountingList
Create a CountingList<T> that wraps an inner T[]-backed list and implements add, addAll, get, size, and exposes addCount. Show that addAll([1,2,3]) counts exactly 3 (not 6 — the bug) because you delegate to the inner push and not to your own add.
Forwarding classes make delegation cheap
Writing 20 delegation methods by hand is tedious. Effective Java calls this a forwarding class — often generated by IDE or annotation processors. Guava's ForwardingList gives you a skeletal forwarder; you override just the methods that need custom behavior.
Quiz: Why prefer composition?
Name three benefits. (1) You're immune to parent changes — only the interface matters. (2) You can swap inner implementation at runtime (wrap a different set). (3) Test doubles — inject a mock inner for unit tests without subclassing.