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.

The three relationships

RelationshipMeaningLifetimeExample
AssociationOne type simply uses anotherIndependentA teacher and a student
AggregationA has-a where the part can exist aloneIndependentA department and its employees
CompositionA strong has-a where the part belongs to the wholeThe part dies with the wholeAn 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

AspectInheritanceComposition
Relationshipis-ahas-a
Fixed atCompile timeCan change at runtime
CouplingTight; subclass depends on superclass internalsLoose; depends only on a public API
Exposed APIEverything inheritedOnly what is delegated
Multiple sourcesOne superclassAny number of components
TestingSuperclass behaviour comes alongComponents 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

  1. Decide association, aggregation or composition for: a house and its rooms, a library and its books, a driver and a car.
  2. Rewrite a class that extends ArrayList so that it composes one instead, and list the methods no longer exposed.
  3. Write a wrapper that adds timing around any Storage implementation.
  4. Why does exposing the internal list of an Order weaken composition?
  5. 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.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.