Rheinwerk Computing < openbook >


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


Download:

- Listings, ca. 2,7 MB


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 Spielobjekte 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.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.4.3 Finale Klassen und finale Methoden
Pfeil 7.4.4 Kovariante Rückgabetypen
Pfeil 7.4.5 Array-Typen und Kovarianz *
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.5.3 Nicht dynamisch gebunden bei privaten, statischen und finalen Methoden
Pfeil 7.5.4 Dynamisch gebunden auch bei Konstruktoraufrufen *
Pfeil 7.5.5 Eine letzte Spielerei mit Javas dynamischer Bindung und überdeckten Attributen *
Pfeil 7.6 Abstrakte Klassen und abstrakte Methoden
Pfeil 7.6.1 Abstrakte Klassen
Pfeil 7.6.2 Abstrakte Methoden
Pfeil 7.7 Schnittstellen
Pfeil 7.7.1 Schnittstellen sind neue Typen
Pfeil 7.7.2 Schnittstellen deklarieren
Pfeil 7.7.3 Abstrakte Methoden in Schnittstellen
Pfeil 7.7.4 Implementieren von Schnittstellen
Pfeil 7.7.5 Ein Polymorphie-Beispiel mit Schnittstellen
Pfeil 7.7.6 Die Mehrfachvererbung bei Schnittstellen
Pfeil 7.7.7 Keine Kollisionsgefahr bei Mehrfachvererbung *
Pfeil 7.7.8 Erweitern von Interfaces – Subinterfaces
Pfeil 7.7.9 Konstantendeklarationen bei Schnittstellen
Pfeil 7.7.10 Nachträgliches Implementieren von Schnittstellen *
Pfeil 7.7.11 Statische ausprogrammierte Methoden in Schnittstellen
Pfeil 7.7.12 Erweitern und Ändern von Schnittstellen
Pfeil 7.7.13 Default-Methoden
Pfeil 7.7.14 Erweiterte Schnittstellen deklarieren und nutzen
Pfeil 7.7.15 Öffentliche und private Schnittstellenmethoden
Pfeil 7.7.16 Erweiterte Schnittstellen, Mehrfachvererbung und Mehrdeutigkeiten *
Pfeil 7.7.17 Bausteine bilden mit Default-Methoden *
Pfeil 7.7.18 Initialisierung von Schnittstellenkonstanten *
Pfeil 7.7.19 Markierungsschnittstellen *
Pfeil 7.7.20 (Abstrakte) Klassen und Schnittstellen im Vergleich
Pfeil 7.8 SOLIDe Modellierung
Pfeil 7.8.1 DRY, KISS und YAGNI
Pfeil 7.8.2 SOLID
Pfeil 7.8.3 Sei nicht STUPID
Pfeil 7.9 Zum Weiterlesen
 

Zum Seitenanfang

7.3    Typen in Hierarchien Zur vorigen ÜberschriftZur nächsten Überschrift

Die Vererbung bringt einiges Neue in Bezug auf Kompatibilität von Typen mit. Dieser Abschnitt beschäftigt sich mit den Fragen, welche Typen kompatibel sind und wie sich ein Typ zur Laufzeit testen lässt.

 

Zum Seitenanfang

7.3.1    Automatische und explizite Typumwandlung Zur vorigen ÜberschriftZur nächsten Überschrift

Die Klassen Room und Player haben wir als Unterklassen von GameObject modelliert. Die eigene Oberklasse GameObject erweitert selbst keine explizite Oberklasse, sodass implizit java.lang.Object die Oberklasse ist. In GameObject gibt es das Attribut name, das Player und Room erben, und der Raum hat zusätzlich size für die Raumgröße.

Ist-eine-Art-von-Beziehung und die automatische Typumwandlung

Mit der Ist-eine-Art-von-Beziehung ist eine interessante Eigenschaft verbunden, die wir bemerken, wenn wir die Zusammenhänge zwischen den Typen beachten:

  • Ein Raum ist ein Raum.

  • Ein Spieler ist ein Spieler.

  • Ein Raum ist ein Spielobjekt.

  • Ein Spieler ist ein Spielobjekt.

  • Ein Spielobjekt ist ein java.lang.Object.

  • Ein Spieler ist ein java.lang.Object.

  • Ein Raum ist ein java.lang.Object.

Kodieren wir das in Java:

Listing 7.19    src/main/java/com/tutego/insel/game/vd/TypeSuptype.java, main()

Player     playerIsPlayer     = new Player();

GameObject gameObjectIsPlayer = new Player();

Object objectIsPlayer = new Player();

