Skip to content
IRC-CodingIRC-Coding
OOPClass RelationshipsAssociationAggregationCompositionInheritanceGenericsStatic MembersIHK Exam

OOP Class Relationships: Association & Inheritance

Master OOP class relationships: association, aggregation, composition, inheritance. Static members and generics for IHK exams.

S

schutzgeist

13 min read
OOP Class Relationships: Association & Inheritance

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 private attributes
  • 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 itself
  • protected: 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, CatAnimal).

Specialization: Deriving a more specific subclass from a more general parent class. The subclass inherits all properties and refines or extends them (e.g., AnimalDog; 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.

FeatureNon-Static (Instance Member)Static (Class Member)
Belongs toThe individual object (instance)The class itself
Memory copiesOne copy per object. 100 objects = 100 copies of the attributeOnly one copy for the entire class. Shared by all objects
Access viaThe object: objectName.methodName()The class name: ClassName.methodName()
Can accessBoth non-static and static membersOnly static members. No access to non-static members (which object would be meant?)
Typical useAttributes 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

FeatureArraysGeneric Containers (e.g. ArrayList<T>, List<T>)
SizeFixed. The size must be defined at creation and cannot be changed laterDynamic/Growing. The size automatically adjusts to the number of elements
Type SafetyProvide basic type safety but can only store a single fixed typeOffer full compile-time type safety through generics
FunctionalityHighly limited. Only basic operations (read, write at index)Provide many useful methods: .add(), .remove(), .contains(), .size(), etc.
PerformanceVery fast for direct index accessSlightly slower due to dynamic management overhead, but negligible in most cases
FlexibilityLowVery 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

ConceptDescriptionUML SymbolLifetime
AssociationKnows relationship between independent classesLineIndependent
Aggregation”has-a” relationship, loose couplingLine with empty diamondIndependent
Composition”consists-of” relationship, tight couplingLine with filled diamondDependent
Inheritance”is-a” relationship, code reuseLine with hollow arrowInherited

Typical Exam Questions

  1. Draw UML class diagrams with various relationships
  2. Implement association, aggregation, and composition
  3. Explain the difference between static and non-static members
  4. Use generics for type-safe containers
  5. 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.

Back to Blog
Share:

Nächster Artikel in Object-Oriented Programming

Weiterlesen
OOP Dispatch: Dynamic Binding & Multiple Dispatch

Related Posts