Polymorphie
In 1.3 hast du mautSumme() einmal geschrieben – in der Oberklasse Fahrzeug. Trotzdem rechnete sie beim Motorrad mit 5 Cent und beim LKW mit 30.
Wie kann das sein? mautSumme() steht in Fahrzeug, und Fahrzeug kennt weder Motorräder noch LKW. Der Mechanismus dahinter hat einen Namen: Polymorphie – Vielgestaltigkeit.
Ein Feld, drei Sorten
Polymorphie: Eine Variable vom Typ der Oberklasse kann auf Objekte jeder Unterklasse verweisen. Beim Aufruf einer überschriebenen Methode entscheidet nicht der Typ der Variablen, sondern der tatsächliche Typ des Objekts, welche Fassung ausgeführt wird.
Diese Entscheidung fällt erst, wenn das Programm läuft. Man spricht deshalb von dynamischer Bindung oder später Bindung.
Der entscheidende Punkt steckt in mautSumme():
public int mautSumme() {
return kilometerstand * maut();
}
Der Aufruf maut() steht in Fahrzeug. Ausgeführt wird trotzdem die Fassung des Objekts, an dem mautSumme() aufgerufen wurde – beim Motorrad also die 5 Cent.
Auch aus einer geerbten Methode heraus greift die überschriebene Fassung. Das ist der Grund, warum man gemeinsame Abläufe genau einmal schreiben kann und trotzdem an den richtigen Stellen etwas Verschiedenes passiert.
Statischer und dynamischer Typ
Eine Variable hat zwei Typen – und die beiden entscheiden Verschiedenes.
Die Variable f hat zwei Typen:
| Typ | entscheidet über | |
|---|---|---|
| statischer Typ | Fahrzeug – so ist sie deklariert |
welche Methoden man aufrufen darf |
| dynamischer Typ | Lkw – so ist das Objekt beschaffen |
welche Fassung ausgeführt wird |
Deshalb ist f.getLadegewicht() ein Übersetzungsfehler: Fahrzeug kennt diese Methode nicht. Und deshalb greift beim Rechnen trotzdem die 30-Cent-Fassung.
Kurz: Der statische Typ bestimmt das Was darf ich?, der dynamische das Was passiert?
Die Typumwandlung nach unten ((Lkw) f) ist möglich, aber gefährlich: Verweist f in Wirklichkeit auf ein Motorrad, bricht das Programm zur Laufzeit ab. Der Übersetzer kann das nicht prüfen – er sieht nur den statischen Typ.
Faustregel: Wenn du im Programm anfängst, nach dem konkreten Typ zu fragen und umzuwandeln, stimmt meist der Entwurf nicht. Dann fehlt der Oberklasse eine Methode.
Der eigentliche Gewinn
Aufgabe: Eine vierte Fahrzeugart
Es kommt eine vierte Fahrzeugart dazu: ein Bus, der 20 Cent Maut zahlt.
a) Was musst du an der Schleife im Hauptprogramm ändern, die über alle Fahrzeuge läuft?
b) Was wäre zu ändern gewesen, wenn du dasselbe Programm stattdessen mit einer if-Kette über eine Zeichenkette art gebaut hättest?
c) Nenne einen Fall, in dem du den Fehler aus b) erst Wochen später bemerkst.
Tipp
Schreib die if-Kette einmal hin:
if (art.equals("pkw")) { ... }
else if (art.equals("lkw")) { ... }
Und dann frag dich, wie oft so etwas in einem größeren Programm vorkommt – und was passiert, wenn man eine Stelle übersieht.
Teil 1: Lesen
Aufgabe 1: Welche Fassung läuft?
Ohne Rechner. Lies die vier Dateien und sag die fünf Ausgabezeilen voraus. Notiere zu jeder Zeile, aus welcher Klasse die ausgeführte Methode stammt.
void main() {
Alarm[] anlage = new Alarm[4];
anlage[0] = new Alarm("Flur");
anlage[1] = new Rauchmelder("Kueche");
anlage[2] = new Wassermelder("Keller");
anlage[3] = new Rauchmelder("Bad");
for (int i = 0; i < anlage.length; i++) {
IO.println(anlage[i].meldung());
}
int summe = 0;
for (int i = 0; i < anlage.length; i++) {
summe = summe + anlage[i].lautstaerke();
}
IO.println("Summe: " + summe);
}
public class Alarm {
private String ort;
public Alarm(String pOrt) {
ort = pOrt;
}
public String getOrt() {
return ort;
}
public int lautstaerke() {
return 60;
}
public String meldung() {
return "Alarm in " + ort + " (" + lautstaerke() + " dB)";
}
}
public class Rauchmelder extends Alarm {
public Rauchmelder(String pOrt) {
super(pOrt);
}
public int lautstaerke() {
return 85;
}
}
public class Wassermelder extends Alarm {
public Wassermelder(String pOrt) {
super(pOrt);
}
public String meldung() {
return "Wasser im " + getOrt() + "!";
}
}
a) Welche fünf Zeilen gibt das Programm aus?
b) meldung() steht nicht in Rauchmelder. Warum erscheint bei der Küche trotzdem 85 dB?
c) Welche lautstaerke()-Fassung gilt beim Wassermelder – und warum?
d) Jemand ergänzt eine Klasse Gasmelder extends Alarm mit lautstaerke() = 100 und legt ein Objekt davon an die Stelle 4 im Feld. Welche Zeilen des Hauptprogramms müssen dafür geändert werden?
Prüf deine Vorhersage erst danach nach.
Tipp zu b)
meldung() ruft lautstaerke() auf. Die Frage ist nicht, in welcher Klasse die Methode steht, sondern welches Objekt hinter dem Aufruf steckt.
Aufgabe 2: Was darf man aufrufen?
Ohne Rechner. Rauchmelder hat zusätzlich eine Methode boolean testeBatterie(), die es in Alarm nicht gibt. Gegeben sind drei Variablen:
Alarm a = new Rauchmelder("Kueche");
Rauchmelder r = new Rauchmelder("Bad");
Alarm b = new Alarm("Flur");
Entscheide für jede Zeile: übersetzt sie? Und wenn ja: was passiert beim Ausführen? Begründe jedes Mal mit dem statischen bzw. dem dynamischen Typ.
// 1
IO.println(a.lautstaerke());
// 2
IO.println(a.testeBatterie());
// 3
IO.println(r.meldung());
// 4
Rauchmelder x = a;
// 5
Rauchmelder y = (Rauchmelder) a;
IO.println(y.testeBatterie());
// 6
Alarm c = r;
IO.println(c.lautstaerke());
Tipp: zwei Fragen, immer in dieser Reihenfolge
- Darf ich das? Diese Frage beantwortet der Übersetzer, und zwar allein am deklarierten Typ der Variablen.
- Was passiert dann? Diese Frage beantwortet erst die Laufzeit, und zwar am tatsächlichen Objekt.
Frage 2 stellt sich nur, wenn Frage 1 mit ja beantwortet ist.
Teil 2: Schreiben
Aufgabe 3: Formen
Setze die Vererbungshierarchie so um, dass alle Tests grün werden.
Achte darauf, welche Klassen flaeche() überschreiben müssen und welche nicht – und schreib keine Methode, die schon richtig geerbt wird.
classDiagram
class Form {
#bezeichnung: String
+Form(pBezeichnung: String)
+getBezeichnung() String
+flaeche() double
+groesserAls(pAndere: Form) boolean
}
class Rechteck {
-breite: double
-hoehe: double
+Rechteck(pBreite: double, pHoehe: double)
+flaeche() double
}
class Quadrat {
+Quadrat(pSeite: double)
}
class Kreis {
-radius: double
+Kreis(pRadius: double)
+flaeche() double
}
Form <|-- Rechteck
Rechteck <|-- Quadrat
Form <|-- Kreis
Tipp 1: Warum überschreibt Quadrat die Flächenmethode nicht?
Weil ein Quadrat ein Rechteck mit gleichen Seiten ist. Der Konstruktor gibt die Seitenlänge zweimal an Rechteck weiter – damit stimmt die geerbte Rechnung bereits.
Das ist Spezialisierung in Reinform: Nur das Besondere wird ergänzt, hier die Bezeichnung.
Tipp 2: Die Bezeichnung im Quadrat ändern
bezeichnung ist in Form als protected deklariert. Eine Unterklasse darf also direkt darauf schreiben:
bezeichnung = "Quadrat";
Das muss nach dem super(...)-Aufruf stehen – vorher gibt es das Attribut noch nicht.
Tipp 3: Der vorletzte Test
groesserAls steht nur in Form und wird nirgends überschrieben. Trotzdem vergleicht sie beim Quadrat 25 mit 12.
Sie ruft dafür zweimal flaeche() auf – einmal am eigenen Objekt, einmal am übergebenen. Beide Aufrufe werden dynamisch gebunden.
Zum Weiterdenken
Vertiefung 1: Überschreiben ist nicht Überladen
Zwei Dinge sehen sich ähnlich und funktionieren gegensätzlich:
- Überschreiben: dieselbe Signatur noch einmal, in einer Unterklasse.
- Überladen: derselbe Methodenname, aber andere Parameter, in derselben Klasse.
a) Sag voraus, was das Programm ausgibt. Die dritte Zeile ist die interessante.
b) Führ es aus. Warum steht in Zeile 3 nicht dasselbe wie in Zeile 2, obwohl in beiden Fällen dasselbe Objekt übergeben wird?
c) Ergänze die Regel: Überschreiben wird … gebunden, Überladen wird … gebunden.
d) Beurteile: Warum ist das so gebaut? Was müsste der Übersetzer sonst können?
Vertiefung 2: Ein Konstruktor, der zu früh fragt
Der folgende Quelltext sieht harmlos aus und ist eine der unangenehmsten Fallen der Objektorientierung.
a) Sag voraus, was neu.getProtokoll() und neu.beschriftung() liefern.
b) Führ es aus. Erkläre den Unterschied. In welcher Reihenfolge laufen die beiden Konstruktoren, und wann bekommt stufe seinen Wert?
c) Warum ist dieser Fehler besonders schwer zu finden? Denk daran, wann er auffällt.
d) Nenne zwei Regeln, mit denen man ihn vermeidet.