Menu

Earn Premium with Referrals

Invite your friends and earn Premium rewards through our referral program.

See how it works and start inviting friends.

Inheritance
LLD

Inheritance

Explore inheritance, class hierarchies, code reuse, and the trade-offs of using inheritance.

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

ConcernInheritanceComposition
CouplingCompile-time, permanentRuntime, swappable
EncapsulationBroken — subclass sees base internals (“white-box”)Intact — only public API known (“black-box”)
Blast radius of base changeEvery subclassThe one wrapper class
FlexibilityFixed at new timeBehaviors mixed per instance (Strategy injection)
TestabilityMust construct real superclass stateInject 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 HashMap for 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: Square is a Rectangle, mathematically true.
  • Behavioral IS-A: Square can substitute Rectangle in 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:

  1. Can the subclass honor the preconditions (never strengthen them)?
  2. Keep all postconditions/invariants (never weaken them)?
  3. Preserve expected behavior — no surprise exceptions where parent threw none? Any “no” → composition or a separate hierarchy.

The Bird Trap

  • Penguin extends Bird.fly() → throws UnsupportedOperationException. Compile-time IS-A, runtime lie. Every caller holding Bird must now special-case penguins — the hierarchy exported its flaw.

Relationship Selection Table

Real-world phraseRelationshipJava mechanism
”Car is a Vehicle”IS-A (behavioral)extends
”Car has an Engine”HAS-Afield / composition
”Duck can Fly” / “acts-as”capabilityimplements interface
”Order uses a PricingService”dependencymethod parameter

Historical Evidence

  • java.util.Stack extends Vector — documented JDK mistake: stack semantics broken by inherited insertElementAt(0); official docs say use Deque instead. Syntactic reuse won over substitution; the API paid forever.
  • Properties extends Hashtable — same disease: getProperty vs inherited put(Object,Object) allow non-String values.

Interview Signals

  • Candidate states the rectangle/square test unprompted when modeling geometry or roles (Manager extends Employee is usually fine; Square extends Rectangle is not).
  • Hierarchy drawn bottom-up from behaviors (“what varies?”) rather than top-down taxonomy trees.
  • Uses interface for capabilities so FlyingBird/Penguin both 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)

  1. Class wins: concrete superclass method beats any interface default.
  2. Most specific interface wins: if Sub extends Super, Sub’s default overrides Super’s.
  3. 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 virtual base 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.

My Private Notes

Notes are auto-saved locally to this device.