Eine Referenz, viele Typen
Polymorphie ist der Lohn von Vererbung und Schnittstellen. Sie bedeutet, dass eine einzige als Elternklasse (oder Schnittstelle) typisierte Variable ein Objekt eines beliebigen Untertyps halten kann, und wenn du eine Methode darauf aufrufst, führt Java die Version aus, die zur tatsächlichen Klasse des Objekts gehört - nicht die Version, die der deklarierte Typ der Variablen nahelegt.
Die Grundlage hast du bereits auf der Seite zur Vererbung gesehen: Eine Unterklasse überschreibt eine Methode ihrer Elternklasse. Polymorphie macht dieses Überschreiben erst lohnenswert - sie erlaubt es dir, Code gegen den allgemeinen Typ zu schreiben und jedes konkrete Objekt auf seine eigene Weise verhalten zu lassen.
Sowohl a als auch b sind als Animal deklariert, dennoch gibt jedes seinen eigenen Laut aus. Diese Wahl geschieht zur Laufzeit anhand des realen Objekts.
Dynamischer Methodenaufruf
Der Mechanismus dahinter ist der dynamische Methodenaufruf (dynamic method dispatch): Bei einer überschriebenen Instanzmethode betrachtet die JVM die Laufzeitklasse des Objekts, um zu entscheiden, welche Implementierung aufgerufen wird. Der Compiler prüft nur, ob die Methode auf dem deklarierten Typ existiert; die eigentliche Auswahl wird bis zur Programmausführung aufgeschoben.
Genau das ermöglicht es einer einzigen Schleife, eine ganze Mischung von Typen zu verarbeiten, ohne jemals zu fragen, was jedes davon ist:
Die Schleife kennt nur Shape. Füge später ein Triangle extends Shape hinzu, und dieser Code funktioniert unverändert weiter - genau das ist der Sinn. Der Code hängt von der Abstraktion ab, nicht von der konkreten Liste der Typen.
Upcasting und Downcasting
Einen Dog in einer Animal-Variablen zu speichern, ist Upcasting - das Aufsteigen in der Hierarchie zu einem allgemeineren Typ. Es ist immer sicher und Java macht es implizit, denn jeder Dog ist ein Animal.
In die andere Richtung zu gehen, ist Downcasting - eine Eltern-Referenz nehmen und sie als einen bestimmten Untertyp behandeln. Das ist nur gültig, wenn das Objekt wirklich dieser Untertyp ist, daher musst du den Cast explizit schreiben und riskierst eine ClassCastException, wenn du dich irrst:
Der letzte Cast kompiliert problemlos - der Compiler kann nicht beweisen, dass er falsch ist - fliegt aber bei der Ausführung auseinander, weil ein Cat kein Dog ist. Mache niemals ein Downcast auf gut Glück.
Downcasts mit instanceof absichern
Prüfe vor einem Downcast den realen Typ mit instanceof. Modernes Java erlaubt es dir, das Ergebnis im selben Ausdruck zu binden (Pattern Matching für instanceof), sodass du den separaten Cast einsparst:
instanceof gibt für null false zurück, daher schützt dich die Prüfung auch vor einer NullPointerException. Wenn du dich jedoch dabei ertappst, lange instanceof-Ketten zu schreiben, ist das oft ein Zeichen dafür, dass das Verhalten als überschriebene Methode in die Klassen gehört - lass die Polymorphie die Verzweigung für dich übernehmen.
Overriding vs. Overloading
Die beiden klingen ähnlich, haben aber nichts miteinander zu tun, und sie zu verwechseln ist eine klassische Quelle von Verwirrung.
Overriding (Überschreiben) ist eine Unterklasse, die eine Methode der Elternklasse mit identischer Signatur ersetzt. Es wird zur Laufzeit über den Typ des Objekts aufgelöst - das ist die Polymorphie, die wir verwendet haben.
Overloading (Überladen) ist eine Klasse mit mehreren Methoden gleichen Namens, aber unterschiedlichen Parameterlisten. Es wird zur Kompilierzeit über die Argumenttypen aufgelöst, ohne dass ein Laufzeitaufruf beteiligt ist:
Der Compiler wählt das passende describe allein anhand des statischen Typs des Arguments. Es ist kein Eltern-/Kindobjekt beteiligt, also ist das keine Laufzeitpolymorphie - es verwendet lediglich einen Methodennamen wieder.
Ein häufiger Fallstrick: Felder sind nicht polymorph
Nur Instanzmethoden werden zur Laufzeit aufgerufen. Felder und statische Methoden werden über den deklarierten Typ aufgelöst, was viele stolpern lässt:
p.name() führt die Version von Child aus (Polymorphie), aber p.label liest das Feld von Parent, weil Felder verdeckt und nicht überschrieben werden. Die Lösung ist einfach: Halte Felder private und greife nur über Methoden darauf zu, damit der polymorphe Aufruf immer gewinnt.
Als Nächstes: Zugriffsmodifikatoren
Polymorphie funktioniert nur dann sauber, wenn Unterklassen die richtigen Member sehen und überschreiben können, während der Rest deines Codes nicht hineingreifen und Invarianten brechen kann. Dieses Gleichgewicht wird durch die Zugriffe public, protected, private und paketprivat gesteuert - die Zugriffsmodifikatoren, als Nächstes.
Häufig gestellte Fragen
Was ist Polymorphie in Java?
Polymorphie bedeutet, dass ein Referenztyp auf Objekte vieler verschiedener Klassen verweisen kann und die tatsächlich ausgeführte Methode anhand des realen Typs des Objekts zur Laufzeit gewählt wird - nicht anhand des deklarierten Typs der Variablen. So kann eine Variable Shape shape ein Circle oder ein Square halten, und der Aufruf von shape.area() führt automatisch die richtige Version aus.
Was ist der Unterschied zwischen Overriding und Overloading in Java?
Overriding (Überschreiben) liegt vor, wenn eine Unterklasse eine Methode der Oberklasse mit derselben Signatur ersetzt - das ist es, was die Laufzeitpolymorphie antreibt. Overloading (Überladen) liegt vor, wenn eine Klasse mehrere Methoden mit gleichem Namen, aber unterschiedlichen Parameterlisten besitzt; der Compiler wählt zur Kompilierzeit anhand der Argumente eine davon aus. Überschreiben wird zur Laufzeit über den Typ des Objekts aufgelöst; Überladen wird zur Kompilierzeit über die Argumenttypen aufgelöst.
Was ist der Unterschied zwischen Upcasting und Downcasting in Java?
Upcasting behandelt ein Kindobjekt als seinen Elterntyp (Animal a = new Dog();) - immer sicher und meist implizit. Downcasting geht in die andere Richtung (Dog d = (Dog) a;) und ist nur sicher, wenn das Objekt wirklich dieser Untertyp ist; andernfalls wirft es eine ClassCastException. Sichere jedes Downcast zuvor mit instanceof ab.