Skip to main content

Liskov Substitution: Subtype Contracts

What This Concept Is

The Liskov Substitution Principle (LSP), due to Barbara Liskov, says:

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.

In practice: wherever code expects a T, you must be able to hand it an S and everything keeps working. The subtype must honor the supertype's behavioral contract, not only its type signature.

Contracts have three parts:

  • Preconditions: what the method requires of callers. Subtypes must not strengthen them.
  • Postconditions: what the method guarantees after running. Subtypes must not weaken them.
  • Invariants: what must always be true of an instance. Subtypes must preserve them.

A compiler can check that the types line up. Only thinking and tests can check that the contract lines up.

Why It Matters Here

Most inheritance trees that "seem fine" violate LSP quietly. When they do, polymorphism stops being trustworthy: callers start adding instanceof checks, special-cases, and comments that begin with "unless this is a...". That is the moment OCP dies.

LSP is also the principle that most clearly reveals the difference between "A is a kind of B" in English and "A can substitute for B" in code.

Concrete Example

The canonical violation -- Square is not a Rectangle:

class Rectangle {
protected int width, height;
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
public int area() { return width * height; }
}

class Square extends Rectangle {
@Override public void setWidth(int w) { width = w; height = w; }
@Override public void setHeight(int h) { width = h; height = h; }
}

void expectDoubleWidth(Rectangle r) {
r.setWidth(10); r.setHeight(5);
assert r.area() == 50; // holds for Rectangle, fails for Square (area=25)
}

Rectangle promised that setWidth does not affect height. Square weakened that postcondition. Any caller that relies on Rectangle's contract can now be surprised by a Square.

A real-world analogue appears in the OOAD treatment: a Pixel is not a SolidRectangle, even if every pixel happens to be a degenerate rectangle.

Common Confusion / Misconception

"If it compiles, LSP holds." The compiler checks method signatures. LSP checks behavior. A subclass can implement every method and still violate LSP by throwing where the parent did not, by returning null where the parent returned a list, or by demanding callers hold a lock the parent did not require.

A second misconception: "LSP is just about exceptions." Exceptions are one symptom. The deeper problem is subtypes lying about what they are. If ImmutableList extends List but throws from add, any caller expecting List-as-mutable-by-default is wrong -- the hierarchy is modeling the wrong axis.

How To Use It

When drawing an inheritance relationship, run the substitution check explicitly:

  1. Write the supertype's behavioral contract in prose: preconditions, postconditions, invariants.
  2. For each subtype method, check whether it keeps its end of that contract.
  3. If the subtype wants to change the contract, it is not really a subtype. Prefer composition (Square has a side length, not is-a Rectangle).
  4. Watch for the test signal: if tests for the parent type keep being written as if (x instanceof Sub) ..., LSP is already broken.

Check Yourself

  1. Why is "Square extends Rectangle" an LSP violation even though every square is geometrically a rectangle?
  2. Why can subclasses weaken preconditions but not postconditions?
  3. How is LSP connected to the Open-Closed Principle?

Mini Drill or Application

Pick an inheritance relationship in code you know. Do all four:

  1. Write the parent type's contract as three short lines: preconditions, postconditions, invariants.
  2. For each subclass, mark each line as keeps or breaks.
  3. For any breaks, decide whether inheritance was a modeling mistake.
  4. Propose a composition-based alternative for at least one case.

Read This Only If Stuck