Interview Prep Zoneby CuriouserLabs

Question 2 of 5 · senior level

Why prefer composition over inheritance? When is inheritance actually the right tool?

🎤 Say this first

Implementation inheritance couples you to a superclass's implementation details across the strongest possible boundary: the fragile base class problem (self-use changes break subclasses — the classic HashSet.addAll calls add trap), single inheritance limits, and equals/symmetry issues. Composition + interfaces keeps coupling at the contract level, composes multiple behaviors, and swaps at runtime. Inheritance earns its keep only for true is-a with LSP intact, in stable hierarchies you control — frameworks' template methods, sealed algebraic hierarchies.

The full picture

The fragile base class, live
class CountingSet<E> extends HashSet<E> {
    private int adds = 0;
    @Override public boolean add(E e) { adds++; return super.add(e); }
    @Override public boolean addAll(Collection<? extends E> c) {
        adds += c.size(); return super.addAll(c);   // HashSet.addAll calls add()…
    }                                               // -> every element counted TWICE
}

// Composition (forwarding) is immune to the superclass's self-use:
class CountingSet<E> implements Set<E> {
    private final Set<E> inner;                    // delegate
    private int adds = 0;
    public boolean add(E e) { adds++; return inner.add(e); }
    public boolean addAll(Collection<? extends E> c) {
        int before = adds; boolean r = false;
        for (E e : c) r |= add(e);                 // OUR contract, our counting
        return r;
    }
    // …forward the rest
}
  • Ask 'is-a and substitutable-under-contract?' — if a Square extends Rectangle breaks setWidth expectations, it's not LSP-is-a.
  • Inheritance across module/team boundaries is a versioning trap: superclass evolves, subclasses in other repos break silently. Design for it (abstract hooks, documented self-use) or prohibit it (final, sealed).
  • Modern Java nudges composition: records can't extend classes; sealed interfaces + pattern matching model closed variants without deep trees; default methods let interfaces carry shared behavior.

🔄 Likely follow-up questions

  • How do sealed classes change the composition/inheritance calculus?
  • What is the decorator pattern if not composition winning over inheritance?
  • Why does the Liskov 'square/rectangle' fail — precondition or postcondition violation?