Skip to content
Java inheritance 6 min read

Aggregation (HAS-A)

Aggregation is a way of building objects out of other objects — one class HAS-A reference to another. Where inheritance says “a Dog IS-A Animal”, aggregation says “a Car HAS-A Engine”. It is one of the most practical tools in object-oriented design, and understanding when to use it (instead of inheritance) will make your code significantly more flexible.

Aggregation heap memory reference layout

The HAS-A Relationship

Aggregation describes a whole-part or uses-a relationship between two classes. The outer (containing) class holds a reference to the inner (contained) class, but neither class is a specialisation of the other.

Classic examples:

Outer classInner classRelationship
CarEngineCar HAS-A Engine
LibraryBookLibrary HAS-A Book
EmployeeAddressEmployee HAS-A Address
OrderProductOrder HAS-A Product

The key question to ask yourself is: can the inner object exist independently of the outer one? If yes (an Address can exist without an Employee), the relationship is aggregation. If the inner object cannot survive on its own (a Room rarely makes sense without a House), it leans toward composition — a stricter form of aggregation.

Note: Java does not have separate keywords for aggregation vs composition. Both are implemented as a reference field. The distinction is a design concept, not a language construct.

A Simple Aggregation Example

Here an Employee HAS-A Address. Both classes can exist independently — you can create an Address object and later assign it to multiple employees.

class Address {
    String street;
    String city;
    String country;

    Address(String street, String city, String country) {
        this.street = street;
        this.city   = city;
        this.country = country;
    }

    @Override
    public String toString() {
        return street + ", " + city + ", " + country;
    }
}

class Employee {
    String name;
    int    id;
    Address address;   // HAS-A relationship

    Employee(String name, int id, Address address) {
        this.name    = name;
        this.id      = id;
        this.address = address;
    }

    void display() {
        System.out.println("ID   : " + id);
        System.out.println("Name : " + name);
        System.out.println("Addr : " + address);
    }
}

public class Main {
    public static void main(String[] args) {
        Address addr = new Address("12 Oak Street", "Toronto", "Canada");
        Employee emp = new Employee("Priya Sharma", 101, addr);
        emp.display();
    }
}

Output:

ID   : 101
Name : Priya Sharma
Addr : 12 Oak Street, Toronto, Canada

The Address object is created independently and then passed in. Nothing stops you from assigning the same addr to a second Employee — the objects are loosely coupled.

Aggregation vs Inheritance

This is one of the most important design decisions you will make. A popular guideline is: “Favour composition over inheritance” (from the Gang of Four design patterns book).

AspectInheritance (IS-A)Aggregation (HAS-A)
Relationship typeSpecialisationOwnership / usage
KeywordextendsNone — just a field
CouplingTight (subclass depends on superclass internals)Loose (outer class depends only on public API)
FlexibilityLow — locked at compile timeHigh — reference can be swapped at runtime
Code reuseImplicit (fields/methods inherited)Explicit (delegation)
ExampleDog extends AnimalCar has an Engine field

Tip: If you can say “B IS-A A” and it makes real-world sense in every context, use inheritance. If you find yourself forcing an IS-A that doesn’t hold universally, switch to aggregation.

Aggregation with Multiple Parts

A single class can hold references to several others — this is both natural and encouraged.

class Engine {
    int horsepower;
    Engine(int hp) { this.horsepower = hp; }
    void start()   { System.out.println("Engine started (" + horsepower + " hp)"); }
}

class Tyre {
    String brand;
    Tyre(String brand) { this.brand = brand; }
    void describe()    { System.out.println("Tyre brand: " + brand); }
}

class Car {
    String model;
    Engine engine;  // HAS-A Engine
    Tyre[] tyres;   // HAS-A four Tyres

    Car(String model, Engine engine, Tyre[] tyres) {
        this.model  = model;
        this.engine = engine;
        this.tyres  = tyres;
    }

