The Has-A Association
Composition over Inheritance
Where inheritance models an “is-a” relationship (a Dog is an Animal), composition models a “has-a” relationship (a Car has an Engine). A class holds a reference to another class as a field, and delegates work to it through method calls.
public class Car {
private Engine engine; // Car has-a Engine
private List<Wheel> wheels; // Car has-a list of Wheels
public Car(Engine engine) {
this.engine = engine;
this.wheels = new ArrayList<>();
}
public void start() {
engine.ignite(); // delegation
}
}
Car does not extend Engine — that would be nonsense (a car is not a kind of engine). Instead, Car holds a reference to an Engine and calls its methods.
Why Prefer Composition?
The maxim “favour composition over inheritance” comes from the seminal Design Patterns book (Gamma et al., 1994). The reasons:
Flexibility at Runtime
With inheritance, the class hierarchy is fixed at compile time. With composition, you can swap the composed object at runtime:
public class Car {
private Engine engine;
public void setEngine(Engine newEngine) {
this.engine = newEngine;
}
}
You can replace the engine without changing the car’s class. Inheritance cannot model this — you cannot change a Dog into a Cat at runtime.
Avoids Deep, Fragile Hierarchies
Inheritance creates tight coupling between subclass and superclass. A change to the superclass can silently affect all subclasses. Over time, as the hierarchy deepens, it becomes difficult to understand which class provides which behaviour and which methods are overridden where.
Composition keeps classes independent. The containing class depends only on the composed class’s public interface, not on its implementation details.
Better Encapsulation
Inheritance exposes the superclass’s protected members to subclasses, breaking encapsulation (the “subclassing for implementation” problem). Composition keeps the composed object as a private field — the containing class controls exactly what is exposed and how.
Multiple Compositions
A class can compose multiple objects but can only extend one superclass. If you need functionality from two sources, composition lets you hold references to both:
public class Smartphone {
private Screen screen;
private Battery battery;
private Camera camera;
private List<Sensor> sensors;
}
A Smartphone is not a Screen or a Battery — it has them.
Aggregation vs Composition
UML distinguishes two flavours of has-a, which map to different design intentions in Java:
Aggregation (Weaker)
The contained object can exist independently of the container. If the container is destroyed, the contained objects may survive:
public class Library {
private List<Book> books;
public Library(List<Book> initialBooks) {
this.books = new ArrayList<>(initialBooks); // books exist outside
}
}
Books are created elsewhere and passed into the library. They have an identity and lifecycle independent of any particular library. A book can be in multiple libraries or in none.
Composition (Stronger)
The contained object’s lifecycle is tied to the container. The container creates and destroys the contained object:
public class House {
private List<Room> rooms;
public House() {
this.rooms = new ArrayList<>();
rooms.add(new Room("Kitchen"));
rooms.add(new Room("Living Room"));
rooms.add(new Room("Bedroom"));
}
}
Rooms are created inside the House constructor and are never exposed to external code. When the House becomes unreachable, the Room objects become unreachable too (assuming no external references were leaked). The rooms cannot exist without the house.
In Java, the distinction is conceptual rather than enforced by language features. Both are implemented as fields holding references. The difference lies in how the references are managed: does the container create the object? Does it expose it externally? Does it accept objects from outside?
Real-World Example: Stack vs JDK Stack
The JDK’s java.util.Stack class extends Vector. This is a classic anti-pattern:
public class Stack<E> extends Vector<E> {
public E push(E item) { ... }
public E pop() { ... }
public E peek() { ... }
}
Because Stack extends Vector, it inherits insertElementAt(), removeElementAt(), setElementAt(), and other methods that let you insert, remove, or modify elements at arbitrary positions. These operations silently violate the LIFO invariant of a stack. A stack should only allow pushing and popping from the top; a Vector inherited by Stack lets you insert into the middle.
The correct design uses composition:
public class Stack<E> {
private List<E> elements = new ArrayList<>();
public void push(E item) {
elements.add(item);
}
public E pop() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.remove(elements.size() - 1);
}
public E peek() {
if (elements.isEmpty()) {
throw new EmptyStackException();
}
return elements.get(elements.size() - 1);
}
}
Stack has-a List, rather than is-a Vector. The public interface exposes only push, pop, and peek. The LIFO invariant is now enforced by the API itself — there is no way for a client to insert into the middle because no such method exists.
When Inheritance Is Appropriate
Composition is not always the answer. Inheritance is appropriate when:
- The subclass genuinely is a more specific version of the superclass — a
SavingsAccounttruly is aBankAccount - The subclass obeys the superclass contract — it satisfies the Liskov Substitution Principle (discussed in the inheritance notes)
- The subclass adds or refines behaviour without removing or contradicting superclass behaviour
- The class hierarchy is shallow and stable — deep hierarchies are a warning sign
The decision heuristic: if you find yourself overriding methods to throw UnsupportedOperationException or to make them do nothing, you are fighting the inheritance. Use composition instead.