What is the difference between composition and inheritance in Java?

Composition vs Inheritance in Java

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 Animal ripples 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

CriterionInheritanceComposition
Relationship typeIS-A (stable)HAS-A
CouplingTight (implementation)Loose (interface)
Behaviour changeCompile-time onlyRuntime possible
Multiple reuseLimited (single inheritance)Compose many objects
TestabilityHarder (parent state)Easy (inject mocks)
EncapsulationMay break parent'sFully 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 aloneStack extends Vector in the JDK is the canonical bad example; Stack should never expose get(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 final to 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.
Subscribe to Our Newsletter

Get the latest Java tips and tutorials delivered to your inbox!