Blog
Liskov Substitution Principle in Java
Liskov Substitution Principle (LSP) in Java
The Liskov Substitution Principle (LSP) is one of the five SOLID principles of object-oriented programming. It helps developers design Java applications that are flexible, maintainable, reusable, and less prone to unexpected behavior.
What is the Liskov Substitution Principle?
The Liskov Substitution Principle states that objects of a superclass should be replaceable
with objects of its subclasses without affecting the correctness of the application.
In simple terms, if class B is a subclass of class A, then we should be able to use
an object of B wherever an object of A is expected, without breaking the program.
The principle was introduced by Barbara Liskov and is commonly described as:
If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the
desirable properties of the program.
Why is LSP Important?
Inheritance is one of the core features of Java. However, simply extending a class does not necessarily mean
that the subclass is a valid behavioral replacement for its parent.
Following LSP provides several benefits:
- Improves code reliability and predictability.
- Reduces unexpected behavior in subclasses.
- Makes polymorphism safer to use.
- Improves maintainability and extensibility.
- Encourages better class and interface design.
- Makes unit testing easier.
Understanding LSP with a Simple Example
Consider a simple Java class hierarchy:
class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
class Sparrow extends Bird {
@Override
public void fly() {
System.out.println("Sparrow is flying");
}
}
Here, Sparrow extends Bird and provides valid behavior for the
fly() method.
Bird bird = new Sparrow();
bird.fly();
The program works as expected because a Sparrow behaves consistently with the expectations of
the Bird abstraction.
A Common LSP Violation: Bird Example
A common example used to explain LSP is the relationship between birds and flying birds.
class Bird {
public void fly() {
System.out.println("Bird is flying");
}
}
class Eagle extends Bird {
@Override
public void fly() {
System.out.println("Eagle is flying");
}
}
class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins cannot fly");
}
}
The problem is that Penguin is a Bird, but it cannot satisfy the behavior promised by
the parent class because the inherited fly() operation does not make sense for it.
Code that expects every Bird to fly may fail unexpectedly:
public static void makeBirdFly(Bird bird) {
bird.fly();
}
makeBirdFly(new Eagle()); // Works
makeBirdFly(new Penguin()); // Throws an exception
This is a sign that the class hierarchy violates the Liskov Substitution Principle.
How to Fix the Bird Example
Instead of placing fly() in the base Bird class, we can separate flying behavior from
the general bird abstraction.
class Bird {
public void eat() {
System.out.println("Bird is eating");
}
}
interface FlyingBird {
void fly();
}
class Eagle extends Bird implements FlyingBird {
@Override
public void fly() {
System.out.println("Eagle is flying");
}
}
class Penguin extends Bird {
// Penguin does not implement FlyingBird
}
Now the design is more accurate. All birds can eat, but only birds capable of flying implement
FlyingBird.
public void makeBirdFly(FlyingBird bird) {
bird.fly();
}
makeBirdFly(new Eagle());
This design prevents clients from receiving an object that cannot support the behavior they expect.
Rectangle and Square Example
Another classic example of LSP involves Rectangle and Square.
Mathematically, a square is a rectangle. However, modeling Square as a subclass of
Rectangle can introduce behavioral problems when the rectangle allows width and height to change
independently.
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) {
this.width = width;
this.height = width;
}
@Override
public void setHeight(int height) {
this.width = height;
this.height = height;
}
}
Consider code that expects the width and height of a rectangle to be independently changeable:
Rectangle rectangle = new Square();
rectangle.setWidth(5);
rectangle.setHeight(10);
System.out.println(rectangle.getArea());
The calling code expects an area of 50, but the square implementation changes both dimensions,
resulting in an area of 100.
The subclass is therefore not behaving like the abstraction expected by the calling code.
LSP and Polymorphism
LSP is closely related to polymorphism. Java allows a parent reference to point to an object
of a child class:
Animal animal = new Dog();
This substitution is useful only when the Dog actually satisfies the behavioral expectations of
Animal.
LSP ensures that inheritance and polymorphism represent genuine is-a relationships in terms of
behavior, not merely shared attributes or code.
Key Rules of the Liskov Substitution Principle
When designing subclasses in Java, keep the following rules in mind:
1. A Subclass Should Preserve Parent Behavior
A subclass should honor the expectations established by its parent class or interface.
2. Do Not Strengthen Preconditions
A subclass should not require stricter input conditions than the parent type requires.
3. Do Not Weaken Postconditions
A subclass should continue to guarantee the results promised by the parent abstraction.
4. Avoid Unexpected Exceptions
A subclass should not suddenly reject operations that are valid according to the parent contract.
5. Preserve Invariants
Important rules and constraints established by the parent abstraction should remain valid in subclasses.
LSP Example Using Interfaces
Interfaces often make it easier to follow LSP because they allow developers to define smaller and more focused
contracts.
interface PaymentProcessor {
void processPayment(double amount);
}
class CreditCardProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing credit card payment: " + amount);
}
}
class UpiProcessor implements PaymentProcessor {
@Override
public void processPayment(double amount) {
System.out.println("Processing UPI payment: " + amount);
}
}
Both implementations satisfy the same contract, so a client can safely substitute one implementation for
another.
public void makePayment(PaymentProcessor processor, double amount) {
processor.processPayment(amount);
}
makePayment(new CreditCardProcessor(), 1000);
makePayment(new UpiProcessor(), 1000);
Signs That Your Code Violates LSP
You may have an LSP violation when you notice patterns such as:
- Subclasses frequently throw
UnsupportedOperationException. - Client code contains many
instanceofchecks. - Client code behaves differently for specific subclasses.
- A subclass changes the expected meaning of a parent method.
- Developers need to document special cases for individual subclasses.
- Replacing the parent object with a subclass causes tests to fail unexpectedly.
LSP in Java Application Design
LSP is particularly important when building large Java applications that rely heavily on inheritance,
interfaces, dependency injection, and polymorphism.
For example, if a service depends on an interface:
interface NotificationService {
void send(String message);
}
Different implementations should honor the same contract:
class EmailNotificationService implements NotificationService {
@Override
public void send(String message) {
System.out.println("Sending email: " + message);
}
}
class SmsNotificationService implements NotificationService {
@Override
public void send(String message) {
System.out.println("Sending SMS: " + message);
}
}
The consuming code can work with the abstraction without needing special logic for every implementation.
LSP and Unit Testing
LSP also has a strong connection with unit testing. A good subclass should be able to pass tests written against
the parent abstraction.
NotificationService service = new EmailNotificationService();
service.send("Hello");
service = new SmsNotificationService();
service.send("Hello");
If a client needs completely different rules, assumptions, or exception handling for one implementation, the
abstraction may not be correctly designed.
Liskov Substitution Principle vs Inheritance
LSP does not say that inheritance is bad. Instead, it encourages developers to use inheritance only when the
subclass can genuinely fulfill the behavioral contract of the parent.
A useful design question is:
“Can this child object be used anywhere the parent object is expected without surprising the client?”
If the answer is no, consider using composition, interfaces, or a different class hierarchy.
Liskov Substitution Principle Best Practices
- Design abstractions around behavior rather than implementation details.
- Keep interfaces small and focused.
- Use composition when inheritance creates behavioral conflicts.
- Do not force subclasses to implement unsupported operations.
- Define clear contracts for classes and interfaces.
- Use polymorphism without requiring subclass-specific conditions in client code.
- Write tests against abstractions and verify that implementations honor their contracts.
Benefits of Following LSP
| Benefit | Description |
|---|---|
| Maintainability | Changes to subclasses are less likely to break existing client code. |
| Reusability | Implementations can be substituted through common abstractions. |
| Testability | Implementations can be tested through the same abstraction. |
| Flexibility | New implementations can be introduced with fewer changes to consumers. |
| Reliability | Objects behave consistently with the contracts expected by clients. |
Liskov Substitution Principle in SOLID
LSP is the third principle in the SOLID design principles:
- S â Single Responsibility Principle
- O â Open/Closed Principle
- L â Liskov Substitution Principle
- I â Interface Segregation Principle
- D â Dependency Inversion Principle
LSP works together with the other SOLID principles to encourage loosely coupled and maintainable object-oriented
software.
LSP in Java: Simple Interview Definition
A commonly useful interview definition is:
A subclass should be able to replace its parent class without changing the expected behavior of the application.
Conclusion
The Liskov Substitution Principle is an important Java design principle that ensures
subclasses remain valid substitutes for their parent abstractions. It is not enough for a subclass to inherit
methods or properties; it must also honor the behavioral contract of the parent.
Problems such as unsupported operations, unexpected exceptions, special-case checks, and inconsistent behavior
are often signs of an LSP violation. By creating focused abstractions, preserving contracts, and choosing
composition when inheritance is inappropriate, developers can build cleaner and more reliable Java applications.
Understanding LSP is especially valuable for Java developers, software engineers, and software testers
because it improves object-oriented design, polymorphism, unit testing, and long-term code maintainability.
Frequently Asked Questions (FAQ)
What does LSP stand for in Java?
LSP stands for Liskov Substitution Principle. It is the third principle of the SOLID design
principles.
Who introduced the Liskov Substitution Principle?
The principle was introduced by computer scientist Barbara Liskov.
What is the main purpose of LSP?
The main purpose of LSP is to ensure that subclasses can replace their parent types without causing incorrect
or unexpected application behavior.
Is inheritance required to apply LSP?
LSP is most commonly discussed in the context of inheritance and subtyping, but the underlying idea applies to
any abstraction where one type is expected to substitute for another.
How can I avoid LSP violations in Java?
Use clear contracts, small interfaces, meaningful inheritance hierarchies, composition where appropriate, and
avoid forcing subclasses to support operations that do not make sense for them.