When should you prefer inheritance (extends) versus composition, and what risks does deep inheritance introduce?
answer
- is-a → inheritance; has-a → composition
- fragile base class: HashSet add/addAll double-count
- favor composition over inheritance (EJ #18)
- subtypes must obey Liskov (substitutable)
- sealed/final/interfaces are safer modern tools
basics
~20 sUse extends only for a true is-a relationship where the subclass is a proper substitute for the parent. Otherwise prefer composition — hold an object as a field and delegate to it — to avoid tight coupling and fragile base classes.
solid answer
~50 sInheritance creates the tightest coupling in OOP: a subclass depends on the parent's implementation details, not just its public contract, so changing the base class can silently break subclasses — the fragile base class problem. The guidance 'favor composition over inheritance' (Effective Java) says: unless there is a genuine is-a relationship and the base was designed and documented for extension, prefer composition — hold the other type as a field and delegate. Composition is more flexible (you can swap the delegate, even at runtime), avoids exposing the parent's full API, and respects encapsulation. Inheritance also must honor the Liskov Substitution Principle: a subclass must be usable anywhere the parent is, without surprising behavior. Java offers safer tools today: interfaces with default methods for shared behavior, sealed classes to constrain hierarchies, and final to forbid extension. Deep hierarchies hurt readability and make behavior hard to trace across super chains.
code
java · 15 lines// FRAGILE: relies on HashSet's internal self-use of add() inside addAll()
class CountingSet<E> extends HashSet<E> {
int count = 0;
@Override public boolean add(E e) { count++; return super.add(e); }
@Override public boolean addAll(Collection<? extends E> c) {
count += c.size(); return super.addAll(c); // addAll calls add -> double counts!
}
}
// ROBUST: composition + delegation, depends only on the public Set API
class CountingSet2<E> {
private final Set<E> delegate = new HashSet<>();
int count = 0;
boolean add(E e) { count++; return delegate.add(e); }
}go deeper
Recognizes the is-a vs has-a distinction and that not every reuse should be inheritance.
States 'favor composition over inheritance' and can describe coupling differences between the two.
Explains the fragile base class problem with a concrete example, applies LSP, and chooses composition/interfaces/sealed appropriately.
Sets hierarchy and extensibility policy across a codebase — when to seal vs open APIs, how to design documented extension points, and migration strategies away from fragile inheritance.
## Two ways to reuse code There are two fundamental ways for class B to use class A's functionality: - **Inheritance** (`class B extends A`): B *is an* A and automatically gets A's members. This is an **is-a** relationship. - **Composition**: B *has an* A — it holds an instance of A as a field and calls (delegates to) it. This is a **has-a** relationship. ```java // Inheritance class Stack extends ArrayList<E> { ... } // Stack IS-A list?? questionable // Composition class Stack { private final List<E> items = new ArrayList<>(); ... } // Stack HAS-A list ``` ## The fragile base class problem Inheritance is the **tightest coupling** available, because a subclass can depend on *how* the parent is implemented, not merely on its public contract. Classic example: you extend `HashSet` and override both `add` and `addAll` to count insertions. But `HashSet.addAll` internally calls `add`, so each added element is counted twice. Your subclass broke because of an *internal* implementation detail of the base — a detail that could also change in a future library version. This is the **fragile base class problem**: innocuous changes to a superclass silently break subclasses. Composition is immune because you only call the public API of the wrapped object. ## The Liskov Substitution Principle (LSP) LSP states: an object of a subclass must be substitutable for an object of its superclass **without altering the correctness** of the program. If `Rectangle` has independent width/height and you make `Square extends Rectangle`, setting width must not surprisingly change height — but a square requires it, violating callers' expectations. When you can't satisfy LSP, you don't actually have an is-a relationship; use composition instead. ## 'Favor composition over inheritance' This well-known guideline (popularized by *Effective Java*, Item 18) advises reaching for composition by default and using inheritance only when: 1. There is a genuine, durable **is-a** relationship, AND 2. The base class was **designed and documented for inheritance** (clear extension points, documented self-use of overridable methods), AND 3. Base and subclass are controlled together / in the same package (extending across an API boundary is the risky case). Benefits of composition: looser coupling, the ability to **swap the delegate** (even at runtime, enabling Strategy/Decorator patterns), no accidental inheritance of the parent's entire API, and preserved encapsulation. ## Costs of deep inheritance - **Traceability:** behavior is scattered across a `super` chain; reading one class no longer tells you what it does. - **Rigidity:** a class can extend only one parent (single inheritance), so an early inheritance choice can box you in. - **Surprising overrides:** an override deep in the hierarchy can change behavior of base-class methods that call it. ## Modern Java tools that help - **Interfaces with default methods** — share behavior without sharing implementation state; a class can implement many. - **`final` classes/methods** — forbid extension/override where it would be unsafe (e.g. `String`). - **`sealed` classes/interfaces** (Java 17+) — permit only a known, listed set of subclasses, giving controlled, exhaustive hierarchies (great with pattern matching). - **records** — immutable data carriers that can't be subclassed, avoiding inheritance entirely for value types. ## Decision heuristic Ask: 'Is B truly a kind of A, substitutable everywhere A is, and was A built to be extended?' If all yes → inheritance is fine. Any no → compose and delegate.
- Give a concrete example of the fragile base class problem.Extending HashSet and overriding add and addAll to count elements: HashSet.addAll calls add internally, so each element gets counted twice. The subclass breaks due to a base-class implementation detail.
- How do sealed classes improve on open inheritance?sealed restricts which classes may extend a type to an explicit permitted list, giving a closed, exhaustive hierarchy that the compiler can check (e.g. in switch pattern matching) and that can't be extended unexpectedly.
- What does the Liskov Substitution Principle require of a subclass?A subclass instance must be usable anywhere its superclass is expected without breaking correctness — same contracts, no strengthened preconditions or weakened postconditions.
Inheritance is welding two parts together — strong but you can't separate them and a change to one stresses the other. Composition is bolting parts together — you can swap a bolted-in component without rebuilding the whole machine.
saying these in an interview costs you the question
- Reaching for extends whenever you want to reuse code, regardless of is-a
- Extending a class across an API boundary that wasn't designed for it
- Ignoring LSP (e.g. Square extends Rectangle)
- Believing deep hierarchies are inherently good OO design
- Not knowing composition can replace most inheritance