    void assemble() {
        System.out.println("Assembling: " + model);
        engine.start();
        for (Tyre t : tyres) t.describe();
    }
}

public class Main {
    public static void main(String[] args) {
        Engine v8     = new Engine(450);
        Tyre[] wheels = {
            new Tyre("Michelin"), new Tyre("Michelin"),
            new Tyre("Michelin"), new Tyre("Michelin")
        };
        Car car = new Car("Mustang GT", v8, wheels);
        car.assemble();
    }
}

Output:

Assembling: Mustang GT
Engine started (450 hp)
Tyre brand: Michelin
Tyre brand: Michelin
Tyre brand: Michelin
Tyre brand: Michelin

Replacing Behaviour at Runtime

Because aggregation stores a reference, you can swap the contained object at runtime — something impossible with inheritance. This pattern underpins the Strategy and Dependency Injection design principles.

interface PaymentStrategy {
    void pay(double amount);
}

class CreditCard implements PaymentStrategy {
    public void pay(double amount) {
        System.out.printf("Paid %.2f via Credit Card%n", amount);
    }
}

class UPI implements PaymentStrategy {
    public void pay(double amount) {
        System.out.printf("Paid %.2f via UPI%n", amount);
    }
}

class ShoppingCart {
    private PaymentStrategy payment;  // HAS-A PaymentStrategy

    // Swap strategy at any time
    void setPayment(PaymentStrategy payment) {
        this.payment = payment;
    }

    void checkout(double total) {
        payment.pay(total);
    }
}

public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();

        cart.setPayment(new CreditCard());
        cart.checkout(1299.99);

        cart.setPayment(new UPI());
        cart.checkout(499.00);
    }
}

Output:

Paid 1299.99 via Credit Card
Paid 499.00 via UPI

Changing payment behaviour required zero changes to ShoppingCart — that is the power of aggregation combined with interfaces.

Under the Hood

Memory layout

When you write Address address; inside Employee, the JVM allocates a reference slot (4 or 8 bytes, depending on the JVM’s compressed-oops setting) inside the Employee object on the heap. The Address object itself lives at its own separate heap location.

Heap
├── Employee object  ──address ref──► Address object
│   [id: 101]                         [street: "12 Oak Street"]
│   [name ref]                        [city: "Toronto"]
│   [address ref] ─────────────────►  [country: "Canada"]

Because both objects are independently allocated, the garbage collector treats them separately. The Address remains reachable as long as at least one live reference points to it — even after the Employee is collected. This is fundamentally different from value embedding in languages like C++.

No bytecode magic

Aggregation compiles to ordinary getfield / putfield bytecode instructions. There is nothing special about it at the JVM level; it is simply a field that holds an object reference. The JIT compiler can inline short delegating methods, so calling emp.address.getCity() is often as fast as a direct field read after optimisation.

Warning: Avoid creating deeply nested aggregation chains (e.g., a.b.c.d.method()). This violates the Law of Demeter (“talk to your immediate friends only”), makes code harder to test, and increases coupling.

Common Mistakes

  • Using inheritance when aggregation fits better. Ask “IS-A” strictly. A Logger is not a HashMap, even if it stores entries — it HAS-A map.
  • Null references. If the contained object is optional, guard with a null check or use Optional to make the absence explicit.
  • Circular references. If A holds a reference to B and B holds a reference back to A, be careful with toString(), equals(), and serialization — they can loop infinitely.
  • Inheritance — understand IS-A before choosing between it and HAS-A
  • Types of Inheritance — single, multilevel, hierarchical, and why Java avoids multiple class inheritance
  • Interfaces — combine aggregation with interfaces for maximum flexibility
  • Polymorphism — how swapping aggregated objects enables runtime polymorphism
  • Classes & Objects — the building blocks you compose in aggregation
  • Design Patterns — Strategy, Decorator, and many others are built on aggregation
Last updated June 13, 2026
Was this helpful?