|
Eines der wesentlichen Designmerkmale objektorientierter Sprachen ist die Zusammenfassung von Variablen und Methoden zu Klassen. Eine weiteres wichtiges Merkmal ist das der Vererbung, also der Möglichkeit, Eigenschaften vorhandener Klassen auf neue Klassen zu übertragen. Fehlt dieses Merkmal, bezeichnet man die Sprache (gem. Booch) auch als lediglich objektbasiert.
Man unterscheidet dabei zwischen einfacher Vererbung, bei der eine Klasse von maximal einer anderen Klasse abgeleitet werden kann, und Mehrfachvererbung, bei der eine Klasse von mehr als einer anderen Klasse abgeleitet werden kann. In Java gibt es lediglich Einfachvererbung, um den Problemen aus dem Weg zu gehen, die durch Mehrfachvererbung entstehen können. Um die Einschränkungen in den Designmöglichkeiten, die bei Einfachvererbung entstehen, zu vermeiden, wurde mit Hilfe der Interfaces eine neue, restriktive Art der Mehrfachvererbung eingeführt. Wir werden später darauf zurückkommen.
Um eine neue Klasse aus einer bestehenden abzuleiten, ist im Kopf der Klasse mit Hilfe des Schlüsselworts extends ein Verweis auf die Basisklasse anzugeben. Hierdurch erbt die abgeleitete Klasse alle Eigenschaften der Basisklasse, d.h. alle Variablen und alle Methoden. Durch Hinzufügen neuer Elemente oder Überladen der vorhandenen kann die Funktionalität der abgeleiteten Klasse erweitert werden.
Wir können nun nicht nur auf die neue Variable vdauer, sondern auch auf alle Elemente der Basisklasse Auto zugreifen:
001 Cabrio kfz1 = new Cabrio(); 002 kfz1.name = "MX5"; 003 kfz1.erstzulassung = 1994; 004 kfz1.leistung = 115; 005 kfz1.vdauer = 120; 006 System.out.println("Alter = "+kfz1.alter()); |
Die Vererbung von Klassen kann beliebig tief geschachtelt werden. Eine abgeleitete Klasse erbt dabei jeweils die Eigenschaften der unmittelbaren Vaterklasse, die ihrerseits die Eigenschaften ihrer unmittelbaren Vaterklasse erbt usw. Wir können also beispielsweise die Klasse Cabrio verwenden, um daraus eine neue Klasse ZweisitzerCabrio abzuleiten: |
![]() |
|
![]() |
001 class ZweisitzerCabrio 002 extends Cabrio 003 { 004 boolean notsitze; 005 } |
Diese könnte nun verwendet werden, um ein Objekt zu instanzieren, das die Eigenschaften der Klassen Auto, Cabrio und ZweisitzerCabrio hat:
001 ZweisitzerCabrio kfz1 = new ZweisitzerCabrio(); 002 kfz1.name = "911-T"; 003 kfz1.erstzulassung = 1982; 004 kfz1.leistung = 94; 005 kfz1.vdauer = 50; 006 kfz1.notsitze = true; 007 System.out.println("Alter = "+kfz1.alter()); |
Nicht jede Klasse darf zur Ableitung neuer Klassen verwendet werden. Besitzt eine Klasse das Attribut final, ist es nicht erlaubt, eine neue Klasse aus ihr abzuleiten. Die möglichen Attribute einer Klasse werden im nächsten Abschnitt erläutert. |
![]() |
|
![]() |
Neben den Membervariablen erbt eine abgeleitete Klasse auch die Methoden ihrer Vaterklasse (wenn dies nicht durch spezielle Attribute verhindert wird). Daneben dürfen auch neue Methoden definiert werden. Die Klasse besitzt dann alle Methoden, die aus der Vaterklasse geerbt wurden und zusätzlich die, die in der Methode neu definiert wurden.
Daneben dürfen auch bereits von der Vaterklasse geerbte Methoden neu definiert werden. In diesem Fall spricht man von Überlagerung der Methode. Wurde eine Methode überlagert, wird beim Aufruf der Methode auf Objekten dieses Typs immer die überlagerte Version verwendet.
Das folgende Beispiel erweitert die Klasse ZweisitzerCabrio um die Methode alter, das nun in Monaten ausgegeben werden soll:
|
![]() |
|
![]() |
Da die Methode alter bereits aus der Klasse Cabrio geerbt wurde, die sie ihrerseits von Auto geerbt hat, handelt es sich um eine Überlagerung. Zukünftig würde dadurch in allen Objekten vom Typ ZweisitzerCabrio bei Aufruf von alter die überlagerte Version, bei allen Objekten des Typs Auto oder Cabrio aber die alte Version verwendet werden. Es wird immer die Variante aufgerufen, die dem aktuellen Objekt beim Zurückverfolgen der Vererbungslinie am nächsten liegt.
Nicht immer kann bereits der Compiler entscheiden, welche Variante einer überlagerten Methode er aufrufen soll. In Kapitel 4 wurde bereits erwähnt, daß ein Objekt einer abgeleiteten Klasse zuweisungskompatibel zu einer Variablen einer übergeordneten Klasse ist. Wir dürfen also beispielsweise ein Cabrio-Objekt ohne weiteres einer Variablen vom Typ Auto zuweisen.
Die Variable vom Typ Auto kann während ihrer Lebensdauer also Objekte verschiedenen Typs enthalten (insbesondere vom Typ Auto, Cabrio und ZweisitzerCabrio). Damit kann natürlich nicht schon zur Compile-Zeit entschieden werden, welche Version einer überlagerten Methode aufgerufen werden soll. Der Compiler muß also Code generieren, um dies zur Laufzeit zu entscheiden. Man bezeichnet dies auch als dynamisches Binden.
Dieses Verhalten wird in C++ durch virtuelle Funktionen realisiert und muß mit Hilfe des Schlüsselworts virtual explizit angeordnet werden. In Java ist eine explizite Deklaration nicht nötig, denn Methodenaufrufe werden immer dynamisch interpretiert. Der dadurch verursachte Aufwand zur Laufzeit ist nicht zu vernachlässigen und liegt deutlich über den Kosten eines Funktionsaufrufs in C. Um das Problem zu umgehen, gibt es drei Möglichkeiten, dafür zu sorgen, daß eine Methode nicht dynamisch interpretiert wird. Dabei wird mit Hilfe zusätzlicher Attribute dafür gesorgt, daß die betreffende Methode nicht überlagert werden kann:
Wird eine Methode x in einer abgeleiteten Klasse überlagert, wird die ursprüngliche Methode x verdeckt. Aufrufe von x beziehen sich immer auf die überlagerte Variante. Oftmals ist es allerdings nützlich, die verdeckte Superklassenmethode aufrufen zu können, beispielsweise, wenn deren Funktionalität nur leicht verändert werden soll. In diesem Fall kann mit Hilfe des Ausdrucks super.x() die Methode der Vaterklasse aufgerufen werden. Der kaskadierte Aufruf von Superklassenmethoden (wie in super.super.x()) ist nicht erlaubt.
Wenn eine Klasse instanziert wird, garantiert Java, daß ein zur Parametrisierung des new-Operators passender Konstruktor aufgerufen wird. Daneben garantiert der Compiler, daß auch der Konstruktor der Vaterklasse aufgerufen wird. Dieser Aufruf kann entweder explizit oder implizit geschehen.
Falls als erste Anweisung innerhalb eines Konstruktors ein Aufruf der Methode super steht, wird dies als Aufruf des Superklassenkonstruktors interpretiert. super wird wie eine normale Methode verwendet und kann mit oder ohne Parameter aufgerufen werden. Der Aufruf muß natürlich zu einem in der Superklasse definierten Konstruktor passen.
Falls als erste Anweisung im Konstruktor kein Aufruf von super steht, setzt der Compiler an dieser Stelle einen impliziten Aufruf super(); ein und ruft damit den parameterlosen Konstruktor der Vaterklasse auf. Falls ein solcher Konstruktor in der Vaterklasse nicht definiert wurde, gibt es einen Compiler-Fehler.
Alternativ zu diesen beiden Varianten, einen Superklassenkonstruktor aufzurufen, ist es auch erlaubt, mit Hilfe der this-Methode einen anderen Konstruktor der eigenen Klasse aufzurufen. Um die oben erwähnten Zusagen einzuhalten, muß dieser allerdings selbst direkt oder indirekt schließlich einen Superklassenkonstruktor aufrufen. |
![]() |
|
![]() |
Das Anlegen von Konstruktoren in einer Klasse ist optional. Falls in einer Klasse überhaupt kein Konstruktor definiert wurde, erzeugt der Compiler beim Übersetzen der Klasse automatisch einen parameterlosen default-Konstruktor. Dieser enthält lediglich einen Aufruf des parameterlosen Superklassenkonstruktors.
Konstruktoren werden nicht vererbt. Alle Konstruktoren, die in einer abgeleiteten Klasse benötigt werden, müssen neu definiert werden, selbst wenn sie nur aus einem Aufruf des Superklassenkonstruktors bestehen. |
![]() |
|
![]() |
Durch diese Regel wird bei jedem Neuanlegen eines Objekts eine ganze Kette von Konstruktoren aufgerufen. Da nach den obigen Regeln jeder Konstruktor zuerst den Superklassenkonstruktor aufruft, wird die Initialisierung von oben nach unten in der Vererbungshierarchie durchgeführt: zuerst wird der Konstruktor der Klasse Object ausgeführt, dann der der ersten Unterklasse usw., bis zuletzt der Konstruktor der zu instanzierenden Klasse ausgeführt wird.
Im Gegensatz zu den Konstruktoren werden die Destruktoren eines Ableitungszweiges nicht automatisch verkettet. Falls eine Destruktorenverkettung erforderlich ist, kann sie durch explizite Aufrufe des Superklassendestruktors mit Hilfe der Anweisung super.finalize() durchgeführt werden. |
![]() |
|
![]() |
|
Go To Java 2, Addison Wesley, Version 1.0.2, © 1999 Guido Krüger, http://www.gkrueger.com |