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.7    Abstrakte Klassen und abstrakte Methoden Zur vorigen ÜberschriftZur nächsten Überschrift

Nicht immer soll eine Klasse sofort ausprogrammiert werden, zum Beispiel dann nicht, wenn die Oberklasse lediglich Methoden für die Unterklassen vorgeben möchte, aber nicht weiß, wie sie diese implementieren soll. In Java gibt es dazu zwei Konzepte: abstrakte Klassen und Schnittstellen (engl. interfaces). Während final im Prinzip die Klasse abschließt und Unterklassen unmöglich macht, sind abstrakte Klassen das Gegenteil: Ohne Unterklassen sind abstrakte Klassen nutzlos.

Es ergeben sich daher drei Szenarien:

Klassentyp

Bedeutung

normale nichtabstrakte und nichtfinale Klasse

Eine Unterklasse kann gebildet werden, muss aber nicht.

finale Klasse

Eine Unterklasse kann nicht gebildet werden.

abstrakte Klasse

Eine Unterklasse muss gebildet werden.

Tabelle 7.3     Klassentypen im Vergleich

 

Zum Seitenanfang

7.7.1    Abstrakte Klassen Zur vorigen ÜberschriftZur nächsten Überschrift

Bisher konnten wir von jeder Klasse mit new ein Objekt bilden. Das Bilden von Exemplaren ist allerdings nicht immer sinnvoll, zum Beispiel soll es untersagt werden, wenn eine Klasse nur als Oberklasse in einer Vererbungshierarchie existieren soll. Sie kann dann als Modellierungsklasse eine Ist-eine-Art-von-Beziehung ausdrücken und Signaturen für die Unterklassen vorgeben. Eine Oberklasse besitzt dabei Vorgaben für die Unterklasse. Das heißt, alle Unterklassen erben die Methoden. Ein Exemplar der Oberklasse selbst muss nicht existieren.

Um dies in Java auszudrücken, setzen wir den Modifizierer abstract an die Typdeklaration der Oberklasse. Von dieser Klasse können dann keine Exemplare gebildet werden, und der Versuch einer Objekterzeugung führt zu einem Compilerfehler. Ansonsten verhalten sich die abstrakten Klassen wie normale Klassen, enthalten die gleichen Eigenschaften und können auch selbst von anderen Klassen erben. Abstrakte Klassen sind das Gegenteil von konkreten Klassen.

Wir wollen die Klasse Event als Oberklasse für alle Ereignisse abstrakt machen, da Exemplare davon nicht existieren müssen:

Listing 7.38     src/main/java/com/tutego/insel/game/c/vp/Event.java, Event

abstract class Event {

String about;

int duration;

}

Mit dieser abstrakten Klasse Event drücken wir aus, dass es eine allgemeine Klasse ist, von der keine konkreten Objekte existieren und gebildet werden können. Der Versuch ergibt einen Compilerfehler:

Event flight = new Event(); // 'Event' is abstract; cannot be instantiated

Der Sinn dahinter ist einfach: Es gibt in der realen Welt keine allgemeinen und unspezifizierten Ereignisse, sondern nur spezielle Unterarten, zum Beispiel ein Nickerchen, ein Fußballspiel oder das Naschen von Süßigkeiten. Es ergibt also keinen Sinn, ein Exemplar der Klasse Event zu bilden. Die Klasse soll nur in der Hierarchie auftauchen und Ereignisse sozusagen kategorisieren und ihnen Eigenschaften geben.

[+]  Tipp

Abstrakte Klassen lassen sich auch nutzen, um zu verhindern, dass ein Exemplar der Klasse gebildet wird. Der Modifizierer abstract sollte aber dazu nicht eingesetzt werden. Besser ist es, die Sichtbarkeit des Konstruktors auf private oder protected zu setzen.

Basistyp abstrakte Klasse

Abstrakte Klassen werden immer in Verbindung mit Vererbung eingesetzt. (Abstrakte) Oberklassen sind allgemein gehalten, und Unterklassen müssen den Basistyp weiter spezialisieren. Eine Klasse wird die abstrakte Klasse erweitern, und von dieser Klasse kann – wenn die Unterklasse nicht selbst abstrakt ist – eine Instanz gebildet werden.

Auch gilt die Ist-eine-Art-von-Beziehung weiterhin, sodass sich bei den Event-Unterklassen Nap und Workout schreiben lässt:

Listing 7.39     src/main/java/com/tutego/insel/game/c/vp/Application.java, Ausschnitt

Event   sleep   = new Nap();

Event running = new Workout();

Event[] events = { new Nap(), new Nap(), new Workout(), new Nap() };

Die Deklaration Event[] events kennzeichnet nur den Typ des Arrays. Das ist unabhängig davon, ob die Klasse Event abstrakt ist oder nicht; das Array enthält Referenzen auf Unterklassen von Event.

