„Amigo, magst du Wale?“

„Wale? Nö, noch nie davon gehört.“

„Ein Wal ist wie eine Kuh, nur größer und er kann schwimmen. Übrigens stammen Wale zufälligerweise von den Kühen ab. Äh, oder zumindest haben sie einen gemeinsamen Vorfahren. Ganz egal.“

Polymorphie und Überladung - 1

„Hör mal zu. Ich möchte dir ein weiteres wirklich mächtiges Werkzeug der OOP vorstellen: Polymorphie. Sie hat vier Merkmale.“

1) Methoden überschreiben.

Stell dir vor, du hast eine „Kuh“-Klasse für ein Spiel geschrieben. Sie hat eine Menge Member-Variablen und Methoden. Objekte dieser Klasse können verschiedene Dinge tun: laufen, essen, schlafen. Kühe klingeln beim Laufen außerdem mit einer Glocke. Nehmen wir an, du hast alles in der Klasse bis ins kleinste Detail umgesetzt.

Polymorphie und Überladung - 2

Dann sagt der Kunde plötzlich, er wolle ein neues Level des Spiels veröffentlichen, in dem alle Aktionen im Meer stattfinden und die Hauptfigur ein Wal ist.

Als du die „Wal“-Klasse entwirfst, erkennst du, dass sie sich nur geringfügig von der „Kuh“-Klasse unterscheidet. Beide Klassen verwenden eine sehr ähnliche Logik, deshalb entscheidest du dich für die Vererbung.

Die Klasse Cow eignet sich ideal als Elternklasse: Sie hat bereits alle notwendigen Variablen und Methoden. Du musst nur die Fähigkeit des Wals zum Schwimmen hinzufügen. Aber es gibt ein Problem: Dein Wal hat Beine, Hörner und eine Glocke. Schließlich implementiert die Klasse Cow genau diese Funktionalität. Was kannst du da tun?

Polymorphie und Überladung - 3

Das Überschreiben von Methoden ist deine Rettung. Wenn wir eine Methode vererben, die nicht genau das tut, was wir in unserer neuen Klasse benötigen, können wir die Methode durch eine andere ersetzen.

Polymorphie und Überladung - 4

Wie wird das gemacht? In unserer Subklasse deklarieren wir die Methode, die wir ändern wollen (mit der gleichen Methodensignatur wie in der Elternklasse). Dann schreiben wir neuen Code für die Methode. Das war‘s. Das ist dann so, als ob die alte Methode der Elternklasse gar nicht existiert.

So funktioniert es:

Code Beschreibung
class Cow
{
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}class Whale extends Cow
{
public void printName()
{
System.out.println("I'm a whale");
}
}
Hier definieren wir zwei Klassen: Cow und WhaleWhale erbt von Cow.

Die Klasse Whale überschreibt die Methode printName();.

public static void main(String[] args)
{
Cow cow = new Cow();
cow.printName();
}
Dieser Code zeigt „Ich bin eine Kuh“ auf dem Bildschirm an.
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
Dieser Code zeigt „Ich bin ein Wal“ auf dem Bildschirm an

Nachdem sie von Cow erbt und printName überschreibt, besitzt die Klasse Whale die folgenden Daten und Methoden:

Code Beschreibung
class Whale
{
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a whale");
}
}
Wir wissen nichts von irgendeiner alten Methode.

„Ehrlich gesagt, genau das habe ich erwartet.“

2) Aber das ist noch nicht alles.

„Angenommen, die Klasse Cow besitzt die Methode printAll, welche die beiden anderen Methoden aufruft. Dann würde der Code folgendermaßen funktionieren:“

Auf dem Bildschirm erscheint:
Ich bin weiß
Ich bin ein Wal

Code Beschreibung
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.println("I'm a whale");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
Auf dem Bildschirm erscheint:
Ich bin weiß
Ich bin ein Wal

Beachte, dass die printName()-Methode von Whale verwendet wird und nicht die von Cow, wenn die printAll ()-Methode der Cow-Klasse mit einem Whale-Objekt aufgerufen wird.

Wichtig ist nicht die Klasse, in der die Methode geschrieben ist, sondern der Typ (die Klasse) des Objekts, mit dem die Methode aufgerufen wird.

„Ach so.“

„Du kannst nur nicht-statische Methoden vererben und überschreiben. Statische Methoden werden nicht vererbt und können daher auch nicht überschrieben werden.“

So sieht die Klasse Whale aus, nachdem wir die Vererbung angewendet und die Methoden überschrieben haben:

Code Beschreibung
class Whale
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a whale");
}
}
So sieht die Klasse Whale aus, nachdem wir die Vererbung angewendet und die Methode überschrieben haben. Wir wissen nichts von irgendeiner alten printName-Methode.

3) Typumwandlung.

Jetzt kommt ein noch interessanterer Punkt. Da eine Klasse alle Methoden und Daten ihrer Elternklasse erbt, kann ein Objekt dieser Klasse von Variablen der Elternklasse (und der Elternklasse der Elternklasse usw., bis hin zur Object-Klasse) referenziert werden. Betrachte dieses Beispiel:

