Loading content...
In the history of object-oriented programming, few examples have sparked more debate, served in more textbooks, or confused more beginners than the Rectangle-Square problem. It represents a deceptively simple case that reveals profound truths about inheritance, polymorphism, and the fundamental nature of object-oriented design.
At first glance, the problem seems trivial—even contrived. A square is a rectangle, mathematically speaking. A square is simply a rectangle with equal width and height. So surely, in an object-oriented design, a Square class should extend a Rectangle class?
This reasoning feels airtight. It follows the 'IS-A' relationship we're taught to use for inheritance. And yet, this seemingly natural design decision creates one of the most instructive violations of the Liskov Substitution Principle ever documented.
By the end of this page, you will understand why the Rectangle-Square problem is considered the canonical LSP violation, how it reveals the distinction between mathematical inheritance and behavioral inheritance, and why 'IS-A' in the real world doesn't automatically translate to 'IS-A' in object-oriented design.
Let's begin with the straightforward design that virtually every object-oriented programmer would intuitively reach for. We're modeling geometric shapes, and we start with a Rectangle class.
The Rectangle class represents a quadrilateral with four right angles. Its essential properties are width and height, both of which can be set independently. This seems perfectly reasonable:
123456789101112131415161718192021222324252627282930313233
public class Rectangle { protected int width; protected int height; public Rectangle(int width, int height) { this.width = width; this.height = height; } public int getWidth() { return width; } public void setWidth(int width) { this.width = width; } public int getHeight() { return height; } public void setHeight(int height) { this.height = height; } public int getArea() { return width * height; } public int getPerimeter() { return 2 * (width + height); }}This Rectangle class is clean, simple, and follows sound object-oriented principles. It encapsulates its data (width and height), provides controlled access through getters and setters, and offers essential behaviors (computing area and perimeter).
Now, the natural next step: We need to model a Square. In geometry, a square is defined as a rectangle with equal sides. This definition practically screams 'inheritance'—a Square is a specialized Rectangle where width always equals height.
The fact that 'a square IS-A rectangle' in mathematics does not automatically mean that a Square class should extend a Rectangle class in software. This distinction—between mathematical truth and behavioral compatibility—is the crux of the entire LSP.
Following our intuition, we create a Square class that extends Rectangle. To maintain the invariant that a square has equal sides, we override the setters to ensure that setting either dimension updates both:
1234567891011121314151617181920212223242526272829
public class Square extends Rectangle { public Square(int side) { super(side, side); // Initialize with equal width and height } @Override public void setWidth(int width) { // Maintain square invariant: both dimensions must be equal this.width = width; this.height = width; } @Override public void setHeight(int height) { // Maintain square invariant: both dimensions must be equal this.width = height; this.height = height; } // getSide() is a convenience method for squares public int getSide() { return width; // width == height, so either works } public void setSide(int side) { setWidth(side); // Delegates to setWidth, which sets both }}At first glance, this looks correct. The Square class:
Rectangle, modeling the 'IS-A' relationshipLet's verify with a simple test:
1234567891011121314151617
public class BasicTest { public static void main(String[] args) { Square square = new Square(5); System.out.println("Width: " + square.getWidth()); // Prints: 5 System.out.println("Height: " + square.getHeight()); // Prints: 5 System.out.println("Area: " + square.getArea()); // Prints: 25 square.setWidth(10); System.out.println("After setWidth(10):"); System.out.println("Width: " + square.getWidth()); // Prints: 10 System.out.println("Height: " + square.getHeight()); // Prints: 10 System.out.println("Area: " + square.getArea()); // Prints: 100 }}Everything works! The square invariant is maintained—setting width also sets height, and vice versa. The area calculation is correct. We might conclude our design is sound.
But we've only tested the Square as a Square. The real test of inheritance—and the essence of the Liskov Substitution Principle—is whether a Square works correctly as a Rectangle.
LSP demands that wherever a Rectangle is expected, a Square must work correctly. This isn't about the Square working in isolation—it's about the Square substituting for Rectangle in any code that expects Rectangle behavior.
Now comes the critical test. Consider a function that works with Rectangle objects—any code that expects to receive a Rectangle and manipulate it according to the Rectangle contract:
1234567891011121314151617181920212223242526272829303132333435
public class GeometryUtils { /** * Increases the width of a rectangle by a given amount. * Expected behavior: Only width changes, height remains the same. */ public static void increaseWidth(Rectangle rect, int amount) { int originalHeight = rect.getHeight(); rect.setWidth(rect.getWidth() + amount); // This assertion should ALWAYS pass for any Rectangle assert rect.getHeight() == originalHeight : "Height should not change when only width is modified!"; } /** * Creates a rectangle with the specified dimensions and verifies * that each dimension can be modified independently. */ public static void resizeIndependently(Rectangle rect, int newWidth, int newHeight) { rect.setWidth(newWidth); rect.setHeight(newHeight); // After setting dimensions independently, they should match inputs assert rect.getWidth() == newWidth : "Width should be " + newWidth; assert rect.getHeight() == newHeight : "Height should be " + newHeight; assert rect.getArea() == newWidth * newHeight : "Area should be width * height"; }}These utility functions are perfectly reasonable for rectangles. The increaseWidth method expects that modifying width leaves height unchanged—a fundamental property of rectangles. The resizeIndependently method expects both dimensions can be set to different values.
Now let's see what happens when we substitute a Square:
123456789101112131415161718192021222324252627282930313233343536373839
public class LSPViolationDemo { public static void main(String[] args) { // Works perfectly with Rectangle Rectangle rect = new Rectangle(4, 5); System.out.println("Testing with Rectangle:"); GeometryUtils.increaseWidth(rect, 3); // ✓ Passes System.out.println("Width: " + rect.getWidth()); // 7 System.out.println("Height: " + rect.getHeight()); // 5 (unchanged) // Fails with Square - THE LSP VIOLATION! Rectangle sneakySquare = new Square(4); // Polymorphism at work System.out.println("Testing with Square (as Rectangle):"); try { GeometryUtils.increaseWidth(sneakySquare, 3); // ✗ FAILS! } catch (AssertionError e) { System.out.println("ASSERTION FAILED: " + e.getMessage()); System.out.println("Width: " + sneakySquare.getWidth()); // 7 System.out.println("Height: " + sneakySquare.getHeight()); // 7 (CHANGED!) } // Another failure case Rectangle anotherSquare = new Square(5); System.out.println("Testing resize independently:"); try { // Attempt to set width=10, height=20 (different values) GeometryUtils.resizeIndependently(anotherSquare, 10, 20); } catch (AssertionError e) { System.out.println("ASSERTION FAILED: " + e.getMessage()); // Width was set to 10, then height was set to 20 // But height setter also set width to 20! System.out.println("Width: " + anotherSquare.getWidth()); // 20 System.out.println("Height: " + anotherSquare.getHeight()); // 20 System.out.println("Expected area: " + (10 * 20)); // 200 System.out.println("Actual area: " + anotherSquare.getArea()); // 400 } }}When a Square is used where a Rectangle is expected, the client code breaks. The assertions fail because Square does not honor the behavioral contract of Rectangle. This is the textbook definition of an LSP violation: a subtype cannot substitute for its base type without breaking the program.
What went wrong?
The Rectangle class establishes a behavioral contract. When clients interact with a Rectangle, they have reasonable expectations:
Square violates all of these expectations except the last. Its overridden setters change the implied contract: setting width also sets height, and vice versa.
The LSP states: if code works with a base class, it must work with any subclass. The Square class—despite being mathematically correct—breaks this principle because it cannot fulfill the behavioral expectations of Rectangle.
Let's dissect exactly what makes this an LSP violation by examining the formal contract violations:
| Rectangle Contract | Square Behavior | Contract Violation |
|---|---|---|
setWidth(w) changes only width | setWidth(w) changes both width and height | Side effect violation: Method has unexpected side effects |
setHeight(h) changes only height | setHeight(h) changes both width and height | Side effect violation: Method has unexpected side effects |
| Width and height are independent | Width and height are coupled | Invariant violation: Subclass introduces new constraints |
After setWidth(w): getWidth() == w | After setWidth(w); setHeight(h): getWidth() == h | Postcondition violation: Later operations violate earlier postconditions |
The root cause is: Square has a stronger invariant than Rectangle. While Rectangle allows any combination of width and height, Square requires width == height at all times.
To maintain this invariant, Square must override methods in ways that violate Rectangle's behavioral contract. There's no way around this—the invariants are fundamentally incompatible.
When a subclass has a stronger invariant than its base class, it typically cannot be substituted for the base class. The subclass will need to override methods in ways that violate the base class's expected behavior. This is a reliable indicator of an LSP violation.
Let's visualize the problem with a formal analysis of pre/postconditions:
12345678910111213141516171819202122232425262728293031323334
/** * Rectangle.setWidth(int w) Contract Analysis * * PRECONDITIONS: * - w >= 0 (non-negative width) * * POSTCONDITIONS: * - getWidth() == w (width updated) * - getHeight() == OLD(getHeight()) (height unchanged) * - getArea() == w * OLD(getHeight()) (area updated accordingly) * * INVARIANT: * - (none imposed by Rectangle) */ /** * Square.setWidth(int w) Contract Analysis * * PRECONDITIONS: * - w >= 0 (same as Rectangle - valid) * * POSTCONDITIONS: * - getWidth() == w (✓ satisfied) * - getHeight() == w (✗ VIOLATES Rectangle's postcondition!) * - getArea() == w * w (different from expected w * OLD(height)) * * INVARIANT: * - width == height (STRONGER than Rectangle - problem!) */ // According to LSP Contract Rules:// - Preconditions can be WEAKENED (accept more) - Square does NOT violate this// - Postconditions can be STRENGTHENED (guarantee more) - BUT NOT VIOLATED// - Square VIOLATES postconditions by changing height when width is setYou might wonder: "This is a contrived geometry example. Does this really matter in real software?"
Absolutely. The Rectangle-Square problem is a simplified illustration of a pattern that appears throughout software development. Any time you create a subclass that restricts the behavior of its base class, you risk the same violation.
Real-world analogs:
push() method, an immutable stack must either throw an exception or silently fail, breaking the contract.In each case, the pattern is the same:
This is why the Rectangle-Square problem is considered canonical—it distills a widespread software design error into its simplest form.
Any time you find yourself overriding a method to throw UnsupportedOperationException, silently ignore the operation, or enforce constraints the base class doesn't have, you're likely creating an LSP violation. The Rectangle-Square problem teaches us to recognize this pattern.
The Rectangle-Square problem isn't just an academic exercise—it has shaped how the software industry thinks about inheritance.
Timeline of the problem's influence:
Why this example persists as the canonical LSP violation:
• Mathematical intuition works against you — Everyone knows a square is a rectangle, making the violation surprising • The code looks correct — All the usual checks pass; the error is subtle • The failure is behavioral, not syntactic — The program compiles and even runs 'correctly' in isolation • It generalizes broadly — The same pattern appears in countless real-world scenarios • It's unforgettable — Once you understand it, you see it everywhere
Almost every developer encountering this problem for the first time feels confused. 'But a square IS a rectangle!' Understanding why this mathematical truth doesn't translate to behavioral compatibility is a rite of passage in object-oriented design thinking.
We've now seen the Rectangle-Square problem in full detail. Let's consolidate the key insights:
What's next:
Now that we've seen the problem, the natural question is: Why exactly does this happen? In the next page, we'll dive deeper into why Square extending Rectangle breaks LSP—examining the theoretical foundations, the contract violations in detail, and the fundamental tension between mathematical truth and behavioral compatibility.
You now understand the Rectangle-Square problem as a canonical LSP violation. You've seen how a seemingly correct inheritance relationship can break polymorphism by violating behavioral contracts. Next, we'll explore the deeper reasons why this violation occurs.