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.6    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.6.1    Abstrakte Klassen Zur vorigen ÜberschriftZur nächsten Überschrift

Bisher haben wir Vererbung eingesetzt, und jede Klasse konnte Objekte 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 GameObject als Oberklasse für die Spielgegenstände abstrakt machen, da Exemplare davon nicht existieren müssen:

Listing 7.44    src/main/java/com/tutego/insel/game/vh/GameObject.java, GameObject

public abstract class GameObject {

public String name;

}

Mit dieser abstrakten Klasse GameObject drücken wir aus, dass es eine allgemeine Klasse ist, zu der keine konkreten Objekte existieren. Es gibt in der realen Welt schließlich keine allgemeinen und unspezifizierten Spielgegenstände, sondern nur spezielle Unterarten, zum Beispiel Spieler, Schlüssel und Räume. Es ergibt also keinen Sinn, ein Exemplar der Klasse GameObject zu bilden. Die Klasse soll nur in der Hierarchie auftauchen, um alle Spielobjekte zum Typ GameObject zu machen und ihnen einige Eigenschaften zu geben. Dies zeigt, dass Oberklassen allgemeiner gehalten sind und Unterklassen weiter spezialisieren.

In der UML werden die Namen abstrakter Klassen kursiv gesetzt.

Abbildung 7.11    In der UML werden die Namen abstrakter Klassen kursiv gesetzt.

[+]  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

Die abstrakten Klassen werden normalerweise in der Vererbung eingesetzt. Eine Klasse kann die abstrakte Klasse erweitern und dabei auch selbst wieder abstrakt sein. Auch gilt die Ist-eine-Art-von-Beziehung weiterhin, sodass sich schließlich Folgendes schreiben lässt:

Listing 7.45    src/main/java/com/tutego/insel/game/vh/Declarations.java, main()

GameObject   go1 = new Room();

GameObject go2 = new Player();

GameObject[] gos = { new Player(), new Room() };
[»]  Hinweis

Die Deklaration GameObject[] gos = { new Player(), new Room() } ist die Kurzform für GameObject[] gos = new GameObject[]{ new Player(), new Room() }. Wenn im Programmcode new GameObject[]{…} steht, kennzeichnet das nur den Typ des Arrays. Das ist unabhängig davon, ob die Klasse GameObject abstrakt ist oder nicht.

 

Zum Seitenanfang

7.6.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.

Mit Spielobjekten muss sich spielen lassen

Damit wir mit den Spielobjekten wie Tür, Schlüssel, Raum und Spieler wirklich spielen können, sollen die Objekte aufeinander angewendet werden können. Das Ziel unseres Programms ist es, Sätze abzubilden, die aus Subjekt, Verb und Objekt bestehen:

  • Schlüssel öffnet Tür.

  • Spieler nimmt Bier.

  • Pinsel kitzelt Spieler.

  • Radio spielt Musik.

Die Programmversion soll etwas einfacher sein und statt unterschiedlicher Aktionen (öffnen, nehmen …) nur »nutzen« kennen.

Zur Umsetzung dieser Aufgabe bekommen die Spielklassen wie Schlüssel, Spieler, Tür, Radio eine spezielle Methode, die ein anderes Spielobjekt nimmt und testet, ob sie aufeinander angewendet werden können. Ein Schlüssel öffnet eine Tür, aber »öffnet« keine Musik. Demnach kann ein Musik-Objekt nicht auf ein Tür-Objekt angewendet werden, wohl aber ein Schlüssel-Objekt. Ist die Anwendung möglich, kann die Methode weitere Aktionen ausführen, etwa Zustände setzen, denn wenn der Schlüssel auf der Tür gültig ist, ist die Tür danach offen. Ob eine Operation möglich war oder nicht, soll eine Rückgabe aussagen.

Eine Methode in GameObject könnte somit folgende Signatur besitzen:

boolean useOn( GameObject object )

Ein Beispiel für die Operation useOn(GameObject) ist key.useOn(door). Das eigene Objekt (key in dem Fall) wendet sich auf das an die Methode übergebene Objekt (door in dem Beispiel) an. Implementiert die Schlüssel-Klasse useOn(GameObject), so kann sie testen, ob das übergebene GameObject eine Tür ist, und außerdem kann sie prüfen, ob Schlüssel und Tür zusammenpassen. Wenn ja, kann der Schlüssel die Tür öffnen, und die Rückgabe ist true, sonst false.

Da jede Spielobjekt-Klasse die Operation useOn(GameObject) implementieren muss, soll die Basisklasse sie abstrakt deklarieren, denn eine abstrakte Methode fordert von den Unterklassen eine Implementierung ein, sonst ließen sich keine Exemplare bilden:

Listing 7.46    src/main/java/com/tutego/insel/game/vi/GameObject.java, GameObject

public abstract class GameObject {

public String name;

public abstract boolean useOn( GameObject object );

}

Die Klasse GameObject deklariert eine abstrakte Methode. Da abstrakte Methoden immer ohne Implementierung sind, steht statt des Methodenrumpfs 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 GameObject mit dem Attribut name zeigt. Konkrete Methoden sind auch erlaubt, die brauchen wir jedoch hier nicht. Eine toString()-Methode wäre vielleicht noch interessant, sie könnte dann auf name zurückgreifen.

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 Unterklasse Door soll die abstrakte Methode useOn (GameObject) überschreiben, aber immer false zurückgeben, da sich eine Tür in unserem Szenario auf nichts anwenden lässt. Nach dem Implementieren der abstrakten Methode sind Exemplare von Türen möglich:

