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 of type there is an object of type such that for all programs defined in terms of , the behaviour of is unchanged when is substituted for , then is a subtype of .
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
intin the superclass cannot throwIllegalArgumentExceptionfor 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
nullin 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:
- Reflexivity:
x.equals(x)istrue. - Symmetry:
x.equals(y)iffy.equals(x). - Transitivity: if
x.equals(y)andy.equals(z), thenx.equals(z). - Consistency: multiple calls return the same result (unless objects are mutated).
- Non-nullity:
x.equals(null)isfalsefor any non-nullx.
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 anyObjectas key and value.Properties.setProperty(key, value)accepts onlyStringkey and value.- But because
PropertiesextendsHashtable, you can callproperties.put(nonStringKey, nonStringValue)— violating thePropertiescontract.
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:
- Write tests for the superclass’s behaviour (the contract).
- Run those same tests against the subclass.
- 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.