LLD
Machine Coding
Interview Course
Java โ€ข Interview Prep
๐Ÿ“ SOLID Principles/

Liskov Substitution Principle (LSP)

Lesson 3 of 5

Liskov Substitution Principle (LSP)


What is it? (The Analogy)

Picture this: you order a rubber duck for your bathtub from an online store. It arrives, it looks like a duck, it feels like a duck, and you happily toss it in the water. It floats. Perfect.

Now imagine someone ships you a different "duck" -- it looks identical, it even quacks when you squeeze it. But when you put it in the water... it needs batteries, it starts spinning around with a propeller, and it electrocutes anyone who touches the water. Sure, it *technically* fits in the "duck-shaped" box, but it absolutely CANNOT replace your original rubber duck without breaking your bath time.

That is a Liskov Substitution Principle violation. Named after computer scientist Barbara Liskov, this principle says: if class B is a subclass of class A, you should be able to replace A with B without anything going wrong. The program should not care which one it is using. No surprises, no explosions, no batteries required.

Think of it like hiring a substitute teacher. If the substitute follows the same lesson plan, gives the same kinds of tests, and treats students the same way, nobody even notices the switch. But if the substitute shows up and says "I don't believe in homework" and starts teaching interpretive dance instead of math -- that is a Liskov violation. The substitute must honor the contract of the original.


Why do we need it?

Let us feel the pain. Imagine you are building a game with different types of birds:

java
1// Seems reasonable, right? All birds fly... right?
2public class Bird {
3    private String name;
4
5    public Bird(String name) {
6        this.name = name;
7    }
8
9    public String getName() {
10        return name;
11    }
12
13    // Every bird can fly!
14    public void fly() {
15        System.out.println(name + " is soaring through the sky!");
16    }
17
18    // Every bird can make a sound
19    public void makeSound() {
20        System.out.println(name + " is chirping!");
21    }
22}
java
1// A Sparrow IS-A Bird. Flies great. No problems.
2public class Sparrow extends Bird {
3    public Sparrow() {
4        super("Sparrow");
5    }
6    // Inherits fly() -- works perfectly!
7}
java
1// Uh oh... a Penguin IS-A Bird... but...
2public class Penguin extends Bird {
3    public Penguin() {
4        super("Penguin");
5    }
6
7    @Override
8    public void fly() {
9        // PROBLEM! Penguins cannot fly!
10        // What do we do here?!
11        throw new UnsupportedOperationException("Penguins cannot fly!");
12    }
13}

Now watch what happens when someone writes code that depends on Bird:

java
1// This code expects ALL birds to fly. It trusts the contract.
2public class BirdShow {
3    public void performFlyingAct(Bird bird) {
4        System.out.println("Ladies and gentlemen, watch this bird fly!");
5        bird.fly();  // BOOM! If bird is a Penguin, this CRASHES!
6        System.out.println("Wasn't that amazing?");
7    }
8}
java
// At runtime:
BirdShow show = new BirdShow();
show.performFlyingAct(new Sparrow());  // Works fine!
show.performFlyingAct(new Penguin());  // UnsupportedOperationException!

The pain: The BirdShow class trusted that every Bird can fly -- because that is what the parent class promises. But Penguin broke that promise. Now every piece of code that uses Bird has to check "wait, is this a penguin?" before calling fly(). That completely defeats the purpose of polymorphism!


The Classic Violation -- Square vs Rectangle

The most famous LSP violation in computer science is the Square/Rectangle problem. Every geometry textbook says "a square IS a rectangle." So naturally:

java
1// Seems mathematically correct...
2public class Rectangle {
3    protected int width;
4    protected int height;
5
6    public void setWidth(int width) {
7        this.width = width;
8    }
9
10    public void setHeight(int height) {
11        this.height = height;
12    }
13
14    public int getArea() {
15        return width * height;
16    }
17}
java
1// A square IS-A rectangle, right? Math says so!
2public class Square extends Rectangle {
3
4    // A square must keep width == height, so we override both setters
5    @Override
6    public void setWidth(int width) {
7        this.width = width;
8        this.height = width;  // Force height to match!
9    }
10
11    @Override
12    public void setHeight(int height) {
13        this.width = height;  // Force width to match!
14        this.height = height;
15    }
16}
java
1// Now watch this BREAK:
2public class AreaTest {
3    public static void main(String[] args) {
4        Rectangle rect = new Square();  // Substituting Square for Rectangle
5        rect.setWidth(5);
6        rect.setHeight(10);
7
8        // We expect area = 5 * 10 = 50 (that is how rectangles work!)
9        System.out.println("Expected: 50");
10        System.out.println("Actual: " + rect.getArea());
11        // OUTPUT: Actual: 100  <-- WRONG! Square made height override width!
12    }
13}

Aha moment: Just because something IS-A relationship holds in the real world (a square IS a rectangle in geometry) does NOT mean it should be modeled with inheritance in code. LSP is about behavioral compatibility, not mathematical taxonomy.


