is-a vs behaves-like-a — LSP in inheritance design
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
An is-a relationship describes classification, while behaves-like-a describes the contract needed for safe substitution. Java inheritance should satisfy both only when code using the base type can receive the subtype without losing promised behavior.
The usual rectangle and square example depends on a mutable rectangle contract. If callers may set width and height independently, a square subtype cannot preserve that behavior because changing one side must change the other. Both shapes can instead implement an immutable Shape contract with area(). In the same way, a penguin can be a Bird without implementing a separate Flyable capability.
The design test is observable behavior: valid base inputs remain valid, required results and invariants remain true, and promised operations remain available. Domain vocabulary starts the model, but the client contract decides whether inheritance is safe.
is-a vs behaves-like-a — LSP in inheritance design
Direct Answer
An is-a statement describes a category, but inheritance also creates a behavioral contract. A subtype is safe only when it behaves as clients of the base type expect: it accepts valid inputs, preserves required results and invariants, and does not remove promised capabilities. A real-world category alone is not enough to justify extends.
Code Example
// IS-A by name, but does NOT behave-like-a -> LSP violation
class Bird {
void fly() { /* moves through the air */ }
}
class Penguin extends Bird { // Penguin IS-A Bird (structurally)
@Override void fly() {
throw new UnsupportedOperationException("penguins can't fly"); // breaks callers
}
}
void migrate(Bird b) { b.fly(); } // expects every Bird to fly
// migrate(new Penguin()) crashes at runtime: behaves-like-a failed
// Fix: model on behavior, not just the noun
interface Flyer { void fly(); }
class Sparrow implements Flyer { public void fly() { /* ok */ } }
// Penguin simply does not implement Flyer -> the compiler prevents misuse
IS-A is structural; behaves-like-a is the real substitution test that LSP enforces.
Common Mistakes & Pitfalls
- Modeling the real world directly: Freshers learn domain concepts like "a car is a vehicle" and immediately create class Car extends Vehicle, without checking if the code actually needs that relationship or if behavioral contracts match.
- Inheritance for code reuse: Using inheritance just to reuse methods — "User has a login method, so AdminUser should extend User to get it" — even when admin login behavior is different, violating LSP.
- Biology and math as OOP rules: Thinking "a penguin is a bird, and birds fly, so penguin should inherit fly method" — then overriding fly to do nothing or throw an exception, which is an LSP violation.
- Ignoring invariants: Employee is-a Person, so extending Person seems right, but if Person has an invariant that SSN is optional and Employee requires it, the invariant is broken.
Check your understanding
Answer these without looking back at the article.
- When should you prefer composition over inheritance?
- How do you test that a subclass satisfies LSP?
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.