Liskov Substitution Principle (LSP) – SOLID Principles in Java
Kalana De Silva
Full-Stack Developer | Spring Boot | Java | ASP.NET | C# | React | Git | API Development | Building Scalable Applications ????
Introduction
The Liskov Substitution Principle (LSP) is the third principle in the SOLID design principles for object-oriented programming. It was introduced by Barbara Liskov in 1987 and states:
Objects of a derived (sub) class must be substitutable for objects of the base (parent) class without affecting the correctness of the program.
Breaking It Down in Simple Terms
Imagine you have a system that expects a parent class (Rectangle). If you replace it with a child class (Square), everything should work without errors or unexpected results. If using a subclass breaks the logic or changes the expected behavior, then LSP is violated.
If a class inherits from another, but it modifies the behavior in unexpected ways, it should not be a subclass – it should be a separate entity!
Think of a Bird class with a fly() method. If we create a Penguin subclass but override fly() to throw an error (because penguins cannot fly), then LSP is violated. The solution? Create a FlyingBird interface instead of forcing Penguin to inherit unwanted behavior.
Why is LSP Important?
Without LSP, subclasses introduce unexpected behavior, which leads to:
? Code that breaks when subclasses replace base classes – making inheritance useless.
? Unexpected results and hidden bugs – because the subclass does not behave like its parent.
? Violations of Open/Closed Principle (OCP) – since modifications in the subclass require changes in client code.
? Difficult debugging and maintenance – because behavior is unpredictable.
The solution? Ensure that subclasses truly behave as their base class and do not introduce conflicting logic.
?? Bad Example – Without LSP
Let’s take an example of Rectangle and Square. A square is a rectangle where width and height are always the same, but making Square a subclass of Rectangle causes issues.
Problem: Inheritance Gone Wrong
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;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // Forces width and height to be equal
}
@Override
public void setHeight(int height) {
super.setWidth(height);
super.setHeight(height);
}
}
Why is this bad?
? Breaks LSP: Square does not behave like a Rectangle because setting width changes height and vice versa.
? Unexpected Behavior: When the program expects a Rectangle, using Square causes incorrect calculations.
? Violates Polymorphism: The class structure cannot be reliably extended.
Failing Scenario: Unexpected Output
public class LSPViolation {
public static void main(String[] args) {
Rectangle rectangle = new Square();
rectangle.setWidth(5);
rectangle.setHeight(10);
System.out.println("Expected Area: 5 x 10 = 50");
System.out.println("Actual Area: " + rectangle.getArea());
// Prints 100 instead of 50!
}
}
What’s Wrong Here?
?? Good Example – Applying LSP Correctly
The issue is that Square should not inherit Rectangle because its behavior is fundamentally different. Instead, we should use composition instead of inheritance.
Solution: Creating an Abstract Interface
interface Shape {
int getArea();
}
class Rectangle implements Shape {
protected int width;
protected int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int getArea() {
return width * height;
}
}
class Square implements Shape {
private int side;
public Square(int side) {
this.side = side;
}
@Override
public int getArea() {
return side * side;
}
}
Refactored Client Code (Now LSP Compliant!)
public class LSPExample {
public static void main(String[] args) {
Shape rectangle = new Rectangle(5, 10);
Shape square = new Square(5);
System.out.println("Rectangle Area: " + rectangle.getArea());
System.out.println("Square Area: " + square.getArea());
}
}
Why is this better?
? LSP Compliant: Rectangle and Square can be used interchangeably through Shape.
? No Unexpected Behavior: Square behaves as expected.
? Extensible & Maintainable: We can add new shapes without modifying existing classes.
?? Key Takeaways
?? LSP ensures subclasses behave correctly when replacing base classes.
?? Use composition over inheritance when behaviors differ significantly.
?? Violating LSP leads to runtime errors and broken implementations.
?? If a subclass requires modifying inherited behavior, it likely should not be a subclass.
The Liskov Substitution Principle (LSP) ensures that subtypes can replace base types without breaking the program. By designing correct inheritance structures and using interfaces/composition instead of forcing inheritance, we create scalable and maintainable software.
Next up: Interface Segregation Principle (ISP) – "Small, Specific Interfaces Are Better" Stay tuned!