Liskov violations that compile perfectly
A subclass that satisfies the compiler and breaks every caller.
Open this lesson in the learning hubKey points
- Liskov substitution says code written against a type must keep working when given any subtype. The compiler only checks signatures; the contract is behaviour, and nothing enforces it.
- The canonical violation is Square extending Rectangle. Both have width and height, but a Square that keeps them equal breaks any caller that sets them independently.
- A subclass may weaken preconditions and strengthen postconditions, never the reverse. Throwing on input the parent accepted is a violation, however reasonable it seems locally.
UnsupportedOperationExceptionis the loudest form. An immutable list is a valid List by signature and breaks every caller that adds to one - which is whyArrays.asListsurprises people.- Behavioural subtyping is the reason "is-a" is a poor test. A Square is-a Rectangle in geometry and is not a valid subtype in code, because the operations differ.
- When behaviour genuinely differs, prefer composition or a shared interface with no inherited implementation - inheritance is a promise about behaviour, not just about fields.
Example
import java.util.Arrays;
import java.util.List;
public class LiskovViolations {
static class Rectangle {
protected int width, height;
void setWidth(int w) { this.width = w; }
void setHeight(int h) { this.height = h; }
int area() { return width * height; }
}
// Compiles perfectly. Breaks every caller of the parent contract.
static class Square extends Rectangle {
@Override void setWidth(int w) { this.width = w; this.height = w; }
@Override void setHeight(int h) { this.width = h; this.height = h; }
}
// Written against Rectangle, and entirely reasonable.
static int resizeAndMeasure(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
return r.area(); // any Rectangle should give 20
}
// Composition: no false promise, and the shapes stay separate.
interface Shape { int area(); }
record Rect(int width, int height) implements Shape {
public int area() { return width * height; }
}
record Sq(int side) implements Shape {
public int area() { return side * side; }
}
public static void main(String[] args) {
System.out.println("resizeAndMeasure(Rectangle) = " + resizeAndMeasure(new Rectangle()));
System.out.println("resizeAndMeasure(Square) = " + resizeAndMeasure(new Square())
+ " <- expected 20");
// The other common violation: strengthening a precondition by refusing.
List<String> fixed = Arrays.asList("a", "b");
System.out.println();
System.out.println("Arrays.asList is a List by signature:");
System.out.println(" get(0) works : " + fixed.get(0));
try {
fixed.add("c");
} catch (UnsupportedOperationException e) {
System.out.println(" add() throws : UnsupportedOperationException");
System.out.println(" -> any method taking List<String> can be broken by it");
}
System.out.println();
System.out.println("Composition - no inherited promise to break:");
List<Shape> shapes = List.of(new Rect(5, 4), new Sq(5));
for (Shape s : shapes) {
System.out.println(" " + s + " area = " + s.area());
}
}
}
The compiler checks signatures, not behaviour - a subclass that narrows what the parent accepted breaks callers that never knew it existed.
This is a reading copy. The full lesson — with the visual explainer, the interactive lab and a Run button for the code — lives in the OOP course, and every lesson in it is listed on the OOP contents page.