7.8 Weiteres zum Überschreiben und dynamischen Binden
7.8.1 Nicht dynamisch gebunden bei privaten, statischen und finalen Methoden
Ist eine Methode privat, statisch oder final, wird sie nicht überschrieben, und solche Methoden nehmen nicht an der dynamischen Bindung teil. Für den Compiler ist es in Ordnung, wenn es eine Methode in der Unterklasse gibt, die den gleichen Namen wie eine private Methode in der Oberklasse trägt. Das ist auch gut so, denn private Implementierungen sind ja ohnehin geheim und versteckt. Die Unterklasse soll von den privaten Methoden in der Oberklasse gar nichts wissen. Statt von Überschreiben sprechen wir hier von Überdecken oder Verdecken.
Dass private, statische und finale Methoden nicht überschrieben werden, ist ein wichtiger Beitrag zur Sicherheit. Falls nämlich Unterklassen interne private Methoden überschreiben könnten, wäre dies eine Verletzung der inneren Arbeitsweise der Oberklasse. In einem Satz: Private Methoden sind nicht in den Unterklassen sichtbar und werden daher nicht überschrieben. Andernfalls könnten private Implementierungen im Nachhinein geändert werden, und Oberklassen wären nicht mehr sicher, dass nur ihre eigenen Methoden benutzt werden.
7.8.2 Kovariante Rückgabetypen
Überschreibt eine Methode mit einem Referenztyp als Rückgabe eine andere, so kann die überschreibende Methode als Rückgabetyp irgendeinen Untertyp des Rückgabetyps der überschriebenen Methode nutzen. Die Erklärung klingt komplizierter, als es ist, daher ein kurzes Beispiel:
class Event {}
class Workout extends Event {}
class Calendar {
Event first() { return new Event(); }
}
class WorkoutCalendar extends Calendar {
// @Override Event first() {
@Override Workout first() {
return new Workout();
}
}
Die Klasse Calendar deklariert eine Methode first() und liefert ein Event. Die Calendar-Unterklasse WorkoutCalendar überschreibt die Methode first() und könnte natürlich den Rückgabetyp Event nutzen, doch liefert sie ein Untertyp von Event, und zwar Workout. Diese Möglichkeit nennt sich kovarianter Rückgabetyp und ist sehr praktisch, da sich auf diese Weise Entwickler oft explizite Typumwandlung sparen können.
[»] Hinweis
Merkwürdig ist in diesem Zusammenhang, dass es in Java schon immer veränderte Zugriffsrechte gegeben hat. Eine Unterklasse kann die Sichtbarkeit erweitern. Auch bei Ausnahmen kann eine Unterklasse speziellere Ausnahmen bzw. ganz andere Ausnahmen als die Methode der Oberklasse erzeugen.
7.8.3 Array-Typen und Kovarianz *
Die Aussage »Wer wenig will, kann viel bekommen« gilt auch für Arrays, denn wenn eine Klasse U eine Unterklasse einer Klasse O ist, ist auch U[] ein Untertyp von O[]. Diese Eigenschaft nennt sich Kovarianz. Da Object die Basisklasse aller Objekte ist, kann ein Object-Array auch alle anderen Objekte aufnehmen.
Bauen wir uns eine statische Methode set(…), die einfach ein Element an die erste Stelle ins Array setzt:
public static void set( Object[] array, Object element ) {
array[ 0 ] = element;
}
Die Kovarianz ist beim Lesen von Eigenschaften nicht problematisch, beim Schreiben jedoch potenziell gefährlich. Schauen wir, was mit unterschiedlichen Array- und Elementtypen passiert:
Object[] objectArray = new Object[ 1 ];
String[] stringArray = new String[ 1 ];
System.out.println( "It's time for change" instanceof Object ); // true
set( stringArray, "It's time for change" );
set( objectArray, "It's time for change" );
set( stringArray, new StringBuilder("It's time for change") ); //
Der String lässt sich in einem String-Array abspeichern. Der zweite Aufruf funktioniert ebenfalls, denn ein String lässt sich auch in einem Object-Array speichern, da ein Object ja ein Basistyp ist. Vor einem Dilemma stehen wir dann, wenn das Array eine Referenz speichern soll, die nicht typkompatibel ist. Das zeigt der dritte set(…)-Aufruf: Zur Compilezeit ist alles noch in Ordnung, aber zur Laufzeit kommt es zu einer ArrayStoreException:
Exception in thread "main" java.lang.ArrayStoreException: java.lang.StringBuilder
at com.tutego.insel.oop.ArrayCovariance.set(ArrayCovariance.java:5)
at com.tutego.insel.oop.ArrayCovariance.main(ArrayCovariance.java:17)
Das haben wir aber auch verdient, denn ein StringBuilder-Objekt lässt sich nicht in einem String-Array speichern. Selbst ein new Object() hätte zu einem Problem geführt.
Das Typsystem von Java kann diese Spitzfindigkeit nicht zur Übersetzungszeit prüfen. Erst zur Laufzeit ist ein Test mit dem bitteren Ergebnis einer ArrayStoreException möglich. Bei Generics ist dies etwas anders, denn hier sind vergleichbare Konstrukte bei Vererbungsbeziehungen verboten.
7.8.4 Dynamisch gebunden auch bei Konstruktoraufrufen *
Dass ein Konstruktor der Unterklasse zuerst den Konstruktor der Oberklasse aufruft, kann die Initialisierung der Variablen in der Unterklasse stören. Schauen wir uns erst Folgendes an:
class Bouncer extends Bodybuilder {
String who = "Ich bin ein Rausschmeißer";
}
Wo wird nun die Variable who initialisiert? Wir wissen, dass die Initialisierungen immer im Konstruktor vorgenommen werden, doch gibt es ja noch gleichzeitig ein super() im Konstruktor. Da die Spezifikation von Java Anweisungen vor super() verbietet, muss die Zuweisung hinter dem Aufruf der Oberklasse folgen. Das Problem ist nun, dass ein Konstruktor der Oberklasse früher aufgerufen wird, als Variablen in der Unterklasse initialisiert wurden. Wenn es die Oberklasse nun schafft, auf die Variablen der Unterklasse zuzugreifen, wird der erst später gesetzte Wert fehlen. Der Zugriff gelingt tatsächlich, doch nur durch einen Trick, da eine Oberklasse (etwa Bodybuilder) nicht auf die Variablen der Unterklasse zugreifen kann. Wir können aber in der Oberklasse genau jene Methode der Unterklasse aufrufen, die die Unterklasse aus der Oberklasse überschreibt. Da Methodenaufrufe dynamisch gebunden werden, kann eine Methode den Wert auslesen:
class Bodybuilder {
Bodybuilder() {
whoAmI();
}
void whoAmI() {
System.out.println( "Ich weiß es noch nicht :-(" );
}
}
public class Bouncer extends Bodybuilder {
String who = "Ich bin ein Rausschmeißer";
@Override
void whoAmI() {
System.out.println( who );
}
public static void main( String[] args ) {
Bodybuilder bb = new Bodybuilder();
bb.whoAmI();
Bouncer bouncer = new Bouncer();
bouncer.whoAmI();
}
}
Die Ausgabe ist nun folgende:
Ich weiß es noch nicht :-(
Ich weiß es noch nicht :-(
null
Ich bin ein Rausschmeißer
Das Besondere an diesem Programm ist die Tatsache, dass überschriebene Methoden – hier whoAmI() – dynamisch gebunden werden. Diese Bindung gibt es auch dann schon, wenn das Objekt noch nicht vollständig initialisiert wurde. Daher ruft der Konstruktor der Oberklasse Bodybuilder nicht whoAmI() von Bodybuilder auf, sondern whoAmI() von Bouncer. Wenn in diesem Beispiel ein Bouncer-Objekt erzeugt wird, dann ruft Bouncer mit super() den Konstruktor von Bodybuilder auf. Dieser ruft wiederum die Methode whoAmI() in Bouncer auf, und er findet dort keinen String, da dieser erst nach super() gesetzt wird. Schreiben wir den Konstruktor von Bouncer einmal ausdrücklich hin:
public class Bouncer extends Bodybuilder {
String who;
Bouncer() {
super();
who = "Ich bin ein Rausschmeißer";
}
}
Die Konsequenz, die sich daraus ergibt, ist folgende: Dynamisch gebundene Methodenaufrufe über die this-Referenz sind in Konstruktoren potenziell gefährlich und sollten deshalb vermieden werden. Vermeiden lässt sich das, indem der Konstruktor nur private (oder finale) Methoden aufruft, da diese nicht dynamisch gebunden werden. Wenn der Konstruktor eine private (finale) Methode in seiner Klasse aufruft, dann bleibt es auch dabei.
7.8.5 Keine dynamische Bindung bei überdeckten Objektvariablen *
Der Kanadier »Furious Pete«[ 166 ](https://guinnessworldrecords.com/news/2016/4/competitive-eater-challenged-to-fastest-time-to-eat-a-12%E2%80%99%E2%80%99-pizza-record-guinnes-426366) isst alles in Rekordzeit; verewigen wir seine Pizzavertilgungsleistungen in einem Java-Programm. Die Oberklasse PizzaEater repräsentiert die Durchschnittsesser, die für eine 12"-Pizza geschätzte 900 Sekunden benötigen. FuriousPete ist eine Spezialisierung und schafft es in 32 Sekunden:
class PizzaEater {
int consumptionTime = 900 /* Seconds */;
void eat() {
System.out.printf( "Ich esse in %d Sekunden eine Pizza%n", consumptionTime );
}
}
public class FuriousPete extends PizzaEater {
int consumptionTime = 32 /* Seconds */;
@Override void eat() {
System.out.println( consumptionTime ); // 32
System.out.println( super.consumptionTime ); // 900
System.out.println( this.consumptionTime ); // 32
System.out.println( ((PizzaEater) this).consumptionTime ); // 900
}
public static void main( String[] args ) {
new FuriousPete().eat();
}
}
Die Oberklasse PizzaEater deklariert eine Objektvariable consumptionTime, und auch die Unterklasse deklariert eine Objektvariable mit dem gleichen Namen – es ist in Java zulässig, dass eine Objektvariable eine andere gleich benannte Objektvariable überdeckt.
Die Unterklasse kann mit super.consumptionTime eine Ebene höher kommen. super ist wie this eine spezielle Referenz und kann auch genauso eingesetzt werden, nur dass super in den Namensraum der Oberklasse geht. Eine Aneinanderreihung von super-Schlüsselwörtern bei einer tieferen Vererbungshierarchie ist nicht möglich. Hinter einem super muss direkt eine Objekteigenschaft stehen, und Anweisungen wie super.super.consumptionTime sind somit immer ungültig.
Bei Methodenaufrufen bindet das Laufzeitsystem immer dynamisch; bei Zugriffen auf Objektvariablen ist das nicht so: Hier bestimmt der Compiler, von welcher Klasse die Objektvariable genommen werden soll. Unser Programm zeigt das an der Anweisung:
System.out.println( ((PizzaEater) this).consumptionTime ); // 900
Die Ausgabe 900 ist identisch mit System.out.println(super.consumptionTime). Die this-Referenz hat in dem Kontext den Typ FuriousPete. Wenn wir den Typ aber in den Basistyp PizzaEater konvertieren, bekommen wir genau die Belegung von consumptionTime aus der Basisklasse unserer Hierarchie. Eine explizite Typumwandlung in Richtung eines Obertyps ist bei dynamisch gebundenen Methodenaufrufen auch nie nötig; die Laufzeitumgebung entscheidet selbstständig, wohin der Aufruf geht. Setzen wir in die eat()-Methoden vom FuriousPete die Zeile
((PizzaEater) this).eat();
ist das identisch mit
eat();
also eine Rekursion.