How to apply the Liskov substitution principle in Java

Twenty-three years ago, in his Design Principles and Design Patterns article, Robert "Uncle Bob" Martin boiled down the Liskov substitution principle to the idea that "derived classes should be substitutable for their base classes."

That's a good start to help developers understand LSP, but Martin's quick description initially reads like a summary of Java's out-of-the-box inheritance and polymorphism mechanisms. We need to dig deeper to understand what he really meant, to then properly understand LSP.

What is the Liskov substitution principle?

The Liskov substitution principle -- the "L" in the SOLID principles of object-oriented design -- simply demands that any time an instance of a parent class is required, any instance of a subclass should suffice. The goal with LSP is philosophical and behavioral sufficiency, not simply the syntactic sufficiency that inheritance, polymorphism and the Java compiler provide.

Consider a simple problem domain for which we need to calculate the area of various shapes, specifically squares and rectangles. The creation of a Square class that inherits from a Rectangle class, along with a runnable main method, is an easy way to explore the relationship between inheritance, polymorphism and LSP.

class Rectangle {
  int height;
  int width;
  Rectangle(int h, int w){
    height = h;
    width = w;
  }
}

class Square extends Rectangle {
  Square(int h) {
    super(h,h);
  }
}

public class Main {
    public static void main(String[] args) {
      calcArea(new Rectangle(4,5));  //prints 20
      calcArea(new Square(5));       //prints 25
  }
  public static void calcArea(Rectangle rect){
    System.out.println(rect.height * rect.width);
  }
  
}

LSP example in Java

The calcArea method in the Main class takes a Rectangle as an argument. However, a Square inherits from Rectangle and thus it is a special type of Rectangle, so we can legally pass an instance of the Square into the calcArea method as well:

calcArea(new Rectangle(4,5));  //prints 20
calcArea(new Square(5));       //prints 25

This looks like the Liskov substitution principle that Uncle Bob Martin described: Substitute a derived class (Square) for a base class (Rectangle).

From the standpoint of the compiler, the above code properly implements polymorphism and abides by the rules of inheritance in Java. However, this code fails to meet the philosophical and behavioral standard that Liskov substitution requires.

Proof by contradiction

One obvious failure of this code is that a Square can set its height without setting its width. For example, the following Square has a height of 5 and width of 4 when the code runs:

Square square = new Square(5);
Square.width = 4;

This issue isn't specifically an LSP problem, but it is a problem, and fixing it can violate the LSP.

Instead, we simply introduce setters and getters, and force the height and width to always synchronize when a Square changes its size:

class Rectangle {
  int height;
  int width;
  
  int getHeight(){return height;}
  int getWidth() {return width;}
  void setHeight(int h) { height = h;}
  void setWidth(int w)  { width = w;} 
  
  Rectangle(int h, int w){
    height = h;
    width = w;
  }
}

class Square extends Rectangle {
  Square(int h) {
    super(h,h);
  }
  void setHeight(int h) { 
    height = h; 
    width = h;
  }

  void setWidth(int w)  { 
    height = w; 
    width = w;
  } 
}

The code now synchronizes the height and width when using the Square class's setter methods. The code compiles and the Square's expected behavior is enforced.

However, this apparent improvement creates new difficulties of LSP noncompliance. Take the following code snippet that inspects the behavior of a Rectangle:

public static void testRectangle(Rectangle rect) {
  rect.setHeight(4);
  rect.setWidth(5);
  assertTrue(rect.getHeight() == 4) ;
}

If you attempt to pass a Square into this method, the code would run, the rules of inheritance would apply and polymorphism would work -- but the application would not behave as expected. One would expect to separately change the height and width of a rectangle, but this expected behavior fails when we substitute a Square for a Rectangle.

It's not good enough to simply use inheritance and apply the rules of polymorphism to your code. To write SOLID code, and properly implement the Liskov substitution principle in Java, your code must be philosophically and behaviorally compliant as well.

Ashik Patel is an associate full-stack developer with a background as a back-end developer and API developer. He has worked with various programming languages and frameworks, including Java, JavaScript, Go and Python.

View All Videos
App Architecture
Software Quality
Cloud Computing
Security
SearchAWS
Close