Listing 7.47    src/main/java/com/tutego/insel/game/vi/Door.java, Door

public class Door extends GameObject {



int id;

boolean isOpen;



public Door( int id ) { this.id = id; }



@Override public boolean useOn( GameObject object ) {

return false;

}

}

Eine Tür hat für den Schlüssel eine ID, denn nicht jeder Schlüssel passt auf jedes Schloss. Außerdem hat die Tür einen Zustand: Sie kann offen oder geschlossen sein.

Die dritte Klasse, Key, speichert ebenfalls eine ID und überschreibt useOn(GameObject):

Listing 7.48    src/main/java/com/tutego/insel/game/vi/Key.java, Key

public class Key extends GameObject {



int id;



public Key( int id ) { this.id = id; }



@Override public boolean useOn( GameObject object ) {

if ( object instanceof Door )

if ( id == ((Door) object).id )

return ((Door) object).isOpen = true;



return false;

}

}

Die Realisierung ist etwas komplexer. Als Erstes prüft die Methode mit instanceof, ob der Schlüssel auf eine Tür angewendet wird. Wenn ja, muss die ID von Schlüssel und Tür übereinstimmen. Ist auch dieser Vergleich wahr, kann isOpen wahr werden, und die Methode liefert true.

UML-Diagramm der Tür, des Schlüssels und der Oberklasse

Abbildung 7.12    UML-Diagramm der Tür, des Schlüssels und der Oberklasse

Im Testprogramm wollen wir zwei Schlüssel auf eine Tür anwenden. Nur der Schlüssel mit der passenden ID öffnet die Tür. Eine Tür kann nicht auf einen Schlüssel angewendet werden, denn die Implementierungen sind nicht symmetrisch ausgelegt:

Listing 7.49    src/main/java/com/tutego/insel/game/vi/Playground.java, main()

Door door1 = new Door( 1 );

GameObject key1 = new Key( 1 );

GameObject key9 = new Key( 9 );



System.out.printf( "erfolgreich=%b, isOpen=%b%n", key9.useOn(door1), door1.isOpen );

System.out.printf( "erfolgreich=%b, isOpen=%b%n", key1.useOn(door1), door1.isOpen );

System.out.printf( "erfolgreich=%b%n", door1.useOn(key1) );

Die Ausgaben sind:

erfolgreich=false, isOpen=false

erfolgreich=true, isOpen=true

erfolgreich=false
[»]  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 GameObject abstrakt überschrieben werden, um etwa den Unterklassen Room oder Player 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 Eclipse mit (Strg)+(1) 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

Wir wollen das Spiel zu Ende bringen und Folgendes implementieren:

  • Von der Konsole soll eine Zeile gelesen werden, die in der Art Verb–Subjekt–Objekt formuliert ist, wie »stecke Schlüssel in Tür«.

  • Da wir nur »nutze« kennen, wird der Verbanteil im Satz ignoriert, also aus der Eingabe gelöscht.

  • Subjekt und Objekt kommen als Strings in der Eingabe vor. Wir müssen also den Namen eines Objekts auf ein GameObject-Exemplar übertragen. Das machen wir mit einer neuen Datenstruktur HashMap, einem Assoziativspeicher.

Listing 7.50    src/main/java/com/tutego/insel/game/vi/Game.java, main()

// Assoziativspeicher verbindet Namen mit Spielobjekten

HashMap<String, GameObject> gameObjects = new HashMap<>();

gameObjects.put( "höllentor", new Door( 1 ) );

gameObjects.put( "höllenschlüssel", new Key( 1 ) );

gameObjects.put( "himmelsschlüssel", new Key( 9 ) );



// Spezielle Syntax, um eine Unterklasse von GameObject zu schreiben

// und dann die Methode zu überschreiben.

GameObject nullGameObject = new GameObject() {

@Override public boolean useOn( GameObject object ) { return false; }

};



while ( true ) {

System.out.printf( "Was möchtest du tun?%n> " );

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

if ( input.matches( "ende|bye|schluss|quit" ) )

System.exit( 0 );

String simplifiedLine =

input.replaceAll( "benutze|stecke|nutze|mit|bei|auf|unter|in", "" );

StringTokenizer tokenizer = new StringTokenizer( simplifiedLine );

if ( tokenizer.countTokens() < 2 ) {

System.out.println( "Details bitte, '" + input + "' reicht mir nicht!" );

continue;

}

GameObject subject = gameObjects.getOrDefault(

tokenizer.nextToken(), nullGameObject );

GameObject object = gameObjects.getOrDefault(

tokenizer.nextToken(), nullGameObject );

System.out.println( subject.useOn( object ) ? "
Ausgeführt" :

"
Konnte '" + input + "' nicht ausführen" );

}

Zum Zerlegen der Eingabe greifen wir auf StringTokenizer zurück. Ein Trick in dem Programm ist das besondere nullGameObject, die Implementierung des Null-Object-Patterns. Die Idee ist, ein Objekt zu haben, das einfach bei jeder Anfrage nichts macht. Wir nutzen das, um einer NullPointerException bei get(…) aus dem Wege zu gehen, denn ist kein GameObject mit einem Namen assoziiert, liefert die Map-Methode null, und eine Kombination von null.useOn(…) ist tödlich. getOrDefault(…) liefert beim Nichtfinden das nullGameObject, und darauf können wir gültig alles anwenden – nur passiert dann nichts. Alternativ hätten wir abfragen können, ob null das Ergebnis von get(…) ist, und dann eine Meldung der Art »Objekt mit dem Namen ist nicht bekannt« ausgeben können.

[+]  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 vom 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: 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