Exercises — SOLID — Liskov Substitution Principle
Quick reminder of the three contract rules you'll use everywhere:
The picture to keep in your head: a caller only knows the base contract. It sends base-legal inputs and trusts base-promised outputs. Anything a subtype does that surprises that caller is a violation.

Level 1 — Recognition
Exercise 1.1
State whether this is an LSP violation, yes or no.
class Account {
int balance = 100;
void withdraw(int amt) { balance -= amt; } // allows overdraft
}
class SavingsAccount extends Account {
void withdraw(int amt) {
if (amt > balance) throw new IllegalStateException("no overdraft");
balance -= amt;
}
}Recall Solution 1.1
Yes, violation. The base withdraw accepted any amount (its precondition is "always true — even overdraft"). The subclass strengthens the precondition to "amt ≤ balance" and throws a new exception. A caller that only knows Account may call withdraw(200) expecting it to work; against SavingsAccount it crashes.
Broken rule: Rule 1 (precondition strengthened) and Rule 3 (new exception type).
Exercise 1.2
Same question.
class Logger { void log(String msg) { System.out.println(msg); } }
class TimestampLogger extends Logger {
void log(String msg) { System.out.println("[" + now() + "] " + msg); }
}Recall Solution 1.2
No violation. TimestampLogger accepts every String the base accepted (precondition unchanged), it still prints the message (postcondition "the message appears in output" still holds — it adds a prefix but never removes the promise), throws nothing new. A caller holding a Logger reference is never surprised. This is exactly what polymorphism should look like — see Polymorphism.
Level 2 — Application
Exercise 2.1
A base method has precondition "amount in range ." Which of these subtype preconditions are legal under Rule 1? For each, answer legal / illegal. (a) (b) (c) (d)
Recall Solution 2.1
Rule 1 needs — every input the base accepted, the subtype must still accept. So the subtype's set must be a superset (or equal) of .
- (a) — legal (equal, superset of itself).
- (b) — legal (wider — contravariant, allowed).
- (c) — illegal (narrower; rejects , which the base accepted).
- (d) — legal (wider). Legal count = 3.
Exercise 2.2
A base method's postcondition is "returns a value in ." Which subtype postconditions are legal under Rule 2? legal / illegal each. (a) (b) (c) (d)
Recall Solution 2.2
Rule 2 needs — everything the subtype returns must still satisfy the base's promise. So the subtype's output set must be a subset (or equal) of .
- (a) — legal (equal).
- (b) — legal (narrower — covariant, allowed).
- (c) — illegal (may return , breaking the base promise).
- (d) — legal (extremely narrow, still inside). Legal count = 3.

