Liskov Substitution Principle in Java – Common Misuses and Clean Fixes

Illustration for Liskov Substitution Principle in Java – Common Misuses and Clean Fixes
By Last updated:

Introduction

In object-oriented programming, inheritance is a powerful tool—but it can easily backfire if not used carefully. That’s where the Liskov Substitution Principle (LSP) comes in.

LSP is the L in the SOLID principles, and it ensures that subclasses remain substitutable for their superclasses without breaking the application’s behavior.

Ignoring this principle can lead to brittle code, runtime exceptions, and broken polymorphism.


What is the Liskov Substitution Principle?

Definition

“If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering the correctness of the program.” — Barbara Liskov

In simpler terms: You should be able to use a subclass anywhere its parent class is expected—without surprises.


LSP in Java – Core Syntax

class Bird {
    void fly() {
        System.out.println("Flying...");
    }
}

class Sparrow extends Bird {}  // ✅ Follows LSP
class Ostrich extends Bird {}  // ❌ Violates LSP if fly() is overridden or disabled

UML-style Diagram

          Bird
           ↑
      -------------
     |             |
  Sparrow       Ostrich

// Code expects Bird but gets an Ostrich

Real-World Violation Example

class Bird {
    void fly() {
        System.out.println("Bird flying...");
    }
}

class Ostrich extends Bird {
    @Override
    void fly() {
        throw new UnsupportedOperationException("Ostrich can't fly!");
    }
}

Problem

  • Code using Bird expects fly() to be safe.
  • Ostrich breaks this expectation.

Clean Fix – Refactor with Interfaces

interface Bird {}

interface FlyingBird extends Bird {
    void fly();
}

class Sparrow implements FlyingBird {
    public void fly() {
        System.out.println("Sparrow flying...");
    }
}

class Ostrich implements Bird {
    // No fly() method
}

✅ Now only birds that can fly implement FlyingBird.


Another Example – Rectangle vs Square

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

class Square extends Rectangle {
    void setWidth(int w) {
        width = height = w;
    }

    void setHeight(int h) {
        width = height = h;
    }
}

Violation

  • Square is not substitutable for Rectangle.
  • Breaks expectation: rect.setWidth(4); rect.setHeight(5); should give area 20.

✅ Fix

Use composition:

class Square {
    private int side;
    void setSide(int s) { this.side = s; }
    int area() { return side * side; }
}

Pros of LSP

  • Ensures robust polymorphism
  • Prevents runtime surprises
  • Leads to cleaner and predictable code

Cons of Ignoring LSP

  • Fragile hierarchies
  • Unexpected runtime failures
  • Increased complexity in testing and debugging

Java 17+ Considerations

Sealed Classes for Safe Hierarchies

sealed class Bird permits Sparrow, Ostrich {}

final class Sparrow extends Bird {}
final class Ostrich extends Bird {}  // Define behavior safely

You can now control allowed subtypes and model sealed-safe hierarchies that help enforce LSP.


Real-World Analogy

Think of LSP like USB devices.

If a port supports USB-C, any compliant device should work seamlessly—whether it’s a phone, keyboard, or charger. If one throws an error or behaves oddly, it's violating the principle.


Best Practices

  • Prefer interface-based segregation when subclass behavior diverges
  • Avoid overriding methods to throw exceptions
  • Use composition over inheritance when modeling doesn’t fit naturally
  • Apply sealed classes in Java 17+ for closed hierarchies
  • Write unit tests to validate subclass substitutability

Conclusion

The Liskov Substitution Principle is not just a theoretical guideline—it’s essential for writing resilient, reusable, and maintainable Java code.

Following LSP prevents the misuse of inheritance and ensures that your classes can be safely extended without breaking core expectations.


Key Takeaways

  • Subclasses must honor the behavior of their base classes
  • Avoid throwing exceptions in overridden methods
  • Use interfaces or composition when LSP is hard to maintain
  • Test substitutability to catch violations early
  • Sealed classes help constrain subclassing where needed

FAQs

1. Can I override a method and throw an exception?
You can, but if it breaks expected behavior, it violates LSP.

2. Does Java enforce LSP?
No. It's a design principle, not a compiler rule.

3. Can interfaces violate LSP too?
Yes—if a class implements an interface and throws exceptions for valid operations.

4. Is composition always better than inheritance?
Not always—but it helps when inheritance can't honor LSP.

5. How can unit tests help enforce LSP?
By testing subclass behavior where superclass is expected.

6. Are abstract classes safer than concrete classes for LSP?
Not inherently—violations depend on behavior, not abstraction.

7. Is LSP only about exceptions?
No. Any behavior that deviates from the parent’s contract can violate it.

8. What’s the difference between OCP and LSP?
OCP is about extension without modification; LSP is about behavioral substitutability.

9. Does LSP apply to constructors?
No. It’s about runtime behavior of instances, not initialization logic.

10. Can sealed classes guarantee LSP?
Not completely, but they help limit improper extension paths.