Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage |
<< | < | > | >> | API | Kapitel 9 - OOP II: Vererbung, Polymorphismus und statische Elemente |
Eines der wesentlichen Designmerkmale objektorientierter Sprachen ist die Möglichkeit, Variablen und Methoden zu Klassen zusammenzufassen. Ein weiteres wichtiges Merkmal ist das der Vererbung, also der Möglichkeit, Eigenschaften vorhandener Klassen auf neue Klassen zu übertragen. Fehlt diese Fähigkeit, bezeichnet man die Sprache 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.
Als Beispiel wollen wir eine neue Klasse Cabrio definieren, die sich von Auto nur dadurch unterscheidet, dass sie zusätzlich die Zeit, die zum Öffnen des Verdecks benötigt wird, speichern soll:
001 class Cabrio 002 extends Auto 003 { 004 int vdauer; 005 } |
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. |
|
Enthält eine Klasse keine extends-Klausel, so besitzt sie die implizite Vaterklasse Object. Jede Klasse, die keine extends-Klausel besitzt, wird direkt aus Object abgeleitet. Jede explizit abgeleitete Klasse stammt am oberen Ende ihrer Vererbungslinie von einer Klasse ohne explizite Vaterklasse ab und ist damit ebenfalls aus Object abgeleitet. Object ist also die Superklasse aller anderen Klassen.
Die Klasse Object definiert einige elementare Methoden, die für alle Arten von Objekten nützlich sind:
boolean equals(Object obj) protected Object clone() String toString() int hashCode() |
java.lang.Object |
Die Methode equals testet, ob zwei Objekte denselben Inhalt haben, clone kopiert ein Objekt, toString erzeugt eine String-Repräsentation des Objekts und hashCode berechnet einen numerischen Wert, der als Schlüssel zur Speicherung eines Objekts in einer Hashtable verwendet werden kann. Damit diese Methoden in abgeleiteten Klassen vernünftig funktionieren, müssen sie bei Bedarf überlagert werden. Für equals und clone gilt das insbesondere, wenn das Objekt Referenzen enthält.
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 sie selbst neu definiert hat.
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 überlagernde Version verwendet.
Das folgende Beispiel erweitert die Klasse ZweisitzerCabrio um die Methode alter, das nun in Monaten ausgegeben werden soll:
001 class ZweisitzerCabrio 002 extends Cabrio 003 { 004 boolean notsitze; 005 006 public int alter() 007 { 008 return 12 * (2011 - erstzulassung); 009 } 010 } |
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 überlagernde Version, bei allen Objekten des Typs Auto oder Cabrio aber die ursprüngliche 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 Abschnitt 5.6 und Abschnitt 8.1.6 wurde bereits erwähnt, dass das Objekt einer abgeleiteten Klasse zuweisungskompatibel zu der 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 solche 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. Erst während das Programm läuft, ergibt sich, welcher Typ von Objekt zu einem bestimmten Zeitpunkt in der Variable gespeichert wird. Der Compiler muss also Code generieren, um dies zur Laufzeit zu entscheiden. Man bezeichnet dies auch als dynamisches Binden.
In C++ wird dieses Verhalten durch virtuelle Funktionen realisiert und es muss 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 Overhead ist allerdings nicht zu vernachlässigen und liegt deutlich über den Kosten eines statischen Methodenaufrufs. Um das Problem zu umgehen, gibt es mehrere Möglichkeiten, dafür zu sorgen, dass eine Methode nicht dynamisch interpretiert wird. Dabei wird mit Hilfe zusätzlicher Attribute dafür gesorgt, dass die betreffende Methode nicht überlagert werden kann:
In Abschnitt 9.4 werden wir das Thema Polymorphismus noch einmal aufgreifen und ein ausführliches Beispiel für dynamische Methodensuche geben. |
|
Wird eine Methode x in einer abgeleiteten Klasse überlagert, wird die ursprüngliche Methode x verdeckt. Aufrufe von x beziehen sich immer auf die überlagernde 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, dass ein zur Parametrisierung des new-Operators passender Konstruktor aufgerufen wird. Daneben garantiert der Compiler, dass 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 muss 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. Das ist genau dann der Fall, wenn in der Superklassendeklaration lediglich parametrisierte Konstruktoren angegeben wurden und daher ein parameterloser default-Konstruktor nicht automatisch erzeugt wurde.
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, muss 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 Ableitungszweigs nicht automatisch verkettet. Falls eine Destruktorenverkettung erforderlich ist, kann sie durch explizite Aufrufe des Superklassendestruktors mit Hilfe der Anweisung super.finalize() durchgeführt werden. |
|
Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 7. Auflage, Addison Wesley, Version 7.0 |
<< | < | > | >> | API | © 1998, 2011 Guido Krüger & Heiko Hansen, http://www.javabuch.de |