2.2.6 · D5Design Principles

Question bank — SOLID — Liskov Substitution Principle

2,102 words10 min readBack to topic

Before you start, hold these three words in mind — they carry the whole principle:

  • Precondition — what a method demands of its caller before it will work.
  • Postcondition — what a method promises to deliver when it finishes.
  • Invariant — a truth about the object that stays true before and after every method.

Picture the two words that trip everyone up: covariant vs contravariant

"Variance" just answers one question: when I go from base to subtype, is a set allowed to grow or must it shrink? Look at the figure — it shows the two allowed directions and why they never surprise a caller who only knows the base.

Figure — SOLID — Liskov Substitution Principle

See the Rectangle–Square break happen at runtime

The classic trap: math says a square is a rectangle, but behaviourally a mutable Square cannot honour setWidth and setHeight independently. The figure traces exactly what a Rectangle-expecting client observes, step by step.

Figure — SOLID — Liskov Substitution Principle

The Bird hierarchy — see the fix, don't just read it

Prose says "split the hierarchy". The class diagram makes the subtype relationships obvious: Penguin must not sit under whatever declares fly().

Bird

FlyingBird has fly

Penguin cannot fly

Eagle

Sparrow


True or false — justify

A square is a rectangle, so Square extends Rectangle always satisfies LSP.
False. LSP tests behavioural substitutability, not real-world taxonomy; a mutable square breaks setWidth's independent-dimension invariant that a Rectangle caller relies on.
LSP is really just a restatement of "inheritance should model IS-A relationships".
False. IS-A is a category claim; LSP is a behaviour claim — you can have a valid IS-A ("penguin is a bird") that still violates LSP because the subtype can't honour every promise.
A subclass that accepts more input types than its base still obeys LSP.
True. Widening (weakening) the precondition is contravariant and safe — the caller only ever sends base-legal inputs, and those remain accepted.
A subclass that guarantees more than the base (a stronger postcondition) obeys LSP.
True. Narrowing (strengthening) the postcondition is covariant and safe — every promise the base made is still kept, plus extra the caller can ignore.
If the method signatures match exactly, LSP is automatically satisfied.
False. LSP is about the behavioural contract (pre/post/invariants), not the type signature; identical signatures can still hide a broken postcondition.
Throwing UnsupportedOperationException in an override is fine because it documents a limitation.
False. It adds an exception not in the base contract (rule 3) and effectively strengthens the precondition; a base caller never planned to catch it, so it crashes at runtime.
Making all fields final (immutable) in both Rectangle and Square can restore substitutability.
True. With no setWidth/setHeight mutators, there is no contract for a Square to violate — both only promise area(), which each honours fully.
LSP violations are always caught by the compiler.
False. They are behavioural, so they compile fine and surface only at runtime (or via a failing assertion/test), which is exactly why they're dangerous.
Overriding a method to return a narrower (more specific) return type breaks LSP.
False. Return types are outputs, so narrowing them is covariant and allowed — callers expecting the base type still receive a valid instance.
Overriding a method to accept a narrower parameter type is safe because "stricter is safer".
False. Parameters are inputs; narrowing them strengthens the precondition, so existing base callers can now send inputs the subtype rejects.
A static method can be "overridden" to change behaviour under LSP just like an instance method.
False. Static methods are hidden, not overridden — the version called is fixed by the compile-time type, so runtime substitution never dispatches to the subtype and LSP (a runtime-substitution rule) doesn't even engage.

Spot the error

class Penguin extends Bird { void fly(){ throw new UnsupportedOperationException(); } } — what LSP rule is broken?
Rule 3 — a new exception not in Bird's contract; any code calling bird.fly() is correct for a Bird but crashes for a Penguin.
Square.setHeight(4) also sets width to 4 — which part of the contract does this violate?
The base's invariant "width and height are independent" and the postcondition of setHeight (it no longer guarantees width is unchanged).
An override void save(File f) { if (f.isLocal()) super.save(f); else throw ...; } — spot the flaw.
It strengthens the precondition (base accepted any File, subtype demands a local File), so a base caller passing a remote file now fails.
A base read() promises "returns non-null string"; the override sometimes returns null. What's wrong?
The postcondition is weakened — the caller relied on non-null and will hit a NullPointerException, breaking correctness.
FlyingBird extends Bird with Penguin extends Bird and Eagle extends FlyingBird — is this a valid fix?
Yes. Splitting the hierarchy means only true fliers extend FlyingBird, so no caller of FlyingBird.fly() can ever receive a Penguin.
A subclass override adds a synchronized lock and can now deadlock under contention the base never could — LSP problem?
Yes, a subtle one — it can violate a liveness/timing property the base guaranteed, surprising a caller that assumed the base's progress behaviour.
A subclass has a field int size = 3 while the base has int size = 0; a method reads this.size. Does hiding this field cause an LSP trap?
Yes — field access is resolved by the static type, so base-typed code reads the base's size (0) while the object "really" holds 3; the two views disagree and can surprise the caller. Prefer private fields with accessor methods.

