- 1 Section
- 10 Lessons
- unbegrenzt
- Java Fortgeschritten10
- 1.1OOP in Java: Klassen, Kapselung, Konstruktoren
- 1.2Vererbung in Java
- 1.3Polymorphismus und Methodenüberschreibung
- 1.4Interfaces und abstrakte Klassen
- 1.5Collections: ArrayList, HashMap, LinkedList
- 1.6Generics und Typsicherheit
- 1.7Streams und Lambda-Ausdrücke
- 1.8Dateioperationen: IO und NIO
- 1.9Unit-Tests in Java: JUnit 5
- 1.10Praxisprojekt: Anwendung mit OOP und Tests
Polymorphismus und Methodenüberschreibung
Polymorphismus (griechisch „Vielgestaltigkeit") ist nach Kapselung und Vererbung das dritte Grundprinzip der OOP. Es beschreibt eine erstaunliche Eigenschaft von Java: derselbe Methodenaufruf kann je nach Objekt unterschiedlich ausgeführt werden. Eine tier.gibLaut()-Anweisung kann „Wuff" oder „Miau" sagen – abhängig davon, ob hinter tier ein Hund oder eine Katze steckt.
Diese Lektion erklärt, was Polymorphismus konkret bedeutet, wie der dynamische Methodenaufruf in Java funktioniert (engl. dynamic dispatch), was der Unterschied zwischen statischem und dynamischem Typ ist und wann instanceof und Type-Casts angebracht sind.
1) Was bedeutet Polymorphismus?
Wörtlich übersetzt: „vielgestaltig". In Java zeigt sich das so:
- Eine Variable vom Typ einer Oberklasse kann auf Objekte verschiedener Unterklassen verweisen
- Beim Aufruf einer Methode auf dieser Variable wird die Implementierung der tatsächlichen Klasse ausgeführt – nicht die der deklarierten
- So lässt sich generischer Code schreiben, der mit allen Subtypen klarkommt
Klassisches Beispiel:
public class Tier { public void gibLaut() { System.out.println("..."); } } public class Hund extends Tier { @Override public void gibLaut() { System.out.println("Wuff!"); } } public class Katze extends Tier { @Override public void gibLaut() { System.out.println("Miau!"); } } Tier t1 = new Hund(); Tier t2 = new Katze(); t1.gibLaut(); // Wuff! t2.gibLaut(); // Miau!
Obwohl t1 und t2 als Tier deklariert sind, ruft Java die jeweilige Unterklassen-Implementierung auf. Das ist Polymorphismus.
2) Statischer vs. dynamischer Typ
Eine Variable in Java hat in dieser Konstellation zwei Typen:
Tier t = new Hund(); ist der statische Typ Tier – der Compiler erlaubt nur Methodenaufrufe, die Tier kennt. Der dynamische Typ Hund entscheidet zur Laufzeit, welche Methoden-Implementierung läuft. Diesen Mechanismus nennt man dynamic dispatch.3) Dynamic Dispatch konkret
Was passiert intern bei t1.gibLaut()? Vereinfacht in drei Schritten:
Tier), ob gibLaut() existiert. Wenn ja, akzeptiert er den Aufruf.Hund). Sie findet die Methodentabelle dieser Klasse.gibLaut()-Implementierung von Hund aufgerufen – nicht die von Tier. „Wuff!" erscheint.4) Warum ist das nützlich?
Polymorphismus lässt dich generischen Code schreiben, der mit beliebigen Unterklassen funktioniert:
public void begruessungstour(Tier[] tiere) { for (Tier t : tiere) { t.gibLaut(); // jeder gibt seinen eigenen Laut } } Tier[] zoo = { new Hund(), new Katze(), new Hund(), new Vogel() }; begruessungstour(zoo); // Wuff! Miau! Wuff! Tschilp!
Die Methode begruessungstour kennt nur die Oberklasse Tier – muss aber nicht wissen, welche konkreten Tier-Arten existieren. Wenn morgen class Pferd extends Tier dazukommt, läuft der Code unverändert mit Pferden. Das ist enorm wertvoll für Erweiterbarkeit.
5) Upcast und Downcast
Die Zuweisung Tier t = new Hund(); ist ein Upcast: ein Hund wird als allgemeineres Tier behandelt. Das ist immer sicher und implizit – der Compiler verlangt keine Klammern.
Der umgekehrte Weg ist ein Downcast – ein Tier als spezifischeren Hund:
Tier t = new Hund(); // t.bellen() würde Compile-Fehler geben (Tier kennt bellen() nicht) Hund h = (Hund) t; // explizit, kann zur Laufzeit fehlschlagen h.bellen();
Wenn das Objekt hinter t aber kein Hund ist, wirft Java eine ClassCastException. Deshalb prüft man vorher meist mit instanceof:
if (t instanceof Hund) { Hund h = (Hund) t; h.bellen(); }
6) Pattern Matching mit instanceof
Seit Java 16 gibt es ein vereinfachtes Pattern Matching für instanceof, das den Cast direkt mit ausführt:
if (t instanceof Hund h) { h.bellen(); // h ist bereits typisiert als Hund }
Sehr handlich – der separate Cast entfällt. Funktioniert in allen aktuellen Java-Versionen.
7) Polymorphismus mit abstrakten Klassen
Manchmal soll eine Oberklasse nicht direkt instanziiert werden können – sie ist nur eine Schablone. Dafür gibt es abstrakte Klassen:
public abstract class Tier { private String name; public Tier(String name) { this.name = name; } // Konkrete Methode – wird vererbt public String getName() { return name; } // Abstrakte Methode – MUSS von Unterklassen implementiert werden public abstract void gibLaut(); } public class Hund extends Tier { public Hund(String name) { super(name); } @Override public void gibLaut() { System.out.println("Wuff!"); } } Tier t = new Tier("X"); // FEHLER – Tier ist abstrakt Tier h = new Hund("Rex"); // OK
Eine abstrakte Klasse erzwingt, dass jede Unterklasse die fehlenden Methoden liefert. Mehr zu Interfaces (die dasselbe noch strenger machen) in L4.
8) Compile-Zeit vs. Laufzeit-Auflösung
Wichtig zu verstehen: was passiert wann?
t.gibLaut() ist OK, weil Tier diese Methode kennt. t.bellen() ist Compile-Fehler.t auf einen Hund zeigt, läuft Hund.gibLaut().9) Wann wird NICHT polymorph aufgelöst?
Polymorphismus gilt nicht für alles. Drei wichtige Ausnahmen:
- Felder: bei Feld-Zugriff entscheidet der statische Typ.
tier.namenimmt das Feld der KlasseTier, auch wenn das Objekt ein Hund ist undHundein gleichnamiges Feld definiert (Schatten) - static-Methoden: werden anhand des statischen Typs aufgerufen, nicht polymorph.
Tier.statischeMethode()ruft immer die Tier-Variante - private-Methoden: sind nicht überschreibbar, also nicht polymorph
name haben, sind das zwei verschiedene Felder. Welches du siehst, hängt vom Referenz-Typ ab. Verwirrend und fehleranfällig. Lieber das Feld nur einmal definieren (in der Oberklasse) und einen Konstruktor mit Wert übergeben.
10) Polymorphismus mit Collections
Im echten Code mischt sich Polymorphismus oft mit Collections (L5):
List<Tier> tiere = new ArrayList<>(); tiere.add(new Hund("Rex")); tiere.add(new Katze("Mia")); tiere.add(new Vogel("Tweet")); for (Tier t : tiere) { t.gibLaut(); // jeweils eigene Implementierung }
Eine Liste vom Typ List<Tier> kann beliebige Tier-Arten enthalten. Beim Durchlaufen wird pro Element die richtige Methode aufgerufen. Genau dieses Muster trifft man in Java-Code überall: Listen von Component, von Shape, von Event …
11) Overriding-Regeln (Wiederholung mit Fokus)
Damit Polymorphismus funktioniert, muss korrekt überschrieben werden. Wichtige Regeln noch einmal kompakt (siehe auch L2):
- Selbe Signatur: Methodenname + Parameter müssen exakt übereinstimmen
- Rückgabetyp: gleich oder kovariant (Subtyp des Originals)
- Sichtbarkeit nicht reduzieren:
publicbleibtpublic, oder wird weiter geöffnet - Keine breiteren Checked Exceptions
@Overrideverwenden, damit der Compiler Tippfehler erkennt
12) Final-Methoden – Override verbieten
Eine als final markierte Methode kann nicht überschrieben werden – damit deaktivierst du Polymorphismus an dieser Stelle:
public class Konto { public final void aktivieren() { // Diese Methode garantiert immer dieselbe Logik, // unabhängig davon, welche Unterklasse genutzt wird. } }
Sinnvoll, wenn das Verhalten einer Methode kritisch ist und nicht überschrieben werden darf (z. B. Sicherheits-relevante Operationen, Initialisierungslogik).
13) Klassischer Anwendungsfall: Strategie-Muster
Polymorphismus ist die Basis vieler Design Patterns (siehe K49). Ein einfaches Beispiel – das Strategie-Muster:
public abstract class Bezahlmethode { public abstract void bezahlen(double betrag); } public class Kreditkarte extends Bezahlmethode { @Override public void bezahlen(double betrag) { System.out.println("Karte abgebucht: " + betrag); } } public class Lastschrift extends Bezahlmethode { @Override public void bezahlen(double betrag) { System.out.println("SEPA-Lastschrift: " + betrag); } } public class Bestellung { public void abschliessen(Bezahlmethode b, double total) { b.bezahlen(total); } }
Die Bestellung muss nichts über konkrete Bezahlmethoden wissen. Solange eine Methode bezahlen existiert, ist sie zufrieden. Neue Bezahlarten lassen sich später hinzufügen, ohne dass Bestellung angepasst werden muss – das Open/Closed-Prinzip.
14) Häufige Fehler bei Polymorphismus
- Felder mit Polymorphismus erwartet: Felder werden nicht polymorph aufgelöst.
tier.namenimmt das Feld der KlasseTier– auch wenn das Objekt einHundist und ein eigenesname-Feld hat - ClassCastException: Downcast ohne
instanceof-Check. Vermeiden, wo möglich - Overload statt Override: kleiner Tippfehler oder anderer Parametertyp – aus dem Override wird eine Überladung, Polymorphismus läuft ins Leere.
@Overridenutzen! - super-Aufruf vergessen: wenn die Override-Methode zusätzlich zum Original wirken soll,
super.methode()nicht vergessen - Equals und HashCode separat: wenn du
equalsfür Polymorphismus überschreibst, immer auchhashCodeentsprechend anpassen (siehe L1) - static-Methoden überschreiben wollen: geht nicht. Static-Methoden gehören zur Klasse, nicht zum Objekt – kein Polymorphismus
new beim Erzeugen stand – nicht die der deklarierten Variablen.
15) Polymorphismus mit Interfaces
Java erlaubt Polymorphismus auch über Interfaces, die in L4 ausführlich erklärt werden. Kleines Vorab-Beispiel:
public interface Druckbar { void drucken(); } public class Rechnung implements Druckbar { ... } public class Lieferschein implements Druckbar { ... } Druckbar[] dokumente = { new Rechnung(), new Lieferschein() }; for (Druckbar d : dokumente) { d.drucken(); }
Statt einer gemeinsamen Oberklasse reicht ein gemeinsames Interface, um Polymorphismus zu nutzen. Das ist oft flexibler, weil eine Klasse mehrere Interfaces implementieren kann, aber nur von einer Klasse erben.
Zusammenfassung
Polymorphismus erlaubt, dass derselbe Methodenaufruf je nach Objekttyp unterschiedlich ausgeführt wird – das Kernprinzip flexibler OOP-Architekturen. In Java gibt es statischen Typ (Deklarationstyp der Variable, bestimmt was der Compiler erlaubt) und dynamischen Typ (tatsächlicher Objekttyp, bestimmt welche Methode läuft) – das nennt man dynamic dispatch. Upcast (Hund → Tier) ist immer sicher und implizit, Downcast (Tier → Hund) braucht eine explizite Klammer und besser einen instanceof-Check oder Pattern Matching (if (t instanceof Hund h)). Felder, static- und private-Methoden sind nicht polymorph; Override-Methoden ja, daher @Override verwenden. Polymorphismus ist die Basis fast aller Design Patterns.