Level 3 — Analysis
Exercise 3.1
Name the exact broken rule (1, 2, or 3) and one sentence why.
class FileReader { String read() { /* returns file contents, never null */ } }
class CachedReader extends FileReader {
String read() { return cache.isStale() ? null : cache.get(); } // may return null
}Recall Solution 3.1
Rule 2 (postcondition weakened). The base guaranteed "never null"; a caller may do read().length() safely. The subtype may now return null, so that same caller crashes with a NullPointerException. The subtype's output set grew to include null, which is not a subset of the base's promised outputs.
Exercise 3.2
Same task.
interface Stack { void push(int x); int pop(); } // pop() returns last pushed
class BrokenStack implements Stack {
public void push(int x) { data.add(x); }
public int pop() { return data.remove(0); } // FIFO, not LIFO
}Recall Solution 3.2
Rule 2 (postcondition weakened / invariant broken). The Stack contract promises pop() returns the last pushed item (LIFO). BrokenStack.pop() returns the first (FIFO). Signatures match, but the behavioural postcondition ("returns the most-recently-pushed value") is violated. This is why LSP is about the behavioural contract, not the method signature — the compiler is happy, the caller is betrayed.
Exercise 3.3
Count how many of the three rules the Square-extends-Rectangle example from the parent violates when you run setWidth(5); setHeight(4); area().
Recall Solution 3.3
1 rule directly, cascading into another. The core break is Rule 3 (invariant): the base's invariant "width and height are independent" is destroyed because setHeight also mutates width. This manifests as a Rule 2 failure too — setHeight's postcondition "width unchanged" is weakened. Preconditions are untouched (Rule 1 intact). Concretely expected, but the square gives .
Answer for grading: the invariant rule (Rule 3) is the root cause.
Level 4 — Synthesis
Exercise 4.1
You have class Bird { void fly(); void eat(); } and a Penguin that eats but cannot fly. Design a hierarchy that satisfies LSP. Give the class/interface structure.
Recall Solution 4.1
Split the capability out of the base. Only classes that can truly honour fly() should be substitutable where flight is required.
interface Bird { void eat(); }
interface FlyingBird extends Bird { void fly(); }
class Sparrow implements FlyingBird {
public void eat() { /*...*/ }
public void fly() { /*...*/ }
}
class Penguin implements Bird { // NOT FlyingBird
public void eat() { /*...*/ }
}Now any code requiring flight takes a FlyingBird — a Penguin can never be handed there, so no fly() call can ever throw. This is Interface Segregation working hand-in-hand with LSP: don't force a type to carry a method it can't honour.
Exercise 4.2
Refactor Exercise 1.1's Account/SavingsAccount so LSP holds. State the new invariant of each type.
Recall Solution 4.2
The mistake was making the overdraft rule differ between parent and child while claiming substitutability. Make the guaranteed behaviour the common base, and put the policy in siblings.
abstract class Account {
int balance;
abstract boolean canWithdraw(int amt); // contract: caller must check
void withdraw(int amt) {
if (!canWithdraw(amt)) throw new IllegalStateException();
balance -= amt;
}
}
class OverdraftAccount extends Account {
boolean canWithdraw(int amt) { return true; } // invariant: any amt ok
}
class SavingsAccount extends Account {
boolean canWithdraw(int amt) { return amt <= balance; } // invariant: never negative
}Now the precondition of withdraw is uniform: "only call when canWithdraw(amt) is true." Every subtype honours it, and the throw is part of the base contract (Rule 3 satisfied — no new exception). Callers query canWithdraw first, so no one is surprised. The two subtypes differ only in a published predicate, not in secret behaviour.
Level 5 — Mastery
Exercise 5.1
Prove, using Rule 1 and Rule 2, why a subtype method may return a narrower type but must accept a wider parameter type. Use a concrete example with Animal / Cat.
Recall Solution 5.1
Let base feed(Animal a) : Animal. A caller passes some Animal and uses the result as an Animal.
Return type (covariant, Rule 2). If the subtype returns Cat (a subtype of Animal), then every returned value is still an Animal, so holds. The caller who wanted "an Animal" is satisfied. Returning a supertype (say Object) would let non-Animal values escape — breaks Rule 2. So returns may narrow ✅.
Parameter type (contravariant, Rule 1). The caller may pass any Animal. If the subtype demanded a Cat parameter, passing a Dog (legal for the base) now fails → . To keep Rule 1, the subtype may only accept a wider type, e.g. Object, since . So parameters may widen ✅.
The two directions are forced opposite — exactly the contravariant-in / covariant-out picture.
Exercise 5.2
A base type has precondition set and postcondition set (as sets of allowed input/output values). Give the precise set conditions a subtype's must satisfy, and compute: given , , is , a valid substitute?
Recall Solution 5.2
Conditions: Rule 1 ⇒ (subtype accepts at least everything the base did). Rule 2 ⇒ (subtype returns at most what the base promised). Check: ✅. ✅. Valid substitute — yes. Both containments hold: inputs widened, outputs narrowed.
Exercise 5.3
Give a one-line justification for why LSP is the principle that makes Polymorphism trustworthy, tying back to the Dependency Inversion Principle.
Recall Solution 5.3
Dependency Inversion says "depend on abstractions (base types), not concretions." That only pays off if any concrete subtype can stand in for the abstraction without breaking correctness — and that is exactly LSP. Without LSP, code written against an abstraction would need to know which concrete subtype it holds, collapsing the whole point of depending on abstractions. LSP is the guarantee that "program to an interface" actually works.
Recall Self-test summary
Inputs widen (contravariant) ::: — subtype accepts at least the base's inputs. Outputs narrow (covariant) ::: — subtype returns at most the base's promised outputs. New exception in an override ::: violates Rule 3 (caller wasn't told to catch it). The compiler accepting an override means ::: nothing about LSP — LSP is about behaviour, not signatures.