How to Fix It -- Step by Step

  1. Identify the broken contract -- Ask: "Can I swap the child for the parent EVERYWHERE and have everything still work?" If not, you have a violation.
  1. Stop and think: is inheritance the right tool? -- Often the answer is NO. Just because A is "a kind of" B in real life does not mean class A extends B is correct.
  1. Use interfaces to define capabilities -- Instead of a rigid hierarchy, define what things CAN DO. Not all birds fly, but all flyable things fly.
  1. Prefer composition over inheritance -- Instead of "Penguin is a Bird that can not fly," think "Penguin has swimming behavior and walking behavior."
  1. Obey the parent class contract -- If a method in the parent says it returns a positive number, the child must also return a positive number. No exceptions (literally).

Let us Build It Together

Let us fix our Bird problem using interfaces and composition. We will build a Zoo Exhibit System that properly handles all types of animals.

java
1// ========================================
2// Step 1: Define CAPABILITIES as interfaces
3// Not every animal can do everything!
4// ========================================
5
6// Can the animal fly?
7public interface Flyable {
8    void fly();
9}
10
11// Can the animal swim?
12public interface Swimmable {
13    void swim();
14}
15
16// Can the animal walk? (Almost all can)
17public interface Walkable {
18    void walk();
19}
java
1// ========================================
2// Step 2: Create a base class with ONLY
3// what ALL animals truly share
4// ========================================
5public abstract class Animal {
6    protected String name;
7    protected String species;
8
9    public Animal(String name, String species) {
10        this.name = name;
11        this.species = species;
12    }
13
14    // ALL animals can make some kind of sound -- this is safe
15    public abstract void makeSound();
16
17    // ALL animals have a name and species -- this is safe
18    public String getName() { return name; }
19    public String getSpecies() { return species; }
20
21    @Override
22    public String toString() {
23        return name + " the " + species;
24    }
25}
java
1// ========================================
2// Step 3: Each animal implements ONLY
3// the capabilities it actually has
4// ========================================
5
6// Eagles can fly AND walk
7public class Eagle extends Animal implements Flyable, Walkable {
8
9    public Eagle(String name) {
10        super(name, "Eagle");
11    }
12
13    @Override
14    public void fly() {
15        System.out.println(name + " soars majestically through the clouds!");
16    }
17
18    @Override
19    public void walk() {
20        System.out.println(name + " hops along the ground.");
21    }
22
23    @Override
24    public void makeSound() {
25        System.out.println(name + " screeches: SKREEEE!");
26    }
27}
java
1// Penguins can SWIM and WALK -- but NOT fly!
2// And that is perfectly fine. No hacks needed.
3public class Penguin extends Animal implements Swimmable, Walkable {
4
5    public Penguin(String name) {
6        super(name, "Penguin");
7    }
8
9    @Override
10    public void swim() {
11        System.out.println(name + " zooms through the water like a torpedo!");
12    }
13
14    @Override
15    public void walk() {
16        System.out.println(name + " waddles adorably.");
17    }
18
19    @Override
20    public void makeSound() {
21        System.out.println(name + " honks: NOOT NOOT!");
22    }
23}
java
1// Ducks can do EVERYTHING -- fly, swim, and walk!
2public class Duck extends Animal implements Flyable, Swimmable, Walkable {
3
4    public Duck(String name) {
5        super(name, "Duck");
6    }
7
8    @Override
9    public void fly() {
10        System.out.println(name + " flaps its wings and takes off!");
11    }
12
13    @Override
14    public void swim() {
15        System.out.println(name + " paddles calmly across the pond.");
16    }
17
18    @Override
19    public void walk() {
20        System.out.println(name + " waddle-walks on the path.");
21    }
22
23    @Override
24    public void makeSound() {
25        System.out.println(name + " says: QUACK QUACK!");
26    }
27}
java
1// ========================================
2// Step 4: The Zoo Show now uses INTERFACES,
3// not the base class, for specific acts
4// ========================================
5public class ZooShow {
6
7    // This method ONLY accepts things that can fly
8    // No penguins will sneak in here!
9    public void performFlyingAct(Flyable flyer) {
10        System.out.println("\n--- THE AMAZING FLYING ACT ---");
11        flyer.fly();  // 100% safe! Everything passed here CAN fly.
12    }
13
14    // This method ONLY accepts things that can swim
15    public void performSwimmingAct(Swimmable swimmer) {
16        System.out.println("\n--- THE SPECTACULAR SWIM SHOW ---");
17        swimmer.swim();  // 100% safe!
18    }
19
20    // This works for ANY animal
21    public void introduceAnimal(Animal animal) {
22        System.out.println("\nPlease welcome: " + animal);
23        animal.makeSound();  // Safe -- ALL animals can make a sound
24    }
25}
java
1// ========================================
2// Step 5: See it all work together!
3// ========================================
4public class ZooApp {
5    public static void main(String[] args) {
6        Eagle eddie = new Eagle("Eddie");
7        Penguin penny = new Penguin("Penny");
8        Duck donald = new Duck("Donald");
9
10        ZooShow show = new ZooShow();
11
12        // Introductions -- works for ALL animals (LSP satisfied!)
13        show.introduceAnimal(eddie);
14        show.introduceAnimal(penny);
15        show.introduceAnimal(donald);
16
17        // Flying act -- only flyable animals
18        show.performFlyingAct(eddie);    // Eagle can fly!
19        show.performFlyingAct(donald);   // Duck can fly!
20        // show.performFlyingAct(penny); // COMPILE ERROR! Penguin is not Flyable!
21        // The compiler PROTECTS us! No runtime surprise!
22
23        // Swimming act -- only swimmable animals
24        show.performSwimmingAct(penny);  // Penguin can swim!
25        show.performSwimmingAct(donald); // Duck can swim!
26        // show.performSwimmingAct(eddie); // COMPILE ERROR! Eagle is not Swimmable!
27    }
28}

