skip to content

Java enforces single inheritance of implementation but multiple inheritance of type. What problems does this design solve and create, and how do modern Java features and principles like 'composition over inheritance' address its limits?

level: seniorimportance: should knowfreq 55%

answer

  1. Single impl inheritance avoids the diamond (state + method ambiguity)
  2. Interfaces = no state historically -> multiple type inheritance is safe
  3. Default methods reintroduce a method-only diamond; resolved by: class wins, most-specific interface, else override
  4. Costs: fragile base class, rigid deep hierarchies, compile-time coupling
  5. Composition over inheritance: hold + delegate (HAS-A), runtime-swappable

basics

~20 s

Allowing a class to extend only one class avoids ambiguity when two parents have conflicting code (the diamond problem), while letting a class implement many interfaces keeps flexibility. The downside is rigid hierarchies, which Java teams usually solve by favoring composition (holding objects and delegating) over deep inheritance.

solid answer

~50 s

Java's rule — extend at most one class, implement any number of interfaces — is a deliberate trade-off. Single inheritance of implementation avoids the classic diamond problem, where inheriting two conflicting method bodies or two copies of state from a shared ancestor is ambiguous. Multiple inheritance of type via interfaces keeps polymorphic flexibility without that ambiguity, because interfaces historically carried no state or implementation. The cost is rigidity: deep inheritance chains are brittle, couple subclasses to superclass internals (the fragile base class problem), and you can't mix in implementation from two sources. Java 8 default methods partly relaxed this by letting interfaces carry behavior, reintroducing a limited diamond that Java resolves with explicit rules (class wins over interface; a subtype must override an unrelated conflict). The broader industry answer is 'composition over inheritance': instead of subclassing, hold collaborators as fields and delegate. This yields looser coupling, runtime flexibility, and avoids inheritance's tight, compile-time binding.

code

java · 18 lines
java
// Two interfaces with conflicting defaults -> the diamond, method half
interface Walker { default String move() { return "walk"; } }
interface Swimmer { default String move() { return "swim"; } }

class Duck implements Walker, Swimmer {
    // Compiler FORCES an override to resolve the conflict:
    @Override public String move() {
        return Walker.super.move() + " and " + Swimmer.super.move();
    }
}

// Composition over inheritance: reuse by delegation, swappable at runtime
class Logger {
    private final Formatter formatter;             // HAS-A, not IS-A
    Logger(Formatter formatter) { this.formatter = formatter; }
    void log(String msg) { write(formatter.format(msg)); } // delegate
    private void write(String s) { /* ... */ }
}

go deeper

for a junior

Knows a class extends one class but implements many interfaces, and that composition means holding an object instead of subclassing.

for a middle

Can explain the diamond problem conceptually and that interfaces avoid it by carrying no state; knows composition reduces coupling.

for a senior

Articulates the default-method conflict-resolution rules, the fragile base class problem, and applies composition-over-inheritance with delegation deliberately.

for a principal

Reasons about API/binary compatibility of evolving interfaces vs classes, when to expose abstract skeletons vs interfaces, and shapes hierarchy-vs-composition policy across a large system with LSP as the discriminator.

