What is Liskov Substitution Principle?
In simpler terms: if class B inherits from class A, then everywhere A is used, B should also work — without surprises.
🟢 Junior Level
Liskov Substitution Principle (LSP) is one of the five SOLID principles, which states: “Objects of a subclass must be substitutable for objects of the base class without breaking the correctness of the program.”
In simpler terms: if class B inherits from class A, then everywhere A is used, B should also work — without surprises.
Simple analogy: Think of a power outlet. If you buy any plug that conforms to the standard, it should work. If some plug is “special” and doesn’t fit — the standard is violated.
LSP violation example:
// Bad: subclass throws exception on parent's method
public class Bird {
public void fly() {
System.out.println("Flying!");
}
}
public class Ostrich extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Ostriches don't fly!");
}
}
// Client code — will break when Ostrich is substituted
public void makeBirdsFly(List<Bird> birds) {
for (Bird bird : birds) {
bird.fly(); // ClassCastException or UnsupportedOperationException!
}
}
LSP-compliant example:
// Good: separation by behavior
public interface Bird {
void sing();
}
public interface FlyingBird extends Bird {
void fly();
}
public class Sparrow implements FlyingBird {
@Override
public void sing() { System.out.println("Chirping!"); }
@Override
public void fly() { System.out.println("Flying!"); }
}
public class Ostrich implements Bird {
@Override
public void sing() { System.out.println("Hissing!"); }
// fly() is absent — and that's correct
}
When to use:
- Always when designing inheritance hierarchies
- When creating base classes that will be extended
- When working with collections of base types
🟡 Middle Level
How It Works
LSP is the most technically complex SOLID principle. It’s not just about inheritance, but about Design by Contract.
Design by Contract terms:
- Preconditions — requirements for input data (what must be true BEFORE the call)
- Postconditions — result guarantees (what will be true AFTER the call)
- Invariants — conditions that are ALWAYS true for an object
- History Constraint — object must not change past state unpredictably
LSP contract rules:
- Preconditions cannot be strengthened: Subclass cannot require more than the parent. (If parent accepts any number, subclass cannot require only positive numbers)
- Postconditions cannot be weakened: Subclass must guarantee at least the same result as the parent
- Invariants must be preserved: Object state that was true for the parent must remain true for the subclass
- History Constraint: Subclass must not change state that was considered immutable in the parent
How to Detect LSP Violation
Signs of violation:
instanceofin code: If you check the object type to call a specific method — the hierarchy is designed incorrectly- Empty methods: Subclass overrides parent’s method with an empty implementation because “it doesn’t need it”
- Throwing unexpected exceptions: As in the
UnsupportedOperationExceptionexample - Documentation “does not support”: Javadoc override says “This implementation does not support…”
Practical Application
Classic example: Square and Rectangle
// LSP violation
public 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; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // "Side effect"
}
@Override
public void setHeight(int height) {
this.width = height; // "Side effect"
this.height = height;
}
}
// Client code — will break when Square is substituted
public void testRectangle(Rectangle r) {
r.setWidth(10);
r.setHeight(20);
// For Rectangle: area = 200
// For Square: area = 400 — client is shocked!
}
Correct solution — composition, not inheritance:
// Good: interface instead of inheritance
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private int width;
private int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
@Override
public int getArea() { return width * height; }
}
public class Square implements Shape {
private int side;
public Square(int side) { this.side = side; }
@Override
public int getArea() { return side * side; }
}
Common Mistakes
-
Mistake: Inheritance for code reuse (“they have common fields!”) Solution: Use composition instead of inheritance
-
Mistake:
java.util.Collections.unmodifiableList()throwsUnsupportedOperationExceptiononadd()Solution: This is a known LSP violation. UseReadOnlyListwrapper with a read-only interface -
Mistake: Modifying parent’s invariant Solution: Clearly document invariants of each class
When LSP is Especially Important
- Public APIs and libraries (users expect predictable behavior)
- Frameworks with plugins/extensions
- Systems with polymorphic collections
🔴 Senior Level
Internal Implementation and Architecture
LSP is about behavioral predictability. Inheritance is an obligation to comply with all parent invariants. If the subclass behaves “strangely” from the perspective of the base class client — LSP is violated.
At the Senior level, it’s important to speak in terms of contract constraints:
| Constraint | Rule | Violation Example |
|---|---|---|
| Preconditions | Cannot be strengthened | SavingsAccount.withdraw requires amount <= balance, while Account allows overdraft |
| Postconditions | Cannot be weakened | FastSort may return partially sorted array |
| Invariants | Must be preserved | Square changes width when setHeight is called |
| History | State must not change unpredictably | CachingList returns stale data |
// Key question: if client code is written for Account and relies on // overdraft, substituting SavingsAccount will break its logic. // If the client does NOT rely on overdraft — no LSP violation.
LSP and Typing in Java
Java supports LSP at the compiler level through covariant return types:
public class Parent {
public Number get() { return 1; }
}
public class Child extends Parent {
@Override
public Integer get() { return 1; } // Covariance: Integer is a Number. OK.
}
However, the compiler cannot check semantic correctness (as in the Square and Rectangle example). This is the architect’s responsibility.
Contravariance of arguments (not supported in Java):
// Theoretically acceptable in LSP:
// If Parent.accept(Object o), then Child.accept(String s) — narrowing the type
// But Java doesn't allow this during override
Fragile Base Class Problem
LSP protects us from situations where changes to a subclass’s internal logic break the logic that the client relies on.
- Classic example:
ArrayListvsCollections.unmodifiableList()unmodifiableListis a wrapper overList, but throwsUnsupportedOperationExceptiononadd()- This is an LSP violation, as a client expecting a
Listis not ready for this behavior
Architectural Trade-offs
Strict LSP compliance:
- ✅ Pros: Predictable behavior, reliable inheritance, minimum runtime errors
- ❌ Cons: Limits code reuse, requires more careful design
Composition instead of inheritance:
- ✅ Pros: Complete behavioral freedom, no obligations to parent’s contract
- ❌ Cons: More boilerplate code, need to delegate methods
Edge Cases
- Template Method Pattern: Base class defines algorithm, subclasses — steps. Does this violate LSP?
- Answer: No, if subclasses don’t change algorithm invariants. But if a step can “break” the algorithm — it’s a violation
- Decorators: Wrappers add behavior. Do they violate LSP?
- Answer: No, if they delegate all methods to the base object and don’t change its invariants
- Proxies and Mocks: Dynamic proxies for testing
- Answer: May violate LSP if they don’t reproduce all postconditions of the original
Performance
- Virtual dispatch: Calling an overridden method = indirection. JIT performs inline caching and class hierarchy analysis
- Inlining barrier: If JIT cannot prove that a method is not overridden — it cannot inline. This affects performance in hot path
- Escape Analysis: When creating subclass objects, JVM can optimize allocation, but only if the hierarchy is known at JIT stage
Production Experience
Real production scenario:
A project used BaseRepository with a save(Entity) method. One subclass AuditRepository overrode save() and added logging after saving. Another subclass ValidatingRepository — validation before saving.
When CachingRepository appeared, which overrode save() and added caching instead of DB — everything broke: validation didn’t work, audit didn’t log.
Solution: Replaced inheritance with a chain of decorators (Decorator Pattern), where each adds its behavior to the base save().
Monitoring and Diagnostics
How to detect LSP violation in code:
- Code Review signs:
- If
instanceofis used to bypass different behaviors instead of polymorphism — this is an LSP violation. Pattern matching with sealed types is a legitimate exception. - Empty override methods
throw new UnsupportedOperationException(...)
- If
- Unit Test signs:
- Tests for the base class fail when run with the subclass
- Different behavior of the same method for different subclasses
- Tools:
- Mutation Testing (Pitest): verifies that tests catch behavior changes
- ArchUnit: verifies that subclasses don’t use forbidden patterns
Best Practices for Highload
- Sealed Interfaces (Java 17+): Limit the hierarchy — compiler checks exhaustiveness, JIT optimizes dispatch
- Immutability: Immutable objects are easier to make LSP-compatible (no mutable state = no invariant violation)
- Contract Testing: Write tests for base type contracts and run them for all subclasses
Relationship with Other Principles
- LSP ← OCP: If subclasses are correctly substitutable, the system is open for extension
- LSP ← SRP: Small classes with one responsibility are easier to inherit correctly
- LSP → ISP: Interface segregation automatically helps comply with LSP, as contracts become smaller
Summary for Senior
- LSP is about behavioral predictability, not inheritance syntax
- Inheritance is an obligation to comply with all parent invariants
- If the subclass behaves “strangely” from the base class client’s perspective — LSP is violated
- Remember: Square is geometrically a special case of Rectangle, but in OOP
Squarecannot inherit fromRectanglewithsetWidth/setHeightmethods - Composition over Inheritance: Often LSP is violated when trying to save code through inheritance. A Senior developer will prefer composition
🎯 Interview Cheat Sheet
Must know:
- LSP: subclass objects must be substitutable for base class objects without breaking correctness
- Design by Contract: preconditions cannot be strengthened, postconditions cannot be weakened, invariants must be preserved
- Classic violation example — Square extends Rectangle (side effects in setters)
UnsupportedOperationExceptionin a subclass — a clear sign of LSP violationinstanceofin code to bypass different behaviors = LSP violated- Composition is preferred over inheritance to comply with LSP
java.util.Date/java.sql.Date— a known LSP violation in JDK
Common follow-up questions:
- How to detect LSP violation? — Base class tests fail for subclass,
instanceofchecks, empty override methods - What is Fragile Base Class Problem? — Changing the parent unpredictably breaks all subclasses
- Can LSP be violated with interfaces? — Yes, if the implementation throws unexpected exceptions or changes the contract
- What is Contract Testing? — Common tests for an interface, run for all implementations
Red flags (DO NOT say):
- “LSP is just about inheritance” (no — it’s about behavioral predictability and contracts)
- “Square inherits Rectangle — that’s fine” (classic violation example)
- “If the compiler doesn’t complain — LSP is complied” (LSP is a semantic, not syntactic principle)
Related topics:
- [[6. Give an example of Liskov Substitution principle violation]]
- [[10. What is composition and inheritance]]
- [[11. When is it better to use composition instead of inheritance]]
- [[7. What is Interface Segregation principle]]