Question 11 · Section 18

When Is It Better to Use Composition Instead of Inheritance

VTable — table of method pointers. Monomorphic = JIT knows exact type → inlining (~1 ns). Megamorphic = 3+ types → table lookup (~10-20 ns).

Language versions: English Russian Ukrainian

🟢 Junior Level

Composition is preferred over inheritance in most cases. This is one of the key rules from Joshua Bloch’s “Effective Java”. Inheritance creates a rigid connection between classes, while composition is flexible.

Simple analogy: Inheritance — like a tattoo (forever), composition — like clothes (can change anytime).

Inheritance problem:

// Bad: inheritance for code reuse
public class MyHashSet extends HashSet<String> {
    private int addCount = 0;

    @Override
    public boolean add(String s) {
        addCount++;
        return super.add(s);
    }

    @Override
    public boolean addAll(Collection<? extends String> c) {
        addCount += c.size();  // ERROR: addAll calls add!
        return super.addAll(c); // Will be double count
    }
}

Composition solution:

// Good: composition + delegation
public class CountingHashSet {
    private final Set<String> set = new HashSet<>();
    private int addCount = 0;

    public boolean add(String s) {
        addCount++;
        return set.add(s);
    }

    public int getAddCount() { return addCount; }
}

When to use composition:

  • When you need to reuse code, but there’s no IS-A relationship
  • When you need to change behavior at runtime
  • When the parent class is not designed for inheritance

🟡 Middle Level

How It Works

Inheritance = White-box reuse: the subclass sees the parent’s internals (protected fields and methods). This creates strong coupling.

Composition = Black-box reuse: interaction only through the public interface. The delegate can be replaced without changing the delegator’s code.

VTable — table of method pointers. Monomorphic = JIT knows exact type → inlining (~1 ns). Megamorphic = 3+ types → table lookup (~10-20 ns).

Practical Application

When composition wins:

1. Avoiding Fragility (Fragile Base Class)

// Stack inherits Vector — encapsulation violation
public class MyStack extends Vector<String> {
    public void push(String item) { addElement(item); }
    public String pop() { return removeElementAt(size() - 1); }
}
// Problem: client can call insertElementAt(0) → LIFO broken!
// Stack assumes LIFO (last in — first out).
// But Vector allows insertElementAt(0, item) — insertion at the beginning.
// Any Vector client can bypass the stack's LIFO invariant.

2. Dynamic Polymorphism

// Runtime strategy change
public class PaymentService {
    private PaymentProcessor processor;

    public void setProcessor(PaymentProcessor processor) {
        this.processor = processor;  // Runtime change
    }
}

3. API Purity Composition allows exposing only the needed methods externally, hiding internal details.

Common Mistakes

Mistake Solution
Inheriting from ArrayList to add functions Composition with List field
Stack extends Vector — broken invariant Stack with internal List
Inheritance to access protected fields Passing data through constructor

When Inheritance is ACCEPTABLE

Case Example Why OK
IS-A Relationship Dog extends Animal Real type hierarchy
Framework extension AbstractHttpMessageConverter Designed for extension
Logical grouping BaseEntity with id, createdAt Common fields for all entities

// BaseEntity is a compromise. It’s not pure SRP, but pragmatism: // all DB entities need an id. The main thing is that BaseEntity // contains no business logic, only infrastructure fields.

When NOT to Use Composition

  • Frameworks with Template Method (designed for inheritance)
  • When access to parent’s protected methods is needed
  • When polymorphism through base type is critical

🔴 Senior Level

Internal Implementation at JVM Level

VTable Overhead:

Inheritance:
  Object → A → B → C → D → E (5 levels)
  e.foo() → invokevirtual → VTable lookup → 5 pointer chases

Composition with final:
  class Service { private final Delegate d; }
  s.d.run() → monomorphic call → JIT inlines → 1 direct call

Memory Layout:

Inheritance: Object Header (12) + A fields + B fields + C fields = one large object
Composition: Service (16 header + ref) + Delegate (16 header + fields) = two objects + reference

