What is Composition and Inheritance
White-box reuse — parent internals are visible (inheritance). Black-box reuse — only public interface (composition). Devirtualization — JIT replaces virtual call with direct. Mo...
🟢 Junior Level
Composition and inheritance are the two main ways to organize connections between classes in OOP. Inheritance creates an “is-a” (IS-A) relationship: one class becomes a special case of another. Composition creates a “has-a” (HAS-A) relationship: one object contains another object as a part.
Simple analogy: Inheritance — like inheriting traits from parents (you are a human). Composition — like buying a phone (you have a phone, can replace it).
Inheritance example (IS-A):
public class Animal {
public void eat() { System.out.println("Eating..."); }
}
public class Dog extends Animal { // Dog IS-A Animal
public void bark() { System.out.println("Woof!"); }
}
Composition example (HAS-A):
public class Engine {
public void start() { System.out.println("Engine started"); }
}
public class Car {
private final Engine engine; // Car HAS-A Engine
public Car(Engine engine) {
this.engine = engine;
}
public void start() {
engine.start(); // Delegation
}
}
When to use:
- Inheritance: when there’s a real type hierarchy (Dog IS-A Animal)
- Composition: in most other cases — it’s a more flexible approach
🟡 Middle Level
How It Works
Inheritance implements the “is-a” (IS-A) relationship and uses virtual method tables (VTable) at the JVM level. The subclass gains access to protected fields of the parent — this is “white-box reuse”.
Composition implements the “has-a” (HAS-A) relationship through method calls on another object. Interaction is only through the public interface — this is “black-box reuse”.
White-box reuse — parent internals are visible (inheritance). Black-box reuse — only public interface (composition). Devirtualization — JIT replaces virtual call with direct. Monomorphic = one call type → inlining. Megamorphic = 3+ types → table lookup.
Practical Application
Why Effective Java says “Favor Composition over Inheritance”:
| Criterion | Inheritance | Composition |
|---|---|---|
| Coupling | Static (compile-time) | Dynamic (runtime) |
| Encapsulation | Broken (protected fields visible) | Preserved |
| Flexibility | Cannot change parent | Can swap component |
| Testability | Hard to mock | Easy to substitute mock |
| Fragility | Parent change breaks all subclasses | Isolated changes |
Common Mistakes
| Mistake | Solution |
|---|---|
| Inheritance for code reuse | Use composition + delegation |
| Deep hierarchy (5+ levels) | Refactor toward composition |
ArrayList → MySpecialList via inheritance |
MySpecialList with a List field inside |
Alternatives to Inheritance
- Strategy: instead of
SmsNotification extends Notification→NotificationServicewith aSenderStrategyfield - Decorator: wrapping an object to add functionality (logging, caching)
- Delegation: a class implements an interface but calls delegate methods
When NOT to Use Composition
- Clean type hierarchies (exceptions:
IOException→SocketException) - Frameworks with Template Method pattern
- When polymorphism and working through a common base type is needed
Template Method vs Strategy: Template Method = you control the algorithm, subclasses control steps. Strategy = client chooses the entire algorithm. Template Method — when the algorithm skeleton is stable. Strategy — when the entire algorithm may change.
🔴 Senior Level
Internal Implementation at JVM Level
Inheritance and VTable: When calling a virtual method, the JVM uses a virtual method table (VTable). Each class has its own VTable with pointers to method implementations. With deep hierarchies (5+ levels), it’s harder for the JIT compiler to perform devirtualization and inlining.
// Inheritance: invokevirtual, VTable lookup
class Parent { void foo() {} }
class Child extends Parent { @Override void foo() {} }
// parent.foo() → invokevirtual → VTable lookup
// Composition with final: JIT can inline
class Service {
private final Delegate delegate; // final → monomorphic call
void execute() { delegate.run(); } // JIT inlines → direct call
}
Architectural Trade-offs
Inheritance:
- ✅ Pros: Out-of-the-box polymorphism, code reuse, Template Method
- ❌ Cons: Fragile base class, implementation leakage, static coupling
Composition:
- ✅ Pros: Dynamic behavior, testability (easy to mock), loose coupling
- ❌ Cons: More boilerplate code (delegation), more objects in heap
Edge Cases
- Fragile Base Class Problem: changing
addAllin the parent, which callsadd, breaks the subclass that overrode both methods- Solution: Composition + Forwarding
- This is a HashSet implementation detail: addAll calls add internally. But you can’t rely on this — in another implementation it may be different. That’s why inheritance from classes not designed for it is fragile.
- Shared State in Composition: multiple delegates work with shared data
- Solution: Explicit context passing or State Object
- Circular Delegation: A → B → A → StackOverflowError
- Solution: Architectural analysis, ArchUnit rules
Performance
| Metric | Inheritance | Composition |
|---|---|---|
| Object size | All parent fields in one object | Separate objects + headers |
| Method call | invokevirtual (VTable lookup) | invokevirtual + possible inlining |
| JIT optimization | Harder with deep hierarchy | Easier with final fields |
| Memory | ~16 byte header + all fields | ~16 bytes × N objects + refs |
- Inlining: JIT easily inlines
finaldelegates → overhead ~0 - VTable: At depth 5+ levels, megamorphic calls → deoptimization
Thread Safety
- Inheritance:
synchronizedon parent method blocks the entire object - Composition: Different locks can be used for different delegates → finer-grained concurrency
Production Experience
Refactoring in an enterprise project:
NotificationService with 2000 lines and subclasses EmailNotification, SmsNotification, PushNotification. Problem: adding a new strategy required changing the base class.
Refactoring: extracted NotificationStrategy interface, each type — a separate implementation. NotificationService became a coordinator. Result: new types are added without modifying existing code (OCP).
Monitoring
ArchUnit rules:
@ArchTest
static void no_deep_inheritance = classes()
.should().notHaveSimpleNameMatching(".*Impl.*")
.andShould().haveLessThanNAncestors(3);
SonarQube: Depth of Inheritance Hierarchy > 5 → Code Smell
Best Practices for Highload
- final delegates for better inlining
- Package-private for internal components
- Sealed classes (Java 17+) for controlled inheritance
Sealed classes are available since Java 17. For Java 8/11, use
finalclasses and document which classes are intended for inheritance.
- Composition over Inheritance as default choice
🎯 Interview Cheat Sheet
Must know:
- Inheritance = IS-A (static coupling, white-box reuse), Composition = HAS-A (dynamic, black-box)
- “Favor Composition over Inheritance” — Effective Java, Josh Bloch
- Composition preserves encapsulation, inheritance breaks it (
protectedfields visible) - JIT easily inlines
finaldelegates → composition overhead ~0 - Inheritance: fragile base class, implementation leakage, static coupling
- Fragile Base Class: changing
addAllin parent breaks subclass that overrodeadd - Sealed classes (Java 17+) for controlled inheritance
Common follow-up questions:
- Why is composition better? — Dynamic component replacement, testability (easy to mock), loose coupling
- When is inheritance acceptable? — True IS-A, frameworks with Template Method, common infrastructure fields (
BaseEntity) - What is devirtualization? — JIT replaces virtual call with direct when it knows the exact type
- VTable overhead with inheritance? — 5+ levels → megamorphic calls → JIT deoptimization
Red flags (DO NOT say):
- “Inheritance is the best way to reuse code” (main cause of fragility)
- “Deep hierarchy is a sign of good design” (5+ levels — code smell)
- “Composition is always better” (there are legit inheritance cases: Template Method, IS-A)
Related topics:
- [[11. When is it better to use composition instead of inheritance]]
- [[12. What is delegation in OOP]]
- [[5. What is Liskov Substitution principle]]
- [[6. Give an example of Liskov Substitution principle violation]]