Liskov Substitution Principle in Java

Design Java subtypes that preserve caller expectations so inheritance and polymorphism remain safe to use.

The Liskov Substitution Principle (LSP) is a fundamental concept in object-oriented design, forming the ‘L’ in the SOLID principles. It was introduced by Barbara Liskov in 1987 and is crucial for ensuring that a system’s architecture remains robust and maintainable. The principle states that objects of a superclass should be replaceable with objects of a subclass without affecting the correctness of the program. This principle is essential for achieving polymorphism and reliable inheritance hierarchies in Java.

Understanding Liskov Substitution Principle

Definition and Role in Polymorphism

The Liskov Substitution Principle can be formally defined as follows:

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program (correctness, task performed, etc.).

This principle is integral to polymorphism, a core concept in object-oriented programming that allows objects to be treated as instances of their parent class. By adhering to LSP, developers ensure that subclasses can stand in for their parent classes seamlessly, allowing for flexible and reusable code.

Violations of LSP and Their Consequences

Violating LSP can lead to unexpected behavior and bugs. Consider a scenario where a subclass overrides a method in a way that changes the expected behavior of the superclass. This can cause issues when the subclass is used in place of the superclass, leading to incorrect program logic.

Example of LSP Violation:

 1class Rectangle {
 2    private int width;
 3    private int height;
 4
 5    public void setWidth(int width) {
 6        this.width = width;
 7    }
 8
 9    public void setHeight(int height) {
10        this.height = height;
11    }
12
13    public int getArea() {
14        return width * height;
15    }
16}
17
18class Square extends Rectangle {
19    @Override
20    public void setWidth(int width) {
21        super.setWidth(width);
22        super.setHeight(width);
23    }
24
25    @Override
26    public void setHeight(int height) {
27        super.setWidth(height);
28        super.setHeight(height);
29    }
30}
31
32// Client code
33Rectangle rect = new Square();
34rect.setWidth(5);
35rect.setHeight(10);
36System.out.println(rect.getArea()); // Expected: 50, Actual: 100

In this example, the Square class violates LSP by altering the behavior of setWidth and setHeight methods. The client code expects a Rectangle behavior, but the Square class changes it, leading to incorrect results.

Rules for Creating Subclasses that Adhere to LSP

To ensure that subclasses adhere to LSP, follow these guidelines:

  1. Method Signature Compatibility: Subclasses should not change the method signatures of the superclass.
  2. Behavioral Consistency: Subclasses should maintain the expected behavior of the superclass methods.
  3. Preconditions and Postconditions: Subclasses should not strengthen preconditions or weaken postconditions.
  4. Invariant Preservation: Subclasses should maintain the invariants established by the superclass.
  5. Exception Handling: Subclasses should not introduce new exceptions that are not present in the superclass.

Supporting Reliable Inheritance Hierarchies

By adhering to LSP, developers can create reliable inheritance hierarchies that facilitate code reuse and maintainability. LSP ensures that subclasses can be used interchangeably with their parent classes, promoting a flexible and extensible system architecture.

Design Patterns and LSP

Several design patterns inherently support LSP by promoting a clear separation of concerns and ensuring that subclasses adhere to the expected behavior of their parent classes.

Template Method Pattern

The Template Method pattern defines the skeleton of an algorithm in a superclass, allowing subclasses to override specific steps without changing the algorithm’s structure. This pattern supports LSP by ensuring that subclasses can modify behavior without altering the overall algorithm.

Example of Template Method Pattern:

 1abstract class DataProcessor {
 2    public final void process() {
 3        readData();
 4        processData();
 5        writeData();
 6    }
 7
 8    abstract void readData();
 9    abstract void processData();
10    abstract void writeData();
11}
12
13class CSVDataProcessor extends DataProcessor {
14    @Override
15    void readData() {
16        System.out.println("Reading CSV data");
17    }
18
19    @Override
20    void processData() {
21        System.out.println("Processing CSV data");
22    }
23
24    @Override
25    void writeData() {
26        System.out.println("Writing CSV data");
27    }
28}
29
30// Client code
31DataProcessor processor = new CSVDataProcessor();
32processor.process();

In this example, the CSVDataProcessor class adheres to LSP by implementing the abstract methods of the DataProcessor class without altering the process method’s structure.

Factory Method Pattern

The Factory Method pattern provides an interface for creating objects in a superclass but allows subclasses to alter the type of objects that will be created. This pattern supports LSP by ensuring that object creation is consistent with the expected behavior of the superclass.

Example of Factory Method Pattern:

 1abstract class Document {
 2    public abstract void open();
 3}
 4
 5class WordDocument extends Document {
 6    @Override
 7    public void open() {
 8        System.out.println("Opening Word document");
 9    }
10}
11
12class DocumentFactory {
13    public static Document createDocument(String type) {
14        if (type.equals("Word")) {
15            return new WordDocument();
16        }
17        // Add more document types as needed
18        return null;
19    }
20}
21
22// Client code
23Document doc = DocumentFactory.createDocument("Word");
24doc.open();

In this example, the WordDocument class adheres to LSP by implementing the open method defined in the Document class, ensuring consistent behavior across different document types.

Conclusion

The Liskov Substitution Principle is a cornerstone of object-oriented design, ensuring that subclasses can be used interchangeably with their parent classes without altering the program’s correctness. By adhering to LSP, developers can create flexible, maintainable, and robust systems that leverage polymorphism and reliable inheritance hierarchies. Design patterns like Template Method and Factory Method inherently support LSP by promoting consistent behavior across subclasses.

Key Takeaways

  • LSP ensures that subclasses can replace their parent classes without affecting program correctness.
  • Violating LSP can lead to unexpected behavior and bugs.
  • Adhering to LSP promotes reliable inheritance hierarchies and facilitates code reuse.
  • Design patterns like Template Method and Factory Method support LSP by ensuring consistent behavior across subclasses.

Exercises

  1. Refactor the Rectangle and Square example to adhere to LSP.
  2. Implement a new subclass using the Template Method pattern and ensure it adheres to LSP.
  3. Create a new document type using the Factory Method pattern and verify its adherence to LSP.

Further Reading

Test Your Knowledge: Liskov Substitution Principle in Java Quiz

Loading quiz…
Revised on Thursday, April 23, 2026