Java Super: Was steckt hinter diesem Schlüsselbegriff?
Die Programmiersprache Java enthält einen Schlüsselbegriff mit der Bezeichnung super. Wenn Programmieranfänger auf diesen treffen, sorgt er jedoch häufig für viel Verwirrung. Der Grund dafür besteht darin, dass dabei auf den ersten Blick häufig nicht klar ist, welchen Zweck er erfüllt. Deshalb soll dieser Artikel klären, was hinter dem Ausdruck Java super steckt. Um diesen Artikel zu verstehen, ist es wichtig, die Grundzüge der objektorientierten Programmierung in Java bereits zu kennen.
Java Super: wichtig für die Vererbung
Wenn wir uns mit dem Begriff super in Java befassen, ist es wichtig, sich zunächst mit der Vererbung zu beschäftigen. Das liegt daran, dass der Schlüsselbegriff super ausschließlich in vererbten Klassen zum Einsatz kommt. Daher ist es notwendig, genau zu wissen, was sich hinter diesem Konzept verbirgt.
Die Vererbung ist ein Prinzip der objektorientierten Programmierung. Hierbei kommen Klassen zum Einsatz, um bestimmte Strukturen für Objekte eines bestimmten Typs vorzugeben. Als Beispiel können wir hierfür eine Klasse für ein Motorrad heranziehen. Dabei legen wir beispielsweise fest, dass wir bei jedem Motorrad die Marke, das Modell und die Höchstgeschwindigkeit festhalten wollen. Wenn wir eine Klasse mit den entsprechenden Attributen erzeugen, geben wir vor, dass wir in unserem Programm bei jedem einzelnen Motorrad, das wir verwenden, diese Eigenschaften angeben müssen. Außerdem können wir hierbei bestimmte Methoden vorgeben. Diese dienen dazu, die Attribute auszugeben, zu verändern oder Berechnungen mit ihnen durchzuführen.
Die Vererbung ermöglicht es nun, hierbei hierarchische Strukturen und damit verschiedene Kategorien vorzugeben. Wenn wir beispielsweise ein Motorrad betrachten, könnten wir hierbei die übergeordnete Kategorie Fahrzeuge einführen. Diese gilt dann auch für Autos und Lkws. Die Eigenschaften, die wir für das Motorrad beschrieben haben, sind auch für alle anderen Fahrzeuge von Bedeutung. Daher könnten wir einfach eine Klasse mit der Bezeichnung Fahrzeug erstellen und hier die entsprechenden Attribute definieren. Diese ermöglicht es dann, Objekte für Motorräder, Autos und Lkws zu erzeugen.
Wenn man solche übergeordnete Kategorien erstellt, kommt es jedoch häufig vor, dass es auch gewisse Unterschiede zwischen den verschiedenen Objekt-Typen gibt, die wir hier zusammengefasst haben. Das wird beispielsweise deutlich, wenn wir die Kofferraumgröße betrachten. Diese ist nur bei Autos von Bedeutung. Bei Lkws gibt es zwar mit der Laderaumgröße ein sehr ähnliches Attribut. Da hierbei jedoch ein anderer Begriff zum Einsatz kommt, ist hierbei eine Differenzierung sinnvoll. Bei Motorrädern ist es hingegen nicht angebracht, diesen Wert anzugeben. Hierbei wäre es jedoch interessant, festzuhalten, ob es sich um einen Zweitakter oder um einen Viertakter handelt.
In diesem Fall ist es sinnvoll, mit der Vererbung zu arbeiten. Dabei gestalten wir eine Klasse mit der Bezeichnung Fahrzeug. Diese enthält alle Eigenschaften, die für alle Fahrzeugtypen von Bedeutung sind. Davon leiten wir dann verschiedene weitere Klassen ab, die alle Details regeln, bei denen es Unterschiede gibt. Das sorgt zum einen für übersichtliche Strukturen und macht deutlich, dass eine Beziehung zwischen den verschiedenen Klassen besteht. Zum anderen reduziert das den Aufwand beim Programmieren. Durch die Vererbung ist es nicht notwendig, alle Attribute und Methoden für jede einzelne untergeordnete Klasse neu zu definieren. Das müssen Sie nur ein einziges Mal in der übergeordneten Klasse erledigen. In den untergeordneten Klassen müssen Sie dann nur diejenigen Eigenschaften vorgeben, die neu hinzukommen.
Die Bedeutung des Begriffs super
Um zu verstehen, was hinter dem Begriff Java super steht, ist es hilfreich, sich mit den Begrifflichkeiten bei der Vererbung auseinanderzusetzen. Bisher haben wir von übergeordneten und untergeordneten Klassen gesprochen. Darüber hinaus sind jedoch auch die Bezeichnungen Eltern- und Kindklasse üblich. Für die übergeordnete Klasse kommt außerdem die Bezeichnung Superklasse zum Einsatz – beziehungsweise auf Englisch super class.
Daraus geht bereits hervor, dass sich der Schlüsselbegriff super immer auf die übergeordnete Klasse bezieht. Wir verwenden ihn dann, wenn wir innerhalb der abgeleiteten Klasse auf eine Methode oder auf ein Attribut der Elternklasse zugreifen möchten.
Die verschiedenen Anwendungsmöglichkeiten
Nachdem wir nun die grundsätzliche Verwendung des Schlüsselbegriffs super geklärt haben, sollen noch einige praktische Anwendungsbeispiele folgen. Auf diese Weise wird die Funktion dieses Ausdrucks klarer. Am häufigsten kommt er zum Einsatz, um auf den Konstruktor der Superklasse zuzugreifen. Daher wird dieses Beispiel zuerst behandelt. Es ist aber auch möglich, damit auf Methoden oder Attribute der Superklasse zuzugreifen.
Mit super() auf den Konstruktor der Superklasse zugreifen
Für unsere Beispielanwendung ist es zunächst notwendig, die übergeordnete Klasse Fahrzeug zu erstellen. Dabei geben wir die Attribute marke, modell und maxkmh für die Höchstgeschwindigkeit vor. Dem Prinzip der Datenkapselung entsprechend zeichnen wir diese mit dem Begriff private aus. Daher können wir von außerhalb der Klasse nicht auf sie zugreifen. Aus diesem Grund müssen wir entsprechende Methoden erstellen, um die Werte auszugeben oder um sie zu verändern. Um das Beispiel so kompakt wie möglich zu halten, fügen wir jedoch nur zwei Methoden als Beispiel ein: getMarke() und getModell(). Außerdem ist es notwendig, einen Konstruktor zu erstellen. Auf diese Weise geben wir die entsprechenden Werte direkt beim Erstellen des Objekts vor. Die entsprechende Klasse sieht dann so aus:
Im nächsten Schritt leiten wir dann die Klasse Motorrad von der Klasse Fahrzeug ab. Dabei erstellen wir ein neues Attribut mit der Bezeichnung raeder. Dieses gibt die Anzahl der Räder vor. Da diese bei Motorrädern stets bei 2 liegt, fügen wir den entsprechenden Wert direkt ein. Außerdem ist dieser Wert unveränderlich, sodass wir ihn als final deklarieren. Darüber hinaus erstellen wir ein weiteres Attribut mit der Bezeichnung takt. Dieses soll festhalten, ob es sich beim entsprechenden Motorrad um einen Zweitakter oder um einen Viertakter handelt. Auch das ist bei diesem Fahrzeugtyp von großer Bedeutung.
Nun wenden wir uns dem Konstruktor zu. Hier müssen wir nun die Werte aller Attribute aufnehmen – unabhängig davon, ob wir diese in der Eltern- oder in der Kindklasse definiert haben. Die einzige Ausnahme stellt die Anzahl der Räder dar, da wir diesen Wert bereits direkt vorgegeben haben. Daher führen wir den Konstruktor wie folgt ein:
public Motorrad(String marke, String modell, int maxkmh, String takt)
Der Wert für den Takt befindet sich direkt in dieser Klasse. Daher können wir ihn über den Begriff this vorgeben. Bei den Werten für die Marke, das Modell und für die Höchstgeschwindigkeit stellt sich jedoch die Frage, auf welche Weise wir diese in die übergeordnete Klasse einfügen können.
Ein direkter Zugriff auf die Attribute ist hierbei nicht möglich, da wir diese als private deklariert haben. Die Lösung für dieses Problem besteht darin, dass wir den Konstruktor der Klasse aufrufen und ihm die entsprechenden Werte übergeben. Hierfür kommt der Begriff super zum Einsatz. Dieser bezieht sich wie bereits beschrieben stets auf die Superklasse. Wenn wir an diesen eine Klammer mit den entsprechenden Übergabewerten anfügen, ruft er immer den Konstruktor der Elternklasse auf. Auf diese Weise erzeugen wir in der Kindklasse ein Objekt der Elternklasse. Zu diesem fügen wir die spezifischen Attribute der abgeleiteten Klasse hinzu. Der gesamte Konstruktor sieht dann wie folgt aus:
Um das Beispiel so kurz wie möglich zu halten, beschränken wir uns bei den übrigen Methoden auf die Ausgabe der Anzahl der Räder. Die komplette Klasse sieht dann wie folgt aus:
Nun können wir ein kleines Hauptprogramm erstellen. Dieses erzeugt ein Objekt des Typs Motorrad. Daraufhin ruft es die Methoden getMarke(), getModell() und getRaeder() auf. Das zeigt, dass wir mit diesem Objekt sowohl auf die Methoden der Eltern- als auch der Kindklasse zugreifen können, ohne dass dafür eine spezielle Kennzeichnung notwendig ist. Über den Begriff super haben wir auch die Werte der Elternklasse vorgegeben. Das Ergebnis ist in der nachfolgenden Abbildung zu sehen. Der Code für das Hauptprogramm sieht wie folgt aus:
Den Schlüsselbegriff super für den Zugriff auf Methoden verwenden
Eine weitere Verwendungsmöglichkeit für den Schlüsselbegriff super besteht darin, auf Methoden der Elternklasse aus der Kindklasse zuzugreifen. Um auch diese an einem Beispiel zu demonstrieren, erstellen wir innerhalb der Klasse Motorrad die neue Methode taktAusgeben().
Diese soll in einem print-Befehl ausgeben, ob es sich um einen Zweitakter oder um einen Viertakter handelt. Für eine ansprechendere Darstellung fügen wir zu dieser Information auch die Marke und das Modell hinzu. Bei unserem Beispiel-Motorrad sollte die folgende Ausgabe erscheinen: „Beim BMW R 1200 RT handelt es sich um einen Viertakter.“
Zu diesem Zweck müssen wir innerhalb dieser Methode auf die Methoden getMarke() und getModell() aus der Elternklasse zugreifen. Dafür kommen mehrere Möglichkeiten infrage. Wir könnten etwa ausprobieren, einfach nur die Methode aufzurufen, ohne eine weitere Spezifikation anzugeben. Da die Methoden auch für die abgeleitete Klasse gelten, können wir auch den Ausdruck this voranstellen. Diese beiden Möglichkeiten probieren wir nun für die beiden Methoden aus:
Nun rufen wir im Hauptprogramm noch die entsprechende Methode auf, indem wir den folgenden Befehl ganz unten in die main()-Methode einfügen:
meinMotorrad.taktAusgeben();
Die folgende Abbildung zeigt, dass wir auf diese Weise genau das gewünschte Ergebnis erzielen.
Daran wird deutlich, dass wir mit diesen beiden Formen problemlos auf die Methoden der Elternklasse zugreifen können. Allerdings könnten wir hierfür auch den Begriff super verwenden. Die Methode würde dann wie folgt aussehen:
Das Ergebnis ist dabei genau das gleiche wie in der vorigen Abbildung. Das zeigt zwar, dass wir hierfür den Begriff super verwenden können. Doch stellt sich die Frage, weshalb wir dies tun sollten – schließlich erzielen wir das gewünschte Ergebnis ja auch mit den bereits bekannten Ausdrücken.
Die Verwendung des Ausdrucks super wird notwendig, wenn wir eine Methode überschreiben. Dazu müssen wir in der Kindklasse eine Methode erstellen, die den gleichen Namen wie die Methode der Elternklasse trägt. Zu diesem Zweck fügen wir eine Methode mit der Bezeichnung getMarke() ein. Diese soll jedoch eine andere Aufgabe erledigen als die Methode der Elternklasse. In unserem Beispiel geben wir lediglich aus, dass der entsprechende Wert in der Elternklasse definiert ist:
Wenn wir nun diese Methode in der Kindklasse ohne weiteren Bezeichner aufrufen oder den Ausdruck this verwenden, führt das Programm die Methode aus der abgeleiteten Klasse aus. Die folgende Abbildung zeigt, dass wir auf diese Weise nicht das gewünschte Ergebnis erzielen:
Wenn wir bei einer überschriebenen Methode auf die ursprüngliche Version in der Elternklasse zugreifen möchten, ist der Ausdruck super zwingend notwendig. Daher führt nun nur noch die zweite Version der Methode taktAusgeben() ans Ziel, da wir hier diesen Begriff verwendet haben. Auf diese Weise erhalten wir wieder das gewünschte Ergebnis.
Der Begriff super ist demnach nur dann erforderlich, wenn wir mit überschriebenen Methoden arbeiten. Allerdings kann es sinnvoll sein, ihn auch in anderen Fällen zu verwenden – immer dann, wenn wir auf eine Methode der Elternklasse zugreifen. Das macht deutlich, dass die entsprechende Methode in einer anderen Klasse definiert ist und erhöht dadurch die Übersichtlichkeit des Programmcodes.
Attribute mit super abrufen
Eine weitere Möglichkeit besteht darin, mit dem Begriff super auf Attribute der Elternklasse zuzugreifen. Diese kommt aber nur äußerst selten zum Einsatz. Der Grund dafür besteht darin, dass dies nur dann möglich ist, wenn man die Attribute der Elternklasse nicht kapselt. Da das keinem guten Programmierstil entspricht, ist es ratsam, darauf zu verzichten.
Obwohl in diesem Fall der Begriff super kaum Verwendung findet, wollen wir zum Abschluss dennoch ein kleines Beispiel anfügen, um eine komplette Darstellung der Verwendungsmöglichkeiten zu erzielen. Dazu nehmen wir nun auch ein Trike in unser Programm auf. Da es sich hierbei nach der Straßenverkehrsordnung um ein Motorrad handelt, wollen wir es über die Klasse Motorrad definieren. Allerdings ist darin die Zahl der Räder dort bereits fest vorgegeben. Die hier definierte Anzahl ist für das Trike jedoch nicht passend.
Aus diesem Grund erstellen wir eine neue Klasse für das Trike, die wir jedoch von der Klasse Motorrad ableiten. Hier definieren wir die Variable raeder neu und setzen sie auf 3. Damit überschreiben wir den Wert aus der Elternklasse. Darüber hinaus fügen wir vorerst nur einen Konstruktor ein. Dessen einzige Aufgabe besteht darin, den Konstruktor aus der Superklasse aufzurufen:
Nun haben wir sowohl in der Klasse Trike als auch in der Klasse Motorrad ein Attribut mit der Bezeichnung raeder. In den meisten Programmen ist es nicht erforderlich, auf überschriebene Attribute aus der Superklasse zuzugreifen. Um dies dennoch zu demonstrieren, erstellen wir die Methode raederAusgeben(). Diese soll darstellen, wie viele Räder ein gewöhnliches Motorrad und wie viele Räder ein Trike hat. Dazu greift sie auf das Attribut raeder zu – einmal in der Eltern- und einmal in der Kindklasse:
Hinweis: Im zweiten print-Befehl ist es auch möglich, lediglich den Ausdruck raeder zu verwenden, ohne dass sich dadurch der Ablauf des Programms ändert.
Nun kommt es jedoch zu einer Fehlermeldung. Das liegt daran, dass das Attribut raeder in der Klasse Motorrad gekapselt und damit nicht aus anderen Klassen zugänglich ist. Daher müssen wir dort den Ausdruck private durch public ersetzen.
Um diese Funktionsweise auszuprobieren, müssen wir dann noch das Hauptprogramm etwas abändern. Nun erzeugen wir hier ein Trike-Objekt. Außerdem rufen wir noch die Methode raederAusgeben() auf:
Wenn wir das Programm nun ausführen, zeigt sich das Ergebnis, das in der folgenden Abbildung zu sehen ist. Daran wird deutlich, dass wir mit dem Begriff super auf das Attribut der Elternklasse zugreifen können – auch wenn dies nicht zu empfehlen ist, da wir die Daten dadurch nicht kapseln können.