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:
| Rule | Direction | Violation example |
|---|---|---|
| Preconditions | May weaken, never strengthen | Base accepts any int; subtype requires positive |
| Postconditions | May strengthen, never weaken | Base returns sorted list; subtype sometimes unsorted |
| Invariants | Must preserve | Mutable subclass of immutable parent |
| Exceptions | May narrow, never broaden | Base throws IOException; subtype adds new checked type |
| History constraint | No new mutability surprises | Square 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
- Split capabilities:
Bird/FlyingBird— penguins stay birds without inheriting flight. - Separate hierarchies: immutable and mutable shapes as siblings, not parent-child.
- 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
| Axis | Rule | Java enforcement |
|---|---|---|
| Parameters | Contravariant-or-equal acceptance | Not enforced — overloading hides instead |
| Return types | Covariant-or-equal | Enforced by compiler since Java 5 |
| Exceptions | Narrower-or-equal checked set | Enforced by compiler |
| Behavior/invariants | Preserved | Not 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
| Pattern | Code smell | Broken axis |
|---|---|---|
| Strengthened precondition | Override adds if (x > 0) check base never had | Precondition |
| Weakened postcondition | Base sorts result; subtype skips sorting sometimes | Postcondition |
| Invariant break | Square coupling w/h behind clients’ backs | History/invariants |
| Broadened exceptions | Subclass throws new checked exception | Exceptions |
| Capability lie | throw new UnsupportedOperationException() in override | Postcondition |
| State surprise | Subclass auto-saves to DB where parent never persisted | Side-effect/history |
Auditing a Hierarchy
- Read the base’s documented behavior (javadoc contracts, not just signatures).
- For each override, ask: would every existing base-typed test still pass unchanged against this subtype?
- Hunt
instanceof/type-checks downstream of the hierarchy — each one marks a suspected substitution failure being patched by callers. - 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: inheritedinsertElementAt()violates stack semantics silently — the cautionary example shipped in the JDK itself.Properties extends Hashtable:put(Object,Object)allows non-String values, breakingProperties’ 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.
Premium Content
Unlock Liskov Substitution Principle and all premium lessons with a subscription.
From ₹199.99/year — See plans