## The rule and the reasoning Java deliberately permits **single inheritance of implementation** (`extends` one class) but **multiple inheritance of type** (`implements` many interfaces). To understand why, you must understand the problem the language designers were avoiding. ## The diamond problem Languages with **multiple implementation inheritance** (e.g. C++) let a class inherit from two parents. If both parents derive from a common grandparent `A`, and a class `D` inherits from both `B` and `C` (which both extend `A`), you get a *diamond*: ``` A / \ B C \ / D ``` Two ambiguities arise: 1. **State duplication** — does `D` get one copy of `A`'s fields or two? (C++ needs `virtual` inheritance to disambiguate.) 2. **Method ambiguity** — if `B` and `C` both override `A.foo()` differently, which body does `D` inherit? These ambiguities make multiple implementation inheritance error-prone. Java's answer: a class has exactly **one** implementation parent, so there's never a question about *whose* fields or *whose* code you inherited. ## Why interfaces escape the problem (originally) Before Java 8, an **interface** had **no method bodies and no instance state** — only abstract signatures and constants. So implementing many interfaces couldn't create state duplication or conflicting implementations: there was nothing to conflict. You inherited *types* (contracts), not *implementations*. That's why multiple inheritance *of type* is safe. ## What the trade-off costs Forbidding implementation mixing has downsides: - **Code duplication** — two classes wanting the same helper logic from two different sources can't both inherit it; pre-Java-8 you'd copy code or use a helper class. - **Rigid, deep hierarchies** — modeling everything as an IS-A chain produces tall trees that are hard to refactor. - **The fragile base class problem** — subclasses depend on the superclass's internal call structure; a seemingly safe change in the base can silently break subclasses (e.g. the base starts calling an overridable method internally). - **Tight, compile-time coupling** — `extends` is fixed at compile time; you can't swap a parent at runtime. ## Java 8 default methods: a controlled re-opening **Default methods** let interfaces carry behavior, which *reintroduces* a limited diamond: a class could inherit two conflicting `default` methods from two interfaces. Java resolves this with explicit, deterministic rules: 1. **Class wins** — a method (concrete or abstract) inherited from a *superclass* beats any interface default. 2. **Most specific interface wins** — a default in a sub-interface overrides one in a super-interface. 3. **Otherwise, the subtype must disambiguate** — if two unrelated interfaces both provide a default, the class *must* override the method, and can call a specific one via `InterfaceName.super.method()`. Crucially, interfaces still have **no instance state**, so the *state-duplication* half of the diamond never returns — only the (resolvable) method half. ## The industry answer: composition over inheritance The guiding principle — *favor composition over inheritance* — sidesteps the whole issue. Instead of subclassing to reuse behavior, **hold a collaborator as a field and delegate** to it (the **delegation pattern**): - **Looser coupling** — you depend on the collaborator's interface, not its internals. - **Runtime flexibility** — you can inject or swap the collaborator (Strategy pattern); inheritance is fixed at compile time. - **Avoids the fragile base class problem** — you're not exposed to a superclass's internal evolution. - **No single-parent limit** — you can compose any number of collaborators, recovering 'multiple inheritance of behavior' through delegation. The trade-off is some boilerplate (forwarding methods) and that composition expresses HAS-A, not IS-A — so use inheritance only for genuine subtype/IS-A relationships where the Liskov Substitution Principle holds, and compose otherwise. ## Putting it together Java's model is a pragmatic middle ground: enough inheritance for genuine polymorphic hierarchies, no dangerous implementation-diamond, multiple type inheritance for flexible contracts, and a strong cultural push toward composition for code reuse. Modern features (default methods, records, sealed types) further reduce the need for deep inheritance.

  • If two implemented interfaces provide conflicting default methods, what does the compiler require?
    It requires the implementing class to override the method to resolve the ambiguity. Inside the override it can explicitly invoke a chosen one via InterfaceName.super.method(). Without the override, the code does not compile.
  • What is the fragile base class problem, and how does composition mitigate it?
    A subclass implicitly depends on the superclass's internal implementation (e.g. which methods the base calls internally). A benign-looking base change can break subclasses. Composition mitigates it because the consumer depends only on the collaborator's published interface, not its internals, so internal changes are insulated behind that contract.

Inheritance is like inheriting your parents' exact house — you're stuck with their layout and any hidden wiring faults. Composition is renting furniture from many suppliers and arranging it yourself: more setup, but you can swap any piece anytime and aren't trapped by one builder's choices.

saying these in an interview costs you the question

  • Saying Java has no diamond problem at all — default methods reintroduce a method-level one (resolved by rules); only state duplication is fully avoided.
  • Claiming composition is always better than inheritance — inheritance is correct for genuine IS-A/LSP-honoring relationships.
  • Believing 'class wins' means an interface default can override a superclass method — it's the reverse: the superclass method wins.
  • Treating multiple interface implementation as 'multiple inheritance of implementation' — it's inheritance of type (plus optional defaults).

context