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

Pfeil7 Objektorientierte Beziehungsfragen
Pfeil7.1 Assoziationen zwischen Objekten
Pfeil7.1.1 Unidirektionale 1:1-Beziehung
Pfeil7.1.2 Zwei Freunde müsst ihr werden – bidirektionale 1:1-Beziehungen
Pfeil7.1.3 Unidirektionale 1:n-Beziehung
Pfeil7.2 Vererbung
Pfeil7.2.1 Vererbung in Java
Pfeil7.2.2 Spielobjekte modellieren
Pfeil7.2.3 Die implizite Basisklasse java.lang.Object
Pfeil7.2.4 Einfach- und Mehrfachvererbung *
Pfeil7.2.5 Sehen Kinder alles? Die Sichtbarkeit protected
Pfeil7.2.6 Konstruktoren in der Vererbung und super(…)
Pfeil7.3 Typen in Hierarchien
Pfeil7.3.1 Automatische und explizite Typumwandlung
Pfeil7.3.2 Das Substitutionsprinzip
Pfeil7.3.3 Typen mit dem instanceof-Operator testen
Pfeil7.4 Methoden überschreiben
Pfeil7.4.1 Methoden in Unterklassen mit neuem Verhalten ausstatten
Pfeil7.4.2 Mit super an die Eltern
Pfeil7.4.3 Finale Klassen und finale Methoden
Pfeil7.4.4 Kovariante Rückgabetypen
Pfeil7.4.5 Array-Typen und Kovarianz *
Pfeil7.5 Drum prüfe, wer sich dynamisch bindet
Pfeil7.5.1 Gebunden an toString()
Pfeil7.5.2 Implementierung von System.out.println(Object)
Pfeil7.5.3 Nicht dynamisch gebunden bei privaten, statischen und finalen Methoden
Pfeil7.5.4 Dynamisch gebunden auch bei Konstruktoraufrufen *
Pfeil7.5.5 Eine letzte Spielerei mit Javas dynamischer Bindung und überdeckten Attributen *
Pfeil7.6 Abstrakte Klassen und abstrakte Methoden
Pfeil7.6.1 Abstrakte Klassen
Pfeil7.6.2 Abstrakte Methoden
Pfeil7.7 Schnittstellen
Pfeil7.7.1 Schnittstellen sind neue Typen
Pfeil7.7.2 Schnittstellen deklarieren
Pfeil7.7.3 Abstrakte Methoden in Schnittstellen
Pfeil7.7.4 Implementieren von Schnittstellen
Pfeil7.7.5 Ein Polymorphie-Beispiel mit Schnittstellen
Pfeil7.7.6 Die Mehrfachvererbung bei Schnittstellen
Pfeil7.7.7 Keine Kollisionsgefahr bei Mehrfachvererbung *
Pfeil7.7.8 Erweitern von Interfaces – Subinterfaces
Pfeil7.7.9 Konstantendeklarationen bei Schnittstellen
Pfeil7.7.10 Nachträgliches Implementieren von Schnittstellen *
Pfeil7.7.11 Statische ausprogrammierte Methoden in Schnittstellen
Pfeil7.7.12 Erweitern und Ändern von Schnittstellen
Pfeil7.7.13 Default-Methoden
Pfeil7.7.14 Erweiterte Schnittstellen deklarieren und nutzen
Pfeil7.7.15 Öffentliche und private Schnittstellenmethoden
Pfeil7.7.16 Erweiterte Schnittstellen, Mehrfachvererbung und Mehrdeutigkeiten *
Pfeil7.7.17 Bausteine bilden mit Default-Methoden *
Pfeil7.7.18 Initialisierung von Schnittstellenkonstanten *
Pfeil7.7.19 Markierungsschnittstellen *
Pfeil7.7.20 (Abstrakte) Klassen und Schnittstellen im Vergleich
Pfeil7.8 SOLIDe Modellierung
Pfeil7.8.1 DRY, KISS und YAGNI
Pfeil7.8.2 SOLID
Pfeil7.8.3 Sei nicht STUPID
Pfeil7.9 Zum Weiterlesen
 

Zum Seitenanfang

7.5    Drum prüfe, wer sich dynamisch bindet Zur vorigen ÜberschriftZur nächsten Überschrift

