The Square-Rectangle Problem
The Classic LSP Violation
The Square-Rectangle problem is the canonical illustration of a Liskov Substitution Principle violation. It demonstrates why a geometrically intuitive “is-a” relationship does not guarantee behavioural substitutability.
The Setup
A Rectangle class with independently settable width and height:
class Rectangle {
protected int width;
protected int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public void setWidth(int w) {
this.width = w;
}
public void setHeight(int h) {
this.height = h;
}
public int getWidth() { return width; }
public int getHeight() { return height; }
public int area() {
return width * height;
}
}
A Square is-a Rectangle geometrically — a square is a rectangle whose width equals its height. So we might model it as a subclass:
class Square extends Rectangle {
public Square(int side) {
super(side, side);
}
@Override
public void setWidth(int w) {
super.setWidth(w);
super.setHeight(w); // keep width == height
}
@Override
public void setHeight(int h) {
super.setHeight(h);
super.setWidth(h); // keep width == height
}
}
This compiles. All types check out. But it violates LSP.
The Violation
Consider client code written against the Rectangle contract:
void testRectangle(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
assert r.area() == 20
: "Expected area 20 but got " + r.area();
}
For any well-behaved Rectangle, setting width to 5 and height to 4 should give area 20. This is an implicit postcondition of setWidth and setHeight: they change one dimension without affecting the other.
Now pass a Square:
Square s = new Square(3);
testRectangle(s); // AssertionError: Expected area 20 but got 16
What happened?
setWidth(5)setswidth = 5,height = 5(square invariant maintained).setHeight(4)setsheight = 4,width = 4(square invariant maintained).area()returns , not .
The Square silently violates the contract that setWidth and setHeight operate independently. Client code that depends on this postcondition is broken.
Why the Model Is Wrong
The error is modelling Square as a subclass of a mutable Rectangle. A square’s defining invariant — width equals height — conflicts with Rectangle’s API, which provides independent setters. The subclass tries to enforce its invariant by overriding setters, but this changes the semantics of the setters in ways the superclass’s contract does not permit.
The problem is specifically with mutable rectangles. If Rectangle were immutable (no setters), then Square extends Rectangle would be fine — a square would simply be a rectangle where width equals height, and there would be no setter contracts to violate.
The Better Design
Option 1: Do not make Square extend Rectangle. Both implement a common Shape interface:
interface Shape {
int area();
}
class Rectangle implements Shape {
private int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
@Override
public int area() { return width * height; }
public void setWidth(int w) { this.width = w; }
public void setHeight(int h) { this.height = h; }
}
class Square implements Shape {
private int side;
public Square(int side) { this.side = side; }
@Override
public int area() { return side * side; }
public void setSide(int s) { this.side = s; }
}
Square and Rectangle both are Shape, but neither inherits from the other. There is no illusion of substitutability where it does not hold.
Option 2: Make Rectangle immutable. If width and height are set at construction and never change, Square extends Rectangle is safe — the width-equals-height constraint is established at construction and never challenged:
class Rectangle {
private final int width, height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int area() { return width * height; }
// no setters
}
class Square extends Rectangle {
public Square(int side) {
super(side, side);
}
}
No LSP violation — the contract of a Rectangle is simply “width and height are whatever was passed to the constructor.”
The Deeper Lesson
The Square-Rectangle problem teaches something fundamental about OOP modelling:
Geometric “is-a” does not imply behavioural substitutability.
When deciding whether B should extend A, the question is not “is a B a kind of A in the real world?” but “does B honour every behavioural contract that A promises to its clients?”
This is why the course emphasises LSP as a design constraint on inheritance, not an afterthought. A hierarchy designed without LSP in mind will produce subtle bugs that the type system cannot catch.
Other Classic Violations
The pattern recurs with other geometrically “obvious” hierarchies:
Ellipse extends Circle— same problem with independent control of two radii vs one.IntegerSet extends IntegerList— sets do not allow duplicates; lists do. Overridingaddto reject duplicates violates the list contract.ImmutableList extends List— overridesaddto throwUnsupportedOperationException, strengthening preconditions and violating LSP.
The consistent lesson: favour composition over inheritance when the subclass cannot faithfully honour the superclass contract.