Menu

Earn Premium with Referrals

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

See how it works and start inviting friends.

Liskov Substitution Principle
LLD

Liskov Substitution Principle

Learn how subtypes should remain substitutable for their base types without breaking expected behavior.

Liskov Substitution Principle (LSP)

The Principle

Barbara Liskov, 1987: if S is a subtype of T, objects of type S may replace objects of type T without altering the correctness of the program. Inheritance is a promise to every future caller holding a base-type reference: anything the base could do, I can do, with equivalent semantics. LSP is that promise made testable.

The Rectangle Trap

class Rectangle {
    protected int w, h;
    void setW(int w) { this.w = w; }
    void setH(int h) { this.h = h; }
    int area() { return w * h; }
}
class Square extends Rectangle {
    @Override void setW(int w) { this.w = w; this.h = w; }  // keep it square!
    @Override void setH(int h) { this.w = h; this.h = h; }
}

// client code written against the BASE contract:
void f(Rectangle r) {
    r.setW(5); r.setH(4);
    assert r.area() == 20;      // holds for Rectangle
}
f(new Square());                // area = 16 → assertion fails

Mathematically a square is-a rectangle. Behaviorally, Square strengthens an implicit postcondition (setW must not change height) that base-class clients rely on. Substitution fails at runtime despite compiling perfectly — which is exactly why LSP failures are dangerous: no compiler catches them.

The Contract Rules

A subtype honors LSP when, for every inherited method:

RuleDirectionViolation example
PreconditionsMay weaken, never strengthenBase accepts any int; subtype requires positive
PostconditionsMay strengthen, never weakenBase returns sorted list; subtype sometimes unsorted
InvariantsMust preserveMutable subclass of immutable parent
ExceptionsMay narrow, never broadenBase throws IOException; subtype adds new checked type
History constraintNo new mutability surprisesSquare coupling width/height behind client’s back

The Smell That Betrays It

class Penguin extends Bird {
    @Override void fly() { throw new UnsupportedOperationException(); }
}

An override whose body is “you shouldn’t have called me” is a confession: the hierarchy claims a capability some members lack. Every Bird-typed caller now needs instanceof special-casing — the flaw exported to all clients.

Fixes

  1. Split capabilities: Bird / FlyingBird — penguins stay birds without inheriting flight.
  2. Separate hierarchies: immutable and mutable shapes as siblings, not parent-child.
  3. Composition: when reuse tempts inheritance but substitution fails.

Accepted Violations (Know Them)

Collections.unmodifiableList(...).add() throws — a documented, deliberate violation trading LSP for safety-by-exception. The difference from Penguin: it’s explicit in the contract (UnsupportedOperationException documented per-method). Documented violations are decisions; silent ones are bugs.

Interview Framing

  • The rectangle proof-on-whiteboard is the canonical question — walking through the failing assertion demonstrates real understanding.
  • Experienced signal: framing LSP as design-time discipline (“choose hierarchies by behavior, not taxonomy”) rather than a runtime testing concern.

From Principle to Checklist

LSP states a property; engineers need an audit list. Every subtype inherits obligations along four axes — argument handling, results, state validity, and exception behavior. Violate any axis and some existing base-typed caller breaks without recompilation.

        Base class contract T                 Subtype S must:
 ┌─────────────────────────────────┐    ┌──────────────────────────────┐
 │ m(x)                            │    │ accept EVERYTHING x T accepts│
 │  requires: preconditions on x   │ →  │ (weaker or equal pre)        │
 │  ensures: postconditions        │ →  │ guarantee MORE, never less   │
 │  invariant: always-true facts   │ →  │ keep them true               │
 │  throws: documented exceptions  │ →  │ same or narrower only        │
 └─────────────────────────────────┘    └──────────────────────────────┘

The Four Axes in Java Terms

AxisRuleJava enforcement
ParametersContravariant-or-equal acceptanceNot enforced — overloading hides instead
Return typesCovariant-or-equalEnforced by compiler since Java 5
ExceptionsNarrower-or-equal checked setEnforced by compiler
Behavior/invariantsPreservedNot enforced — the entire risk lives here

Java checks syntax; LSP is about the unchecked behavioral rows. That gap is why substitution bugs survive compilation.

Violation Catalog

PatternCode smellBroken axis
Strengthened preconditionOverride adds if (x > 0) check base never hadPrecondition
Weakened postconditionBase sorts result; subtype skips sorting sometimesPostcondition
Invariant breakSquare coupling w/h behind clients’ backsHistory/invariants
Broadened exceptionsSubclass throws new checked exceptionExceptions
Capability liethrow new UnsupportedOperationException() in overridePostcondition
State surpriseSubclass auto-saves to DB where parent never persistedSide-effect/history

Auditing a Hierarchy

  1. Read the base’s documented behavior (javadoc contracts, not just signatures).
  2. For each override, ask: would every existing base-typed test still pass unchanged against this subtype?
  3. Hunt instanceof/type-checks downstream of the hierarchy — each one marks a suspected substitution failure being patched by callers.
  4. Check constructors: do they establish all base invariants before returning?

The JDK’s Own Gray Areas

  • Collections.unmodifiable*: throws on mutation — deliberate, documented violation.
  • java.util.Stack extends Vector: inherited insertElementAt() violates stack semantics silently — the cautionary example shipped in the JDK itself.
  • Properties extends Hashtable: put(Object,Object) allows non-String values, breaking Properties’ own contract.

Interview Framing

  • Asked “how do you verify LSP?”, walking axes 1–4 plus the instanceof-hunt is the complete answer.
  • Distinguishing documented violations (design decisions) from silent ones (bugs) separates senior judgment from rule-recitation.

My Private Notes

Notes are auto-saved locally to this device.