Skip to content

LSP - The Liskov Substitution Principle

If code works with a base type, it must keep working with any subtype. No surprise exceptions, no stricter inputs, no weaker promises.

Builds on Abstract classes and inheritance and Method overriding and polymorphism

Stage 2core1 min read

LSP Violation Example — "Bad Code"

Code Overview

package SOLID.LSP.BadCode;

public class File {
    public void read() {
        System.out.println("reading from file...");
    }

    public void write() {
        System.out.println("writing a file....");
    }
}

public class ReadOnlyFile extends File {
    @Override
    public void write() {
        // The parent promised writing, but this subtype takes it away.
        throw new UnsupportedOperationException("Can't write to a read-only file");
    }
}

public class Main {
    public static void main(String[] args) {
        File file = new ReadOnlyFile();
        file.read(); // works fine
        file.write(); // throwing an exception, violation of LSP.
    }
}

The comment inside ReadOnlyFile is reworded for teaching. The repo file has a shorter one.

The assignment File file = new ReadOnlyFile() is legal Java. It only shows that the types fit together. It does not show that ReadOnlyFile behaves like a File.


Why This Code is Bad

Subclasses should be replaceable for their parent classes without changing the expected behavior of the program.

Follow the broken promise step by step:

  1. File exposes write(). A caller receiving a File is allowed to call it.
  2. The promise (postcondition) is that the write completes. Nothing in File says writing is optional.
  3. ReadOnlyFile inherits that public promise.
  4. Its override throws a runtime exception the parent never threw, instead of writing.
  5. Substituting ReadOnlyFile therefore breaks code that worked with an ordinary File.

Java checks the method signature, not this promise. Both methods return void and take no arguments, so the compiler accepts the override. LSP covers the behavior the compiler cannot check.


What’s Wrong Conceptually

  • ReadOnlyFile does not behave like the File described by the parent API.
  • write() changes from a supported operation into an unsupported one.
  • The subtype adds a surprise unchecked exception (one the compiler does not force callers to handle).
  • Callers start adding checks such as if (!(file instanceof ReadOnlyFile)). Now every caller must know about ReadOnlyFile, which defeats the point of substitution.

The deeper problem is the parent class: it bundles reading and writing, but some files can only be read. The GoodCode version separates those capabilities so impossible calls fail at compile time.


See GoodCode for the capability-based design and LSP-Notes.md for contracts, Rectangle/Square, payment examples, and trade-offs.

In this topic

Before code3 files

BadCode/File.java13 linesGitHub
package SOLID.LSP.BadCode;public class File {    public void read() {        System.out.println("reading from file...");    }    public void write() {        System.out.println("writing a file....");    }}
BadCode/Main.java12 linesGitHub
package SOLID.LSP.BadCode;public class Main {    @SuppressWarnings("unused")    public static void main(String[] args) {        File file = new ReadOnlyFile();        file.read(); // works fine        file.write(); // throwing an exception, violation of LSP.    }}
BadCode/ReadOnlyFile.java13 linesGitHub
package SOLID.LSP.BadCode;public class ReadOnlyFile extends File {    // here in read-only file, the write method is not supported hence this is BAD_CODE    @Override    public void write() {        throw new UnsupportedOperationException(            "Can't write to a read-only file"        );    }}

Done with this topic?

How well do you know it?

PreviousOCP - The Open/Closed Principle
Practise 7 questions