Liskov Substitution
Definition

Classic Violation: Rectangle/Square
// BAD: Square violates LSP when substituted for Rectangle
public class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int getArea() {
return width * height;
}
}
// Square IS-A Rectangle mathematically, but...
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Must keep square!
}
@Override
public void setHeight(int height) {
this.width = height; // Must keep square!
this.height = height;
}
}
// This test passes for Rectangle, FAILS for Square!
public void testRectangle(Rectangle rect) {
rect.setWidth(5);
rect.setHeight(4);
assert rect.getArea() == 20; // FAILS for Square! Returns 16
}
// Square cannot be substituted for Rectangle without breaking behavior
Proper Design
// GOOD: Separate abstractions for different behaviors
// Common interface for shapes
public interface Shape {
int getArea();
}
// Rectangle - independent dimensions
public class Rectangle implements Shape {
private final int width;
private final int height;
public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
public int getArea() {
return width * height;
}
// Immutable or with proper validation
public Rectangle withWidth(int newWidth) {
return new Rectangle(newWidth, this.height);
}
public Rectangle withHeight(int newHeight) {
return new Rectangle(this.width, newHeight);
}
}
// Square - single dimension
public class Square implements Shape {
private final int side;
public Square(int side) {
this.side = side;
}
public int getArea() {
return side * side;
}
public Square withSide(int newSide) {
return new Square(newSide);
}
}
// Now code that uses Shape works correctly with both
public int calculateTotalArea(List<Shape> shapes) {
return shapes.stream()
.mapToInt(Shape::getArea)
.sum();
}
LSP Rules

More Violation Examples
// Violation 1: Strengthening preconditions
class Bird {
void fly(int altitude) {
// Any altitude accepted
}
}
class Penguin extends Bird {
@Override
void fly(int altitude) {
if (altitude > 0) {
throw new UnsupportedOperationException("Can't fly!");
}
// Strengthened precondition - breaks LSP
}
}
// Violation 2: Weakening postconditions
class FileReader {
String read(String path) {
// Always returns non-null content or throws
return Files.readString(Path.of(path));
}
}
class CachingFileReader extends FileReader {
@Override
String read(String path) {
// Might return null if not cached - weaker guarantee!
return cache.get(path);
}
}
// Violation 3: Breaking invariants
class BankAccount {
protected double balance;
void withdraw(double amount) {
if (amount > balance) {
throw new InsufficientFundsException();
}
balance -= amount;
assert balance >= 0; // Invariant
}
}
class OverdraftAccount extends BankAccount {
@Override
void withdraw(double amount) {
balance -= amount; // Allows negative balance!
// Breaks the invariant of BankAccount
}
}
Correct Inheritance
// GOOD: Proper inheritance that respects LSP
// Base class with clear contract
public abstract class Account {
protected Money balance;
public Money getBalance() {
return balance;
}
public abstract void withdraw(Money amount);
// Invariant: withdraw should throw if insufficient funds
// Postcondition: balance >= 0 after any operation
}
// Subclass respects base contract
public class SavingsAccount extends Account {
@Override
public void withdraw(Money amount) {
if (amount.isGreaterThan(balance)) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
}
}
// Different account type with EXPLICIT different rules
public class OverdraftAccount extends Account {
private Money overdraftLimit;
@Override
public void withdraw(Money amount) {
Money available = balance.add(overdraftLimit);
if (amount.isGreaterThan(available)) {
throw new InsufficientFundsException();
}
balance = balance.subtract(amount);
// Note: balance CAN be negative, but this is EXPECTED
// and documented for OverdraftAccount
}
}
// Better design: Different interface for overdraft
public interface WithdrawCapable {
void withdraw(Money amount);
Money getAvailableBalance(); // May differ from actual balance
}
Design-By-Contract

Tips & Tricks
