Liskov Substitution Principle (LSP)
The Liskov Substitution Principle requires a subtype to be usable wherever its declared supertype is expected without violating the client’s assumptions. In Java, this means inheritance should preserve the behavioral contract of the parent type, not merely its method signatures.
LSP is the behavioral part of subtyping. If code accepts a `Rectangle`, replacing it with a subtype such as `Square` should not cause that code to behave incorrectly because the subtype changes rules that callers reasonably rely on.
A subtype can strengthen neither the conditions a caller must satisfy nor weaken the guarantees it provides. For example, if a base method accepts every non-null value promised by its contract, an overriding method should not reject some of those valid inputs. Likewise, an override should preserve promised results and important state or side-effect behavior.
LSP applies to more than return types and method signatures. Exceptions, invariants, mutability, ordering, and other observable behavior can all matter. An implementation may compile correctly while still violating substitutability if clients must add subtype-specific checks to remain correct.
A common Java warning sign is an override that throws `UnsupportedOperationException` for an operation the parent type promises to support. Another is a subtype that silently changes important semantics, forcing callers to know the concrete subtype rather than relying on the abstraction.
When inheritance cannot preserve the parent contract, composition or a narrower abstraction is often a better design. The goal is not to eliminate all behavioral differences between types, but to ensure those differences remain compatible with what the supertype promises.
Key points
- LSP concerns behavioral compatibility, not just inheritance syntax.
- Valid inputs accepted by a supertype should remain valid for its subtypes.
- Subtype implementations must preserve the guarantees and invariants clients rely on.
- Unexpected exceptions or unsupported inherited operations can signal an LSP violation.
- Prefer composition or a better abstraction when a subtype cannot honor the parent contract.
Interview questions
What is the Liskov Substitution Principle?
LSP says that an object of a subtype should be usable anywhere its supertype is expected without breaking the program's assumptions. It is primarily about behavior rather than whether the Java compiler accepts the inheritance relationship. The subtype should honor the promises made by the parent abstraction. If callers need special cases for a particular subtype, that can indicate a substitutability problem.
Can a Java program compile while still violating LSP?
Yes. Java's type system can verify that a subtype has compatible method signatures while it cannot generally verify the full behavioral contract. For example, a subclass can override a method and throw `UnsupportedOperationException` even though callers of the parent type expect the operation to work. The resulting code may compile but fail when a client relies on the parent abstraction's promised behavior.
How does the Rectangle/Square example demonstrate LSP?
A typical rectangle abstraction may allow width and height to change independently. A square must keep those dimensions equal, so an inherited setter that promises independent changes cannot retain the same meaning for a square. Code written against the rectangle contract can therefore observe behavior it was not designed to handle. The problem is the mismatch between the abstraction's contract and the subtype's invariants, not the mathematical relationship between squares and rectangles.
What are common signs that a hierarchy violates LSP?
A strong warning sign is an override that rejects inputs or operations that the parent contract accepts. Another is an override that changes important postconditions, invariants, exception behavior, or side effects in ways callers cannot reasonably anticipate. Repeated `instanceof` checks or subtype-specific branches in clients can also indicate that the abstraction is not genuinely substitutable. In such cases, I would examine whether the base type is too broad or whether composition is more appropriate.
How should LSP influence API design in a Java backend?
I would design interfaces around stable behavioral contracts that implementations can consistently honor. Before introducing inheritance, I would identify what callers are entitled to assume about inputs, outputs, exceptions, state transitions, and side effects. If different implementations fundamentally need different contracts, I would separate those abstractions rather than forcing unrelated behavior into one parent type. This makes polymorphic client code simpler because it can rely on the abstraction instead of concrete-type knowledge.