Skip to content
Part IA Michaelmas Term

The Liskov Substitution Principle

The Principle

Barbara Liskov introduced the principle in 1987 (in a paper on data abstraction and hierarchy). In its original, formal formulation:

If for each object o1o_1 of type SS there is an object o2o_2 of type TT such that for all programs PP defined in terms of TT, the behaviour of PP is unchanged when o1o_1 is substituted for o2o_2, then SS is a subtype of TT.

The practical version used in OOP design:

Objects of a subclass should be substitutable for objects of the superclass without altering the correctness of the program.

If S is a subtype of T, any code that works correctly with T instances must continue to work correctly when given S instances. The subclass must not surprise its callers.

The Contract View of LSP

LSP is fundamentally about contracts. A method’s contract includes:

  • Preconditions: what must be true before the method is called (what the caller must guarantee)
  • Postconditions: what the method guarantees will be true after it returns (what the caller can rely on)
  • Invariants: conditions that always hold for instances of the class

LSP says that a subclass:

  • Must not strengthen preconditions — it cannot demand more from the caller than the superclass did. A method that accepted any int in the superclass cannot throw IllegalArgumentException for negative inputs in the subclass.
  • Must not weaken postconditions — it cannot deliver less than the superclass promised. A method that guaranteed a non-null return in the superclass cannot return null in the subclass.
  • Must preserve invariants — any condition that always held for superclass instances must also hold for subclass instances.

Why LSP Is Not Just About Compilation

A subclass can compile perfectly, compile all type checks, and still violate LSP. The Java type system enforces signature compatibility — but it does not enforce behavioural compatibility. LSP is a behavioural requirement that goes beyond what the compiler checks.

Consider:

class Stack {
    int pop() { /* ... */ }   // contract: removes and returns top element
    void push(int x) { /* ... */ }
}

class BuggyStack extends Stack {
    @Override
    int pop() {
        super.push(0);  // silently adds an element before popping
        return super.pop();
    }
}

BuggyStack compiles, satisfies all type constraints, overrides pop() with the correct signature — but it violates LSP. Code written against Stack that expects pop() not to add elements will break when given a BuggyStack. The postcondition “the stack size decreases by 1” is violated.

The Formal Contract of Object.equals()

The Object.equals() Javadoc specifies a contract that includes:

  1. Reflexivity: x.equals(x) is true.
  2. Symmetry: x.equals(y) iff y.equals(x).
  3. Transitivity: if x.equals(y) and y.equals(z), then x.equals(z).
  4. Consistency: multiple calls return the same result (unless objects are mutated).
  5. Non-nullity: x.equals(null) is false for any non-null x.

Any override of equals() that violates one of these properties breaks LSP — code that relies on these properties (e.g. HashSet, HashMap, ArrayList.contains()) will malfunction.

A common violation: using instanceof in equals() in a subclass that adds fields:

class Point {
    private final int x, y;
    // equals() uses instanceof Point
}

class ColouredPoint extends Point {
    private final String colour;
    // equals() must now account for colour
    // But: p.equals(cp) might be true while cp.equals(p) is false → broken symmetry
}

The fix: use getClass() comparison instead of instanceof, or (better) favour composition over inheritance for value-like classes that need to add fields.

LSP Violation in the JDK: Properties

java.util.Properties extends Hashtable<Object, Object> — a classic LSP violation shipped in the JDK itself.

  • Hashtable.put(key, value) accepts any Object as key and value.
  • Properties.setProperty(key, value) accepts only String key and value.
  • But because Properties extends Hashtable, you can call properties.put(nonStringKey, nonStringValue) — violating the Properties contract.

The Javadoc for Properties warns: “Because Properties inherits from Hashtable, the put and putAll methods can be applied to a Properties object. Their use is strongly discouraged as they allow the caller to insert entries whose keys or values are not Strings. The setProperty method should be used instead.”

This is a design flaw that exists for historical reasons — Properties was created before the Java Collections Framework and before the LSP was widely understood.

Testing for LSP

How to check whether your subclass respects LSP:

  1. Write tests for the superclass’s behaviour (the contract).
  2. Run those same tests against the subclass.
  3. If any test fails, the subclass violates LSP.

This technique — running superclass tests on subclass instances — is a simple, practical way to catch LSP violations early.