☕ Java
Aggregation
Aggregation is a specialised form of association that represents a whole-part relationship where the part can exist independently of the whole. The whole object contains or manages references to the part objects, but the part objects have their own identity and lifecycle that is not tied to the whole. If the whole is destroyed, the parts continue to exist. Aggregation models the has-a relationship where ownership is weak — the whole has the parts, but does not own them in the sense of being responsible for their creation and destruction.
Understanding Aggregation — Whole and Part With Independent Lives
The key characteristic that distinguishes aggregation from composition is lifecycle independence. In aggregation, the part objects exist independently of the whole — they were not created by the whole, they are not owned by the whole, and they continue to exist when the whole is destroyed. The whole merely references or groups the parts for some period of time.
Think of a University and its Departments. The university contains departments — in that sense, departments are parts of the university. But departments do not cease to exist if the university merges with another institution or restructures. The Computer Science Department existed before the current university structure and would continue to exist under a different name in a different institution. The lifecycle of a Department is not controlled by the University object.
Similarly, a Playlist and its Songs form an aggregation. The playlist groups songs — it has songs — but the songs exist independently. Deleting a playlist does not delete the songs. A song can appear in many playlists simultaneously. The song's existence is entirely independent of any playlist that happens to reference it.
In Java, aggregation is implemented by holding references to externally created objects. The aggregating class receives its parts through constructor injection or setter methods — the parts are passed in from outside, not created inside. This is the technical marker of aggregation: the parts are not new'd inside the whole's constructor. Their construction is the responsibility of whoever created them and passed them in, and their destruction is not the whole's concern.
The practical design implication is that aggregation-managed objects should not be deleted or closed by the aggregating object. If a Logger is passed into a Service, the Service uses the Logger but should not call logger.close() when it finishes — the logger may be shared with other services and its lifecycle is not the Service's responsibility to manage.
Java
// ── Aggregation: University has Departments, but Departments ─────────
// exist independently — they are passed in, not created inside: ───────
public class Department {
private String name;
private String building;
private List<String> courses;
public Department(String name, String building, List<String> courses) {
this.name = name;
this.building = building;
this.courses = new ArrayList<>(courses);
}
public String getName() { return name; }
public String getBuilding() { return building; }
public List<String> getCourses() { return new ArrayList<>(courses); }
@Override
public String toString() {
return "Department{" + name + ", " + building + "}";
}
}
public class University {
private String name;
private List<Department> departments = new ArrayList<>();
public University(String name) {
this.name = name;
}
// Aggregation: Department is passed in, not created here.
// University does NOT control Department's lifecycle.
public void addDepartment(Department department) {
departments.add(department);
}
public void removeDepartment(Department department) {
departments.remove(department);
// Department is NOT destroyed — it continues to exist
// and can be added to another University.
}
public List<Department> getDepartments() {
return new ArrayList<>(departments);
}
public void printStructure() {
System.out.println("University: " + name);
departments.forEach(d ->
System.out.println(" - " + d.getName() +
" (" + d.getBuilding() + ")"));
}
}
// ── Departments created independently, shared across universities: ────
Department cs = new Department("Computer Science", "Tech Block",
List.of("Algorithms", "Databases", "AI"));
Department math = new Department("Mathematics", "Science Block",
List.of("Calculus", "Linear Algebra", "Statistics"));
Department phys = new Department("Physics", "Science Block",
List.of("Mechanics", "Quantum", "Thermodynamics"));
University uni1 = new University("University of Example");
uni1.addDepartment(cs);
uni1.addDepartment(math);
University uni2 = new University("Institute of Technology");
uni2.addDepartment(cs); // same cs object — shared across universities
uni2.addDepartment(phys);
uni1.printStructure();
uni2.printStructure();
// If uni1 is garbage-collected, cs and math still exist.
// uni2 still holds a reference to cs — it is alive.
uni1 = null;
System.out.println("CS still exists: " + cs.getName()); // Computer ScienceAggregation in Practice — Constructor Injection
The practical implementation of aggregation in Java follows a consistent pattern: the aggregated objects are provided to the aggregating class through its constructor or through setter methods, rather than being created inside the aggregating class. This pattern is called dependency injection — the dependencies (the parts) are injected into the whole from outside rather than instantiated inside.
Constructor injection is the preferred form because it makes the dependencies explicit and enables immutability — the aggregating class can store injected references in final fields. It also makes the class's dependencies visible at a glance: you can see exactly what a class needs by looking at its constructor parameters. This is why constructor injection is favoured in professional Java code and is the default mode for dependency injection frameworks like Spring.
The lifecycle independence of aggregation means that the aggregating class should never close, destroy, or set to null the references it has been given. The external owner that created the parts is responsible for their cleanup. This is particularly important for resources like database connections, thread pools, file handles, and network sockets — if these are passed in as part of an aggregation, the receiving class uses them but must not close them.
Java
// ── Aggregation through constructor injection: ───────────────────────
public class Logger {
private final String name;
public Logger(String name) { this.name = name; }
public void log(String level, String message) {
System.out.printf("[%s] [%s] %s%n",
java.time.LocalTime.now(), level, message);
}
public void close() {
System.out.println("Logger " + name + " closed");
}
}
public class UserRepository {
private final Logger logger; // aggregation — Logger passed in
// Constructor injection — Logger is NOT created here:
public UserRepository(Logger logger) {
this.logger = Objects.requireNonNull(logger, "logger required");
}
public void save(String username) {
logger.log("INFO", "Saving user: " + username);
// ... actual save logic ...
logger.log("INFO", "User saved: " + username);
}
public String findByUsername(String username) {
logger.log("INFO", "Finding user: " + username);
return username; // simplified
}
// UserRepository does NOT close the logger — it doesn't own it.
// The Logger may be shared with other classes.
}
public class OrderRepository {
private final Logger logger; // same aggregation pattern
public OrderRepository(Logger logger) {
this.logger = Objects.requireNonNull(logger, "logger required");
}
public void save(String orderId) {
logger.log("INFO", "Saving order: " + orderId);
}
}
// ── Shared logger — aggregation allows sharing: ───────────────────────
Logger sharedLogger = new Logger("AppLogger");
// Both repositories use the same logger — aggregation (not composition)
// makes this sharing safe and natural:
UserRepository userRepo = new UserRepository(sharedLogger);
OrderRepository orderRepo = new OrderRepository(sharedLogger);
userRepo.save("alice");
orderRepo.save("ORD-001");
// sharedLogger is still active — neither repository closed it.
// The application code that created it is responsible for closing:
sharedLogger.close(); // closed only when the application is done with itAggregation vs Composition vs Association
The three relationships form a spectrum of increasing strength of ownership. Association is the loosest — two objects know about each other, but neither is part of the other and neither controls the other's lifecycle. Aggregation adds the whole-part structure — one object is conceptually a container for others — but the parts have independent lifecycles and can exist without the whole. Composition is the strongest — the parts are created by the whole, exist only within the whole, and are destroyed when the whole is destroyed.
In practice, the distinction is sometimes subtle and depends on the semantics of the domain rather than the Java syntax. Java does not have a syntactic difference between aggregation and composition — both are implemented using reference fields. The difference is entirely in the semantics: who creates the parts, who is responsible for their lifecycle, and what happens to the parts when the whole is destroyed.
A useful set of questions to identify the relationship type: Can the part exist without the whole? (If yes, it is aggregation or association.) Can the part be shared with multiple wholes? (If yes, it is aggregation or association — composition does not allow sharing.) Is the part created inside the whole's constructor? (If yes, it is likely composition.) Is the part passed into the whole from outside? (If yes, it is likely aggregation or association.)
Java
// ── Comparing all three relationships side by side: ──────────────────
// ASSOCIATION — uses, no ownership, fully independent:
public class Driver {
public void drive(Car car) { // Car is a parameter — no field
System.out.println("Driving: " + car.getModel());
}
// Driver has no field holding Car — the association is temporary,
// existing only for the duration of the method call.
}
// AGGREGATION — has-a, parts exist independently, passed in:
public class Playlist {
private String name;
private List<Song> songs; // Songs are passed in, not created here
public Playlist(String name) {
this.name = name;
this.songs = new ArrayList<>();
}
public void addSong(Song song) {
songs.add(song); // Song already exists — just adding a reference
}
public void removeSong(Song song) {
songs.remove(song); // Song continues to exist after removal
}
// Deleting a Playlist does not delete the Songs.
// The same Song can appear in many Playlists.
}
// COMPOSITION — owns parts, creates them, destroys them with itself:
public class House {
private final Room livingRoom; // Rooms created inside House
private final Room bedroom;
private final Room kitchen;
public House(String address) {
// Rooms are created BY House — they are owned parts:
this.livingRoom = new Room("Living Room", 25.0);
this.bedroom = new Room("Bedroom", 18.0);
this.kitchen = new Room("Kitchen", 12.0);
}
// When House is garbage-collected, all Room objects become
// unreachable (assuming no external references) and are GC'd too.
// The Rooms have no meaning or existence outside this House.
}
// ── Decision guide: ───────────────────────────────────────────────────
//
// Can the part exist without the whole?
// Yes → Association or Aggregation
// No → Composition
//
// Is the part created inside the whole's constructor?
// Yes → Composition (strong ownership)
// No → Aggregation (part passed in, weak ownership)
//
// Does the whole have a field storing the part?
// No → Association (temporary or method-scoped)
// Yes → Aggregation or CompositionRelated Topics in Object-Oriented Programming
OOP Concepts
Object-Oriented Programming (OOP) is a programming paradigm that organises software around objects — self-contained units that combine data (fields) and behaviour (methods). Java is a class-based, object-oriented language where almost everything is an object. OOP provides four foundational principles: encapsulation, inheritance, polymorphism, and abstraction. Together they produce software that is modular, reusable, maintainable, and easier to reason about as systems grow in complexity.
Classes
A class is the fundamental building block of Java. It is a blueprint that defines the structure and behaviour of objects — what data each object holds (fields) and what operations it can perform (methods). Every Java program is composed of classes. Understanding how to design a class well — choosing the right access modifiers, separating state from behaviour, and writing cohesive single-responsibility classes — is the foundation of object-oriented programming in Java. This entry covers class anatomy, fields, methods, access modifiers, static vs instance members, the this keyword, and class design principles.
Objects
An object is a runtime instance of a class. Where a class is a blueprint that exists in source code, an object is a living entity that exists in memory during program execution — it has its own identity, its own state stored in its fields, and the ability to respond to method calls. Every object has three fundamental properties: identity (a unique memory address), state (the current values of its fields), and behaviour (the methods it responds to). This entry covers object identity vs equality, the Object class hierarchy, object state and mutation, method calls, toString, equals and hashCode, and the object lifecycle.
Object Creation
Object creation in Java is the process of allocating memory, initialising fields, and running constructor logic to bring an object into existence. The new keyword is the primary mechanism, but Java also provides factory methods, builder patterns, copy constructors, and object cloning. Constructors are special methods that set up the initial state — their design determines how easy or difficult the class is to use correctly. This entry covers constructors in depth, constructor overloading and chaining, copy constructors, factory methods, the builder pattern, and the difference between shallow and deep copy.