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.
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.
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.
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.
| Multiplicity | Meaning | In Java |
|---|---|---|
| 1 | exactly one | Restaurant restaurant – never null, set in every constructor |
| 0..1 | one or none | a field that may be null – check before use |
| * | any number, also zero | List<Animal> animals – may be empty |
| 1..* | at least one | a 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.
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).
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.
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_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.