Rheinwerk Computing < openbook >


 
Inhaltsverzeichnis
Materialien zum Buch
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Arrays und ihre Anwendungen
5 Der Umgang mit Zeichen und Zeichenketten
6 Eigene Klassen schreiben
7 Objektorientierte Beziehungsfragen
8 Schnittstellen, Aufzählungen, versiegelte Klassen, Records
9 Ausnahmen müssen sein
10 Geschachtelte Typen
11 Besondere Typen der Java SE
12 Generics<T>
13 Lambda-Ausdrücke und funktionale Programmierung
14 Architektur, Design und angewandte Objektorientierung
15 Java Platform Module System
16 Die Klassenbibliothek
17 Einführung in die nebenläufige Programmierung
18 Einführung in Datenstrukturen und Algorithmen
19 Einführung in grafische Oberflächen
20 Einführung in Dateien und Datenströme
21 Einführung ins Datenbankmanagement mit JDBC
22 Bits und Bytes, Mathematisches und Geld
23 Testen mit JUnit
24 Die Werkzeuge des JDK
A Java SE-Module und Paketübersicht
Stichwortverzeichnis


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 7 Objektorientierte Beziehungsfragen
Pfeil 7.1 Assoziationen zwischen Objekten
Pfeil 7.1.1 Unidirektionale 1:1-Beziehung
Pfeil 7.1.2 Zwei Freunde müsst ihr werden – bidirektionale 1:1-Beziehungen
Pfeil 7.1.3 Unidirektionale 1:n-Beziehung
Pfeil 7.2 Vererbung
Pfeil 7.2.1 Vererbung in Java
Pfeil 7.2.2 Ereignisse modellieren
Pfeil 7.2.3 Die implizite Basisklasse java.lang.Object
Pfeil 7.2.4 Einfach- und Mehrfachvererbung *
Pfeil 7.2.5 Sehen Kinder alles? Die Sichtbarkeit protected
Pfeil 7.2.6 Konstruktoren in der Vererbung und super(…)
Pfeil 7.3 Typen in Hierarchien
Pfeil 7.3.1 Automatische und explizite Typumwandlung
Pfeil 7.3.2 Das Substitutionsprinzip
Pfeil 7.3.3 Typen mit dem instanceof-Operator testen
Pfeil 7.3.4 Pattern-Matching bei instanceof
Pfeil 7.4 Methoden überschreiben
Pfeil 7.4.1 Methoden in Unterklassen mit neuem Verhalten ausstatten
Pfeil 7.4.2 Mit super an die Eltern
Pfeil 7.5 Drum prüfe, wer sich dynamisch bindet
Pfeil 7.5.1 Gebunden an toString()
Pfeil 7.5.2 Implementierung von System.out.println(Object)
Pfeil 7.6 Finale Klassen und finale Methoden
Pfeil 7.6.1 Finale Klassen
Pfeil 7.6.2 Nicht überschreibbare (finale) Methoden
Pfeil 7.7 Abstrakte Klassen und abstrakte Methoden
Pfeil 7.7.1 Abstrakte Klassen
Pfeil 7.7.2 Abstrakte Methoden
Pfeil 7.8 Weiteres zum Überschreiben und dynamischen Binden
Pfeil 7.8.1 Nicht dynamisch gebunden bei privaten, statischen und finalen Methoden
Pfeil 7.8.2 Kovariante Rückgabetypen
Pfeil 7.8.3 Array-Typen und Kovarianz *
Pfeil 7.8.4 Dynamisch gebunden auch bei Konstruktoraufrufen *
Pfeil 7.8.5 Keine dynamische Bindung bei überdeckten Objektvariablen *
Pfeil 7.9 Zum Weiterlesen und Programmieraufgabe
 

Zum Seitenanfang

7.4    Methoden überschreiben Zur vorigen ÜberschriftZur nächsten Überschrift

Wir haben gesehen, dass eine Unterklasse durch Vererbung die sichtbaren Eigenschaften ihrer Oberklasse erbt. Die Unterklasse kann nun wiederum Methoden hinzufügen. Dabei zählen überladene Methoden – also Methoden, die den gleichen Namen wie eine andere Methode aus einer Oberklasse tragen, aber eine andere Parameteranzahl oder andere Parametertypen haben – zu ganz normalen, hinzugefügten Methoden.

 

Zum Seitenanfang

7.4.1    Methoden in Unterklassen mit neuem Verhalten ausstatten Zur vorigen ÜberschriftZur nächsten Überschrift

