29. Composition Over Inheritance
Favour composition (has-a) over inheritance (is-a) to avoid brittle base-class coupling; delegation wraps an object and forwards calls, giving you flexibility to change the implementation without breaking the class hierarchy.
Java 21 (core language)Explanation
The "fragile base class" problem: changes to a superclass can silently break subclasses even without touching them โ because subclasses depend on implementation details of the parent that are not part of the public contract.
Composition means your class has-a collaborator object (a field) instead of is-a subtype of it. You delegate calls to the collaborator. This lets you swap implementations (use a different collaborator), avoid inheriting unwanted methods, and combine multiple behaviours without the restrictions of single inheritance.
Inheritance is still appropriate when: the is-a relationship is genuine and stable, you're within the same team's codebase (so fragile-base-class risk is controlled), and you specifically want to expose the full parent API. Favour composition for third-party base classes or when only a subset of the parent API is needed.
Example
import java.util.ArrayList;
import java.util.List;
public class CompositionDemo {
// Problem: inheriting from ArrayList exposes all 40+ list methods
static class InstrumentedListBAD<E> extends ArrayList<E> {
private int addCount = 0;
@Override public boolean add(E e) { addCount++; return super.add(e); }
@Override public boolean addAll(java.util.Collection<? extends E> c) {
addCount += c.size(); return super.addAll(c); // BUG: super.addAll calls add() again!
}
int getAddCount() { return addCount; }
}
// Solution: compose โ wrap the list, forward only the operations we need
static class InstrumentedList<E> {
private final List<E> delegate;
private int addCount = 0;
InstrumentedList(List<E> list) { this.delegate = list; }
public boolean add(E e) { addCount++; return delegate.add(e); }
public boolean addAll(java.util.Collection<? extends E> c) {
addCount += c.size(); return delegate.addAll(c); // no double-count!
}
public int size() { return delegate.size(); }
public int getAddCount() { return addCount; }
@Override public String toString() { return delegate.toString(); }
}
public static void main(String[] args) {
// BAD (inheritance): double-counts due to super.addAll calling add()
InstrumentedListBAD<String> bad = new InstrumentedListBAD<>();
bad.addAll(List.of("a", "b", "c"));
System.out.println("BAD addCount: " + bad.getAddCount()); // 6! (should be 3)
// GOOD (composition): counts correctly
InstrumentedList<String> good = new InstrumentedList<>(new ArrayList<>());
good.addAll(List.of("a", "b", "c"));
System.out.println("GOOD addCount: " + good.getAddCount()); // 3 โ
}
}Sources
- https://www.pearson.com/en-us/subject-catalog/p/effective-java/P200000000138 โ Effective Java, Item 18
- https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html โ Subclasses tutorial