Liskov Substitution Principle (LSP)
SOLID Principles
We have already established that inheritance represents an "is-a" relationship. Geometrically, a Square
certainly "is-a" Rectangle
. This might lead you to model it with inheritance, and a compiler would agree with you. But what happens when we consider the behavior?
Let's define a simple client method that operates on a Rectangle
. Its assumption("contract") is that setting the width and height are independent operations.
public class AreaCalculator {
public void verifyArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// Based on the contract of a Rectangle, we expect the area to be 20.
if (r.getArea() != 20) {
throw new IllegalStateException("Assertion failed! Expected 20, got " + r.getArea());
}
System.out.println("Assertion successful for: " + r.getClass().getSimpleName());
}
}
Now, imagine we create a Square
class that inherits from Rectangle
. To maintain its own invariant (that width must always equal height), it must override the setters.
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return this.width * this.height; }
}
// The deceptive Square subclass
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
super.setWidth(width);
super.setHeight(width); // To maintain the square's invariant
}
@Override
public void setHeight(int height) {
super.setWidth(height); // To maintain the square's invariant
super.setHeight(height);
}
}
What happens when we pass a Square
object to our client?
AreaCalculator calculator = new AreaCalculator();
calculator.verifyArea(new Rectangle()); // -> Prints "Assertion successful for: Rectangle"
calculator.verifyArea(new Square()); // -> Throws IllegalStateException! Expected 20, got 16.
The program breaks. Even though a Square
is-a Rectangle
structurally, it doesn't behave like one. The subclass broke the implicit behavioral contract of the superclass, making it an untrustworthy substitute.
The Principle: Upholding the Contract
This brings us to the Liskov Substitution Principle, named after Barbara Liskov. The practical definition is what matters most for an interview:
Subtypes must be substitutable for their base types without altering the correctness of the program.
This means that any piece of code written to work with a superclass T
must also work correctly if it's given an object of a subclass S
.
LSP is all about honoring the behavioral contract of the superclass. A subclass can add new behaviors, but it cannot change the inherited behaviors in a way that violates the assumptions of clients.
A Practical Refactor: Choosing the Right Abstraction
The Square/Rectangle problem shows that inheritance was the wrong tool because the "is-a" relationship was only superficial. The fix is not to make a "better" Square
subclass, but to recognize the design is flawed and choose a different one.
A better approach is to break the incorrect inheritance link and use a more general abstraction.
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
// ... same implementation as before
}
public class Square implements Shape {
private int side;
public void setSide(int side) { this.side = side; }
@Override
public int getArea() { return this.side * this.side; }
}
Now it's impossible for our AreaCalculator
to receive a Square
, because its method specifically requires a Rectangle
. We have prevented the bug by creating a more honest type hierarchy.
Interview Focus: Spotting LSP Violations
Since the compiler can't help you find LSP violations, you need to know the tell-tale signs. Here are common red flags to discuss in an interview:
- Type Checking in Client Code: If you see client code full of
if (obj instanceof Square)
checks, the abstraction is "leaky." The client is being forced to know about specific subtypes, which means polymorphism has failed. - Overridden Methods That Do Nothing: If a subclass provides an empty implementation for an inherited method or throws an
UnsupportedOperationException
, it's almost certainly an LSP violation. AnOstrich
class that inheritsfly()
fromBird
and does nothing is a classic example. - Violating the Superclass Contract: A subclass cannot have stricter preconditions (e.g., parent method accepts any integer, child only accepts positive integers) or weaker postconditions (e.g., parent guarantees a resource is closed, child leaves it open).
How LSP Governs System Design
LSP principle ensures the integrity of a system's abstractions and makes other principles like OCP possible.
- The Litmus Test for Inheritance: LSP is the primary test for whether you are using inheritance correctly. If a subclass cannot be seamlessly substituted for its superclass, your inheritance model is flawed, and you should almost certainly favor composition instead.
- The Foundation for the Open/Closed Principle: OCP relies on the ability to extend a system with new subclasses. This is only safe if those new subclasses are trustworthy substitutes. If a new "extension" violates LSP, it can break the "closed" client code, forcing it to change to accommodate the new subclass's unique, non-compliant behavior.
- Making Polymorphism Reliable: Polymorphism is a powerful tool for decoupling code, but it's built on a foundation of trust. LSP is what provides that trust. It guarantees that any object you treat polymorphically through a base class interface will honor the contract of that interface, making your system predictable and robust.
- Enabling Design by Contract: LSP is a cornerstone of a methodology called Design by Contract. This is the idea that software components should interact with each other based on formal "contracts" (preconditions, postconditions, and invariants). LSP ensures that subclasses uphold the contracts defined by their superclasses.