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).
🟢 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
protectedmethods 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
- Diamond Problem: Java prohibits multiple class inheritance
- Solution: Composition + multiple interfaces (default methods)
- Sealed Classes (Java 17+): Limiting the circle of subclasses
public sealed class Expr permits Constant, Plus, Minus { } // Predictable hierarchy, pattern matching - 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 allowsinsertElementAt(0)— LIFO brokenMyHashSet extends HashSet— double count:addAllcallsaddinternally, 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 Vectorbad? — Vector client can bypass the stack’s LIFO invariant viainsertElementAt - 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
ArrayListis 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]]