Question 5 · Section 18

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.

Language versions: English Russian Ukrainian

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

  1. Preconditions cannot be strengthened: Subclass cannot require more than the parent. (If parent accepts any number, subclass cannot require only positive numbers)
  2. Postconditions cannot be weakened: Subclass must guarantee at least the same result as the parent
  3. Invariants must be preserved: Object state that was true for the parent must remain true for the subclass
  4. History Constraint: Subclass must not change state that was considered immutable in the parent

How to Detect LSP Violation

Signs of violation:

  1. instanceof in code: If you check the object type to call a specific method — the hierarchy is designed incorrectly
  2. Empty methods: Subclass overrides parent’s method with an empty implementation because “it doesn’t need it”
  3. Throwing unexpected exceptions: As in the UnsupportedOperationException example
  4. 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

  1. Mistake: Inheritance for code reuse (“they have common fields!”) Solution: Use composition instead of inheritance

  2. Mistake: java.util.Collections.unmodifiableList() throws UnsupportedOperationException on add() Solution: This is a known LSP violation. Use ReadOnlyList wrapper with a read-only interface

  3. 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: ArrayList vs Collections.unmodifiableList()
    • unmodifiableList is a wrapper over List, but throws UnsupportedOperationException on add()
    • This is an LSP violation, as a client expecting a List is 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

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

  1. Code Review signs:
    • If instanceof is 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(...)
  2. Unit Test signs:
    • Tests for the base class fail when run with the subclass
    • Different behavior of the same method for different subclasses
  3. 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 Square cannot inherit from Rectangle with setWidth/setHeight methods
  • 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)
  • UnsupportedOperationException in a subclass — a clear sign of LSP violation
  • instanceof in 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, instanceof checks, 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]]