🧬 Inheritance & Polymorphism · Intermediate

Composition over inheritance in Java

has-a vs is-a, fragile base class problem.

🧩 The mysteryYou extend HashSet to count how many items get added. Add 3 items with addAll — your counter says 6. You didn't write a bug... your parent class did something you never saw.

is-a vs has-a

Inheritance models *is-a*: a Cat is an Animal. Composition models *has-a*: a Car has an Engine. With composition, a class keeps a reference to another object in a field and delegates work to it.

Delegation

The Car doesn't *become* an engine — it holds one and forwards calls. It exposes only the methods it chooses.

class Car {
    private final Engine engine;
    Car(Engine e) { engine = e; }
    void start() { engine.start(); }
}
🤔 Think first

The counting mystery

CountingSet extends HashSet overrides add (added++) and addAll (added += c.size()), both then calling super. After addAll of 3 items, added is 6. Why?

Think about it, then reveal the answer

HashSet's inherited addAll works by **calling add for each element** — and add is now your override. So each item is counted once in addAll and again in add. Your subclass silently depended on a private implementation detail of its parent.

The fragile base class

That's the fragile base class problem: subclasses depend on how the parent works *inside*. Change the parent's internals — even without changing its API — and subclasses can break. A wrapper that *holds* a Set and forwards calls is immune.

Building a Stack

✗ Inherits too much
class Stack extends ArrayList<Integer> {
    void push(int x) { add(x); }
}
// stack.add(0, 99) breaks order!

Every public ArrayList method comes along for the ride.

✓ Composition
class Stack {
    private final List<Integer> items =
        new ArrayList<>();
    void push(int x) { items.add(x); }
    int pop() {
        return items.removeLast();
    }
}

Exposes only push and pop. The rules are safe.

Swap parts at runtime

A delegate is just a field, so you can inject or replace it — give a Car an ElectricEngine in a test or at runtime. A superclass, by contrast, is fixed forever when the class is compiled.

💼 In the real world

In real projects

The JDK has its own cautionary tales: java.util.Stack extends Vector (so you can insert into the middle of a "stack"), and Properties extends Hashtable. Modern advice: use ArrayDeque for stacks and prefer composition by default.

Key takeaways

  1. is-a: inheritance; has-a: composition
  2. Subclasses depend on parent implementation details
  3. Composition exposes only the methods you choose
  4. Delegates can be swapped at runtime
🤯 Did you know?

The counting puzzle is a classic from Joshua Bloch's *Effective Java*, where the class is called InstrumentedHashSet.

Practice questions

Which pair should be modeled with composition rather than inheritance?

  1. Car and Engine
  2. Cat and Animal
  3. Circle and Shape
  4. SavingsAccount and Account
Check your answer

Car and Engine. A car has an engine; it isn't a kind of engine. The others are genuine is-a relationships.

CountingSet extends HashSet and overrides both add (added++) and addAll (added += c.size()), each then calling super. After addAll of 3 items, added is 6. Why?

class CountingSet extends HashSet<String> {
    int added;
    public boolean add(String s) {
        added++; return super.add(s);
    }
    // addAll: added += c.size();
    //         return super.addAll(c);
}
  1. HashSet's inherited addAll calls add for each element, so every item is counted twice
  2. addAll is called twice by the JVM
  3. HashSet stores each element twice
  4. added is static and shared
Check your answer

HashSet's inherited addAll calls add for each element, so every item is counted twice. The subclass silently depended on an implementation detail of its parent: whether addAll uses add. That's the fragile base class problem; a wrapper that holds a Set avoids it.

Next: a Square is a Rectangle... right? Liskov says: not so fast.