[»]  Hinweis

Abstrakte Klassen können natürlich auch wiederum abstrakte Unterklassen haben:

abstract class Event { }

abstract class Hackathon extends Event { }

Auch von Hackathon lässt sich keine Instanz mit new bilden.

 

Zum Seitenanfang

7.7.2    Abstrakte Methoden Zur vorigen ÜberschriftZur nächsten Überschrift

Der Modifizierer abstract vor dem Schlüsselwort class leitet die Deklaration einer abstrakten Klasse ein. Doch auch eine Methode kann abstrakt sein. Sie gibt lediglich die Signatur vor, und eine Unterklasse implementiert irgendwann diese Methode. Die abstrakte Klasse ist somit für den Kopf der Methode zuständig, während die Implementierung an anderer Stelle erfolgt. Das ist eine klare Trennung von »Was kann ich?« und »Wie mache ich es?«. Während die Oberklasse mit der Deklaration der abstrakten Methode ausdrückt, dass sie etwas kann, realisieren die Unterklassen, wie der Code dazu aussieht. Überspitzt gesagt: Abstrakte Methoden drücken aus, dass sie keine Ahnung von der Implementierung haben und dass sich die Unterklassen darum kümmern müssen. Das Ganze muss natürlich im Rahmen der Spezifikation geschehen.

Da eine abstrakte Klasse abstrakte Methoden enthalten kann, aber nicht enthalten muss, unterscheiden wir:

  • Rein (pure) abstrakte Klassen: Die abstrakte Klasse enthält ausschließlich abstrakte Methoden.

  • Partiell abstrakte Klassen: Die Klasse ist abstrakt, enthält aber auch konkrete Implementierungen, also nichtabstrakte Methoden. Das bietet den Unterklassen ein Gerüst, das sie nutzen können.

Bei der Reise kann ein Ereignis auftreten

In unserem kleinen Spiel soll es möglich sein, dass der Spieler die Stadt wechselt. Bei der Reise können unglücklicherweise verschiedene Ereignisse vorkommen.

  • Der Spieler könnte überfallen und ihm könnten seine Süßigkeiten geklaut werden.

  • Der Spieler könnte hungrig werden, und dann isst er einige Süßigkeiten auf.

  • Oder vielleicht werden durch einen glücklichen Zufall dem Spieler neue Süßigkeiten geschenkt.

Die Ereignisse sind zufällig, und vielleicht passiert bei der Reise auch überhaupt nichts.

Für ein Ereignis hatten wir schon eine Klasse deklariert. Jetzt soll in der Oberklasse Event eine abstrakte Methode für Programmcode deklariert werden, die bei dem Eintreten des Ereignisses ausgeführt werden kann. Damit die einzelnen Ereignisse Zugriff auf den Spieler haben, wird der Methode der aktuelle Spieler übergeben. Damit können die Ereignismethoden auf den Spieler zurückgreifen. Wir nennen so etwas auch Kontextobjekt.

Die abstrakte Methode sieht so aus:

Listing 7.40     src/main/java/com/tutego/insel/game/c/vq/Event.java, Event

abstract class Event {

String about;

int duration;



abstract void process( Player player );

}

Da abstrakte Methoden immer ohne Implementierung sind, steht statt des Methodenrumpfs in geschweiften Klammern ein Semikolon. Ist mindestens eine Methode abstrakt, so ist es automatisch die ganze Klasse. Deshalb müssen wir das Schlüsselwort abstract ausdrücklich vor den Klassennamen schreiben. Vergessen wir das Schlüsselwort abstract bei einer solchen Klasse, erhalten wir einen Compilerfehler. Eine Klasse mit einer abstrakten Methode muss abstrakt sein, da sonst irgendjemand ein Exemplar konstruieren und genau diese Methode aufrufen könnte. Versuchen wir, ein Exemplar einer abstrakten Klasse zu erzeugen, so bekommen wir ebenfalls einen Compilerfehler. Natürlich kann eine abstrakte Klasse nichtabstrakte Eigenschaften haben, so wie es Event mit den Objektvariablen about und duration zeigt. Konkrete Methoden sind auch erlaubt, die brauchen wir jedoch hier nicht. Eine toString()-Methode hatten wir in früheren Beispielen ja auch schon programmiert.

Prinzipiell wäre es auch möglich gewesen, einen leeren Rumpf zu haben und dann darauf zu hoffen, dass die Unterklassen die Methode überschreiben. Doch es ist eine bewusste Designentscheidung, die Methode abstrakt zu machen, weil ja unklar ist, was genau bei diesem speziellen Ereignis passieren soll. Jedes Ereignis ist ein wenig anders, und es ist einfach unmöglich, hier eine gewisse Implementierung anzubieten. Abstrakte Methoden drücken aus, dass diese Implementierung zwingend nötig sein muss.