Architectural Trade-offs

Inheritance:

  • ✅ Pros: Polymorphism, code reuse, Template Method, framework extension
  • ❌ Cons: Fragile base class, API pollution, static binding, coupling

Composition:

  • ✅ Pros: Runtime flexibility, clean API, testability, loose coupling
  • ❌ Cons: Boilerplate (delegation), more objects, indirection overhead

Edge Cases

  1. Diamond Problem: Java prohibits multiple class inheritance
    • Solution: Composition + multiple interfaces (default methods)
  2. Sealed Classes (Java 17+): Limiting the circle of subclasses
    public sealed class Expr permits Constant, Plus, Minus { }
    // Predictable hierarchy, pattern matching
    
  3. Framework Constraints: Spring/Hibernate sometimes require inheritance
    • Solution: Isolate in one layer, use composition for business logic

Performance

Metric Inheritance Composition
VTable depth 5+ levels → megamorphic N/A (direct calls)
Inlining Harder with deep hierarchy Easier with final
Memory One large object N objects + refs
Startup Faster (fewer objects) Slightly slower
  • VTable lookup: ~1-3 ns for monomorphic, ~10-20 ns for megamorphic
  • Final delegate: JIT inlines → ~0.3 ns (direct call)

Thread Safety

  • Inheritance: One monitor for the entire object → contention
  • Composition: Different locks for different delegates → finer-grained concurrency
class ConcurrentService {
    private final ReadDelegate read = new ReadDelegate();
    private final WriteDelegate write = new WriteDelegate();
    // Different locks → parallel execution
}

Production Experience

Refactoring in e-commerce: OrderProcessor extends BaseService with 3000 lines. Problem: changing validation logic broke saving. Refactoring: extracted OrderValidator, OrderRepository, NotificationService via composition. Result: regression bugs reduced by 65%.

Monitoring

ArchUnit:

@ArchTest
static void no_deep_inheritance = classes()
    .should().haveLessThanNAncestors(3)
    .because("Deep inheritance is fragile");

SonarQube: Depth of Inheritance Tree > 5 → Code Smell

Best Practices for Highload

  • final delegates for inlining
  • Sealed classes for controlled inheritance
  • Interface + Composition instead of deep hierarchies
  • Pure DI for performance-critical sections
  • Choose composition by default, but not fanatically. Inheritance — when there’s a true IS-A, framework requires it, or common infrastructure fields.

🎯 Interview Cheat Sheet

Must know:

  • Composition is preferred: runtime flexibility, clean API, testability, loose coupling
  • Stack extends Vector — invariant violation: Vector allows insertElementAt(0) — LIFO broken
  • MyHashSet extends HashSet — double count: addAll calls add internally, counter doubles
  • VTable: 5+ levels → megamorphic (~10-20 ns), final delegate → JIT inlines (~0.3 ns)
  • Diamond Problem is solved by composition + multiple interfaces (default methods)
  • Sealed Classes (Java 17+) — limiting the circle of subclasses

Common follow-up questions:

  • Why is Stack extends Vector bad? — Vector client can bypass the stack’s LIFO invariant via insertElementAt
  • When is inheritance ACCEPTABLE? — IS-A Relationship, Framework extension, Logical grouping (BaseEntity)
  • What is false sharing in the context of God Object? — Fields of different responsibilities in the same cache line → CPU cores invalidate each other’s cache
  • Thread Safety: composition vs inheritance? — Composition: different locks for different delegates → finer-grained concurrency

Red flags (DO NOT say):

  • “Inheriting from ArrayList is a normal way to add functions” (encapsulation violation)
  • “Composition always gives better performance” (in hot path with switch on enum — inheritance may be faster)
  • “BaseEntity is an SRP violation” (pragmatism: all entities need an id, main thing — no business logic)

Related topics:

  • [[10. What is composition and inheritance]]
  • [[12. What is delegation in OOP]]
  • [[1. What is Single Responsibility principle and how to apply it]]
  • [[18. How to refactor God Object]]