Bei der Vererbung haben wir eine Form der Ist-eine-Art-von-Beziehung, sodass die Unterklassen immer auch vom Typ der Oberklassen sind. Die sichtbaren Methoden, die die Oberklassen besitzen, existieren somit auch in den Unterklassen. Der Vorteil bei der Spezialisierung ist, dass die Oberklasse eine einfache Implementierung vorgibt und eine Unterklasse diese überschreiben kann. Wir hatten das bisher bei toString() gesehen. Doch nicht nur die Spezialisierung ist aus Sicht des Designs interessant, sondern auch die Bedeutung der Vererbung. Bietet eine Oberklasse eine sichtbare Methode an, so wissen wir immer, dass alle Unterklassen diese Methode haben werden, egal ob sie die Methode überschreiben oder nicht. Wir werden gleich sehen, dass dies zu einem der wichtigsten Konstrukte in objektorientierten Programmiersprachen führt.

 

Zum Seitenanfang

7.5.1    Gebunden an toString() Zur vorigen ÜberschriftZur nächsten Überschrift

Da jede Klasse Eigenschaften von java.lang.Object erbt, lässt sich auf jedem Objekt die toString()-Methode aufrufen. Sie soll in unseren Klassen GameObject und Room wie schon vorgestellt implementiert sein:

Listing 7.37    src/main/java/com/tutego/insel/game/vf/GameObject.java, GameObject

public class GameObject {



public String name;



@Override public String toString() {

return String.format( "%s[name=%s]", getClass().getSimpleName(), name );

}

}

Listing 7.38    src/main/java/com/tutego/insel/game/vf/Room.java, Room

public class Room extends GameObject {



public int size;



@Override public String toString() {

return super.toString() + "[size=" + size+ "]";

}

}

Die Unterklassen GameObject und Room überschreiben die toString()-Methode aus Object. Bei einem toString() auf einem GameObject kommt nur der Name in die toString()-Kennung, und bei einem toString() auf einem Room-Objekt kommen Name und Größe in die String-Repräsentation.

Es fehlen noch einige kleine Testzeilen, die drei Räume aufbauen. Alle rufen die toString()-Methoden auf den Räumen auf, wobei der Unterschied darin besteht, dass die verweisende Referenzvariable alle Typen von Room durchgeht: Ein Room ist ein Room, ein Room ist ein GameObject, und ein Room ist ein Object:

Listing 7.39    src/main/java/com/tutego/insel/game/vf/Playground.java, main()

Room rr = new Room();

rr.name = "Affenhausen";

rr.size = 7349944;

System.out.println( rr.toString() );



GameObject rg = new Room();

rg.name = "Affenhausen";

System.out.println( rg.toString() );



Object ro = new Room();

System.out.println( ro.toString() );

Jetzt ist die spannendste Frage in der gesamten Objektorientierung folgende: Was passiert bei dem Methodenaufruf toString()?

Antwort:

Room[name=Affenhausen][size=7349944]

Room[name=Affenhausen][size=0]

Room[name=null][size=0]

Die Ausgabe ist leicht zu verstehen, wenn wir berücksichtigen, dass der Compiler nicht die gleiche Weisheit besitzt wie die Laufzeitumgebung. Vom Compiler würden wir erwarten, dass er jeweils das toString() in Room, aber auch in GameObject (die Ausgabe wäre nur der Name) und toString() aus Object aufruft – dann wäre die Kennung die kryptische.

Entscheidend ist, dass die Laufzeitumgebung den Objekttyp nutzt und nicht den Referenztyp – das ist das gleiche Verhalten wie bei instanceof. Da dem im Programmtext vereinbarten Variablentyp nicht zu entnehmen ist, welche Implementierung der Methode toString() aufgerufen wird, sprechen wir von später dynamischer Bindung, kurz dynamischer Bindung. Erst zur Laufzeit (das ist spät, im Gegensatz zur Übersetzungszeit) wählt die Laufzeitumgebung dynamisch die entsprechende Objektmethode aus – passend zum tatsächlichen Typ des aufrufenden Objekts. Die virtuelle Maschine weiß, dass hinter den drei Variablen jeweils ein Raum-Objekt steht, und ruft daher das toString() vom Room auf.