Vererben von abstrakten Methoden

Wenn wir von einer Klasse abstrakte Methoden erben, so haben wir zwei Möglichkeiten:

  • Wir überschreiben alle abstrakten Methoden und implementieren sie. Dann muss die Unterklasse nicht mehr abstrakt sein (wobei sie es auch weiterhin sein kann). Von der Unterklasse kann es ganz normale Exemplare geben.

  • Wir überschreiben die abstrakte Methode nicht, sodass sie normal vererbt wird. Das bedeutet: Eine abstrakte Methode bleibt in unserer Klasse, und die Klasse muss wiederum abstrakt sein.

Kommen wir zurück zum Beispiel: Die abstrakte Methode aus der Oberklasse soll von den Unterklassen (unterschiedlich) implementiert werden, denn jedes Ereignis macht mit den Spielern etwas anderes.

Beginnen wir mit dem Überfall. Dabei verliert der Spieler alle Süßigkeiten.

Listing 7.41     src/main/java/com/tutego/insel/game/c/vqi/Mugging.java, Ausschnitt

class Mugging extends Event {

@Override void process( Player player ) {

player.candy.quantity = 0;

}

}

Ein Geschenk ist da schon viel besser: Der Spieler bekommt eine willkürliche Anzahl von neuen Süßigkeiten geschenkt:

Listing 7.42     src/main/java/com/tutego/insel/game/c/vq/Gift.java, Ausschnitt

class Gift extends Event {

@Override void process( Player player ) {

player.candy.quantity += ThreadLocalRandom.current().nextInt( 1, 10 );

}

}
In der UML werden die Namen abstrakter Klassen kursiv gesetzt. Unterklassen überschreiben die abstrakte Methode der Oberklasse.

Abbildung 7.12     In der UML werden die Namen abstrakter Klassen kursiv gesetzt. Unterklassen überschreiben die abstrakte Methode der Oberklasse.

Im Testprogramm wollen wir den Spieler initialisieren, ihm eine Süßigkeit geben, dann ihn überfallen und anschließend beschenken:

Listing 7.43     src/main/java/com/tutego/insel/game/c/vq/Playground.java, main

Candy liquorice = new Candy();

liquorice.quantity = 10;

liquorice.name = "Die salzige Leckmuschel";

Player peter = new Player();

peter.candy = liquorice;



Event mugging = new Mugging();

Event gift = new Gift();



System.out.println( peter.candy.quantity ); // 10

mugging.process( peter );

System.out.println( peter.candy.quantity ); // 0

gift.process( peter );

System.out.println( peter.candy.quantity ); // 7

An der Ausgabe lässt sich gut ablesen, wie die Ereignisbehandlung auf den Spieler peter einwirkt und die Anzahl Süßigkeiten verändert.

[»]  Hinweis

Wenn Methoden einmal mit Rumpf existieren, so können sie nicht später abstrakt überschrieben werden und somit noch tieferen Unterklassen vorschreiben, sie zu überschreiben. Wenn eine Implementierung einmal vorhanden ist, kann sie nicht wieder versteckt werden. So existiert z. B. toString() in Object und könnte nicht in Event abstrakt überschrieben werden, um etwa den Unterklassen Gift oder Mugging vorzuschreiben, dass sie toString() überschreiben müssen.

inline image  Implementiert eine Klasse nicht alle geerbten abstrakten Methoden, so muss die Klasse selbst wieder abstrakt sein. Ist unsere Unterklasse einer abstrakten Basisklasse nicht abstrakt, so bietet IntelliJ mit (Alt)+(¢) an, entweder die eigene Klasse abstrakt zu machen oder alle geerbten abstrakten Methoden mit einem Dummy-Rumpf zu implementieren.

Das Schöne an abstrakten Methoden ist, dass sie auf jeden Fall von konkreten Exemplaren realisiert werden. Hier finden also immer polymorphe Methodenaufrufe statt.

Zu Ende gespielt

Bisher haben wir noch nicht den Wechsel der Städte implementiert, das vorherige Beispiel sollte nur die Typbeziehungen verdeutlichen. Jetzt wollen wir das Spiel erweitern, dass der Spieler die Stadt wechseln kann und dann zufällige Dinge passieren können.

Zunächst noch einmal die drei Event-Unterklassen, die jetzt eine Konsolenausgabe bekommen.

Listing 7.44     src/main/java/com/tutego/insel/game/c/vq/Mugging.java, Mugging

class Mugging extends Event {

@Override void process( Player player ) {

System.out.println( "Hilfe! Jemand klaut dir alle Süßigkeiten." );

player.candy.quantity = 0;

}

}

