What is the difference between composition and inheritance in Java?
Table of Contents
1. Short Answer
Inheritance models an IS-A relationship. A subclass extends a superclass and inherits its state and behaviour. Composition models a HAS-A relationship. A class holds a reference to another object and delegates work to it.
GoF Principle
"Favour object composition over class inheritance." — Design Patterns (Gang of Four, 1994)
2. IS-A vs HAS-A Relationship
The simplest mental test:
- If you can say "A is a B" meaningfully and the relationship is stable, inheritance can be appropriate. Example:
Dog is an Animal. - If you can say "A has a B", prefer composition. Example:
Car has an Engine.
If the IS-A test fails — or the relationship might change — reach for composition first.
3. Inheritance in Detail
In Java, a class inherits from exactly one superclass (single inheritance). The subclass gains all public and protected members of the parent and can override methods.
// Superclass
public class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public String speak() {
return name + " makes a sound";
}
}
// Subclass — IS-A relationship
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
@Override
public String speak() {
return name + " barks"; // overrides parent behaviour
}
}
// Usage
Animal animal = new Dog("Rex");
System.out.println(animal.speak()); // Rex barks (polymorphism)
Key points:
- Java supports single inheritance for classes.
- The subclass is tightly coupled to the superclass's implementation, not just its interface.
- Any change to
Animalripples through all subclasses.
4. Fragile Base Class Problem
Because subclasses depend on the internal implementation of the parent, changing the superclass can silently break subclasses — even when the public API stays the same.
// A classic example: InstrumentedHashSet
// Goal: count every element ever added
public class InstrumentedHashSet<E> extends HashSet<E> {
private int addCount = 0;
@Override
public boolean add(E e) {
addCount++;
return super.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return super.addAll(c); // ← BUG: HashSet.addAll calls add() internally
}
public int getAddCount() { return addCount; }
}
// After addAll(List.of("a","b","c"))
// addCount becomes 6 instead of 3 — addAll calls add(), doubling the count
Why this happens
The subclass relied on an implementation detail of HashSet.addAll (that it calls add). When Java changes that detail, the count logic breaks. This is the fragile base class problem in action.
5. Composition in Detail
Instead of extending, hold a reference to the object you need and call its methods explicitly. This avoids inheriting implementation details and gives you full control.
// Fixed InstrumentedSet using composition
public class InstrumentedSet<E> implements Set<E> {
private final Set<E> set; // composed object (HAS-A)
private int addCount = 0;
public InstrumentedSet(Set<E> set) {
this.set = set;
}
@Override
public boolean add(E e) {
addCount++;
return set.add(e);
}
@Override
public boolean addAll(Collection<? extends E> c) {
addCount += c.size();
return set.addAll(c); // delegates — no internal double-counting
}
public int getAddCount() { return addCount; }
// delegate remaining Set methods to set...
@Override public int size() { return set.size(); }
@Override public boolean isEmpty() { return set.isEmpty(); }
@Override public boolean contains(Object o){ return set.contains(o); }
@Override public Iterator<E> iterator() { return set.iterator(); }
@Override public Object[] toArray() { return set.toArray(); }
@Override public <T> T[] toArray(T[] a) { return set.toArray(a); }
@Override public boolean remove(Object o) { return set.remove(o); }
@Override public boolean containsAll(Collection<?> c){ return set.containsAll(c); }
@Override public boolean removeAll(Collection<?> c) { return set.removeAll(c); }
@Override public boolean retainAll(Collection<?> c) { return set.retainAll(c); }
@Override public void clear() { set.clear(); }
}
Now InstrumentedSet works correctly regardless of any changes to HashSet's internal implementation.
6. Delegation Pattern
Delegation is the heart of composition. An object receives a request and passes ("delegates") it to a helper object to fulfil it. This separates the what from the how.
// Printer capability — modelled as a separate class
public class Printer {
public void print(String document) {
System.out.println("Printing: " + document);
}
}
// OfficeWorker HAS-A Printer — uses delegation
public class OfficeWorker {
private final Printer printer; // composed dependency
public OfficeWorker(Printer printer) {
this.printer = printer; // injected — easy to swap/mock
}
public void submitReport(String report) {
System.out.println("Finalising report...");
printer.print(report); // delegates printing responsibility
}
}
// Usage
Printer p = new Printer();
OfficeWorker worker = new OfficeWorker(p);
worker.submitReport("Q2 Sales Numbers");
Because Printer is injected, you can swap it with a MockPrinter in tests or replace it with a PDFPrinter at runtime — no subclassing required.
7. Same Design: Both Ways
Here is a classic example — a logging stack — implemented first with inheritance, then with composition, so you can see the trade-offs side by side.
7.1 Inheritance approach
// Inheritance: LoggingStack extends Stack
public class Stack<T> {
protected final java.util.LinkedList<T> list = new java.util.LinkedList<>();
public void push(T item) { list.addFirst(item); }
public T pop() { return list.removeFirst(); }
public T peek() { return list.getFirst(); }
public int size() { return list.size(); }
}
public class LoggingStack<T> extends Stack<T> { // IS-A Stack
@Override
public void push(T item) {
System.out.println("PUSH: " + item);
super.push(item);
}
@Override
public T pop() {
T item = super.pop();
System.out.println("POP: " + item);
return item;
}
}
// Problem: LoggingStack inherits direct access to `list` — tight coupling
7.2 Composition approach (preferred)
// Composition: LoggingStack HAS-A Stack
public class LoggingStack<T> {
private final Stack<T> stack; // composed — not inherited
public LoggingStack(Stack<T> stack) {
this.stack = stack;
}
public void push(T item) {
System.out.println("PUSH: " + item);
stack.push(item);
}
public T pop() {
T item = stack.pop();
System.out.println("POP: " + item);
return item;
}
public int size() { return stack.size(); }
}
// Usage — can wrap any Stack implementation at runtime
LoggingStack<Integer> ls = new LoggingStack<>(new Stack<>());
ls.push(42);
ls.pop();
The composition version is independent of Stack's internals and can wrap any compatible implementation.
8. When to Use Each
| Criterion | Inheritance | Composition |
|---|---|---|
| Relationship type | IS-A (stable) | HAS-A |
| Coupling | Tight (implementation) | Loose (interface) |
| Behaviour change | Compile-time only | Runtime possible |
| Multiple reuse | Limited (single inheritance) | Compose many objects |
| Testability | Harder (parent state) | Easy (inject mocks) |
| Encapsulation | May break parent's | Fully preserved |
8.1 Use Inheritance when:
- A genuine IS-A relationship exists that will not change.
- You need polymorphic substitution (Liskov Substitution Principle).
- The subclass is a true specialisation (e.g.,
ArrayList extends AbstractList).
8.2 Favour Composition when:
- You want to reuse behaviour without tight coupling.
- You need to combine behaviour from multiple sources.
- The relationship might vary at runtime (Strategy pattern).
- The IS-A test is questionable.
9. Interview Context
Interviewers ask this question to gauge your understanding of OOP design trade-offs. Strong answers:
- Define both relationships with precise terminology (IS-A / HAS-A).
- Mention the GoF principle by name.
- Name the fragile base class problem and give a concrete example.
- State when inheritance is the right choice (Liskov, true specialisation).
- Discuss testability — composition enables dependency injection and mocking.
10. Common Pitfalls
- Extending for code reuse alone —
Stack extends Vectorin the JDK is the canonical bad example; Stack should never exposeget(index). - Deep inheritance hierarchies — every extra level multiplies fragility.
- Overusing composition — if a true IS-A exists, composition means duplicating the interface manually.
- Forgetting to delegate all methods — a composed wrapper must forward every relevant method, or it subtly breaks contracts.
- Making composed fields non-final — injected collaborators should normally be
finalto prevent accidental reassignment.
11. Conclusion
Both inheritance and composition are valid OOP tools, but they serve different purposes:
- Use inheritance for stable IS-A hierarchies where polymorphism is needed.
- Use composition when you need flexible, loosely coupled reuse.
- The GoF principle "favour composition over inheritance" holds because composition avoids the fragile base class problem and supports dependency injection.
- The delegation pattern is the practical mechanism behind composition.
- Ask the IS-A test first — if uncertain, default to composition.