Why questions

Why are preconditions contravariant (may only widen)?
A caller who knows only the base sends base-legal inputs; if the subtype demanded more, those legal inputs would suddenly be rejected and crash the unaware caller.
Why are postconditions covariant (may only narrow)?
The caller relies on the base's promises; if the subtype guaranteed less, the caller's later code that depended on that promise would break.
Why does an LSP violation destroy the value of Polymorphism?
It forces callers to add if (it's really a Square) ... type checks, which defeats the whole point of writing code once against a base type and trusting all subtypes.
Why is "fix the hierarchy" preferred over "throw NotImplemented" for a capability a subtype lacks?
Restructuring (often with help from SOLID — Interface Segregation Principle) ensures the subtype is never handed where the capability is required, so the violation can't occur at all.
Why does LSP connect to Design by Contract?
Design by Contract formalises pre/post/invariants; LSP is exactly the rule for how those contracts must vary under subtyping (weaken pre, strengthen post, preserve invariants).
Why does favouring composition (a shared Shape interface) often dodge the Rectangle/Square trap?
Because you only share the method both types can honour fully (area()), so there's no over-broad contract like setWidth for a subtype to break.
Why is LSP considered the "glue" that lets SOLID — Open Closed Principle work?
Open-Closed relies on extending via new subtypes without editing callers; that only stays safe if each new subtype is genuinely substitutable, which is precisely LSP.
Why might a base method be declared final specifically to protect LSP?
A final method cannot be overridden, so no subtype can weaken its postcondition or break its invariant — the base guarantees that piece of the contract stays intact for every subtype.

Edge cases

Does LSP apply when a subclass adds new methods the base doesn't have?
Adding methods is fine on its own — LSP only constrains overridden base behaviour; existing base callers never call the new methods, so they can't be surprised.
A base method promises to complete in O(n); the override runs in O(n²). LSP violation?
Arguably yes — imagine a UI that repaints on every keystroke assuming O(n) redraw; swap in an O(n²) subtype and typing freezes, so the caller relied on the implied performance contract and was silently surprised.
An abstract base method has no implementation; can a subclass override it however it likes?
Not freely — it must still satisfy the documented contract of the abstract method (its declared pre/post/invariants), even though there's no inherited code.
Is it an LSP violation for a subclass to make an operation faster or return extra fields?
No — that only strengthens the postcondition (delivers more/better), which is covariant and safe for every base caller.
A subclass constructor requires an extra non-null argument the base didn't. Does LSP care?
LSP governs substitutability of instances after construction; if the object still honours every base method contract, construction differences don't violate it — but if construction can leave invariants unmet, it does.
Does LSP say anything about a subtype throwing a subclass of an exception the base already declares?
No violation — a declared exception's subtype is still "an exception the caller was told to expect", so the caller's existing catch handles it.
Does overriding a final method to change behaviour risk an LSP violation?
You can't — final forbids the override entirely; the corner case is the reverse, where marking a method final removes the very possibility of a subtype breaking that contract.
Can a private method create an LSP problem when a subclass declares one with the same name?
No override happens — private methods aren't polymorphic, so each class calls its own; there's no runtime substitution, hence LSP (about substitutable dispatch) is not engaged for them.

Recall One-line self test before you close this page

If I hand a subtype where a base type was expected, can the caller — who knows only the base contract — ever be surprised? ::: If yes, LSP is violated; the fix is to weaken preconditions, strengthen postconditions, preserve invariants, or restructure the hierarchy so the mismatch is impossible.