Code Beschreibung
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printColor();
}
Auf dem Bildschirm erscheint:
Ich bin weiß.
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printColor();
}
Auf dem Bildschirm erscheint:
Ich bin weiß.
public static void main(String[] args)
{
Object o = new Whale();
System.out.println(o.toString());
}
Auf dem Bildschirm erscheint:
Whale@da435a.
Die Methode toString() wird von der Object-Klasse geerbt.

„Hört sich gut an. Aber wozu braucht man das?“

„Das ist eine sehr nützliche Funktion. Später wirst du verstehen, dass sie wirklich sehr, sehr nützlich ist.“

4) Dynamische Bindung.

So sieht das aus:

Code Beschreibung
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printName();
}
Auf dem Bildschirm erscheint:
Ich bin ein Wal.
public static void main(String[] args)
{
Cow cow = new Whale();
cow.printName();
}
Auf dem Bildschirm erscheint:
Ich bin ein Wal.

Beachte, dass nicht der Typ der Variable bestimmt, welche spezifische printName-Methode wir aufrufen (die der Klasse Cow oder Whale), sondern der Typ des von der Variable referenzierten Objekts.

Die Cow-Variable speichert eine Referenz auf ein Whale-Objekt, und die in der Whale-Klasse definierte printName-Methode wird aufgerufen.

„Naja, das haben sie wegen der Klarheit nicht hinzugefügt.“

„Ja, das ist nicht so offensichtlich. Merke dir diese wichtige Regel:“

Die Auswahl der Methoden, die du über eine Variable aufrufen kannst, wird durch den Typ der Variable bestimmt. Welche spezifische Methode/Implementierung aufgerufen wird, wird jedoch durch den Typ/die Klasse des von der Variable referenzierten Objekts bestimmt.

„Ich werde es versuchen.“

„Du wirst ständig damit konfrontiert, also wirst du es schnell verstehen und nie mehr vergessen.“

5) Typumwandlung.

Die Typumwandlung funktioniert bei Referenztypen, also Klassen, anders als bei primitiven Typen. Allerdings gelten auch für Referenztypen erweiternde und einschränkende Umwandlungen. Betrachte dieses Beispiel:

Erweiternde Umwandlung Beschreibung
Cow cow = new Whale();

Eine klassische erweiternde Umwandlung. Jetzt kannst du nur noch Methoden mit dem Whale-Objekt aufrufen, die in der Klasse Cow definiert sind.

Der Compiler lässt dich die Variable cow nur für den Aufruf der durch den Cow-Typ definierten Methoden verwenden.

Einschränkende Umwandlung Beschreibung
Cow cow = new Whale();
if (cow instanceof Whale)
{
Whale whale = (Whale) cow;
}
Eine klassische einschränkende Umwandlung mit einer Typprüfung. Die Variable cow vom Typ Cow speichert eine Referenz auf ein Whale-Objekt.
Wir prüfen, ob dies der Fall ist und führen dann die (erweiternde) Typumwandlung durch. Dies wird auch als Typecasting bezeichnet.
Cow cow = new Cow();
Whale whale = (Whale) cow; //exception
Du kannst auch eine einschränkende Umwandlung eines Referenztyps ohne Typprüfung des Objekts durchführen.
Wenn die Variable cow in diesem Fall auf etwas anderes als ein Whale-Objekt zeigt, wird eine Ausnahme (InvalidClassCastException) ausgelöst.

6) Und nun zu einem echten Leckerbissen. Ursprüngliche Methode aufrufen.

Manchmal möchte man beim Überschreiben einer geerbten Methode diese nicht vollständig ersetzen. Manchmal möchte man sie einfach nur ein bisschen ergänzen.

In diesem Fall möchtest du, dass der Code der neuen Methode die gleiche Methode aufruft, aber über die Basisklasse. Mit Java ist auch das möglich. Und so wird es gemacht: super.method().

Hier ein paar Beispiele:

Code Beschreibung
class Cow
{
public void printAll()
{
printColor();
printName();
}
public void printColor()
{
System.out.println("I'm white");
}
public void printName()
{
System.out.println("I'm a cow");
}
}

class Whale extends Cow
{
public void printName()
{
System.out.print("This is false: ");
super.printName();

System.out.println("I'm a whale");
}
}
public static void main(String[] args)
{
Whale whale = new Whale();
whale.printAll();
}
Auf dem Bildschirm erscheint:
Ich bin weiß
Das ist falsch: Ich bin eine Kuh
Ich bin ein Wal

„Hmm. Okay, das war eine tolle Lektion. Meine Roboter-Ohren sind fast geschmolzen.“

„Ja, das ist kein einfaches Zeug. Das ist eines der schwierigsten Themen, auf das du stoßen wirst. Der Professor hat versprochen, Links zu Materialien anderer Autoren bereitzustellen, so dass du deine Verständnislücken schließen kannst.“