Alle Tabs der Lerneinheit (Erklärung · Interaktiv verstehen · Praxis-Übung · Klausur-Quiz) als durchgehender Text. Ideal zum Wiederholen vor der Klausur, und für Suchmaschinen wie Google, Bing und KI-Suche (ChatGPT, Perplexity).
Diese Lerneinheit wurde für typische Bachelor-Klausuren konzipiert. So prüfen wir · Fehler entdeckt? Melde ihn uns oder markiere die fragliche Stelle direkt im Text oben.
Alle Tabs der Lerneinheit (Erklärung · Interaktiv verstehen · Praxis-Übung · Klausur-Quiz) als durchgehender Text. Ideal zum Wiederholen vor der Klausur, und für Suchmaschinen wie Google, Bing und KI-Suche (ChatGPT, Perplexity).
Du kannst Code auf zwei Arten wiederverwenden: erben (extends) oder ein Objekt als Feld halten und an es delegieren. Anfänger greifen reflexartig zur Vererbung, weil sie zuerst gelehrt wird. Fortgeschrittene Programmierung fragt aber: war das die richtige Wahl? Dieses Topic zeigt, wann Vererbung trägt (echte ist ein-Beziehung) und wann sie zur Falle wird, und warum die Standard-Empfehlung lautet: im Zweifel Komposition (Joshua Bloch, Effective Java, Item 18).
Was du können musst:
Faustregel zum Mitnehmen: Wenn dein Problem zwei unabhängige Achsen hat (hier: Fahrzeug-Art und Antrieb), modelliere die eine als Klasse und die andere als gehaltenes Objekt, nicht beide über Vererbung. Sonst multiplizieren sich die Achsen zu einer Klassen-Explosion. Komposition hält jede Achse separat und kombiniert sie zur Laufzeit, das ist genau die Idee hinter dem Strategy-Pattern.
Anmelden, um den Fortschritt zu speichern.
Nächster Schritt
Aktives Abrufen festigt Wissen schneller als nochmal lesen.
Du kannst Code auf zwei Arten wiederverwenden: erben (extends) oder ein Objekt als Feld halten und an es delegieren. Anfänger greifen reflexartig zur Vererbung, weil sie zuerst gelehrt wird. Fortgeschrittene Programmierung fragt aber: war das die richtige Wahl? Dieses Topic zeigt, wann Vererbung trägt (echte ist ein-Beziehung) und wann sie zur Falle wird, und warum die Standard-Empfehlung lautet: im Zweifel Komposition (Joshua Bloch, Effective Java, Item 18).
Was du können musst:
Vererbung modelliert "ein X ist ein Y" und übernimmt die Implementierung der Oberklasse; Komposition modelliert "ein X hat ein Y", hält das Y als Feld und leitet Aufrufe daran weiter (Delegation).
Du brauchst einen Stack (Stapel: nur push und pop). ArrayList kann doch schon alles, also erbst du einfach:
// ANTI-PATTERN: Vererbung nur um Code zu sparen
class Stack<E> extends ArrayList<E> {
void push(E e) { add(e); }
E pop() { return remove(size() - 1); }
}
Sieht praktisch aus, ist aber kaputt: dein Stack hat jetzt auch add(int index, E e), remove(int index), set(...), clear(), geerbt von ArrayList. Jemand kann stack.add(0, x) aufrufen und mitten in den Stapel schreiben. Die Invariante "Zugriff nur oben" ist hin. Du hast die ganze Schnittstelle der Oberklasse mitgeerbt, auch die Teile, die du gar nicht willst.
// HAS-A: Stack HÄLT eine Liste und zeigt nur push/pop nach außen
class Stack<E> {
private final List<E> elems = new ArrayList<>(); // Komposition
void push(E e) { elems.add(e); }
E pop() {
if (elems.isEmpty()) throw new EmptyStackException();
return elems.remove(elems.size() - 1); // Delegation
}
boolean isEmpty() { return elems.isEmpty(); }
}
Mit Komposition entscheidest du, welche Methoden nach außen sichtbar sind. Die Liste ist
private, niemand kann an der Invariante vorbei.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Vererbung bricht Kapselung: die Subklasse hängt an Implementierungsdetails der Oberklasse, die sich ändern können. Der Klassiker (aus Bloch, Effective Java): ein HashSet, das mitzählt, wie viele Elemente je hinzugefügt wurden.
class ZaehlendesSet<E> extends HashSet<E> {
private int hinzugefuegt = 0;
@Override public boolean add(E e) {
hinzugefuegt++;
return super.add(e);
}
@Override public boolean addAll(Collection<? extends E> c) {
hinzugefuegt += c.size();
return super.addAll(c);
}
int getHinzugefuegt() { return hinzugefuegt; }
}
var s = new ZaehlendesSet<String>();
s.addAll(List.of("a", "b", "c"));
System.out.println(s.getHinzugefuegt()); // erwartet 3 ... liefert 6!
Warum 6? Weil HashSet.addAll intern selbst add aufruft (self-use). Also zählt addAll die 3 Elemente, und das von ihm aufgerufene überschriebene add zählt sie nochmal. Du müsstest wissen, dass addAll sich auf add abstützt, ein undokumentiertes Implementierungsdetail, das in der nächsten Java-Version anders sein kann.
Dieses konkrete Beispiel steht für ein allgemeines Muster, die fragile Basisklasse: deine Subklasse kann brechen, ohne dass du sie anfasst, nur weil sich die Oberklasse intern ändert. Komposition hat dieses Problem nicht: das gehaltene Objekt arbeitet über seine eigene öffentliche Schnittstelle und ruft keine überschriebenen Methoden deiner Klasse auf, es entsteht keine implizite self-use-Kopplung (außer du gibst ihm bewusst einen Callback).
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Delegation heißt: deine Methode macht die Arbeit nicht selbst, sondern reicht sie an das gehaltene Objekt weiter. Das entkoppelt: du programmierst gegen ein Interface, nicht gegen eine konkrete Oberklasse, und kannst das gehaltene Objekt zur Laufzeit austauschen (das ist im Kern das Strategy-Pattern). Strategy heißt hier: das austauschbare Verhalten steckt in einem eigenen Objekt, nicht fest verdrahtet in der Auto-Klasse.
interface Antrieb { void starten(); }
class Elektro implements Antrieb {
public void starten() { System.out.println("leise surren"); }
}
class Benzin implements Antrieb {
public void starten() { System.out.println("brummen"); }
}
class Auto {
private Antrieb antrieb; // HAS-A
Auto(Antrieb antrieb) { this.antrieb = antrieb; }
void setAntrieb(Antrieb a) { this.antrieb = a; } // zur Laufzeit wechselbar!
void losfahren() { antrieb.starten(); } // Delegation
}
var auto = new Auto(new Benzin());
auto.losfahren(); // brummen
auto.setAntrieb(new Elektro());
auto.losfahren(); // leise surren
Mit Vererbung müsstest du jede Kombination als eigene Klasse bauen: BenzinAuto, ElektroAuto, BenzinLKW, ElektroLKW. Bei F Fahrzeug-Typen und A Antrieben sind das F × A Klassen (multiplikativ). Mit Komposition: F + A + 1 Klassen (additiv). Genau das kannst du im interaktiven Tab durchspielen.
Vererbung ist nicht verboten, sie ist nur kein Default. Sie passt, wenn alle Punkte gelten:
1. Sprachtest IS-A vs. HAS-A. "Ein Auto ist ein Motor"? Nein, ein Auto hat einen Motor, also Komposition. Sag den Satz laut, dann weißt du es.
2. Im Zweifel Komposition (Bloch, Item 18). Sie ist lose gekoppelt, flexibler und bricht keine Kapselung. Vererbung ist die Ausnahme, die du begründen musst.
3. Komposition = Feld + Delegation. Halte das andere Objekt private als Feld und leite nur die Methoden weiter, die du wirklich anbieten willst. So bleibt die Schnittstelle minimal.
4. Vererbung nur bei echtem IS-A plus Liskov. Die Subklasse muss überall für die Oberklasse einsetzbar sein. Kannst du das nicht garantieren, ist Vererbung falsch.
5. Verhalten zur Laufzeit wechselbar nötig? Komposition. Bei Vererbung steht die Klasse eines Objekts mit new fest (ein ElektroAuto bleibt eines) und lässt sich nicht mehr austauschen; ein per Komposition gehaltenes Objekt (Strategy) wechselst du dagegen jederzeit per Setter.
6. Vererbung nur bei kontrollierter/dokumentierter Basisklasse. Eine fremde Klasse zu erweitern (z.B. ArrayList) ist riskant, weil du an ihre internen Aufrufe (self-use) gebunden bist.
1. Vererbung nur zum Code-Sparen. Stack extends ArrayList erbt add(index), remove(index) usw. und zerstört die Stapel-Invariante. Reuse rechtfertigt keine Vererbung, die Beziehung muss IS-A sein.
2. Fragile Basisklasse durch self-use. addAll ruft intern add; überschreibst du beide, zählst du doppelt. Subklassen hängen an undokumentierten Implementierungsdetails der Oberklasse.
3. Oberflächliches IS-A bricht Liskov. "Quadrat ist ein Rechteck" klingt richtig, aber setBreite/setHoehe verhalten sich beim Quadrat anders, Code, der mit Rechtecken rechnet, bricht. Sprache reicht nicht, das Verhalten muss passen.
4. Tiefe Vererbungstürme. Fünf Ebenen extends sind schwer nachzuvollziehen; eine Änderung oben kann unten alles brechen. Komposition bleibt flach und lokal.
5. Komposition mit Aggregation verwechseln. In UML ist ◆ (Komposition) eine starke Ganzes-Teil-Beziehung mit gemeinsamer Lebensdauer (eine Rechnung besitzt ihre Posten, die mit ihr verschwinden), ◇ (Aggregation) eine schwache (ein Team hat Spieler, die auch ohne das Team existieren). In Java gibt es dafür keine syntaktische Unterscheidung: beide sind ein Feld, der Unterschied liegt allein in Intention und Lebenszyklus.
6. "Java kann keine Mehrfachvererbung" als Sackgasse sehen. Du brauchst sie nicht: beliebig viele HAS-A-Felder plus mehrere Interfaces ersetzen Mehrfachvererbung sauber und ohne Diamant-Problem.
Dasselbe Problem, zwei Designs: links Vererbung (eine Klasse pro Fahrzeug-Antrieb-Kombination), rechts Komposition (Fahrzeug hält einen Antrieb über ein Interface).
Probier folgendes:
Das ist der praktische Grund für "favor composition over inheritance": neue Anforderungen kosten bei Komposition eine Klasse, bei Vererbung eine ganze Reihe.
Interaktive Visualisierung
Interaktive Komponente: probiere sie im Topic-Player oben aus.
Faustregel zum Mitnehmen: Wenn dein Problem zwei unabhängige Achsen hat (hier: Fahrzeug-Art und Antrieb), modelliere die eine als Klasse und die andere als gehaltenes Objekt, nicht beide über Vererbung. Sonst multiplizieren sich die Achsen zu einer Klassen-Explosion. Komposition hält jede Achse separat und kombiniert sie zur Laufzeit, das ist genau die Idee hinter dem Strategy-Pattern.
Klausurfragen mit Lösungen (6)
Antwort: Auto und Motor
Erklärung: Ein Auto HAT einen Motor (HAS-A), es IST kein Motor. Das gehört als Feld (Komposition) modelliert. Die anderen drei sind echte IS-A-Beziehungen (ein Hund ist ein Tier, ein Sparkonto ist ein Konto, ein Student ist eine Person) und passen zu Vererbung.
Antwort: Falsch
Erklärung: Falsch. Der Stack erbt dann auch add(index), remove(index), set(...) usw. und jeder kann mitten in den Stapel schreiben, die Invariante 'Zugriff nur oben' ist kaputt. Besser: der Stack HÄLT eine private Liste (Komposition) und zeigt nur push/pop nach außen. Reuse allein rechtfertigt keine Vererbung.
Typ: Wahr/Falsch
Zuordnungen:
Erklärung: HAS-A (ein Ganzes hält Teile, oder hält ein austauschbares Verhalten) wird zu Komposition. IS-A (ein Spezialfall eines allgemeineren Typs) wird zu Vererbung. Der Sprachtest 'ist ein' vs. 'hat ein' trennt die Fälle zuverlässig.
Typ: Zuordnung
class Auto {
private Antrieb antrieb;
void setAntrieb(Antrieb a) { this.antrieb = a; }
void losfahren() { antrieb.starten(); }
}Antwort: Der Antrieb kann zur Laufzeit ausgetauscht werden
Erklärung: Per Komposition hält das Auto ein Antrieb-Objekt als Feld und kann es per Setter zur Laufzeit wechseln (Benzin -> Elektro). Vererbung legt das Verhalten zur Compile-Zeit fest (ein ElektroAuto bleibt ein ElektroAuto). Das ist im Kern das Strategy-Pattern.
Antwort: Vererbung 9, Komposition 7
Erklärung: Vererbung: 3 Fahrzeuge × 3 Antriebe = 9 Kombinationsklassen (BenzinAuto, ElektroAuto, ...). Komposition: 3 Fahrzeug-Klassen + 3 Antriebs-Klassen + 1 Antrieb-Interface = 7. Multiplikativ (F×A) gegen additiv (F+A+1): mit jeder neuen Achse rennt die Vererbung davon.
class ZaehlendesSet<E> extends HashSet<E> {
int n = 0;
public boolean add(E e) { n++; return super.add(e); }
public boolean addAll(Collection<? extends E> c) {
n += c.size();
return super.addAll(c);
}
}Antwort: HashSet.addAll ruft intern add auf, daher wird doppelt gezählt
Erklärung: self-use: HashSet.addAll ruft intern für jedes Element das (überschriebene) add auf. Also zählt addAll die 3 Elemente (n += 3) und das von super.addAll aufgerufene add zählt jedes nochmal (n += 3) = 6. Das ist das Fragile-Base-Class-Problem: die Subklasse hängt an einem undokumentierten Implementierungsdetail der Oberklasse.
Klausurfragen mit Lösungen (6)
Antwort: Komposition bevorzugen, Vererbung nur bei echtem IS-A und kontrollierter/dokumentierter Basisklasse
Erklärung: 'Favor composition over inheritance': Komposition ist der Default (lose gekoppelt, flexibel, kapselungssicher). Vererbung ist die begründungspflichtige Ausnahme, erlaubt bei echtem IS-A, gültigem Liskov-Prinzip und einer Basisklasse, die du kontrollierst oder die für Vererbung dokumentiert ist. Verboten ist Vererbung nicht.
Antwort: Das Liskov-Substitutionsprinzip: die Subklasse ist überall einsetzbar, wo die Oberklasse erwartet wird
Erklärung: Liskov-Substitutionsprinzip: ein Objekt der Subklasse muss überall korrekt funktionieren, wo ein Objekt der Oberklasse erwartet wird, ohne das Verhalten zu brechen. Hält das nicht (klassisch Quadrat/Rechteck mit setBreite/setHoehe), ist Vererbung die falsche Wahl, auch wenn der Satz 'ist ein' sprachlich passt.
Antwort: Falsch
Erklärung: Falsch. Sprachlich stimmt 'ist ein', aber das Verhalten bricht Liskov: ein Rechteck erlaubt setBreite und setHoehe unabhängig, beim Quadrat müssen beide gleich bleiben. Code, der für Rechtecke geschrieben ist (Breite ändern, ohne dass die Höhe folgt), liefert beim Quadrat falsche Ergebnisse. IS-A im Satz reicht nicht, das Verhalten muss substituierbar sein.
Typ: Wahr/Falsch
Lösungen pro Lücke:
Erklärung: Komposition = Feld + Delegation. Das gehaltene Objekt ist ein privates Feld (so bleibt die Schnittstelle minimal und gekapselt), und die eigenen Methoden delegieren (reichen die Arbeit weiter) an dieses Objekt, statt sie zu erben.
Typ: Lückentext
Antwort: Weil die Subklasse von internen Aufrufmustern (self-use) der Oberklasse abhängt, die sich ändern können
Erklärung: Die Subklasse kann an undokumentierten Implementierungsdetails hängen, etwa daran, dass addAll intern add aufruft. Ändert die Oberklasse dieses Verhalten, bricht die Subklasse, ohne dass man sie anfasst (fragile base class). Komposition ruft nur die öffentliche Schnittstelle des gehaltenen Objekts auf und ist davon entkoppelt. (Private Felder sieht eine Subklasse übrigens NICHT direkt.)
Antwort: Wenn eine echte IS-A-Beziehung besteht, Liskov hält und die Oberklasse für Vererbung entworfen/dokumentiert ist (oder man sie selbst kontrolliert)
Erklärung: Vererbung trägt, wenn alle drei Punkte zusammenkommen: echtes IS-A, gültiges Liskov-Prinzip und eine Basisklasse, die für Vererbung gedacht und dokumentiert ist (oder die man selbst kontrolliert, z.B. gleiches Paket). Nur Methoden einer fremden Klasse 'mitnehmen' zu wollen ist gerade das Anti-Pattern. Mehrfachvererbung über Klassen gibt es in Java ohnehin nicht.