Topic 5.12
Composition over Inheritance
In one line
Composition means building a class by holding other objects in fields and delegating work to them (has-a), instead of inheriting from them (is-a). It avoids the fragile base class problem and lets behaviour change at run time, so prefer it unless the relationship is a true, stable is-a.
Think of it like this
Building with LEGO versus being born into a family. A LEGO car *has* wheels, *has* an engine block and *has* a seat. If you want bigger wheels, you unclip them and clip on new ones; the car doesn't change its identity. Inheritance is like being born into a family: you get everything, good and bad, and you can't swap your parents later. Composition is the LEGO way.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Composition
- Building a class from other objects stored in its fields (a has-a relationship).
- Delegation
- Passing a call on to another object to do the work:
engine.ignite()insidecar.start(). - has-a relationship
- One object owns or uses another. A car has an engine. Modelled with a field, not
extends. - Fragile base class problem
- Changes inside a parent class silently break subclasses that depended on its internal behaviour.
- Forwarding (wrapper) class
- A class that implements an interface by passing every call to an inner object of that interface, adding behaviour where needed.
- Strategy pattern
- Putting a swappable algorithm behind an interface and holding it in a field so it can be changed.
- Decorator pattern
- Wrapping an object in another object with the same interface to add behaviour, like adding counting or logging.
- Liskov Substitution Principle
- A subtype must be usable anywhere its parent type is expected, without breaking correctness.
Step by step
01The fragile base class, in one example
You want a HashSet that counts how many elements were ever added. Inheritance looks natural: extend HashSet, override add and addAll to increment a counter, then call super.
Add three elements with addAll and the count says 6. Why? HashSet.addAll is inherited from AbstractCollection, which implements it by calling add for each element. Your addAll adds 3, then super.addAll calls *your* overridden add three times (dynamic dispatch on this, Topic 5.4), adding 3 more.
Nothing in HashSet's public API told you that addAll calls add. It's an implementation detail, and a future JDK could change it, fixing or breaking your class again.
02The fix: wrap, don't extend
Instead of *being* a HashSet, the counting class *has* a Set in a private field and forwards calls to it. Its addAll adds c.size() to the counter and calls inner.addAll(c). Whatever the inner set does internally, it calls *its own* add, not ours, because we're a separate object.
A full version implements Set<E> and forwards all methods (Effective Java calls it a ForwardingSet). Then it can wrap any Set and be used anywhere a Set is expected: a Decorator.
Extending ForwardingSet is safe: we wrote it, and its addAll does not call add.
class ForwardingSet<E> implements Set<E> {
private final Set<E> s;
ForwardingSet(Set<E> s) { this.s = s; }
public boolean add(E e) { return s.add(e); }
public boolean addAll(Collection<? extends E> c) { return s.addAll(c); }
public int size() { return s.size(); }
public boolean contains(Object o) { return s.contains(o); }
// ... every other Set method forwards the same way
}
class CountingSet<E> extends ForwardingSet<E> { // extends our own, stable wrapper
private int addCount;
CountingSet(Set<E> s) { super(s); }
@Override public boolean add(E e) { addCount++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c);
}
int addCount() { return addCount; }
}03Composition for swappable behaviour: Strategy
Model ducks with inheritance and you get MallardDuck, RubberDuck, DecoyDuck, each overriding fly(). Rubber ducks can't fly, so they override with an empty method; then wooden ducks too; then a rocket-powered duck needs a new subclass. Behaviours get copied between unrelated branches.
With composition, Duck holds a FlyBehavior field. Flying styles are small classes (FlyWithWings, NoFly, RocketFly), mixed and matched freely, and even changed at run time with a setter. See the System Design course's Strategy pattern (/topic/phase-2/strategy) and Decorator pattern (/topic/phase-2/decorator).
04The JDK's own mistakes
java.util.Stack extends Vector. A stack should only allow push, pop and peek at the top. Because it inherits all of Vector's methods, you can call stack.add(0, x) or stack.remove(1) and corrupt the stack's order. The Javadoc now recommends ArrayDeque instead.
java.util.Properties extends Hashtable<Object, Object>. You're meant to store String keys and values, but put accepts anything, and getProperty returns null for non-String values. Both would have been safer as classes that *contain* a Vector or Hashtable.
05When inheritance is still the right tool
Ask: is every B really an A, in every situation, forever? Does B need *all* of A's API, and nothing in A's API is wrong for B? Is A designed and documented for extension (or under your own control)?
If all three are yes, extends is fine: IOException extends Exception, ArrayList extends AbstractList, your SavingsAccount extends Account. If any is no, or you only want to reuse some code, compose.
This is the Liskov Substitution Principle from the System Design course's SOLID topic (/topic/phase-1/solid): a Stack that lets you insert in the middle can't safely be used wherever a Vector is, and a Vector can't be used wherever a Stack is.
Try it yourself
- 1
Add a logging decorator
Write
class LoggingFly implements FlyBehaviorthat holds anotherFlyBehaviorand returns"(log) " + inner.fly(). Give the mallardnew LoggingFly(new FlyWithWings()). You've just written a Decorator. - 2
Fix the broken version two ways
In the InstrumentedHashSet example, delete the
addAlloverride. The count is now 3, but only because of HashSet's internal behaviour. Explain why this 'fix' is still fragile, then compare with the CountingSet version.
Code & diagrams
Expected output
attempts = 3, size = 3
attempts = 4, size = 3
[apple, fig, pear] attempts = 3Expected output
Mallard flaps its wings
Rubber duck can't fly
Rubber duck zooms on a rocketDeque's toString shows the top of the stack first; Stack (a Vector) shows the bottom first.
Expected output
Stack contents: [99, 1, 3]
pop: 3
Deque contents: [3, 2, 1]
pop: 3The bug lives in the gap between HashSet's public API and its private implementation.
Expected output
expected 3, got 6Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Count adds by extending HashSet
Override both add and addAll in a HashSet subclass and add three elements with addAll.
class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
@Override public boolean add(E e) { addCount++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); }
}Break #2
Corrupt a Stack through inherited methods
Call Vector's add(index, e) on a java.util.Stack.
Myth vs fact
Myth
Composition over inheritance means never use inheritance.
Fact
It means prefer composition when you mainly want reuse. True, stable is-a relationships with parents designed for extension are fine.
Myth
Composition needs lots of boilerplate, so it's not worth it.
Fact
You forward only the methods you need, and a forwarding class is written once and reused. IDEs generate delegate methods automatically.
Myth
Overriding only the method you care about is safe.
Fact
Other parent methods may call (or stop calling) your overridden method. The InstrumentedHashSet bug comes from exactly this.
When it breaks
Library upgrade changes a parent's internal call pattern
What you see
A subclass of a third-party class (e.g. a custom HashMap or HTTP client subclass that overrides one method) starts double-counting, skipping work, or recursing infinitely after a minor library upgrade, with no compile error.
Fix & prevent
Replace the subclass with a wrapper that holds the library object and delegates. Pin behaviour with tests that assert counts and call order. Only extend classes documented for extension (@implSpec).
A class used for reuse leaks inappropriate methods
What you see
Callers use inherited methods that break invariants (like inserting into the middle of a Stack, or putting a non-String into Properties), causing corrupted state far from the cause.
Fix & prevent
Hide the reused class behind a field and expose only the operations that make sense. For existing code, make the wrapper the public type and deprecate the leaking one.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Composition has one known weakness: the self problem. A wrapped object doesn't know about its wrapper, so if it passes
thisto a callback (e.g. registers itself as a listener), callers get the inner object and bypass the wrapper's added behaviour. - ▸
Performance: delegation adds a call per forwarded method, but the JIT inlines small forwarding methods aggressively, so the cost is usually zero after warm-up. The memory cost is one extra object header and reference per wrapper.
- ▸
Java's collections are built for composition:
Collections.unmodifiableList,synchronizedList, andcheckedListare all wrappers (decorators) around anyList. Reading their source is a quick lesson in the pattern. - ▸
Frameworks use composition plus interfaces for extension: Spring beans get collaborators injected rather than inheriting from base classes, which is why Spring's own docs recommend constructor injection of interfaces. See the System Design course's Dependency Injection topic (
/topic/phase-2/dependency-injection).
Remember this
- 1
Composition = a class holds references to other objects in fields and calls them to do parts of its work (delegation). A
Carhas anEngine;car.start()callsengine.ignite(). The relationship is has-a, not is-a. - 2
Why it's often better: inheritance exposes the parent's implementation, not just its interface. A subclass that overrides methods depends on *how* the parent calls its own methods internally. If that changes in the next version, the subclass breaks without any change of its own. This is the fragile base class problem, and the classic example (shown below) is a counting
HashSetsubclass that counts double. - 3
Composition only depends on the other object's public contract. The wrapper decides exactly which methods to expose, can't be broken by the inner object's private call patterns, and can wrap any implementation of an interface, so one
CountingSetworks with HashSet, TreeSet or anything else. - 4
Composition lets behaviour change at run time. A
Duckthat holds aFlyBehaviorfield can swap it for aRocketFlyobject later. ADucksubclass hierarchy can't change class after construction. This is the Strategy pattern, and wrapping an object to add behaviour while keeping its interface is the Decorator pattern. - 5
The JDK has famous inheritance mistakes:
Stack extends Vector(so a 'stack' lets you insert in the middle) andProperties extends Hashtable(so you can put non-String values that breakgetProperty). PreferArrayDequefor stacks. - 6
Inheritance is still right when the subclass truly is a specialised version of the parent, both are under your control (same package or module), and the parent was designed and documented for extension. Liskov's rule must hold: a subclass object must work everywhere a parent object does.
Explain it without notes
What is the difference between composition and inheritance? Give an example of each.
Explain the fragile base class problem using the counting HashSet example.
How does composition enable changing behaviour at run time?
When is inheritance the right choice?
Practice
Write a Car that has an Engine interface field with PetrolEngine and ElectricEngine implementations. car.start() delegates to the engine. Swap the engine at run time.
Write a decorator UpperCaseWriter around an interface Writer { String write(String s); } and stack two decorators: one upper-cases, one adds !.
Rewrite class Stack2 extends ArrayList<Integer> as a class that composes an ArrayDeque<Integer> and exposes only push, pop, peek and isEmpty.
Trade-offs
- ↔
Composition is more robust and flexible, but you write forwarding methods and the wrapper is a different type from the inner class unless both implement a shared interface.
- ↔
Inheritance is shorter for true is-a relationships and gives polymorphism for free, but couples you to the parent's implementation and fixes behaviour at construction.
- ↔
Decorators stack nicely, but deep stacks make debugging harder (many small objects, long stack traces). Keep wrapper chains short and name them clearly.
Done when you can
Done when you can explain has-a vs is-a and pick one for a given design.
Done when you can explain and reproduce the counting-HashSet fragile base class bug.
Done when you can write a forwarding/decorator class around an interface.
Done when you can implement Strategy with a swappable field.
Done when you can name the JDK's Stack and Properties mistakes and the better alternatives.