CMD Guide
HomeOO & Low-Level DesignSOLID Principles

Introduction to the Liskov Substitution Principle

The Liskov Substitution Principle (LSP) works because calling code is written against the static type of a reference, not the runtime object: when you hold a Car reference and call refuel(), the compiler accepts it because Car declares refuel(), but at runtime Java dispatches to the actual object's method via the vtable. If the subclass's override silently narrows that contract — throws where the parent succeeded, or returns a result the caller never expected — the substitution that the type system promised was safe blows up at runtime. LSP is the rule that keeps that promise honest: a subtype must accept everything the supertype accepts and deliver everything the supertype guarantees, so that no caller written against the supertype can tell the difference.

The broken hierarchy, traced

Here is the classic violation. Car exposes a three-method contract; ElectricCar inherits it but cannot honour refuel(), so it throws.

public class Car {
    public void start()  { System.out.println("Car is starting"); }
    public void stop()   { System.out.println("Car is stopping"); }
    public void refuel() { System.out.println("Car is refueling"); }
}

public class ElectricCar extends Car {
    @Override
    public void refuel() {
        // Electric cars have no fuel tank — the parent's contract is impossible to keep.
        throw new UnsupportedOperationException("Electric cars don't need refueling");
    }
    public void recharge() { System.out.println("Car is recharging"); }
}

Now a perfectly ordinary client — a fleet routine that tops up every car before a trip — is written against the base type Car. It has no idea electric cars exist; it just trusts the contract.

public class Main {
    // Caller depends ONLY on the Car contract. This is the substitution point.
    static void prepareForTrip(Car car) {
        car.start();
        car.refuel();   // contract says: this returns normally
        car.stop();
    }

    public static void main(String[] args) {
        Car gasCar      = new Car();          // a plain Car
        Car electricCar = new ElectricCar();  // an ElectricCar, typed as Car

        prepareForTrip(gasCar);       // OK
        prepareForTrip(electricCar);  // BOOM at the refuel() line
    }
}

Walk the second call through the runtime, line by line. The reference type is Car, so each call compiles; the object type is ElectricCar, so each call dispatches to ElectricCar's version.

StepStatement (inside prepareForTrip)Static type seen by compilerMethod actually run (dynamic dispatch)Result
1car.start()CarCar.start() (inherited)prints "Car is starting" — fine
2car.refuel()Car → compiles, looks safeElectricCar.refuel() (override)throws UnsupportedOperationException
3car.stop()Carnever reached; stack unwinds, exception propagates out of main

The program crashes at step 2. Crucially, prepareForTrip did nothing wrong — it used only methods the Car type promised would work. The defect is that ElectricCar is-a Car in the type system but is not a Car in behaviour. That gap between "compiles" and "works" is exactly what LSP forbids.

diagram
diagram

Why the naive "just throw" version is wrong

Throwing from refuel() feels defensive — "I'm honestly signalling that this isn't supported." But it breaks LSP in a way the compiler can't catch. LSP, as formalised by Barbara Liskov and Jeannette Wing, requires a subtype to obey three behavioural rules relative to its supertype:

ElectricCar.refuel() violates the second and third: it weakens the postcondition (no longer returns normally) and introduces an exception the Car contract never advertised. The real problem is upstream — refuel() was modelled as a behaviour of all cars when it is really a behaviour of fuel-powered cars. The fix (next lesson) is to break the hierarchy: pull the energy-replenishment behaviour out of Car entirely, e.g. give Car a start()/stop() core and model refuelling/recharging through a separate Refuelable / Rechargeable capability or by composing an EnergySource.

LSP, formally: the variance rules the compiler already enforces

The three prose rules above are the behavioural contract. Part of that contract is structural — the Java compiler enforces it for you on every override, and knowing exactly which part is checked (and which is not) is a common senior follow-up.

Beyond types, the Liskov–Wing formulation adds one rule the compiler cannot check — the history constraint: a subtype may not permit a state change that the supertype's contract forbids. Two objects can have byte-for-byte identical method signatures and still violate substitutability through allowed mutations over time. This is exactly why an immutable or fixed-size List is not substitutable for a mutable one: every signature matches, but calling add() on Arrays.asList(...) throws UnsupportedOperationException — the fixed-size subtype forbids a state change (growth) that the mutable List contract permits, breaking the history constraint (and tying directly back to the Arrays.asList pitfall below).

Pitfalls

How a senior engineer decides: inheritance vs the alternatives

LSP is the litmus test for whether extends is the right tool at all. The decision signal is one question: can every method the parent promises be honoured by the child for every input the parent accepts? If yes, inheritance is safe and cheap. If even one method can't (our refuel()), you've hit a fork.

Choose inheritance when the subtype passes the substitution test on every method; prefer composition with capability interfaces when any inherited method would have to throw, no-op, or weaken its result to compile. Concretely for our case: because ElectricCar cannot honour refuel(), drop refuel() from Car, keep Car as the start/stop base, and let only fuel cars implement a Refuelable interface — composition wins here.

Takeaways


Re-authored and deepened for this guide. Sources: Barbara Liskov & Jeannette Wing, "A Behavioral Notion of Subtyping" (ACM TOPLAS, 1994); Robert C. Martin, Agile Software Development: Principles, Patterns, and Practices (the canonical Rectangle/Square treatment of LSP); the original Car/ElectricCar example carried over from this lesson; and the Java Language Specification on dynamic dispatch, covariant arrays, and ArrayStoreException. Worked trace, mechanism, hierarchy diagram, pitfalls, and the inheritance-vs-composition trade-off section added to meet the depth bar.

🤖 Don't fully get this? Learn it with Claude

Stuck on Introduction to the Liskov Substitution Principle? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **Introduction to the Liskov Substitution Principle** (OO & Low-Level Design) and want to truly understand it. Explain Introduction to the Liskov Substitution Principle from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **Introduction to the Liskov Substitution Principle** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **Introduction to the Liskov Substitution Principle** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **Introduction to the Liskov Substitution Principle** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes