Overview
The Liskov Substitution Principle (LSP) says objects of a subtype must be usable wherever the base type is expected without breaking the program. A subclass may extend behavior, but it must honor the parent's contract: it cannot demand more from callers (stronger preconditions) or promise less (weaker postconditions), and it must preserve the parent's invariants.
The classic violation is Square extends Rectangle: setting a square's width also changes its height, breaking code that assumes width and height are independent. LSP violations show up as instanceof checks, overridden methods that throw 'not supported', and surprising behavior in polymorphic code.
If a company says 'any licensed driver can drive this delivery van', a substitute driver must be able to do everything the job requires. A driver who refuses to reverse breaks the promise, even if they are technically a driver.
When to use it
- Designing any inheritance hierarchy or interface implementation.
- Reviewing subclasses that override behavior.
- Debugging polymorphic code that behaves differently for one subtype.
Where it shows up in interviews
Recognize it when: subclass throws or ignores parent methods.
- Refactor Bird with Penguin.fly()
- Refactor Square extends Rectangle
Recognize it when: implementations behave inconsistently.
- Design a Repository interface
- Design a Cache interface
Where it is used in real software
List.of returns lists that throw on add, a known LSP compromise in the JDK; callers must know which lists are immutable.
Java's Stack inherits Vector's random-access methods, letting callers break stack semantics.
Teams write shared test suites every implementation of an interface must pass.
Key terms
- Substitutability
- A subtype works anywhere the base type does.
- Precondition
- What must be true before calling; subtypes cannot strengthen it.
- Postcondition
- What is guaranteed after; subtypes cannot weaken it.
- Invariant
- Always-true property; subtypes must preserve it.
- Contract test
- Shared tests run against every implementation.
How it works, step by step
- 1Write down the base contract
Inputs accepted, outputs promised, errors thrown, invariants.
- 2Check each override
Does it accept at least the same inputs and promise at least the same results?
- 3Look for red flags
Throwing not-supported, empty overrides, instanceof checks by callers.
- 4Redesign the hierarchy
Split interfaces or use composition.
- 5Add contract tests
Run the same tests against all implementations.
Square extends Rectangle
Code: r.setWidth(5); r.setHeight(4); expect area 20
| Object | After setWidth(5) | After setHeight(4) | area() | Contract holds? |
|---|---|---|---|---|
| Rectangle | 5 x ? | 5 x 4 | 20 | Yes |
| Square | 5 x 5 | 4 x 4 | 16 | No: width changed unexpectedly |
NOWObject: Rectangle | After setWidth(5): 5 x ? | After setHeight(4): 5 x 4 | area(): 20 | Contract holds?: Yes
A square is a rectangle in geometry, but not a substitutable mutable Rectangle object. Fix: separate immutable Shape types with area(), no shared setters.
Implementation
// Violation: Penguin cannot honor Bird.fly()abstract class Bird { abstract void fly(); }class Penguin extends Bird { void fly() { throw new UnsupportedOperationException(); } // breaks callers} // Fix: model capabilities separatelyinterface Bird2 { String name(); }interface Flyer { void fly(); } record Sparrow(String name) implements Bird2, Flyer { public void fly() { System.out.println(name + " flies"); }}record Penguin2(String name) implements Bird2 {} void migrate(List<Flyer> flyers) { flyers.forEach(Flyer::fly); } // only accepts things that can flyComplexity and performance
Cheap insurance.
Hard to trace.
Trade-offs
Real-world is-a relationships do not always mean behavioral substitutability; model behavior.
Some libraries accept small LSP compromises (immutable lists) for simplicity; document them clearly.
Variants and related techniques
Explicit preconditions, postconditions, and invariants (Eiffel, assertions).
Parameter types can be contravariant and return types covariant in subtypes.
Common mistakes
- Overrides that throw 'not supported'.
Fix: Split the interface so the subtype does not claim the capability.
- Subtypes adding stricter validation.
Fix: Callers of the base type will be surprised; keep preconditions equal or weaker.
Interview questions
Why is Square extends Rectangle an LSP violation?
Rectangle's contract lets width and height change independently. Square must keep them equal, so code relying on Rectangle's behavior gets wrong results when given a Square.
How do you detect LSP violations?
Look for overrides that throw or do nothing, callers that check concrete types, and implementations that fail shared contract tests.
Practice problems
| Problem | Difficulty | What it trains |
|---|---|---|
| Fix the Bird/Penguin hierarchy | Easy | Capability interfaces. |
| Write contract tests for a Repository interface | Medium | Behavioral contracts. |