Room roomIsRoom = new Room();

GameObject gameObjectIsRoom = new Room();

Object objectIsRoom = new Room();

Es gilt also, dass immer dann, wenn ein Typ gefordert ist, auch ein Untertyp erlaubt ist. Der Compiler führt eine implizite Typumwandlung durch. Wir werden uns dieses sogenannte liskovsche Substitutionsprinzip im folgenden Abschnitt anschauen.

[»]  Hinweis

Wenn wir var nutzen, dann wird der Typ der Variablen automatisch so sein wie der auf der rechten Seite.

Was wissen Compiler und Laufzeitumgebung über unser Programm?

Compiler und Laufzeitumgebung haben verschiedene Sichten auf das Programm und wissen unterschiedliche Dinge. Durch den Einsatz von new gibt es zur Laufzeit nur zwei Arten von Objekten: Player und Room. Auch dann, wenn es

GameObject goIsRoom = new Room();

heißt, referenziert goIsRoom zur Laufzeit ein Room-Objekt. Der Compiler aber »vergisst« dies und glaubt, goIsRoom wäre nur ein einfaches GameObject. In der Klasse GameObject ist jedoch nur name deklariert, aber kein Attribut size, obwohl das tatsächliche Room-Objekt natürlich eine size kennt. Auf size können wir aber erst einmal nicht zugreifen:

println( goIsRoom.name );

println( goIsRoom.size ); // inline image gameObjectIsRoom.size cannot

// be resolved or is not a field

Schreiben wir noch einschränkender

Object objectIsRoom = new Room();

println( objectIsRoom.name ); // inline image objectIsRoom.name cannot be

// resolved or is not a field

println( objectIsRoom.size ); // inline image objectIsRoom.size cannot be

// resolved or is not a field

so steht hinter der Referenzvariablen objectIsRoom ein vollständiges Room-Objekt, aber weder size noch name sind nutzbar; es bleiben nur die Fähigkeiten aus java.lang.Object.

[»]  Begrifflichkeit

Um den Typ, den der Compiler kennt, von dem Typ, den die JVM kennt, zu unterscheiden, nutzen wir die Begriffe Referenztyp und Objekttyp. Im Fall von GameObject p = new Player(); ist GameObject der Referenztyp und Player der Objekttyp (Merkhilfe: Es steht ein Objekt zur Laufzeit im Speicher). Der Compiler sieht nur den Referenztyp, aber nicht den Objekttyp. Vereinfacht gesagt: Der Compiler interessiert sich bei einer Konstruktion wie GameObject p = new Player(); nur für den linken Teil GameObject p und die Laufzeitumgebung nur für den rechten Teil p = new Player().

Explizite Typumwandlung

Diese Typeinschränkung gilt auch an anderer Stelle. Ist eine Variable vom Typ Room deklariert, können wir die Variable nicht mit einem »kleineren« Typ initialisieren:

GameObject go         = new Room();    // Raum zur Laufzeit

Room cubbyhole = go; // inline image Type mismatch: cannot convert from

// GameObject to Room

Auch wenn zur Laufzeit go ein Room referenziert, können wir cubbyhole nicht damit initialisieren. Der Compiler kennt go nur unter dem »kleineren« Typ GameObject, und das reicht nicht zur Initialisierung des »größeren« Typs Room.

Es ist aber möglich, das Objekt hinter go durch eine explizite Typumwandlung für den Compiler wieder zu einem vollwertigen Room mit Größe zu machen:

Room       cubbyhole = (Room) go;

System.out.println( cubbyhole.size ); // Room hat das Attribut size

Unmögliche Anpassung und ClassCastException

Dies funktioniert aber lediglich dann, wenn go auch wirklich einen Raum referenziert. Dem Compiler ist das in dem Moment relativ egal, sodass auch Folgendes ohne Fehler compiliert wird:

Listing 7.20    src/main/java/com/tutego/insel/game/vd/ClassCastExceptionDemo.java, main()

GameObject go        = new Player();

Room cubbyhole = (Room) go; // inline image ClassCastException

System.out.println( cubbyhole.size );

Zur Laufzeit kommt es bei diesem Kuckucksobjekt zu einer ClassCastException:

Exception in thread "main" java.lang.ClassCastException: c.t.i.g.vd.Player cannot 

be cast to c.t.i.g.vd.Room

at c.t.i.g.vd.ClassCastExceptionDemo.main(ClassCastExceptionDemo.java:8)
[»]  Hinweis

Wenn wir

GameObject go = new Room();

