OOP Class Relationships: Association, Aggregation, Composition & Inheritance
Individual objects are useful, but OOP truly shines when objects work together to solve complex problems. This guide explains class structure and the various types of relationships between classes—essential preparation for IHK certification exams.
1. Class Components (The Inner Structure)
A class is like a blueprint. It consists of several defined components:
1. Class Name
The unique identifier for the class (e.g., Customer, BankAccount). By convention, it starts with a capital letter.
2. Attributes (Data Fields)
Variables inside the class that describe the state of a future object.
- Each attribute has a name (e.g.,
balance) and a data type (e.g.,double,String) - They’re typically declared
private(see encapsulation)
3. Constructor
A special method that runs automatically when you create a new object of the class using new.
Purpose: Initializes the new object by assigning initial values to its attributes.
- The constructor has the same name as the class and no return type (not even
void) - You can define multiple constructors (overloading)—for example, a default constructor with no parameters and another that initializes all attributes
// Constructor example
public class BankAccount {
private double balance;
private String holder;
// Constructor
public BankAccount(String holderName, double initialBalance) {
holder = holderName;
balance = initialBalance;
}
// Default constructor
public BankAccount() {
this("Unknown", 0.0);
}
// Overloaded constructor
public BankAccount(String holderName) {
this(holderName, 0.0);
}
}
// Creating objects:
BankAccount myAccount = new BankAccount("Max Mustermann", 1000.0);
BankAccount emptyAccount = new BankAccount();
4. Methods (Member Functions)
Define the behavior of objects. They operate on attributes.
- Getters/Setters: Public methods that provide controlled access to read and write
privateattributes - Business Methods: Implement the core functionality (e.g.,
deposit(double amount),withdraw(double amount))
5. Access Modifiers (Visibility Modifiers)
Control where class members can be accessed from. Critical for encapsulation.
private: Visible only within the class itselfprotected: Visible within the class and its subclasses (inheritance)public: Visible everywhere- package-private/default: Visible only within the same package/namespace
2. Class Relationships: How Classes Connect
Classes rarely exist in isolation. They interact with one another. These relationships are modeled in UML class diagrams.
a) Association
Description: The most general relationship. It describes a semantic connection between two independent classes. One class “knows about” another.
UML notation: Solid line.
Example: A Professor teaches a Student. The professor and student exist independently. The relationship is often bidirectional (both “know” each other).
Implementation: Usually through reference attributes.
// Association example
public class Professor {
private String name;
private String department;
private List<Student> students; // Professor IS ASSOCIATED WITH Student
public Professor(String name, String department) {
this.name = name;
this.department = department;
this.students = new ArrayList<>();
}
public void addStudent(Student student) {
students.add(student);
student.addProfessor(this); // Bidirectional relationship
}
public void teach() {
System.out.println("Professor " + name + " teaches " + students.size() + " students");
}
// Getter
public String getName() { return name; }
public List<Student> getStudents() { return new ArrayList<>(students); }
}
public class Student {
private String name;
private int matriculationNumber;
private List<Professor> professors; // Student IS ASSOCIATED WITH Professor
public Student(String name, int matriculationNumber) {
this.name = name;
this.matriculationNumber = matriculationNumber;
this.professors = new ArrayList<>();
}
public void addProfessor(Professor professor) {
professors.add(professor);
}
public void study() {
System.out.println("Student " + name + " studies with " + professors.size() + " professors");
}
// Getter
public String getName() { return name; }
public List<Professor> getProfessors() { return new ArrayList<>(professors); }
}
b) Aggregation
Description: A more specific form of association. It represents a “part-of” or “has-a” relationship where the parts can exist independently of the whole. It’s a loose coupling.
UML notation: Solid line with an unfilled diamond on the side of the whole.
Example: A Department has Employees. When the department closes, employees aren’t deleted—they can be reassigned to another department. The part (employee) exists independently of the whole (department).
Implementation: Like association, but the lifetimes of objects are decoupled.
// Aggregation example
public class Department {
private String name;
private String location;
private List<Employee> employees; // Department HAS Employees (aggregation)
public Department(String name, String location) {
this.name = name;
this.location = location;
this.employees = new ArrayList<>();
}
public void addEmployee(Employee employee) {
this.employees.add(employee);
employee.setDepartment(this);
}
public void removeEmployee(Employee employee) {
this.employees.remove(employee);
employee.setDepartment(null); // Employee continues to exist!
}
public void dissolve() {
// Employees are not deleted, just removed from the department
for (Employee e : employees) {
e.setDepartment(null);
}
employees.clear();
System.out.println("Department " + name + " dissolved, employees continue to exist");
}
// Getter
public String getName() { return name; }
public List<Employee> getEmployees() { return new ArrayList<>(employees); }
}
public class Employee {
private String name;
private String position;
private Department department; // Employee can exist without a department
public Employee(String name, String position) {
this.name = name;
this.position = position;
this.department = null; // Employee can be created without a department
}
public void setDepartment(Department department) {
this.department = department;
}
public void work() {
String departmentName = department != null ? department.getName() : "no department";
System.out.println(name + " works as " + position + " in " + departmentName);
}
// Getter
public String getName() { return name; }
public Department getDepartment() { return department; }
}
c) Composition
Description: An even stronger form of the “is-part-of” relationship. The parts cannot exist without the whole. The whole is responsible for the lifetime of its parts. Strict ownership rules apply.
UML representation: Solid line with a filled diamond on the side of the whole.
Example: A Car is composed of an Engine. The engine has no independent existence without the car. When the car is scrapped, the engine is destroyed as well. Their lifetimes are tightly coupled.
Implementation: The whole creates its parts directly in its constructor.
// Composition example
public class Auto {
private String marke;
private String modell;
private Motor motor; // Auto COMPOSED OF Motor
private List<Rad> raeder; // Auto COMPOSED OF Wheels
public Auto(String marke, String modell) {
this.marke = marke;
this.modell = modell;
// Parts are created with the car (strict lifetime coupling)
this.motor = new Motor(2.0, 150); // Engine created with car
this.raeder = new ArrayList<>();
// 4 wheels are created
for (int i = 0; i < 4; i++) {
raeder.add(new Rad("225/45R17"));
}
}
public void starten() {
motor.starten();
System.out.println(marke + " " + modell + " wird gestartet");
}
public void verschrotten() {
// All parts are destroyed when the car is scrapped
motor.zerstoeren();
for (Rad rad : raeder) {
rad.zerstoeren();
}
raeder.clear();
System.out.println("Auto und alle Teile wurden verschrottet");
}
// Getter
public String getMarke() { return marke; }
public Motor getMotor() { return motor; }
}
public class Motor {
private double hubraum;
private int leistungPS;
private boolean laeuft;
public Motor(double hubraum, int leistungPS) {
this.hubraum = hubraum;
this.leistungPS = leistungPS;
this.laeuft = false;
}
public void starten() {
this.laeuft = true;
System.out.println("Motor (" + hubraum + "L, " + leistungPS + "PS) gestartet");
}
public void zerstoeren() {
System.out.println("Motor zerstört");
}
// Getter
public double getHubraum() { return hubraum; }
public int getLeistungPS() { return leistungPS; }
}
public class Rad {
private String groesse;
private double druck;
public Rad(String groesse) {
this.groesse = groesse;
this.druck = 2.5;
}
public void zerstoeren() {
System.out.println("Rad (" + groesse + ") zerstört");
}
// Getter
public String getGroesse() { return groesse; }
}
d) Generalization & Specialization (Inheritance)
Description: This is the “is-a” relationship and is implemented through inheritance.
Generalization: Extracting common characteristics from multiple classes into a more general parent class (e.g., Dog, Cat → Animal).
Specialization: Deriving a more specific subclass from a more general parent class. The subclass inherits all properties and refines or extends them (e.g., Animal → Dog; the dog adds a bark() method).
UML representation: Solid line with a hollow arrow pointing from the subclass to the parent class.
Example: Manager is an Employee. It inherits all attributes (name, salary) and may add an attribute like bonus.
// Inheritance example
public class Mitarbeiter {
protected String name;
protected double grundgehalt;
protected int mitarbeiterId;
private static int naechsteId = 1;
public Mitarbeiter(String name, double grundgehalt) {
this.name = name;
this.grundgehalt = grundgehalt;
this.mitarbeiterId = naechsteId++;
}
public void arbeiten() {
System.out.println(name + " arbeitet für " + grundgehalt + "€ Grundgehalt");
}
public double berechneGehalt() {
return grundgehalt;
}
// Getter
public String getName() { return name; }
public int getMitarbeiterId() { return mitarbeiterId; }
}
public class Manager extends Mitarbeiter {
private double bonus;
private List<Mitarbeiter> team;
public Manager(String name, double grundgehalt, double bonus) {
super(name, grundgehalt); // Call parent class constructor
this.bonus = bonus;
this.team = new ArrayList<>();
}
public void addTeamMitglied(Mitarbeiter mitarbeiter) {
team.add(mitarbeiter);
}
@Override
public void arbeiten() {
System.out.println(name + " manages Team von " + team.size() + " Mitarbeitern");
}
@Override
public double berechneGehalt() {
return grundgehalt + bonus; // Additional calculation
}
public void teamMeeting() {
System.out.println(name + " führt Team-Meeting durch");
for (Mitarbeiter m : team) {
System.out.println("- " + m.getName());
}
}
// Getter
public double getBonus() { return bonus; }
public List<Mitarbeiter> getTeam() { return new ArrayList<>(team); }
}
public class Entwickler extends Mitarbeiter {
private List<String> programmiersprachen;
public Entwickler(String name, double grundgehalt, List<String> sprachen) {
super(name, grundgehalt);
this.programmiersprachen = new ArrayList<>(sprachen);
}
@Override
public void arbeiten() {
System.out.println(name + " programmiert in " + programmiersprachen);
}
public void lerneNeueSprache(String sprache) {
programmiersprachen.add(sprache);
System.out.println(name + " lernt " + sprache);
}
// Getter
public List<String> getProgrammiersprachen() { return new ArrayList<>(programmiersprachen); }
}
3. Static vs. Non-Static Methods and Attributes
This distinction is fundamental and determines whether a feature belongs to the class or to the object.
| Feature | Non-Static (Instance Member) | Static (Class Member) |
|---|---|---|
| Belongs to | The individual object (instance) | The class itself |
| Memory copies | One copy per object. 100 objects = 100 copies of the attribute | Only one copy for the entire class. Shared by all objects |
| Access via | The object: objectName.methodName() | The class name: ClassName.methodName() |
| Can access | Both non-static and static members | Only static members. No access to non-static members (which object would be meant?) |
| Typical use | Attributes that differ from object to object (e.g., balance, name) | Utility methods (e.g., Math.sqrt()), constants (e.g., Math.PI), counters (e.g., totalObjectsCreated) |
// Example of Static vs. Non-Static
public class Student {
// Non-static attribute (one per object)
private String name;
private int matrikelNr;
// Static attribute (one for the class)
private static int anzahlStudenten = 0;
private static final String UNIVERSITAET = "Technische Universität";
public Student(String name, int matrikelNr) {
this.name = name;
this.matrikelNr = matrikelNr;
anzahlStudenten++; // Access static attribute
}
// Non-static method
public void studieren() {
System.out.println(name + " studiert an " + UNIVERSITAET);
System.out.println("Aktuelle Anzahl Studenten: " + anzahlStudenten);
}
// Static method
public static int getAnzahlStudenten() {
return anzahlStudenten;
// return name; // ERROR: name is non-static and inaccessible!
}
// Static utility method
public static boolean istGueltigeMatrikelNr(int nummer) {
return nummer >= 100000 && nummer <= 999999;
}
// Getter and setter
public String getName() { return name; }
public int getMatrikelNr() { return matrikelNr; }
public static String getUniversitaet() { return UNIVERSITAET; }
}
// Usage
public class Main {
public static void main(String[] args) {
// Create objects
Student stud1 = new Student("Max Mustermann", 123456);
Student stud2 = new Student("Erika Mustermann", 234567);
// Call non-static methods on objects
stud1.studieren();
stud2.studieren();
// Call static methods on class
System.out.println("Anzahl Studenten: " + Student.getAnzahlStudenten());
System.out.println("Universität: " + Student.getUniversitaet());
// Static utility method
boolean gueltig = Student.istGueltigeMatrikelNr(123456);
System.out.println("Matrikelnummer gültig: " + gueltig);
// Access non-static attributes
System.out.println("Student 1 Name: " + stud1.getName());
// ERROR: Static method cannot access object
// Student.getName(); // Compiler error!
}
}
4. Generic Classes (Generics) — for example List<T>
What’s the Problem?
Before generics, collections like ArrayList were defined for the Object type. You could insert anything (strings, integers, whatever). When retrieving elements, you had to tediously cast and check the type (String s = (String) myList.get(0);). Runtime errors were common.
What’s the Solution?
Generic classes. They’re class templates that use one or more type placeholders (typically T for “Type”, E for “Element”).
Purpose: compile-time type safety. The compiler checks that only objects of the correct type are inserted. Casting becomes unnecessary, and runtime errors are prevented.
// Example of Generics
// Without Generics (deprecated, error-prone)
List myOldList = new ArrayList();
myOldList.add("Hello");
myOldList.add(123); // Compiler says nothing, but...
String s = (String) myOldList.get(1); // Runtime error: ClassCastException!
// With Generics (type-safe)
List<String> myList = new ArrayList<>(); // T becomes String
myList.add("Hello");
// myList.add(123); // COMPILER ERROR: 123 is not a String!
String s = myList.get(0); // No casting needed, safe.
// Custom generic class
public class Box<T> {
private T inhalt;
public void setInhalt(T inhalt) {
this.inhalt = inhalt;
}
public T getInhalt() {
return inhalt;
}
public boolean istLeer() {
return inhalt == null;
}
}
// Using the generic class
public class GenericsBeispiel {
public static void main(String[] args) {
// Box for Strings
Box<String> stringBox = new Box<>();
stringBox.setInhalt("Hello World");
String inhalt = stringBox.getInhalt(); // No casting needed
// Box for Integer
Box<Integer> integerBox = new Box<>();
integerBox.setInhalt(42);
Integer zahl = integerBox.getInhalt(); // No casting needed
// Box for custom objects
Box<Student> studentBox = new Box<>();
studentBox.setInhalt(new Student("Max", 123456));
Student student = studentBox.getInhalt();
System.out.println("String Box: " + inhalt);
System.out.println("Integer Box: " + zahl);
System.out.println("Student Box: " + student.getName());
}
}
// Generic methods
public class Utility {
// Generic method for swapping
public static <T> void tausche(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Generic method for finding maximum
public static <T extends Comparable<T>> T maximum(T x, T y, T z) {
T max = x;
if (y.compareTo(max) > 0) max = y;
if (z.compareTo(max) > 0) max = z;
return max;
}
}
5. Benefits of Generic Containers (Templates in C++) Over Arrays
| Feature | Arrays | Generic Containers (e.g. ArrayList<T>, List<T>) |
|---|---|---|
| Size | Fixed. The size must be defined at creation and cannot be changed later | Dynamic/Growing. The size automatically adjusts to the number of elements |
| Type Safety | Provide basic type safety but can only store a single fixed type | Offer full compile-time type safety through generics |
| Functionality | Highly limited. Only basic operations (read, write at index) | Provide many useful methods: .add(), .remove(), .contains(), .size(), etc. |
| Performance | Very fast for direct index access | Slightly slower due to dynamic management overhead, but negligible in most cases |
| Flexibility | Low | Very high. Different containers exist for different purposes (lists, sets, maps, queues) |
// Comparison: Array vs. ArrayList
public class ArrayVsContainer {
public static void main(String[] args) {
// Array - fixed size
String[] namenArray = new String[3];
namenArray[0] = "Alice";
namenArray[1] = "Bob";
namenArray[2] = "Charlie";
// namenArray[3] = "David"; // ERROR: ArrayIndexOutOfBoundsException!
// ArrayList - dynamic size
ArrayList<String> namenList = new ArrayList<>();
namenList.add("Alice");
namenList.add("Bob");
namenList.add("Charlie");
namenList.add("David"); // No problem!
namenList.add("Eve"); // Add as many elements as needed!
// Functionality comparison
System.out.println("Array length: " + namenArray.length);
System.out.println("ArrayList size: " + namenList.size());
// ArrayList has more methods
namenList.remove("Bob"); // Remove element
boolean enthaeltAlice = namenList.contains("Alice"); // Check
Collections.sort(namenList); // Sort
System.out.println("ArrayList after removal and sorting: " + namenList);
}
}
The key advantage: Generic containers combine type safety with the flexibility of dynamic data structures, making them superior to arrays in nearly every practical scenario.
Summary for IHK Exams
- Class components: Name, attributes, constructor, methods
- Relationships:
- Association: Knows relationship
- Aggregation: “has-a” (loose, part persists independently)
- Composition: “consists-of” (strong, part is destroyed)
- Inheritance: “is-a” (generalization/specialization)
- Static vs. Non-Static: Class vs. instance
- Generics: Make classes type-safe by using placeholders for data types
- Containers vs. Arrays: Containers are dynamic, type-safe, and more functional
Exam-Relevant Concepts
Important Distinctions
| Concept | Description | UML Symbol | Lifetime |
|---|---|---|---|
| Association | Knows relationship between independent classes | Line | Independent |
| Aggregation | ”has-a” relationship, loose coupling | Line with empty diamond | Independent |
| Composition | ”consists-of” relationship, tight coupling | Line with filled diamond | Dependent |
| Inheritance | ”is-a” relationship, code reuse | Line with hollow arrow | Inherited |
Typical Exam Questions
- Draw UML class diagrams with various relationships
- Implement association, aggregation, and composition
- Explain the difference between static and non-static members
- Use generics for type-safe containers
- Compare arrays with generic containers
These concepts are fundamental to understanding object-oriented software architecture and form the foundation for complex system design.
Continue on the OOP Learning Path
All OOP articles are now complete. Return to the first article: Object-Oriented Programming OOP Fundamentals.

