Loading learning content...
The Rectangle-Square problem persists in software engineering education because it exposes a tension at the heart of object-oriented design. On one side, we have mathematical truth: a square is, unambiguously, a special case of a rectangle. On the other side, we have behavioral compatibility: a square cannot substitute for a rectangle in mutable contexts.
This page explores this tension in depth. We'll see that there are actually two different meanings of 'IS-A', and confusing them leads to design errors. Understanding this distinction is not just academic—it's a practical tool for evaluating inheritance relationships.
By mastering the difference between mathematical and behavioral inheritance, you'll be able to avoid the Rectangle-Square trap in its many disguised forms throughout your software engineering career.
By the end of this page, you will understand the difference between mathematical IS-A (set-theoretic subtyping) and behavioral IS-A (Liskov substitutability), why mathematical relationships don't automatically translate to software design, and how to evaluate inheritance candidates using behavioral criteria rather than mathematical ones.
In mathematics, the relationship between types (or sets) is governed by set theory. A set S is a subtype of set T if every element of S is also an element of T.
The mathematical view of Rectangle and Square:
Rectangle as the set of all quadrilaterals with four right anglesSquare as the set of all quadrilaterals with four right angles AND four equal sidesSquare ⊆ Rectangle (Square is a subset of Rectangle)This relationship is incontrovertible. In pure mathematics, there is no argument: a square IS-A rectangle.
Set Theory Analysis of Rectangle/Square━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Let R = { all quadrilaterals with 4 right angles } (Rectangles)Let S = { all quadrilaterals with 4 right angles AND 4 equal sides } (Squares) Claim: S ⊆ R Proof: Take any element s ∈ S. By definition of S, s has 4 right angles. Therefore, s satisfies the definition of element in R. Therefore, s ∈ R. Since s was arbitrary, all elements of S are in R. Therefore, S ⊆ R. ∎ Consequence: Every statement true for all rectangles is true for all squares. Properties of rectangles apply to squares. In mathematical terms: Square IS-A Rectangle. "A square is a rectangle with the additional constraint that all sides are equal."Properties preserved under mathematical inheritance:
When S ⊆ T mathematically, every property true for all elements of T is also true for all elements of S. For rectangles and squares:
Every static, read-only property that applies to rectangles also applies to squares. Mathematical inheritance is about static properties.
In mathematics, a rectangle with width 3 and height 4 doesn't 'become' a rectangle with width 5 and height 4. You simply have two different rectangles. Mathematical objects don't have mutable state. This is crucially different from software objects.
In object-oriented programming, inheritance is about more than static properties—it's about behavioral substitutability. The Liskov Substitution Principle defines subtyping in terms of what objects do, not just what they are.
The behavioral view of Rectangle and Square:
A Rectangle object is not just a mathematical entity—it's an entity with behavior:
For Square to be a behavioral subtype of Rectangle, it must support all behaviors of Rectangle. This is where the problem emerges.
Behavioral Substituability Analysis━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Rectangle Behaviors: 1. setWidth(w) → Changes width to w, height unchanged 2. setHeight(h) → Changes height to h, width unchanged 3. getWidth() → Returns current width 4. getHeight() → Returns current height 5. getArea() → Returns width × height Question: Can Square support ALL these behaviors? Analysis: Behavior 3, 4, 5: ✓ Square can support these (read-only operations) Behavior 1: setWidth(w) → Changes width to w, height unchanged Square's constraint: width must always equal height If setWidth(10) sets width=10, height must also become 10 This CHANGES height, violating "height unchanged" ✗ Square CANNOT support this behavior Behavior 2: setHeight(h) → Changes height to h, width unchanged Same analysis as above ✗ Square CANNOT support this behavior Conclusion: Square CANNOT support all behaviors of Rectangle. Square is NOT a behavioral subtype of Rectangle. Square CANNOT substitute for Rectangle. In behavioral terms: Square IS-NOT-A Rectangle.The key insight:
Mathematical inheritance deals with static properties—characteristics that don't change. Behavioral inheritance deals with dynamic behavior—how objects respond to operations over time.
Rectangle's behavior includes the ability to change dimensions independently. This behavior is fundamental to what a mutable Rectangle does. Square cannot support this behavior—its invariant prevents it. Therefore, Square is not behaviorally substitutable for Rectangle.
The rectangle with width=3, height=4 in mathematics is a fixed, immutable value. But Rectangle(3, 4) in OOP is an object with identity and mutable state. The object isn't just what it IS—it's what it CAN DO. This behavioral dimension is where the substitution fails.
The Rectangle-Square problem is interesting precisely because the mathematical and behavioral views give contradictory answers. Let's see this clash clearly:
| Perspective | Question | Answer | Reasoning |
|---|---|---|---|
| Mathematical | Is Square a type of Rectangle? | YES | Every square satisfies the definition of rectangle (4 right angles) |
| Behavioral | Can Square substitute for Rectangle? | NO | Square cannot support independent dimension modification |
Why do they clash?
The answer lies in what each perspective considers 'essential' to being a Rectangle:
Mathematical view: A Rectangle is defined by its static properties—having four right angles, opposite sides equal, etc. These properties are in no way violated by squares.
Behavioral view: A Rectangle is defined by its capabilities—what operations it supports and how it responds to them. One key capability is independent dimension modification, which squares cannot support.
In mathematics, we define types by what they ARE. In OOP (per LSP), we define types by what they CAN DO.
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
/** * The two perspectives formalized as type checks */ // Mathematical definition (static properties)interface MathematicalRectangle { double getWidth(); double getHeight(); double getArea(); boolean hasFourRightAngles(); // Always true boolean hasOppositeEqualSides(); // Always true} // Both Rectangle and Square satisfy this interface!// Square IS-A MathematicalRectangle ✓ // Behavioral definition (capabilities)interface BehavioralRectangle extends MathematicalRectangle { // Additional behavioral contracts: /** * Sets width independently. * @postcondition getWidth() == w * @postcondition getHeight() == old.getHeight() // UNCHANGED! */ void setWidth(double w); /** * Sets height independently. * @postcondition getHeight() == h * @postcondition getWidth() == old.getWidth() // UNCHANGED! */ void setHeight(double h);} // Square CANNOT satisfy this interface without violating postconditions!// Square IS-NOT-A BehavioralRectangle ✗ /** * The clash: Same name ("Rectangle"), different definitions. * Mathematical: About static properties * Behavioral: About dynamic capabilities (with contracts) * * In OOP, we care about BEHAVIORAL subtyping. * The mathematical relationship is a red herring. */In object-oriented design, what an object CAN DO is more important than what it IS (in a mathematical sense). LSP is fundamentally about behavioral contracts, not set membership. Always evaluate inheritance in behavioral terms.
Why do mathematical and behavioral inheritance clash specifically for mutable types? The answer involves a subtle but crucial concept in type theory: the relationship between subtyping and mutability.
For immutable (read-only) types:
If Square only had read-only operations, it would be a valid subtype of Rectangle:
All read-only operations on Rectangle work correctly for Square. An immutable Square IS-A immutable Rectangle.
For mutable (read-write) types:
Once we add setters, the relationship breaks. The problem is that writes require the type to support the full state space of the base type, not just a subset.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
/** * The Read-Only vs Read-Write Analysis */ // Read-only interface - Square CAN substituteinterface ReadonlyRectangle { int getWidth(); int getHeight(); int getArea(); int getPerimeter();} // Square satisfies this perfectly!class ImmutableSquare implements ReadonlyRectangle { private final int side; ImmutableSquare(int side) { this.side = side; } public int getWidth() { return side; } // ✓ Works public int getHeight() { return side; } // ✓ Works public int getArea() { return side * side; } // ✓ Works public int getPerimeter() { return 4 * side; } // ✓ Works}// Immutable Square IS-A ReadonlyRectangle ✓ // Read-write interface - Square CANNOT substituteinterface MutableRectangle extends ReadonlyRectangle { void setWidth(int w); // Postcondition: height unchanged void setHeight(int h); // Postcondition: width unchanged} // Square CANNOT satisfy this without breaking postconditions!// A mutable square must couple width and height changes.// This violates the independent modification postcondition.// Mutable Square IS-NOT-A MutableRectangle ✗ /** * KEY INSIGHT: * * Subtyping for READS (covariance): * Subtypes can return more specific values. * Square returning 'side' for getWidth() is fine. * * Subtyping for WRITES (contravariance): * Subtypes must ACCEPT all values the base type accepts. * Rectangle accepts any (width, height) pair. * Square cannot accept pairs where width != height. * Square's set of acceptable inputs is SMALLER. * * Mutable types combine reads AND writes. * Therefore, mutable subtypes must satisfy BOTH: * - Return compatible values (Square does this) * - Accept all inputs (Square FAILS this) */The covariance/contravariance perspective:
Mutable types occupy BOTH positions simultaneously. A mutable Square is covariant as a data source (readable) but contravariant as a data sink (writable). It can only be a subtype if it supports the full intersection of these requirements—which it doesn't.
Functional programming's preference for immutable data structures isn't just about thread safety—it's also about type safety. Immutable types only have covariant operations (reads), which simplifies the subtyping rules. The Rectangle-Square problem wouldn't exist in a purely immutable world.
The Rectangle-Square problem is famous, but the mathematical-vs-behavioral clash appears in many forms. Understanding it abstractly helps you recognize it in other contexts:
| Proposed Inheritance | Mathematical View | Behavioral View | Verdict |
|---|---|---|---|
| Circle extends Ellipse | Every circle is an ellipse (major=minor axis) | Ellipse can have axes set independently; circle cannot | LSP Violation (same as Rectangle-Square) |
| ReadOnlyList extends List | A read-only list is a list (with restrictions) | List promises add/remove; read-only cannot support these | LSP Violation (stronger invariant) |
| Penguin extends Bird | Penguin is a bird (flightless species exist) | If Bird.fly() is expected, Penguin breaks it | LSP Violation (if fly() is part of Bird's contract) |
| Stack extends Deque | Stack operations ⊆ Deque operations | Deque allows access at both ends; Stack restricts one | LSP Violation (Stack has stronger invariant) |
| Integer extends Real | Every integer is a real number | Real allows any value; Integer restricts to whole numbers | LSP Violation for mutable cases |
The pattern:
In every case, the proposed subclass has a restriction that the base class doesn't have:
These restrictions make the subclass a smaller set mathematically, but they also prevent the subclass from supporting all behaviors of the base class. The mathematical subset relationship exists, but behavioral substitutability does not.
Whenever you find yourself saying 'B is like A, but with this restriction...' or 'B is a special case of A that can't do X...', you have a potential LSP violation. Restrictions in subclasses often indicate behavioral incompatibility.
Understanding the distinction between mathematical and behavioral inheritance has practical implications for how we design object hierarchies:
Principle 1: Design by Behavior, Not by Category
Don't ask 'What category does this belong to?' Ask 'What does this need to do?'
The question 'Is a square a type of rectangle?' leads you astray. The question 'Can a square do everything a rectangle can do?' leads to correct design.
Principle 2: Prefer Composition for Constrained Subtypes
When you have a mathematically-subset relationship that isn't behaviorally compatible, don't use inheritance. Use composition instead:
123456789101112131415161718192021222324252627282930313233343536373839404142434445
// Instead of: Square extends Rectangle (broken)// Use: Square is an independent type, possibly sharing interfaces // Shared read-only interface for common queriesinterface Shape { double getArea(); double getPerimeter();} // Rectangle is its own typeclass Rectangle implements Shape { private double width, height; public Rectangle(double w, double h) { width = w; height = h; } public double getWidth() { return width; } public double getHeight() { return height; } public void setWidth(double w) { width = w; } public void setHeight(double h) { height = h; } public double getArea() { return width * height; } public double getPerimeter() { return 2 * (width + height); }} // Square is its own type, NOT extending Rectangleclass Square implements Shape { private double side; public Square(double s) { side = s; } public double getSide() { return side; } public void setSide(double s) { side = s; } public double getArea() { return side * side; } public double getPerimeter() { return 4 * side; } // Factory method if you need to convert to Rectangle public Rectangle toRectangle() { return new Rectangle(side, side); }} // Now neither type promises capabilities it can't deliver.// Both support Shape's behavioral contract.// No LSP violation because no problematic inheritance exists.If a mathematical IS-A relationship doesn't translate to behavioral IS-A, don't force the inheritance. Use interfaces for shared behaviors, and keep the types separate. You can still represent the mathematical relationship through conversion methods without creating a behavioral subtype.
Learning to think behaviorally rather than mathematically is a significant conceptual shift for many developers. Here's how to internalize it:
Mathematical mindset (problematic):
'This class is defined as a set of entities with these properties. A subclass is a subset with additional properties. If Square has all Rectangle properties plus one more (equal sides), it's a subtype.'
Behavioral mindset (correct for OOP):
'This class is defined by its behavioral contract—what operations it supports and what guarantees they make. A subclass is valid if it can fulfill all these behavioral promises. If Square can't make width and height independent, it can't fulfill Rectangle's promises.'
Focus: Static properties, set membership
Question: Is B a subset of A?
Answer for Square: Yes, every square is a rectangle
Conclusion: Square IS-A Rectangle
Focus: Dynamic behavior, contracts
Question: Can B do everything A can do?
Answer for Square: No, can't modify dimensions independently
Conclusion: Square IS-NOT-A Rectangle
Practice exercises for the shift:
The Rectangle-Square example is taught precisely because it's counterintuitive. Your mathematical intuition says 'square IS rectangle.' Training yourself to override this intuition with behavioral analysis is a key growth step in becoming a skilled OOP designer.
We've explored the deep distinction between mathematical and behavioral inheritance. Let's consolidate the key insights:
What's next:
Now that we understand why the Rectangle-Square inheritance fails, the natural question is: What should we do instead? In the next page, we'll explore alternative designs—ways to model the Rectangle-Square relationship (or avoid modeling it altogether) that honor LSP while maintaining clean, usable code.
You now understand the fundamental distinction between mathematical/set-theoretic inheritance and behavioral/substitutability inheritance. You can evaluate inheritance proposals using behavioral criteria and recognize when mathematical relationships don't translate to valid OOP hierarchies. Next, we'll explore alternative designs that avoid the Rectangle-Square problem.