Loading content...
In the previous page, we witnessed the Rectangle-Square problem in action. We saw how a Square class extending Rectangle causes client code to break when a Square is used polymorphically as a Rectangle. But seeing the problem isn't the same as understanding it.
This page answers the deeper question: Why does this happen? We'll explore the theoretical foundations—contract theory, invariant analysis, and the crucial distinction between syntactic and semantic compatibility—that explain why the seemingly natural 'Square extends Rectangle' relationship is fundamentally flawed.
Understanding why this violation occurs is essential. Without this understanding, you'll encounter the same pattern in different forms and fail to recognize it. With it, you'll develop an intuition for spotting LSP violations early in design.
By the end of this page, you will understand the theoretical basis for why Square cannot substitute for Rectangle, how to analyze inheritance relationships using contract theory, and how to distinguish between syntactic compatibility (same method signatures) and semantic compatibility (same behavioral guarantees).
To understand the Rectangle-Square violation, we must first grasp a fundamental distinction in type systems: the difference between syntactic and semantic compatibility.
Syntactic compatibility (also called structural compatibility) means that two types have the same shape—the same method signatures, parameter types, and return types. Compilers can typically verify syntactic compatibility.
Semantic compatibility (also called behavioral compatibility) means that two types have the same behavior—they honor the same contracts, maintain the same invariants, and meet the same expectations. Compilers generally cannot verify semantic compatibility.
The key insight: Square is syntactically compatible with Rectangle—it has all the same methods with the same signatures. Your compiler happily accepts a Square wherever a Rectangle is expected. But Square is not semantically compatible with Rectangle—it doesn't honor the same behavioral contracts.
This is precisely why LSP violations are subtle and dangerous. The type system says 'yes, this substitution is valid' while the behavior says 'no, this will break at runtime.'
123456789101112131415161718192021222324
// The type system only checks SYNTACTIC compatibility public class Rectangle { public void setWidth(int w) { ... } // Signature: void setWidth(int) public void setHeight(int h) { ... } // Signature: void setHeight(int)} public class Square extends Rectangle { @Override public void setWidth(int w) { ... } // ✓ Same signature @Override public void setHeight(int h) { ... } // ✓ Same signature} // Compiler says: "Square has all required methods with correct signatures"// Compiler says: "Square IS-A Rectangle (structurally)"// Compiler says: "Substitution is ALLOWED" // But the compiler CANNOT verify:// - Does Square.setWidth() honor Rectangle.setWidth()'s postconditions?// - Does Square maintain Rectangle's invariant expectations?// - Will client code behave correctly with Square in place of Rectangle? // LSP demands SEMANTIC compatibility, which compilers can't check!LSP violations compile successfully. They may even pass unit tests if tests only exercise subclasses in isolation. The violation manifests only when the subclass is used polymorphically through the base class interface. This is why understanding semantic compatibility is so crucial.
Barbara Liskov's formulation of the substitution principle is grounded in Design by Contract (DbC), a methodology developed by Bertrand Meyer. In DbC, every method has a contract consisting of three parts:
LSP specifies precise rules for how subclasses may relate to these contracts:
| Contract Element | LSP Requirement | Reasoning |
|---|---|---|
| Preconditions | Same or WEAKER in subclass | Subclass should accept all inputs the base class accepts (and possibly more) |
| Postconditions | Same or STRONGER in subclass | Subclass should provide all guarantees the base class provides (and possibly more) |
| Invariants | Must be maintained in subclass | Subclass must never violate properties that the base class guarantees |
Now let's apply these rules to the Rectangle-Square problem:
Rectangle.setWidth(int w) Contract:
12345678910111213141516171819202122
/** * Rectangle.setWidth(int w) Contract Specification * * @precondition: w >= 0 * The width must be a non-negative integer. * * @postcondition: getWidth() == w * After the method returns, getWidth() returns the value that was passed. * * @postcondition: getHeight() == old(getHeight()) * The height is UNCHANGED by this operation. * (This is the IMPLICIT but CRITICAL postcondition) * * @postcondition: getArea() == w * old(getHeight()) * The area is updated to reflect the new width with unchanged height. * * @invariant: (none specified by Rectangle) * Rectangle does not require width == height or any similar constraint. */public void setWidth(int w) { this.width = w;}Square.setWidth(int w) Contract:
12345678910111213141516171819202122232425
/** * Square.setWidth(int w) Contract Specification * * @precondition: w >= 0 * Same as Rectangle — this is acceptable (not stronger). * * @postcondition: getWidth() == w * ✓ SATISFIED — width is set to w. * * @postcondition: getHeight() == old(getHeight()) * ✗ VIOLATED! getHeight() is now equal to w, not the old height! * This is where the LSP violation occurs. * * @postcondition: getArea() == w * old(getHeight()) * ✗ VIOLATED! getArea() is now w * w, not w * old(getHeight()). * * @invariant: width == height (ALWAYS) * This invariant is STRONGER than Rectangle's (which has none). * To maintain this invariant, Square MUST violate Rectangle's postconditions. */@Overridepublic void setWidth(int w) { this.width = w; this.height = w; // Required to maintain Square's invariant} // BUT violates Rectangle's postcondition!Square.setWidth() violates Rectangle.setWidth()'s postcondition that height remains unchanged. This isn't a technicality—it's a fundamental breach of the behavioral contract. Any client code that depends on this postcondition (consciously or not) will break when given a Square.
The root cause of the Rectangle-Square problem is invariant incompatibility. An invariant is a property that must always hold for an object—before and after every method call.
Rectangle's implicit invariant:
Square's required invariant:
These invariants are fundamentally incompatible. Square's invariant is stronger (more restrictive) than Rectangle's. To maintain its invariant, Square must violate Rectangle's behavioral expectations.
12345678910111213141516171819202122232425262728293031323334353637383940414243
/** * Invariant Compatibility Analysis * * Rectangle allows states: * | Width | Height | Valid? | * |---------|----------|----------| * | 3 | 5 | ✓ | * | 7 | 2 | ✓ | * | 4 | 4 | ✓ | * | 10 | 10 | ✓ | * * Square allows states: * | Width | Height | Valid? | * |---------|----------|----------| * | 3 | 5 | ✗ | <- Invalid for Square! * | 7 | 2 | ✗ | <- Invalid for Square! * | 4 | 4 | ✓ | * | 10 | 10 | ✓ | * * Square's valid states are a SUBSET of Rectangle's valid states. * This means Square is MORE RESTRICTED than Rectangle. * * For LSP to hold, the subclass should support AT LEAST all states * that the base class supports. Square supports FEWER states. * * This is the INVARIANT TRAP: A subclass with a stronger invariant * cannot substitute for a base class with a weaker invariant. */ // The problem manifests when clients try to use Rectangle's full state space: void clientCode(Rectangle rect) { rect.setWidth(10); rect.setHeight(5); // Perfectly valid for Rectangle // Client expects width=10, height=5 // But if rect is actually a Square: // - setWidth(10) sets width=10, height=10 // - setHeight(5) sets width=5, height=5 // - Final state: width=5, height=5 // // The client's expectations are VIOLATED}The invariant trap generalized:
Whenever a subclass has a stronger invariant (restricts the valid states) than its base class, it cannot properly substitute for the base class. The base class's clients may depend on states or behaviors that the subclass cannot support.
This gives us a useful heuristic for detecting LSP violations:
Ask yourself: Does my subclass reject any states or operations that the base class accepts? If yes, your subclass has a stronger invariant, and you likely have an LSP violation. The subclass restricts the 'state space' of the base class, breaking substitutability.
An interesting observation: the Rectangle-Square problem only manifests because Rectangle is mutable. If Rectangle were immutable—without setters—the problem wouldn't occur.
Consider an immutable design:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950
public class ImmutableRectangle { private final int width; private final int height; public ImmutableRectangle(int width, int height) { this.width = width; this.height = height; } public int getWidth() { return width; } public int getHeight() { return height; } public int getArea() { return width * height; } // No setters — the object is immutable after construction // Factory methods to create modified copies public ImmutableRectangle withWidth(int newWidth) { return new ImmutableRectangle(newWidth, this.height); } public ImmutableRectangle withHeight(int newHeight) { return new ImmutableRectangle(this.width, newHeight); }} public class ImmutableSquare extends ImmutableRectangle { public ImmutableSquare(int side) { super(side, side); } // Override factory methods to return squares @Override public ImmutableRectangle withWidth(int newWidth) { return new ImmutableRectangle(newWidth, getHeight()); // Returns a Rectangle, not a Square — this is correct! // Changing one dimension breaks the "squareness" } @Override public ImmutableRectangle withHeight(int newHeight) { return new ImmutableRectangle(getWidth(), newHeight); // Returns a Rectangle, not a Square — also correct! } // Square-specific method public ImmutableSquare withSide(int newSide) { return new ImmutableSquare(newSide); }}Why does immutability help?
With immutable objects, there are no state-modifying operations that could violate postconditions. The object is set once at construction and never changes. The factory methods (withWidth, withHeight) return new objects, so the original object's 'state' is never altered.
The immutable solution works because:
ImmutableSquare.withWidth() can legitimately return an ImmutableRectanglewidth == heightHowever, this only sidesteps the problem rather than solving it. If your domain requires mutable rectangles, immutability isn't an option.
The Rectangle-Square problem doesn't disappear with immutability—the underlying type relationship is still wrong. Immutability merely removes the specific mechanism (setters) that triggers the violation. The fundamental tension between mathematical and behavioral inheritance remains.
Barbara Liskov and Jeannette Wing formalized an important aspect of LSP called the History Rule (sometimes called the History Constraint). This rule addresses how objects evolve over time through sequences of operations.
The History Rule states:
A subtype should allow only those state transitions that the base type would allow. If the base type can transition from state A to state B through an operation, and the subtype supports that operation, it must also support that transition.
In simpler terms: If I can take a Rectangle through a sequence of operations reaching a certain state, I must be able to take a Square through the same sequence reaching compatible states.
12345678910111213141516171819202122232425262728293031323334353637383940414243
/** * History Rule Violation Demonstration * * Consider the following sequence of operations on a Rectangle: */ void demonstrateHistoryViolation() { // Valid sequence for Rectangle Rectangle rect = new Rectangle(4, 4); // State: {4, 4} rect.setWidth(10); // State: {10, 4} rect.setHeight(5); // State: {10, 5} // Final state: width=10, height=5 // Rectangle's "history" includes: // {4,4} -> {10,4} -> {10,5} // This is a VALID history for Rectangle // Now apply same sequence to Square: Rectangle square = new Square(4); // State: {4, 4} square.setWidth(10); // State: {10, 10} <- Different! square.setHeight(5); // State: {5, 5} <- Different! // Final state: width=5, height=5 // Square's "history" is: // {4,4} -> {10,10} -> {5,5} // // Square CANNOT reach state {10, 5} or {10, 4} // These are valid Rectangle states that Square cannot support // // The HISTORY RULE is violated because: // - Rectangle can reach {10, 4} but Square cannot // - Rectangle can reach {10, 5} but Square cannot // - The possible "histories" of Square are a proper subset // of the possible histories of Rectangle} /** * For LSP to hold, any history possible for the base type * must also be possible for the subtype. * * Square violates this because many Rectangle histories * are impossible for Square. */The History Rule is closely related to the invariant trap we discussed earlier. Because Square's state space is a subset of Rectangle's state space, many valid Rectangle histories are impossible for Square.
Why this matters:
Client code often depends on being able to manipulate objects through sequences of operations. If a subtype cannot support the same operation sequences (histories) as the base type, substitution breaks the client's expectations.
Single-operation testing may not reveal LSP violations. It's often the sequence of operations that exposes the problem. Always test substitutability through realistic operation sequences, not just individual method calls.
Let's synthesize our analysis into a formal checklist. For Square to satisfy LSP as a subtype of Rectangle, it must pass ALL of these checks:
| Requirement | Status | Analysis |
|---|---|---|
| Signature Compatibility | ✓ PASS | Square has all Rectangle methods with compatible signatures |
| Precondition Rule (no strengthening) | ✓ PASS | Square.setWidth() accepts any non-negative int, same as Rectangle |
| Postcondition Rule (no weakening) | ✗ FAIL | Square.setWidth() changes height, violating Rectangle's postcondition |
| Invariant Preservation | ✗ FAIL | Square introduces a stronger invariant (width==height) not present in Rectangle |
| History Rule | ✗ FAIL | Many valid Rectangle state sequences are impossible for Square |
| Exception Rule | ✓ PASS | Square doesn't throw exceptions not thrown by Rectangle |
Verdict: LSP VIOLATED
Square fails three of the six LSP requirements. Any one failure is sufficient to violate LSP; Square fails three.
The failures cascade from a single root cause: Square has a stronger invariant than Rectangle. This forces Square to modify its behavior (changing height when width is set) to maintain that invariant, which breaks postconditions and history compatibility.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
/** * Systematic LSP Verification for Square extends Rectangle * * This demonstrates how to formally verify LSP compliance. */public class LSPVerification { // Test 1: Postcondition Check public static boolean postconditionCheck(Rectangle r) { int originalHeight = r.getHeight(); r.setWidth(10); // Rectangle's postcondition: height unchanged after setWidth return r.getHeight() == originalHeight; // FAILS for Square } // Test 2: Independent Dimension Check public static boolean independentDimensionCheck(Rectangle r) { r.setWidth(10); r.setHeight(20); // Must be able to set dimensions to different values return r.getWidth() == 10 && r.getHeight() == 20; // FAILS for Square } // Test 3: History Check public static boolean historyCheck(Rectangle r) { // Can we reach state {10, 5}? r.setWidth(10); r.setHeight(5); return r.getWidth() == 10 && r.getHeight() == 5; // FAILS for Square } // Running with different types: public static void main(String[] args) { Rectangle rect = new Rectangle(5, 5); System.out.println("Rectangle - Postcondition: " + postconditionCheck(rect)); // true System.out.println("Rectangle - Independent: " + independentDimensionCheck(rect)); // true System.out.println("Rectangle - History: " + historyCheck(rect)); // true Rectangle square = new Square(5); System.out.println("Square - Postcondition: " + postconditionCheck(square)); // false! System.out.println("Square - Independent: " + independentDimensionCheck(square)); // false! System.out.println("Square - History: " + historyCheck(square)); // false! }}Why do so many developers fall into the Rectangle-Square trap? The answer lies in the difference between mathematical thinking and behavioral thinking.
Mathematical thinking focuses on what things are:
Behavioral thinking focuses on what things do:
The LSP is a BEHAVIORAL principle. It's not asking 'IS Square a kind of Rectangle?' It's asking 'Can Square DO everything Rectangle can do?'
Square IS-A Rectangle.
'Every square is a rectangle' is mathematically true. There is no square that is not also a rectangle. This is set theory.
Square BEHAVES-LIKE-A Rectangle?
'Every square behaves like a rectangle' is false. Squares have behavioral constraints that rectangles don't. This is object-oriented design.
The lesson:
When designing inheritance hierarchies, don't ask 'Is B a kind of A?' Ask instead: 'Can B substitute for A in all contexts where A is expected?'
This shift from 'IS-A' to 'BEHAVES-AS' thinking is fundamental to mastering LSP. Many mathematical IS-A relationships do not translate to behavioral BEHAVES-AS relationships in object-oriented design.
Before creating an inheritance relationship, ask: 'Would any client code break if I substituted the subclass for the base class?' If the answer is yes—or even maybe—reconsider the inheritance. Mathematical IS-A is irrelevant; behavioral IS-SUBSTITUTABLE-FOR is everything.
We've now deeply explored why Square extending Rectangle violates LSP. Let's consolidate:
What's next:
We've established that Square breaks Rectangle and why it breaks. In the next page, we'll explore the deeper tension between mathematical inheritance and behavioral inheritance—understanding why 'IS-A' in mathematics doesn't translate to 'IS-A' in object-oriented design, and how this insight shapes our approach to inheritance.
You now understand the theoretical foundations of why the Rectangle-Square inheritance violates LSP. You can analyze inheritance relationships using contract theory, invariant analysis, and the history rule. Next, we'll explore the mathematical vs behavioral inheritance distinction in depth.