Wichtig ist, dass eine Methode überschrieben wird; von einer gleichlautenden Methode in beiden Unterklassen GameObject und Room hätten wir nichts, da sie nicht in Object deklariert ist. Sonst hätten die Klassen nur rein »zufällig« diese Methode, aber die Ober- und Unterklassen verbindet nichts. Wir nutzen daher ausdrücklich die Gemeinsamkeit, dass GameObject, Player und weitere Unterklassen toString() aus Object erben. Ohne die Oberklasse gäbe es kein Bindeglied, und folglich bietet die Oberklasse immer eine Methode an, die Unterklassen überschreiben können. Würden wir eine neue Unterklasse von Object schaffen und toString() nicht überschreiben, so fände die Laufzeitumgebung toString() in Object, aber die Methode gäbe es auf jeden Fall – entweder die Originalmethode oder die überschriebene Variante.

[»]  Begrifflichkeit

Dynamische Bindung wird oft auch Polymorphie genannt; ein dynamisch gebundener Aufruf ist dann ein polymorpher Aufruf. Das ist im Kontext von Java in Ordnung, allerdings gibt es in der Welt der Programmiersprachen unterschiedliche Dinge, die »Polymorphie« genannt werden, etwa parametrische Polymorphie (in Java heißt das Generics), und die Theoretiker kennen noch viel mehr beängstigende Begriffe.

 

Zum Seitenanfang

7.5.2    Implementierung von System.out.println(Object) Zur vorigen ÜberschriftZur nächsten Überschrift

Werfen wir einen Blick auf ein Programm, das dynamisches Binden noch deutlicher macht. Die printXXX(…)-Methoden sind so überladen, dass sie jedes beliebige Objekt annehmen und dann die String-Repräsentation ausgeben:

Listing 7.40    java/io/PrintStream.java, Skizze von println()

public void println( Object x ) {

String s = String.valueOf( x );

// String s = (obj == null) ? "null" : obj.toString();

synchronized ( this ) {

print( s );

newLine();

}

}

Die println(Object)-Methode besteht aus drei Teilen: Als Erstes wird die String-Repräsentation eines Objekts erfragt – hier findet sich der dynamisch gebundene Aufruf –, dann wird dieser String an print(String) weitergegeben, und newLine() produziert abschließend den Zeilenumbruch.

Der Compiler hat überhaupt keine Ahnung, was x ist; es kann alles sein, denn alles ist ein java.lang.Object. Statisch lässt sich aus dem Argument x nichts ablesen, und so muss die Laufzeitumgebung entscheiden, an welche Klasse der Methodenaufruf geht. Das ist das Wunder der dynamischen Bindung.

inline image  Eclipse zeigt bei der Tastenkombination (Strg)+(T) eine Typhierarchie an, standardmäßig die Oberklassen und bekannten Unterklassen.

 

Zum Seitenanfang

7.5.3    Nicht dynamisch gebunden bei privaten, statischen und finalen Methoden Zur vorigen ÜberschriftZur nächsten Überschrift

Obwohl Methodenaufrufe eigentlich dynamisch gebunden sind, gibt es bei privaten, statischen und finalen Methoden eine Ausnahme. Das liegt daran, dass nur überschriebene Methoden an dynamischer Bindung teilnehmen, und wenn es kein Überschreiben gibt, dann gibt es auch keine dynamische Bindung. Und da weder private noch statische oder finale Methoden überschrieben werden können, sind Methodenaufrufe auch nicht dynamisch gebunden. Sehen wir uns das an einer privaten Methode an:

Listing 7.41    src/main/java/com/tutego/insel/oop/NoPolyWithPrivate.java

class NoPolyWithPrivate {



public static void main( String[] args ) {

Banana unsicht = new Banana();

System.out.println( unsicht.bar() ); // 2

}

}



class Fruit {



private int furcht() {

return 2;

}



int bar() {

return furcht();

}

}



class Banana extends Fruit {



// Überschreibt nicht, daher kein @Override

public int furcht() {

return 1;

}

}

Der Compiler meldet bei der Methode furcht() in der Unterklasse keinen Fehler. Für den Compiler ist es in Ordnung, wenn es eine Methode in der Unterklasse gibt, die den gleichen Namen wie eine private Methode in der Oberklasse trägt. Das ist auch gut so, denn private Implementierungen sind ja ohnehin geheim und versteckt. Die Unterklasse soll von den privaten Methoden in der Oberklasse gar nichts wissen. Statt von Überschreiben sprechen wir hier von Überdecken oder Verdecken.