Listing 7.45     src/main/java/com/tutego/insel/game/c/vq/Gift.java, Mugging

class Gift extends Event {

@Override void process( Player player ) {

System.out.println( "Du Glückspilz! Jemand schenkt dir Süßigkeiten." );

player.candy.quantity += ThreadLocalRandom.current().nextInt( 1, 10 );

}

}

Listing 7.46     src/main/java/com/tutego/insel/game/c/vq/Eating.java, Eating

class Eating extends Event {

@Override void process( Player player ) {

System.out.println( "Du hast Hunger und isst einige deiner Süßigkeiten." );

player.candy.quantity *= Math.random();

}

}

Neu ist eine Event-Unterklasse, die nichts macht. Wir nennen solche Objekte auch Nullobjekte:

Listing 7.47     src/main/java/com/tutego/insel/game/c/vq/NoopEvent.java, NoopEvent

class NoopEvent extends Event {

@Override void process( Player player ) { }

}

Da die Ereignisse zufällig auftreten können, soll eine statische Methode random() ein Zufallsereignis liefern.

Listing 7.48     src/main/java/com/tutego/insel/game/c/vq/RandomGameEvents.java, RandomGameEvents

class RandomGameEvents {

private RandomGameEvents() {}

static Event next() {

double random = Math.random();

if ( random < 0.5 ) return new NoopEvent();

if ( random < 0.8 ) return new Gift();

if ( random < 0.9 ) return new Eating();

return new Mugging();

}

}

Aus der Methode lässt sich ablesen, dass die Ereignisse nicht gleich wahrscheinlich sind. Zu 50 % soll beim Reisen nichts passieren, zu 30 % soll es Geschenke geben, zu 10 % hat der Spieler Hunger, und zu 10 % wird er überfallen.

Die Klasse Candy ist vereinfacht und enthält nur Namen und Anzahl:

Listing 7.49     src/main/java/com/tutego/insel/game/c/vq/Candy.java, Candy

public class Candy {

String name;

int quantity;

}

Interessant wird es beim Spieler, da er zu einem neuen Ziel reisen kann:

Listing 7.50     src/main/java/com/tutego/insel/game/c/vq/Player.java, Player

class Player {

Candy candy;

String location;



void moveTo( String cityName ) {

if ( location.equalsIgnoreCase( cityName ) ) {

System.out.println( "Du bist doch schon da! (inline image_inline image)" );

return;

}

RandomGameEvents.next().process( this );

location = cityName;

}

}

Die Methode moveTo(String) bekommt einen beliebigen Städtenamen übergeben, und falls sich der Spieler schon in der Stadt befindet, ist eine Reise unnötig. Der interessante Teil folgt, wenn die Reise angetreten wird: Ein zufälliges Ereignis wird ermittelt und der Spieler der process(Player)-Methode übergeben.

Listing 7.51     src/main/java/com/tutego/insel/game/c/vq/Application.java, main

Candy liquorice = new Candy();

liquorice.quantity = 10;

liquorice.name = "Salzige Leckmuschel";

Player peter = new Player();

peter.candy = liquorice;

peter.location = "Dortmund";



System.out.println( "Gib das Ziel ein, wohin du reisen möchtest. " +

"Eine Leereingabe beendet das Spiel." );

while ( true ) {

System.out.printf( "Du bist aktuell in %s und besitzt %d '%s'%n",

peter.location, peter.candy.quantity, peter.candy.name );

System.out.print( "Ziel: " );

String input = new Scanner( System.in ).nextLine();

if ( input.trim().isEmpty() ) break;

peter.moveTo( input );

}

Das eigentliche Hauptprogramm ist jetzt nicht mehr groß. Eine Süßigkeit wird wie der Spieler aufgebaut und die Endlosschleife betreten. Das Programm bittet um eine Eingabe, und wenn keine Eingabe erfolgt, beendet das die Schleife, und das Programm ist zu Ende. Falls die Eingabe nicht leer war, ist das ein Reiseziel, und Peter reist an diesen Ort, mit allen Überraschungen.

[+]  Warum?

Abstrakte Methoden sind ein sehr wichtiges Werkzeug, denn sie geben ein Versprechen ab, dass eine Implementierung später vorhanden sein wird. Es ist weniger so, dass wir selbst diese Methoden aufrufen, sondern andere. Wir erlauben somit anderen Parteien, unsere Implementierungen aufzurufen. Das wird oft von Frameworks genutzt: Die Bibliothek deklariert eine abstrakte Klasse mit einer abstrakten Methode, und wir implementieren diese Methode, bauen ein Exemplar von dieser Implementierung und übergeben das Objekt wieder dem Framework. Das weiß nun, dass die Methode implementiert wurde, und kann sie aufrufen. Ohne dynamisches Binden würde das alles überhaupt nicht funktionieren.

 


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