Session 2 · Programming Technology

UML relationships: association, aggregation, composition, inheritance

The line ends of a UML class diagram and the Java each one means, read on a zoo and designed on a role-playing game where subclasses override one damage rule – and why that rule is better abstract than defaulted.

16 min readAssociation, aggregation, compositionMultiplicity as field typesAbstract classes and methodsOverriding and dynamic dispatchConstructor chaining

A program of ten classes is already too big to keep in your head as code. A UML class diagram is the map: a box per class, a line per relationship, and the shape of the line end says what kind of relationship it is. This session goes through those line ends – association, aggregation, composition, inheritance – and what each one means in Java. Then we read one diagram (a zoo) and design one (a role-playing game) where inheritance and method overriding do the real work.

Every line in a class diagram is a decision you will have to code: who holds a reference to whom, who creates whom, and which class decides a rule. If you can't draw the line, you don't know yet what to write.

Why draw before coding

The class diagram shows structure: the classes, their fields and methods, and how they are connected. It leaves out what the methods do. That is exactly the right level for the first design decisions – which classes exist, which one owns which data – and it is cheap to change. Moving a field from one box to another is an eraser stroke; moving it in finished code is an afternoon.

+ public
Visible to every class.
# protected
Visible to subclasses (and, in Java, to the same package).
- private
Visible only inside the class.
italic name
An abstract class or method – no objects / no body. I also write «abstract» above the name.
name : Type
A field or a return type: HP : int, isAlive() : boolean.

Five relationships, five line ends

All relationships between classes boil down to five kinds, and each has its own line end (Fig. 1). The first three are all "A has B" – they differ in how strongly A owns the B.

ABassociationclass A { B b; }A knows a B and can call its methodsABaggregationnew A(listOfBs)A has Bs made elsewhere – they can outlive AABcompositionparts.add(new B())A creates and owns its Bs – they go with AABinheritanceclass B extends Aevery B is an A, and can stand in for oneABdependencyvoid m(B b) { … }A uses a B briefly: a parameter or a local
Fig. 1 · The line end carries the meaning. A diamond always sits on the whole, a triangle always on the parent, an arrowhead on the class being used.

An association is the general case: the two classes know each other, nothing is said about ownership. A bank and its employees – the bank has a set of employees, the employees were hired, not created by the bank.

Association: a field, nothing more

class Bank {
    private String bankName;
    private Set<Employee> employees;     // Bank knows its employees …

    public void setEmployees(Set<Employee> employees) {
        this.employees = employees;      // … but did not create them
    }
}

Inheritance (generalization) is a different kind of relationship: not "has a" but "is a". A Lion is an Animal, so anywhere an Animal is expected, a Lion will do. A dependency is the weakest link: a class uses another one inside a method – a parameter, a local variable, a thrown exception – without keeping a reference.

Aggregation vs composition

Both are "whole–part" relationships, and students mix them up more than anything else on this page. The question that separates them is about lifetime: can the part live without the whole?

  • Aggregation (hollow diamond): the parts exist independently. A department has students, but the students were created elsewhere and handed to it; closing the department doesn't delete the students.
  • Composition (filled diamond): the whole creates and owns its parts. A company's departments make no sense without the company; nobody outside holds them.

Aggregation: the parts come from outside

// main creates the students – they exist on their own
List<Student> cseStudents = new ArrayList<>();
cseStudents.add(new Student("Parul", 1));
cseStudents.add(new Student("Sachin", 2));

// the department is handed parts that already exist
Department cse = new Department("CSE", cseStudents);

Composition: the whole creates its parts and guards them

class Company {
    private final List<Department> departments = new ArrayList<>();

    public void addDepartment(String name) {
        departments.add(new Department(name));   // the whole creates its parts
    }

    public List<Department> getDepartments() {
        return new ArrayList<>(departments);     // a copy: outsiders can't add or remove
    }
}

In Java nothing is ever "destroyed" by hand – an object disappears when nothing refers to it any more, and the garbage collector reclaims it. So in Java terms, composition means: the whole is the only one holding references to its parts (Fig. 2). That is why getDepartments() returns a copy of the list – handing out the real list would let anyone add or remove departments behind the company's back.

aggregation – Department and StudentsDepartmentremovedStudent"Parul"Student"Sachin"cseStudentsa list in maincomposition – Company and DepartmentsCompanyremovedDepartment"Engineering"Department"Finance"no other reference points at them –the garbage collector removes themthe parts survivethe parts go with the whole
Fig. 2 · Remove the whole and see what is still reachable. Aggregated parts have other references and survive; composed parts had only the whole.

Multiplicity becomes a field type

The numbers at the line ends say how many objects take part. They translate directly into the type of the field and into a rule the constructor has to keep.

MultiplicityMeaningIn Java
1exactly oneRestaurant restaurant – never null, set in every constructor
0..1one or nonea field that may be null – check before use
*any number, also zeroList<Animal> animals – may be empty
1..*at least onea list, and the constructor refuses an empty one