Die Laufzeitumgebung macht etwas Erstaunliches für unsicht.bar(): Die Methode bar() wird aus der Oberklasse geerbt. Wir wissen, dass in bar() aufgerufene Methoden normalerweise dynamisch gebunden werden, das heißt, dass wir eigentlich bei furcht() in Banana landen müssten, da wir ein Objekt vom Typ Banana haben. Bei privaten Methoden ist das aber anders, da sie nicht vererbt werden. Wenn eine aufgerufene Methode den Modifizierer private trägt, wird nicht dynamisch gebunden, und unsicht.bar() bezieht sich bei furcht() auf die Methode aus Fruit.

System.out.println( unsicht.bar() );   // 2

Anders wäre es, wenn bei furcht() der Sichtbarkeitsmodifizierer public wäre; wir bekämen dann die Ausgabe 1.

Dass private, statische und finale Methoden nicht überschrieben werden, ist ein wichtiger Beitrag zur Sicherheit. Falls nämlich Unterklassen interne private Methoden überschreiben könnten, wäre dies eine Verletzung der inneren Arbeitsweise der Oberklasse. In einem Satz: Private Methoden sind nicht in den Unterklassen sichtbar und werden daher nicht überschrieben. Andernfalls könnten private Implementierungen im Nachhinein geändert werden, und Oberklassen wären nicht mehr sicher, dass nur ihre eigenen Methoden benutzt werden.

Schauen wir, was passiert, wenn wir in der Methode bar() über die this-Referenz auf ein Objekt vom Typ Banana casten:

int bar() {

return ((Banana)(this)).furcht();

}

Dann wird ausdrücklich diese furcht() aus Banana aufgerufen, was jedoch kein typisches objektorientiertes Konstrukt darstellt, da Oberklassen ihre Unterklassen im Allgemeinen nicht kennen. bar() in der Klasse Fruit ist somit unnütz.

 

Zum Seitenanfang

7.5.4    Dynamisch gebunden auch bei Konstruktoraufrufen * Zur vorigen ÜberschriftZur nächsten Überschrift

Dass ein Konstruktor der Unterklasse zuerst den Konstruktor der Oberklasse aufruft, kann die Initialisierung der Variablen in der Unterklasse stören. Schauen wir uns erst Folgendes an:

class Bouncer extends Bodybuilder {

String who = "Ich bin ein Rausschmeißer";

}

Wo wird nun die Variable who initialisiert? Wir wissen, dass die Initialisierungen immer im Konstruktor vorgenommen werden, doch gibt es ja noch gleichzeitig ein super() im Konstruktor. Da die Spezifikation von Java Anweisungen vor super() verbietet, muss die Zuweisung hinter dem Aufruf der Oberklasse folgen. Das Problem ist nun, dass ein Konstruktor der Oberklasse früher aufgerufen wird, als Variablen in der Unterklasse initialisiert wurden. Wenn es die Oberklasse nun schafft, auf die Variablen der Unterklasse zuzugreifen, wird der erst später gesetzte Wert fehlen. Der Zugriff gelingt tatsächlich, doch nur durch einen Trick, da eine Oberklasse (etwa Bodybuilder) nicht auf die Variablen der Unterklasse zugreifen kann. Wir können aber in der Oberklasse genau jene Methode der Unterklasse aufrufen, die die Unterklasse aus der Oberklasse überschreibt. Da Methodenaufrufe dynamisch gebunden werden, kann eine Methode den Wert auslesen:

Listing 7.42    src/main/java/com/tutego/insel/oop/Bouncer.java

class Bodybuilder {



Bodybuilder() {

whoAmI();

}



void whoAmI() {

System.out.println( "Ich weiß es noch nicht :-(" );

}

}



public class Bouncer extends Bodybuilder {



String who = "Ich bin ein Rausschmeißer";



@Override

void whoAmI() {

System.out.println( who );

}



public static void main( String[] args ) {

Bodybuilder bb = new Bodybuilder();

bb.whoAmI();



Bouncer bouncer = new Bouncer();

bouncer.whoAmI();

}

}

Die Ausgabe ist nun folgende:

