Liskov Substitution Principle (LSP)
Subtypes must be substitutable for their base types — without surprising callers.
S is a subtype of T, then objects of type T may be replaced with objects of type S without breaking any desirable property of the program (correctness, expectations).Why It Matters
- Polymorphism actually works — Code that accepts
Readermust not special-caseBadReader— or abstraction is a lie. - Fewer `instanceof` hacks — If you need
if (x instanceof Square)everywhere, LSP is already broken. - Safer libraries — Frameworks assume substitutability — break LSP and subtle bugs appear only in production.
In Layman's Terms
If you rent a car and the agency hands you a substitute vehicle, it must still drive like a car — steering wheel, pedals, fuel cap in the usual place. If the substitute is a submarine shaped like a car, something is wrong with your type hierarchy.
LSP says: inheritance is not "IS-A in the real world" — it is "honours the contract of the parent for every method a caller might invoke."
Real-World Analogy
🎭 Stand-in actor
A theatre understudy must perform the same script cues as the lead — not improvise a different ending because they prefer it.
Your subclass is the understudy: same observable behaviour from the caller's point of view (preconditions, postconditions, invariants).
Visual
void reportArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert area == 20; // true for Rectangle
}
reportArea(new Rectangle()); // OK
reportArea(new Square()); // ??? Square must not break expectations class Square extends Rectangle {
void setWidth(int w) { super.setWidth(w); super.setHeight(w); }
void setHeight(int h){ super.setHeight(h); super.setWidth(h); }
}
// Caller thinks width and height are independent — Square couples them.Rectangle vs Square is the textbook example: geometric "is-a" ≠ behavioural substitutability.
When to Use
- Whenever you use inheritance or implementation of an interface — ask: can I swap implementations blindly?
- When you design API contracts — document pre/post conditions and enforce them in subclasses.
- When reviewing PRs that add a new implementation of an existing type.
When NOT to Use
- You are using composition only — LSP applies to inheritance/subtyping, not every delegation.
- Value objects with no polymorphic clients — less relevant than deep class hierarchies.
Anti-Pattern (What NOT to Do)
class Rectangle {
protected int w, h;
public void setWidth(int w) { this.w = w; }
public void setHeight(int h) { this.h = h; }
public int area() { return w * h; }
}
class Square extends Rectangle {
@Override public void setWidth(int w) { super.setWidth(w); super.setHeight(w); }
@Override public void setHeight(int h) { super.setHeight(h); super.setWidth(h); }
}
// Caller using Rectangle contract:
void resizeTo(Rectangle r, int w, int h) {
r.setWidth(w);
r.setHeight(h);
// Expected area = w * h for independent dimensions — fails for Square.The Right Way
interface Shape { int area(); }
record Rectangle(int width, int height) implements Shape {
public int area() { return width * height; }
}
record Square(int side) implements Shape {
public int area() { return side * side; }
}
// No fake inheritance — each type models its own invariants clearly.Pros & Cons
- Polymorphic code stays simple — no special cases.
- Aligns with Design by Contract (preconditions, postconditions).
- Makes testing straightforward: one suite per abstraction.
- Strict LSP can push you toward composition — sometimes more boilerplate than naive inheritance.
- Teams disagree on what the "contract" is — document it.
Good subtype vs bad subtype
| Aspect | Honours contract ✅ | Surprises callers 🚨 |
|---|---|---|
| Preconditions | Not stricter than parent | Rejects inputs parent accepted |
| Postconditions | At least as strong | Weaker guarantees / wrong results |
| Invariants | Preserves parent invariants | New state parent forbids |
| Exceptions | Same or narrower | New checked exceptions parent didn't |
Common Confusions Cleared Up
Duck typing substitutes by behaviour at runtime without a declared supertype. LSP is the OO contract version: the supertype's expectations must hold for all subtypes that claim to be substitutable.
SRP asks one reason to change per class. LSP asks subtypes don't weaken parent guarantees. A class can be SRP-clean but LSP-broken (Square/Rectangle).
Common Mistakes / Anti-Patterns
- Empty overrides that throw
UnsupportedOperationException— interface segregation often fixes this instead. - Subclass narrows return type incorrectly or strengthens preconditions beyond what callers expect.
- Using inheritance only for code reuse (
extends Utility) — no real IS-A relationship.
Interview Q&A
Likely Follow-up Questions
- How does covariance/contravariance relate to LSP in Java generics?
- When is Template Method safe for LSP vs when does it trap subclasses?
- Explain Design by Contract in two sentences.
Quick Cheat Sheet
instanceof before calling subclass-specific behaviour.