Composition, Aggregation and Association in Java
Most relationships between classes are has-a, not is-a. Composition is usually the more flexible and more honest design.
-
Java Basics
- Introduction to Java
- Setting Up Java and Writing Your First Program
- Variables, Data Types and Literals in Java
- Type Casting and Type Conversion in Java
- Operators and Expressions in Java
- Input and Output in Java
- Comments, Keywords and Naming Conventions in Java
- Control Flow in Java: if, else and switch
- Loops in Java: for, while and do-while
- Methods
- Arrays and Strings
-
OOP
- Classes and Objects in Java
- Constructors in Java
- The this Keyword in Java
- The static Keyword in Java
- Encapsulation in Java
- Access Modifiers in Java
- Inheritance in Java
- Method Overriding and super in Java
- Polymorphism in Java
- Abstraction, Abstract Classes and Interfaces in Java
- Composition, Aggregation and Association in Java
- The Object Lifecycle in Java
- Core Java
- Exception Handling
-
Collections
- The Java Collections Framework
- List in Java: ArrayList, LinkedList, Vector and Stack
- Set in Java: HashSet, LinkedHashSet and TreeSet
- Map in Java: HashMap, LinkedHashMap and TreeMap
- How HashMap Works Internally in Java
- Queue and Deque in Java: ArrayDeque and PriorityQueue
- Iterators in Java
- Comparable and Comparator in Java
- Collections Utilities and Choosing the Right Collection
- Generics
- Java 8+
- Stream API
- Date and Time
- File and I/O
-
Multithreading
- Threads in Java: Processes, Runnable and Thread
- Thread Lifecycle in Java
- Synchronization in Java: synchronized and volatile
- Locks and Atomic Classes in Java
- Race Conditions and Deadlocks in Java
- The Executor Framework and Thread Pools in Java
- Future and CompletableFuture in Java
- Concurrent Collections in Java
- The Java Memory Model
- JVM and Memory
- Advanced Java
- Networking
- JDBC
- Testing
The three relationships
| Relationship | Meaning | Lifetime | Example |
|---|---|---|---|
| Association | One type simply uses another | Independent | A teacher and a student |
| Aggregation | A has-a where the part can exist alone | Independent | A department and its employees |
| Composition | A strong has-a where the part belongs to the whole | The part dies with the whole | An order and its line items |
All three are has-a. Inheritance is the only is-a relationship, and it is the one used most often by mistake.
Association
class EmailService {
void send(String to, String body) { }
}
class RegistrationService {
private final EmailService emails; // used, not owned
RegistrationService(EmailService emails) {
this.emails = emails;
}
void register(String address) {
emails.send(address, "Welcome");
}
}Aggregation
class Department {
private final String name;
private final List<Employee> members = new ArrayList<>();
Department(String name) {
this.name = name;
}
void add(Employee employee) {
members.add(employee); // the employee existed before, and survives after
}
List<Employee> members() {
return List.copyOf(members);
}
}Composition
class Order {
private final String reference;
private final List<LineItem> items = new ArrayList<>();
Order(String reference) {
this.reference = reference;
}
void addItem(String product, int quantity, double unitPrice) {
items.add(new LineItem(product, quantity, unitPrice)); // created and owned here
}
double total() {
return items.stream().mapToDouble(LineItem::subtotal).sum();
}
record LineItem(String product, int quantity, double unitPrice) {
double subtotal() {
return quantity * unitPrice;
}
}
}A line item has no meaning outside its order. The order creates it, owns it and never exposes the internal list directly.
Composition instead of inheritance
The classic mistake is extending a class purely to reuse its methods.
// Problem: a Stack is not a List, yet it now offers add(index, element),
// which lets a caller insert into the middle and break LIFO behaviour.
class BrokenStack<E> extends ArrayList<E> { }// Better: hold a list, expose only stack operations
class Stack<E> {
private final List<E> items = new ArrayList<>();
void push(E item) {
items.add(item);
}
E pop() {
if (items.isEmpty()) {
throw new NoSuchElementException("Stack is empty");
}
return items.remove(items.size() - 1);
}
boolean isEmpty() {
return items.isEmpty();
}
}Inheritance exposes the whole superclass API to every caller, whether or not it makes sense. Composition exposes exactly what you choose. That difference is why the usual advice is to prefer composition.
Comparison
| Aspect | Inheritance | Composition |
|---|---|---|
| Relationship | is-a | has-a |
| Fixed at | Compile time | Can change at runtime |
| Coupling | Tight; subclass depends on superclass internals | Loose; depends only on a public API |
| Exposed API | Everything inherited | Only what is delegated |
| Multiple sources | One superclass | Any number of components |
| Testing | Superclass behaviour comes along | Components can be substituted |
Delegation with an interface
interface Storage {
void save(String key, String value);
}
class LoggingStorage implements Storage {
private final Storage delegate;
LoggingStorage(Storage delegate) {
this.delegate = delegate;
}
@Override
public void save(String key, String value) {
System.out.println("Saving " + key);
delegate.save(key, value);
}
}Behaviour is added by wrapping rather than by extending. Any number of wrappers can be combined, and the behaviour can be chosen at runtime, which inheritance cannot do.
When inheritance is right
- The subtype genuinely is a kind of the supertype.
- Everything the supertype promises remains true for the subtype.
- The supertype was designed and documented for extension.
- You control both classes, or the superclass is explicitly meant to be extended.
Common mistakes
- Extending a class only to reuse a few methods.
- Building a deep hierarchy where a component would do.
- Exposing the internal collection of a composed object, which lets callers modify what the owner is responsible for.
- Modelling aggregation as composition and deleting parts that other objects still need.
Best practices
- Ask "is-a or has-a" before writing
extends. - Delegate to a component and expose only the operations that make sense.
- Return copies or unmodifiable views of composed collections.
- Inject collaborators through the constructor, which makes dependencies visible and testable.
Practice
- Decide association, aggregation or composition for: a house and its rooms, a library and its books, a driver and a car.
- Rewrite a class that extends
ArrayListso that it composes one instead, and list the methods no longer exposed. - Write a wrapper that adds timing around any
Storageimplementation. - Why does exposing the internal list of an
Orderweaken composition? - Give one example where inheritance is genuinely the better choice, and justify it.
Conclusion
Inheritance says is-a and hands out the whole superclass. Composition says has-a and hands out only what you choose. Reach for composition by default, and use inheritance when the substitution is honest.