Liskov Substitution Principle in Java: Subclasses Should Replace Parent Classes Safely
-
Jason Yang - 12 May, 2026
- Updated 13 May, 2026
- Views —
The Liskov Substitution Principle, also known as LSP, is the third principle in SOLID.
The definition is usually written like this:
A subclass should be replaceable for its parent class without breaking the program.
At first, this sounds simple.
If Dog extends Animal, then we should be able to use Dog wherever Animal is expected.
But in real code, this principle can be tricky.
The problem is not just about whether inheritance is technically possible.
The real question is whether the child class truly behaves like the parent class expects.
Let’s look at a simple Java example.
A Bad Example: Bird and Ostrich
Imagine we have a Bird class.
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
Now we create a few child classes.
public class Sparrow extends Bird {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
public class Eagle extends Bird {
@Override
public void fly() {
System.out.println("Eagle is flying");
}
}
So far, this looks fine.
A sparrow can fly.
An eagle can fly.
But what about an ostrich?
An ostrich is a bird, but it cannot fly.
Still, if we follow this inheritance structure, we may write something like this:
public class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich cannot fly");
}
}
This code compiles.
But the design is wrong.
Where the Problem Happens
Now imagine we have a service that makes birds fly.
public class BirdService {
public void makeBirdFly(Bird bird) {
bird.fly();
}
}
The service receives a Bird.
So it naturally expects that the bird can fly.
Now let’s use it:
public class Main {
public static void main(String[] args) {
BirdService birdService = new BirdService();
Bird sparrow = new Sparrow();
Bird eagle = new Eagle();
Bird ostrich = new Ostrich();
birdService.makeBirdFly(sparrow);
birdService.makeBirdFly(eagle);
birdService.makeBirdFly(ostrich);
}
}
The result will be:
Sparrow is flying
Eagle is flying
Exception in thread "main" java.lang.UnsupportedOperationException: Ostrich cannot fly
This is an LSP violation.
Why?
Because Ostrich is a subclass of Bird, but it cannot safely replace Bird in this code.
The parent class says:
A Bird can fly.
But the child class says:
I am a Bird, but I cannot fly.
That breaks the expectation of the parent type.
The Real Issue
The issue is not that an ostrich is not a bird.
In the real world, an ostrich is definitely a bird.
The issue is that our code design made a wrong assumption:
All birds can fly.
That assumption is false.
So the problem is not with Ostrich.
The problem is with the design of Bird.
If Bird has a fly() method, then every child class of Bird is expected to support flying.
If some birds cannot fly, then fly() should not be in the base Bird class.
Applying LSP
A better design is to separate common bird behavior from flying behavior.
First, we keep only common behavior in Bird.
public class Bird {
public void eat() {
System.out.println("Bird is eating");
}
public void move() {
System.out.println("Bird is moving");
}
}
Now we create a separate interface for birds that can fly.
public interface Flyable {
void fly();
}
Now Sparrow can extend Bird and implement Flyable.
public class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
Eagle can do the same.
public class Eagle extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Eagle is flying");
}
}
But Ostrich only extends Bird.
public class Ostrich extends Bird {
public void run() {
System.out.println("Ostrich is running");
}
}
Now we create a service for flying behavior.
public class FlyingService {
public void makeFly(Flyable flyable) {
flyable.fly();
}
}
Usage:
public class Main {
public static void main(String[] args) {
FlyingService flyingService = new FlyingService();
Flyable sparrow = new Sparrow();
Flyable eagle = new Eagle();
flyingService.makeFly(sparrow);
flyingService.makeFly(eagle);
Ostrich ostrich = new Ostrich();
ostrich.eat();
ostrich.run();
// This does not compile:
// flyingService.makeFly(ostrich);
}
}
This is much safer.
An ostrich cannot be passed to FlyingService, because it does not implement Flyable.
Instead of failing at runtime, the compiler prevents the mistake.
That is a better design.
Why This Design Is Better
The new design makes the model more honest.
Before:
Bird → fly()
Ostrich extends Bird → but cannot fly
This creates a false promise.
After:
Bird → common bird behavior
Flyable → flying behavior
Sparrow → Bird + Flyable
Eagle → Bird + Flyable
Ostrich → Bird only
Now each type only supports behavior that actually makes sense.
This is the main idea of LSP.
A child class should not surprise the code that uses the parent type.
A More Practical Backend Example: Discount Policy
The bird example is useful, but let’s also look at a backend-style example.
Imagine we have a discount policy.
public class DiscountPolicy {
public int discount(int price) {
return price * 10 / 100;
}
}
Now we create a regular discount policy.
public class RegularDiscountPolicy extends DiscountPolicy {
@Override
public int discount(int price) {
return price * 10 / 100;
}
}
Then we create a no-discount policy.
public class NoDiscountPolicy extends DiscountPolicy {
@Override
public int discount(int price) {
throw new UnsupportedOperationException("Discount is not allowed");
}
}
This looks similar to the ostrich problem.
The parent class says:
A DiscountPolicy can calculate a discount.
But the child class says:
I am a DiscountPolicy, but I cannot calculate a discount.
Now imagine this order service:
public class OrderService {
public int calculateFinalPrice(int price, DiscountPolicy discountPolicy) {
int discountAmount = discountPolicy.discount(price);
return price - discountAmount;
}
}
Usage:
public class Main {
public static void main(String[] args) {
OrderService orderService = new OrderService();
int finalPrice = orderService.calculateFinalPrice(
10000,
new NoDiscountPolicy()
);
System.out.println(finalPrice);
}
}
This will fail at runtime.
That means NoDiscountPolicy is not safely substitutable for DiscountPolicy.
So this design violates LSP.
A Better Discount Policy Design
A better approach is to treat “no discount” as a valid discount policy.
Instead of throwing an exception, it can simply return 0.
First, define an interface.
public interface DiscountPolicy {
int discount(int price);
}
Now create a regular discount policy.
public class RegularDiscountPolicy implements DiscountPolicy {
@Override
public int discount(int price) {
return price * 10 / 100;
}
}
And create a no-discount policy.
public class NoDiscountPolicy implements DiscountPolicy {
@Override
public int discount(int price) {
return 0;
}
}
Now the order service can use any DiscountPolicy.
public class OrderService {
public int calculateFinalPrice(int price, DiscountPolicy discountPolicy) {
int discountAmount = discountPolicy.discount(price);
return price - discountAmount;
}
}
Usage:
public class Main {
public static void main(String[] args) {
OrderService orderService = new OrderService();
int regularPrice = orderService.calculateFinalPrice(
10000,
new RegularDiscountPolicy()
);
int noDiscountPrice = orderService.calculateFinalPrice(
10000,
new NoDiscountPolicy()
);
System.out.println("Regular discount price: " + regularPrice);
System.out.println("No discount price: " + noDiscountPrice);
}
}
Output:
Regular discount price: 9000
No discount price: 10000
Now both implementations work safely.
RegularDiscountPolicy returns a discount amount.
NoDiscountPolicy returns zero.
Neither of them breaks the expectation of DiscountPolicy.
This follows LSP.
Common Signs of LSP Violations
In real projects, LSP violations are not always obvious.
But there are some warning signs.
One common sign is this:
@Override
public void someMethod() {
throw new UnsupportedOperationException();
}
This often means the child class does not really support behavior promised by the parent class or interface.
Another warning sign is type checking:
if (bird instanceof Ostrich) {
// special case
}
If the code often checks for specific child types, it may mean the abstraction is not correct.
Another sign is empty method implementation:
@Override
public void send() {
// do nothing
}
Sometimes this is acceptable, but often it means the class was forced into the wrong inheritance structure.
LSP problems often appear when we use inheritance only because two things look similar in the real world.
But software design is not only about real-world classification.
It is also about behavior.
Inheritance Should Be About Behavior
A common mistake is thinking like this:
An ostrich is a bird.
So Ostrich should extend Bird.
That sounds reasonable.
But in object-oriented design, we also need to ask:
Can Ostrich safely behave like Bird in this program?
If the answer is no, the inheritance relationship may be wrong.
Inheritance should not only mean “is-a” in the real world.
It should also mean “can be used as” in the code.
This is an important difference.
For example:
Ostrich is a bird in the real world.
But Ostrich is not a Flyable bird in the code.
That is why separating Bird and Flyable gives us a better model.
LSP and Interface Design
LSP is closely related to interface design.
If an interface has a method, every implementation should support that method in a meaningful way.
For example:
public interface PaymentMethod {
void pay(int amount);
}
If a class implements PaymentMethod, the caller expects that pay() can be called safely.
This would be suspicious:
public class DisabledPaymentMethod implements PaymentMethod {
@Override
public void pay(int amount) {
throw new UnsupportedOperationException("Payment is disabled");
}
}
If a payment method cannot pay, maybe it should not implement PaymentMethod.
Or maybe the design needs another concept, such as:
public interface PaymentAvailabilityChecker {
boolean isAvailable();
}
The point is simple:
Do not make a class promise behavior it cannot actually provide.
That is one of the most practical ways to understand LSP.
LSP and OCP
LSP also supports OCP.
With OCP, we want to add new behavior by adding new classes.
But this only works well if the new classes can safely replace the abstraction.
For example, if we add a new DiscountPolicy, the OrderService should not break.
public class OrderService {
public int calculateFinalPrice(int price, DiscountPolicy discountPolicy) {
int discountAmount = discountPolicy.discount(price);
return price - discountAmount;
}
}
If every DiscountPolicy behaves correctly, we can add new policies easily.
But if some implementations throw unexpected exceptions or break the contract, the design becomes fragile.
So OCP depends on good LSP.
Adding new classes is not enough.
Those classes must respect the abstraction.
Before and After
Before LSP:
public class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
public class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostrich cannot fly");
}
}
Problem:
Ostrich extends Bird, but it cannot safely behave like Bird.
After LSP:
public class Bird {
public void eat() {
System.out.println("Bird is eating");
}
}
public interface Flyable {
void fly();
}
public class Sparrow extends Bird implements Flyable {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
public class Ostrich extends Bird {
public void run() {
System.out.println("Ostrich is running");
}
}
Better design:
Common bird behavior stays in Bird.
Flying behavior is separated into Flyable.
Only birds that can fly implement Flyable.
A Practical Way to Think About LSP
When I think about LSP, I usually ask these questions:
Can this child class really replace the parent class?
Does it keep the same expected behavior?
Does it throw exceptions for methods that the parent supports?
Do I need instanceof checks to handle this child class differently?
Is the inheritance based only on real-world category, or also on behavior?
These questions are more useful than memorizing the formal definition.
LSP is really about trust.
When a method accepts a parent type, it should be able to trust that any child type will behave correctly.
Summary
The Liskov Substitution Principle says that a child class should be able to replace its parent class without breaking the program.
In simple words:
If code works with a parent type, it should also work with any child type.
The bird and ostrich example shows a common problem. If Bird has a fly() method, then Ostrich should not extend Bird and throw an exception from fly().
A better design is to keep common behavior in Bird and move flying behavior into a separate Flyable interface.
The same idea applies to backend code. If a DiscountPolicy interface promises a discount() method, every implementation should return a valid discount amount. A no-discount policy should return 0, not throw an exception.
LSP helps us design inheritance and interfaces that are safe, predictable, and easier to extend.
The key question is:
Can this subclass truly replace the parent class without surprising the caller?
If the answer is yes, the design is likely following LSP.