6.8 Vererbung
Neben der Assoziation von Objekten gibt es in der Objektorientierung eine weitere wichtige Möglichkeit zur Wiederverwendung, die Vererbung. Sie basiert auf der Idee, dass Eltern ihren Kindern Eigenschaften mitgeben. Vererbung bindet die Klassen noch dichter aneinander. Mittels dieser engen Verbindung können wir später sehen, dass Klassen in gewisser Weise austauschbar sind.
6.8.1 Vererbung in Java
Die Klassen in Java sind in einer Hierarchie geordnet. Von Object erben automatisch alle Klassen, direkt oder indirekt. Eine neu definierte Klasse kann durch das Schlüsselwort extends eine Klasse erweitern. Sie wird dann zur Unter- oder Subklasse beziehungsweise Kindklasse. Die Klasse, von der die Unterklasse erbt, heißt Oberklasse (auch Superklasse oder Elternklasse). Durch den Vererbungsmechanismus werden alle sichtbaren Eigenschaften der Oberklasse auf die Unterklasse übertragen. Eine Oberklasse vererbt also Eigenschaften, und die Unterklasse erbt sie.
Syntaktisch wird die Vererbung durch das Schlüsselwort extends beschrieben. Allgemein gilt für eine erbende Klasse Unter und eine Oberklasse Ober:
class Unter extends Ober
{
}
Alles, was nun Ober an sichtbaren Eigenschaften hat, wird auf Unter vererbet, kann Unter also nutzen. Wenn sich die Implementierung einer Methode der Oberklasse ändert, so wird die Unterklasse diese Änderung mitbekommen.
Hier klicken, um das Bild zu Vergrößern
Eclipse zeigt bei der Tastenkombination (Ctrl)+(T) eine Typhierarchie an.
6.8.2 Einfach- und Mehrfachvererbung
In Java ist auf direktem Weg nur die Einfachvererbung (engl. single inheritance) erlaubt. In der Einfachvererbung kann eine Klasse lediglich eine andere erweitern. In Programmiersprachen wie C++ können auch mehrere Klassen zu einer neuen verbunden werden. Dies bringt aber einige Probleme mit sich, die in Java vermieden werden. Wie müsste etwa eine Unterklasse auf die Eigenschaften einer Oberklasse zugreifen, wenn beide Oberklassen das gleiche Attribut oder die gleiche Methode definieren? Wenn die Unterklasse etwas von der Oberklasse nutzt, welche Oberklasse soll angesprochen werden? In C++ wird das durch einen Scope-Operator (::) gelöst, so dass dem Programmierer bewusst sein muss, welche Eigenschaft von welcher Oberklasse kommt.
Dazu gesellt sich auch das Diamanten- oder Rauten-Problem. Zwei Klassen K1 und K2 erben von einer Oberklasse O eine Eigenschaft x. Eine Unterklasse U könnte von den Klassen K1 und K2 erben. Was ist mit der Eigenschaft x in O? Da sie eigentlich nur einmal vorliegt, dürfte es keinen Grund zur Sorge geben. Dennoch stellt dieses Szenario ein Problem dar, welches durch Einfachvererbung nicht entstehen kann. Da die Mehrfachvererbung in Java nicht gültig ist, steht letztendlich hinter dem Schlüsselwort extends lediglich eine einzige Klasse.
6.8.3 Gebäude modelliert
Wir wollen nun eine Klassenhierarchie für Gebäude aufbauen. Die Hierarchie geht von oben nach unten, von der Oberklasse zur Unterklasse. Im Fall einer Diskothek ist diese ein Gebäude, so dass schon die erste Ist-eine-Art-von Hierarchie existiert. Eine Diskothek ist eine Art Gebäude. Es lassen sich auch spezielle Diskotheken vorstellen, etwa Kinderdiskotheken. Wir können auch dann sagen: Eine Kinderdisko ist eine spezielle Art von Diskothek.
Schreiben wie die Hierarchie für Kinderdiskotheken auf. Der Quellcode der Oberklasse Disko muss dazu nicht geändert werden. Das ist typisch für die Modellierung mit Klassenhierarchien - die Oberklasse weiß gar nichts von einer Unterklasse!
Beginnen wir mit der Basisklasse Disko.
Listing 6.25 v7/Disko.java
public class Disko
{
...
}
Da keine ausdrückliche extends-Anweisung hinter dem Klassennamen steht, erbt die Klasse automatisch von Object, einer impliziten Basisklasse. Das ist jetzt nicht sonderlich spannend, aber KinderDisko wird interessant.
Listing 6.26 v7/KinderDisko.java
public class KinderDisko extends Disko
{
public String maskottchen;
}
Die Definition der Klasse trägt den Anhang extends Disko und erbt somit alle sichtbaren Eigenschaften der Oberklasse. Sie selbst fügt der Klasse nur ein Attribut maskottchen zu.
Damit ergibt sich das nachfolgende UML-Diagramm. Vererbung ist durch einen Pfeil in Richtung der Oberklasse angegeben:
Hier klicken, um das Bild zu Vergrößern
Abbildung 6.7 Eine Kinderdisko ist eine Spezialisierung einer Disko
Haben wir ein KinderDisko-Objekt erzeugt, können wir auf alle Eigenschaften der Kinderdisko zugreifen, aber auch auf die Eigenschaften, die geerbt wurden.
Listing 6.27 Ausschnitt aus KiDiDemo.java
KinderDisko saloon = new KinderDisko();
saloon.maskottchen = "Yosemite Sam";
saloon.personRein();
6.8.4 Konstruktoren in der Vererbung
Obwohl Konstruktoren Ähnlichkeiten mit Methoden haben, etwa in der Eigenschaft, dass sie überladen werden oder Ausnahmen erzeugen können, werden sie im Gegensatz zu Methoden nicht vererbt. Das heißt, eine Unterklasse muss ganz neue Konstruktoren angeben, denn mit den Konstruktoren der Oberklasse kann ein Objekt der Unterklasse nicht erzeugt werden. Ob das nun reine Objektorientierung ist, kann diskutiert werden, in der Sprache Python etwa werden auch Konstruktoren vererbt. In Java gehören Konstruktoren eigentlich zum statischen Teil einer Klasse. Die Klasse selbst weiß, wie neue Objekte konstruiert werden. Sehen wir Konstruktoren eher als Initialisierungsmethoden an, läge es natürlich näher, sie wie Objektmethoden zu behandeln. Dagegen spricht jedoch, dass eine Unterklasse mehr Eigenschaften hat und der Konstruktor der Oberklasse dann nur einen Teil initialisieren würde.
In Java sammelt eine Unterklasse zwar automatisch alle sichtbaren Eigenschaften der Oberklasse, aber die Objekte in der Hierarchie existieren einzeln. Das heißt, wenn eine Unterklasse erzeugt wird, dann ruft der Konstruktor der Unterklasse automatisch den Standard-Konstruktor der Oberklasse auf, um das obere Objekt zu initialisieren. Es ist dabei egal, ob der Konstruktor in der Unterklasse parametrisiert ist oder nicht; jeder Konstruktor der Unterklasse muss einen der Oberklasse aufrufen.
Der Aufruf wird meistens automatisch vom Compiler eingefügt, und eine Modifikation des Bytecodes würde die Aufrufreihenfolge empfindlich stören, denn im Bytecode gibt es diese Verpflichtung nicht. Die Sprache sieht für den ausdrücklichen Aufruf des Konstruktors der Oberklasse die Anweisung super() vor. Die Referenz super zeigt nur auf ein Objekt der Oberklasse. Mit Klammern ist immer ein Aufruf verbunden. Erinnern wir uns an dieser Stelle noch einmal an this und this().
Ein Beispiel mit Konstruktorweiterleitung
Sehen wir uns noch einmal die Konstruktorverkettung an:
class Gebaeude
{
}
class Disko extends Gebaeude
{
}
Da wir keine expliziten Konstruktoren haben, fügt der Compiler zwei Standard-Konstruktoren ein. Sie rufen zudem den Standard-Konstruktor der Oberklasse auf. Daher ergibt sich folgendes Bild in den Klassen für die Laufzeitumgebung im Bytecode:
class Gebaeude
{
Gebaeude() {
super(); // für Object()
}
}
class Disko extends Gebaeude
{
Disko() {
super(); // für Gebaeude()
}
}
Wir sehen, dass wir nicht ausdrücklich super() schreiben müssen, da es der Compiler macht.
Ein unnötiges super() in der ersten Zeile?
In vielen Java-Programmen (auch in der Java-Klassenbibliothek, besonders bei den Ausnahmen) steht aber trotzdem in der ersten Zeile des Konstruktors super(). So zum Beispiel in der Klasse Vector:
public Vector(int initialCapacity, int capacityIncrement)
{
super();
...
}
oder in der Klasse IOException:
public IOException()
{
super();
}
Wie wir gesehen haben, ist dies nicht notwendig, kann aber die Lesbarkeit fördern. Wir sind uns dann sofort bewusst, dass die »Methode« ein Konstruktor ist und dass der Standard-Konstruktor aufgerufen wird.
super() mit Parameter aufrufen
Mitunter ist es nötig, nicht nur den Standard-Konstruktor anzusteuern, sondern einen anderen Konstruktor der Oberklasse, den wir uns aussuchen wollen. Dazu kann super() mit Parametern gefüllt werden. Gründe dafür könnten sein:
1. |
Ein parametrisierter Konstruktor der Unterklasse leitet oft die Parameter an die Oberklasse weiter. |
2. Wenn wir keinen Standard-Konstruktor in der Oberklasse anbieten, dann müssen wir in der Unterklasse mittels super(Parameter,...) einen speziellen, parametrisierten Konstruktor aufrufen.
Dazu noch einmal einen Blick auf die Implementierung von IOException, wo wir direkt die Zeichenkette weiter nach oben geben:
public class IOException extends Exception
{
public IOException()
{
super();
}
public IOException( String s )
{
super(s);
}
}
6.8.5 Sichtbarkeit
Die Vererbung kann durch private eingeschränkt werden. Eine Subklasse erbt dann alles von einer Superklasse, was nicht private ist. Zusätzlich kommt zu private noch eine Sonderform protected hinzu. Hier kann auch eine Unterklasse alle Eigenschaften sehen. Nur von außen sind die Eigenschaften privat. Eine Ausnahme bilden jedoch Klassen, die im gleichen Paket sind; auch sie können die Eigenschaften eine protected-Klasse sehen. Damit ist protected mehr als nur die Sichtbarkeit für Unterklassen, denn wenn auch Klassen im gleichen Paket lesen können, ist die Sichtbarkeit fast public. Nehmen wir eine Klasse K und L im Paket p an. Deklariert K die Attribute protected, so kann L diese Lesen und Modifizieren.
Machen wir ein anderes Beispiel mit zwei Klassen aus zwei unterschiedlichen Paketen. Ein gemeinsames Oberpaket pakettest besitzt die beiden Unterpakete berlusconi und stefani mit den Klassen Silvio und Stefano.
Listing 6.28 pakettest.berlusconi.Silvio
package pakettest.berlusconi;
public class Silvio
{
protected String schulz = "einförmiger, supernationalistischer Blonde";
}
Die Klasse Silvio definiert lediglich eine geschützte Variable schulz. Sie wird also von allen Unterklassen und auch allen Klassen im gleichen Paket nutzbar sein. Wir definieren eine zweite Klasse Stefano jedoch in einem anderen Paket:
Listing 6.29 pakettest.stefani.Silvio
package pakettest.stefani;
import pakettest.berlusconi.Silvio;
public class Stefano extends Silvio
{
String übernommen = schulz;
void demokratieverständnis( Silvio s )
{
// s.schulz ist hier nicht definiert
}
}
Obwohl eine Unterklasse die protected-Eigenschaft schulz nutzen kann (in übernommen = schulz ist ein Zugriff auf die Oberklassenvariable), kann sie nicht über den Typ Silvio auf schulz zugreifen.
6.8.6 Das Substitutionsprinzip
Stellen wir uns vor, Bekannte kommen ausgehungert von einer Wandertour und fragen: »Haste was zu essen?«. Die Frage zieht wohl darauf ab, dass es bei Hunger ziemlich egal ist, was wir anbieten, wichtig ist nur etwas Essbares. Daher können wir Eis, aber auch Frittierfett anbieten.
Diese Ausgangslage führt uns zu einem wichtigen Konzept in der Objektorientierung: wenn wenig gefordert wird, kann mehr angeboten werden. Genauer gesagt, wenn eine Unterkasse U die Oberklasse O erweitert, können wir überall, wo O gefordert wird, etwa als Parameter einer Funktion, auch ein U übergeben, denn wir werden mit der Unterklasse nur spezieller. Derjenige, dem wir mehr übergeben, kann damit zwar nichts anfangen, aber ablehnen wird er das Objekt nicht, da es alle geforderten Eigenschaften aufweist.
Da an Stelle eines Objekts auch ein Objekt der Unterklasse auftauchen kann, sprechen wir von Substitution. Das Prinzip wurde von Professorin Barbara Liskov1 formuliert und nennt sich daher auch Liskov'sches Substitutionsprinzip.
Hier klicken, um das Bild zu Vergrößern
Bleiben wir bei unserem Beispiel des Parameters. Für unsere Disko-Vererbungsbeziehung heißt das, überall dort, wo ein Gebäude gefordert ist, können wir eine Disko übergeben oder auch eine Kirche, wenn Kirche eine Unterklasse von Gebäude ist. Auch können wir eine Kinderdisko eingeben, wenn sie eine Unterklasse von Disko ist. Denn alle diese Dinge sind vom Typ Gebäude und daher typkompatibel.
In der Java-Bibliothek finden sich endlose weitere Beispiele. Häufigstes Anwendungsfeld sind Datenstrukturen - etwa eine Liste. Die Datenstrukturen nehmen beliebige Objekte entgegen, denn der Parametertyp ist Object - zu sehen etwa an der Methode add(Object) in java.util.ArrayList, der Klasse für Listen. Die Substitution besagt, dass wir alle Objekte dort einsetzen können, da alle Klassen von Object abgeleitet sind.
6.8.7 Automatische und explizite Typanpassung
Das nachfolgende Beispiel zeigt, dass auch ein Exemplar einer Unterklasse einer Variablen vom Typ der Oberklasse zugewiesen werden kann. Wir erzeugen zunächst ein KinderDisko-Objekt:
KinderDisko saloon = new KinderDisko();
Dikso d = saloon;
Da eine Kinderdisko eine spezielle Disko ist (KinderDikso ist Unterklasse von Disko), funktioniert diese Zuweisung. Auf den ersten Blick erscheint das nicht sonderlich sinnvoll, erfüllt aber einen Zweck: d übernimmt alle Eigenschaften einer Disko von der mächtigeren Klasse KinderDisko, verzichtet aber auf alle anderen Informationen, die eine Kinderdisko oder sonstige Unterklasse noch bietet, beispielsweise das Attribut maskottchen.
Die Klasse Disko bietet dabei das Attribut anzahlLeute an, so dass auch Folgendes problemlos ist:
System.out.println( d.anzahlLeute );
Versuchen wir aber eine spezielle Eigenschaft von KinderDisko zu benutzen, etwa das Attribut maskottchen, so ist dies nicht möglich:
System.out.println( d.maskottchen ); // geht nicht
Hier ist der Typ der Variable d entscheidend. Der Compiler hat d vom Typ Disko kennen gelernt, daher weiß er nicht, dass d eigentlich ein verkapptes KinderDisko-Objekt ist.
Genauso gut lässt sich keine neue Referenz vom Typ KinderDikso auf die Disko legen. Hier gilt wiederum, dass die Typen unvereinbar sind, so dass wir einen Compilerfehler erhalten:
KinderDisko kd = d; // geht nicht
Es ist aber möglich, das Objekt d durch eine Typumwandlung in eine KinderDisko umzuwandeln. Dies funktioniert aber lediglich dann, wenn d auch wirklich eine Kinderdisko ist. Dem Compiler ist dies in dem Moment egal. Diese Bedingung wird erst zur Laufzeit geprüft:
KinderDikso kd = (KinderDikso) d; // geht wohl
Wir werden in den folgenden beiden Abschnitten nun kennen lernen, wieso das Sinn macht und es ein mächtiges Konzept ist. Wir werden sehen, dass eine Basisklasse geschaffen werden kann und diese verschiedenen Unterklassen Grundfunktionalität beibringen kann. So liefert die Basisklasse einen gemeinsamen Nenner.
Fassen wir die oberen Zeilen noch einmal in einem kompletten Programm zusammen:
Listing 6.30 DiskoSubstitution.java
public class DiskoSubstitution
{
public static void main( String args[] )
{
KinderDisko saloon = new KinderDisko();
saloon.maskottchen = "Yosemite Sam";
saloon.personRein();
Disko zumDickenHomer = new KinderDisko();
// zumDickenHomer.maskottchen = "Homer Simpson";
zumDickenHomer.personRein();
Object hase = new KinderDisko();
// hase.maskottchen = "Bugs Bunny";
// hase.personRein();
}
}
6.8.8 Finale Klassen
Soll eine Klasse keine Unterklassen bilden, so werden Klassen mit dem Modifizierer final versehen. Dadurch kann vermieden werden, dass Unterklassen Eigenschaften nachträglich verändern können. Ein Versuch, von einer finalen Klasse zu erben, führt zu einem Compilerfehler. Dies schränkt zwar die objektorientierte Wiederverwendung ein, wird aber aufgrund von Sicherheitsaspekten in Kauf genommen. Eine Passwortüberprüfung soll zum Beispiel nicht einfach überschrieben werden können.
In der Java-Bibliothek gibt es eine Reihe von finalen Klassen, von denen wir einige auch schon kennen gelernt haben:
|
String, StringBuffer |
|
Wrapper-Klasssen |
|
Math |
|
System |
|
Font, Color |
6.8.9 Unterklassen prüfen mit dem Operator instanceof
In Java haben die Entwickler den Operator instanceof in den Wortschatz aufgenommen, mit dem Exemplare auf ihre Verwandtschaft mit einer Klasse geprüft werden können. Mit instanceof kann zur Laufzeit festgestellt werden, ob ein definiertes Objekt vom Typ einer Klasse ist. Dies ist sinnvoll, denn durch objektorientiertes Programmieren werden laufend Basisobjekte definiert und erweitert, und zum Teil verschwindet der Typ für den Compiler, wenn etwa Objekte in Datenstrukturen gelegt werden.
boolean b;
String str = "Toll";
b = ( str instanceof String ); // wahr
b = ( str instanceof Object ); // wahr
b = ( str instanceof Date ); // nö
Deklariert ist eine Variable str als Objekt vom Typ String. Für den zweiten Fall gilt: Alle Objekte gehen irgendwie aus Object hervor und sind somit logischerweise Erweiterungen. Im dritten Fall ist Date keine Basisklasse für String, der Ausdruck ist falsch. In Test auf instanceof null ist immer falsch. Falls zur Übersetzungszeit schon feststeht, dass der Test falsch sein wird, meldet der Compiler schon einen Fehler:
Object o = new int[100];
boolean bo = ( o instanceof String ); // für den Compiler O.K.
boolean bf = ( new int[100] instanceof String ); // mag der Compiler nicht
6.8.10 Methoden überschreiben
Wir haben gesehen, dass durch Vererbung eine Unterklasse die sichtbaren Eigenschaften erbt. Die Unterklasse kann nun wiederum Methoden hinzufügen. Dabei ist eine überladene Methode, also eine Funktion, die den gleichen Namen wie die Methode aus einer Oberklasse trägt, aber verschiedene Parameter hat, eine ganz normale, hinzugefügte Methode.
Eine Unterklasse kann eine Methode aber auch überschreiben. Dazu gibt es in der Unterklasse eine Methode mit der exakten Parameterliste, dem Methodennamen und dem Rückgabewert der Oberklasse. Implementiert die Unterklasse die Methode neu, so sagt sie auf diese Weise: »Ich kann's besser«. Die überschreibende Methode kann demnach den Funktionscode spezialisieren und Eigenschaften nutzen, die in der Oberklasse nicht bekannt sind. Überladene Funktionen und überschriebene Funktionen sind damit etwas anderes, da eine überladene Funktion mit der Ursprungsfunktion nur »zufällig« den Namen teilt, aber sonst kein Bezug zur Logik hat.
Beispiel In eine Disko dürfen nur Personen, die über 18 sind. Eine Methode personRein() soll daher testen, ob Personen Eintritt bekommen. Da KinderDisko von Disko erbt, hätten wir ein Problem, wenn wir personRein() so stehen lassen würden - dann würde kein Kind mehr in die Kinderdisko kommen! Wir überschreiben daher die Funktion personRein() in der Unterklasse KinderDisko, so dass auch Personen, die größer als 6 Jahre sind, in die Spezialdisko kommen.
|
Listing 6.31 Ausschnitt aus v9/Disko.java
public void personRein( int alter )
{
if ( alter > 18 )
anzahlPersonen++;
else
System.out.println( "Noch zu jung!" );
}
Listing 6.32 Ausschnitt aus v9/Disko.java
public class KinderDisko extends Disko
{
public String maskottchen;
public void personRein( int alter )
{
if ( alter > 6 )
anzahlPersonen++;
else
System.out.println( "Du bist noch zu klein! Du musst draußen spielen." );
}
}
Damit die Unterklasse auf das Attribut anzahlPerson zugreifen kann, machen wir es in der Oberklasse protected. Das UML-Diagramm zeigt es an der Raute.
Hier klicken, um das Bild zu Vergrößern
Abschließend schreiben wir uns noch ein Testprogramm. Wird ein Kinderdisko-Objekt angelegt, und rufen wir personRein() auf, so wird die überschriebene Funktion verwendet.
Listing 6.33 KiDiDomo.java
public class KiDiDemo
{
public static void main( String args[] )
{
Disko ttwister = new Disko();
ttwister.personRein( 45 );
ttwister.personRein( 10 ); // Noch zu jung!
KinderDisko flohzirkus = new KinderDisko();
flohzirkus.personRein( 10 );
flohzirkus.personRein( 5 ); // Du bist noch zu klein! Du musst draußen spielen.
}
}
}
Somit bieten sich generell drei Möglichkeiten für Methoden in der Unterklasse an: Hinzufügen, Überladen oder Überschreiben. Wird die Signatur eines Funktionsblocks beim Überschreiben nicht aufmerksam genug beachtet, wird unbeabsichtigt eine Methode überladen. Dieser Fehler ist schwer zu finden.
6.8.11 super: Aufrufen einer Methode aus der Oberklasse
Wenn wir eine Methode überschreiben, dann entscheiden wir uns für eine gänzlich neue Implementierung. Was ist aber, wenn die Funktionalität im Großen und Ganzen gut war und nur eine Kleinigkeit fehlte? In diesem Fall kann mit der Referenz super auf eine Eigenschaft der Oberklasse verwiesen werden. super ist vergleichbar mit this und kann auch genauso eingesetzt werden.
class PrahlerOber
{
int i = 1;
void m()
{
System.out.println( "Ich bin toll" );
}
}
class PrahlerUnter extends PrahlerOber
{
int i = 2;
void m()
{
super.m();
System.out.println( "Und ich bin noch toller" );
System.out.println( super.i );
System.out.println( i );
}
}
Die Methode m() aus PrahlerUnter bezieht sich mittels super.m() auf die Methode der Oberklasse. Im zweiten Fall können wir auf die verdeckte Objektvariable mit super.i zugreifen.
Wir sehen hier, dass super ein allgemeines Konzept ist, das eine Referenz auf die Oberklasse verwaltet. Wir rufen zwar in m() die überschriebene Methode auf, aber wir benötigen dafür ihren Namen. Änderungsfreundlicher wäre eine Variante wie super - die es aber nicht gibt -, denn dann müsste bei einer Änderung des Programmtexts nicht an den Methodennamen angepasst werden. Ein super() für die Oberklasse existiert nur für Konstruktoren.
Eine Aneinanderreihung von super-Schlüsselwörtern bei einer tieferen Vererbungshierarchie ist nicht möglich. Hinter einem super muss eine Objekteigenschaft stehen. Anweisungen wie super.super.i sind somit immer ungültig. Für Variablen gibt es jedoch eine Möglichkeit, die sich durch einen Cast in die Oberklasse ergibt. Wir erfinden eine neue Unterklasse Oberangeber, die wiederum von PrahlerUnter erbt.
class Oberangeber extends PrahlerUnter
{
void m()
{
System.out.println( ((PrahlerOber)this).i );
}
}
Die this-Referenz entspricht einem Objekt vom Typ Oberangeber. Wenn wir dies aber in den Typ PrahlerOber konvertieren, bekommen wir genau das i aus der Basisklasse unserer Hierarchie. Wir erkennen hier eine sehr wichtige Eigenschaft von Java, nämlich, dass Variablen nicht dynamisch gebunden werden. Anders wäre es, wenn wir die Funktion ändern in
void m()
{
((PrahlerOber)this).m();
}
Meine Leser sollten einmal testen, warum hier nicht m() aus PrahlerOber aufgerufen wird, sondern etwas anderes. Eine genauere Erklärung des dynamischen Bindens folgt gleich.
this == super
In unseren Köpfen existiert vielleicht die Idee, dass die Referenz super ein Verweis auf ein Objekt der Oberklasse ist. Dies ist aber so nicht ganz korrekt, obwohl diese Ausdrucksweise sich so eingebürgert hat. Es handelt sich für die Unterklasse um ein Objekt, welches alle Eigenschaften der Oberklasse übernimmt. Dass die Realisierung der virtuellen Maschine anders aussieht, ist kein Problem für uns; wir stellen uns vor, dass es genau ein Objekt gibt, das alles sammelt und verwaltet. this ist dann immer noch eine Referenz auf das aktuelle Objekt, und super bezeichnet das gleiche Objekt, nur als Typ der Oberklasse. Das soll das nachfolgende Beispiel noch einmal zeigen:
Listing 6.34 ThisIstWirklichSuper.java
public class ThisIstWirklichSuper
{
void out()
{
System.out.println( super.toString());
System.out.println( this.toString());
System.out.println( super.equals( this ) );
}
public static void main( String args[] )
{
ThisIstWirklichSuper o = new ThisIstWirklichSuper();
o.out();
}
}
Die Referenz super kann nur im Zusammenhang mit einer Eigenschaft verwendet werden. Einfach nur super auszugeben, führt zu einem Übersetzungsfehler. Es lässt sich jedoch super.toString() einsetzen, um eine einfache String-Repräsentation zu bekommen. Diese fällt natürlich anders aus, wenn die aktuelle Klasse toString() überschreibt. Ähnliches gilt für equals() im obigen Beispiel.
System.out.println( super ); // Das geht nicht.
Wenn wir das obere Programm ausführen, erhalten wir zum Beispiel folgende Ausgabe:
ThisIstWirklichSuper@8f14553c
ThisIstWirklichSuper@8f14553c
true
Wir erinnern uns: Die toString()-Methode von Object ist so implementiert, dass wir den Hash-Wert und den Namen der Klasse sehen. Beide zeigen auf das gleiche Objekt.
6.8.12 Nicht überschreibbare Funktionen
In der Vererbungshierarchie möchte ein Designer in manchen Fällen verhindern, dass Unterklassen eine Methode überschreiben und neu definieren. Da Methodenaufrufe immer dynamisch gebunden werden, könnte ein Aufrufer unbeabsichtigt in der Unterklasse landen. Das kann verhindert werden, indem das Schlüsselwort final vor die Methodendefinition gestellt wird.
Beispiel Die Oberklasse definiert die Methode sicher() final. Bei dem Versuch, in einer Unterklasse die Funktion zu überschreiben, meldet der Compiler einen Fehler.
|
class Ober
{
final void sicher() { }
}
class Unter extends Ober
{
void sicher() { } // Compilerfehler!
}
6.8.13 Fehlende kovariante Rückgabewerte
Bisher sieht die Java-Sprachdefinition nicht vor, dass es mehrere Methoden in einer Klasse geben kann, die sich nur in ihren Rückgabetypen unterscheiden. Das wären kovariante Rückgabewerte. Ebenfalls ist es nicht möglich, in einer Unterklasse eine Methode zu überschreiben, die einen anderen Rückgabetyp besitzt. Die Methode würde nicht überschrieben, und es gibt einen Compilerfehler, da ererbte und neue Methode gegen die Regel verstoßen, dass zwei Methoden sich nie nur im Rückgabetyp unterscheiden dürfen.
Im Design von großen Programmen wäre es jedoch günstig, wenn ein nichtprimitiver Rückgabetyp einer überschriebenen Funktion ebenfalls vom Typ dieser Unterklasse ist. Auf diese Weise ließen sich korrekt angepasste, überschriebene Methoden implementieren, und Entwickler könnten sich die expliziten Typanpassungen ersparen.
Betrachten wir zum Beispiel die clone()-Methode aus Object. Sie trägt ausdrücklich den Rückgabetyp Object. Auch eine Unterklasse muss den Rückgabetyp Object deklarieren, daher ist Folgendes leider falsch:
class X // automatisch extends Object
{
public X clone()
{
return (X)super.clone();
}
}
Der Aufrufer der Methode clone() von X muss selbstständig eine Typumwandlung auf X vornehmen.
Das Komische in diesem Zusammenhang ist, dass es schon veränderte Zugriffsrechte gibt. Eine Unterklasse kann die Sichtbarkeit erweitern. Auch bei Ausnahmen kann eine Unterklasse speziellere Ausnahmen beziehungsweise ganz andere als die Methode der Oberklasse erzeugen.
Die aktuelle Sprachdefinition von C++ unterstützt kovariante Rückgabewerte. Im Zuge der kommenden Version 1.5 werden auch kovariante Rückgabewerte in die Sprache Einzug erhalten. Auch einige Java-Erweiterungen, unter ihnen der freie GJ-Compiler, erlauben kovariante Rückgabewerte ohne Änderung der JVM durch Einfügen von Typanpassungen.
1 Die Zeitschrift »Discover« zählt sie zu den 50 wichtigsten Frauen in der Wissenschaft. Genauso übrigens wie Heidi Hammel, einer Astronomin, die sich dem Jupiter verschrieben hat und sich an bayerischem Starkbier erfreut.
|