7.5 Drum prüfe, wer sich dynamisch bindet
Bei der Vererbung haben wir eine Form der Ist-eine-Art-von-Beziehung, sodass die Unterklassen immer auch vom Typ der Oberklassen sind. Die sichtbaren Methoden, die die Oberklassen besitzen, existieren somit auch in den Unterklassen. Der Vorteil bei der Spezialisierung ist, dass die Oberklasse eine einfache Implementierung vorgibt und eine Unterklasse diese überschreiben kann. Wir hatten das bisher bei toString() gesehen. Doch nicht nur die Spezialisierung ist aus Sicht des Designs interessant, sondern auch die Bedeutung der Vererbung. Bietet eine Oberklasse eine sichtbare Methode an, so wissen wir immer, dass alle Unterklassen diese Methode haben werden, egal, ob sie die Methode überschreiben oder nicht. Wir werden gleich sehen, dass dies zu einem der wichtigsten Konstrukte in objektorientierten Programmiersprachen führt.
7.5.1 Gebunden an toString()
Da jede Klasse Eigenschaften von java.lang.Object erbt, lässt sich auf jedem Objekt die toString()-Methode aufrufen. In unseren Beispielen haben die Klassen Object, Event, Workout alle eine eigene toString()-Methode. Event überschreibt toString() von Object, Workout überschreibt toString() von Event.
Es gibt nun ein interessantes Szenario, wenn die toString()-Methode aufgerufen wird, aber der Referenztyp und Objekttyp unterschiedlich sind.
Workout ww = new Workout();
ww.about = "Running";
ww.duration = 100;
ww.caloriesBurned = 300;
System.out.println( ww.toString() );
Event ew = new Workout();
ew.about = "Running";
ew.duration = 100;
System.out.println( ew.toString() );
Object ow = new Workout();
System.out.println( ow.toString() );
Dreimal findet ein Aufruf von toString() statt, wobei der Objekttyp immer gleich bleibt (Workout), aber der Referenztyp immer ein anderer ist (Workout, Event, Object).
Jetzt ist die spannendste Frage in der gesamten Objektorientierung folgende: Was passiert bei dem Methodenaufruf toString()? Antwort:
Workout[about=Running, duration=100][caloriesBurned=300]
Workout[about=Running, duration=100][caloriesBurned=0]
Workout[about=null, duration=0][caloriesBurned=0]
Die Ausgabe ist leicht zu verstehen, wenn wir berücksichtigen, dass es zwei Typsysteme gibt und der Compiler nicht die gleiche Weisheit besitzt wie die Laufzeitumgebung. Entscheidend ist, dass die Laufzeitumgebung beim Methodenaufruf auf den Objekttyp schaut und nicht auf den Referenztyp – das ist das gleiche Verhalten wie bei instanceof. Da dem im Programmtext vereinbarten Variablentyp nicht zu entnehmen ist, welche Implementierung der Methode toString() aufgerufen wird, sprechen wir von später dynamischer Bindung, kurz dynamischer Bindung. Erst zur Laufzeit (das ist spät, im Gegensatz zur Übersetzungszeit) wählt die Laufzeitumgebung dynamisch die entsprechende Objektmethode aus – passend zum tatsächlichen Typ des aufrufenden Objekts. Die virtuelle Maschine weiß, dass hinter den drei Variablen jeweils ein Workout-Objekt steht, und ruft daher das toString() vom Workout auf.
Programmiersprachenvergleich
Dynamische Bindung ist in Java automatisch und lässt sich auch nicht über einen Modifizierer steuern. In C++ muss das Schlüsselwort virtual vor der virtuellen Funktion stehen, damit dynamisch gebunden wird.
Wichtig ist, dass eine Methode überschrieben wird. Nehmen wir an, es gäbe in Object kein toString(), sondern nur eine Implementierung in den Unterklassen Nap und Workout. Davon hätten wir nichts! Wir nutzen daher ausdrücklich die Gemeinsamkeit, dass Event, Workout und weitere Unterklassen toString() aus Object erben. Ohne die Oberklasse gäbe es kein Bindeglied, und folglich bietet die Oberklasse immer eine Methode an, die Unterklassen überschreiben können. Würden wir eine neue Unterklasse von Object schaffen und toString() nicht überschreiben, so fände die Laufzeitumgebung toString() in Object, aber die Methode gäbe es auf jeden Fall – entweder die Originalmethode oder die überschriebene Variante.
[»] Begrifflichkeit
Dynamische Bindung wird oft auch Polymorphie genannt; ein dynamisch gebundener Aufruf ist dann ein polymorpher Aufruf. Das ist im Kontext von Java in Ordnung, allerdings gibt es in der Welt der Programmiersprachen unterschiedliche Dinge, die »Polymorphie« genannt werden, etwa parametrische Polymorphie (in Java heißt das Generics), und die Theoretiker kennen noch viel mehr beängstigende Begriffe.
7.5.2 Implementierung von System.out.println(Object)
Werfen wir einen Blick auf ein Programm, das dynamisches Binden noch deutlicher macht. Die print*(…)-Methoden sind so überladen, dass sie jedes beliebige Objekt annehmen und dann die String-Repräsentation ausgeben:
public void println( Object x ) {
String s = String.valueOf( x );
// String s = (obj == null) ? "null" : obj.toString();
synchronized ( this ) {
print( s );
newLine();
}
}
Die println(Object)-Methode besteht aus drei Teilen: Als Erstes wird die String-Repräsentation eines Objekts erfragt – hier findet sich der dynamisch gebundene Aufruf –, dann wird dieser String an print(String) weitergegeben, und newLine() produziert abschließend den Zeilenumbruch.
Der Compiler hat überhaupt keine Ahnung, was x ist; es kann alles sein, denn alles ist ein java.lang.Object. Statisch lässt sich aus dem Argument x nichts ablesen, und so muss die Laufzeitumgebung entscheiden, an welche Klasse der Methodenaufruf geht. Das ist das Wunder der dynamischen Bindung.
IntelliJ zeigt bei der Tastenkombination (Strg)+(H) eine Typhierarchie an, standardmäßig die Oberklassen und bekannten Unterklassen.