Aha moment: Notice how the compiler now PREVENTS LSP violations at compile time! You literally cannot pass a Penguin to performFlyingAct() -- the code will not even compile. That is infinitely better than a runtime UnsupportedOperationException crashing your production app at 3 AM.


Visual Mental Model

java
1  BEFORE (LSP Violation):                AFTER (LSP Compliant):
2
3  +----------+                           +----------+
4  |   Bird   |                           |  Animal  |
5  |----------|                           |----------|
6  | + fly()  | <-- ALL birds must fly    | + sound()|  <-- only shared stuff
7  | +sound() |                           +----------+
8  +----------+                                |
9     /    \                              /    |    \
10    /      \                            /     |     \
11+-------+ +--------+            +-------+ +-------+ +------+
12|Sparrow| |Penguin |            | Eagle | |Penguin| | Duck |
13|       | |--------|            +-------+ +-------+ +------+
14|  OK!  | | fly(){ |            |Flyable| |Swimmable|Flyable|
15|       | | THROW! |            |Walkable |Walkable |Swimmable
16+-------+ | }      |            +-------+ +-------+ |Walkable
17          +--------+                                 +------+
18          LSP BROKEN!
19                                Every substitution is SAFE!

How This Connects to Design Patterns

LSP is the hidden backbone of several design patterns. The Strategy Pattern works because you can swap one strategy for another -- both implement the same interface, and the calling code does not care which one it gets. If a strategy violated LSP (say, by throwing exceptions the interface does not declare), the whole pattern falls apart.

The Template Method Pattern also relies heavily on LSP. The base class defines the skeleton of an algorithm, and subclasses fill in the details. If a subclass radically changes the behavior in unexpected ways, the template breaks. LSP keeps subclasses honest.


Common Mistakes and Gotchas

  • Throwing exceptions in overridden methods -- If the parent method does not throw an exception, the child should not either. This is the most common LSP violation.
  • Strengthening preconditions -- If the parent accepts any positive number, the child should not suddenly require "only even numbers." The child can be MORE accepting, never LESS.
  • Weakening postconditions -- If the parent promises to return a non-null value, the child must also return a non-null value. The child can promise MORE, never LESS.
  • Confusing IS-A in the real world with IS-A in code -- A square IS-A rectangle in geometry, but Square extends Rectangle violates LSP in code. Always think about behavioral compatibility.
  • **Using instanceof checks everywhere** -- If your code is full of if (bird instanceof Penguin) checks, you almost certainly have an LSP violation hiding underneath.
  • Empty method overrides -- Overriding a method to do nothing (@Override public void fly() { /* do nothing */ }) is a sneaky LSP violation. The caller expects something to happen!

Interview Tip

When an interviewer shows you an inheritance hierarchy and asks "what is wrong with this design?", check for LSP violations first. Ask yourself: "Can I substitute any child class for the parent without the calling code breaking?" If the answer involves instanceof checks or empty methods or thrown exceptions, say: "This violates Liskov Substitution. I would refactor this to use interfaces that represent capabilities, so the type system enforces correctness at compile time instead of failing at runtime." Interviewers LOVE hearing "compile-time safety" -- it shows you think about robust design.


Quick Quiz

  1. A ReadOnlyFile class extends File which has a write() method. ReadOnlyFile overrides write() to throw an UnsupportedOperationException. Is this an LSP violation? Why or why not?
  1. You have an ElectricCar extends Car but Car has a refuel() method. How would you redesign this hierarchy to satisfy LSP?
  1. Your colleague says: "I fixed the LSP violation by making fly() do nothing for Penguin instead of throwing an exception." Is this actually fixed? Explain.

Summary -- Key Takeaways

  • Subclasses must be fully substitutable for their parent classes. If swapping a child for a parent breaks anything, you have a violation.
  • "IS-A" in the real world does not always mean "extends" in code. Think about behavioral contracts, not taxonomy.
  • Use interfaces to model capabilities instead of deep inheritance hierarchies. Let the compiler prevent violations at compile time.
  • The "no surprises" rule: A subclass should never surprise code that expects the parent. No unexpected exceptions, no silently skipped behavior, no altered contracts.