Liskov Substitution Principle (LSP)
The Liskov Substitution Principle (LSP) is the third of the five SOLID principles. It says that a child class must be usable anywhere its parent class is used, without surprises.
Definition
If
Sis a subtype ofT, then objects of typeTcan be replaced with objects of typeSwithout breaking the program.
In simple words: if your code works with a Bird, it must keep working when you hand it any subclass of Bird. The caller should not need to know which subclass it received.
Analogy: you ask for a driver to drive your car. Any driver you get should be able to drive. If one "driver" turns out to not know how to drive, then that person was never really a driver, even if they have the job title.
Why It Matters
- Inheritance becomes safe. You can pass any subclass to existing code.
- No hidden
instanceofchecks. Callers do not need special cases for certain subclasses. - OCP depends on it. We add new subclasses to extend the system, and that only works if the new subclasses behave correctly.
- Fewer runtime surprises. A violation is not caught by the compiler. It shows up as a crash or a wrong result in production.
A Violation
public class Bird {public void fly() {System.out.println("Flying");}}public class Sparrow extends Bird { }public class Penguin extends Bird {@Overridepublic void fly() {throw new UnsupportedOperationException("Penguins cannot fly");}}public class Sky {public void makeAllFly(List<Bird> birds) {for (Bird bird : birds) {bird.fly(); // crashes when the list contains a Penguin}}}
In real life a penguin is a bird. But in this code, Penguin cannot be used in place of Bird, because Bird promises that fly() works. The code compiles fine and fails at runtime.
The Fix
The mistake was putting fly() in the parent, when not every bird can do it. Move it to a place where it is true for everyone who has it.
public abstract class Bird {public abstract void eat();}public interface Flyable {void fly();}public class Sparrow extends Bird implements Flyable {public void eat() { System.out.println("Eating seeds"); }public void fly() { System.out.println("Flying"); }}public class Penguin extends Bird {public void eat() { System.out.println("Eating fish"); }public void swim() { System.out.println("Swimming"); }}public class Sky {public void makeAllFly(List<Flyable> flyers) {for (Flyable flyer : flyers) {flyer.fly(); // every item here can really fly}}}
Now a Penguin cannot be put in the flyers list at all, so the mistake is caught by the compiler and not at runtime.
Another Classic: Square and Rectangle
In maths, a square is a rectangle. In code, it often breaks LSP:
public class Rectangle {protected int width;protected int height;public void setWidth(int width) { this.width = width; }public void setHeight(int height) { this.height = height; }public int area() { return width * height; }}public class Square extends Rectangle {@Overridepublic void setWidth(int width) {this.width = width;this.height = width; // a square keeps both sides equal}@Overridepublic void setHeight(int height) {this.width = height;this.height = height;}}// Client code written for Rectanglepublic void resize(Rectangle rectangle) {rectangle.setWidth(5);rectangle.setHeight(4);// The caller expects area() == 20// With a Square, area() == 16, so the expectation is broken}
Square changes the behavior that Rectangle promised: setting the width should not change the height. The fix is usually to not make Square extend Rectangle. Use a shared Shape interface with area() instead, and let each shape manage its own sides.
The Rules
A subclass must keep the promises of its parent. In practice:
| Rule | Meaning |
|---|---|
| Do not strengthen preconditions | The child must accept at least what the parent accepts |
| Do not weaken postconditions | The child must deliver at least what the parent promises |
| Keep the invariants | Rules that are always true for the parent (like "balance is never negative") stay true |
| Do not throw new unexpected exceptions | A caller does not expect UnsupportedOperationException from an override |
| Do not break the parent's expected behavior | Like the Rectangle setters above |
public class Account {// Promise: any positive amount can be withdrawn if the balance allows itpublic void withdraw(double amount) { }}public class SavingsAccount extends Account {@Overridepublic void withdraw(double amount) {if (amount > 1000) {throw new IllegalArgumentException("Limit is 1000"); // stricter than the parent: violation}}}
Key idea: "is-a" in code is about behavior, not about real life. A penguin is a bird in nature, but it is not a flying bird in your code.
Spotting a Violation
- An overridden method that throws
UnsupportedOperationExceptionor does nothing. instanceofchecks in the client code, such asif (bird instanceof Penguin).- A subclass that adds stricter checks than its parent.
- Comments like "do not call this on the X subclass".
- Unit tests written for the parent fail when you run them with the subclass.
The JDK has a few of these too. For example, an unmodifiable list implements List, but add() throws an exception. This is a known trade-off, and you should not copy it in your own code.
Common Mistakes
1. Using inheritance only to reuse code. If the child does not truly behave like the parent, use composition (a field that holds the other object) instead.
2. Fixing it with instanceof. Adding if (bird instanceof Penguin) continue; hides the problem, and it also breaks OCP, because every new subclass needs another check.
3. Overriding to disable a method. If a child must turn off something the parent offers, the hierarchy is wrong. Split it, as we did with Flyable.
LSP and Other Principles
| Principle | Link to LSP |
|---|---|
| Open/Closed | Adding subclasses extends the system only if they are safely substitutable |
| Interface Segregation | Small interfaces like Flyable stop children from inheriting methods they cannot support |
| Composition over inheritance | The usual way out when "is-a" does not hold |
Summary
- LSP: a subclass must be usable in place of its parent without breaking the program.
- Inheritance must follow behavior, not just real-world "is-a".
- A child must not demand more, not deliver less, keep invariants, and not throw unexpected exceptions.
- Warning signs:
UnsupportedOperationExceptionin overrides,instanceofchecks, and empty overrides. - Fix by splitting the hierarchy (
Flyable), using interfaces, or choosing composition. - A violation is not caught by the compiler, so it appears at runtime.
Interview Answer: What Is the Liskov Substitution Principle?
The Liskov Substitution Principle says that objects of a subclass should be replaceable for objects of the parent class without changing the correctness of the program. Any code that works with the parent type must keep working when given any child type.
The classic example is a Bird class with a fly() method, and a Penguin that extends it and throws an exception in fly(). A method that loops over birds and calls fly() crashes on the penguin, so the subclass is not substitutable. I fix it by moving fly() into a Flyable interface that only flying birds implement, so the compiler stops me from passing a penguin where flying is required.
The rules are that a subclass should not strengthen preconditions, not weaken postconditions, keep the parent's invariants, and not throw unexpected exceptions. I spot violations by looking for instanceof checks and overrides that throw UnsupportedOperationException. When "is-a" does not really hold, I prefer composition over inheritance.