Aggregation: Has-A, Can Outlive
Before You Start
Check each box you can do from memory. A box you cannot check yet is not a problem; it points you to a quick refresher, not a grade.
Not sure? Take the 60-second self-check.
1. HAS-A marker. What syntactic element establishes HAS-A?
Check
A field declaration of another class’s type.
2. Association vs. HAS-A. A method parameter is association. A field is HAS-A. What is the additional distinction between aggregation and composition?
Check
Aggregation: the contained object was created outside and passed in (received). Composition: the contained object was created inside (via new). The question is “who made this object?”
3. Shared reference. If the caller holds a reference to an object that was passed into a constructor and stored as a field, what can the caller do?
Check
The caller can modify the object (if it is mutable), and the class that stored it will see those changes because both hold the same reference.
Try This First
public class Department {
private final List<Professor> professors;
public Department(final List<Professor> professors) {
this.professors = professors;
}
}
Before reading: after calling new Department(profList), the caller does profList.clear(). What happens inside the Department object?
Check
this.professors inside Department now sees an empty list. The field and profList are the same list object. Clearing through one reference clears through both.
What You Need To Walk In With
The Insight: Aggregation is “has-a, received from outside.” The marker is the constructor body: if the contained object is assigned from a parameter (this.professors = professors), not constructed from scratch, the relationship is aggregation. The contained object existed before the container was built and can exist after the container is gone. This means the caller shares the reference and can mutate through it.
The practical question this idea answers is: “what happens if the caller modifies the object after handing it to me?” Aggregation says the answer is expected and visible. Composition says the caller handed over data, not ownership, and the container made its own copy. Knowing which situation you are in determines whether shared mutation is a feature or a bug. You can: identify aggregation by reading a constructor, predict what a caller mutation does to the stored field, explain when aggregation is the correct design, and convert aggregation to composition by adding a defensive copy.
How It Works
The aggregation marker
The diagnostic is the constructor body:
// AGGREGATION: assigned from parameter
public Department(final List<Professor> professors) {
this.professors = professors; // same reference as caller's list
}
// COMPOSITION: constructed internally
public Department() {
this.professors = new ArrayList<>(); // private, exclusive list
}
In the aggregation form, the same List<Professor> object is now referenced from both the caller’s variable and this.professors. They are aliases for the same object.
The lifetime implication
The contained object can outlive the container. If Department is garbage-collected, the List<Professor> it referenced persists wherever else references to it exist. One professor can be in multiple departments simultaneously: the same Professor instance can appear in multiple Department.professors lists.
This is the expected and correct behavior for aggregation. A Professor is a free-standing entity; Departments reference Professors. The Department does not own the Professor.
When to choose aggregation
Use aggregation when:
- The contained object is independently managed (created, destroyed, mutated) by the caller.
- Sharing the object across multiple containers is expected or desirable.
- The caller wants changes to the object to be visible inside the container.
Use composition when the container should own and isolate the contained object.
Worked Example: Predict Then Check
List<Professor> dept1Profs = new ArrayList<>(List.of(profA, profB));
List<Professor> dept2Profs = new ArrayList<>(List.of(profB, profC));
Department cs = new Department(dept1Profs);
Department ee = new Department(dept2Profs);
// Both departments share profB (same Professor instance)
dept1Profs.add(profD);
System.out.println(cs has profD: true or false?
Reasoning
cs stores this.professors = dept1Profs (same reference). Adding profD to dept1Profs is adding to cs.professors because they are the same object.
Show answer
cs has profD: true. The caller and the Department share the same list; adding to the caller’s reference adds to the Department’s view of its professors.
profB is in both cs and ee as the same instance: classic aggregation.
Quick check
Check your understanding
A caller builds a List<Professor>, passes it to new Department(profList), then calls profList.clear(). What does the Department’s internal professors field contain immediately after the clear?
Common Misconceptions
Misconception: aggregating a mutable shared object without realizing the caller can mutate it
Wrong mental model: “I received the list from outside and stored it; the caller will not touch it after handing it to me.”
Why it breaks: The caller still holds a reference to the same list. Any mutation through the caller’s reference is visible inside the container. This is the correct behavior for aggregation, but it is a bug when the design expected isolation.
How to correct: If isolation is the intent, defensive-copy the received object in the constructor. That changes the relationship from aggregation to composition. If sharing is the intent, document it and accept the coupling.
Source: Bloch, Effective Java, Item 50.
Quick check
Check your understanding
A developer writes: this.students = students; in a constructor, then reasons ‘the caller handed us this list so they will not touch it again.’ What is the flaw in this reasoning?
Formal Definition
UML 2.5 defines aggregation as a binary association with the additional semantics of “part-whole” where the part can exist independently of the whole. The UML notation is an open (hollow) diamond on the container (whole) side of the relationship line. In Java, the open diamond maps to a field assigned from a constructor parameter.
Mental Model
Aggregation is a library catalog. The library’s catalog holds references to the books in its collection. A book exists independently: it can be in multiple catalogs, it can move to a different library, and it exists before the catalog entry was created and after the catalog is deleted. The catalog “has” the book in the sense of indexing it, but the book’s lifetime is not controlled by the catalog.
Connections
Within CSCD 210/211: Injecting a Comparator or Scanner into a class via constructor and storing the reference is aggregation. Both objects existed before the class was created and can exist after it is gone.
Looking back: Association: Uses-A covers the weakest relationship (uses, via parameter). This lesson covers the first true HAS-A.
Looking ahead: Composition: Has-A, Owns Lifetime covers the strongest HAS-A form, where the container builds the contained object and owns its lifetime.
Practice
Level 1
Classify as aggregation or composition:
// (a)
class Library {
private final List<Book> books;
public Library(final List<Book> books) { this.books = books; }
}
// (b)
class Library {
private final List<Book> books;
public Library() { this.books = new ArrayList<>(); }
public void register(final Book b) { this.books.add(b); }
}
Show answer
(a) Aggregation: the list is received from outside and stored by reference.
(b) The list (this.books) is created internally (aggregation of the list object by the library). But Book objects are added via register, so the library aggregates the books too (they exist outside and are referenced here). The list itself is owned by the library; the books inside are not. This is nuanced: the list is composed (private, exclusive), but the books inside the list are aggregated.
Level 2
A Course class receives a List<Student> from outside and stores it. A semester later, the roster changes: students drop. The caller updates the list. Predict whether the Course object sees the change and explain why.
Show answer
Yes, Course sees the change. Aggregation means Course.students and the caller’s list reference the same object. Any mutation through either reference is immediately visible through both. This is the expected behavior for aggregation.
If Course should have an independent view of the roster (isolated from external changes), the constructor should defensively copy: this.students = new ArrayList<>(students). That converts the relationship to composition.
Level 3
Explain the difference between aggregation and composition using the two diagnostic questions: (1) who created the contained object? and (2) can the contained object outlive the container?
Show answer
Aggregation: The contained object was created outside the container and passed in. The container received it. Because the object was created externally, other references to it exist outside the container. When the container is garbage-collected, those external references keep the object alive. The contained object CAN outlive the container.
Composition: The contained object was created inside the container, typically with new in the constructor. No external references exist to that specific object at creation time (unless the container subsequently exposes them). When the container is garbage-collected, no other references keep the contained object alive. The contained object CANNOT outlive the container under normal conditions.
Go Deeper (optional)
Nothing here is needed to write correct code. These connections are for the curious.
Aggregation and composition map directly onto two recurring patterns in professional Java codebases. Aggregation is the correct structure for many-to-many relationships: a student enrolled in multiple courses, a tag attached to multiple articles, a user belonging to multiple groups. Composition is the correct structure for part-whole exclusive ownership: an order item that belongs to exactly one order, a transaction record owned by exactly one ledger entry. Recognizing which situation you are in is a day-one professional instinct, not an advanced topic.
The design goes deeper than Java. In order theory, the “who created it?” question is really a question about control over an object’s lifecycle, which is a binary relation: either the container owns it (composition) or it does not (aggregation). Passing a Comparator into a class is aggregation because the Comparator existed before the class and the class does not manage its lifetime. That same Comparator is also an instance of the Strategy pattern (Gamma et al., 1994): a behavior object injected at construction time so the containing class can vary its algorithm without changing its own code. The function-object idea captured by Strategy is, in type-theory terms, the simplest form of a first-class function: an object that wraps a single behavior and can be passed around like a value. The has-a versus is-a distinction that separates aggregation from inheritance maps, at the type-theory level, to product types versus sum types: a class that holds a field (has-a) is a product of its field types, while a class that extends another (is-a) describes a subtype relationship. The single-responsibility pressure behind preferring has-a over is-a traces to Parnas (1972), who argued that modules should be decomposed around information that changes independently, which is precisely why injecting a Comparator via aggregation is better than subclassing: the sorting strategy is an independently varying concern.
Check Yourself
Close the notes and answer each one from memory, then reveal it. Pulling an idea back from memory is one of the strongest ways to make it stick.
Check your understanding
Which constructor body marks a relationship as aggregation rather than composition?
A Professor object is passed into two different Department constructors and stored by reference in each. The Professor is then modified by the caller. Which statement is true?
Trace this code and predict what prints: List<String> names = new ArrayList<>(List.of(“Alice”, “Bob”)); Department d = new Department(names); // stores: this.members = names; names.add(“Carol”); System.out.println(d.memberCount()); // returns this.members.size() What does memberCount() print?
According to the lesson, when is aggregation the correct design choice?
What does it mean that an aggregated object can outlive its container?