Informatik

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.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

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.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

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
  1. Darf ich das? Diese Frage beantwortet der Übersetzer, und zwar allein am deklarierten Typ der Variablen.
  2. 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.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

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.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

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?

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

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.

Lösung. Erfrage das Passwort bei deiner Lehrkraft.

Selbsttest

Polymorphie

Teilbare URL erstellen

Abschnitte auswählen