Inheritance & Polymorphism

Composition vs Inheritance

15 min Lesson 9 of 14

Composition vs Inheritance

You now know how to build class hierarchies with extends. But inheritance is not always the right tool. In this lesson you will learn about composition — a different way to reuse code — and understand when each technique belongs.

is-a vs has-a

The single most useful question when deciding between inheritance and composition is:

  • Is-a relationship? Use inheritance. A Dog is-a Animal. A SavingsAccount is-a BankAccount.
  • Has-a relationship? Use composition. A Car has-a Engine. A Person has-a Address.

When the relationship is genuinely hierarchical and substitution makes sense (anywhere a BankAccount is expected, a SavingsAccount works), inheritance is appropriate. When one class simply owns or uses another, composition is the better fit.

A Quick Example: the Wrong Way

Suppose you model a car by inheriting from Engine:

// BAD — a Car is not an Engine class Engine { public void start() { System.out.println("Engine started"); } } class Car extends Engine { // wrong: Car is-an Engine? private String model; Car(String model) { this.model = model; } }

This compiles, but it is conceptually wrong. A Car is not a kind of Engine; it has one. Worse, every public method of Engine is now exposed on Car — callers can call car.start() thinking they are starting the car, but they are really reaching inside an implementation detail.

The Right Way: Composition

With composition, Car holds a reference to an Engine as a field:

class Engine { private final String type; Engine(String type) { this.type = type; } public void start() { System.out.println(type + " engine started"); } } class Car { private final String model; private final Engine engine; // has-a Car(String model, Engine engine) { this.model = model; this.engine = engine; } public void drive() { engine.start(); System.out.println(model + " is moving"); } } public class Main { public static void main(String[] args) { Engine v8 = new Engine("V8"); Car car = new Car("Mustang", v8); car.drive(); // Output: // V8 engine started // Mustang is moving } }

Car delegates the starting behaviour to Engine through a method call, not through inheritance. Engine's internals are completely hidden from callers of Car.

Delegation is the key pattern. Composition means one class keeps an instance of another and calls its methods. The outer class decides which methods to expose and which to keep private — giving you fine-grained control over the public API.

The Fragile Base Class Problem

One of the most important reasons to prefer composition is the fragile base class problem: when you change a parent class, you can silently break all subclasses — even ones you never touched.

Here is a classic illustration. Imagine a custom list that counts how many elements were ever added:

import java.util.ArrayList; class CountingList<E> extends ArrayList<E> { private int addCount = 0; @Override public boolean add(E element) { addCount++; return super.add(element); } @Override public boolean addAll(java.util.Collection<? extends E> c) { addCount += c.size(); return super.addAll(c); } public int getAddCount() { return addCount; } } public class FragileDemo { public static void main(String[] args) { CountingList<String> list = new CountingList<>(); list.addAll(java.util.List.of("a", "b", "c")); System.out.println(list.getAddCount()); // Prints 6, not 3! } }

Why 6 instead of 3? Because ArrayList.addAll internally calls add for each element. So CountingList.addAll increments by 3, and then super.addAll calls CountingList.add three more times, each incrementing again. The subclass broke because it depended on an internal implementation detail of the parent class — a detail that is not part of the public contract and could change in any future JDK release.

Never inherit from a class just to reuse its implementation unless you truly model an is-a relationship and the parent class was explicitly designed for extension. Extending concrete library classes like ArrayList, HashMap, or Stack almost always leads to this kind of subtle bug.

Fixing It with Composition

Wrap the list instead of extending it:

import java.util.*; class CountingList<E> { private final List<E> list = new ArrayList<>(); private int addCount = 0; public boolean add(E element) { addCount++; return list.add(element); } public boolean addAll(Collection<? extends E> c) { addCount += c.size(); return list.addAll(c); } public int size() { return list.size(); } public E get(int i) { return list.get(i); } public int getAddCount() { return addCount; } } public class FixedDemo { public static void main(String[] args) { CountingList<String> list = new CountingList<>(); list.addAll(List.of("a", "b", "c")); System.out.println(list.getAddCount()); // Correctly prints 3 } }

Now CountingList owns its counting logic entirely. Changes to ArrayList's internals cannot affect it.

Favoring Composition: the Guiding Principle

The phrase "favor composition over inheritance" comes from the classic book Design Patterns (the "Gang of Four"). It does not mean never use inheritance — it means use inheritance only when the is-a test passes convincingly and the base class was designed to be extended. In all other cases, compose.

  • Composition is more flexible. You can swap the composed object at runtime (e.g., inject a different Engine), which is the foundation of many design patterns like Strategy and Decorator.
  • Composition has tighter encapsulation. Internal details of the helper class do not leak into the outer class's API.
  • Inheritance is still right sometimes. Modeling a true type hierarchy — Shape / Circle / Rectangle — is exactly what inheritance is for. The key is honesty about the relationship.
Quick decision checklist: Can you say "A [subclass] is a kind of [superclass]" convincingly? Does the subclass honour the parent's contract in every situation? Was the parent designed to be subclassed? If all three answers are yes, inheritance is fine. Otherwise, prefer composition.

Summary

Inheritance models is-a relationships; composition models has-a relationships. The fragile base class problem shows that extending concrete classes to reuse their code can break silently when the parent changes. Composition — holding a reference to a helper object and delegating to it — avoids this coupling, gives you a cleaner API, and makes your code easier to swap and test. In the next and final lesson of this tutorial you will apply everything you have learned by building a complete Shape hierarchy.