OOP Klassenbeziehungen: Assoziation, Aggregation, Komposition & Vererbung
Einzelne Objekte sind nützlich, aber die wahre Stärke der OOP zeigt sich, wenn Objekte miteinander in Beziehung stehen und zusammenarbeiten, um komplexe Aufgaben zu lösen. Dieses Dokument erklärt den Aufbau von Klassen und die verschiedenen Arten von Beziehungen zwischen ihnen - perfekt für die IHK-Prüfungsvorbereitung.
1. Bestandteile von Klassen (Der innere Aufbau)
Eine Klasse ist wie eine Schablone. Sie besteht aus mehreren definierten Bestandteilen:
1. Klassenname
Der eindeutige Name der Klasse (z.B. Kunde, Bankkonto). Beginnt per Konvention mit einem Großbuchstaben.
2. Attribute (Datenfelder)
Dies sind die Variablen innerhalb der Klasse. Sie beschreiben den Zustand eines zukünftigen Objekts.
- Jedes Attribut hat einen Namen (z.B.
kontostand) und einen Datentyp (z.B.double,String) - Sie werden meist
privatedeklariert (siehe Kapselung)
3. Konstruktor (Constructor)
Eine spezielle Methode, die automatisch aufgerufen wird, wenn ein neues Objekt der Klasse mit new erstellt wird.
Zweck: Initialisiert die neuen Objekte, d.h. weist den Attributen Startwerte zu.
- Der Konstruktor hat denselben Namen wie die Klasse und keinen Rückgabetyp (nicht einmal
void) - Es kann mehrere Konstruktoren geben (Überladung), z.B. ein Standardkonstruktor ohne Parameter und ein Konstruktor, der alle Attribute initialisiert
// Beispiel für Konstruktor
public class Bankkonto {
private double kontostand;
private String inhaber;
// Konstruktor
public Bankkonto(String inhaberName, double startGuthaben) {
inhaber = inhaberName;
kontostand = startGuthaben;
}
// Standardkonstruktor
public Bankkonto() {
this("Unbekannt", 0.0);
}
// Überladener Konstruktor
public Bankkonto(String inhaberName) {
this(inhaberName, 0.0);
}
}
// Objekterstellung:
Bankkonto meinKonto = new Bankkonto("Max Mustermann", 1000.0);
Bankkonto leeresKonto = new Bankkonto();
4. Methoden (Memberfunktionen)
Definieren das Verhalten der Objekte. Sie operieren auf den Attributen.
- Getter/Setter: Öffentliche Methoden zum kontrollierten Lesen und Schreiben
privateAttribute - Geschäftsmethoden: Implementieren die eigentliche Funktionalität (z.B.
geldEinzahlen(double betrag),geldAbheben(double betrag))
5. Sichtbarkeitsmodifikatoren (Access Modifiers)
Legen fest, von wo aus auf die Klassenbestandteile zugegriffen werden kann. Wichtig für die Kapselung.
private: Nur aus der eigenen Klasse heraus sichtbarprotected: Nur aus der eigenen Klasse und ihren Subklassen (Vererbung) sichtbarpublic: Von überall aus sichtbar- (package-private/default): Nur innerhalb des gleichen Pakets/Namespaces sichtbar
2. Klassenbeziehungen: Wie Klassen zueinander stehen
Klassen existieren selten isoliert. Sie arbeiten zusammen. Diese Beziehungen werden in UML-Klassendiagrammen modelliert.
a) Assoziation (Association)
Beschreibung: Die allgemeinste Beziehung. Sie beschreibt eine semantische Verbindung zwischen zwei unabhängigen Klassen. Eine Klasse “kennt” eine andere Klasse.
UML-Darstellung: Durchgezogene Linie.
Beispiel: Ein Professor unterrichtet einen Studenten. Der Professor und der Student existieren unabhängig voneinander. Die Beziehung ist oft bidirektional (beide “kennen” sich).
Implementierung: Meist durch Referenz-Attribute.
// Assoziation Beispiel
public class Professor {
private String name;
private String fachbereich;
private List<Student> studenten; // Professor ASSOZIIERT MIT Student
public Professor(String name, String fachbereich) {
this.name = name;
this.fachbereich = fachbereich;
this.studenten = new ArrayList<>();
}
public void addStudent(Student student) {
studenten.add(student);
student.addProfessor(this); // Bidirektionale Beziehung
}
public void unterrichten() {
System.out.println("Professor " + name + " unterrichtet " + studenten.size() + " Studenten");
}
// Getter
public String getName() { return name; }
public List<Student> getStudenten() { return new ArrayList<>(studenten); }
}
public class Student {
private String name;
private int matrikelNr;
private List<Professor> professoren; // Student ASSOZIIERT MIT Professor
public Student(String name, int matrikelNr) {
this.name = name;
this.matrikelNr = matrikelNr;
this.professoren = new ArrayList<>();
}
public void addProfessor(Professor professor) {
professoren.add(professor);
}
public void lerne() {
System.out.println("Student " + name + " lernt bei " + professoren.size() + " Professoren");
}
// Getter
public String getName() { return name; }
public List<Professor> getProfessoren() { return new ArrayList<>(professoren); }
}
b) Aggregation (Aggregation)
Beschreibung: Eine speziellere Form der Assoziation. Sie beschreibt eine “ist-Teil-von” oder “hat-ein” Beziehung, bei der die Teile auch ohne das Ganze existieren können. Es ist eine lose Beziehung.
UML-Darstellung: Durchgezogene Linie mit unausgefüllter Raute auf der Seite des Ganzen.
Beispiel: Eine Abteilung hat Mitarbeiter. Wenn die Abteilung aufgelöst wird, werden die Mitarbeiter nicht entlassen, sondern können einer anderen Abteilung zugeordnet werden. Der Teil (Mitarbeiter) existiert unabhängig vom Ganzen (Abteilung).
Implementierung: Wie Assoziation, aber die Lebensdauer der Objekte ist nicht gekoppelt.
// Aggregation Beispiel
public class Abteilung {
private String name;
private String standort;
private List<Mitarbeiter> mitarbeiter; // Abteilung HAT Mitarbeiter (Aggregation)
public Abteilung(String name, String standort) {
this.name = name;
this.standort = standort;
this.mitarbeiter = new ArrayList<>();
}
public void addMitarbeiter(Mitarbeiter mitarbeiter) {
this.mitarbeiter.add(mitarbeiter);
mitarbeiter.setAbteilung(this);
}
public void removeMitarbeiter(Mitarbeiter mitarbeiter) {
this.mitarbeiter.remove(mitarbeiter);
mitarbeiter.setAbteilung(null); // Mitarbeiter existiert weiter!
}
public void aufloesen() {
// Mitarbeiter werden nicht zerstört, nur aus Abteilung entfernt
for (Mitarbeiter m : mitarbeiter) {
m.setAbteilung(null);
}
mitarbeiter.clear();
System.out.println("Abteilung " + name + " aufgelöst, Mitarbeiter existieren weiter");
}
// Getter
public String getName() { return name; }
public List<Mitarbeiter> getMitarbeiter() { return new ArrayList<>(mitarbeiter); }
}
public class Mitarbeiter {
private String name;
private String position;
private Abteilung abteilung; // Mitarbeiter kann ohne Abteilung existieren
public Mitarbeiter(String name, String position) {
this.name = name;
this.position = position;
this.abteilung = null; // Mitarbeiter kann ohne Abteilung erstellt werden
}
public void setAbteilung(Abteilung abteilung) {
this.abteilung = abteilung;
}
public void arbeiten() {
String abteilungsName = abteilung != null ? abteilung.getName() : "keine Abteilung";
System.out.println(name + " arbeitet als " + position + " in " + abteilungsName);
}
// Getter
public String getName() { return name; }
public Abteilung getAbteilung() { return abteilung; }
}
c) Komposition (Composition)
Beschreibung: Eine noch stärkere Form der “ist-Teil-von” Beziehung. Die Teile können nicht ohne das Ganze existieren. Das Ganze ist für die Lebensdauer der Teile verantwortlich. Strikte Besitzverhältnisse.
UML-Darstellung: Durchgezogene Linie mit ausgefüllter Raute auf der Seite des Ganzen.
Beispiel: Ein Auto besteht aus einem Motor. Der Motor hat keine eigenständige Existenz ohne das Auto. Wenn das Auto verschrottet wird, wird auch der Motor zerstört. Die Lebensdauer ist stark gekoppelt.
Implementierung: Das Ganze erstellt die Teile selbst in seinem Konstruktor.
// Komposition Beispiel
public class Auto {
private String marke;
private String modell;
private Motor motor; // Auto KOMPONIERT AUS Motor
private List<Rad> raeder; // Auto KOMPONIERT AUS Rädern
public Auto(String marke, String modell) {
this.marke = marke;
this.modell = modell;
// Teile werden mit Auto erstellt (strikte Lebensdauerkopplung)
this.motor = new Motor(2.0, 150); // Motor wird mit Auto erstellt
this.raeder = new ArrayList<>();
// 4 Räder werden erstellt
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() {
// Alle Teile werden zerstört, wenn Auto verschrottet wird
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) Generalisierung & Spezialisierung (Vererbung)
Beschreibung: Dies ist die “ist-ein” Beziehung und wird durch Vererbung umgesetzt.
Generalisierung: Das Herausziehen gemeinsamer Merkmale mehrerer Klassen in eine allgemeinere Oberklasse (z.B. Hund, Katze → Tier).
Spezialisierung: Das Ableiten einer spezielleren Klasse (Subklasse) von einer allgemeineren Klasse (Oberklasse). Die Subklasse erbt alle Eigenschaften und verfeinert oder erweitert sie (z.B. Tier → Hund; Der Hund fügt die Methode bellen() hinzu).
UML-Darstellung: Durchgezogene Linie mit hohlem Pfeil von der Sub- zur Oberklasse.
Beispiel: Manager ist ein Mitarbeiter. Er erbt alle Attribute (Name, Gehalt) und fügt vielleicht ein Attribut bonus hinzu.
// Vererbung Beispiel
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); // Konstruktor der Oberklasse aufrufen
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; // Zusätzliche Berechnung
}
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. Statische vs. Nicht-Statische Methoden/Attribute
Dieser Unterschied ist fundamental und bezieht sich darauf, ob ein Merkmal zur Klasse oder zum Objekt gehört.
| Merkmal | Nicht-Statisch (Instanz-Member) | Statisch (Klassen-Member) |
|---|---|---|
| Zugehörigkeit | Gehört zum einzelnen Objekt (Instanz) | Gehört zur Klasse selbst |
| Anzahl im Speicher | Pro Objekt eine Kopie. 100 Objekte = 100 Kopien des Attributes | Nur eine einzige Kopie für die gesamte Klasse. Wird von allen Objekten geteilt |
| Aufruf | Über das Objekt: objektName.methodenName() | Über den Klassennamen: KlassenName.methodenName() |
| Zugriff auf… | Kann auf sowohl nicht-statische als auch statische Member zugreifen | Kann NUR auf statische Member zugreifen. Kein Zugriff auf nicht-statische Member (denn: welches Objekt sollte gemeint sein?) |
| Typische Verwendung | Attribute, die sich von Objekt zu Objekt unterscheiden (z.B. kontostand, name) | Utility-Methoden (z.B. Math.sqrt()), Konstanten (z.B. Math.PI), Zählvariablen (z.B. anzahlErstellterObjekte) |
// Beispiel für Statisch vs. Nicht-Statisch
public class Student {
// Nicht-statisches Attribut (pro Objekt einmal)
private String name;
private int matrikelNr;
// Statisches Attribut (nur einmal für die Klasse)
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++; // Zugriff auf statisches Attribut
}
// Nicht-statische Methode
public void studieren() {
System.out.println(name + " studiert an " + UNIVERSITAET);
System.out.println("Aktuelle Anzahl Studenten: " + anzahlStudenten);
}
// Statische Methode
public static int getAnzahlStudenten() {
return anzahlStudenten;
// return name; // FEHLER: name ist nicht-statisch und nicht zugreifbar!
}
// Statische Utility-Methode
public static boolean istGueltigeMatrikelNr(int nummer) {
return nummer >= 100000 && nummer <= 999999;
}
// Getter und Setter
public String getName() { return name; }
public int getMatrikelNr() { return matrikelNr; }
public static String getUniversitaet() { return UNIVERSITAET; }
}
// Verwendung
public class Main {
public static void main(String[] args) {
// Objekte erstellen
Student stud1 = new Student("Max Mustermann", 123456);
Student stud2 = new Student("Erika Mustermann", 234567);
// Nicht-statische Methoden über Objekte aufrufen
stud1.studieren();
stud2.studieren();
// Statische Methoden über Klasse aufrufen
System.out.println("Anzahl Studenten: " + Student.getAnzahlStudenten());
System.out.println("Universität: " + Student.getUniversitaet());
// Statische Utility-Methode
boolean gueltig = Student.istGueltigeMatrikelNr(123456);
System.out.println("Matrikelnummer gültig: " + gueltig);
// Zugriff auf nicht-statische Attribute
System.out.println("Student 1 Name: " + stud1.getName());
// FEHLER: Statische Methode kann nicht auf Objekt zugreifen
// Student.getName(); // Compilerfehler!
}
}
4. Generische Klassen (Generics) - z.B. List<T>
Was ist das Problem?
Vor Generics wurden Sammlungen (wie ArrayList) für den Typ Object definiert. Man konnte alles einfügen (Strings, Integers, etc.). Beim Auslesen musste man den Typ mühsam per Casting prüfen und zurückkonvertieren (String s = (String) myList.get(0);). Laufzeitfehler waren häufig.
Was ist die Lösung?
Generische Klassen. Sie sind Klassenvorlagen, die einen oder mehrere Platzhalter für Datentypen (oft T wie “Type”, E wie “Element”) verwenden.
Zweck: Typsicherheit zur Übersetzungszeit (Compile-Time). Der Compiler prüft, dass nur Objekte des richtigen Typs eingefügt werden. Casting entfällt, Laufzeitfehler werden vermieden.
// Beispiel für Generics
// Ohne Generics (veraltet, fehleranfällig)
List myOldList = new ArrayList();
myOldList.add("Hallo");
myOldList.add(123); // Compiler sagt nichts, aber...
String s = (String) myOldList.get(1); // Laufzeitfehler: ClassCastException!
// Mit Generics (typsicher)
List<String> myList = new ArrayList<>(); // T wird zu String
myList.add("Hallo");
// myList.add(123); // COMPILERFEHLER: 123 ist kein String!
String s = myList.get(0); // Kein Casting nötig, sicher.
// Eigene generische Klasse
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;
}
}
// Verwendung der generischen Klasse
public class GenericsBeispiel {
public static void main(String[] args) {
// Box für Strings
Box<String> stringBox = new Box<>();
stringBox.setInhalt("Hallo Welt");
String inhalt = stringBox.getInhalt(); // Kein Casting nötig
// Box für Integer
Box<Integer> integerBox = new Box<>();
integerBox.setInhalt(42);
Integer zahl = integerBox.getInhalt(); // Kein Casting nötig
// Box für eigene Objekte
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());
}
}
// Generische Methoden
public class Utility {
// Generische Methode zum Tauschen
public static <T> void tausche(T[] array, int i, int j) {
T temp = array[i];
array[i] = array[j];
array[j] = temp;
}
// Generische Methode für 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. Vorteile generischer Container (Templates in C++) gegenüber Arrays
| Merkmal | Arrays | Generische Container (z.B. ArrayList<T>, List<T>) |
|---|---|---|
| Größe | Statisch/Fest. Die Größe muss bei der Erstellung definiert werden und kann später nicht geändert werden | Dynamisch/Wachsend. Die Größe passt sich automatisch der Anzahl der Elemente an |
| Typsicherheit | Bieten grundlegende Typsicherheit, aber können nur einen festen Typ speichern | Bieten durch Generics volle Typsicherheit zur Übersetzungszeit |
| Funktionalität | Sehr eingeschränkt. Grundlegende Operationen (Lesen, Schreiben an Index) | Bieten viele hilfreiche Methoden: .add(), .remove(), .contains(), .size(), usw. |
| Performance | Sehr schnell für direkten Index-Zugriff | Etwas langsamer aufgrund des Overheads der dynamischen Verwaltung, aber in den meisten Fällen vernachlässigbar |
| Flexibilität | Gering | Sehr hoch. Es gibt verschiedene Container für verschiedene Zwecke (Listen, Sets, Maps, Queues) |
// Vergleich: Array vs. ArrayList
public class ArrayVsContainer {
public static void main(String[] args) {
// Array - feste Größe
String[] namenArray = new String[3];
namenArray[0] = "Alice";
namenArray[1] = "Bob";
namenArray[2] = "Charlie";
// namenArray[3] = "David"; // FEHLER: ArrayIndexOutOfBoundsException!
// ArrayList - dynamische Größe
ArrayList<String> namenList = new ArrayList<>();
namenList.add("Alice");
namenList.add("Bob");
namenList.add("Charlie");
namenList.add("David"); // Kein Problem!
namenList.add("Eve"); // Fügt beliebig viele Elemente hinzu!
// Funktionalitätsvergleich
System.out.println("Array Länge: " + namenArray.length);
System.out.println("ArrayList Größe: " + namenList.size());
// ArrayList hat mehr Methoden
namenList.remove("Bob"); // Element entfernen
boolean enthaeltAlice = namenList.contains("Alice"); // Prüfen
Collections.sort(namenList); // Sortieren
System.out.println("ArrayList nach Entfernung und Sortierung: " + namenList);
}
}
Zusammenfassender Vorteil: Generische Container kombinieren die Typsicherheit mit der Flexibilität einer dynamischen Datenstruktur und sind damit Arrays in den allermeisten Anwendungsfällen überlegen.
Zusammenfassung für die IHK-Prüfung
- Klassenbestandteile: Name, Attribute, Konstruktor, Methoden
- Beziehungen:
- Assoziation: Kennt-Beziehung
- Aggregation: “hat-ein” (lose, Teil existiert weiter)
- Komposition: “besteht-aus” (stark, Teil wird zerstört)
- Vererbung: “ist-ein” (Generalisierung/Spezialisierung)
- Statisch vs. Nicht-Statisch: Klasse vs. Objekt
- Generics: Machen Klassen typsicher, indem sie Platzhalter für Datentypen verwenden
- Container vs. Arrays: Container sind dynamisch, typsicher und funktionaler
Prüfungsrelevante Konzepte
Wichtige Unterscheidungen
| Konzept | Beschreibung | UML-Symbol | Lebensdauer |
|---|---|---|---|
| Assoziation | Kennt-Beziehung zwischen unabhängigen Klassen | Linie | Unabhängig |
| Aggregation | ”hat-ein” Beziehung, lose Kopplung | Linie mit leerer Raute | Unabhängig |
| Komposition | ”besteht-aus” Beziehung, strikte Kopplung | Linie mit gefüllter Raute | Abhängig |
| Vererbung | ”ist-ein” Beziehung, Code-Wiederverwendung | Linie mit hohlem Pfeil | Vererbt |
Typische Prüfungsaufgaben
- Zeichnen Sie UML-Klassendiagramme mit verschiedenen Beziehungen
- Implementieren Sie Assoziation, Aggregation und Komposition
- Erklären Sie den Unterschied zwischen statischen und nicht-statischen Membern
- Verwenden Sie Generics für typsichere Container
- Vergleichen Sie Arrays mit generischen Containern
Diese Konzepte sind fundamental für das Verständnis objektorientierter Softwarearchitektur und bilden die Basis für komplexe Systemdesigns.