All topics
SOLID principles
intermediate

Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering correctness.

If code works with a class, it should still work when you hand it a subclass. That is LSP.

A USB port promises power and data. Any USB device should work in any USB port without changing the host's expectations. A device that says "I'm USB" but only works upside down on Wednesdays violates the contract.

Key Concepts

1
The compiler already checks the obvious part: the subclass has the right methods with the right parameters. LSP is about the part the compiler cannot check — whether the subclass behaves the way callers expect.
2
The classic example. A square is a rectangle in mathematics, so people write class Square extends Rectangle. It compiles. Then this code runs:
class Square extends Rectangle
3
    void resize(Rectangle r) {
        r.setWidth(5);
        r.setHeight(4);
        // area should now be 20
    }
4
Pass it a Square and the area is 16, because setting the height also changed the width. The method did nothing wrong. The subclass broke a promise the parent made: width and height move independently.
Square
5
No amount of care inside Square fixes this. The relationship itself is wrong.
Square
6
The rules a subclass must follow.
  Do not demand more than the parent did.
      Parent accepts any positive number.
      Subclass rejects anything over 100.  -> callers now break
7
  Do not promise less than the parent did.
      Parent's add() always adds the item.
      Subclass silently ignores long items. -> callers now break
8
  Keep the parent's guarantees true.
9
The everyday version. You implement an interface and half the methods throw UnsupportedOperationException. A read-only list that refuses add. A repository that refuses save. Callers now have to know which concrete class they are holding, which is exactly what interfaces are supposed to prevent. Java's own Collections.unmodifiableList does this — a useful thing to admit, because it shows you are describing the rule rather than defending it.
UnsupportedOperationExceptionaddsaveCollections.unmodifiableList
10
How to catch it. Write your tests against the interface, not the class. Then run the same tests against every implementation. A subclass that fails its parent's tests is breaking LSP, and now you have a failure instead of an argument.
11
The usual fix is not inheritance. If you are overriding methods to switch them off, the relationship was never "is a kind of". Use composition: hold the other object as a field and expose only what makes sense.
12
What the interviewer is probing.1. "What is LSP?" Probing: behaviour versus signatures. Stalls: "A subclass should work like the parent." Moves up: the three rules — do not demand more, do not promise less, keep the guarantees.
13
2. "Why is Square extends Rectangle a problem?" Probing: the classic case. Stalls: "A square is a rectangle." Moves up: true in maths, false in behaviour, because the setters stop being independent.
14
3. "Is throwing UnsupportedOperationException a violation?" Probing: honesty. Stalls: "No, the JDK does it." Moves up: yes, and the JDK does it as a trade-off — worth naming rather than defending.
15
4. "How would you find violations?" Probing: method. Stalls: "Code review." Moves up: one test suite written against the interface, run against every implementation.
16
5. "What is the fix?" Probing: the alternative. Stalls: "Override more carefully." Moves up: composition — the inheritance itself is the bug.
17
  The contract a subtype must honour
18
    Preconditions    may be WEAKER  (accept more), never stronger
    Postconditions   may be STRONGER (guarantee more), never weaker
    Invariants       must be preserved
    Exceptions       no new checked exceptions the caller cannot
                     expect
19
  Square / Rectangle, concretely
20
    void resize(Rectangle r) {
        r.setWidth(5);
        r.setHeight(4);
        assert r.area() == 20;      // holds for Rectangle
    }                               // fails for Square: area == 16
21
    Square cannot satisfy this while keeping its own invariant
    (width == height). The types are not substitutable, whatever
    geometry says.
22
  Everyday violations
23
    class ReadOnlyRepo implements Repository {
        public void save(T t) {
            throw new UnsupportedOperationException();
        }
    }
    -> callers must now know the concrete type. Fix: split the
       interface (ReadableRepo / WritableRepo) -- which is ISP
       doing LSP's work.
24
    class FastList extends ArrayList<String> {
        @Override public boolean add(String s) {
            if (s.length() > 10) return false;   // silently drops
            return super.add(s);
        }
    }
    -> weakened postcondition: callers expect add to have added.
25
  The mechanical check
    Write the test suite against the INTERFACE, run it against every
    implementation. A subtype that fails the parent's tests is an
    LSP violation with a stack trace attached.