On this page

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 S is a subtype of T, then objects of type T can be replaced with objects of type S without 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 instanceof checks. 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 {
@Override
public 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 {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // a square keeps both sides equal
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
// Client code written for Rectangle
public 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:

RuleMeaning
Do not strengthen preconditionsThe child must accept at least what the parent accepts
Do not weaken postconditionsThe child must deliver at least what the parent promises
Keep the invariantsRules that are always true for the parent (like "balance is never negative") stay true
Do not throw new unexpected exceptionsA caller does not expect UnsupportedOperationException from an override
Do not break the parent's expected behaviorLike the Rectangle setters above
public class Account {
// Promise: any positive amount can be withdrawn if the balance allows it
public void withdraw(double amount) { }
}
public class SavingsAccount extends Account {
@Override
public 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 UnsupportedOperationException or does nothing.
  • instanceof checks in the client code, such as if (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

PrincipleLink to LSP
Open/ClosedAdding subclasses extends the system only if they are safely substitutable
Interface SegregationSmall interfaces like Flyable stop children from inheriting methods they cannot support
Composition over inheritanceThe 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: UnsupportedOperationException in overrides, instanceof checks, 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.

Next TopicInterface Segregation Principle (ISP)