Die Methoden sind das Angebot eines Objekts und die Schnittstelle nach außen. In erster Linie ist das ein Was, aber kein Wie. Unterklassen müssen bedingungslos das Gleiche können wie ihre Oberklasse, allerdings kann das Wie abweichen. In so einem Fall kann die Unterklasse eine Methode der Oberklasse überschreiben. Implementiert die Unterklasse die Methode neu, so sagt sie auf diese Weise: »Ich kann’s besser.« Die überschreibende Methode der Unterklasse kann demnach den Programmcode spezialisieren und Eigenschaften nutzen, die in der Oberklasse nicht bekannt sind. Die überschriebene Methode der Oberklasse ist dann erst einmal aus dem Rennen, und ein Methodenaufruf auf einem Objekt der Unterklasse würde sich in der überschriebenen Methode verfangen.

Damit eine Methode eine andere Methode überschreibt, muss die Unterklasse eine Methode mit dem gleichen Methodennamen und der exakt gleichen Parameterliste (also der gleichen Signatur) besitzen. Der Name der Parametervariablen ist irrelevant. Ist der Rückgabetyp void oder ein primitiver Typ, so muss er in der überschreibenden Methode der gleiche sein. Bei Referenztypen kann der Rückgabetyp etwas variieren, doch das werden wir in Abschnitt 7.8.2, »Kovariante Rückgabetypen«, genauer sehen.

[»]  Hinweis