schreiben, haben wir uns gezielt bei go für den Typ GameObject entschieden. Wir sollten uns bewusst sein, dass bei einer Variablendeklaration mit var die neue Variable exakt den Typ der rechten Seite bekommt.

var go = new Room();

Hier ist go ein Room, »hat« also mehr als ein GameObject. Es kann bei der Programmierung relevant sein, den Typ zu beschränken.

 

Zum Seitenanfang

7.3.2    Das Substitutionsprinzip Zur vorigen ÜberschriftZur nächsten Überschrift

Nehmen wir an, wir möchten Räume und Spieler an eine Methode übergeben, um eine Frage zu stellen. Die erste Idee wäre vermutlich die, zwei Methoden zu deklarieren:

public static void printQuestion( Room obj ) {

System.out.println( "Woher kommt " + obj.name + "?" );

}

public static void printQuestion( Player obj ) {

System.out.println( "Woher kommt " + obj.name + "?" );

}

Allerdings geht es besser, denn die Vererbung erlaubt eine Vereinfachung.

Stellen wir uns vor, Bekannte kommen ausgehungert von einer Wandertour zurück und fragen: »Haste was zu essen?« Die Frage zielt wohl darauf ab, dass es bei Hunger ziemlich egal ist, was wir anbieten, wichtig ist nur etwas Essbares. Daher können wir Eis, aber auch gegrillte Heuschrecken und Hakarl (isländischer fermentierter Hai) anbieten.

Diese Ausgangslage führt uns zu einem wichtigen Konzept in der Objektorientierung: »Wer wenig will, kann viel bekommen.« Genauer gesagt: Wenn Unterklassen wie Player oder Room die Oberklasse GameObject erweitern, können wir überall, wo GameObject gefordert wird, auch einen Player oder Room übergeben, da beide ja vom Typ GameObject sind und wir mit der Unterklasse nur spezieller werden. Auch können wir weitere Unterklassen von Player und Room übergeben, da auch die Unterklasse weiterhin zusätzlich das »Gen« GameObject in sich trägt. Alle diese Dinge wären vom Typ GameObject und daher typkompatibel. Wenn nun etwa eine Methode eine Übergabe vom Typ GameObject erwartet, kann sie alle Eigenschaften von GameObject nutzen, also das Attribut name, da ja alle Unterklassen die Eigenschaften erben und Unterklassen die Eigenschaften nicht »wegzaubern« können. Derjenige, dem wir »mehr« übergeben, kann zwar nichts mit den Erweiterungen anfangen, ablehnen wird er das Objekt aber nicht, weil es alle geforderten Eigenschaften aufweist.

Weil anstelle eines Objekts auch ein Objekt der Unterklasse auftauchen kann, sprechen wir von Substitution. Das Prinzip wurde von der Professorin Barbara Liskov[ 163 ](Die Zeitschrift »Discover« zählt sie zu den 50 wichtigsten Frauen in der Wissenschaft. ) formuliert und heißt daher auch liskovsches Substitutionsprinzip.

Die folgende Klasse AskForNameOfGameObject nutzt diese Eigenschaft. Sie fordert in der Methode printQuestion(GameObject) irgendein GameObject, von dem bekannt ist, dass es ein Attribut name hat, und formuliert eine Frage, woher der Name kommt. Im Hauptprogramm kann printQuestion(GameObject) ein Spieler oder Raum übergeben werden:

Listing 7.21    src/main/java/com/tutego/insel/game/vd/AskForNameOfGameObject.java, Ausschnitt

public static void printQuestion( GameObject go ) {

System.out.println( "Woher kommt " + go.name + "?" );

}



public static void main( String[] args ) {

Player player = new Player();

player.name = "Godman";

printQuestion( player ); // Woher kommt Godman?



GameObject room = new Room();

room.name = "Hogwurz";

printQuestion( room ); // Woher kommt Hogwurz?

}

Mit GameObject haben wir eine Basisklasse geschaffen, die verschiedenen Unterklassen Grundfunktionalität beibringt, in unserem Fall das Attribut name. So liefert die Basisklasse einen gemeinsamen Nenner, etwa gemeinsame Attribute oder Methoden, die jede Unterklasse besitzen wird. Das ist viel flexibler, als für die beiden Typen Room und Player eine Methode quote(Room) und printQuestion(Player) zu schreiben. Denn wenn es im Spiel später neue GameObject-Typen gibt, behandelt printQuestion(GameObject) diese ganz selbstverständlich mit.

In der Java-Bibliothek finden sich zahllose weitere Beispiele. Die println(Object)-Methode ist so ein Beispiel. Die Methode nimmt beliebige Objekte entgegen, denn der Parametertyp ist Object. Die Substitution besagt, dass wir alle Objekte dort einsetzen können, da alle Klassen von Object abgeleitet sind.

 

