| » Tutorial / Java Grundlagen / Vererbung |
|
Heute geht es um die Fähigkeit der Klassen, durch Vererbung ihre Eigenschaften auf andere Klassen zu übertragen. Dieses Konzept soll hier ausführlich besprochen und im nächsten Kapitel komplettiert werden. |
|
» Superklassen und Subklassen » Ableiten einer Klasse » Instanziierung und Konstruktorverkettung » globale Superklasse » nichtvererbbares Inventar » Überlagerung von Elementen » geschützte Klassen » abstrakte Klassen » Referenzen in Hierarchien » Instanzbestimmung |
| » Superklassen und Subklassen | nach oben « |
|
Bei der detaillierten Besprechung der Vererbung werden uns immer wieder zwei wichtige Begriffe begegnen, die wir hier kurz abgrenzen möchten. Die Superklasse ist die Klasse, welche als Basis (daher auch als Basisklasse bezeichnet) für die Subklasse (abgeleitete Klasse) dient. Bei der Vererbung werden dann, unter gewissen Bedingungen, die Elemente der Superklasse auf die Subklasse übertragen, welche wiederum die Funktionalität der Elemente erweitern (Spezialisierung) kann.
|
|
| » Ableiten einer Klasse | nach oben « |
|
Nun wolle wir uns ansehen, wie diese Technologie genutzt werden kann. Eine Ableitung von einer Klasse zur anderen zieht zwangsläufig eine Klassenhierarchie nachsich, in der alle Klassen und ihre Beziehungen untereinander dargestellt werden kann. Um nun eine Ableitung vorzunehmen, nutzt man das Extends - Schlüsselwort im Kopf der Klassendefinition. Exakt diesem schließt sich der Name der Superklasse kann. Dazu folgender syntaktischer Bauplan. |
|
| Syntax | |
[Modifikator] Subklasse
extends Superklasse
{
// Anweisungen
}
|
|
|
Als Beispiel definieren wir zwei Klassen, wobei die zweite Klasse von der ersten abgeleitet wird. |
|
| Quellcode | |
class MySuperClass
{
}
class MySubClass extends MySuperClass
{
}
|
|
|
Auch wenn unsere Klassen hier noch keine Elemente haben, so würde die Subklasse automatisch alle Variablen und Methoden der Superklasse erben. Doch dazu gleich mehr. |
|
| » Instanziierung und Konstruktorverkettung | nach oben « |
|
Nachdem wir eine Subklasse erfogreich abgeleitet haben, können wir diese nun wie gewohnt instanziieren. Nun kommen allerdings einige weitere Betrachtungen hinzu, besonders was die Objektkonstruktion in Hierarchien anbelangt. Unsere Subklasse basiert mit ihren Elementen auf denen der Vaterklasse. Des weiteren definieren die Subklassen meist neue Elemente. Der Compiler muss dieses neue Objekt nun so konstruieren, dass alle Elemente korrekt angelegt werden. Dabei beginnt er bei der Basis, sprich der Superklasse. Zunächst einmal wird nur der Teil der Superklasse konstruirt und erst danach die neuen Elemente der Subklasse. Dieses Vorgehen ist bei großen Hierarchien über mehrere Ebenen hinweg äquivalent. Umgekehrt verläuft der Abbau des Objekts vom Speicher. Der Compiler nutzt nun zur Konstruktion die jeweiligen Standardkonstruktoren aller beteiligten Klassen. Was aber, wenn es einen solchen in der Basisklasse nicht gibt und der Compiler keinen impliziten Aufruf durchführen kann? In diesem Fall gibt es eine Fehlermeldung und man ist selbst dafür verantwortlich, die korrekten Konstruktoren der Basisklasse aufzurufen. Das Vorgehen ist das folgende. Der Konstruktor der Subklasse muss als erste Anweisung den Konstruktor der Basisklasse aufrufen. Um sich nun auf die Basisklasse beziehen zu können, nutzt man das Super - Schlüsselwort. Dieses bezeichnet immer die nächsthöhere Klasse, die der aktuellen Klasse übergeordnet ist. Der Sinn dahinter ist, dass jeder Konstruktor der übergeordneten Klassen selbständig seine Elemente aufbaut und initialisiert. Erst dann kommt der nächste Konstruktor der Subklassen dran. |
|
| Syntax | |
super.Konstruktor();
|
|
Unser folgendes Beispiel definiert wieder zwei Klassen in einer Vererbungshierarchie. Die Subklasse ruft über den Super - Befehl den Konstruktor der Superklasse auf und übergibt ihm alle relevanten Parameter, um das Basisobjekt vollständig konstruiren zu können. |
|
| Quellcode | |
class MySuperClass
{
public MySuperClass(int i)
{
this.i = i;
}
public int i;
}
class MySubClass extends MySuperClass
{
public MySubClass(int i,String s)
{
super(i);
this.s = s;
}
public String s;
}
public class MyClass
{
public static void main(String[] args)
{
MySubClass object = new MySubClass(0,"ProgrammersBase.NET");
System.out.println(object.i);
System.out.println(object.s);
}
}
|
|
| Ausgabe | |
0
ProgrammersBase.NET
|
|
|
Das hier soeben besprochene Prinzip wird auch als Konstruktorverkettung bezeichnet. Über mehrere Ebenen hinweg bleibt die Anwendung die gleiche. Die Parameter müssen lediglich von Konstruktor zu Konstruktor weiter durchgereicht werden. |
|
| » globale Superklasse | nach oben « |
|
Alle in Java erzeugten Klassen werden implizit immer von einer globalen Superklasse abgeleitet, auch wenn man sie nicht selbst bei der Definition angibt. Daher wäre auch die folgende Definiton korrekt. |
|
| Quellcode | |
class MySuperClass extends Object
{
}
class MySubClass extends MySuperClass
{
}
|
|
|
In diesem Zusammenhang soll noch einmal darauf hingewiesen werden, dass jede Klasse nur eine Superklasse haben kann. Daraus schließt sich, dass letzten Endes jede Klasse und jede Hierarchie auf Object zurückgeht. Daher haben alle Klassen die selben Elemente und Methoden wie diese globale Superklasse. Die Referenz finden Sie in unserem Referenzteil. |
|
| » nichtvererbbares Inventar | nach oben « |
|
Bei der Ableitung einer Klasse gibt es zwei Bestandteile, die der Subklasse nicht mitvererbt werden.
Auch statische Elemente werden nie vererbt, da sie ja global an eine Klasse und nicht an ein konkretes Objekt gebunden sind. Die Einschränkungen bei privaten Elementen wird noch genauer behandelt. |
|
| » Überlagerung von Elementen | nach oben « | ||||||||||||
|
Die abgeleitete Klasse erbt alle Elemente ihrer Vaterklasse. Dennoch können die Variablen und Methoden der Vaterklasse in der Kindklasse redefiniert oder gar mit neuen Werten überschrieben werden. Dabei überdecken sie fortan die lokalen Elemente mit den neuen Einstellungen. Eine Überlagerung findet immer dann statt, wenn eine syntaktisch identische Methode in der abgeleiteten Klasse noch einmal definiert wird. Dabei muss sie in allen Teilen ihrer Signatur mit der Basismethode übereinstimmen. Gleiches gilt für überdeckte Datenelemente. |
|||||||||||||
|
» Überlagerung von Elementen » Aufrufkonventionen » dynamischer Methodenaufruf » Bezug auf die Basisklasse » Modifikatoren » geschützte Methoden |
|||||||||||||
|
» Überlagerung von Elementen Im Folgenden wollen wir das eben Besprochene praktisch anwenden. In der Subklasse unserer Hierarchie redefinieren wir eine Methode der Basisklasse. Dabei können wir auch den Aufgabenbereich dieser komplett neu schreiben. |
|||||||||||||
| Quellcode | |||||||||||||
class MySuperClass
{
public void print()
{
System.out.print("Superklasse");
}
}
class MySubClass extends MySuperClass
{
public void print()
{
// Redefinition und Überdeckung
System.out.print("Subklasse");
}
}
|
|||||||||||||
|
» Aufrufkonventionen Letztlich gilt es zu klären, welche Methode denn nun wann aufgerufen wird. Oft herrscht Unklarheit darüber, ob sich die Version der Superklasse oder die der Subklasse zu Wort meldet. Hierbei gibt es eine einfache Regel, die wir auch als Polymorphismus bezeichnen. Wird eine Methode in einer Hierarchie definiert, so wird immer die Methode aufgerufen, die der nächsthöheren Klasse des Objekts entspricht. Redefinieren wir also eine Methode in unserer Subklasse, so wird für ein Objekt diesen Klassentyps auch diese Methode aufgerufen. Ansonsten sucht der Compiler automatisch nach der nächsthöheren Variante.
In unserem Beispiel definiert unsere Superklasse sowohl eine Variable als auch eine Methode. Die erste Subklasse redefiniert lediglich die Methode und erbt die Variable. Die daraus wiederum angeleitete Subklasse drei erbt die Methode der ersten Subklasse und redefiniert die Variable. Soviel zum Stammbaum. Auf alle (vererbten) Elemente einer Klasse kann nun wieder über die Instanziierung eines Objekts zugegriffen werden. Welche Variable, beziehungsweise Methode aufgerufen wird, hängt davon ab, ob das betreffende Element nur vererbt oder vollständig redefiniert wurde. Dazu das folgende Beispiel. |
|||||||||||||
| Quellcode | |||||||||||||
class MySuperClass
{
public int i = 1;
public String print()
{
return "Superklasse";
}
}
class MySubClass extends MySuperClass
{
// public int i = 1;
public String print()
{
return "Subklasse";
}
}
class MySubSubClass extends MySubClass
{
public int i = 2;
/*
public String print()
{
return "Subklasse";
}
*/
}
public class MyClass
{
public static void main(String[] args)
{
// MySuperClass
MySuperClass object1 = new MySuperClass();
System.out.println(object1.i);
System.out.println(object1.print());
// MySubClass
MySubClass object2 = new MySubClass();
System.out.println(object2.i);
System.out.println(object2.print());
// MySubSubClass
MySubSubClass object3 = new MySubSubClass();
System.out.println(object3.i);
System.out.println(object3.print());
}
}
|
|||||||||||||
| Ausgabe | |||||||||||||
1
Superklasse
1
Subklasse
2
Subklasse
|
|||||||||||||
|
Die Subklassen redefinieren jeweils nur ein Element ihrer Basisklasse. Aus diesem Grund haben wir die lediglich vererbten Elemente auskommentiert. So würden sie eigentlich im Quellcode stehen, wenn man sie sich implizit hinzudenkt. Wie an der Ausgabe zu sehen ist, so wird immer die aktuell vorhandene Variante des Elements ausgegeben. Ist das aufgerufene Element redefiniert, so wird diese Variante ausgegeben. Ansonsten die letztmöglich bekannte Version, die ja auf die neue Klasse übertragen wird. » dynamischer Methodenaufruf Das Verhalten der Klassen, sich jedesmal die bestmögliche Variante aus der Hierarchie herauszusuchen, nennt man auch die dynamische Methodensuche, denn der Weg der Aufrufkonventionen kann sich mit jeder neuen Klasse in einer Hierarchie dynamisch mitverändern und wird auch dadurch beeinflußt, welche Klasse wie instanziiert wurde. Dazu gleich mehr. » Bezug auf die Basisklasse Wie wir es bei den Konstruktoren kennengelernt haben, so können wir uns auch auf andere Elemente einer Basisklasse über deren direkte Subklasse bedienen. Dadurch ist es uns möglich, sowohl Variablen als auch Methoden der Basisklasse aufzurufen. Das ist vorallem dann nützlich, wenn lediglich geringe Veränderungen an der Basismethode vorgenommen werden sollen oder deren Funktionalität erweitert wird. Syntaktisch wird wiederum die Super - Anweisung verwendet, um sich auf die Vaterklasse zu beziehen. Eine Verkettung mehrerer Super - Anweisungen, um beispielsweise mehrere Ebenen einer Hierarchie nach oben zu klettern, ist nicht gestattet. |
|||||||||||||
| Syntax | |||||||||||||
super.Element;
super.Methode();
|
|||||||||||||
|
Das anschließende Beispiel nutzt die Basisversion einer bereits überschriebenen Methode, um einen Text auszugeben. Nach dem Aufruf der Basismethode folgen noch weitere ergänzende Anweisungen in der redefinierten Variante der Subklasse. |
|||||||||||||
| Quelltext | |||||||||||||
class MySuperClass extends Object
{
public void print()
{
System.out.println("Superklasse");
}
}
class MySubClass extends MySuperClass
{
public void print()
{
super.print();
System.out.println("Subklasse");
}
}
public class MyClass
{
public static void main(String[] args)
{
MySubClass object = new MySubClass();
object.print();
}
}
|
|||||||||||||
| Ausgabe | |||||||||||||
Superklasse
Subklasse
|
|||||||||||||
|
» Modifikatoren Auch bei der Vererbung spielt die Wahl der richtigen Modifikatoren eine entscheidende Rolle, wenn es darum geht, einen Zugriff auf Elemente der Basisklasse zu erlangen. Der Typ des verwendeten Modifikators für ein Element einer Basisklasse bestimmt gleichzeitig, ob dieser in der abgeleiteten Klasse sichtbar ist. So läßt sich die Hierarchie nachhaltig steuern und beeinflussen.
Ist also ein Element der Vaterklasse als privat gekennzeichnet, so besteht auch von der abgeleiteten Klasse her keine Zugriffsmöglichkeit, da dieses Element auch nicht vererbt wird. Soll es dennoch genutzt werden, so muss es überschrieben und redefiniert werden. » geschützte Methoden Möchte man die Überlagerung einer Methode in einer abgeleiteten Klasse verhindern, so kann man sich eines speziellen Modifikators bedienen. Dabei ist der betroffenen Methode lediglich das Final - Schlüsselwort voranzusetzen. |
|||||||||||||
| Syntax | |||||||||||||
final [Modifikator] Typ Methode()
{
// Anweisungen
}
|
|||||||||||||
|
Der Versuch der Redefinition in einer Subklasse führt nun zu einer Fehlermeldung. In Verbindung mit dem Private - Modifikator wird die Methode des weiteren vollständig vor den Subklassen isoliert. |
|||||||||||||
| Quellcode | |||||||||||||
class MySuperClass extends Object
{
final public void print() {}
}
class MySubClass extends MySuperClass
{
// nicht möglich
// public void print() {}
}
|
|||||||||||||
| » geschützte Klassen | nach oben « |
|
Auch gegen die Ableitung einer Klasse lassen sich Vorkehrungen treffen. Auch hier kommt wieder unser Final - Schlüsselwort ins Spiel. Eine mit diesem Modifikator versehene Klassendefinition ist fortan gegen die Vererbung geschützt. Neue Subklassen sind also nicht mehr möglich. |
|
| Syntax | |
final class Klasse
{
// Anweisungen
}
|
|
|
Die Deklaration kann dabei auch mitten in einer Hierarchie auftreten und ist nicht zwangsläufig auf einzelne Klassen zugeschnitten. Wird sie bei einer Subklasse verwendet, endet der Hierarchiebaum in diesem Zweig der Klasse, denn neue Subklassen sind wiederum verboten. |
|
| Quellcode | |
final class MySuperClass extends Object {}
// nicht möglich
// class MySubClass extends MySuperClass {}
|
|
| » abstrakte Klassen | nach oben « |
|
Eine abstrakte Klasse implementiert selbst keine Funktionalitäten, sondern fungiert lediglich als eine Art Vorschrift, welche Methoden eine ihrer Subklassen zu redefinieren hat. Daher ist es auch nicht möglich, eine Instanz dieser Klassentypen zu erzeugen. |
|
|
» Definition einer abstrakten Klasse » Ableitung » abstrakte Methoden |
|
|
» Definition einer abstrakten Klasse Eine Klasse wird über das Abstract - Schlüsselwort entsprechend deklariert. Dies hat unter anderem die Folge, dass alle Methoden im Rumpf der Klasse lediglich deklariert werden können. Die Nutzung von Funktionsrümpfen ist den nichtabstrakten Subklassen vorbehalten, die alle Methoden implementieren müssen. |
|
| Syntax | |
abstract class Klasse
{
Modifikator Typ Methode();
}
|
|
|
Diese Besonderheit kommt lediglich bei den Methoden vor. Datenelemente können wie gewohnt ihre Verwendung finden. Konstruktoren dürfen ebenfalls definiert werden, um interne Variablen zu initialisieren. » Ableitung Die Ableitung einer Subklasse von einer Basisklasse ist die selbe wie bei nicht abstrakten Klassen. Zu beachten ist allerdings, dass alle Methoden der abstrakten Klasse in der Subklasse vollständig implementiert werden müssen. Dabei müssen die Methoden der Subklasse mit den Signaturen der Vaterklasse exakt übereinstimmen. » abstrakte Methoden Alle Methoden einer abstrakten Klassen können ebenfalls als abstrakt angesehen werden, da sie keinerlei Aufgaben erfüllen. Sie fungieren lediglich als Deklaration. Daher könnte man diese Methoden ebenfalls mit dem Abstract - Befehl kennzeichnen, was bei einer als abstrakt deklarierten Klasse allerdings optional ist. Anders herum spielt es aber eine Rolle. Sobald eine Klasse eine abstrakte Methode enthält, gilt sie ebenfalls als abstrakt und muss dementsprechend deklariert werden. Die abstrakten Methoden enthalten keinen Rumpf mit Anweisungen. Das folgende Beispiel zeigt die Verwendung einer abstrakten Klasse auf, deren Funktionalität durch eine Subklasse implementiert und gegebenenfalls erweitert wird. |
|
| Quellcode | |
abstract class MySuperClass
{
abstract public void print();
protected String s;
public MySuperClass()
{
s = "ProgrammersBase.NET";
}
}
class MySubClass extends MySuperClass
{
public void print()
{
System.out.print(s);
}
}
public class MyClass
{
public static void main(String[] args)
{
MySubClass object = new MySubClass();
object.print();
}
}
|
|
| Ausgabe | |
ProgrammersBase.NET
|
|
| » Referenzen in Hierarchien | nach oben « |
|
Auch bei Klassen handelt es sich um Datentypen. In dieser Betrachtungsweise bezeichnet man sie auch als benutzerdefinierte Typen. In Klassenhierarchien bilden diese Typen eine enge Verwandschaft zwischeneinander. Subklassen bilden eine Spezialisierung ihrer Superklassen, indem sie neue Eigenschaften und Verhaltensweisen definieren. Die Superklasse wiederum ist eine Generalisierung, da sie immer die Grundelemente aller ihrer Subklassen enthält. Daher ist es in Java möglich, eine Variable vom Typ einer Superklasse auf ein Objekt der Subklasse verweisen zu lassen. Der umgekehrte Weg ist nicht möglich, da die Subklassen mehr Elemente haben als ihre Basis.
Die Superklasse hat in der Regel weniger Elemente als ihre Subklassen. Eine Referenz vom Typ einer Superklasse kann aber nur auf soviele Elemente einer Subklasse verweisen, wie diese selber hat. Sollte man also Subklassen über die Referenz einer Vaterklasse ansprechen, so stehen nur die gemeinsamen Elemente von Superklasse und Subklasse zum Zugriff bereit. Andersherum hat die Subklasse viel mehr Elemente als eine Superklasse und einige ihrer Verweise würden ins leere führen, daher ist ein umgekehrter Weg von vornherein ausgeschlossen. Die Schlußfolgerung aus diesem System ist die, dass man einem Objekt einer Superklasse die Speicheradresse einer Subklasse zuweisen kann, um alle Elemente einer Hierarchie gemeinsam verwalten zu können. Das folgende Beispiel nutzt diese Technik, um über eine Basisklassenreferenz auf eine Subklasse zu verweisen und dort eine abgeleitete Methode aufzurufen. Dabei kennt die Referenz nur die Elemente ihrer Basisklasse, da sie selbst vom Datentyp her dieser angehört. |
|
| Quellcode | |
class MySuperClass
{
public void print()
{
System.out.print("Superklasse");
}
}
class MySubClass extends MySuperClass
{
public void print()
{
System.out.print("Subklasse");
}
public int i;
}
public class MyClass
{
public static void main(String[] args)
{
// Superklasse wird eine Subklasse zugewiesen
MySuperClass object = new MySubClass();
object.print();
// Element in Superklasse unbekannt
// System.out.print(object.i);
// Referenz auf Subklasse nicht
// mit Basisklasseninstanz verwendbar
// MySubClass object = new MySuperClass();
}
}
|
|
| Ausgabe | |
Subklasse
|
|
|
Auf Grund des Polymorphismus erkennt der Compiler hier automatisch die zu verwendende Methode. Dabei handelt es sich um die überschriebene Methode der Subklasse. Wäre diese nicht vorhanden, würde automatisch die Methode der Basisklasse aufgerufen werden, da diese ohnehin vererbt wird. Die Variable hingegen kann nicht ausgelesen werden, da eine Referenz auf die Basisklasse auch nur deren Elemente kennt. Somit wäre es auch nicht möglich, eine Subklassenreferenz mit einer Superklasse zu initialisieren. |
|
| » Instanzbestimmung | nach oben « | |||||||||||||||
|
An dieser Stelle möchten wir noch einmal die Rolle des Instanceof - Operators aus dem vorhergehenden Kapitel besprechen. Dieser kann auch dazu verwendet werden, die hierarchische Beziehungen zwischen Klassen zu bestimmen. Der Operator entspricht der syntaktischen Anwendung wie im vorhergehenden Kapitel. Aus diesem Grund soll diese hier noch einmal dargestellt werden.
Im Falle einer Klassenhierarchie liefert der Operator immer dann True, wenn das zu vergleichende Objekt entweder exakt dem Typ der Vergleichsklasse entspricht, oder eine Subklasse der Hierarchie ist. Das folgende Beispiel verdeutlicht diesen Zusammenhang. |
||||||||||||||||
| Quellcode | ||||||||||||||||
class MySuperClass
{}
class MySubClass extends MySuperClass
{}
public class MyClass
{
public static void main(String[] args)
{
MySubClass object = new MySubClass();
System.out.print(object instanceof MySuperClass);
}
}
|
||||||||||||||||
| Ausgabe | ||||||||||||||||
true
|
||||||||||||||||
| « Kapitel | Kapitelübersicht | nach oben | Kapitel » |