Reading a diagram: the zoo

The zoo example is a small "simulation": a zoo with animals and a restaurant, and a caretaker who looks after some of the animals (Fig. 3). Read it top-down: first the relationships around Animal, then the hierarchy below it.

Zoo+ step()Restaurant«abstract»Animal+ move()+ makeSound()1..*CareTakertakes care of1..*«abstract»Pet+ move()«abstract»WildAnimalCat+ makeSound()Dog+ move()+ makeSound()Hamster+ move()+ makeSound()Lion+ move()+ makeSound()Elephant+ move()+ makeSound()
Fig. 3 · The zoo. Composition for the restaurant (the zoo builds it), aggregation for the animals (they are passed in), association for the caretaker, and a two-level hierarchy below the abstract Animal.

Animal is abstract: there is no "just an animal" in the zoo, so it can't be instantiated. It gives every animal a default move() and declares makeSound() without a body – every concrete animal has to say how it sounds. Pet overrides move() ("moves lazily"), so a Cat, which doesn't override it, moves like a pet; a Dog overrides it again and runs.

Animal.java

public abstract class Animal {

    public void move() {                  // a default every animal inherits
        System.out.println("Animal moves");
    }

    public abstract void makeSound();     // no body: each concrete animal must say how
}

The payoff is in Zoo.step(): one loop over Animal references, and every object answers with its own behaviour. Adding a Giraffe later needs a new class and nothing else – step() doesn't change. This is polymorphism.

Zoo.java – one loop, many behaviours

public void step() {
    for (Animal a : animals) {   // static type: Animal
        a.move();                // runs Dog.move(), Lion.move(), Pet.move() …
        a.makeSound();           // abstract in Animal – every concrete class has one
    }
}

Designing a hierarchy: the role-playing game

The exercise of this session: in a role-playing game a character is the main character, an orc or a dragon. Orcs are fighters, berserkers or defenders; a dragon is red or black. Every character has a name, hit points (HP) and an attack value, and can attack another character, whose HP drops by the attacker's attack value – except that

  • a berserker loses twice the attack value, a defender only half,
  • a black dragon is hurt only if the attack is greater than 20, a red dragon only above 60,
  • the main character has a real-valued defense, and the attack is divided by it,
  • a character is alive exactly when its HP is 0 or more.

The design question is: which part of the behaviour differs per class? Not attacking – every character attacks the same way. Only the damage rule differs. So the common protocol lives in Character, and the varying step is one protected method, applyDamageFrom, that subclasses override (Fig. 4).

«abstract»Character# name : String# HP : int# attack : int+ attack(c : Character)+ attacked(c : Character)+ isAlive() : boolean# applyDamageFrom(c : Character)MainCharacter# defense : double# applyDamageFrom(…)«abstract»Orc«abstract»Dragon# ATTACK_THRESHOLD : int# applyDamageFrom(…)FighterBerserker# applyDamageFrom(…)Defender# applyDamageFrom(…)RedDragonBlackDragonthreshold 60threshold 20
Fig. 4 · One method varies, so one method is overridden. Orc and Dragon are abstract groupings; Dragon also holds the rule shared by both colours, and the colours only supply the threshold.

Character.java – the fixed protocol and the variable step

public void attack(Character character) {
    if (isAlive()) {
        character.attacked(this);          // "I hit you"
    }
}

public void attacked(Character character) {
    if (isAlive()) {
        applyDamageFrom(character);        // the one step that differs per class
    }
}

protected void applyDamageFrom(Character character) {
    HP -= character.attack;                // the default rule
}

Follow one hit through the objects (Fig. 5). main only knows it has two Characters. Which applyDamageFrom runs is decided at run time by the class of the object that receives the call – this is dynamic dispatch.

Game.mainranger : MainCharacterorc1 : Berserkerattack(orc1)if alive:attacked(ranger)if alive:applyDamageFrom(ranger)Berserker's override runs: HP −= attack × 2The variable's type is Character;the object's class picks the method.
Fig. 5 · attack → attacked → applyDamageFrom. The first two methods are the same for everyone; the third is the hook each subclass fills in.

Two overrides

// Berserker: double damage
@Override
protected void applyDamageFrom(Character character) {
    HP -= character.attack * 2;
}

// Dragon: hurt only above its threshold (20 for black, 60 for red)
@Override
protected void applyDamageFrom(Character character) {
    if (character.attack > ATTACK_THRESHOLD) {
        HP -= character.attack;
    }
}

The dragons show a second trick. The rule "hurt only above a threshold" is the same for both colours, so it is written once, in Dragon. The colours differ only in a number, and a number is data, not behaviour – so RedDragon and BlackDragon just pass it to the parent constructor.

Dragon.java, RedDragon.java

