Question 6 · Section 18

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.

Language versions: English Russian Ukrainian

🟢 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?

  1. Extract a common interface: Instead of Square extends Rectangle, create a Shape interface with a getArea() method
  2. 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.Date and java.sql.Date
  • Essence: java.sql.Date (subclass) “disables” time handling (hours, minutes, seconds), even though the base class java.util.Date supports them. If code expects millisecond precision and receives sql.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 if is 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 setHeight rewrote width) Result: Client is shocked, the invariant “changing height doesn’t affect width” is broken.

Why Do We Do This? (Root Cause)

  1. Desire to save effort: “Why write the name field again if it’s already in User?”. So we inherit Admin from User, even though they have different life cycles.
  2. Incorrect hierarchy: We model the world “as it is”, not “as it is used”. In biology, an ostrich is a bird; in OOP, Ostrich cannot inherit from FlyingBird.

How to Recognize Violation

Signs:

  1. instanceof checks: If you check the type to call specific logic
  2. UnsupportedOperationException: Method “not supported” in subclass
  3. Empty overrides: Method overridden with an empty implementation
  4. 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

  1. 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
  2. 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)
  3. 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: instanceof checks 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:

  1. 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);
        }
    }
    
  2. Metrics:
    • Number of instanceof checks
    • Number of UnsupportedOperationException
    • Number of empty override methods
  3. 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 UnsupportedOperationException or makes method empty
  • Square extends Rectangle — side effects: setHeight changes width, area 400 instead of 200
  • java.sql.Date inherits java.util.Date, but throws IllegalArgumentException in getHours() — LSP violation in JDK
  • Collections.unmodifiableList() throws UnsupportedOperationException on add() — LSP violation
  • instanceof to 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]]