Java Polymorphismus
Flexibler Java-Code mit Compile-Zeit-Polymorphismus (Überladen) und Laufzeit-Polymorphismus (Überschreiben).
Polymorphismus bedeutet „eine Schnittstelle, viele Implementierungen." In Java gibt es zwei Ausprägungen:
- Compile-Zeit-Polymorphismus (Überladen): Der Compiler wählt zwischen Methoden gleichen Namens anhand der übergebenen Argumenttypen.
- Laufzeit-Polymorphismus (Überschreiben): Die JVM wählt zwischen Methodenimplementierungen anhand des tatsächlichen Objekts, auf dem der Aufruf erfolgt.
Die Laufzeitvariante ist das, was man in einem OOP-Kontext normalerweise mit „Polymorphismus" meint – und sie ist der Grund, warum Vererbung überhaupt sinnvoll ist. Ohne sie wäre eine Cat-Referenz der einzige Weg, Cat.speak() aufzurufen – man könnte keinen Code schreiben, der über eine gemischte Tierliste iteriert und jedes Tier bittet zu sprechen.
Compile-Zeit-Polymorphismus — Überladen
Zwei Methoden in derselben Klasse können den gleichen Namen haben, solange sich ihre Parameterlisten unterscheiden. Der Compiler entscheidet, welche aufgerufen wird, anhand der Argumenttypen an der Aufrufstelle:
public class Printer {
void print(int n) { System.out.println("int: " + n); }
void print(double d) { System.out.println("double: " + d); }
void print(String s) { System.out.println("string: " + s); }
}
Printer p = new Printer();
p.print(5); // int
p.print(5.0); // double
p.print("hi"); // stringDies wird vollständig zur Compile-Zeit entschieden. Die gewählte Methode ist in den Bytecode eingebettet; zur Laufzeit ändert sich nichts. Das Überladen von Methoden wurde ausführlich in method overloading in Teil 5 behandelt.
Laufzeit-Polymorphismus — Überschreiben und dynamische Dispatch
Die interessante Variante. Wenn eine Unterklasse eine Methode überschreibt, werden Aufrufe über eine Referenz des Elterntyps dennoch an die Version der Unterklasse weitergeleitet:
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
@Override String speak() { return "meow"; }
}
class Dog extends Animal {
@Override String speak() { return "woof"; }
}
Animal[] zoo = { new Cat(), new Dog(), new Animal() };
for (Animal a : zoo) {
System.out.println(a.speak());
}
// meow
// woof
// (noise)In jeder Iteration ist a als Animal typisiert, aber das tatsächliche Objekt ist ein Cat, ein Dog oder ein Animal. Der Aufruf a.speak() wählt die Methode nicht zur Compile-Zeit – zur Compile-Zeit weiß der Compiler nur, dass a irgendein Animal ist. Zur Laufzeit schaut die JVM auf das tatsächliche Objekt und leitet den Aufruf an die speak-Methode der Klasse dieses Objekts weiter.
Dies ist dynamische Dispatch (manchmal auch virtueller Dispatch genannt). Es ist das, was die obige Schleife interessant macht: Sie ist generisch gegen Animal geschrieben und funktioniert für jede Unterklasse – einschließlich solcher, die noch nicht existierten, als die Schleife geschrieben wurde.
Warum es wichtig ist
Polymorphismus ist das OOP-Feature, das Code offen für Erweiterungen ohne Modifikation macht. Eine Funktion, die eine Shape entgegennimmt und area() darauf aufruft, funktioniert für jede Form, die heute existiert, und für jede Form, die jemand morgen hinzufügt. Die Funktion braucht keine if (shape instanceof Circle)-Kette.
double totalArea(List<Shape> shapes) {
double sum = 0;
for (Shape s : shapes) sum += s.area(); // dispatches to each subclass
return sum;
}Füge Triangle extends Shape hinzu, und totalArea funktioniert kostenlos mit Listen von Dreiecken. Das ist der Kern des Open/Closed-Prinzips – offen für Erweiterungen, geschlossen für Modifikationen.
Upcasting und Downcasting
Den Weg von einem Unterklassentyp zu einem Elterntyp nennt man Upcast. Er ist implizit und immer sicher:
Cat c = new Cat();
Animal a = c; // upcast — implicitDen umgekehrten Weg – eine Referenz des Elterntyps einer Unterklassenvariable zuzuweisen – nennt man Downcast. Er erfordert einen Cast-Ausdruck, und die JVM prüft zur Laufzeit, ob das Objekt tatsächlich von diesem Untertyp ist:
Animal a = new Cat();
Cat c = (Cat) a; // downcast — runtime check
Animal a2 = new Dog();
Cat c2 = (Cat) a2; // ClassCastException at runtimeDie compile-zeitfreundliche Alternative ist die instanceof-Prüfung, die in modernem Java oft mit Pattern Matching kombiniert wird:
if (a instanceof Cat c) {
c.purr();
}Felder sind nicht polymorph
Dynamische Dispatch gilt nur für Instanzmethoden. Felder, static-Methoden und private-Methoden werden zur Compile-Zeit basierend auf dem deklarierten Typ der Referenz gebunden:
class A {
String label = "A";
static String klass() { return "A"; }
}
class B extends A {
String label = "B";
static String klass() { return "B"; }
}
A a = new B();
System.out.println(a.label); // "A" — field, not polymorphic
System.out.println(a.klass()); // "A" — static, not polymorphicDas ist einer der Gründe, warum man Felder privat hält und über Methoden auf sie zugreift – Methoden nehmen am Polymorphismus teil, Felder nicht.
@Override und stille Fehler
Überschreibungen sollten immer mit @Override annotiert werden. Die Annotation teilt dem Compiler mit: „Dies soll eine Methode des Elterntyps überschreiben – schlag fehl, wenn das nicht der Fall ist." Ohne sie erzeugt ein kleiner Tippfehler eine neue Methode, die aussieht wie eine Überschreibung, es aber nicht ist:
class Animal {
String speak() { return "(noise)"; }
}
class Cat extends Animal {
String Speak() { return "meow"; } // capital S — typo, new method
}
Animal a = new Cat();
System.out.println(a.speak()); // "(noise)" — Cat.Speak was never calledDas Hinzufügen von @Override lässt den Compiler dies sofort erkennen.
Polymorphismus mit Interfaces
Vererbung ist nicht der einzige Weg. Ein Interface ist ebenfalls ein Elterntyp – verschiedene konkrete Klassen implementieren es, und Code, der den Interface-Typ entgegennimmt, funktioniert mit allen:
interface Greeter {
String greet();
}
class English implements Greeter {
public String greet() { return "Hello"; }
}
class French implements Greeter {
public String greet() { return "Bonjour"; }
}
Greeter g = new French();
System.out.println(g.greet()); // "Bonjour" — dispatched to French.greetGleiche Idee – Code gegen die Abstraktion schreiben und die Laufzeit die Implementierung wählen lassen. Das Kapitel interfaces geht auf die Mechanik ein.
Ein ausgearbeitetes Beispiel
Was kommt als Nächstes
Polymorphismus beruht auf einem Mechanismus: einer Unterklasse, die eine geerbte Methode ersetzt. Dieser Mechanismus – was erlaubt ist, was nicht, und die @Override-Annotation, die einen ehrlich hält – ist Thema des nächsten Kapitels. Weiter zu method overriding.