Ich weiß es noch nicht :-(

Ich weiß es noch nicht :-(

null

Ich bin ein Rausschmeißer

Das Besondere an diesem Programm ist die Tatsache, dass überschriebene Methoden – hier whoAmI() – dynamisch gebunden werden. Diese Bindung gibt es auch dann schon, wenn das Objekt noch nicht vollständig initialisiert wurde. Daher ruft der Konstruktor der Oberklasse Bodybuilder nicht whoAmI() von Bodybuilder auf, sondern whoAmI() von Bouncer. Wenn in diesem Beispiel ein Bouncer-Objekt erzeugt wird, dann ruft Bouncer mit super() den Konstruktor von Bodybuilder auf. Dieser ruft wiederum die Methode whoAmI() in Bouncer auf, und er findet dort keinen String, da dieser erst nach super() gesetzt wird. Schreiben wir den Konstruktor von Bouncer einmal ausdrücklich hin:

public class Bouncer extends Bodybuilder {



String who;



Bouncer() {

super();

who = "Ich bin ein Rausschmeißer";

}

}

Die Konsequenz, die sich daraus ergibt, ist folgende: Dynamisch gebundene Methodenaufrufe über die this-Referenz sind in Konstruktoren potenziell gefährlich und sollten deshalb vermieden werden. Vermeiden lässt sich das, indem der Konstruktor nur private (oder finale) Methoden aufruft, da diese nicht dynamisch gebunden werden. Wenn der Konstruktor eine private (finale) Methode in seiner Klasse aufruft, dann bleibt es auch dabei.

 

Zum Seitenanfang

7.5.5    Eine letzte Spielerei mit Javas dynamischer Bindung und überdeckten Attributen * Zur vorigen ÜberschriftZur nächsten Überschrift

Der Kanadier »Furious Pete«[ 165 ](http://guinnessworldrecords.com/news/2016/4/competitive-eater-challenged-to-fastest-time-to-eat-a-12%E2%80%99%E2%80%99-pizza-record-guinnes-426366) isst alles in Rekordzeit; verewigen wir seine Pizzavertilgungsleistungen in einem Java-Programm. Die Oberklasse PizzaEater repräsentiert die Durchschnittsesser, die für eine 12"-Pizza geschätzte 900 Sekunden benötigen. FuriousPete ist eine Spezialisierung und schafft es in 32 Sekunden:

Listing 7.43    src/main/java/com/tutego/insel/oop/FuriousPete.java

class PizzaEater {



int consumptionTime = 900 /* Seconds */;



void eat() {

System.out.printf( "Ich esse in %d Sekunden eine Pizza%n", consumptionTime );

}

}



public class FuriousPete extends PizzaEater {



int consumptionTime = 32 /* Seconds */;



@Override void eat() {

System.out.println( consumptionTime ); // 32

System.out.println( super.consumptionTime ); // 900

System.out.println( this.consumptionTime ); // 32

System.out.println( ((PizzaEater) this).consumptionTime ); // 900

}



public static void main( String[] args ) {

new FuriousPete().eat();

}

}

Die Oberklasse PizzaEater deklariert eine Objektvariable consumptionTime, und auch die Unterklasse deklariert ein Attribut mit dem gleichen Namen – es ist in Java zulässig, dass ein Attribut ein anderes gleich benanntes Attribut überdeckt.

Die Unterklasse kann mit super.consumptionTime eine Ebene höher kommen. super ist wie this eine spezielle Referenz und kann auch genauso eingesetzt werden, nur dass super in den Namensraum der Oberklasse geht. Eine Aneinanderreihung von super-Schlüsselwörtern bei einer tieferen Vererbungshierarchie ist nicht möglich. Hinter einem super muss direkt eine Objekteigenschaft stehen, und Anweisungen wie super.super.consumptionTime sind somit immer ungültig.

Bei Methodenaufrufen bindet das Laufzeitsystem immer dynamisch; bei Attributzugriffen ist das nicht so: Hier bestimmt der Compiler, von welcher Klasse das Attribut genommen werden soll. Unser Programm zeigt das an der Anweisung:

System.out.println( ((PizzaEater) this).consumptionTime ); // 3000

Die Ausgabe 3000 ist identisch mit System.out.println(super.consumptionTime). Die this-Referenz hat in dem Kontext den Typ FuriousPete. Wenn wir den Typ aber in den Basistyp PizzaEater konvertieren, bekommen wir genau die Belegung von consumptionTime aus der Basisklasse unserer Hierarchie. Eine explizite Typumwandlung in Richtung eines Obertyps ist bei dynamisch gebundenen Methodenaufrufen auch nie nötig; die Laufzeitumgebung entscheidet selbstständig, wohin der Aufruf geht. Setzen wir in die eat()-Methoden vom FuriousPete die Zeile

((PizzaEater) this).eat();

ist das identisch mit

eat();

also eine Rekursion.

 


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