Stage 2: Object-oriented programming, lesson 9 of 15

Why prefer composition over inheritance?

Intermediate3 min read@since 9Code runs on your Java 25
Explain it forThe essentials plus production detail and pitfalls.

There are two ways to reuse code:

  • Inheritance (IS-A): class Car extends Vehicle. The child gets the parent's code, but is tightly bound to it.
  • Composition (HAS-A): class Car { private final Engine engine; }. The class holds other objects and forwards work to them.

Why composition usually wins:

  1. The fragile base class problem. A child depends on how the parent works inside. When the parent changes (or calls its own methods in unexpected ways), children break, as the example below shows.
  2. You inherit everything, even methods that make no sense for the child. The JDK's own Stack extends Vector lets you insert into the middle of a stack.
  3. Only one parent. Inheritance uses up your one extends; composition lets you combine many parts.
  4. Flexibility at run time. Parts can be swapped (a different PaymentGateway, a fake in tests); a parent class is fixed at compile time.

Use inheritance when the child truly is a special kind of the parent and the parent was designed for extension (framework base classes, exception hierarchies, sealed type hierarchies). Otherwise, compose.

Example

Java
// Inheritance: looks right, counts wrong
class CountingSet<E> extends HashSet<E> {
    int added = 0;

    @Override public boolean add(E e) { added++; return super.add(e); }

    @Override public boolean addAll(Collection<? extends E> c) {
        added += c.size();
        return super.addAll(c);            // HashSet.addAll() calls add() for each element...
    }
}

CountingSet<String> set = new CountingSet<>();
set.addAll(List.of("a", "b", "c"));
System.out.println(set.added);             // 6, not 3: every element was counted twice
The same idea with composition: correct, and immune to HashSet's internals
class CountingSet<E> {
    private final Set<E> set = new HashSet<>();   // HAS-A: we use a set, we aren't one
    private int added = 0;

    public boolean add(E e) { added++; return set.add(e); }

    public boolean addAll(Collection<? extends E> c) {
        added += c.size();
        return set.addAll(c);                     // whatever HashSet does inside can't affect our count
    }

    public boolean contains(Object o) { return set.contains(o); }
    public int added() { return added; }
}
Composition in everyday Spring code
@Service
class OrderService {
    private final PaymentGateway payments;   // swap Razorpay for another gateway, or a fake in tests
    private final Notifier notifier;

    OrderService(PaymentGateway payments, Notifier notifier) {   // constructor injection = composition
        this.payments = payments;
        this.notifier = notifier;
    }
}

Common mistake

Extending a class just to reuse a few of its methods. You inherit its whole public API and every future change to its internals.

Under the hood

This example comes from Joshua Bloch's Effective Java (item "Favor composition over inheritance"). Many design patterns are composition in disguise: Strategy (swap an algorithm object), Decorator (new BufferedReader(new InputStreamReader(in)) wraps behaviour around another object) and Delegation. A good default is to make classes final unless you deliberately design and document them for extension.

Check yourself

CountingSet extends HashSet and counts in both add() and addAll(). After addAll(List.of("a","b","c")), what is the count?

How this connects

Part of OOP in practice.

Was this lesson helpful?

Finished reading? Mark it complete to track your progress.