Loslegen
Javaneer
Zurück zur Stufe
Stufe 0·SOLID in der Praxis

Liskov-Substitution

Subtypen müssen über den Basistyp nutzbar sein, ohne Überraschungen. Die klassische Quadrat/Rechteck-Falle, verhaltensbasierte Subtypisierung und warum 'ist-ein' nicht genügt.

14 Min. LesezeitExperte

Deutsche Übersetzung in Arbeit

Diese Lektion ist noch nicht ins Deutsche übersetzt und wird daher auf Englisch angezeigt. Der Rest der Seite ist vollständig lokalisiert.

Auf dieser Seite

The Liskov Substitution Principle is the one that makes polymorphism safe. Named after Barbara Liskov, it says: objects of a subtype must be usable anywhere the base type is expected, without breaking the program's correctness. If code works with a FeePolicy, it must work with every FeePolicy - no exceptions, no "but this one throws if you call it wrong." Violate LSP and Open/Closed collapses, because you can no longer trust the abstraction.

Substitutability, not just "is-a"

Inheritance in Java gives you "is-a": a Square is a Rectangle, a PenguinAccount is an Account. But LSP demands more than the type checker's approval - it demands behavioral compatibility. The subtype must honor the contract the base type promised: same or weaker preconditions, same or stronger postconditions, no surprising exceptions, no strengthened requirements.

The famous counterexample is the square/rectangle:

class Rectangle {
    protected int width, height;
    void setWidth(int w)  { this.width = w; }
    void setHeight(int h) { this.height = h; }
    int area() { return width * height; }
}
class Square extends Rectangle {          // a square IS-A rectangle, right?
    void setWidth(int w)  { this.width = w; this.height = w; }   // keep it square
    void setHeight(int h) { this.width = h; this.height = h; }
}

Now this perfectly reasonable code, written against Rectangle, breaks:

void resizeAndCheck(Rectangle r) {
    r.setWidth(5);
    r.setHeight(4);
    assert r.area() == 20;   // holds for Rectangle... FAILS for Square (area == 16)
}

Square passes the compiler but violates the behavioral contract: Rectangle promised width and height are independent, and Square broke that promise. Any code relying on the base type's contract is now unsafe. "Is-a" was true; substitutable was not.

Tells of an LSP violation

You've probably smelled these without naming them:

  • A subtype throwing on an inherited method - UnsupportedOperationException in an override (the classic List.of(...) immutable list that throws on add).
  • instanceof checks before using a subtype - code that must ask "which subclass is this?" is admitting the subtypes aren't truly substitutable.
  • Overrides that do nothing or weaken behavior - an override that silently ignores a call the base type honored.
  • Strengthened preconditions - a subtype that rejects inputs the base type accepted.

In ledger-legacy, a ReadOnlyAccount extends Account that throws on deposit() is an LSP landmine: any method taking an Account and calling deposit will blow up on that one subtype.

Fixing it: model the real relationship

The fix is rarely "make the subtype behave" - it's usually that inheritance was the wrong tool. A square and a rectangle are both shapes with an area(), but neither is a safe subtype of the other under mutation. Prefer:

  • Composition over inheritance - Square has dimensions, it isn't a mutable Rectangle.
  • A common abstraction - both implement a Shape interface with area(), neither extends the other.
  • Immutability - if Rectangle were immutable (no setters), Square could safely extend it, because the broken promise was about mutation.

LSP violations poison Open/Closed

The whole point of OCP was that FeeCalculator can trust any FeePolicy. If one FeePolicy secretly throws or returns nonsense for certain inputs, the calculator must special-case it - and you're back to modifying working code. LSP is what lets the other principles' abstractions actually be trusted. A leaky subtype forces instanceof checks that unravel the whole design.

A job description and the temp who can't do it

A base type is a job description: 'this role answers phones and processes refunds.' Substitutability means any person you send fills that role without the caller noticing who showed up. LSP is violated when the temp agency sends someone who answers phones but throws a tantrum at refunds - technically 'an employee,' but now every manager must check who they got before assigning work (instanceof), and code written to the job description breaks. The fix isn't to force the temp to fake it; it's to realize this person was hired for a different job - model them under the role they can actually fulfill.

Spot and fix the violation

ledger-legacy has class SavingsAccount extends Account where the base Account.withdraw(amount) promises to reduce the balance, but SavingsAccount.withdraw throws IllegalStateException if more than 3 withdrawals were made this month. A reporting job iterates all Accounts calling withdraw during reconciliation and now crashes on savings accounts. Explain the LSP violation and propose a fix.

What does the Liskov Substitution Principle require of a subtype?

Key takeaways

  • LSP: a subtype must be substitutable for its base type without breaking the base type's behavioral contract - more than the compiler's 'is-a'.
  • The subtype must not strengthen preconditions, weaken postconditions, or throw surprising exceptions on inherited methods.
  • Tells: an override that throws UnsupportedOperationException, instanceof checks before use, or do-nothing/weakening overrides (the square/rectangle trap).
  • LSP violations poison Open/Closed - abstractions become untrustworthy and callers must special-case subtypes, unraveling the design.
  • The fix is usually that inheritance was wrong: prefer composition, a shared interface, or immutability so the real relationship is modeled honestly.
War diese Lektion hilfreich?
Diese Seite auf GitHub bearbeiten