🧬 Inheritance & Polymorphism · Intermediate

Liskov Substitution in Java

Subtypes must be usable wherever the parent is expected.

🧩 The mysteryIn math class, every square is a rectangle. In Java, making Square extends Rectangle can quietly break code that worked perfectly for years.

The substitution promise

The Liskov Substitution Principle (LSP): anywhere a parent type is expected, any subtype must work without surprises. Compiling isn't enough — the subtype must keep the parent's behavioral promises.

Rectangle's promise

A Rect promises that setW and setH change the sides independently: set width 5 and height 4, get area 20. A Square must keep its sides equal, so it overrides both setters to set both sides.

class Square extends Rect {
    void setW(int v) { w = h = v; }
    void setH(int v) { w = h = v; }
}
🔮 Predict it

Substitute the square

Code written for Rect receives a Square. What prints?

// Square's setters keep w == h
Rect r = new Square();
r.setW(3);
r.setH(6);
System.out.println(r.w * r.h);
  1. 18
  2. 36
  3. 9
Show the answer

36. setW(3) makes a 3×3 square, then setH(6) makes it 6×6. Code that expects Rect's promise (3 × 6 = 18) gets a result the contract never allows — an LSP violation, not just a bug.

Rules of thumb

A subtype may accept more and promise more — never less. Don't strengthen preconditions (rejecting inputs the parent accepted). Don't weaken postconditions (delivering less than promised). Don't throw where the parent wouldn't.

⚠️ The trap

The refusing subclass

Overriding a method just to throw UnsupportedOperationException breaks every caller that trusted the parent's promise. If a subtype can't do what the parent does, it probably shouldn't be a subtype.

// in ReadOnlyList extends MyList:
@Override
void add(String s) {
    throw new UnsupportedOperationException();
}
🤔 Think first

Allowed or not?

Which of these break LSP: returning a more specific type from an override, adding a brand-new method, logging before doing the same work, rejecting inputs the parent accepted?

Think about it, then reveal the answer

Only the last one. A covariant return, an extra method or extra logging all keep the parent's promises. Rejecting inputs the parent accepted (a stronger precondition) breaks callers written for the parent.

💼 In the real world

In real projects

LSP is why you should read the contract, not just the type: List.of(...) returns a List whose add throws, which surprises many developers. Test doubles and plugin implementations must honor their interface's contract too, or they break the code using them.

Key takeaways

  1. Subtypes must honor the parent's contract
  2. Don't strengthen preconditions
  3. Don't weaken postconditions
  4. Compiling isn't enough; behavior must fit too
🤯 Did you know?

Barbara Liskov introduced the principle in a 1987 keynote and received the Turing Award — computing's highest honor — in 2008.

Practice questions

Square extends Rectangle and its setWidth also changes the height. A test sets width 5 and height 4 on a Rectangle and expects area 20, but fails when given a Square. Which principle is broken?

  1. Liskov Substitution
  2. Encapsulation
  3. Single inheritance
  4. Overloading
Check your answer

Liskov Substitution. Rectangle's contract says width and height change independently. Square can't keep that promise, so it is not a safe substitute.

Rect has setters setW and setH. Square extends Rect and overrides both so they set w and h together. What does this print?

Rect r = new Square();
r.setW(5);
r.setH(4);
System.out.println(r.w * r.h);
  1. 20
  2. 16
  3. 25
  4. 0
Check your answer

16. Square's setters keep both sides equal. setW(5) makes it 5x5, then setH(4) makes it 4x4, so the area is 16, not the 20 Rect promises.

Next: the trap that bites even experts — calling overridable methods from a constructor.