public abstract class Dragon extends Character {

    protected final int ATTACK_THRESHOLD;

    public Dragon(String name, int HP, int attack, int threshold) {
        super(name, HP, attack);           // the Character part first
        this.ATTACK_THRESHOLD = threshold;
    }
}

public class RedDragon extends Dragon {
    public RedDragon(String name, int HP, int attack) {
        super(name, HP, attack, 60);       // the subclass only fixes the number
    }
}

Abstract methods: no silent defaults

In the first version, Character has a default damage rule (HP -= attack), and Fighter uses it without saying so. The second version, rpgame_abstract, makes applyDamageFrom abstract: there is no default, so every concrete class must state its rule, and forgetting one is a compile error instead of a silent bug (Fig. 6).

rpgame – a default in the parent«abstract»Character# applyDamageFrom(c)Fighterinherits HP −= attack, silentlyrpgame_abstract – no default«abstract»Character# applyDamageFrom(c) {abstract}Fighter# applyDamageFrom(c)must state its rule, or no compile
Fig. 6 · A default body is convenient; an abstract method is explicit. Choose abstract when there is no sensible rule that fits “any” subclass.

rpgame_abstract – Character.java

public abstract class Character {
    // …
    private void attacked(Character character) {         // private: only attack() may call it
        if (isAlive()) {
            applyDamageFrom(character);
        }
    }

    protected abstract void applyDamageFrom(Character character);   // no default any more
}

The same version also makes attacked private. It was public before, so main could call orc.attacked(dragon) and deal damage without an attack ever happening. Now the only way in is attack(), which checks that the attacker is alive. Make public only what other classes are supposed to call.

Practice

Implement the role-playing game from the description above, step by step.

Task 1

Draw the class diagram first

On paper or in any UML tool, draw the classes, their fields and methods, and the inheritance lines. Mark which classes are abstract and which method varies.

Where to start

Find the nouns that are "kinds of" something else (orc, dragon, berserker …) – those are the hierarchy. Then find the one sentence of the task that says something differs per kind – that is the method to override.

Task 2

The Character base class

Write the abstract Character with name, HP, attack, a constructor, attack, attacked, isAlive and the protected damage method.

Keep the protocol in one place

attack(other) calls other.attacked(this); attacked calls applyDamageFrom. Only the last one will ever be overridden. Make the fields protected so subclasses can change HP.

Task 3

The subclasses

Add MainCharacter, the abstract Orc with Fighter, Berserker, Defender, and the abstract Dragon with RedDragon and BlackDragon.

The main character's defense

Defense is a double. Dividing an int by a double gives a double, and -= quietly converts the result back to int – fine here, but know it happens.

@Override
protected void applyDamageFrom(Character character) {
    HP -= character.attack / defense;   // defense is a double
}
The dragons

Put the threshold check in Dragon. Each colour only calls super(…) with its own number.

Task 4

The melee

In main, put several characters into one list. Pick a random attacker and a different random target, let them fight, and remove whoever dies. Repeat until one character is left and print the winner. Use Random.nextInt.

Loop skeleton
Random rand = new Random();
while (characters.size() > 1) {
    int attackerInd = rand.nextInt(characters.size());
    int attackedInd = rand.nextInt(characters.size());
    // … pick again while the two indexes are equal
    // … attack, and remove the attacked one if it died
}

Remove by index, after the attack. The list's static type is List<Character> – this is the zoo's polymorphic loop again.

Watch the name clash

java.lang.Character is imported automatically. Your own Character class in the same package wins, but the clash is confusing in error messages – GameCharacter is a fine alternative name.

Task 5

Make the rule abstract

Change applyDamageFrom to abstract in Character and make attacked private. Fix every compile error that follows. Which classes needed a change, and why exactly those?

What to expect

Only the concrete classes that relied on the default break: Fighter (and nobody else, because Dragon already supplies the rule for both colours). The abstract Orc may stay without one.

Task 6

Fix the zoo

Make every Zoo have its restaurant regardless of the constructor, and make CareTaker's list private. Then add a new animal type without touching Zoo.

Approach

One constructor does the work, the other calls it with this(…). The new animal extends Pet or WildAnimal and overrides makeSound() – the compiler insists.

Summary

  • Association: A has a field of type B. Aggregation (hollow diamond): A holds parts made elsewhere. Composition (filled diamond): A creates and solely owns its parts. Inheritance (triangle): B is an A. Dependency (dashed arrow): A uses B in a method.
  • In Java, composition means the whole is the only holder of references to its parts – create them inside, and hand out copies.
  • Multiplicities become field types and constructor rules: 1 is never null, * is a possibly empty list, 1..* is a list the constructor guards.
  • Put the common protocol in the base class and let subclasses override one protected method; dynamic dispatch picks the right one at run time.
  • Make a method abstract when there is no sensible default – forgetting it becomes a compile error, not a bug.