📐 Section 18 · Question #6

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

  1. 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.
  2. 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$ true
  • Point(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: ColorPoint must contain a private Point field 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 to false, 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:

  1. 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)).
  2. 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$).
  3. 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 UnsupportedOperationException on an inherited method.
  • Precondition Strengthening: Subclass rejects valid parent inputs (e.g., disallowing overdraft in DebitAccount).
  • Postcondition Weakening: Subclass returns null where 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).