Skip to content

Liskov Substitution

Definition

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

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

Design by Contract & LSP


Tips & Tricks

LSP Tips