EMZETT.
Login

Polymorphism

Kurz: Ein Objekt einer Unterklasse kann überall dort verwendet werden, wo die Oberklasse erwartet wird — beim Methodenaufruf entscheidet der tatsächliche Objekttyp zur Laufzeit, welche Implementierung läuft.

Genauer: Beispiel: eine Variable vom Typ Tier kann zur Laufzeit ein Hund-Objekt enthalten; ruft man geraeusch() auf, läuft die überschriebene Hund-Version, obwohl der Variablentyp Tier ist (“dynamic dispatch”). Das erlaubt z. B. eine Liste List<Tier> mit gemischten Unterklassen, bei der jedes Element sein eigenes Verhalten zeigt.

Im Detail

class Tier { void geraeusch() { System.out.println("..."); } }
class Hund extends Tier { void geraeusch() { System.out.println("Wuff"); } }
class Katze extends Tier { void geraeusch() { System.out.println("Miau"); } }
 
List<Tier> tiere = new ArrayList<>();
tiere.add(new Hund());
tiere.add(new Katze());
tiere.add(new Tier());
 
for (Tier t : tiere) {
    t.geraeusch(); // "Wuff", "Miau", "..." - jeweils die richtige, überschriebene Version
}

Diese Art von Polymorphie (“Laufzeitpolymorphie”/“dynamic dispatch”) unterscheidet sich grundlegend vom statischen Overloading (siehe Overloading): welche geraeusch()-Implementierung tatsächlich läuft, entscheidet die JVM erst ZUR LAUFZEIT anhand des tatsächlichen Objekttyps (nicht des Variablentyps Tier). Genau das macht die Liste List<Tier> mit gemischten Unterklassen überhaupt sinnvoll nutzbar: der aufrufende Code (die for-Schleife) muss nicht wissen, ob ein konkretes Element ein Hund oder eine Katze ist, sondern verlässt sich darauf, dass JEDE Tier-Unterklasse ihre eigene, passende geraeusch()-Version liefert. Das ist die praktische Grundlage z. B. für Plugin-Systeme oder austauschbare Strategien (Strategy-Pattern), bei denen neuer Code (eine weitere Tierart) hinzugefügt werden kann, ohne den bestehenden Schleifen-Code anzufassen.

Polymorphie über Interfaces statt Vererbung

Polymorphie funktioniert genauso gut über Interfaces statt konkreter Vererbung — sogar noch flexibler, weil eine Klasse mehrere Interfaces implementieren, aber nur von EINER Oberklasse erben kann:

interface Fahrbar { void fahren(); }
 
class Auto implements Fahrbar { public void fahren() { System.out.println("Fährt auf Rädern"); } }
class Boot implements Fahrbar { public void fahren() { System.out.println("Fährt auf Wasser"); } }
 
List<Fahrbar> fahrzeuge = List.of(new Auto(), new Boot());
for (Fahrbar f : fahrzeuge) {
    f.fahren(); // jeweils die passende Implementierung, unabhängig von gemeinsamer Vererbung
}

instanceof — den tatsächlichen Typ erkennen, wenn nötig

Polymorphie ist der Normalfall, aber manchmal muss Code doch wissen, mit welchem KONKRETEN Untertyp er es zu tun hat — dafür gibt es instanceof, seit Java 16 mit direkter Typumwandlung (“Pattern Matching”):

for (Tier t : tiere) {
    if (t instanceof Hund hund) { // prüft UND castet in einem Schritt
        System.out.println(hund.bellen()); // eine Hund-spezifische Methode, die Tier nicht hat
    }
}

Häufiges instanceof-Ketten über alle Unterklassen gilt allerdings oft als Zeichen, dass Polymorphie (eine gemeinsame, überschriebene Methode) die elegantere Lösung wäre — jede neue Unterklasse würde sonst eine Anpassung an JEDER instanceof-Kette im ganzen Programm erfordern, statt einfach nur die gemeinsame Methode selbst zu implementieren.

Siehe auch: Inheritance, Interface, OOP