Skip to main content

Command Palette

Search for a command to run...

Liskov Substitution Principle (LSP) in Java

Published
5 min read
Liskov Substitution Principle (LSP) in Java

Introduction

When designing software using inheritance, a common mistake developers make is creating subclasses that change expected behavior.

This leads to:

Unexpected bugs
Broken polymorphism
Difficult maintenance

The Liskov Substitution Principle (LSP) helps prevent this.

It is the third principle in the SOLID design principles, introduced by Barbara Liskov.


Definition of LSP

Liskov Substitution Principle states:

Objects of a superclass should be replaceable with objects of its subclass without affecting program correctness.


Simple Understanding

If:

Parent obj = new Child();

Then:

👉 The program should still behave correctly

👉 The subclass must not break expected behavior

That is the core idea of LSP.


🧩 Why LSP Matters in Java

Java heavily uses:

  • Inheritance

  • Polymorphism

  • Method overriding

If subclasses behave differently than expected, it leads to:

🚩 Runtime errors
🚩 Logical bugs
🚩 Hard-to-debug systems

LSP ensures:

✔ Safe inheritance
✔ Reliable polymorphism
✔ Maintainable code


Example — Violating LSP (Rectangle–Square Problem)

This is the most famous example of LSP violation.

Step 1 — Create Rectangle Class

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 getArea() {
        return width * height;
    }
}

Step 2 — Create Square Class (Wrong Design)

class Square extends Rectangle {

    @Override
    public void setWidth(int width) {
        this.width = width;
        this.height = width; // forces square behavior
    }

    @Override
    public void setHeight(int height) {
        this.width = height;
        this.height = height; // forces square behavior
    }
}

Step 3 — Test Code

public class Main {

    public static void main(String[] args) {

        Rectangle rectangle = new Square();

        rectangle.setWidth(5);
        rectangle.setHeight(10);

        System.out.println(rectangle.getArea());
    }
}

Expected vs Actual Output

Expected:

50

Actual:

100

🚨 Why This Violates LSP

Because:

  • Rectangle expects width and height to be independent

  • Square forces width and height to be equal

So:

👉 Square changes behavior

👉 Rectangle cannot be safely replaced

👉 LSP is violated


🧩 Problem Flow (Violation Case)

Rectangle expected behavior
        ↓
Square modifies behavior
        ↓
Rectangle replaced by Square
        ↓
Unexpected result occurs
        ↓
Program correctness breaks 

Correct Design — Following LSP

Instead of forcing inheritance, we use abstraction.


Step 1 — Create Abstract Shape

abstract class Shape {

    abstract int getArea();
}

Step 2 — Create Rectangle Class

class Rectangle extends Shape {

    private int width;
    private int height;

    public Rectangle(int width, int height) {
        this.width = width;
        this.height = height;
    }

    @Override
    int getArea() {
        return width * height;
    }
}

Step 3 — Create Square Class

class Square extends Shape {

    private int side;

    public Square(int side) {
        this.side = side;
    }

    @Override
    int getArea() {
        return side * side;
    }
}

Step 4 — Test Code

public class Main {

    public static void main(String[] args) {

        Shape rectangle = new Rectangle(5, 10);
        Shape square = new Square(5);

        System.out.println(rectangle.getArea());
        System.out.println(square.getArea());
    }
}

Output

50
25

Now:

✔ No behavior change

✔ Safe substitution

✔ LSP followed


🧠 LSP Design Flow (Correct Case)

Create Parent Abstraction
            ↓
Create Independent Child Classes
            ↓
Override Behavior Safely
            ↓
Replace Parent with Child
            ↓
Program works correctly ✔

🧩 Real-World Example — Bird System

Let's look at a realistic case.


Bad Design (Violates LSP)

class Bird {

    public void fly() {
        System.out.println("Bird is flying");
    }
}

class Penguin extends Bird {

    @Override
    public void fly() {
        throw new UnsupportedOperationException("Penguins cannot fly");
    }
}

Problem:

Bird bird = new Penguin();
bird.fly(); // Runtime error

Better Design (Follows LSP)

Separate behavior properly.

abstract class Bird {

    abstract void move();
}

class FlyingBird extends Bird {

    @Override
    void move() {
        System.out.println("Flying");
    }
}

class Penguin extends Bird {

    @Override
    void move() {
        System.out.println("Swimming");
    }
}

Now:

✔ All birds move

✔ Behavior remains valid

✔ No broken expectations


🧠 Key Rules of LSP

Follow these rules while designing subclasses:


1️⃣ Do Not Change Expected Behavior

Subclass must behave like parent.

Bad:

throw new UnsupportedOperationException();

2️⃣ Do Not Strengthen Preconditions

Subclass should not require stricter input rules.


3️⃣ Do Not Weaken Postconditions

Subclass must return valid results.


4️⃣ Preserve Parent Contracts

Follow the parent class expectations.


⚠️ Signs You're Violating LSP

Watch for:

🚩 Many instanceof checks

🚩 Overridden methods throwing exceptions

🚩 Unexpected output changes

🚩 Subclass restricting parent behavior

🚩 Breaking polymorphism


Benefits of Following LSP

✔ Predictable inheritance

✔ Cleaner architecture

✔ Fewer runtime errors

✔ Better extensibility

✔ Easier maintenance


Summary

The Liskov Substitution Principle (LSP) ensures that:

✔ Subclasses behave correctly

✔ Parent objects can be replaced safely

✔ Polymorphism works properly

✔ Software becomes maintainable


🧩 One-Line Takeaway

👉 "If a subclass cannot replace its parent without breaking behavior, the design violates LSP."


Design Principles for Java Developers

Part 3 of 10

This series explains core Java design principles and SOLID principles with simple examples, real-world use cases, and interview-focused explanations to help developers write clean, maintainable, and scalable code.

Up next

Open/Closed Principle (OCP) in Java

What is Open/Closed Principle? The Open/Closed Principle (OCP) is one of the SOLID design principles. 👉 It states: Software entities (classes, modules, functions) should be open for extension but cl