Java Aggregation and Composition: Understanding Object Relationships

Learning Objectives

By the end of this post, you will understand the fundamental differences between aggregation and composition in Java, recognize when to apply each relationship type in object-oriented design, and implement both patterns effectively in your code.

Introduction

Object-oriented programming relies heavily on how classes relate to one another. Two fundamental relationship types—aggregation and composition—define how objects interact and depend on each other within a system. While both represent "has-a" relationships, they differ significantly in terms of lifecycle management and ownership semantics. Understanding these distinctions enables developers to create more maintainable and logically sound class hierarchies.

Aggregation in Java

Aggregation represents a relationship where one class contains references to objects of another class, yet these contained objects maintain independent existence. The container and the component exist in a loosely coupled arrangement—the component's lifecycle extends beyond that of the container.

Consider a library system where books exist as discrete entities:

package academy.javapro;

import java.util.ArrayList;
import java.util.List;

class Book {
    private String title;
    private String author;
    private String isbn;

    public Book(String title, String author, String isbn) {
        this.title = title;
        this.author = author;
        this.isbn = isbn;
    }

    public String getTitle() {
        return title;
    }

    public String getAuthor() {
        return author;
    }

    public String getIsbn() {
        return isbn;
    }
}

class Library {
    private String name;
    private List<Book> books;

    public Library(String name) {
        this.name = name;
        this.books = new ArrayList<>();
    }

    public void addBook(Book book) {
        books.add(book);
    }

    public void removeBook(Book book) {
        books.remove(book);
    }

    public String getName() {
        return name;
    }

    public List<Book> getBooks() {
        return books;
    }
}

public class AggregationExample {
    public static void main(String[] args) {
        Book book1 = new Book("Effective Java", "Joshua Bloch", "978-0134685991");
        Book book2 = new Book("Clean Code", "Robert Martin", "978-0132350884");

        Library cityLibrary = new Library("City Central Library");
        Library schoolLibrary = new Library("School Library");

        cityLibrary.addBook(book1);
        cityLibrary.addBook(book2);
        schoolLibrary.addBook(book1);

        System.out.println(cityLibrary.getName() + " has " +
                cityLibrary.getBooks().size() + " books");
        System.out.println(schoolLibrary.getName() + " has " +
                schoolLibrary.getBooks().size() + " book");
        System.out.println("Book '" + book1.getTitle() + "' exists in multiple libraries");
    }
}

The books in this example can be shared across multiple libraries or exist independently. Destroying the library object does not eliminate the book objects from memory if other references to them exist.

Composition in Java

Composition establishes a stronger relationship where the whole exclusively owns its parts. The contained objects cannot exist independently—their lifecycles are intrinsically tied to the container. When the container ceases to exist, all its constituent parts are destroyed as well.

A car and its engine components demonstrate this relationship:

package academy.javapro;

import java.util.ArrayList;
import java.util.List;

class Engine {
    private String type;
    private int horsepower;

    public Engine(String type, int horsepower) {
        this.type = type;
        this.horsepower = horsepower;
    }

    public String getType() {
        return type;
    }

    public int getHorsepower() {
        return horsepower;
    }
}

class Transmission {
    private String type;
    private int gears;

    public Transmission(String type, int gears) {
        this.type = type;
        this.gears = gears;
    }

    public String getType() {
        return type;
    }

    public int getGears() {
        return gears;
    }
}

class Car {
    private String model;
    private Engine engine;
    private Transmission transmission;

    public Car(String model) {
        this.model = model;
        this.engine = new Engine("V6", 280);
        this.transmission = new Transmission("Automatic", 8);
    }

    public String getModel() {
        return model;
    }

    public Engine getEngine() {
        return engine;
    }

    public Transmission getTransmission() {
        return transmission;
    }

    public void displaySpecs() {
        System.out.println("Model: " + model);
        System.out.println("Engine: " + engine.getType() + " - " +
                engine.getHorsepower() + " HP");
        System.out.println("Transmission: " + transmission.getType() + " - " +
                transmission.getGears() + " gears");
    }
}

public class CompositionExample {
    public static void main(String[] args) {
        Car car = new Car("Sedan XL");

        System.out.println("Car Specifications:");
        car.displaySpecs();

        System.out.println("\nThe engine and transmission are integral parts of this car");
        System.out.println("They cannot exist independently outside of the car object");
    }
}

The engine and transmission are instantiated within the Car constructor and remain bound to that specific car instance. No external reference to these components exists, ensuring they are removed from memory when the car object is garbage collected.

Key Differences Between Aggregation and Composition

The distinction between these two relationship types manifests in several critical aspects:

  • Relationship strength: Aggregation establishes a weak, flexible connection where components maintain autonomy. Composition creates a rigid, dependent structure where parts exist solely as elements of the whole.

  • Lifecycle management: Aggregated objects persist beyond the container's destruction. Composed objects are eliminated when their container is destroyed.

  • Ownership semantics: Aggregation involves no exclusive ownership—multiple containers can reference the same component. Composition enforces strict ownership where the whole class exclusively controls its parts.

  • Independence: Aggregated components function as standalone entities that may participate in multiple relationships. Composed parts lack independent meaning outside their containing object.

  • Memory implications: In aggregation, destroying the container does not affect component objects if other references exist. In composition, the container's destruction guarantees the elimination of all constituent parts.

The choice between aggregation and composition depends on the logical relationship between entities in the domain model. Use aggregation when components have independent significance and may be shared. Use composition when parts are intrinsic to the whole and have no meaningful existence outside the container.

Summary

Aggregation and composition represent distinct approaches to modeling object relationships in Java. Aggregation suits scenarios where components maintain independent existence and may be shared across multiple containers, exemplified by books within libraries. Composition applies when parts are inseparable from the whole, as demonstrated by engine and transmission components within a car. Recognizing the appropriate relationship type for each design scenario leads to code that accurately reflects real-world semantics and maintains clearer ownership boundaries.

Complete Core Java Programming Course

Master Java from scratch to advanced! This comprehensive bootcamp covers everything from your first line of code to building complex applications, with expert guidance on collections, multithreading, exception handling, and more.

Leave a Comment