Implementation inheritance violates encapsulation — the subclass becomes coupled to internal details of the superclass that the superclass never contracted to preserve. The relationship is "white-box" reuse: the subclass sees through the superclass, depends on its self-use patterns, and breaks when those patterns change. Composition is "black-box" reuse: the wrapper sees only the public contract, the same surface every other client sees.
The four concrete problems
1. Subclass depends on superclass internals. A subclass override may be called by other superclass methods in ways the docs don't promise. Override one method, accidentally affect three.
2. Superclass evolution breaks subclasses. Adding a method to Super may collide with a same-named method already defined in Sub. If signatures differ, you get a compile error; if they match, you've silently changed the subclass's behavior.
3. Inherited API surface. Every public method of the superclass leaks into the subclass — including ones the subclass author doesn't want exposed. You can't subtract from an inherited interface.
4. Tight coupling defeats polymorphism. A class that extends ArrayList can't be swapped for LinkedList without a rewrite. A class that holds a List<T> can.
The canonical example
// BROKEN: extends instead of wraps
public 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);
}
public int addCount() { return addCount; }
}
var s = new InstrumentedHashSet<String>();
s.addAll(List.of("a", "b", "c"));
System.out.println(s.addCount()); // expected 3, got 6
HashSet.addAll calls this.add internally, so each element is counted twice. Nothing in the Set contract said this would happen — the subclass made an assumption about an implementation detail and got punished for it.
The fix — composition
Wrap a Set, forward unchanged methods, intercept the ones you care about:
public class InstrumentedSet<E> implements Set<E> {
private final Set<E> s;
private int addCount = 0;
public InstrumentedSet(Set<E> s) { this.s = s; }
@Override public boolean add(E e) { addCount++; return s.add(e); }
@Override public boolean addAll(Collection<? extends E> c) {
addCount += c.size(); return s.addAll(c);
}
// forward everything else to s
}
new InstrumentedSet<>(new HashSet<>()).addCount(); // counts correctly for ANY Set
Bonus: this works over TreeSet, LinkedHashSet, ConcurrentSkipListSet, anything implementing Set. The inheritance version locked you to HashSet.