Give an Example of Liskov Substitution Principle Violation
LSP violations are often masked as "code reuse". We try to inherit one class from another simply because they share common fields, ignoring differences in their behavior.
🟢 Junior Level
LSP violations are often masked as “code reuse”. We try to inherit one class from another simply because they share common fields, ignoring differences in their behavior.
Example 1: The “Lazy” Subclass When a subclass overrides a parent’s method and throws an exception or does nothing.
// Bad: subclass doesn't support parent's method
public class Bird {
public void fly() {
System.out.println("Flying!");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
throw new UnsupportedOperationException("Penguins don't fly!");
}
}
// Will break:
public void letBirdsFly(List<Bird> birds) {
for (Bird bird : birds) {
bird.fly(); // UnsupportedOperationException for Penguin!
}
}
Example 2: Rectangle and Square
// Bad: Square changes setter behavior
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int w) {
this.width = w;
this.height = w; // Side effect!
}
@Override
public void setHeight(int h) {
this.width = h; // Side effect!
this.height = h;
}
}
// Client expects one behavior, gets another:
public void test(Rectangle r) {
r.setWidth(5);
r.setHeight(10);
// For Rectangle: width=5, height=10
// For Square: width=10, height=10 — bug!
}
How to fix?
- Extract a common interface: Instead of
Square extends Rectangle, create aShapeinterface with agetArea()method - Composition: If you need another class’s functionality, don’t inherit it — make it a field
// Good: composition instead of inheritance
public class ReadOnlyList<T> implements Iterable<T> {
private final List<T> list;
public ReadOnlyList(List<T> list) { this.list = list; }
// Read-only methods only...
}
🟡 Middle Level
Sneaky LSP Violation Examples
1. The “Lazy” Subclass Problem (The Refusal of Bequest)
When a subclass overrides a parent’s method and throws an exception or does nothing.
- Real example from Java:
java.util.Dateandjava.sql.Date - Essence:
java.sql.Date(subclass) “disables” time handling (hours, minutes, seconds), even though the base classjava.util.Datesupports them. If code expects millisecond precision and receivessql.Date, it will break. - Calling with java.sql.Date will throw IllegalArgumentException!
- getHours() in java.sql.Date is not supported (throws exception), although the parent class supports this method.
2. Invariant Violation (The Account Example)
public class Account {
protected double balance;
public void withdraw(double amount) {
this.balance -= amount;
}
}
public class SavingsAccount extends Account {
@Override
public void withdraw(double amount) {
if (amount > balance) throw new RuntimeException("No overdraft!"); // Strengthening precondition
super.withdraw(amount);
}
}
Why is this a violation?
If client code is written for the base Account, it may rely on the balance being able to go negative (overdraft). Substituting SavingsAccount will break the client’s logic, which doesn’t expect an exception.
Not every
ifis a violation. Violation is when the parent contract allows an action, but the subclass forbids it. If both classes agree on the contract (both forbid overdraft) — no LSP violation.
3. Hidden State Corruption (Side Effects)
Classic example with Rectangle and Square.
- Client:
rect.setWidth(10); rect.setHeight(20); - For Rectangle: area 200
- For Square: area 400 (because
setHeightrewrotewidth) Result: Client is shocked, the invariant “changing height doesn’t affect width” is broken.
Why Do We Do This? (Root Cause)
- Desire to save effort: “Why write the
namefield again if it’s already inUser?”. So we inheritAdminfromUser, even though they have different life cycles. - Incorrect hierarchy: We model the world “as it is”, not “as it is used”. In biology, an ostrich is a bird; in OOP,
Ostrichcannot inherit fromFlyingBird.
How to Recognize Violation
Signs:
instanceofchecks: If you check the type to call specific logicUnsupportedOperationException: Method “not supported” in subclass- Empty overrides: Method overridden with an empty implementation
- Different test behavior: Base class tests fail for subclass
How to Fix
1. Extract a Common Interface
// Instead of Square extends Rectangle:
public interface Shape {
int getArea();
}
public class Rectangle implements Shape {
private int width, height;
// setters work independently
@Override
public int getArea() { return width * height; }
}
public class Square implements Shape {
private int side;
@Override
public int getArea() { return side * side; }
}
2. Composition (Delegation)
// GOOD: Using composition instead of crooked inheritance
public class ReadOnlyList<T> implements Iterable<T> {
private final List<T> list; // Delegate
public ReadOnlyList(List<T> list) { this.list = list; }
@Override
public Iterator<T> iterator() { return list.iterator(); }
public int size() { return list.size(); }
// Read-only methods only — no add/remove
}
3. Splitting the Hierarchy
// Instead of Penguin extends Bird:
public interface Bird {
void sing();
}
public interface FlyingBird extends Bird {
void fly();
}
public class Sparrow implements FlyingBird {
@Override public void sing() { /* ... */ }
@Override public void fly() { /* ... */ }
}
public class Penguin implements Bird {
@Override public void sing() { /* ... */ }
// fly() is absent — and that's correct
}
🔴 Senior Level
Internal Implementation and Architecture
At the Senior level, it’s important to understand: LSP violation is not just a “code bug”. It’s a contract breach that may only manifest at runtime, in production, under certain conditions.
LSP is about Design by Contract: a subclass can only weaken input requirements and strengthen output guarantees.
| Constraint | Rule | Violation |
|---|---|---|
| Preconditions | Cannot be strengthened | SavingsAccount requires amount <= balance |
| Postconditions | Cannot be weakened | FastSort returns partially sorted array |
| Invariants | Must be preserved | Square changes width on setHeight |
| History | State must not change unpredictably | CachingList returns stale data |
Real Examples from JDK
java.util.Date and java.sql.Date
// java.util.Date — stores time with millisecond precision
// java.sql.Date — subclass, but "disables" time (hours, minutes, seconds)
public class java.sql.Date extends java.util.Date {
// Many methods are overridden and throw IllegalArgumentException
@Override
public int getHours() {
throw new IllegalArgumentException();
}
}
This is one of the most famous LSP violations in the Java Standard Library.
Collections.unmodifiableList()
List<String> modifiable = new ArrayList<>();
List<String> unmodifiable = Collections.unmodifiableList(modifiable);
// unmodifiable is implemented as a wrapper over List,
// but add() throws UnsupportedOperationException
unmodifiable.add("test"); // BOOM!
Consequences for Highload and Testing
- Broken Unit Tests: Tests written for the base class start to “flaky” or fail when run with a subclass object. This is a sure sign of LSP violation
- Heisenbugs: Bugs manifest only under certain conditions, when an object of the wrong type “leaks” into the system through DI or a factory. Heisenbug — a bug that manifests only under certain conditions (specific object type in DI, specific load) and is hard to reproduce. Named after Heisenberg’s uncertainty principle.
- Regression Testing: Every new subclass requires a full run of the base type’s tests
Architectural Trade-offs
Inheritance (violates LSP):
- ✅ Pros: Code reuse, minimum boilerplate
- ❌ Cons: Rigid contract, LSP violation, fragility
Composition (complies with LSP):
- ✅ Pros: Complete behavioral freedom, loose coupling
- ❌ Cons: More boilerplate, need to delegate methods
Interfaces (complies with LSP):
- ✅ Pros: Clean contract, minimum obligations
- ❌ Cons: No implementation reuse
Edge Cases
- Template Method with “optional” steps: Base class defines algorithm, subclass can skip a step
- Problem: Skipping a step may break the algorithm’s invariant
- Solution: Chain of Responsibility or Decorator instead of Template Method
- Abstract Classes with default “do nothing” implementation: Method does “nothing” in the base class
- Problem: Subclasses may forget to override — silent contract breach
- Solution: Abstract methods without implementation (forced override)
- Proxies and Dynamic Proxies: Dynamically created objects
- Problem: May not reproduce all postconditions of the original
- Solution: Contract Testing for proxies
Performance
- Virtual dispatch + LSP: When JIT is confident that all subclasses comply with LSP, it can more aggressively perform devirtualization and inlining
- LSP violation cost: If a subclass violates the contract, JIT optimizations may produce incorrect results (theoretically; in practice, JVM performs deoptimization)
- Branch prediction:
instanceofchecks to bypass LSP violation destroy branch prediction
Production Experience
Real production scenario:
In an e-commerce project, BaseOrderProcessor had a calculateTax(Order) method. Subclass InternationalOrderProcessor overrode it and applied a different formula. Problem: the base class also called calculateTax inside processOrder, and the developers of InternationalOrderProcessor didn’t account for calculateTax being called twice — for the product and for shipping.
Result: Incorrect tax calculation for international orders. Losses — $50K per month before detection.
Solution: Extracted TaxCalculator into a separate component (composition) instead of inheriting calculation logic.
Monitoring and Diagnostics
How to detect LSP violation:
- Contract Tests:
// Common test for all Shape implementations public abstract class ShapeContractTest<T extends Shape> { abstract T createShape(); @Test void areaIsAlwaysNonNegative() { assertThat(createShape().getArea()).isGreaterThanOrEqualTo(0); } } - Metrics:
- Number of
instanceofchecks - Number of
UnsupportedOperationException - Number of empty override methods
- Number of
- Tools:
- Pitest (Mutation Testing): verifies that tests catch behavior changes
- ArchUnit: architectural tests on inheritance
Best Practices for Highload
- Sealed Interfaces (Java 17+): Limit the hierarchy — compiler checks exhaustiveness
- Immutability: Immutable objects are easier to make LSP-compatible
- Contract Testing: Write tests for base type contracts and run them for all subclasses
- If you use
if (obj instanceof Subclass)— you’ve already violated LSP
Summary for Senior
- If you use
if (obj instanceof Subclass), you’ve already violated LSP - Inheritance is the strongest coupling in OOP. Use it only when the subclass fully replaces the parent in all scenarios
- Remember Design by Contract: a subclass can only weaken input requirements and strengthen output guarantees
- Composition over Inheritance — the preferred approach for a Senior developer
🎯 Interview Cheat Sheet
Must know:
- “Lazy” subclass — throws
UnsupportedOperationExceptionor makes method empty - Square extends Rectangle — side effects:
setHeightchangeswidth, area 400 instead of 200 java.sql.Dateinheritsjava.util.Date, but throwsIllegalArgumentExceptioningetHours()— LSP violation in JDKCollections.unmodifiableList()throwsUnsupportedOperationExceptiononadd()— LSP violationinstanceofto bypass different behaviors = LSP already violated- Solution: composition (Delegation) or hierarchy splitting via interfaces
Common follow-up questions:
- Why can’t Square inherit Rectangle? — Because it changes setter behavior, violating the parent’s invariant
- What is a Heisenbug in the context of LSP? — A bug that manifests only with a specific object type in DI, hard to reproduce
- How to test LSP? — Contract Testing: common tests for base type, run for all implementations
- Real consequences of violation? — In e-commerce: incorrect tax calculation, losses $50K/month
Red flags (DO NOT say):
- “Not every if is an LSP violation” (true, but better not give cause for questions)
- “Inheritance is fine if classes share fields” (ignoring behavioral subtyping)
- “LSP is violated only with exceptions” (no — side effects are also violations)
Related topics:
- [[5. What is Liskov Substitution principle]]
- [[10. What is composition and inheritance]]
- [[11. When is it better to use composition instead of inheritance]]
- [[22. Which anti-patterns contradict SOLID principles]]