The Problem With Reusing Via Extends
The most common junior justification for inheritance is “so I don’t have to rewrite these methods.” It works — until the superclass evolves internally. A subclass silently depends not just on the base class’s public contract but on which private methods the base calls internally, and no compiler enforces that dependency. When the JDK team reorganizes ArrayList internals, your counting subclass breaks without any API change anywhere.
The Canonical Breakage
FRAGILE BASE CLASS — silent breakage
class CountingList extends ArrayList<Object> { // override to count adds
int count;
boolean add(Object o){ count++; return super.add(o); }
}
list.addAll(otherList); // ArrayList.addAll does NOT call add()
// → elements inserted, count unchanged. Base internals = your contract.
The subclass assumed addAll routes through add. It doesn’t — that’s an internal implementation choice of the base. The subclass compiled fine against the public API yet violates its own invariant on first use: the definition of fragile-base coupling.
Why Composition Wins for Reuse
| Concern | Inheritance | Composition |
|---|---|---|
| Coupling | Compile-time, permanent | Runtime, swappable |
| Encapsulation | Broken — subclass sees base internals (“white-box”) | Intact — only public API known (“black-box”) |
| Blast radius of base change | Every subclass | The one wrapper class |
| Flexibility | Fixed at new time | Behaviors mixed per instance (Strategy injection) |
| Testability | Must construct real superclass state | Inject a fake behind the interface |
The Forwarding Wrapper Alternative
class CountingList implements List<Object> {
private final List<Object> delegate; int count = 0;
CountingList(List<Object> d) { this.delegate = d; }
@Override public boolean add(Object o){ count++; return delegate.add(o); }
@Override public boolean addAll(Collection<?> c){
count += c.size(); return delegate.addAll(c); } // no base surprises
/* remaining List methods forward to delegate */
}
Same reuse, but the wrapper knows only the public List contract — internals can never betray it.
When Inheritance Is Correct
- Genuine IS-A with LSP-safe substitution:
FileNotFoundException extends IOException. - Framework-designed extension points: template methods in bases written to be extended (
AbstractList,HttpServlet) — fragility managed because self-use is documented as part of the contract. - Effective Java Item 18 rule: extend classes from other packages only when designed and documented for extension.
Failure Modes
- Superclass evolution breaking subclasses silently — JDK changelogs repeatedly note new default/added methods altering extension behavior.
- Deep hierarchies (4+ levels): change requires reading the whole chain; comprehension cost grows multiplicatively.
- Inherited baggage: extending
HashMapfor map convenience also inherits mutability and concurrency semantics never wanted. - Interview tell: “extends” chosen because it saves typing overrides, not because substitution is intended.
Two Kinds of IS-A
- Syntactic IS-A:
Squareis aRectangle, mathematically true. - Behavioral IS-A:
Squarecan substituteRectanglein every client context without surprises — false: code that sets width and height independently breaks on squares.
Client expects Rectangle contract:
r.setWidth(5); r.setHeight(4); assert area == 20;
│
┌───────────────┴────────────────┐
▼ ▼
true Rectangle → passes Square extends Rectangle
setWidth also sets height
→ area = 16 → CONTRACT BROKEN
The Substitution Test (LSP operational form)
Before extends, ask for every public method of the parent:
- Can the subclass honor the preconditions (never strengthen them)?
- Keep all postconditions/invariants (never weaken them)?
- Preserve expected behavior — no surprise exceptions where parent threw none? Any “no” → composition or a separate hierarchy.
The Bird Trap
Penguin extends Bird.fly()→ throwsUnsupportedOperationException. Compile-time IS-A, runtime lie. Every caller holdingBirdmust now special-case penguins — the hierarchy exported its flaw.
Relationship Selection Table
| Real-world phrase | Relationship | Java mechanism |
|---|---|---|
| ”Car is a Vehicle” | IS-A (behavioral) | extends |
| ”Car has an Engine” | HAS-A | field / composition |
| ”Duck can Fly” / “acts-as” | capability | implements interface |
| ”Order uses a PricingService” | dependency | method parameter |
Historical Evidence
java.util.Stack extends Vector— documented JDK mistake: stack semantics broken by inheritedinsertElementAt(0); official docs say useDequeinstead. Syntactic reuse won over substitution; the API paid forever.Properties extends Hashtable— same disease:getPropertyvs inheritedput(Object,Object)allow non-String values.
Interview Signals
- Candidate states the rectangle/square test unprompted when modeling geometry or roles (
Manager extends Employeeis usually fine;Square extends Rectangleis not). - Hierarchy drawn bottom-up from behaviors (“what varies?”) rather than top-down taxonomy trees.
- Uses interface for capabilities so
FlyingBird/Penguinboth remain birds without lying about flight.
The Problem
Two parents hand down the same rule — whose wins? Formally: type D inherits member m() through two paths (via B and via C). Someone must decide which implementation D gets. Languages diverge sharply on who decides and how painful the decision is.
C++ classic diamond (state duplication) Java default-method diamond
A { int x; print() } interface A { default String m() }
/ \ / \
B extends A C extends A interface B interface C
\ / { default m } { default m }
D: inherits TWO copies of x \ /
→ explicit virtual inheritance ▼
or scope resolution D::B::print() class D implements B, C
→ MUST override m()
Left: C++ allows full multiple class inheritance; D receives two x subobjects unless the author opts into virtual inheritance — ambiguity resolved manually at each use site, initialization order becomes subtle. Right: Java banned state diamonds entirely (single class parent), but Java 8 default methods reintroduced behavior diamonds — resolved by fixed language rules rather than per-site manual choice.
Resolution Rules (priority order)
- Class wins: concrete superclass method beats any interface default.
- Most specific interface wins: if
Sub extends Super,Sub’s default overridesSuper’s. - Otherwise compile error — implementer must override and may delegate explicitly:
interface A { default String name() { return "A"; } }
interface B extends A { default String name() { return "B"; } } // more specific
class Impl implements B, C { // B and C both define name()
@Override public String name() { return C.super.name(); } // explicit pick
}
Rule 3 is deliberate design: ambiguity is forced to the author at compile time, never silently resolved.
Edge Cases
- Re-abstraction: an interface may override a default with an abstract declaration (
default String m();) — pushing the burden down to concrete implementers. - Diamond with no defaults on some path → rules still apply in order.
- Static interface methods are never inherited — no diamond possible for them.
- C++ solves its version with
virtualbase classes (one shared subobject) at memory-layout cost; Java cannot duplicate state because interfaces hold none.
Interview Framing
- Question shape: “Does Java have multiple inheritance?” Answer: of type (interfaces), never of state (classes); since Java 8, default methods create behavior diamonds with deterministic resolution.
- Strong candidates explain why class MI was banned: duplicated subobjects plus constructor-order complexity outweighed rare benefit.
Failure Modes
- Depending silently on rule 2 across library versions: when both interfaces become equally specific, previously-compiling code breaks — but breaks at compile time, which is exactly why the conflict was made an error rather than a silent pick.
Premium Content
Unlock Inheritance and all premium lessons with a subscription.
From ₹199.99/year — See plans