What is the Liskov Substitution Principle: An Explanation with Examples in Java

Sharing is caring

In this post, we will understand the Liskov substitution principle and illustrate how it works with an extended example in Java.

The Liskov substitution principle states that an object of a superclass should be replaceable with an object of any of its subclasses. It is one of the SOLID design principles in object-oriented software engineering.

Understanding the Liskov Substitution Principle

The ability to replace a superclass with any of its subclasses may sound trivial at first to anyone familiar with object-oriented design. After all, an inheritance hierarchy is created to enable subclasses to inherit functionality from superclasses. Doesn’t that imply that subclasses have at least the same functionality as their superclasses and can therefore replace superclasses? Many superclasses are abstract, so you can only use subclasses when the compiler expects an object of the superclass. So why do we need to Liskov Substitution principle?

Building a class hierarchy of a superclass and subclasses that implement the superclasses methods does not mean the abstractions used in the inheritance hierarchy are logically sound. The Liskov substitution principle helps ensure that the abstraction hierarchies you build are logically sound.

Liskov Substitution Principle Violation Example

liskov substitution principle

Imagine we are building a marketplace website for used vehicles. People can sell and buy all sorts of vehicles, from bicycles to race cars.
To avoid code duplication, we create a superclass called vehicle. All vehicles share some functionality on our site. For example, you can access their age and number of previous owners. Accordingly, we can add these methods to the superclass.
Here is a simple implementation of the vehicle class in pseudocode:

class Vehicle {
    getAge(){ return age; }
    getNoPrevOwners(){ return noOwners; }
}

So far, so good. The individual subclasses do not have to have their own implementations of the getAge and getNoPrevOwners methods, which significantly reduces code overhead.
When we want to launch the first version of the platform, we realize that all the individual subclasses for the vehicles we currently have in our system need some method to access the fuel requirements. To reduce code, we decide to add a method for accessing fuel requirements per 100 miles to the vehicle superclass.

class Vehicle {
    getAge(){ return age; }
    getNoPrevOwners(){ return noOwners; }
    get100MileFuelRequirements(){ return gallonsRequired; }
}

In our next release cycle, we also add the functionality to purchase bicycles. Bicycles, of course, do not require fuel. We may naively think that that’s not a problem because the bicycle class simply doesn’t need to implement the method for adding fuel.
But that would be a violation of the Liskov substitution principle. Since the bicycle class does not implement the method for checking fuel requirements, it cannot replace an object of its superclass.

Why is this a problem?
If we make a class a subclass of another class, clients using the code can expect the subclass objects to behave like the superclass objects and to conform to the contract established by the superclass. In our specific case, a client using a bicycle object may try to call the get100MileFuelRequirements method. He would even be able to do so since the method exists in the superclass.

The Liskov Substitution Principle in Java

Let’s use the same example covered above and implement it in Java to see how the Liskov substitution principle works in practice. We start by creating a class for vehicles constituting our superclass.

public abstract class Vehicle {

    public void getAge(){
        System.out.println("Age: ");
    }

    public void getNoPreviousOwners(){
        System.out.println("Previous Owners:");
    }

    public void get100MileFuelRequirements()
    {
        System.out.println("Required Fuel:");
    }

}

Naturally, nobody will purchase a generic vehicle, so we make the class abstract. At runtime, we expect only subclass implementations because everyone on the site will want to purchase a specific car or bike. However, we do want the vehicle class to contain non-abstract methods so that we can implement functionality that is shared across subclasses saving us some work later on.

Adding the Car Subclass

The most common type of vehicle is a car, so we implement the car class first.

The methods in the vehicle class already contain some functionality to print a generic string that is not bound to the concrete implementation. To make use of that functionality in the car class, we override the methods in its superclass class, call the implementation in the superclass, and extend them by some code specific to that car.

public class Car extends Vehicle {

    @Override
    public void getAge() {
        super.getAge();
        //go into the database and retrieve the age of the concrete car
        System.out.println("2 years");
    }

    @Override
    public void getNoPreviousOwners() {
        super.getNoPreviousOwners();
        //go into the database and retrieve the number of previous owners
        System.out.println("1 prev. owner");
    }

    @Override
    public void get100MileFuelRequirements() {
        super.get100MileFuelRequirements();
        //go into the database and retrieve the 100 mile fuel requirement
        System.out.println("4 gallons");
    }
}

When we call these methods, they should print a generic string from the superclass, followed by the specific information for the object at hand. Since I’ve written this code purely for demonstration, I’ve hardcoded the specific information in the subclass. In a real system, you would probably retrieve that information from a database.

Let’s compile and run the code to check our assumptions. To do that, we create an additional class we call control and put the main method in it that we can execute. We create an object of the car class and call the methods in the class.

public class Control {

    public static void main(String args[]){

        Vehicle car = new Car();
        car.getAge();
        car.getNoPreviousOwners();
        car.get100MileFuelRequirements();

    }
}

For each method, we should get the string printed by its implementation in the vehicle class, followed by the string printed in the overriding subclass.

calling the superclass methods

So far so good. The car class makes use of all the methods implemented in the vehicle class and the implementations work as expected.

Adding the Bicycle Subclass

As a next step, we add the bicycle subclass. A bicycle does not require fuel, so we simply skip the implementation of the get100MileFuelRequirements() method naively thinking that that is not a problem.

public class Bicycle extends Vehicle {

    @Override
    public void getAge() {
        super.getAge();
    }
    @Override
    public void getNoPreviousOwners() {
        super.getNoPreviousOwners();
    }

}

But the method for retrieving fuel requirements has an implementation in the vehicle class. Since the bicycle class extends the vehicle class, clients using the vehicle class can call that implementation on the bicycle class.

public static void main(String args[]){
    Vehicle bike = new Bicycle();
    bike.get100MileFuelRequirements();
}
violating the liskov substitution principle

The code compiles and runs without any problems, but leaves us with a meaningless fragment of text. In this case, the customer will just receive a confusing message, but the results could be worse.

The problem here is that we have chosen the wrong abstraction when implementing the vehicle class.
To conform to the Liskov substitution principle, we have to make sure that all subclasses actually require implementations of the methods we put into the vehicle class. That’s not the case here. There are vehicles such as bicycles that do not require fuel and therefore the get100MileFuelRequirements() method should not be in the vehicle class.

Instead, we probably require an intermediate abstraction such as “Fuellable Vehicles” that specifically only covers vehicles requiring fuel.

Conforming to the Liskov Substitution Principle

We remove the get100MileFuelRequirements() method from the vehicle class and move it into a new class FuelableVehicle.

public abstract class Vehicle {
    public void getAge(){
        System.out.println("Age: ");
    }
    public void getNoPreviousOwners(){
        System.out.println("Previous Owners:");
    }
}
public abstract class FuelableVehicle extends Vehicle {
    public void get100MileFuelRequirements()
    {
        System.out.println("Required Fuel:");
    }
}

The fuel-able vehicle extends the vehicle class so we can still make use of the methods that are common to all vehicles. Since the fuel-able vehicle still represents an abstraction and not a concrete vehicle that can be purchased, we make the class abstract.

Next, we have the car class conform to the fuel-able vehicle superclass:

public class Car extends FuelableVehicle {
...
}

The bicycle remains a direct subclass of the vehicle class unless we are able to identify a subgroup of vehicles with specific functionality that the bicycle class belongs. Now our abstraction hierarchy is logically sound because the vehicle class only contains functionality that all the concrete vehicles implement. The same is true for fuel-able vehicles.


Sharing is caring

Leave a Reply

*Your email address will not be published. Required fields are marked

*