Wir sprechen nur von überschriebenen Methoden und nicht von überschriebenen Objekt-/Klassenvariablen, da Variablen nicht überschrieben, sondern nur überdeckt[ 165 ](Die JLS unterscheidet genau genommen »shadowing« und »hiding«. Interessierte Leser mögen das unter https://docs.oracle.com/javase/specs/jls/se17/html/jls-6.html#jls-6.4 nachlesen. ) werden. Objektvariablen werden auch nicht dynamisch gebunden – eine Eigenschaft, die später in Abschnitt 7.8.5, »Keine dynamische Bindung bei überdeckten Objektvariablen *«, genauer erklärt wird.

Überschreiben von toString()

Aus der absoluten Basisklasse java.lang.Object bekommen alle Unterklassen eine Methode toString() vererbt, die, meist zu Debug-Zwecken, eine Objektkennung ausgibt:

Listing 7.26     java/lang/Object.java, toString()

public String toString() {

return getClass().getName() + "@" + Integer.toHexString(hashCode());

}

Die Methode liefert den Namen der Klasse, gefolgt von einem "@" und einer hexadezimalen Kennung. Die Klasse Event ohne eigenes toString() soll die Wirkung testen:

Listing 7.27     src/main/java/com/tutego/insel/game/c/vm/Event.java, Event

class Event {

String about;

int duration;

}

Auf einem Event-Objekt liefert toString() eine etwas kryptische Kennung:

Listing 7.28     src/main/java/com/tutego/insel/game/c/vm/Application.java, Ausschnitt

Event e = new Event();

String s = e.toString();

System.out.println( s ); // com.tutego.insel.game.vm.Event@1a2b3c4d

Es ist also eine gute Idee, toString() in den Unterklassen zu überschreiben. Eine textuelle Objektkennung sollte den Namen der Klasse und die Zustände eines Objekts beinhalten. Für ein Workout, das ein (geerbtes) about und duration sowie selbst caloriesBurned hat, kann dies wie folgt aussehen:

Listing 7.29     src/main/java/com/tutego/insel/game/c/vm/Workout.java, Workout

public class Workout extends Event {

int caloriesBurned;



@Override public String toString() {

return String.format( "%s[about=%s, duration=%d, caloriesBurned=%d]",

getClass().getSimpleName(),

about, duration, caloriesBurned );

}

}

Den Ausdruck getClass().getSimpleName() übernimmt die Implementierung aus der Klasse Object, und wir bekommen damit dynamisch den Klassennamen, den wir somit nicht fest als String einkodieren müssen. Das ist nützlich, wenn sich einmal der Klassenname ändert.

»Workout« ist eine Unterklasse von »Event« und hat ein eigenes »toString()«. Ein UML-Stereotyp kennzeichnet die Überschreibung

Abbildung 7.11     »Workout« ist eine Unterklasse von »Event« und hat ein eigenes »toString()«. Ein UML-Stereotyp kennzeichnet die Überschreibung

Und der Test sieht so aus:

Listing 7.30     src/main/java/com/tutego/insel/game/c/vm/Application.java, Ausschnitt

Workout running = new Workout();

running.about = "Joggen";

running.duration = 60;

running.caloriesBurned = 250;

System.out.println( running );

Zur Erinnerung: Ein println(Object) auf einem beliebigen Objekt ruft die toString()-Methode von diesem Objekt auf. Die Ausgabe ist:

Workout[about=Joggen, duration=60, caloriesBurned=250]
[»]  Exkurs zur Annotation

Wir haben schon oft mit unterschiedlichen Modifizierern gearbeitet, etwa static oder public. Das Besondere an diesen Modifizierern ist, dass sie die Programmsteuerung nicht beeinflussen, aber dennoch wichtige Zusatzinformationen darstellen, also Semantik einbringen. Diese Informationen nennen sich Metadaten. Die Modifizierer static, public sind Metadaten für den Compiler, doch mit etwas Fantasie lassen sich auch Metadaten vorstellen, die nicht vom Compiler, sondern von einer Java-Bibliothek ausgewertet werden. So wie public zum Beispiel dem Compiler sagt, dass ein Element für jeden sichtbar ist, kann auf der anderen Seite auch zum Beispiel ein besonderes Metadatum an einem Element hängen, um auszudrücken, dass das Element nur bestimmte Wertebereiche annehmen kann.

Java bietet eine eingebaute Fähigkeit für Metadaten: Annotationen. Die Annotationen lassen sich wie benutzerdefinierte Modifizierer erklären. Wir können zwar keine neue Sichtbarkeit erfinden, aber dennoch dem Compiler, bestimmten Werkzeugen oder der Laufzeitumgebung durch die Annotationen Zusatzinformationen geben. Dazu ein paar Beispiele für Annotationen und Anwendungsfälle:

Annotation

Erklärung

@WebService class Calculator {

@WebMethod int add( int x, int y ) ...

Definiert einen Webservice mit einer Webservice-Methode.

@Override public String toString() ...

Überschreibt eine Methode der Oberklasse.

@XmlRoot class Person { ...

Ermöglicht die Abbildung eines Objekts auf eine XML-Datei.

Tabelle 7.2     Beispiele für Annotationen und Anwendungsfälle

Annotationen werden wie zusätzliche Modifizierer gebraucht, doch unterscheiden sie sich durch ein vorangestelltes @-Zeichen (das @-Zeichen, at, ist auch eine gute Abkürzung für Annotation Type). Daher ist auch die Reihenfolge egal, sodass man zum Beispiel

  • @Override public String toString() oder

  • public @Override String toString()

schreiben kann. Es ist aber üblich, die Annotationen an den Anfang zu setzen. Und wenn Annotationen an Typen gesetzt werden, bekommen sie in der Regel eine eigene Zeile.

Die Annotationstypen sind die Deklarationen, wie etwa ein Klassentyp. Werden sie an ein Element gehängt, ist es eine konkrete Annotation. Während also Override selbst der Annotationstyp ist, ist @Override vor toString() die konkrete Annotation.

Die Annotation @Override

Unsere Beispielklasse Workout nutzt die Annotation @Override an der Methode toString() und macht auf diese Weise deutlich, dass die Klasse eine Methode des Obertyps überschreibt. Die Annotation @Override bedeutet nicht, dass diese Methode in Unterklassen überschrieben werden muss, sondern nur, dass sie selbst eine Methode überschreibt. Annotationen sind zusätzliche Modifizierer, die entweder vom Compiler überprüft werden oder von uns nachträglich abgefragt werden können. Obwohl wir die Annotation @Override nicht nutzen müssen, hat dies zwei Vorteile:

  • Zwar weiß die Laufzeitumgebung, dass eine Methode überschrieben wird, allerdings sollte Code dem Leser auch alle Informationen darüber geben, was passiert. Wird eine Methode überschrieben, ist das etwas Bedeutsames, das im Code dokumentiert werden sollte.

  • Außerdem überprüft der Compiler, ob wir tatsächlich eine Methode aus der Oberklasse überschreiben. Haben wir uns zum Beispiel im Methodennamen verschrieben und somit der Unterklasse unbeabsichtigt eine neue Methode hinzugefügt, so würde das der Compiler aufgrund seiner Kenntnis von @Override als Fehler melden. Einfache Schreibfehler wie tostring() fallen schnell auf. Überladene Methoden und überschriebene Methoden sind etwas anderes, da eine überladene Methode mit der Ursprungsmethode nur »zufällig« den Namen teilt, aber sonst keinen Bezug zur Logik hat. Und so hilft @Override, dass Entwickler wirklich Methoden überschreiben und nicht aus Versehen Methoden mit falschen Parametern überladen.

Finale Parameter in der Vererbung *

Wird eine Methode überschrieben, dann sind die Typen der Parameterliste bestimmend, nicht die Namen. Auch spielt es keine Rolle, ob die Parametervariablen final sind oder nicht. Wir können es als zusätzliche Information für die jeweilige Methode betrachten. Eine Unterklasse kann demnach beliebig das final hinzufügen oder auch wegnehmen. Alte Bibliotheken lassen sich so leicht weiterverwenden.

 

Zum Seitenanfang

7.4.2    Mit super an die Eltern Zur vorigen ÜberschriftZur nächsten Überschrift

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? Im Fall der überschriebenen toString()-Methode realisiert die Unterklasse eine völlig neue Implementierung und bezieht sich dabei nicht auf die Logik der Oberklasse.

Möchte eine Unterklasse sagen: »Was meine Eltern können, ist doch gar nicht so schlecht«, kann mit der speziellen Referenz super auf die Eigenschaften im Namensraum der Oberklasse zugegriffen werden. (Natürlich ist das Objekt hinter super und this das gleiche, nur der Namensraum ist ein anderer.) Auf diese Weise können Unterklassen immer noch etwas Eigenes machen, aber die Realisierung aus der Elternklasse ist weiterhin verfügbar.

In unserem Spiel hatte Event kein toString(). Ändern wir dies:

Listing 7.31     src/main/java/com/tutego/insel/game/c/vn/Event.java, Event

class Event {

String about;

int duration;



@Override public String toString() {

return String.format( "%s[about=%s, duration=%d]",

getClass().getSimpleName(),

about, duration );

}

}

Die Unterklasse Workout erweitert Event und sollte toString() neu realisieren, da ein Workout eine zusätzliche Objektvariable hat, nämlich caloriesBurned. Wird toString() allerdings in Workout überschrieben, muss sich toString() auch um die geerbten Eigenschaften kümmern, sprich: um about und duration. Das ist ungünstig, denn kommt etwa in der Oberklasse Event eine Objektvariable hinzu oder wird gelöscht, müssen alle toString()-Methoden von allen Unterklassen geändert werden, wenn die Unterklassen alle Belegungen der Objektvariablen mit in die String-Kennung einbinden möchten. Das ist ungünstig. Allgemein gesprochen: Eine Unterklasse sollte keine Verantwortung für die Oberklasse übernehmen.

Eine Lösung für das Problem ist, in toString() einer Unterklasse wie Workout einfach auf die toString()-Methode der Oberklasse Event zuzugreifen und dann die möglichen Zustände vom Basistyp mit aufzunehmen:

Listing 7.32     src/main/java/com/tutego/insel/game/c/vn/Event.java, Ausschnitt

public class Workout extends Event {

int caloriesBurned;



@Override public String toString() {

return String.format( "%s[caloriesBurned=%d]",

super.toString(),

caloriesBurned );

}

}

Stünde statt super.toString() nur toString() im Rumpf, würde der Methodenaufruf in die Endlosrekursion führen. Daher funktioniert es ohne super-Referenz nicht.

Ein Test zeigt das Resultat:

Listing 7.33     src/main/java/com/tutego/insel/game/c/vn/Application.java, Ausschnitt

Workout running = new Workout();

running.about = "Joggen";

running.duration = 60;

running.caloriesBurned = 250;

System.out.println( running );

Die Ausgabe ist:

Workout[about=Joggen, duration=60][caloriesBurned=250]

Eigenschaften der super-Referenz *

Nicht nur in überschriebenen Methoden kann die super-Referenz sinnvoll eingesetzt werden: Sie ist auch interessant, wenn Methoden der Oberklasse aufgerufen werden sollen und nicht eigene überschriebene. So macht das folgende Beispiel klar, dass auf jeden Fall toString() der Oberklasse Object aufgerufen werden soll und nicht die eigene überschriebene Variante:

Listing 7.34     src/main/java/com/tutego/insel/oop/ToStringFromSuper.java

public class ToStringFromSuper {



public ToStringFromSuper() {

System.out.println( super.toString() ); // Aufruf von Object toString()

}



@Override

public String toString() {

return "Nein";

}



public static void main( String[] args ) {

new ToStringFromSuper(); // ToStringFromSuper@3e25a5

}

}

Natürlich kann super nur dann eingesetzt werden, wenn in der Oberklasse die Methode eine gültige Sichtbarkeit hat. Es ist also nicht möglich, mit diesem Konstrukt das Geheimnisprinzip zu durchbrechen.

Eine Aneinanderreihung von super-Schlüsselwörtern bei einer tieferen Vererbungshierarchie ist nicht möglich. Hinter einem super muss eine Objekteigenschaft stehen; sie gilt also für eine überschriebene Methode oder eine überdeckte Objektvariable. Anweisungen wie super. super.lol() sind somit immer ungültig. Eine Unterklasse empfängt alle Eigenschaften ihrer Oberklassen als Einheit und unterscheidet nicht, aus welcher Hierarchie etwas kommt.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Spring Boot 3 und Spring Framework 6

Spring Boot 3 und Spring Framework 6




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2024

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.

Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de



Cookie-Einstellungen ändern