Objects of a superclass should be replaceable with objects of its subclasses without affecting the correctness of the program..
In simpler terms, if class B is a subclass of class A, then objects of class A should be able to be replaced by objects of class B
without causing any errors or changing the desirable behavior of the program.
The key idea is that subtypes must adhere to the contract of their base type. If a subclass deviates from the behavior of the base class in a way that violates the base class’s expectations, then LSP is violated. This leads to problems like unexpected behavior or errors when you try to substitute the subclass where the base class is expected.
Let’s assume the Car class has a method refuel(), which fills the tank with gasoline. However, for an electric car, this concept doesn’t make sense because it doesn’t have a fuel tank, but instead, it has a battery that needs charging.
If we are not careful, this difference in behavior could lead to a violation of LSP because the ElectricCar class cannot correctly implement the behavior of refuel() that makes sense for the Car class.
// Base Class: Car
class Car {
public void refuel() {
System.out.println("Filling the fuel tank...");
}
}
// Subclass: ElectricCar
class ElectricCar extends Car {
@Override
public void refuel() {
// Electric cars don't use fuel, so we throw an exception
throw new UnsupportedOperationException("Electric cars don't have fuel tanks!");
}
}
// Method to test LSP violation
public class Main {
public static void refuelCar(Car car) {
car.refuel(); // Expect this method to work for any 'Car'
}
public static void main(String[] args) {
Car myCar = new Car();
refuelCar(myCar); // Works fine
Car myElectricCar = new ElectricCar();
refuelCar(myElectricCar); // Throws an exception, violating LSP
}
}
In the above code, the method refuel() is part of the Car class, which implies that all Car subclasses should support this behavior. However, the ElectricCar class throws an exception when refuel() is called, which violates the Liskov Substitution Principle. This happens because the ElectricCar class doesn’t behave as expected when substituted for a Car.
The client code (refuelCar() method) expects all Car objects, including ElectricCar, to be refueled, but ElectricCar breaks this assumption.
To achieve LSP compliance, we need to rethink the design so that each subclass correctly represents the behavior of its superclass. One way to solve this is by splitting the Car class into two separate abstractions: one for fuel-powered cars and one for electric-powered cars. We can introduce a common base class (or interface) like Vehicle that contains shared behavior, and then separate the logic for refueling and recharging into different subclasses.
// Base Class: Vehicle
abstract class Vehicle {
public abstract void start();
}
// Fuel-Powered Car
class FuelCar extends Vehicle {
@Override
public void start() {
System.out.println("Starting fuel car...");
}
public void refuel() {
System.out.println("Filling the fuel tank...");
}
}
// Electric Car
class ElectricCar extends Vehicle {
@Override
public void start() {
System.out.println("Starting electric car...");
}
public void recharge() {
System.out.println("Recharging the battery...");
}
}
// Method to demonstrate LSP
public class Main {
public static void main(String[] args) {
Vehicle myFuelCar = new FuelCar();
myFuelCar.start(); // Works fine
FuelCar fuelCar = new FuelCar();
fuelCar.refuel(); // Works fine for FuelCar
Vehicle myElectricCar = new ElectricCar();
myElectricCar.start(); // Works fine
ElectricCar electricCar = new ElectricCar();
electricCar.recharge(); // Works fine for ElectricCar
}
}