skip to content

When is inheritance the right choice over composition in Java, and what tests help you decide?

level: seniorimportance: should knowfreq 55%

answer

  1. Ask literally: is every B really an A?
  2. LSP — substitutable without surprises (Square/Rectangle)
  3. Design & document for inheritance, or forbid it (final)
  4. Inherit type (interface) freely; inherit concrete code warily
  5. Stack/Vector and Properties/Hashtable = IS-A gone wrong

basics

~20 s

Use inheritance only when the child truly IS-A parent and can be used anywhere the parent is expected, and the parent was designed to be extended. Otherwise, compose by holding the other object as a field.

solid answer

~50 s

Composition is the default, but inheritance is correct in specific cases. The primary test is a genuine IS-A relationship that satisfies the Liskov Substitution Principle: the subtype must work correctly anywhere the supertype is expected, with no surprising behavior. A practical screen from Effective Java is to ask 'is B really an A?' — if you'd never substitute the subtype for the supertype, you don't have IS-A. The second condition is that the superclass must be designed and documented for extension (its self-use of overridable methods is part of its contract), or be a class you own and control. Inheriting type from interfaces is always fine and encouraged. Also weigh: inheritance across package boundaries to a concrete class is dangerous; if the parent's API has methods that don't make sense for the child, that's a red flag. When in doubt, compose — you can always wrap, and you keep your single inheritance slot free.

go deeper

for a junior

Knows the rough rule: use extends only when the child is a kind of the parent; otherwise hold it as a field.

for a middle

Applies the IS-A test and recognizes leaky examples like Stack/Vector; defaults to composition when unsure.

for a senior

Reasons with LSP and the 'design for inheritance or prohibit it' rule, distinguishes type vs implementation inheritance, and justifies the choice per case.

for a principal

Sets codebase conventions (final by default, sealed hierarchies, documented extension points) and evaluates inheritance choices against long-term API evolution and substitutability guarantees.

## Framing "Favor composition over inheritance" is a *default*, not an absolute ban. The skill is knowing the cases where inheritance is the *better* tool, and applying explicit tests so the choice isn't accidental. ## Test 1 — Is there a genuine IS-A relationship? Inheritance (`class B extends A`) declares "every B **is an** A." Before using it, ask Joshua Bloch's question literally: *"Is every B really an A?"* If you can't honestly answer yes, you don't have inheritance — you have a HAS-A or USES-A relationship, which means **compose**. Classic violations: - `Stack extends Vector` — a stack is *not* a general-purpose growable array; inheriting leaks `get(int)`/`add(index, e)` and lets callers corrupt LIFO order. - `Properties extends Hashtable` — `Properties` is meant to hold String→String, but inheriting `Hashtable` lets callers insert any object via `put`, bypassing the intended invariant. Both are cases where IS-A was *assumed* but the supertype's API didn't actually fit; composition would have hidden the unwanted methods. ## Test 2 — Liskov Substitution Principle (LSP) LSP states: **a subtype must be usable anywhere its supertype is expected without breaking the program's expectations.** If substituting `B` for `A` could surprise a caller — different preconditions, weaker guarantees, thrown exceptions the supertype never throws — then `B extends A` is wrong even if it *feels* like IS-A. A famous example: making `Square extends Rectangle`. A `Rectangle` lets you set width and height independently; a `Square` cannot honor that without violating its own invariant, so it breaks any code that relies on `Rectangle`'s contract. Composition (a `Square` that *has* dimensions) sidesteps the conflict. ## Test 3 — Was the superclass designed for extension? Even for a true IS-A, inheriting a **concrete** class is safe only when that class **documents its self-use of overridable methods** — i.e. it tells you which methods it calls internally so your overrides won't cause surprises (the `HashSet.addAll`-calls-`add` problem). *Effective Java* Item 19 says: *design and document for inheritance, or else prohibit it* (mark the class `final` or hide its constructor). So: - Extending classes **you own** and can keep in sync: acceptable. - Extending a third-party concrete class purely for reuse, across a package boundary, where its self-use is undocumented: dangerous — prefer composition. ## Test 4 — Inheritance of *type* vs *implementation* There are two kinds of inheritance, and they have different risk: - **Inheritance of implementation** (`extends` a concrete class) — the risky kind the guideline warns about. - **Inheritance of type** (`implements` an interface, or `extends` an interface/abstract type) — *encouraged*. Implementing `Comparable`, `Runnable`, or a domain interface is always fine; it's a contract, not borrowed code. Using a well-documented abstract skeletal class like `AbstractList` is also fine because it was *built* to be extended. ## A quick decision checklist 1. Is it genuinely IS-A and Liskov-substitutable? If no → compose. 2. Does every method of the parent make sense on the child? If no → compose. 3. Is the parent designed/documented for extension, or owned by you? If no → compose. 4. Are you inheriting *type* (interface)? Then inherit freely. 5. Still unsure? Compose — it's reversible and keeps the single `extends` slot free. ## Terms - **IS-A / HAS-A:** subtype relationship vs ownership/usage relationship. - **Liskov Substitution Principle (LSP):** subtypes must be safely substitutable for their supertypes. - **Self-use:** a class calling its own overridable methods internally; must be documented for safe subclassing. - **Inheritance of type vs implementation:** implementing a contract (interface) vs borrowing concrete code.

  • Why is Square extends Rectangle a classic LSP violation?
    Rectangle's contract lets width and height vary independently. A Square must keep them equal, so a setWidth that leaves height unchanged breaks the Square invariant; code written against Rectangle's contract gets surprising results. Substituting a Square where a Rectangle is expected violates expectations, so the inheritance is unsound.
  • What does 'design and document for inheritance, or else prohibit it' mean in practice?
    Either document a concrete class's self-use of overridable methods (and provide hooks) so subclasses can override safely, or make the class final / hide its constructor so it can't be subclassed at all. Leaving a concrete class accidentally extensible invites fragile-base-class bugs.

saying these in an interview costs you the question

  • Treating inheritance as forbidden — interfaces and designed-for-extension abstract classes are the right tool.
  • Using inheritance whenever there is shared code, regardless of IS-A.
  • Ignoring LSP and assuming any 'feels like a kind of' relationship justifies extends.
  • Subclassing a third-party concrete class for reuse without checking it's documented for extension.

context