W3docs

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");       // string

Dies 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 — implicit

Den 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 runtime

Die 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 polymorphic

Das 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 called

Das 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.greet

Gleiche 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

java— editable, runs on the server

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.

Übungen

Übung
Animal a = new Cat(); a.speak(); — welches speak() wird tatsächlich ausgeführt?
Animal a = new Cat(); a.speak(); — welches speak() wird tatsächlich ausgeführt?
Was this page helpful?