Liskov Substitution Principle (LSP) in Java with Real-World Examples | SOLID Principles Explained
When developers first learn inheritance, they often think: "If Dog extends Animal, then everything is correct." But inheritance alone does not guarantee a good design. A child class…
When developers first learn inheritance, they often think:
“If
DogextendsAnimal, then everything is correct.”
But inheritance alone does not guarantee a good design.
A child class should not only inherit from its parent—it should also behave like its parent.
This is exactly what the Liskov Substitution Principle (LSP) teaches us.
In this article, we’ll understand LSP from scratch using simple Java examples, real-world scenarios, interview-friendly explanations, common violations, and different ways to solve them.
What is the Liskov Substitution Principle (LSP)?
LSP is the L in the SOLID principles.
Official Definition
If class B extends class A, then we should be able to replace an object of A with an object of B without breaking the correctness of the program.
While technically correct, this definition is difficult to remember during interviews.
Interview-Friendly Definition
You can explain LSP like this:
A child class should be able to replace its parent class without changing the expected behavior of the application.
Or even simpler:
If a method expects a parent object, it should work correctly even when we pass any child object.
Understanding LSP with a Simple Example
Let’s create a parent class.
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
Now create a child class.
class Dog extends Animal {
@Override
public void eat() {
System.out.println("Dog is eating");
}
}
Now use both classes.
public class Main {
public static void main(String[] args) {
Animal animal1 = new Animal();
animal1.eat();
Animal animal2 = new Dog();
animal2.eat();
}
}
Output
Animal is eating
Dog is eating
What Happened Here?
Initially, we had:
Animal animal = new Animal();
Later, we replaced it with:
Animal animal = new Dog();
Notice that the rest of the program did not change.
The method call is still:
animal.eat();
The application still works correctly.
This means:
Animal
▲
│
Dog
Animal animal = new Dog();
The child object successfully replaced the parent object.
This follows the Liskov Substitution Principle.
But the Output Changed!
This is one of the most common interview questions.
Many people think:
“If the output changes, doesn’t that violate LSP?”
No.
LSP does not say that the output must remain identical.
It says:
The expected behavior should remain correct.
For example,
Parent output:
Animal is eating
Child output:
Dog is eating
The output is different, but it is still valid.
The application continues to work correctly.
No exception occurs.
No unexpected behavior occurs.
Therefore, LSP is still satisfied.
What Does “Expected Behavior” Mean?
Think of every parent class as making a contract.
For example,
class Animal {
public void eat() {
System.out.println("Animal is eating");
}
}
The contract is:
Every Animal can eat.
Now suppose:
Animal animal = new Dog();
Dog can eat.
The contract is still satisfied.
Now imagine:
Animal animal = new Cat();
Cat can also eat.
Again, the contract is satisfied.
Different animals may eat differently, but every animal can still eat.
That is why LSP holds.
LSP Violation Example
Let’s consider another example.
class Bird {
public void fly() {
System.out.println("Flying");
}
}
Now create Penguin.
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins can't fly");
}
}
Now write:
Bird bird = new Penguin();
bird.fly();
Output
Exception
UnsupportedOperationException
Why Is This an LSP Violation?
The parent class promises:
Every Bird can fly.
But Penguin says:
I cannot fly.
So when we replace
Bird bird = new Bird();
with
Bird bird = new Penguin();
the application crashes.
The child class breaks the parent’s contract.
This violates the Liskov Substitution Principle.
Visual Representation
Correct
Animal
▲
│
Dog
Animal animal = new Dog();
✔ Works correctly
Incorrect
Bird
▲
│
Penguin
Bird bird = new Penguin();
✘ Throws Exception
Parent Contract vs Child Contract
Parent says:
Every Bird can fly.
Child says:
I cannot fly.
These contracts are different.
The child no longer behaves like the parent.
Therefore, inheritance is incorrect.
How Can We Fix an LSP Violation?
One important thing to remember is:
There is no single solution.
The correct solution depends on the business requirement and the design.
Let’s look at the most common approaches.
Solution 1: Extract an Interface (Recommended)
Instead of making every bird fly,
remove fly() from Bird.
class Bird {
public void eat() {
System.out.println("Eating");
}
}
Create a separate interface.
interface Flyable {
void fly();
}
Flying birds implement it.
class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Flying");
}
}
Penguin simply becomes:
class Penguin extends Bird {
}
Now,
Flyable bird = new Sparrow();
bird.fly();
Everything works correctly.
This is one of the most common solutions.
Solution 2: Split the Hierarchy
Instead of
Bird
▲
Penguin
Create separate categories.
Bird
/ \
FlyingBird NonFlyingBird
▲ ▲
Sparrow Penguin
Example:
class Bird {
}
class FlyingBird extends Bird {
public void fly() {
System.out.println("Flying");
}
}
class Penguin extends Bird {
}
Now Penguin never inherits fly().
Solution 3: Use Composition Instead of Inheritance
Sometimes inheritance is simply the wrong choice.
Instead of
Penguin extends Bird
Use
Penguin HAS-A Bird
Example:
class Bird {
public void eat() {
System.out.println("Eating");
}
}
class Penguin {
private Bird bird = new Bird();
public void eat() {
bird.eat();
}
public void swim() {
System.out.println("Swimming");
}
}
Now there is no incorrect inheritance.
Solution 4: Redesign the Parent Contract
Maybe the parent class itself is wrong.
Instead of saying:
class Bird {
public void fly() {
}
}
Say:
abstract class Bird {
public abstract void move();
}
Now Sparrow:
class Sparrow extends Bird {
@Override
public void move() {
System.out.println("Flying");
}
}
Penguin:
class Penguin extends Bird {
@Override
public void move() {
System.out.println("Walking and Swimming");
}
}
Every bird can move.
The contract now matches reality.
Solution 5: Remove the Inheritance
Sometimes the inheritance itself provides no value.
Instead of forcing inheritance,
Bird
Penguin
Sparrow
can simply be independent classes if they do not share meaningful behavior.
Which Solution Should You Choose?
| Situation | Best Solution |
|---|---|
| Only some classes support a behavior | Extract an interface |
| Parent contract is incorrect | Redesign the parent |
| Inheritance is not appropriate | Use composition |
| Need separate categories | Split the hierarchy |
| Inheritance adds no value | Remove inheritance |
Real-World Example
Suppose you have a payment system.
class Payment {
public void pay(double amount) {
System.out.println("Payment Successful");
}
}
Child classes:
class CreditCardPayment extends Payment {
@Override
public void pay(double amount) {
System.out.println("Paid using Credit Card");
}
}
class UPIPayment extends Payment {
@Override
public void pay(double amount) {
System.out.println("Paid using UPI");
}
}
Now,
public static void checkout(Payment payment) {
payment.pay(1000);
}
Usage:
checkout(new CreditCardPayment());
checkout(new UPIPayment());
Both child classes can replace the parent without breaking the application.
LSP is satisfied.
Common Interview Questions
Q1. What is LSP?
Answer:
A child class should be able to replace its parent class without changing the expected behavior of the application.
Q2. Does the output have to remain the same?
Answer:
No.
The output can be different.
The important thing is that the program should continue to work correctly and the child should honor the parent’s contract.
Q3. Why is Penguin a bad example?
Because the parent promises every bird can fly, but Penguin cannot.
The child breaks the parent’s contract, causing runtime errors.
Q4. How do you fix an LSP violation?
There is no single solution.
Depending on the design, you can:
- Extract an interface
- Redesign the parent class
- Use composition instead of inheritance
- Split the hierarchy
- Remove inheritance
Q5. What is meant by the “parent’s contract”?
A contract is the behavior that the parent class guarantees to its clients.
Every child class must honor that guarantee.
If the child weakens, removes, or breaks that guarantee, it violates LSP.
Key Takeaways
- Inheritance alone does not guarantee a good design.
- A child class must honor the behavior promised by its parent.
- Replacing a parent object with a child object should not break the application.
- Different implementations are acceptable, but broken behavior is not.
- LSP is about behavioral compatibility, not identical output.
- When a child cannot fulfill the parent’s contract, rethink the design using interfaces, composition, hierarchy redesign, or a better abstraction.
Final Interview Answer (30 Seconds)
The Liskov Substitution Principle states that if a child class extends a parent class, the child should be able to replace the parent anywhere in the application without breaking the program. The child can provide its own implementation through method overriding, so the output may differ, but it must still honor the parent’s contract. If replacing the parent with the child causes runtime errors or unexpected behavior—like a
PenguinextendingBirdand throwing an exception fromfly()—then LSP is violated. Depending on the design, we can fix it by extracting interfaces, redesigning the hierarchy, using composition, or correcting the parent abstraction.
This explanation is simple, technically accurate, and suitable for Java/Spring Boot interviews at product-based companies.