Give an Example of Liskov Substitution Principle Violation
Violations of the Liskov Substitution Principle (LSP) typically occur when developers misuse class inheritance to reuse code between concepts that share a real-world taxonomy bu...
🟢 Junior Level
Violations of the Liskov Substitution Principle (LSP) typically occur when developers misuse class inheritance to reuse code between concepts that share a real-world taxonomy but exhibit incompatible behavioral contracts.
Example 1: Refusal of Bequest
A subclass overrides a base class method and throws an exception indicating that the operation is unsupported:
public class Bird {
public void fly() {
System.out.println("Flying in the sky!");
}
}
public class Penguin extends Bird {
@Override
public void fly() {
// LSP Violation: Refusing the parent contract
throw new UnsupportedOperationException("Penguins swim, they cannot fly!");
}
}
// Client application crashes unexpectedly:
public class ZooService {
public void releaseBirds(List<Bird> birds) {
for (Bird bird : birds) {
bird.fly(); // 💥 CRASHES with UnsupportedOperationException when Penguin is processed!
}
}
}
Example 2: Mutable Rectangle and Square (Invariant Corruption)
While a square is geometrically a rectangle, in mutable object-oriented programming it corrupts caller invariants:
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: silently mutates height!
}
@Override
public void setHeight(int height) {
this.width = height; // Side effect: silently mutates width!
this.height = height;
}
}
public void verifyArea(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
// Client assumes height mutation does not alter width: 5 * 4 = 20.
// When Square is passed: width became 4, so area is 16!
assert r.getArea() == 20 : "LSP Violation! Expected 20, but got: " + r.getArea();
}
Basic Remediation
- Never inherit purely for fields: If two classes share attributes (
width,height) but require different mutation behavior, extract a shared read-only interface (Shape) without mutator methods. - Favor Composition over Inheritance: If a class needs logic from an existing class, inject it as a private dependency and delegate calls.
🟡 Middle Level
4 Formal Contract Violations Under Design by Contract (DbC)
LSP violations can be categorized into four distinct contractual breakdowns:
1. Strengthening Preconditions
The subclass imposes stricter input requirements than the parent:
public class Account {
protected BigDecimal balance = BigDecimal.ZERO;
// Base Contract: Overdraft is permitted (accepts any positive withdrawal)
public void withdraw(BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("Amount must be positive");
}
balance = balance.subtract(amount);
}
}
public class DebitAccount extends Account {
@Override
public void withdraw(BigDecimal amount) {
// ❌ LSP VIOLATION: Strengthens precondition by forbidding overdraft
if (amount.compareTo(balance) > 0) {
throw new IllegalStateException("Overdraft not allowed for debit account!");
}
super.withdraw(amount);
}
}
Impact: Client services designed for Account crash unexpectedly with IllegalStateException during high-value transactions.
2. Weakening Postconditions
The subclass provides fewer guarantees about its output:
public class UserRepository {
// Base Contract: Guaranteed to NEVER return null (returns empty list on no matches)
public List<User> findAllActive() {
return database.queryActiveUsers(); // returns Collections.emptyList() if empty
}
}
public class CachedUserRepository extends UserRepository {
@Override
public List<User> findAllActive() {
// ❌ LSP VIOLATION: Weakens postcondition by returning null on cache miss
UserCacheEntry entry = cache.get("active_users");
return (entry != null) ? entry.getUsers() : null;
}
}
Impact: Calling code executing repo.findAllActive().size() crashes with a NullPointerException.
3. Invariant Violations
An invariant is a condition that must remain true throughout an object’s entire lifecycle:
public class PositiveCounter {
protected int count = 0;
// Class Invariant: count >= 0 at all times
public void increment() { count++; }
public int getCount() { return count; }
}
public class ResetCounter extends PositiveCounter {
// ❌ LSP VIOLATION: Permits negative values, corrupting the base invariant
public void forceSet(int value) {
this.count = value; // Can be set to -100!
}
}
4. History Constraint Violations
The subclass allows state mutations that the base class declared immutable:
public class ImmutableMoney {
private final BigDecimal amount;
private final Currency currency;
public ImmutableMoney(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
public BigDecimal getAmount() { return amount; }
}
public class MutableMoney extends ImmutableMoney {
private BigDecimal mutableAmount;
// ❌ LSP VIOLATION: Callers cache this thinking it is immutable, but another thread mutates it!
public void setAmount(BigDecimal newAmount) {
this.mutableAmount = newAmount;
}
}
🔴 Senior Level
1. The equals() Equivalence Trap (Joshua Bloch, Effective Java)
Attempting to extend an instantiable class and add a new value component inevitably breaks the mathematical equivalence relation of equals() (symmetry or transitivity):
public class Point {
private final int x, y;
public Point(int x, int y) { this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (!(o instanceof Point p)) return false;
return p.x == x && p.y == y;
}
}
public class ColorPoint extends Point {
private final Color color;
public ColorPoint(int x, int y, Color color) {
super(x, y);
this.color = color;
}
@Override
public boolean equals(Object o) {
if (!(o instanceof ColorPoint cp)) return false;
return super.equals(o) && cp.color == color;
}
}
The Symmetry Breakdown:
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);
p.equals(cp); // true (Point compares only x and y)
cp.equals(p); // false (ColorPoint rejects p because it is not a ColorPoint)
If you attempt to fix symmetry by having ColorPoint.equals ignore color when compared to a plain Point, you break transitivity:
ColorPoint(1, 2, RED).equals(Point(1, 2))$\to$truePoint(1, 2).equals(ColorPoint(1, 2, BLUE))$\to$true- But
ColorPoint(1, 2, RED).equals(ColorPoint(1, 2, BLUE))$\to$false!
[!IMPORTANT] Joshua Bloch’s Rule: There is no way to extend an instantiable class and add a value component while preserving the
equals()contract. The only clean solution is Composition:ColorPointmust contain a privatePointfield rather than extending it.
2. Hibernate / JPA Proxy Identity Trap
In JPA entities, implementing equals() using this.getClass() == o.getClass() breaks LSP during lazy loading:
- Hibernate wraps lazily loaded associations in dynamic ByteBuddy proxies:
User$HibernateProxy$xyz extends User. - When comparing
user.equals(proxy),getClass() == o.getClass()evaluates tofalse, even though both represent the identical database row with the same primary key. - LSP-Compliant JPA Pattern:
@Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof User other)) return false; // Accepts Hibernate proxies! return getId() != null && getId().equals(other.getId()); // Use getters, not direct field access! }
3. Non-Functional Contract Violations (Algorithmic Degradation)
LSP extends beyond syntax to non-functional requirements (SLAs and Big-O complexity):
- Suppose a base repository interface specifies that
findById(ID id)operates in $O(1)$ time (e.g., in-memory hash table lookup). - A developer provides an audit-logging subclass that performs a linear disk scan or calls an external HTTP endpoint, degrading latency from $100\,\mu\text{s}$ to $500\,\text{ms}$ ($O(N)$).
- When substituted into a high-throughput processing pipeline handling $50,000\text{ req/sec}$, Tomcat worker threads are exhausted, cascading into thread pool starvation and system-wide outages.
4 Tricky Questions
1. Why does Joshua Bloch assert that it is impossible to extend an instantiable class with a new value field while preserving the equals() contract?
Answer:
If a subclass introduces a new field (e.g., ColorPoint adding Color to Point), overriding equals creates a contradiction:
- If the subclass checks the new field, symmetry is broken when comparing a base instance with a subclass instance (
p.equals(cp) != cp.equals(p)). - If the subclass ignores the field when comparing against a base instance, transitivity is broken when comparing two different subclass instances through a shared base instance ($A = B$ and $B = C$, but $A \neq C$).
- If
getClass() == o.getClass()is used, Liskov substitution is broken because a subclass instance cannot equal its parent even if their shared fields are identical. The only architectural solution is Composition over Inheritance.
2. Is narrowing the return range of numbers in a subclass an LSP violation?
Answer:
No. Narrowing the return range is an example of strengthening postconditions, which is fully permitted by Design by Contract.
If base method int calculateScore() promises to return an integer between $-100$ and $+100$, and a subclass StrictScorer returns only values between $0$ and $+100$, any client expecting the wider range will handle the subclass output without error.
An LSP violation only occurs when postconditions are weakened (e.g., returning numbers outside the declared $-100 \dots +100$ range or returning null).
3. How can the Decorator Pattern violate LSP in multithreaded systems?
Answer:
If the decorated base class guarantees thread-safety (e.g., ConcurrentMap or an atomic service), but a decorator wraps it with unsynchronized mutable state (e.g., a primitive private int invocationCount++), the resulting combined object loses the thread-safety invariant of the base contract.
Callers substituting the decorator into a concurrent environment will experience race conditions and corrupted metrics, violating LSP invariants.
4. Does changing algorithmic time complexity from $O(1)$ to $O(N)$ in a subclass violate LSP?
Answer: Syntactically in Java, the code compiles and runs. However, architecturally it is a severe violation of the non-functional behavioral contract.
If client services rely on $O(1)$ lookup guarantees to meet strict SLA budgets (e.g., high-frequency trading or API gateways), substituting an $O(N)$ implementation causes CPU thread starvation, request timeouts, and cascading failure across distributed downstream dependencies.
🎯 Interview Cheat Sheet
Core Violations at a Glance:
- Refused Bequest: Subclass throws
UnsupportedOperationExceptionon an inherited method. - Precondition Strengthening: Subclass rejects valid parent inputs (e.g., disallowing overdraft in
DebitAccount). - Postcondition Weakening: Subclass returns
nullwhere parent guaranteed non-null collections. - Invariant Corruption:
Square.setWidth()silently mutating height, violating the rectangle’s independent side invariant. - Equals Contract Breakdown: Adding value fields in subclasses breaks symmetry and transitivity in
equals().
Red Flags to Avoid:
- ❌ “Inheritance is appropriate whenever two classes share 80% of the same fields.” (Inheritance couples behavior and invariants, not just data fields).
- ❌ “Using
getClass() == o.getClass()in JPA entities is standard best practice.” (Breaks lazy-loading Hibernate ByteBuddy proxies). - ❌ “LSP only applies to exceptions, not performance or concurrency.” (Non-functional contracts and thread-safety invariants are core to LSP).