Zum Seitenanfang

7.3.3    Typen mit dem instanceof-Operator testen Zur vorigen ÜberschriftZur nächsten Überschrift

Der relationale Operator instanceof hilft dabei, Exemplare auf ihre Verwandtschaft mit einem Referenztyp zu prüfen. Er stellt zur Laufzeit fest, ob eine Referenz ungleich null und von einem bestimmten Typ ist. Der Operator ist binär, hat also zwei Operanden:

Listing 7.22    src/main/java/com/tutego/insel/game/vd/InstanceofDemo.java, Ausschnitt 1

String s = "Toll";

System.out.println( s instanceof String ); // true

System.out.println( "Toll" instanceof Object ); // true

System.out.println( new Player() instanceof Object ); // true

Alles in doppelten Anführungsstrichen ist ein String, sodass instanceof String wahr ergibt. Für den zweiten und dritten Fall gilt: Alle Objekte gehen irgendwie aus Object hervor und sind somit logischerweise Erweiterungen.

[»]  Hinweis

Der Operator instanceof testet ein Objekt auf seine Hierarchie. So ist zum Beispiel o instanceof Object für jedes Objekt o wahr, denn jedes Objekt ist immer Kind von java.lang.Object. Die Programmiersprache Smalltalk unterscheidet hier mit zwei Nachrichten isMemberOf (exakt) und isKindOf (wie Javas instanceof). Um den exakten Typ zu testen, lässt sich mit dem Class-Objekt arbeiten, etwa wie im Ausdruck o.getClass() == Object.class, der testet, ob o genau ein Object-Objekt ist.

Die bisherigen Beziehungen hätte der Compiler bereits herausfinden können. Vervollständigen wir das, um zu sehen, dass instanceof wirklich zur Laufzeit den Test durchführen muss. In allen Fällen ist das Objekt zur Laufzeit ein Raum:

Listing 7.23    src/main/java/com/tutego/insel/game/vd/InstanceofDemo.java, Ausschnitt 2

Room       go1 = new Room();

System.out.println( go1 instanceof Room ); // true

System.out.println( go1 instanceof GameObject ); // true

System.out.println( go1 instanceof Object ); // true



GameObject go2 = new Room();

System.out.println( go2 instanceof Room ); // true

System.out.println( go2 instanceof GameObject ); // true

System.out.println( go2 instanceof Object ); // true

System.out.println( go2 instanceof Player ); // false



Object go3 = new Room();

System.out.println( go3 instanceof Room ); // true

System.out.println( go3 instanceof GameObject ); // true

System.out.println( go3 instanceof Object ); // true

System.out.println( go3 instanceof Player ); // false

System.out.println( go3 instanceof String ); // false

Keine beliebigen Typtests mit instanceof

Der Compiler lässt aber nicht alles durch. Liegen zwei Typen überhaupt nicht in der Typhierarchie, lehnt der Compiler den Test ab, da die Vererbungsbeziehungen schon inkompatibel sind:

System.out.println( "Toll" instanceof StringBuilder );

// inline image Incompatible conditional operand types String and StringBuilder

Der Ausdruck ist falsch, da StringBuilder keine Basisklasse für String ist.

Zum Schluss:

Object ref1 = new int[ 100 ];

System.out.println( ref1 instanceof String );

System.out.println( new int[100] instanceof String ); // inline image Compilerfehler
[»]  Hinweis

Mit instanceof lässt sich der Programmfluss aufgrund der tatsächlichen Typen steuern, etwa mit Anweisungen wie if(reference instanceof Typ) A else B. In der Regel zeigt Kontrolllogik dieser Art aber tendenziell ein Designproblem an und kann oft anders gelöst werden. Das dynamische Binden ist so eine Lösung; sie wird später in Abschnitt 7.5, »Drum prüfe, wer sich dynamisch bindet«, vorgestellt.

instanceof und null

Ein instanceof-Test mit einer Referenz, die null ist, gibt immer false zurück:

String ref2 = null;

System.out.println( ref2 instanceof String ); // false

System.out.println( ref2 instanceof Object ); // false

Das leuchtet ein, denn null entspricht ja keinem konkreten Objekt.

[+]  Tipp

Da instanceof einen null-Test enthält, sollte statt etwa

if ( s != null && s instanceof String )

immer vereinfacht so geschrieben werden:

if ( s instanceof String )

 


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: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Objektorientierte Programmierung

Objektorientierte Programmierung




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

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2021

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