0% fanden dieses Dokument nützlich (0 Abstimmungen)
13 Ansichten674 Seiten

Java Lernen Mit Blue J

Hochgeladen von

r9tfk5kz52
Copyright
© All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen
0% fanden dieses Dokument nützlich (0 Abstimmungen)
13 Ansichten674 Seiten

Java Lernen Mit Blue J

Hochgeladen von

r9tfk5kz52
Copyright
© All Rights Reserved
Wir nehmen die Rechte an Inhalten ernst. Wenn Sie vermuten, dass dies Ihr Inhalt ist, beanspruchen Sie ihn hier.
Verfügbare Formate
Als PDF, TXT herunterladen oder online auf Scribd lesen

Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.

Java lernen mit BlueJ

Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.


Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
David J. Barnes • Michael Kölling

Objects first - Eine Einführung in Java


6., aktualisierte Auflage

Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.


Bibliografische Information der Deutschen Nationalbibliothek
Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie;
detaillierte bibliografische Daten sind im Internet über [Link] abrufbar.

Die Informationen in diesem Buch werden ohne Rücksicht auf einen eventuellen Patentschutz veröf-
fentlicht. Warennamen werden ohne Gewährleistung der freien Verwendbarkeit benutzt. Bei der Zu-
sammenstellung von Texten und Abbildungen wurde mit größter Sorgfalt vorgegangen. Trotzdem
können Fehler nicht ausgeschlossen werden. Verlag, Herausgeber und Autoren können für fehlerhafte
Angaben und deren Folgen weder eine juristische Verantwortung noch irgendeine Haftung überneh-
men. Für Verbesserungsvorschläge und Hinweise auf Fehler sind Verlag und Autor dankbar.

Authorized translation from the English language edition, entitled OBJECTS FIRST WITH JAVA –
A PRACTICAL INTRODUCTION USING BLUEJ, 6th Edition by DAVID BARNES and
MICHAEL KÖLLING, published by Pearson Education, Inc, publishing as Prentice Hall,
Copyright © 2017 Pearson Education Inc.

All rights reserved. No part of this book may be reproduced or transmitted in any form or by any means,
electronic or mechanical, including photocopying, recording or by any information storage retrieval
system, without permission from Pearson Education, Inc.

GERMAN language edition published by PEARSON DEUTSCHLAND GMBH, Copyright © 2017.

Fast alle Produktbezeichnungen und weitere Stichworte und sonstige Angaben, die in diesem Buch
verwendet werden, sind als eingetragene Marken geschützt. Da es nicht möglich ist, in allen Fällen
zeitnah zu ermitteln, ob ein Markenschutz besteht, wird das ®-Symbol in diesem Buch nicht verwendet.

10 9 8 7 6 5 4 3 2 1

20 19 18 17

ISBN 978-3-86894-911-7 (Buch)


ISBN 978-3-86326-961-6 (E-Book)

© 2017 by Pearson Deutschland GmbH


Lilienthalstraße 2, 85399 Hallbergmoos/Germany
Alle Rechte vorbehalten
[Link]
A part of Pearson plc worldwide

Programmleitung: Birger Peil, bpeil@[Link]


Übersetzung und Korrektorat: Katharina Pieper, Berlin, [Link]@[Link]
Coverillustration: Blue jay image. Reprinted with permission copyright Suman Roy Choudhury@sqr-
photo@[Link]
Herstellung: Claudia Bäurle, cbaeurle@[Link]
Satz: inpunkt[w]o, Haiger ([Link])
Druck- und Verarbeitung: DZS-Grafik, d.o.o., Ljubljana
Printed in Slovenia

Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.


Inhaltsverzeichnis

Vorwort von James Gosling, Erfinder von Java 13


Vorwort für den Lehrenden 14
Danksagungen 25
Projekte, die in diesem Buch detailliert besprochen werden 26

Teil I Objekte und Klassen 29

Kapitel 1 Objekte und Klassen 31


1.1 Objekte und Klassen 31
1.2 Instanzen erzeugen 32
1.3 Methoden aufrufen 33
1.4 Parameter 35
1.5 Datentypen 36
1.6 Eine Klasse, viele Instanzen 37
1.7 Zustand 38
1.8 Das Innenleben eines Objekts 38
1.9 Java-Code 40
1.10 Objektinteraktion 41
1.11 Quelltext 42
1.12 Ein weiteres Beispiel 44
1.13 Aufrufergebnisse 44
1.14 Objekte als Parameter 45

Kapitel 2 Klassendefinitionen 51
2.1 Ticketautomaten 51
2.2 Eine Klassendefinition untersuchen 53
2.3 Der Kopf der Klasse 55
2.4 Datenfelder, Konstruktoren und Methoden 56
2.5 Datenübergabe mit Parametern 62
2.6 Zuweisungen 64
2.7 Methoden 65
2.8 Sondierende und verändernde Methoden 67

5
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis

2.9 Ausgaben in Methoden 70


2.10 Zusammenfassung der Methoden 72
2.11 Zusammenfassung des naiven Ticketautomaten 72
2.12 Bewertung des Entwurfs des naiven Ticketautomaten 73
2.13 Entscheidungen treffen: die bedingte Anweisung 76
2.14 Ein weiteres Beispiel für eine bedingte Anweisung 78
2.15 Hervorhebung von Sichtbarkeitsbereichen 79
2.16 Lokale Variablen 80
2.17 Datenfelder, Parameter und lokale Variablen 82
2.18 Zusammenfassung des besseren Ticketautomaten 84
2.19 Übungen zur Selbstüberprüfung 84
2.20 Vertrautes neu betrachtet 86
2.21 Methoden aufrufen 88
2.22 Ausdrücke testen: die Direkteingabe 89

Kapitel 3 Objektinteraktion 97
3.1 Das Uhren-Beispiel 97
3.2 Abstraktion und Modularisierung 98
3.3 Abstraktion in Software 99
3.4 Modularisierung im Uhren-Beispiel 100
3.5 Implementierung der Uhrenanzeige 100
3.6 Klassendiagramme und Objektdiagramme 101
3.7 Primitive Typen und Objekttypen 103
3.8 Die Klasse Nummernanzeige 103
3.9 Die Klasse Uhrenanzeige 110
3.10 Objekte erzeugen Objekte 112
3.11 Mehrere Konstruktoren 114
3.12 Methodenaufrufe 114
3.13 Ein weiteres Beispiel für Objektinteraktion 118
3.14 Die Benutzung eines Debuggers 122
3.15 Mehr zu Methodenaufrufen 126

Kapitel 4 Objektsammlungen 131


4.1 Themen aus Kapitel 3 vertiefen 131
4.2 Die Abstraktion Sammlung 132
4.3 Ein Verwaltungssystem für Musikdateien 133
4.4 Eine Bibliotheksklasse verwenden 135
4.5 Objektstrukturen mit Sammlungen 138
4.6 Generische Klassen 139
4.7 Nummerierung in Sammlungen 140
4.8 Musikdateien abspielen 144

6
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis

4.9 Komplette Sammlungen verarbeiten 146


4.10 Unbestimmte Iteration 152
4.11 Verbesserung der Struktur – die Klasse Track 159
4.12 Der Typ Iterator 163
4.13 Zusammenfassung des Musiksammlung-Beispiels 167
4.14 Ein weiteres Beispiel: ein Auktionssystem 169

Kapitel 5 Funktionale Verarbeitung von Sammlungen


(fortgeschrittene Konzepte) 183
5.1 Ein neuer Blick auf die Themen von Kapitel 4 183
5.2 Monitoring von Tierpopulationen 185
5.3 Lambda-Ausdrücke – ein erster Blick 188
5.4 Die forEach-Methode von Sammlungen 190
5.5 Streams 193

Kapitel 6 Bibliotheksklassen nutzen 207


6.1 Die Dokumentation der Bibliotheksklassen 208
6.2 Das Kundendienstsystem 209
6.3 Die Klassendokumentation lesen 215
6.4 Zufälliges Verhalten einbringen 221
6.5 Pakete und Importe 226
6.6 Benutzung von Map-Klassen für Abbildungen 227
6.7 Der Umgang mit Mengen 232
6.8 Zeichenketten zerlegen 233
6.9 Abschluss des Kundendienstsystems 234
6.10 Autoboxing und Wrapper-Klassen 236
6.11 Die Klassendokumentation schreiben 239
6.12 Öffentliche und private Eigenschaften 242
6.13 Klassen über ihre Schnittstelle verstehen 245
6.14 Klassenvariablen und Konstanten 250
6.15 Klassenmethoden 253
6.16 Programmausführung ohne BlueJ 254
6.17 Weitere fortgeschrittene Konzepte 255

Kapitel 7 Sammlungen mit fester Größe – Arrays 263


7.1 Sammlungen fester Größe 263
7.2 Arrays 263
7.3 Die Analyse einer Logdatei 264
7.4 Die for-Schleife 270
7.5 Das Projekt Automat 276
7.6 Arrays mit mehr als einer Dimension (fortgeschritten) 285
7.7 Arrays und Streams (fortgeschritten) 291

7
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis

Kapitel 8 Klassenentwurf 293


8.1 Einführung 294
8.2 Die Welt von Zuul 296
8.3 Kopplung und Kohäsion 297
8.4 Code-Duplizierung 298
8.5 Erweiterungen für Zuul 301
8.6 Kopplung 303
8.7 Entwurf nach Zuständigkeiten 308
8.8 Änderungen lokal halten 311
8.9 Implizite Kopplung 311
8.10 Vorausdenken 315
8.11 Kohäsion 316
8.12 Refactoring 320
8.13 Refactoring für Sprachunabhängigkeit 324
8.14 Entwurfsregeln 330

Kapitel 9 Fehler vermeiden 335


9.1 Einführung 335
9.2 Testen und Fehlerbeseitigung 336
9.3 Modultests in BlueJ 337
9.4 Tests automatisieren 343
9.5 Refactoring bei Streams einsetzen (fortgeschritten) 351
9.6 Debugging 351
9.7 Kommentierung und Programmierstil 353
9.8 Manuelle Ausführung 354
9.9 Ausgabeanweisungen 360
9.10 Debugger 363
9.11 Das Debugging von Streams (fortgeschritten) 365
9.12 Die Wahl der richtigen Teststrategie 366
9.13 Techniken umsetzen 367

Teil II Anwendungsstrukturen 369

Kapitel 10 Bessere Struktur durch Vererbung 371


10.1 Das Beispiel „Netzwerk“ 372
10.2 Einsatz von Vererbung 382
10.3 Vererbungshierarchien 384
10.4 Vererbung in Java 385
10.5 Weitere Einsendungen für Netzwerk 389
10.6 Vorteile durch Vererbung (bis hierher) 391

8
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis

10.7 Subtyping 391


10.8 Die Klasse Object 397
10.9 Die Hierarchie der Sammlungstypen 398

Kapitel 11 Mehr über Vererbung 403


11.1 Das Problem: die Methode zum Anzeigen 403
11.2 Statischer und dynamischer Typ 405
11.3 Überschreiben von Methoden 408
11.4 Dynamische Methodensuche 410
11.5 super-Aufrufe in Methoden 413
11.6 Methoden-Polymorphie 414
11.7 Methoden aus Object: toString 414
11.8 Objektgleichheit: equals und hashCode 417
11.9 Der Zugriff über protected 420
11.10 Der Operator instanceof 422
11.11 Ein weiteres Beispiel für Vererbung mit Überschreiben 423

Kapitel 12 Weitere Techniken zur Abstraktion 429


12.1 Simulationen 429
12.2 Die Fuechse-und-Hasen-Simulation 431
12.3 Abstrakte Klassen 444
12.4 Weitere abstrakte Methoden 451
12.5 Multiple Vererbung 453
12.6 Interfaces 457
12.7 Ein weiteres Beispiel für ein Interface 465
12.8 Die Klasse Class 467
12.9 Abstrakte Klasse oder Interface? 468
12.10 Ereignisgesteuerte Simulationen 468
12.11 Zusammenfassung der Vererbung 470

Kapitel 13 Grafische Benutzungsoberflächen 475


13.1 Einführung 475
13.2 Komponenten, Layout und Ereignisbehandlung 476
13.3 AWT und Swing 477
13.4 Das Beispiel: ein Bildbetrachter 478
13.5 Bildbetrachter 1.0: die erste komplette Version 490
13.6 Bildbetrachter 2.0: die Programmstruktur verbessern 504
13.7 Bildbetrachter 3.0: weitere GUI-Komponenten 509
13.8 Innere Klassen 513
13.9 Zusätzliche Erweiterungen 518
13.10 Ein weiteres Beispiel: der Musikplayer 520

9
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis

Kapitel 14 Fehlerbehandlung 527


14.1 Das Adressbuch-Projekt 528
14.2 Defensive Programmierung 531
14.3 Fehlermeldungen durch den Dienstleister 534
14.4 Prinzipien der Exception-Behandlung 539
14.5 Die Behandlung von Exceptions 545
14.6 Neue Exception-Klassen definieren 552
14.7 Die Verwendung von Zusicherungen 553
14.8 Wiederaufsetzen und Fehlervermeidung 557
14.9 Dateibasierte Ein- und Ausgabe 560

Kapitel 15 Entwurf von Anwendungen 573


15.1 Analyse und Entwurf 573
15.2 Klassenentwurf 581
15.3 Dokumentation 583
15.4 Kooperation 584
15.5 Prototyping 585
15.6 Softwarewachstum 586
15.7 Der Einsatz von Entwurfsmustern 588

Kapitel 16 Eine Fallstudie 597


16.1 Die Fallstudie 597
16.2 Analyse und Entwurf 599
16.3 Klassenentwurf 602
16.4 Iterative Entwicklung 608
16.5 Ein weiteres Beispiel 616
16.6 Ein Blick nach vorn 616

Anhang A Arbeiten mit BlueJ-Projekten 617


A.1 BlueJ installieren 617
A.2 Ein Projekt öffnen 617
A.3 Der Debugger in BlueJ 617
A.4 BlueJ konfigurieren 618
A.5 Auf deutsche Schnittstelle umstellen 618
A.6 Einbinden einer lokalen API-Dokumentation 619
A.7 Vorlagen für neue Klassen ändern 619

Anhang B Datentypen in Java 621


B.1 Primitive Typen 621
B.2 Cast-Operator für primitive Typen 622
B.3 Objekttypen 623

10
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis

B.4 Wrapper-Klassen 623


B.5 Cast-Operator für Objekttypen 624

Anhang C Operatoren 625


C.1 Arithmetische Ausdrücke 625
C.2 Boolesche Ausdrücke 626
C.3 Abkürzungsoperatoren 627

Anhang D Kontrollstrukturen in Java 629


D.1 Kontrollstrukturen 629
D.2 Auswahlanweisungen 629
D.3 Schleifen 632
D.4 Exceptions 634
D.5 Zusicherungen 635

Anhang E Java ohne BlueJ 637


E.1 Java ohne BlueJ ausführen 637
E.2 Konsolenanwendungen und die Problematik der Umlaute 639
E.3 Ausführbare jar-Dateien erzeugen 641
E.4 Entwickeln ohne BlueJ 642

Anhang F Benutzung des Debuggers 643


F.1 Haltepunkte 644
F.2 Die Kontrollknöpfe 644
F.3 Anzeige der Variablen 645
F.4 Die Anzeige der Aufruffolge 646
F.5 Die Thread-Anzeige 646

Anhang G Testwerkzeuge für Modultests mit JUnit 647


G.1 Aktivieren der Test-Funktionalität 647
G.2 Eine Testklasse erzeugen 647
G.3 Eine Testmethode erzeugen 647
G.4 Zusicherungen bei Tests 648
G.5 Tests ausführen 648
G.6 Testgerüste 648

Anhang H Werkzeuge für die Teamarbeit 649


H.1 Server-Einrichtung 649
H.2 Teamarbeit-Funktionalität aktivieren 649
H.3 Ein Projekt zur gemeinsamen Nutzung einrichten 649
H.4 An einem Projekt gemeinsam arbeiten 650
H.5 Aktualisieren und Abgeben 650
H.6 Weitere Informationen 650

11
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis

Anhang I Javadoc 651


I.1 Dokumentationskommentare 651
I.2 Unterstützung für Javadoc in BlueJ 654

Anhang J Quelltextkonventionen 655


J.1 Benennung 655
J.2 Layout 656
J.3 Dokumentation 657
J.4 Restriktionen bei der Sprachbenutzung 658
J.5 Programmiermuster 659

Anhang K Wichtige Bibliotheksklassen 661


K.1 Das Paket [Link] 661
K.2 Das Paket [Link] 662
K.3 Die Pakete [Link] und [Link] 664
K.4 Das Paket [Link] 665
K.5 Das Paket [Link] 666
K.6 Weitere wichtige Pakete 666

Register 667

12
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort von James Gosling, Erfinder von Java

Widmung
Für meine Frau Helen
–djb

Für K.W.
–mk

Vorwort von James Gosling,


Erfinder von Java
Es war eine schmerzhafte Erfahrung für mich, meine Tochter Kate und die ande-
ren Kinder in ihrer Mittelstufenklasse mit einem Java-Kurs kämpfen zu sehen, in
dem eine kommerzielle Entwicklungsumgebung benutzt wurde. Die Leistungs-
fähigkeit des Werkzeugs hat die Komplexität des Lernprozesses erheblich erhöht.
Ich wünschte, ich hätte früher begriffen, was da passierte. Leider konnte ich mit
dem Lehrer erst über das Problem sprechen, als es schon zu spät war. Dies ist
eine Situation, für die BlueJ ideal geeignet ist.
BlueJ ist eine interaktive Entwicklungsumgebung mit einem Auftrag: Sie wurde spe-
ziell für Studenten entwickelt, die Programmieren lernen. Sie wurde entworfen von
Dozenten, die seit vielen Jahren Tag für Tag Objektorientierung lehren. Es war erfri-
schend, mit den Entwicklern von BlueJ zu sprechen: Sie haben eine sehr klare Vor-
stellung von ihrem Ziel. Unsere Diskussionen handelten meist eher davon, welche
Dinge weggelassen werden sollten, und weniger davon, welche noch hinzugenom-
men werden sollten. BlueJ ist sehr klar strukturiert und ausgesprochen zielgerichtet.
Aber dies ist kein Buch über BlueJ. Es geht ums Programmieren.
In Java.
In den letzten Jahren hat Java in der Programmierausbildung eine immer größere
Bedeutung bekommen. Dafür gibt es mehrere Gründe. Einer ist, dass Java viele
Eigenschaften besitzt, die die Sprache für die Lehre geeignet machen: eine relativ
klare Definition; ausführliche Prüfungen zur Übersetzungszeit durch den Compiler,
die den Studenten frühes Feedback bei Problemen geben; und ein sehr robustes
Speichermodell, das viele „mysteriöse“ Fehler von vornherein ausschaltet, die durch
das Überschreiten von Objektgrenzen oder Typregeln auftreten können. Ein weite-
rer Grund ist, dass Java inzwischen kommerziell sehr interessant geworden ist.
Dieses Buch geht direkt das am schwierigsten zu vermittelnde Konzept an: Objekte.
Das Buch führt die Studenten von den allerersten Schritten hin zu sehr anspruchs-
vollen Konzepten.
Und es löst eines der schwierigsten Probleme für Lehrbücher über Programmierung:
Es thematisiert explizit auch das Schreiben und Ausführen von Programmen. Die
meisten Lehrbücher gehen über diese Punkte stillschweigend hinweg oder streifen

13
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

sie nur kurz und überlassen es dem Dozenten, diese Aspekte zu klären. Dieser wird
mit dem Problem alleingelassen, das zu vermittelnde Material in Beziehung zu setzen
zu den Schritten, die zur Lösung der Übungen gemacht werden müssen. Stattdessen
setzt dieses Buch den Einsatz von BlueJ voraus und kann deshalb das Verstehen der
Konzepte auf die Mechanismen, die ihre Erforschung ermöglichen, abstimmen.
Ich wünschte, für meine Tochter hätte BlueJ schon im letzten Jahr zur Verfügung
gestanden. Vielleicht im nächsten Jahr …

Vorwort für den Lehrenden


Neues in der sechsten Auflage
Dies ist die sechste Auflage dieses Buchs und der Inhalt wurde – wie immer bei
Neuauflagen – an die jüngsten Entwicklungen bei objektorientierten Program-
men angepasst.
Viele der Änderungen können dieses Mal oberflächlich einer neuen Java-Version
zugeschrieben werden: Java 8. Diese Version wurde 2014 herausgegeben und
wird jetzt sehr häufig in der Praxis eingesetzt. In der Tat wurde bisher keine Java-
Version so schnell angenommen – daher ist es jetzt an der Zeit, auch die Art zu
verändern, wie wir Einsteiger darin unterrichten.
Die Veränderungen umfassen jedoch mehr als nur das Hinzufügen von einigen
neuen Sprachkonstrukten. Die wichtigsten neuen Aspekte von Java 8 drehen sich
um neue Konstrukte, die einen (teilweise) funktionalen Programmierstil unter-
stützen. Die wachsende Beliebtheit der funktionalen Programmierung hat diese
Änderung vorangetrieben. Der Unterschied ist viel tiefer und fundamentaler und
geht über das bloße Hinzufügen von neuer Syntax hinaus. Und es ist das Wie-
dererwachen von funktionalen Ideen in der Programmierung allgemein – nicht
nur die Existenz von Java 8 –, was es nahelegt, diese Aspekte jetzt in einer
modernen Auflage eines Programmierlehrbuchs zu behandeln.
Die Ideen und Techniken der funktionalen Programmierung – obwohl recht alt
und im Prinzip gut bekannt – haben in den letzten Jahren einen deutlichen Popu-
laritätsschub erlebt. Es wurden neue Sprachen entwickelt und ausgewählte funk-
tionale Techniken in die bestehenden traditionellen imperativen eingebettet.
Einer der Hauptgründe dafür sind die Neuerungen bei der verfügbaren Compu-
terhardware sowie ein Wandel der Problemstellungen, die wir angehen wollen.
Fast alle Programmierplattformen bieten heutzutage parallele Verarbeitung. Selbst
Laptops der Mittelklasse und Mobiltelfefone haben Prozessoren mit mehreren Ker-
nen, wodurch Parallelität eine echte Möglichkeit auf alltäglich genutzten Geräten
ist. Doch in der Praxis setzt sich dieser Trend noch nicht im größeren Rahmen fort.
Anwendungen zu entwickeln, die eine optimale Ausnutzung von paralleler Verar-
beitung und von mehreren Prozessoren bieten, ist sehr, sehr schwierig. Die wenigs-
ten der heute verfügbaren Anwendungen nutzen die aktuelle Hardware in dem
Ausmaß, wie es theoretisch möglich wäre.

14
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

Dies wird sich auch nicht viel ändern: Die Möglichkeit (und Herausforderung) von
paralleler Hardware wird bestehen bleiben, und die Programmierung dieser Geräte
mit traditionellen imperativen Sprachen wird nicht einfacher werden.
An dieser Stelle kommt die funktionale Programmierung ins Spiel.
Mit funktionalen Sprachkonstrukten ist es möglich, ein wenig Parallelität sehr effizi-
ent zu automatisieren. Programme können potenziell mehrere Kerne ohne viel Auf-
wand seitens des Programmieres verwenden. Funktionale Konstrukte haben andere
Vorteile – eleganterer Ausdruck für bestimmte Probleme und häufig eine bessere
Lesbarkeit –, aber es ist die Fähigkeit, mit Parallelität umzugehen, die sicherstellen
wird, dass funktionale Programmieraspekte uns noch lange begleiten werden.
Jeder Lehrer, der seine Schüler auf die Zukunft vorbereiten möchte, sollte ihnen auch
ein gewisses Verständnis für funktionale Aspekte vermitteln. Ohne diese Kenntnisse
wird man es nicht mehr zur Meisterschaft im Programmieren bringen können. Ein
Neuling muss sicher nicht die gesamte funktionale Programmierung beherrschen,
aber ein Grundverständnis davon – und was wir damit erreichen können – wird bald
wesentlich sein.
Wann genau funktionale Techniken eingeführt werden sollten, ist eine interes-
sante Frage. Wir glauben nicht, dass es eine einzige richtige Antwort darauf gibt;
verschiedene Abfolgen sind denkbar. Funktionale Programmierung könnte als
fortgeschrittenes Thema am Ende des gesamten Buchs behandelt werden oder
man könnte es ansprechen, wenn wir zum ersten Mal auf die Bereiche treffen, für
die es anwendbar ist, als Alternative zu den imperativen Techniken. Man könnte
das Thema sogar zuerst behandeln.
Eine weitere Frage ist, wie mit dem traditionellen Programmierstil in den Berei-
chen umgegangen werden soll, in denen die funktionalen Konstrukte nun ver-
fügbar sind: Sollten sie ersetzt werden oder behandelt man beide?
Wir sind für dieses Buch davon ausgegangen, dass jeder Lehrer seine eigenen
Bedürfnisse und Vorlieben hat. Deshalb haben wir eine Struktur entwickelt, die –
so hoffen wir – unterschiedliche Ansätze ermöglicht, je nach den Vorzügen des
Lernenden oder Lehrers.
 Wir haben die „alten“ Techniken nicht ersetzt. Wir behandeln den neuen funk-
tionalen Ansatz zusätzlich zu den bestehenden Inhalten. Funktionale Konstrukte
in Java sind am herausragendsten, wenn man es mit Sammlungen von Objekten
zu tun hat, und die traditionellen Konzepte – die Verwendung von Schleifen
und expliziter Iteration – zu beherrschen, ist für jeden Programmierer immer
noch wesentlich. Nicht nur, weil es Millionen Codezeilen in der Welt gibt, die in
diesem Stil geschrieben sind – und weiterhin geschrieben werden –, sondern es
gibt auch spezielle Fälle, in denen diese Techniken genutzt werden müssen, selbst
wenn man generell die neuen funktionalen Konstrukte vorzieht. Das Ziel ist es,
beide Techniken zu beherrschen.
 Wir präsentieren das neue funktional-orientierte Material im Buch an den Stel-
len, an denen wir die Probleme besprechen, die mit diesen Konstrukten ange-
gangen werden. Zum Beispiel behandeln wir die funktionale Verarbeitung von
Sammlungen, sobald wir den Sammlungen begegnen.

15
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

 Kapitel und Abschnitte, die dieses neue Material behandeln, sind jedoch deut-
lich als „fortgeschritten“ markiert und so strukturiert, dass sie gefahrlos beim
ersten Lesen übersprungen (oder überhaupt ausgelassen) werden können.
 Die beiden vorigen Punkte ermöglichen unterschiedliche Herangehensweisen,
dieses Buch durchzuarbeiten: Wenn es die Zeit erlaubt, kann das Buch in der
vorgestellten Reifenfolge gelesen werden, damit wird der gesamte Inhalt abge-
deckt, einschließlich der funktionalen Ansätze als Alternative zu den imperati-
ven – wenn die Probleme auftauchen, die wir mit funktionalen Konzepten lösen
wollen. Ist die Zeit jedoch knapp, können diese weiterführenden Abschnitte
übersprungen werden, die Betonung wird dann erst einmal auf ein gründliches
Durchdringen der imperativen, objektorientierten Programmierung gesetzt. (Wir
sollten betonen, dass funktional nicht der Gegensatz von objektorientiert ist:
Egal, ob das funktionale Material in das Studium einbezogen wird oder ob das
Augenmerk im Großen und Ganzen auf den imperativen Techniken liegt – jeder
Leser dieses Buchs wird am Ende mit einem guten Verständnis der Objektorien-
tierung ausgestattet sein!) Eine weitere Möglichkeit des Zugangs ist es, die wei-
terführenden Abschnitte erst einmal zu überspringen und diese später als eine
separate Einheit zu behandeln. Sie enthalten alternative Herangehensweisen zu
anderen Konstrukten und können unabhängig bearbeitet werden.
Wir hoffen, dies macht deutlich, dass dieses Buch Flexibilität bietet, falls es ge-
wünscht ist, aber auch Orientierungshilfe für Leser, die noch keine klare Vorliebe
haben: Dann kann man das Buch einfach von vorne nach hinten lesen.
Abgesehen von den bisher genannten größeren Änderungen, präsentiert diese
Auflage auch zahlreiche kleinere Verbesserungen. Die Gesamtstruktur, der Ton und
die Herangehensweise sind unverändert: Sie haben sich in der Vergangenheit
bewährt und es gibt keinen Grund, davon abzurücken. Doch wir überarbeiten das
Material ständig und suchen nach Verbesserungsmöglichkeiten. Wir haben jetzt
fast 15 Jahre kontinuierliche Lehrerfahrung mit diesem Buch und dies schlägt sich
überall in den vielen kleineren Verbesserungen nieder.
Dieses Buch ist eine Einführung in die objektorientierte Programmierung für Pro-
grammieranfänger. Der Fokus dieses Buches liegt auf allgemeinen objektorien-
tierten Programmierkonzepten aus der Sicht der Softwaretechnik.
Während die ersten Kapitel auf Studenten ohne jegliche Programmiererfahrung
abzielen, sind spätere Kapitel auch für fortgeschrittene oder professionelle Program-
mierer interessant. Insbesondere sollten auch Programmierer von diesem Buch profi-
tieren können, die Erfahrung in einer nicht objektorientierten Sprache haben und
ihre Fähigkeiten in Richtung Objektorientierung ausbauen möchten.
Wir verwenden das gesamte Buch hindurch zwei Werkzeuge, um die vorgestellten
Konzepte in die Praxis umzusetzen: die Programmiersprache Java und die Java-Ent-
wicklungsumgebung BlueJ.

Java
Die Sprache Java wurde wegen ihres Sprachentwurfs und ihrer Popularität gewählt.
Die Programmiersprache Java selbst bietet eine saubere Umsetzung der meisten
wichtigen objektorientierten Konzepte und eignet sich gut als einführende Sprache.
Ihre Popularität garantiert einen enormen Vorrat an unterstützendem Material.

16
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

Auf jedem Lehrgebiet ist eine große Menge unterschiedlicher Quellen sowohl für
Lehrende als auch für Studenten hilfreich. Für Java gibt es zahllose Bücher, Beschrei-
bungen, Übungen, Compiler, Entwicklungsumgebungen und Fragenkataloge in
den unterschiedlichsten Arten und Stilen. Viele sind online verfügbar und viele
sind kostenlos. Die große Menge und die gute Qualität an Zusatzmaterial machen
Java zu einer exzellenten Wahl als Einführung in die objektorientierte Program-
mierung.
Wenn so viel Java-Material bereits verfügbar ist, gibt es dann überhaupt noch
etwas Neues dazu zu sagen? Wir denken ja, und das zweite Werkzeug ist einer
der Gründe dafür …

BlueJ
BlueJ erfordert einige Erläuterungen. Dieses Buch ist einzigartig in seiner kom-
pletten Integration der BlueJ-Umgebung.
BlueJ ist eine Java-Entwicklungsumgebung, die an der Deakin-Universität, Aust-
ralien, und der Universität von Kent in Canterbury, England, ausdrücklich mit
dem Ziel entwickelt wird, für die Einführung in die objektorientierte Programmie-
rung zu dienen. Sie eignet sich aus mehreren Gründen für diese Lehrsituation
besser als andere Umgebungen:
 Die Benutzungsschnittstelle ist sehr viel einfacher. Programmieranfänger kön-
nen die BlueJ-Umgebung typischerweise bereits nach 20 Minuten Einführung
kompetent benutzen. Von diesem Zeitpunkt an kann sich die Darstellung auf
die wichtigen Konzepte konzentrieren – Objektorientierung und Java –, statt
wertvolle Zeit mit der Diskussion über Umgebungen, Dateisysteme, Klassen-
pfade, DOS-Kommandos oder DLL-Konflikte zu vergeuden.
 BlueJ bietet wichtige Lehrwerkzeuge, die in anderen Umgebungen nicht zur Ver-
fügung stehen. Eines davon ist die Visualisierung der Klassenstruktur. BlueJ zeigt
automatisch ein UML-ähnliches Diagramm der Klassen und Beziehungen in
einem Projekt. Die Visualisierung dieser wichtigen Konzepte ist eine große Hilfe
für Lehrende und Studierende. Es ist nicht leicht, das Konzept von Objekten zu
verstehen, wenn auf dem Bildschirm ausschließlich Programmtext zu sehen ist!
Die Notation der Diagramme ist eine einfache Untermenge der UML, zugeschnit-
ten auf den Bedarf von Programmieranfängern. Dies macht es leicht verständ-
lich, erlaubt aber gleichzeitig die Migration auf die volle UML in späteren Kursen.
 Eine der wichtigsten Stärken der BlueJ-Umgebung ist die Möglichkeit für den
Benutzer, direkt Objekte von beliebigen Klassen erzeugen und die Methoden die-
ser Objekte aufrufen zu können. Dies gibt die Möglichkeit zum unmittelbaren
Umgang mit Objekten ohne großen Aufwand durch die Umgebung. Studenten
bekommen förmlich ein „Gefühl“ dafür, wie Objekte erzeugt, Methoden aufge-
rufen, Parameter übergeben und Ergebnisse zurückgeliefert werden. Sie können
eine Methode ausprobieren, unmittelbar nachdem sie sie geschrieben haben,
ohne Testklassen schreiben zu müssen. Diese Möglichkeit ist eine unschätzbare
Hilfe beim Verstehen der zugrunde liegenden Konzepte und Sprachdetails.
 BlueJ weist zahlreiche weitere Tools und Charakteristika auf, die speziell für Stu-
denten der Softwareentwicklung entworfen wurden. Einige sind dafür gedacht,
grundlegende Konzepte besser zu verstehen (wie z.B. die Hervorhebung von
Sichtbarkeitsbereichen), andere dienen dazu, die Studenten, sobald sie bereit

17
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

dafür sind, an fortgeschrittene Werkzeuge und Techniken heranzuführen, wie z.B.


das Testen mit JUnit oder die Teamarbeit mit einem Versionsüberwachungsssys-
tems wie Subversion. Einige dieser Features gibt es nur in der BlueJ-Umgebung.
BlueJ ist eine vollständige Java-Entwicklungsumgebung. Es ist keine zurechtge-
stutzte, vereinfachte Version von Java für die Lehre. Sie läuft auf dem Java Develop-
ment Kit von Oracle und benutzt den Standardcompiler und die zugehörige virtuelle
Maschine. Dies garantiert, dass sie immer mit der offiziellen und aktuellen Java-Spe-
zifikation übereinstimmt.
Die Autoren dieses Buches haben mehrere Jahre Lehrerfahrung mit der BlueJ-Umge-
bung (und viele Jahre mehr aus der Zeit vor BlueJ). Wir haben beide festgestellt, dass
die Verwendung von BlueJ bei den Studenten in unseren Kursen zu größerem
Engagement, besserem Verständnis und gesteigerter Aktivität geführt hat. Einer der
Autoren dieses Buches ist außerdem der Leiter der Entwicklung des BlueJ-Systems.

Objekte wirklich zuerst


Unsere Wahl ist unter anderem deshalb auf BlueJ gefallen, weil mit dieser Umge-
bung wirklich die wichtigen Konzepte zuerst vorgestellt werden können. „Objects
First“ war lange Zeit der Schlachtruf vieler Buchautoren und Lehrer. Leider macht
die Sprache Java die Umsetzung dieses hehren Ansatzes nicht leicht. Zahlreiche
Hürden in Syntax und anderen Details müssen überwunden werden, bevor die
ersten „lebenden“ Objekte sichtbar werden. Der minimale Ablauf zum Erzeugen
und Aufrufen eines Objekts in Java schließt typischerweise folgende Schritte ein:
 das Schreiben einer Klasse
 das Schreiben einer main-Methode unter Benutzung der Konzepte von stati-
schen Methoden, Parametern und Arrays in der Signatur
 eine Anweisung zur Objekterzeugung (mit new)
 eine Zuweisung an eine Variable
 eine Variablendeklaration, inklusive Typ
 einen Methodenaufruf mit der Punkt-Notation
 möglicherweise eine Parameterliste
Aus diesem Grund müssen die meisten Lehrbücher entweder
 diese Liste zuerst durcharbeiten und können deshalb erst einige Kapitel später
auf Objekte zu sprechen kommen oder
 ein Programm im Stil von „Hello, World“ mit einer einzelnen statischen Methode
als erstes Beispiel wählen und damit gar keine Objekte erzeugen.
Mit BlueJ ist das kein Problem. Ein Student kann als erste Aktivität ein Objekt
erzeugen und seine Methoden aufrufen! Weil Benutzer Objekte direkt erzeugen
und manipulieren können, können Konzepte wie Klassen, Objekte, Methoden und
Parameter ohne Weiteres direkt diskutiert werden, bevor die erste Zeile Java-Quell-
text betrachtet werden muss. Statt an dieser Stelle weitere Details zu beschreiben,
empfehlen wir dem neugierigen Leser einen schnellen Blick in Kapitel 1 – dort wer-
den die hier gemachten Aussagen schnell deutlich.

18
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

Iteratives Vorgehen
Ein weiterer wichtiger Aspekt an diesem Buch ist das iterative Vorgehen. Ein in
Kreisen der Informatikpädagogik bekanntes Lehrmuster (aus dem „pedagogical
patterns project“) besagt, dass wichtige Konzepte früh und häufig geschult wer-
den sollten.1 Es ist eine große Versuchung für den Autor eines Lehrbuches, bei
der Vorstellung eines Konzepts alle seine Aspekte darzustellen. Beispielsweise ist
es üblich, bei der Einführung des Typkonzepts eine vollständige Liste der vordefi-
nierten Datentypen anzugeben oder bei der Vorstellung von Schleifen alle Schlei-
fenarten zu diskutieren.
Diese beiden Ansätze stehen im Konflikt miteinander: Wir können uns nicht
einerseits auf die wichtigen Konzepte konzentrieren und gleichzeitig Vollständig-
keit fordern. Unsere Erfahrung mit Lehrbüchern ist, dass durch zu viele Details zu
Anfang die wichtigen Konzepte untergehen und damit schwerer erfassbar wer-
den.
In diesem Buch werden wir wichtige Punkte immer wieder ansprechen, sowohl
innerhalb eines Kapitels als auch über die Kapitel hinweg. Konzepte werden übli-
cherweise so eingeführt, dass sie das vorliegende Problem lösen. Sie werden dann
später aus einem anderen Blickwinkel erneut betrachtet, sodass das Verständnis in
den späteren Kapiteln immer weiter vertieft wird. Dieser Ansatz erleichtert auch ein
Verständnis der Konzepte, die gegenseitig voneinander abhängen.
Einige Dozenten sind möglicherweise mit dem iterativen Vorgehen nicht vertraut.
Beim Blick auf die ersten Kapitel könnten Dozenten, die mit einer eher sequenziellen
Vorgehensweise vertraut sind, über die Fülle der angesprochenen Konzepte über-
rascht sein. Dies scheint eine sehr steile Lernkurve zu implizieren.
Aber die Geschichte endet nicht an dieser Stelle. Von den Studenten wird nicht
erwartet, dass sie alle Details dieser Konzepte unmittelbar verstehen. Stattdessen
werden die fundamentalen Konzepte im Laufe des Buches immer wieder ange-
sprochen, sodass die Studenten ein immer besseres Verständnis bekommen. Da
sich ihr Wissensstand während der Beschäftigung mit diesen Themen stetig ver-
ändert, können sie durch die Wiederholung wichtiger Konzepte ein tieferes Ver-
ständnis erlangen.
Wir haben dieses Vorgehen viele Male mit Studenten erprobt. Manchmal haben
die Studenten mit diesem Vorgehen weniger Probleme als die Lehrenden. Und
bedenken Sie: Eine steile Lernkurve ist nicht problematisch, solange Sie sicherstel-
len, dass Ihre Studenten den Anstieg schaffen können!

Keine komplette Abdeckung der Sprache Java


Verbunden mit unserem iterativen Vorgehen ist die Entscheidung, dass wir nicht
versuchen wollen, die Sprache Java in diesem Buch komplett zu behandeln.
Der Fokus dieses Buches liegt auf der Vermittlung allgemeiner objektorientierter
Programmierprinzipien, nicht von Details der Sprache Java. Studenten, die mit

1 Das „Early Bird“-Muster, in J. Bergin: „Fourteen pedagogical patterns for teaching computer
science”, Proceedings of the Fifth European Conference on Pattern Languages of Programs
(EuroPLoP 2000), Irsee, Deutschland, Juli 2000.

19
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

diesem Buch Programmieren lernen, können leicht 30 bis 40 Jahre als professio-
nelle Softwareentwickler arbeiten – da ist es eine ziemlich sichere Wette, dass der
Großteil ihrer Arbeit nicht in Java stattfinden wird. Natürlich sollte jedes Lehrbuch
den Anspruch haben, mehr Fundamentales zu vermitteln als nur die Program-
miersprache, die gerade in Mode ist.
Auf der anderen Seite sind viele Details von Java wichtig, um die praktischen Auf-
gaben erledigen zu können. In diesem Buch besprechen wir die Java-Konstrukte
so detailliert wie notwendig, um die Konzepte verstehen und die praktischen
Aufgaben lösen zu können. Einige Java-spezifische Konstrukte hingegen wurden
bewusst ausgelassen.
Uns ist klar, dass einige Dozenten Themen behandeln werden, die wir nicht im
Detail besprechen. Statt zu versuchen, alle Themen selbst abzudecken (und damit
den Umfang dieses Buches auf 1500 Seiten aufzublähen), versuchen wir „Aufhän-
ger“ anzubieten. Solche Aufhänger sind Verweise, häufig in Form einer Frage, die
ein Thema anspricht und einen Hinweis auf einen Anhang oder externes Material
gibt. So wird sichergestellt, dass bestimmte Themen zum passenden Zeitpunkt
angesprochen werden, und es bleibt dem Leser oder dem Lehrenden überlassen,
wie weit dem Verweis gefolgt werden sollte. Die Aufhänger dienen als Erinnerung
an ein Thema und als Platzhalter, an denen eine vertiefende Diskussion vorgenom-
men werden kann.
Lehrende können dem von uns vorgegebenen Pfad im Buch folgen oder an ent-
sprechenden Stellen in Seitenpfade verzweigen.
In den Kapiteln sind häufig auch Fragen enthalten, die verwandte Themen anspre-
chen, ohne dass diese im Buch diskutiert werden. Wir empfehlen Lehrenden, einige
dieser Fragen im Kurs zu besprechen, bzw. den Studierenden, diese Übungsauf-
gaben zu bearbeiten.

Projektorientiertes Vorgehen
Die Einführung von neuen Themen in diesem Buch orientiert sich an Projekten.
Das Buch diskutiert zahlreiche Programmierprojekte und bietet viele Übungen.
Statt ein neues Konzept vorzustellen und es dann anhand einer Übungsaufgabe
zur Lösung anwenden zu lassen, stellen wir zuerst ein Ziel auf und formulieren
eine Aufgabe. Beim Analysieren der Aufgabe stellen wir fest, welche Lösungen
wir benötigen. Als Konsequenz werden neue Konstrukte dann vorgestellt, wenn
sie uns bei der Lösung einer Aufgabenstellung unterstützen können.
Die ersten Kapitel enthalten mindestens zwei Diskussionsbeispiele. Das sind Pro-
jekte, die detailliert besprochen werden, um die wichtigen Konzepte in jedem
Kapitel zu illustrieren. Die Verwendung zweier unterschiedlicher Beispiele unter-
stützt das iterative Vorgehen: Jedes Konzept wird nach seiner Einführung noch
einmal in einem anderen Kontext wiederholt.
Beim Entwurf dieses Buches haben wir versucht, viele unterschiedliche Beispiel-
projekte zu finden. Dies weckt hoffentlich die Neugierde des Lesers und illustriert
auch die Bandbreite der Bereiche, in denen die Konzepte angewendet werden
können. Wir hoffen, dass unsere Projekte den Lehrenden gute Startpunkte lie-
fern und viele Ideen für vielfältige und spannende Aufgaben bieten.

20
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

Die Implementierung aller Projekte wurde sehr sorgfältig vorgenommen, sodass


beim Lesen des Quelltextes auch viele Nebenaspekte betrachtet werden können.
Wir glauben sehr stark an das Lernen durch Lesen und Nachahmen guter Bei-
spiele. Damit dies funktioniert, ist es wichtig, dass die Beispiele gut und nachah-
menswert geschrieben sind. Wir haben versucht, interessante Beispiele zu finden.
Alle Projekte sind als offene Problemstellungen entworfen. Auch wenn eine oder
mehrere Versionen jedes Problems ausführlich im Buch dargestellt werden, sind
alle Projekte so entworfen, dass zusätzliche Erweiterungen und Verbesserungen
in Studentenprojekten vorgenommen werden können. Der komplette Quelltext
aller Projekte ist beigefügt. Eine Liste der Projekte, die in diesem Buch diskutiert
werden, ist ab Seite 26 zu finden.

Konzepte statt Sprachkonstrukte


Ein weiterer Aspekt, der dieses Buch von vielen anderen unterscheidet, ist die Orien-
tierung an fundamentalen Aufgaben der Softwareentwicklung und nicht notwendi-
gerweise an spezifischen Java-Konstrukten. Ein Hinweis darauf sind die Kapitelüber-
schriften. In diesem Buch werden Sie keine klassischen Kapitelüberschriften wie
„Primitive Datentypen“ oder „Kontrollstrukturen“ finden. Die Strukturierung nach
fundamentalen Entwicklungsaufgaben ermöglicht uns eine sehr viel allgemeinere
Einführung, die sich nicht an den Besonderheiten einer speziellen Programmierspra-
che orientiert. Wir glauben auch, dass es den Studenten leichter fällt, der Motivation
der jeweiligen Einleitung zu folgen, und dass die Lektüre dadurch interessanter wird.
Ein Resultat dieses Vorgehens ist, dass sich dieses Buch weniger als eine Referenz
eignet. Einführende Lehrbücher und Referenzbücher haben unterschiedliche und
teilweise konkurrierende Ziele. Bis zu einem gewissen Grad kann ein Lehrbuch
versuchen, beides zu bieten, aber irgendwann muss ein Kompromiss eingegan-
gen werden. Unser Buch ist deutlich als ein Lehrbuch entworfen, und immer,
wenn es zu einem Konflikt kam, haben wir den Stil eines Lehrbuches dem eines
Referenzbuches vorgezogen.
Dennoch haben wir Unterstützung für die Verwendung als Referenzbuch gebo-
ten, indem wir die Java-Konstrukte, die in den einzelnen Kapiteln angesprochen
werden, in der Einführung jedes Kapitels aufgeführt haben.

Übersicht der Kapitel


Kapitel 1 beschäftigt sich mit den fundamentalen Konzepten der Objektorientie-
rung: Objekte, Klassen und Methoden. Es liefert eine solide, pragmatische Einfüh-
rung in diese Konzepte, ohne auf Details der Java-Syntax einzugehen. Nebenbei
wird auch bereits das Konzept der Abstraktion kurz eingeführt – ein Thema, das
sich durch viele Kapitel ziehen wird. Schließlich wird auch ein erster Blick auf den
Quelltext geworfen. Zwei Projekte beschäftigen sich mit grafischen Figuren, die
interaktiv gezeichnet werden können, ein weiteres Projekt behandelt das Anmel-
den von Studenten für Laborkurse.
Kapitel 2 öffnet Klassendefinitionen und untersucht, wie mit Quelltext das Ver-
halten von Objekten definiert wird. Wir diskutieren, wie Datenfelder definiert
und Methoden implementiert werden, und arbeiten heraus, welche wichtige
Rolle der Konstruktor beim Aufbau des Objektszustands, verkörpert durch seine

21
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

Felder, spielt. Hier führen wir auch die ersten Anweisungen ein. Das Hauptbeispiel
ist die Implementierung eines Ticketautomaten. Wir untersuchen das Beispiel mit
den Laborkursen aus Kapitel 1 ein bisschen genauer.
Kapitel 3 geht einen Schritt weiter, indem die Interaktion zwischen mehreren
Objekten diskutiert wird. Wir sehen, wie Objekte zusammenarbeiten können, indem
sie gegenseitig ihre Methoden aufrufen, um eine gemeinsame Aufgabe zu lösen.
Wir diskutieren auch, wie ein Objekt weitere Objekte erzeugen kann. Die Anzeige
einer digitalen Uhr wird diskutiert, die zwei Objekte zur Anzeige von Stunden und
Minuten benutzt. Eine Version des Projekts, die eine grafische Benutzungsoberfläche
umfasst, verfolgt eine Strategie, die uns im weiteren Verlauf des Buches immer wie-
der begegnen wird: Wir bieten für den interessierten und eifrigen Leser zusätzlichen
Code, auf den wir jedoch im Text nicht ausführlich eingehen. Als zweites größeres
Beispiel untersuchen wir die Simulation eines E-Mail-Systems, in dem Nachrichten
zwischen E-Mail-Clients verschickt werden können.
In Kapitel 4 fahren wir fort mit der Konstruktion größerer Objektstrukturen und
greifen die Themen Abstraktion und Objektinteraktion noch einmal auf. Vor allem
beginnen wir hier, Sammlungen von Objekten zu benutzen. Wir betrachten ein
Verwaltungssystem für Musikdateien und ein Auktionssystem, um Sammlungen
einzuführen. Gleichzeitig diskutieren wir das Iterieren über Sammlungen und wer-
fen einen ersten Blick auf for-each- und while-Schleifen. Die erste Sammlung, die
benutzt wird, ist die ArrayList.
In Kapitel 5 beginnen wir, die fortgeschrittenen Konzepten vorzustellen (dieser
Abschnitt kann übersprungen werden, wenn die Zeit knapp ist): Es ist eine Ein-
führung in funktionale Programmierkonstrukte. Die funktionalen Konstrukte bie-
ten eine Alternative zur imperativen Verarbeitung von Sammlungen, die wir in
Kapitel 4 besprechen. Die dargestellten Probleme könnten auch ohne diese Tech-
niken gelöst werden, doch funktionale Konstrukte eröffnen elegantere Möglich-
keiten, unsere Ziele zu erreichen. Dieses Kapitel führt den funktionalen Ansatz
allgemein ein sowie einige der neuen Sprachkonstrukte in Java.
Kapitel 6 behandelt Bibliotheken und Schnittstellen. Wir stellen die Standardbib-
liothek von Java vor und diskutieren einige wichtige Bibliotheksklassen. Am wich-
tigsten ist hier die Darstellung, wie die Dokumentation der Bibliothek gelesen
und verstanden werden kann. Es wird diskutiert, wie wichtig das Schreiben von
Dokumentationen für Softwareentwicklungsprojekte ist, und schließlich üben wir
das Erstellen einer angemessenen Dokumentation für unsere eigenen Klassen.
Random, Set und Map sind Beispiele von Klassen, die wir in diesem Kapitel unter-
suchen. Wir implementieren ein Eliza-ähnliches Dialogsystem und eine grafische
Simulation von springenden Bällen, um diese Klassen anzuwenden.
Kapitel 7 konzentriert sich auf einen sehr speziellen Typ von Sammlung: Arrays.
Das Arbeiten mit Arrays und der mit ihnen verbundenen Schleifenarten wird aus-
führlich besprochen.
In Kapitel 8 diskutieren wir etwas formaler die Probleme beim Aufteilen eines Pro-
blems in geeignete Klassen für die Implementierung. Wir stellen Konzepte für
guten Klassenentwurf vor, einschließlich Entwurf nach Verantwortlichkeiten, Kopp-
lung, Kohäsion und Refactoring. Für diese Diskussion verwenden wir ein interakti-

22
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

ves, textbasiertes Adventure-Game (Die Welt von Zuul). Wir verbessern in mehreren
Iterationen den internen Entwurf des Spiels und erweitern seine Funktionalität, bis
wir schließlich mit einer umfangreichen Liste von Erweiterungsmöglichkeiten Anre-
gungen für weiterführende Studentenprojekte geben.
Kapitel 9 beschäftigt sich mit Konzepten, mit denen korrekte, verständliche und
wartbare Klassen geschrieben werden können. Es behandelt Aspekte, die vom
Schreiben eines verständlichen Quelltextes (einschließlich Stil und Kommentie-
rung) bis hin zum Testen (einschließlich formalisierter Regressions- und JUnit-Tests)
und zur Fehlerbeseitigung reichen. Es werden Teststrategien vorgestellt und einige
Methoden zur Fehlerbeseitigung ausführlich diskutiert. Wir benutzen das Beispiel
eines Onlineshops und die Implementierung eines Taschenrechners für die Dis-
kussion dieser Konzepte.
Kapitel 10 und 11 führen Vererbung, Polymorphie und damit verbundene Konzepte
ein. Wir untersuchen einen Teil eines sozialen Netzwerks, um die Konzepte darzustel-
len. Fragen der Quelltextvererbung, des Subtypings, der polymorphen Methodenauf-
rufe und des Überschreibens von Methoden werden ausführlich diskutiert.
In Kapitel 12 implementieren wir eine Jäger-Beute-Simulation. Sie dient als Aus-
gangspunkt für die Diskussion weiterer Abstraktionsmechanismen, die auf Verer-
bung aufsetzen, insbesondere Interfaces und abstrakte Klassen.
Kapitel 13 behandelt zwei neue Beispiele: einen Bildbetrachter und eine grafi-
sche Benutzungsoberfläche (GUI) für das Verwaltungssystem von Musiksamm-
lungen (dem wir bereits in Kapitel 4 begegnet sind). Anhand dieser Beispiele wird
dargestellt, wie GUIs konstruiert werden.
Kapitel 14 beschäftigt sich dann mit der schwierigen Frage, wie mit Fehlern zur
Laufzeit umgegangen werden soll. Einige Probleme und Lösungen werden disku-
tiert und der Exception-Mechanismus von Java wird ausführlich vorgestellt. Wir
erweitern und verbessern eine Adressbuch-Anwendung, um diese Konzepte dar-
zustellen. Als Fallstudie für einen Aufgabenbereich, wo die Fehlerbehandlung
unverzichtbar ist, dient uns die Ein-/Ausgabe.
Kapitel 15 beleuchtet ausführlicher die nächste Abstraktionsstufe: wie man zu
einem vage beschriebenen Problem passende Klassen und Methoden findet. In
den vorigen Kapiteln haben wir angenommen, dass große Teile der Anwendung
bereits existieren und wir Verbesserungen vornehmen. Jetzt ist es an der Zeit, uns
anzusehen, wie man mit einem Code bei Null beginnt. Dies schließt eine detail-
lierte Darstellung von Vorgehensweisen ein, wie wir die passenden Klassen für eine
Anwendung finden, wie diese interagieren und wie die Zuständigkeiten verteilt
sein sollten. Wir benutzen CRC-Karten als Herangehensweise, um ein Kinobu-
chungssystem zu entwerfen.
In Kapitel 16 führen wir alles bis dahin Dargestellte zusammen. Es enthält eine
komplette Fallstudie, die mit einem Anwendungsentwurf beginnt und über den
Entwurf von Klassenschnittstellen und die Diskussion vieler wichtiger funktionaler
und nichtfunktionaler Aspekte bis hin zu Details der Implementierung reicht. Kon-
zepte aus früheren Kapiteln (wie Zuverlässigkeit, Datenstrukturen, Klassenentwurf,
Testen und Erweiterbarkeit) werden erneut in einem neuen Kontext dargestellt.

23
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Vorwort für den Lehrenden

Zusatzmaterial
Website zum Buch: Alle Projekte, die als Diskussionsbeispiele und Übungen
in diesem Buch verwendet wurden, können von der Buchwebsite unter http://
[Link]/objects-first/ heruntergeladen werden. Die Website enthält außer-
dem Links zum Download von BlueJ sowie weiteres Material.
Die Companion Website (CWS) für Studenten: Die folgenden Ressourcen stehen
allen Lesern dieses Buches im Internet unter [Link]
[Link] zur Verfügung:2
 Quelltext zu allen Projekten
 Links zu weiterem interessanten Material
Auf der englischsprachigen Seite [Link] finden Sie außer-
dem noch den Program Style Guide zu allen Beispielen in diesem Buch sowie Links
zu allen im Buch erwähnten Ressourcen.
Diskussionsgruppen für Studenten: Studenten, die Fragen haben oder das Mate-
rial in diesem Buch beziehungsweise Themen zu BlueJ allgemein diskutieren wollen,
können dies unter [Link] in der Gruppe
bluej-discuss machen.
Ressourcen für Dozenten: Das folgende Material steht nur qualifizierten Dozenten
zur Verfügung:
 Lösungen zu den Übungen am Ende des Kapitels
 PowerPoint-Folien

Der Blueroom
Vielleicht noch wichtiger als das statische Material der oben genannten Websites ist
das sehr aktive Forum der BlueJ-Gemeinde. Es wendet sich an Dozenten, die BlueJ
und dieses Buch im Unterricht einsetzen. Das Forum läuft unter der Bezeichnung
Blueroom und ist unter
[Link]
zu finden. Der Blueroom enthält eine Sammlung von umfangreichem Lehrmaterial,
das von den Lehrkräften gemeinsam genutzt werden kann, sowie ein Diskussions-
forum, in denen die Lehrkräfte Fragen stellen, Themen diskutieren und sich über
die neuesten Entwicklung auf dem Laufenden halten können. Im Blueroom haben
Sie die Möglichkeit, zu vielen anderen Dozenten, Entwicklern sowie den Autoren
dieses Buches Kontakt aufzunehmen.

2 Oder geben Sie gerne in das Suchfeld unter [Link] die Buchnummer 4911
ein und gelangen direkt zur Webseite des Buches.

24
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Danksagungen

Danksagungen
Viele Menschen haben auf unterschiedliche Weise zu diesem Buch beigetragen
und seine Fertigstellung möglich gemacht.
An erster Stelle muss John Rosenberg genannt werden. Es ist lediglich ein unglück-
licher Umstand, dass John nicht einer der Autoren dieses Buches ist. Er war von
Anfang an eine der treibenden Kräfte bei der Entwicklung von BlueJ, samt den
dahinterstehenden Ideen und der Pädagogik, und wir haben über dieses Buch-
projekt schon seit vielen Jahren geredet. Vieles von dem Material, das in diesem
Buch vorgestellt wird, wurde in Diskussionen mit John entwickelt. Die schlichte
Tatsache, dass ein Tag nur 24 Stunden hat, von denen zu viele schon durch zu
viele andere Aufgaben belegt waren, hat ihn selbst vom Schreiben dieses Buches
abgehalten. John hat fortwährend zu diesem Buch beigetragen, während es
geschrieben wurde, und hat bei seiner Verbesserung an vielen Stellen geholfen.
Wir haben seine Freundschaft und Zusammenarbeit sehr zu schätzen gewusst.
Einige weitere Personen haben geholfen, BlueJ seine heutige Form zu geben: Bruce
Quig, Davin McCall und Andrew Patterson in Australien, Ian Utting, Poul Henriksen
und Neil Brown in England. Alle arbeiten seit vielen Jahren an BlueJ, verbessern
und erweitern den Entwurf und die Implementierung neben all ihren anderen
Verpflichtungen. Ohne ihren Beitrag hätte BlueJ niemals seine heutige Qualität und
Popularität erreicht und dieses Buch wäre möglicherweise nie geschrieben worden.
Ein weiterer wichtiger Beitrag, der BlueJ und dieses Buch erst ermöglicht hat, ist die
großzügige Unterstützung zuerst durch Sun Microsystems und jetzt durch Oracle.
Sun hat BlueJ jahrelang großzügig unterstützt – eine Strategie, die Oracle nach der
Übernahme von Sun nicht geändert hat. Wir sind sehr dankbar für diese wichtige
Hilfe.
Das Pearson-Team hat auf fantastische Weise dafür gesorgt, dass dieses Buch
fertig wurde, und die schlimmste Befürchtung jedes Autors ausgeräumt – dass
sein Buch unbemerkt bleiben könnte. Ein besonderer Dank geht daher an unsere
Lektorin Tracy Johnson sowie an Camille Trentacoste und Carole Snyder, die uns
beim Schreiben in jedweder Hinsicht unterstützt haben.
David möchte seinen persönlichen Dank an die Mitarbeiter und Studenten der
School of Computing an der Universität von Kent richten. Es war immer ein gro-
ßes Privileg, die Studenten der einführenden OO-Veranstaltung unterrichten zu
dürfen. Sie liefern den entscheidenden Ansporn und die Motivation, durch die
das Lehren so viel Spaß macht. Ohne die wertvolle Hilfe von Kollegen und stu-
dentischen Betreuern wäre es unmöglich, Lehrveranstaltungen durchzuführen.
Michael möchte Davin, Neil und Phil danken, die eine so hervorragende Arbeit
bei der Entwicklung und Wartung von BlueJ, Greenfoot und unserer Website
geleistet haben. Ohne ein solch großartiges Team wäre dies nicht zu realisieren
gewesen.

25
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Projekte, die in diesem Buch detailliert besprochen werden

Projekte, die in diesem Buch


detailliert besprochen werden
Figuren Kapitel 1
Einfaches Zeichnen mit grafischen Figuren; demonstriert die Erzeugung von
Objekten, Methodenaufrufe und Parameter.

Haus Kapitel 1
Ein Beispiel, in dem mit einigen Figuren eine Zeichnung erstellt wird; führt Quell-
text, Java-Syntax und Übersetzung ein.

Laborkurse Kapitel 1, Kapitel 2, Kapitel 10


Ein einfaches Beispiel mit Kursen und Studenten; demonstriert Objekte, Daten-
felder und Methoden wird erneut verwendet in Kapitel 10, um Vererbung hinzu-
zufügen.

Ticketautomat Kapitel 2
Eine Simulation eines Ticketautomaten für Zugtickets; führt weiter in Datenfelder,
Konstruktoren, sondierende und verändernde Methoden, Parameter und einige
einfache Anweisungen ein.

Buch-Aufgabe Kapitel 2
Speicherung von Informationen über Bücher; wiederholt die Konstrukte, die im
Beispiel des Ticketautomaten benutzt werden.

Zeitanzeige Kapitel 3
Eine Implementierung einer Anzeige einer Digitaluhr; illustriert die Konzepte Abs-
traktion, Modularisierung und Objektinteraktion.

Mail-System Kapitel 3
Eine einfache Simulation eines Mail-Systems; demonstriert Objekterzeugung und
Objektinteraktion.

Musiksammlung Kapitel 4, Kapitel 13


Eine Implementierung eines Verwaltungssystems für Musik-Clips; dient zur Einfüh-
rung von Sammlungen und Schleifen. Umfasst die Fähigkeit, mp3-Dateien abzu-
spielen. In Kapitel 11 wird das System um eine Benutzungsoberfläche ergänzt.

Auktion Kapitel 4
Ein Auktionssystem; mehr über Sammlungen und Schleifen, diesmal mit Iterato-
ren.

26
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Projekte, die in diesem Buch detailliert besprochen werden

Tiermonitoring Kapitel 5, 6
Ein System, um Tierpopulationen zu beobachten bzw. zu überwachen, z.B. in
Nationalparks. Dieses System wird verwendet, um funktionale Verarbeitung von
Sammlungen einzuführen.

Technischer-Kundendienst Kapitel 6, 14
Eine Implementierung eines Eliza-ähnlichen Dialogsystems, das den technischen
Kundendienst Kunden gegenüber simulieren soll; führt in die Benutzung von Bib-
liotheksklassen im Allgemeinen und in einige bestimmte Klassen im Besonderen
ein; beschreibt das Lesen und Schreiben von Dokumentationen.

Kritzeln Kapitel 6
Ein Programm zum Zeichnen, das zeigt, wie man die Klassen über ihre Schnitt-
stellen kennenlernen kann.

Baelle Kapitel 6
Eine grafische Animation springender Bälle; demonstriert die Trennung von Schnitt-
stelle und Implementierung und einfache grafische Operationen.

Weblog-Auswertung Kapitel 7, 14
Ein Programm zum Analysieren einer Logdatei für Zugriffe auf Webseiten; führt
Arrays und for-Schleifen ein.

Automat Kapitel 7
Eine Reihe von Beispielen zellulärer Automaten. Wird verwendet, um Praxis mit
Array-Programmierung zu bekommen.

Brain Kapitel 7
Eine Version von Brian's Brain, die wir verwenden, um zweidimensionale Arrays
zu besprechen.

Zuul Kapitel 8, 11, 14


Ein textbasiertes, interaktives Adventure-Game; sehr gut erweiterbar, hervorra-
gend für offene Studentenprojekte. Wird hier benutzt, um guten Klassenent-
wurf, Kopplung und Kohäsion zu diskutieren, und wird in Kapitel 11 noch einmal
als Beispiel für die Anwendung von Vererbung aufgegriffen.

Onlineshop Kapitel 9
Die frühen Phasen der Implementierung eines Teils einer Website für einen Online-
shop, der auch Benutzerkommentare berücksichtigt; wird zur Diskussion von
Test- und Fehlersuchstrategien benutzt.

27
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Projekte, die in diesem Buch detailliert besprochen werden

Rechner Kapitel 9
Eine Implementierung eines Taschenrechners. Dieses Beispiel wiederholt bereits
vorgestellte Konzepte und wird für die Diskussion über Testen und Fehlerbeseiti-
gung verwendet.

Ziegelsteine Kapitel 9
Eine einfache Übung zur Fehlersuche; modelliert das Beladen von Paletten mit
Ziegelsteinen für einfache Berechnungen.

Netzwerk Kapitel 10, 11


Teil einer sozialen Netzwerk-Anwendung. Dieses Projekt wird ausführlich disku-
tiert und erweitert, um die Grundlagen von Vererbung und Polymorphie darzu-
stellen.

Fuechse-und-Hasen Kapitel 12
Eine klassische Jäger-Beute-Simulation; wiederholt Vererbungskonzepte und führt
Interfaces und abstrakte Klassen ein.

Bildbetrachter Kapitel 13
Eine einfache Anwendung zur Bildbetrachtung und -bearbeitung. Wir konzent-
rieren uns vor allem auf den Aufbau einer Benutzungsoberfläche.

Musikplayer Kapitel 13
Als weiteres Beispiel für den Aufbau einer Benutzungsoberfläche wird dem Pro-
jekt Musiksammlung aus Kapitel 4 eine GUI hinzugefügt.

Adressbuch Kapitel 14
Eine Implementierung eines Adressbuches mit einer optionalen grafischen Benut-
zungsoberfläche. Das Nachschlagen im Adressbuch ist flexibel: Adressen können
auf Basis unvollständiger Angaben des Namens oder der Telefonnummer gesucht
werden. Dieses Projekt macht ausführlich Gebrauch von Exceptions.

Kinobuchungssystem Kapitel 15
Ein System, das Sitzplatzreservierungen für ein Kino verwaltet. Dieses Beispiel wird
benutzt, um das Identifizieren von Klassen und den Entwurf einer Anwendung zu
diskutieren. Es wird kein Programmtext vorgegeben, da in diesem Beispiel eine
Anwendung ausgehend von einem leeren Blatt Papier entwickelt wird.

Taxi Kapitel 16
Das Taxi-Beispiel ist eine Kombination eines Buchungssystems, eines Verwaltungs-
systems und einer Simulation. Es wird als Fallstudie benutzt, um viele der Kon-
zepte und Techniken, die im Buch vorgestellt wurden, zusammenzuführen.

28
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
TEIL

I Objekte und Klassen

1 Objekte und Klassen............................................................................ 31


2 Klassendefinitionen ............................................................................. 51
3 Objektinteraktion ................................................................................ 97
4 Objektsammlungen ............................................................................. 131
5 Funktionale Verarbeitung von Sammlungen
(fortgeschrittene Konzepte) ............................................................. 183
6 Bibliotheksklassen nutzen ................................................................. 207
7 Sammlungen mit fester Größe – Arrays ....................................... 263
8 Klassenentwurf ..................................................................................... 293
9 Fehler vermeiden ................................................................................. 335

Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.


Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

1 Objekte und Klassen

Lernziele
Zentrale Konzepte in diesem Kapitel: Objekte, Methoden, Klassen, Para-
meter

Zeit loszulegen und mit unserer Einführung in die objektorientierte Programmierung


zu beginnen. Um Programmieren zu lernen, bedarf es zweierlei: ein wenig Theorie
und viel, viel Praxis. Das eine wie das andere bekommen Sie in diesem Buch gebo-
ten, sodass sich beide gegenseitig unterstützen und befruchten können.
Zuerst müssen wir zwei zentrale Konzepte der Objektorientierung verstehen:
Objekte und Klassen. Sie bilden die Basis jedweder objektorientierten Programmie-
rung, gleich in welcher objektorientierten Sprache. Lassen Sie uns also mit einer
kurzen Besprechung dieser beiden grundlegenden Konzepte beginnen.

1.1 Objekte und Klassen


Wenn Sie ein Computerprogramm in einer objektorientierten Sprache schreiben, Konzept
dann erstellen Sie in Ihrem Computer ein Modell eines Ausschnitts der realen
Java-Objekte
Welt. Dieser Ausschnitt setzt sich aus den Objekten zusammen, die im Anwen-
modellieren
dungsbereich vorkommen. Diese Objekte müssen im zu erstellenden Computer- Objekte eines
modell geeignet repräsentiert werden. Die Objekte des Anwendungsbereichs Anwendungs-
unterscheiden sich je nach Programm, das Sie schreiben. Es können Wörter und bereichs.
Absätze sein, wenn Sie eine Textverarbeitung programmieren, Nutzer und Nach-
richten, wenn Sie an einem sozialen Netzwerk arbeiten, oder Monster, wenn Sie
ein Computerspiel schreiben.
Objekte können klassifiziert werden. Eine Klasse beschreibt – auf abstrakte Weise –
alle Objekte einer bestimmten Art.
Wir können diese etwas abstrakten Aussagen klarer machen, indem wir ein Bei-
spiel betrachten. Nehmen Sie an, Sie möchten eine Verkehrssimulation entwerfen.
Ein Begriff, mit dem Sie dann sicherlich umgehen werden, ist „Auto“. Was ist ein
Auto in unserem Kontext? Ist es eine Klasse oder ein Objekt? Einige zusätzliche
Fragen können uns helfen, eine Entscheidung zu treffen.

31
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

Welche Farbe hat ein Auto? Welche Höchstgeschwindigkeit hat es? Wo befindet
es sich gerade?

Konzept Wie Sie sicher bemerkt haben, können wir diese Fragen nicht beantworten, bevor
wir über ein bestimmtes Auto sprechen. Der Grund ist, dass wir uns mit dem
Objekte werden
Wort „Auto“ auf die Klasse Auto beziehen – wir reden über Autos im Allgemei-
durch Klassen
erzeugt. Eine nen, nicht über ein bestimmtes.
Klasse beschreibt Wenn ich aber sage: „Das Auto, das zu Hause in meiner Garage parkt“, dann kann
eine bestimmte Art
ich die obigen Fragen beantworten. Das Auto ist rot, es fährt nicht sehr schnell,
von Objekten;
Objekte repräsen- und es steht in meiner Garage. Jetzt rede ich über ein Objekt – über ein spezielles
tieren individuelle Exemplar eines Autos.
Instanzen einer
Wir nennen üblicherweise ein bestimmtes Objekt eine Instanz (instance, Exemp-
Klasse.
lar). Diesen Begriff werden wir von nun an recht häufig verwenden. „Instanz“ ist
ungefähr synonym zu „Objekt“ – wir beziehen uns auf Objekte als Instanzen,
wenn wir betonen wollen, dass sie von einer bestimmten Klasse sind (wie in „die-
ses Objekt ist eine Instanz der Klasse Auto“).
Bevor wir diese etwas theoretische Diskussion fortsetzen, wollen wir zunächst ein
Beispiel betrachten.

1.2 Instanzen erzeugen


Starten Sie BlueJ und öffnen Sie das Projekt Figuren.1 Sie sollten daraufhin ein Fens-
ter sehen, das dem in Abbildung 1.1 ähnelt.

Abbildung 1.1
Das Projekt
Figuren in BlueJ.

1 Wir empfehlen, dass Sie beim Lesen dieses Buches immer wieder selbst aktiv werden und die
Übungen durcharbeiten. An dieser Stelle nehmen wir an, dass Sie bereits wissen, wie man BlueJ
startet und das Beispielprojekt öffnet. Falls dies nicht der Fall ist, lesen Sie bitte zuerst Anhang A.

32
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.3 Methoden aufrufen

In diesem Fenster sollte ein Diagramm sichtbar sein. Jedes der gefärbten Recht-
ecke in diesem Diagramm repräsentiert eine Klasse in diesem Projekt. Die Klassen
dieses Projekts heißen Kreis, Quadrat, Dreieck, Person und Leinwand.
Klicken Sie mit der rechten Maustaste auf die Klasse Kreis und wählen Sie
new Kreis()
aus dem Kontextmenü. Das System fragt Sie nun nach einem „Instanznamen“ –
klicken Sie auf OK, der vorgegebene Name ist vorerst gut genug. Im unteren Bereich
des Fensters sollte nun ein rotes Rechteck erscheinen, das mit „kreis1“ beschrif-
tet ist (Abbildung 1.2).

Abbildung 1.2
Eine Instanz in
der Objektleiste.

Gerade haben Sie Ihr erstes Objekt erzeugt! „Kreis“, das rechteckige Symbol in
Abbildung 1.1, repräsentiert die Klasse Kreis; kreis1 ist eine frisch erzeugte In-
stanz dieser Klasse. Den Bereich am unteren Ende des Fensters, in dem die Instanz
angezeigt wird, nennen wir die Objektleiste.

Konvention
Wir beginnen den Namen einer Klasse mit einem Großbuchstaben (wie in
Kreis) und benennen Objekte mit Kleinbuchstaben (wie in kreis1). Dies hilft
uns, die Dinge voneinander zu unterscheiden.

Übung 1.1 Erzeugen Sie einen weiteren Kreis. Erzeugen Sie dann ein Quad-
rat.

1.3 Methoden aufrufen


Führen Sie einen Rechtsklick auf einer der Kreis-Instanzen aus (nicht auf der Klasse!).
Es erscheint ein Kontextmenü (Abbildung 1.3) mit mehreren Einträgen. Wählen
Sie sichtbarMachen aus – dies bewirkt, dass eine Repräsentation dieses Kreises in
einem eigenen Fenster angezeigt wird (Abbildung 1.4).

33
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

Abbildung 1.3
Das Kontextmenü
eines Objekts mit
der Liste seiner
Operationen.

Abbildung 1.4
Die Visualisierung
eines Kreises.

Konzept Sie sehen einige weitere Einträge im Kontextmenü des Kreises. Versuchen Sie,
nachRechtsBewegen oder nachUntenBewegen auszuwählen und damit den Kreis näher
Wir können mit
an die Ränder des Fensters zu rücken. Probieren Sie auch unsichtbarMachen und
Objekten kommuni-
zieren, indem wir sichtbarMachen, um den Kreis zu verstecken und wieder anzuzeigen.
ihre Methoden
aufrufen. Ein Objekt
tut üblicherweise Übung 1.2 Was geschieht, wenn Sie nachUntenBewegen zweimal aufrufen?
etwas, wenn eine Oder dreimal? Was passiert, wenn Sie unsichtbarMachen zweimal aufrufen?
seiner Methoden
aufgerufen wird.

Die Einträge im Kontextmenü der Kreis-Instanz repräsentieren Operationen, mit


denen die Instanz manipuliert werden kann. Diese Operationen nennen wir Metho-
den. Die übliche Sprechweise ist, dass diese Methoden aufgerufen werden. Wir
wollen diese Terminologie von nun an einhalten. Wir können Sie beispielsweise auf-
fordern, „die Methode nachRechtsBewegen an dem Objekt kreis1“ aufzurufen.

34
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.4 Parameter

1.4 Parameter
Rufen Sie nun die Methode horizontalBewegen auf. Es erscheint ein Dialog, der Sie Konzept
um eine Eingabe bittet (Abbildung 1.5). Tippen Sie „50“ ein und klicken Sie auf OK.
Methoden können
Der Kreis wird sich daraufhin um 50 Bildschirmpunkte nach rechts bewegen.2
Parameter
haben, mit denen
zusätzliche Infor-
mationen für eine
Aufgabe ange-
geben werden.

Abbildung 1.5
Ein Dialog zum Aufruf
von Methoden.

Die Methode horizontalBewegen, die Sie gerade aufgerufen haben, ist so gestal-
tet, dass sie zusätzliche Informationen von Ihnen benötigt, um ihre Aufgabe
erfüllen zu können. In diesem Fall ist diese zusätzliche Information, um wie viele
Bildschirmpunkte der Kreis bewegt werden soll. Die Methode horizontalBewegen
ist somit flexibler als die Methoden nachRechtsBewegen und nachLinksBewegen. Die
Letzteren bewegen den Kreis immer um eine festgelegte Entfernung, wohingegen
horizontalBewegen die Möglichkeit bietet anzugeben, wie weit der Kreis bewegt
werden soll.

Übung 1.3 Bevor Sie weiterlesen, sollten Sie die Methoden vertikalBewegen,
langsamVertikalBewegen und groesseAendern aufrufen. Finden Sie heraus, wie
man horizontalBewegen benutzen kann, um den Kreis 70 Bildschirmpunkte
nach links zu bewegen.

Die zusätzlichen Werte, die manche Methoden benötigen, werden Parameter Konzept
genannt. Eine Methode gibt an, welche Art von Parametern sie erwartet. Wenn
Der Name und die
beispielsweise die Methode horizontalBewegen wie in Abbildung 1.5 aufgerufen
Parametertypen
wird, dann wird im Dialogfenster die Zeile einer Methode wer-
void horizontalBewegen(int entfernung) den als ihre Sig-
natur bezeichnet.
im oberen Bereich angezeigt. Dies ist der Kopf der Methode. Der Kopf liefert Infor- Sie benennen die
mationen über eine Methode. Der Teil zwischen den Klammern (int entfernung) lie- benötigten Infor-
fert Informationen über benötigte Parameter. Für jeden Parameter wird ein Typ und mationen für einen
ein Name definiert. Der Kopf im obigen Beispiel sagt aus, dass die Methode horizon- Aufruf der
Methode.
talBewegen einen Parameter vom Typ int mit dem Namen entfernung erwartet. Der
Name gibt dabei einen Hinweis auf die Bedeutung des Parameters. Der Name einer
Methode und die Parametertypen, die im Kopf vorgefunden werden, werden
zusammen als die Signatur der Methode bezeichnet.

2 Bildschirmpunkte werden auch als Pixel bezeichnet. Der Bildschirm besteht aus einem Raster
von Bildschirmpunkten.

35
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

1.5 Datentypen
Konzept Ein Typ gibt an, welche Art von Information als Parameter übergeben werden kann.
Der Typ int beispielsweise bezeichnet ganze Zahlen (aus dem Englischen integer,
Parameter haben
daher die Abkürzung „int“).
Typen. Ein Typ
definiert, welche Im obigen Beispiel besagt die Signatur von horizontalBewegen, dass wir, bevor die
Arten von Werten Methode ausgeführt werden kann, eine ganze Zahl festlegen müssen, die die Ent-
ein Parameter
fernung angibt. Das Eingabefeld in Abbildung 1.5 ermöglicht diese Eingabe.
annehmen kann.
In den bisherigen Beispielen haben wir nur den Datentyp int kennengelernt. Die
Parameter der Methoden zum Bewegen und der Methode groesseAendern sind
alle von diesem Typ.
Eine nähere Betrachtung des Kontextmenüs einer Instanz zeigt, dass die Einträge
in diesem Menü bereits die Parameterinformation enthalten. Wenn eine Methode
keine Parameter hat, folgen auf den Namen nur leere Klammern. Wenn sie einen
Parameter hat, wird der Typ dieses Parameters angezeigt. In der Liste der Metho-
den für einen Kreis können Sie eine Methode mit einem anderen Parametertyp
entdecken: Die Methode farbeAendern hat einen Parameter vom Typ String.
Der Typ String signalisiert, dass ein Stück Text (beispielsweise ein Wort oder ein
Satz) erwartet wird. Da ein solcher Text sich aus einzelnen Zeichen zusammen-
setzt, bezeichnen wir ihn als eine Zeichenkette. Solche Zeichenketten werden in
doppelten Anführungsstrichen angegeben. Um beispielsweise das Wort rot als
Zeichenkette zu übergeben, tippen Sie:
"rot"
Der Aufrufdialog enthält außerdem über der Zeile mit dem Methodenkopf einen
Textabschnitt, den wir als Kommentar bezeichnen. Kommentare werden angege-
ben, um dem menschlichen Leser Informationen zu geben; sie werden ausführli-
cher in Kapitel 2 behandelt. Der Kommentar der Methode farbeAendern beschreibt
beispielsweise, welche Farbnamen dem System bekannt sind.

Übung 1.4 Rufen Sie die Methode farbeAendern an einer Ihrer Kreis-Instan-
zen auf und übergeben Sie die Zeichenkette "rot". Dies sollte die Farbe des
Kreises ändern. Versuchen Sie es auch mit anderen Farben.
Übung 1.5 Dies ist ein sehr einfaches Beispiel, es werden nicht sehr viele
Farben unterstützt. Was passiert, wenn Sie eine Farbe angeben, die nicht
unterstützt wird?
Übung 1.6 Rufen Sie die Methode farbeAendern auf und geben Sie die Farbe
ohne die Anführungszeichen an. Was passiert?

36
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.6 Eine Klasse, viele Instanzen

Fallstrick
Ein typischer Fehler von Programmieranfängern ist, dass sie bei Angabe eines
Parameters vom Typ String die doppelten Anführungszeichen vergessen. Wenn
Sie beispielsweise blau statt "blau" tippen, bekommen Sie eine Fehlermeldung
in der Art: "Error: cannot resolve symbol – variable blau".

Java unterstützt etliche weitere Datentypen, einschließlich Typen für Fließkomma-


zahlen und einzelne Zeichen. Wir werden diese hier nicht alle diskutieren, sondern
zu einem späteren Zeitpunkt auf sie zurückkommen. Wenn Sie jetzt mehr darüber
wissen wollen, dann können Sie sich Anhang B ansehen.

1.6 Eine Klasse, viele Instanzen


Sobald Sie eine Klasse haben, können Sie beliebig viele Instanzen dieser Klasse erzeu-
gen. Mit der Klasse Kreis können Sie beliebig viele Kreise erzeugen, mit der Klasse
Quadrat beliebig viele Quadrate.

Übung 1.7 Erzeugen Sie mehrere Kreis-Instanzen auf der Objektleiste, indem
Sie mehrfach den Eintrag new Kreis() aus dem Kontextmenü der Klasse Kreis
auswählen. Machen Sie die Instanzen sichtbar und bewegen Sie sie mit den
Bewegungsmethoden. Machen Sie einen Kreis groß und gelb, einen anderen
klein und grün. Probieren Sie auch die anderen Figuren: Erzeugen Sie ein paar
Dreiecke und Quadrate. Ändern Sie deren Positionen, Größen und Farben.

Jedes dieser Objekte hat seine eigene Position, Farbe und Größe. Sie ändern eine Konzept
Eigenschaft eines Objekts (etwa seine Farbe), indem Sie eine Methode an dem
Eine Klasse, viele
Objekt aufrufen. Dieser Aufruf beeinflusst dieses spezielle Objekt, aber keine
Instanzen: Von
anderen Objekte. einer Klasse kön-
Sie haben möglicherweise auch ein weiteres Detail über Parameter entdeckt. Sehen nen viele gleich-
artige Instanzen
Sie sich die Methode groesseAendern an einem Dreieck an. Der Methodenkopf lautet:
erzeugt werden.
void groesseAendern (int neueHoehe, int neueBreite)
Dies ist ein Beispiel für eine Methode mit mehr als einem Parameter. Diese Methode
hat zwei Parameter, die im Kopf durch ein Komma voneinander getrennt sind. All-
gemein kann eine Methode beliebig viele Parameter haben.

37
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

1.7 Zustand
Konzept Die Menge der momentanen Werte der Attribute, die ein Objekt definieren (wie
x - und y -Position, Farbe, Durchmesser und Sichtbarkeit eines Kreises), wird auch
Objekte haben
als der Zustand des Objekts bezeichnet. Dies ist ein weiteres Beispiel der allge-
einen Zustand.
Dieser Zustand mein üblichen Terminologie, an die wir uns von nun an halten werden.
wird durch Werte In BlueJ kann der Zustand eines Objekts genauer untersucht werden, indem der
repräsentiert, die
Eintrag INSPIZIEREN aus dem Kontextmenü einer Instanz ausgewählt wird. Wenn
in Datenfeldern
gehalten werden. ein Objekt auf diese Weise untersucht wird, erscheint ein Fenster, das dem in
Abbildung 1.6 ähnlich ist. Dieses Fenster nennen wir den Objektinspektor.

Abbildung 1.6
Ein Objektinspektor,
der Details eines
Objekts anzeigt.

Übung 1.8 Sorgen Sie dafür, dass mehrere Objekte auf der Objektleiste liegen,
und untersuchen Sie diese der Reihe nach. Versuchen Sie, den Zustand eines
Objekts zu ändern (beispielsweise durch den Aufruf von nachLinksBewegen),
während der Objektinspektor geöffnet ist. Sie sollten beobachten können, wie
sich die Werte im Objektinspektor ändern.

Einige Methoden verändern den Zustand eines Objekts, wenn sie aufgerufen wer-
den. Die Methode nachLinksBewegen ändert beispielsweise das Attribut xPosition. In
Java werden diese Attribute von Objekten häufig Datenfelder oder einfach nur Fel-
der (fields) genannt.

1.8 Das Innenleben eines Objekts


Beim Untersuchen verschiedener Objekte stellen wir fest, dass alle Instanzen der-
selben Klasse auch die gleichen Datenfelder haben. Das heißt, Anzahl, Typen und
Namen der Datenfelder sind gleich, während der aktuelle Wert eines Datenfelds
in jedem Objekt anders sein kann. Im Unterschied dazu haben Instanzen unter-
schiedlicher Klassen auch unterschiedliche Datenfelder. Ein Kreis beispielsweise
hat ein Datenfeld „durchmesser“, während ein Dreieck die Datenfelder „breite“
und „hoehe“ hat.
Dies ist deshalb so, weil Anzahl, Typen und Namen von Datenfeldern durch die
Klasse definiert sind, nicht in einem Objekt. Die Klasse Kreis definiert, dass jede

38
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.8 Das Innenleben eines Objekts

Kreis-Instanz fünf Datenfelder hat, und zwar durchmesser, xPosition, yPosition, farbe
und istSichtbar. Sie legt auch fest, welche Typen diese Datenfelder haben. Die ers-
ten drei sind vom Typ int, die Farbe ist vom Typ String und istSichtbar ist vom Typ
boolean. (Eine boolesche Variable kann einen von zwei Werten annehmen: true und
false. Der Typ boolean wird später noch ausführlicher diskutiert.)

Wenn eine Instanz der Klasse Kreis erzeugt wird, dann hat diese Instanz automa-
tisch diese Datenfelder. Die Werte der Datenfelder werden in der Instanz gespei-
chert. Dies garantiert, dass jede Instanz beispielsweise eine Farbe hat, ermöglicht
aber auch, dass jede Instanz eine andere Farbe haben kann (Abbildung 1.7).

Abbildung 1.7
Eine Klasse und ihre
Instanzen mit Daten-
feldern und Werten.

Das Gleiche gilt für Methoden. Die Methoden werden in der Klasse eines Objekts
definiert. Folglich haben alle Instanzen einer gegebenen Klasse die gleichen Metho-
den. Aber die Methoden werden an konkreten Instanzen aufgerufen. Auf diese
Weise ist eindeutig, welches Objekt seinen Zustand ändern muss, wenn beispiels-
weise die Methode nachRechtsBewegen aufgerufen wird.

Übung 1.9 In Abbildung 1.8 auf Seite 42 sehen Sie zwei Bilder. Suchen Sie sich
eines dieser Bilder aus und versuchen Sie, es mithilfe der Figuren aus dem Pro-
jekt Figuren nachzustellen. Notieren Sie gleichzeitig, was Sie im Einzelnen tun
müssen, um dies zu erreichen. Könnten Sie es auch auf andere Weise errei-
chen?

39
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

1.9 Java-Code
Wenn wir in Java programmieren, schreiben wir eigentlich Befehle nieder, um
Methoden an Objekten aufzurufen – so wie wir es oben für unsere Figur-Objekte
gemacht haben. Allerdings tun wir dies nicht interaktiv, indem wir mit der Maus
Methoden aus einem Menü auswählen, sondern indem wir die Befehle als Text
eintippen. In der BlueJ-Konsole können wir uns ansehen, wie diese Befehle als
Text aussehen.

Übung 1.10 Wählen Sie im Menü ANSICHT den Befehl KONSOLE ANZEIGEN.
Damit wird ein anderes Fenster aufgerufen, das BlueJ zur Ausgabe von Text
verwendet. Wählen Sie in diesem Terminalfenster den Befehl METHODENAUFRUFE
PROTOKOLLIEREN im Menü OPTIONEN. Mit dieser Funktion werden unsere
Methodenaufrufe (als Text) in der Konsole angezeigt. Erzeugen Sie jetzt
einige Objekte, rufen Sie einige ihrer Methoden auf und beobachten Sie die
Ausgabe im Konsolenfenster.

Mithilfe des Befehls METHODENAUFRUFE PROTOKOLLIEREN können wir erkennen, dass


die Folge, ein Personenobjekt zu erzeugen und seine Methoden sichtbarMachen
und nachRechtsBewegen aufzurufen, als Java-Code in Textform wie folgt aussieht:
Person person1 = new Person();
[Link]();
[Link]();
An diesem Text lassen sich einige Dinge beobachten:
 Wir sehen, wie der Code aussieht, wenn ein Objekt erzeugt und mit einen Namen
verbunden wird. Technisch gesehen speichern wir das Person-Objekt in einer
Variablen. Was dies genau bedeutet, besprechen wir ausführlich im nächsten
Kapitel.
 Wir sehen, dass wir für den Aufruf einer Methode an einem Objekt den Namen
des Objekts gefolgt von einem Punkt und dem Namen der Methode schreiben.
Der Befehl endet mit einer Parameterliste beziehungsweise einem leeren Paar
Klammern, wenn es keine Parameter gibt.
 Alle Java-Anweisungen enden mit einem Semikolon.
Doch wir wollen nicht nur Java-Anweisungen betrachten, sondern auch welche
eingeben. Hierzu verwenden wir die Direkteingabe. (Sie können jetzt die Funk-
tion METHODENAUFRUFE PROTOKOLLIEREN ausschalten und die Konsole schließen.)

Übung 1.11 Wählen Sie im Menü ANSICHT den Befehl DIREKTEINGABE ANZEIGEN
aus. Es sollte ein neues Fenster neben der Objektleiste in Ihrem BlueJ-Haupt-
fenster erscheinen, bei dem es sich um die Direkteingabe handelt. Hier kön-
nen Sie Ihren Java-Code eingeben.

40
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.10 Objektinteraktion

In der Direkteingabe können Sie Java-Code eingeben, der das Gleiche macht wie
der Code, den wir zuvor interaktiv erstellt haben.

Übung 1.12 Geben Sie in der Direkteingabe den obigen Code ein, um ein
Person-Objekt zu erzeugen und ihre Methoden sichtbarMachen und nach-
RechtsBewegen aufzurufen.

Das Eintippen dieser Befehle sollte die gleiche Wirkung haben wie der Aufruf des
gleichen Befehls aus dem Menü des Objekts. Wenn Sie stattdessen eine Fehlermel-
dung erhalten, dann haben Sie beim Befehl einen Tippfehler gemacht. Achten Sie
also auf Ihre Schreibweise. Sie werden feststellen, dass auch nur ein einziges fal-
sches Zeichen reicht, damit der Befehl nicht funktioniert

Tipp
Sie können in der Direkteingabe unter Verwendung des Aufwärts-Pfeils bereits
verwendete Befehle erneut aufrufen.

1.10 Objektinteraktion
Für den nächsten Abschnitt werden wir mit einem anderen Projekt arbeiten. Schlie-
ßen Sie das Projekt Figuren, falls Sie es noch geöffnet haben, und öffnen Sie das
Projekt Haus.

Übung 1.13 Öffnen Sie das Projekt Haus. Erzeugen Sie eine Instanz der Klasse
Bild und rufen Sie an ihr die Methode zeichne auf. Probieren Sie dann die
Methoden inSchwarzWeissAendern und inFarbeAendern aus.
Übung 1.14 Wie erzeugt Ihrer Meinung nach die Klasse Bild die Zeichnung?

Fünf der Klassen in diesem Projekt sind identisch mit den Klassen aus dem Projekt
Figuren. Hier haben wir eine zusätzliche Klasse: Bild. Diese Klasse ist so program-
miert, dass sie automatisch das tut, was Sie manuell in Übung 1.9 getan haben.
In der Praxis würden wir, wenn wir eine Folge von Aufgaben erledigt haben möch-
ten, dies nicht von Hand tun. Stattdessen würden wir eine Klasse erzeugen, die die
Arbeit für uns erledigt. Das genau macht die Klasse Bild.
Die Klasse Bild ist so geschrieben, dass durch den Aufruf der Methode zeichne
zwei Quadrat-Instanzen (eines für die Wand, eines für das Fenster) sowie ein Drei-
eck und ein Kreis erzeugt werden. Wenn anschließend die Methode zeichne aufge-
rufen wird, dann werden diese Instanzen so lange bewegt und in Farbe und Form
verändert, bis die Figuren so aussehen, wie in Abbildung 1.8 dargestellt.

41
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

Abbildung 1.8
Zwei Zeichnungen, die
mit einigen Figuren
erstellt wurden.

Konzept Die entscheidenden Punkte an dieser Stelle sind: Objekte können andere Objekte
erzeugen und sie können die Methoden dieser Objekte aufrufen. In einem norma-
Methoden-
len Java-Programm können es sehr schnell Hunderte oder Tausende von Objekten
aufrufe: Objekte
können miteinan- werden. Der Benutzer eines Programms startet es lediglich (indem üblicherweise
der kommunizieren, ein erstes Objekt erzeugt wird), und alle weiteren Objekte werden direkt oder indi-
indem sie gegen- rekt durch dieses Objekt erzeugt.
seitig ihre
Methoden Die große Frage ist nun: Wie schreiben wir die Klasse für solch ein Objekt?
aufrufen.

1.11 Quelltext
Konzept Jeder Klasse ist ein Quelltext zugeordnet. Der Quelltext ist ein Text, der die Details
einer Klasse beschreibt. In BlueJ kann der Quelltext einer Klasse betrachtet wer-
Der Quelltext
den, indem aus dem Kontextmenü der Klasse der Eintrag BEARBEITEN aufgerufen
einer Klasse legt
die Struktur und oder ein Doppelklick auf dem Klassensymbol ausgeführt wird.
das Verhalten (die
Datenfelder und die
Methoden) aller Übung 1.15 Aktivieren Sie das Kontextmenü der Klasse Bild. Sie sollten einen
Instanzen dieser Eintrag BEARBEITEN sehen – wählen Sie diesen aus. Dies startet einen Texteditor,
Klasse fest.
der den Quelltext der Klasse anzeigt.

Der Quelltext ist in der Programmiersprache Java formuliert. Er definiert, welche


Datenfelder und Methoden eine Klasse hat, und legt exakt fest, was passiert, wenn
eine Methode aufgerufen wird. Im nächsten Kapitel werden wir ausführlich darauf
eingehen, was der Quelltext einer Klasse umfasst und wie er strukturiert ist.
Um die Kunst des Programmierens zu erlernen, muss man in erster Linie lernen,
solche Definitionen von Klassen zu erstellen. Wir verwenden zu diesem Zweck die
Programmiersprache Java (obwohl es viele andere Programmiersprachen gibt, in
denen Programme geschrieben werden können).
Wenn Sie eine Änderung am Quelltext vornehmen und den Editor schließen,3
erscheint das Symbol für die Klasse im Diagramm gestreift. Die Streifen zeigen an,
dass der Quelltext geändert wurde. Die Klasse muss nun übersetzt werden, indem
der ÜBERSETZEN-Knopf gedrückt wird (falls Sie mehr darüber erfahren möchten, was

3 In BlueJ ist es nicht notwendig, vor dem Schließen des Editors den Text explizit zu speichern.
Beim Schließen wird der Quelltext automatisch gespeichert.

42
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.11 Quelltext

beim Übersetzen passiert, dann lesen Sie den Abschnitt Programmübersetzung).


Sobald die Klasse übersetzt ist, können Instanzen von ihr erzeugt werden, um an
ihnen die Änderungen auszuprobieren.

Programmübersetzung
Wenn Menschen Computerprogramme schreiben, benutzen sie typischerweise
eine „höhere“ Programmiersprache wie beispielsweise Java. Ein Problem dabei
ist, dass ein Computer den Quelltext von Java nicht direkt ausführen kann.
Java wurde entworfen, um einigermaßen lesbar für Menschen zu sein, nicht
für Computer. Computer arbeiten intern mit einer binären Repräsentation
eines Maschinencodes, der sich von Java erheblich unterscheidet. Das Problem
ist nun, dass für uns der Maschinencode so unleserlich ist, dass wir ihn nicht
direkt schreiben wollen. Wir schreiben lieber in Java. Was ist also zu tun?
Die Lösung ist ein spezielles Programm, der Compiler. Ein Compiler über-
setzt Java-Text in Maschinencode. Wir können Java schreiben, den Compiler
starten – dieser generiert den Maschinencode – und der Computer kann
den Maschinencode ausführen. Als Konsequenz ergibt sich, dass wir jedes
Mal, wenn wir den Quelltext ändern, zuerst den Compiler laufen lassen
müssen, bevor wir die Klasse zum Erzeugen einer Instanz benutzen können.
Andernfalls existiert nicht die Version des Maschinencodes, die der Compu-
ter braucht.

Übung 1.16 Finden Sie im Quelltext der Klasse Bild den Abschnitt, der das
eigentliche Zeichnen ausführt. Ändern Sie ihn so, dass die Sonne blau statt
gelb ist.
Übung 1.17 Fügen Sie der Zeichnung eine zweite Sonne hinzu. Dazu sollten
Sie sich die Definitionen der Datenfelder zu Beginn der Klassendefinition anse-
hen. Sie sehen folgendermaßen aus:
private Quadrat wand;
private Quadrat fenster;
private Dreieck dach;
private Kreis sonne;
An dieser Stelle kann eine Zeile für die zweite Sonne eingefügt werden. Bei-
spielsweise:
private Kreis sonne2;
Schreiben Sie dann auch die entsprechenden Anweisungen für die zweite
Sonne an zwei unterschiedlichen Stellen und machen Sie diese sichtbar, wenn
das Bild gemalt wird.

43
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

Übung 1.18 Zusatzaufgabe. (Dies heißt, dass diese Übung eventuell nicht
schnell zu lösen ist. Wir erwarten nicht, dass jeder diese Aufgabe zu diesem
Zeitpunkt lösen kann. Wenn Sie es schaffen – großartig. Wenn nicht, dann
ist das kein Problem. Die Dinge werden klarer werden, sobald Sie weiter-
gelesen haben. Kommen Sie später auf diese Übung zurück.) Fügen Sie der
Version von Bild mit nur einer Sonne einen Sonnenuntergang hinzu. Also:
Lassen Sie die Sonne langsam untergehen. Hinweis: Die Klasse Kreis hat
eine Methode langsamVertikalBewegen, die Sie zu diesem Zweck verwenden
können.
Übung 1.19 Zusatzaufgabe. Wenn Sie den Sonnenuntergang am Ende der
Methode zeichne eingefügt haben (sodass die Sonne automatisch unter-
geht, sobald das Bild gezeichnet ist), so ändern Sie dies nun. Für den Son-
nenuntergang soll eine eigene Methode sonnenuntergang definiert werden.
Auf diese Weise können wir zuerst die Methode zeichne aufrufen, um die
Zeichnung mit der Sonne zu sehen, und anschließend sonnenuntergang, um
die Sonne untergehen zu lassen.
Übung 1.20 Zusatzaufgabe. Erzeugen Sie eine Person, die nach dem Son-
nenuntergang auf das Haus zugeht.

1.12 Ein weiteres Beispiel


In diesem Kapitel haben wir bereits etliche neue Konzepte diskutiert. Um das Ver-
ständnis dieser Konzepte zu vertiefen, wollen wir sie noch einmal aus einem ande-
ren Zusammenhang heraus betrachten. Dazu verwenden wir ein anderes Beispiel.
Schließen Sie das Projekt Haus, falls Sie es noch geöffnet haben, und öffnen Sie
das Projekt Laborkurse.
Dieses Projekt ist ein vereinfachter Teil einer Studentendatenbank, die entworfen
wurde, um Studenten in Laborkursen zu verwalten und Kurslisten auszudrucken.

Übung 1.21 Erzeugen Sie eine Instanz der Klasse Student. Sie werden fest-
stellen, dass Sie nicht nur nach dem Namen für die Instanz gefragt werden,
sondern auch nach weiteren Parametern. Füllen Sie die Felder aus und kli-
cken Sie auf OK. (Zur Erinnerung: Parameter vom Typ String müssen mit
doppelten Anführungsstrichen angegeben werden.)

1.13 Aufrufergebnisse
Wie schon zuvor können Sie mehrere Instanzen erzeugen. Und wie zuvor haben
die Instanzen Methoden, die jeweils aus ihrem Kontextmenü heraus aufgerufen
werden können.

44
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.14 Objekte als Parameter

Übung 1.22 Erzeugen Sie einige Instanzen der Klasse Student. Rufen Sie die
Methode gibName an jeder Instanz auf. Erklären Sie, was passiert.

Wenn wir die Methode gibName aufrufen, bemerken wir etwas Neues: Methoden Konzept
können ein Ergebnis liefern. Tatsächlich sagt uns der Kopf einer Methode, ob diese
Ergebnis:
ein Ergebnis liefert oder nicht und von welchem Typ das Ergebnis ist. Der Kopf von
Methoden können
gibName (dargestellt im Kontextmenü einer Instanz) ist definiert als Informationen über
String gibName() ein Objekt durch
einen Ergebnis-
Das Wort String vor dem Namen der Methode spezifiziert den Ergebnistyp. In wert zurücklie-
diesem Fall wird ausgesagt, dass der Aufruf dieser Methode ein Ergebnis vom Typ fern.
String liefert. Die Signatur von nameAendern lautet:
void nameAendern(String neuerName)
Das Schlüsselwort void sagt an dieser Stelle, dass diese Methode kein Ergebnis
liefert.
Methoden mit einem Ergebnistyp ermöglichen es uns, durch ihren Aufruf Informati-
onen von einem Objekt abzufragen. Dies bedeutet, wir können Methoden entwe-
der benutzen, um den Zustand eines Objekts zu ändern oder um etwas über seinen
Zustand herauszufinden. Der Ergebnistyp einer Methode ist nicht Teil ihrer Signatur.

1.14 Objekte als Parameter

Übung 1.23 Erzeugen Sie eine Instanz der Klasse Laborkurs. Die Signatur be-
sagt dabei, dass Sie eine Maximalzahl für die Studenten in dem Kurs angeben
müssen (eine ganze Zahl).
Übung 1.24 Rufen Sie die Methode anzahlStudenten an dem Laborkurs auf.
Was tut sie?
Übung 1.25 Betrachten Sie die Signatur der Methode trageStudentEin. Sie
werden bemerken, dass der Typ des erwarteten Parameters Student ist. Sor-
gen Sie dafür, dass zwei oder drei Instanzen der Klasse Student und eine Ins-
tanz von Laborkurs auf der Objektleiste liegen. Rufen Sie dann die Methode
trageStudentEin an der Laborkurs-Instanz auf. Mit dem Eingabefokus im Ein-
gabefeld klicken Sie nun auf eine der Instanzen von Student – dies trägt den
Namen der Instanz in das Eingabefeld ein (Abbildung 1.9). Klicken Sie auf
OK und der Student ist in den Laborkurs eingetragen. Tragen Sie weitere
Studenten auf diese Weise ein.
Übung 1.26 Rufen Sie die Methode listeAusgeben an dem Laborkurs-Objekt
auf. Es erscheint eine Liste aller Kursteilnehmer auf der BlueJ-Konsole (Abbil-
dung 1.10).

45
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

Abbildung 1.9
Einen Studenten
in einen Laborkurs
eintragen.

Abbildung 1.10
Ausgabe der Teil-
nehmerliste einer
Laborkurs-Instanz.

Diese Übung zeigt, dass Objekte als Parameter an andere Objekte übergeben wer-
den können. Wenn eine Methode ein Objekt als Parameter erwartet, dann wird
der Name der Klasse des Objekts als Parametertyp in der Signatur angegeben.
Sehen Sie sich dieses Projekt etwas genauer an. Versuchen Sie, die Konzepte aus
dem Projekt Figuren in diesem Zusammenhang wiederzuerkennen.

Übung 1.27 Erzeugen Sie drei Studenten mit den folgenden Eigenschaften:
Schneewittchen, Matrikelnummer: 100234, Scheine: 4
Lisa Simpson, Matrikelnummer: 122044, Scheine: 9
Charlie Brown, Matrikelnummer 12003P, Scheine: 1
Tragen Sie die drei anschließend in einen Laborkurs ein und lassen Sie die
Kursliste ausgeben.
Übung 1.28 Verwenden Sie den Objektinspektor an dem Laborkurs, um he-
rauszufinden, welche Datenfelder er hat.
Übung 1.29 Setzen Sie für einen Laborkurs den Dozenten, den Raum und die
Anfangszeit fest. Überprüfen Sie anschließend in der Kursliste in der Konsole,
ob diese Informationen angezeigt werden.

46
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.14 Objekte als Parameter

Zusammenfassung
In diesem Kapitel haben wir die Grundlagen von Klassen und Objekten unter-
sucht. Wir haben gesehen, dass Objekte durch Klassen definiert werden. Klas-
sen repräsentieren das generelle Konzept eines Gegenstands, während Objekte
konkrete Instanzen einer Klasse repräsentieren. Wir können von jeder Klasse
beliebig viele Instanzen erzeugen.
Objekte haben Methoden, damit wir mit ihnen kommunizieren können. Wir
können eine Methode benutzen, um den Zustand eines Objekts zu ändern
oder um Informationen über ein Objekt zu bekommen. Methoden können
Parameter haben, und Parameter haben Typen. Methoden definieren einen
Ergebnistyp, der festlegt, was sie zurückliefern. Wenn der Ergebnistyp void
ist, dann liefern sie nichts zurück.
Objekte speichern Daten in ihren Datenfeldern (die auch Typen haben). Die
Werte, die in den Datenfeldern eines Objekts gehalten werden, definieren
seinen Zustand.
Objekte werden auf der Basis von Klassendefinitionen erzeugt, die in einer
bestimmten Programmiersprache formuliert sind. Der überwiegende Anteil
der Programmierung in Java beschäftigt sich mit dem Erstellen von Klassen-
definitionen. Ein großes Java-Programm besteht aus vielen Klassen, jede mit
vielen Methoden, die sich gegenseitig auf vielfältige Weise aufrufen.
Um Java-Programme schreiben zu können, müssen wir lernen, wie man
Klassendefinitionen mit Datenfeldern und Methoden schreibt und wie man
diese Klassen geeignet miteinander kombiniert. Der Rest dieses Buches wird
sich genau damit beschäftigen.

NEUE BEGRIFFE IN DIESEM KAPITEL


Objekt, Klasse, Instanz, Methode, Signatur, Parameter, Typ, Zustand, Quell-
text, Ergebniswert, Compiler

47
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen

Zusammenfassung der Konzepte


 Objekt Java-Objekte modellieren Objekte eines Anwendungsbereichs.
 Klasse Objekte werden durch Klassen erzeugt. Eine Klasse beschreibt eine
bestimmte Art von Objekten; Objekte repräsentieren individuelle Instanzen
einer Klasse.
 Methode Wir können mit Objekten kommunizieren, indem wir ihre Metho-
den aufrufen. Ein Objekt tut üblicherweise etwas, wenn eine seiner Metho-
den aufgerufen wird.
 Parameter Methoden können Parameter haben, mit denen zusätzliche
Informationen für eine Aufgabe angegeben werden.
 Signatur Der Name und die Parametertypen einer Methode werden als
deren Signatur bezeichnet. Sie benennt die benötigten Informationen für
einen Aufruf der Methode.
 Typ Parameter haben Typen. Ein Typ definiert, welche Arten von Werten
ein Parameter annehmen kann.
 Eine Klasse, viele Instanzen Von einer Klasse können viele gleichartige
Instanzen erzeugt werden.
 Zustand Objekte haben einen Zustand. Dieser Zustand wird durch Werte
repräsentiert, die in Datenfeldern gehalten werden.
 Methodenaufrufe Objekte können miteinander kommunizieren, indem
sie gegenseitig ihre Methoden aufrufen.
 Quelltext Der Quelltext einer Klasse legt die Struktur und das Verhalten
(die Datenfelder und die Methoden) aller Instanzen dieser Klasse fest.
 Ergebnis Methoden können Informationen über ein Objekt durch einen
Ergebniswert zurückliefern.

48
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
1.14 Objekte als Parameter

Übung 1.30 In diesem Kapitel haben wir die Datentypen int und String
erwähnt. Java hat einige weitere vordefinierte Datentypen. Finden Sie he-
raus, welche das sind und welchem Zweck sie dienen. Sie können dies in
Anhang B nachlesen oder in einem anderen Buch über Java oder in einem
Onlinehandbuch zu Java. Ein englisches Onlinehandbuch ist beispielsweise
zu finden unter:
[Link]
Übung 1.31 Welche Typen haben die folgenden Werte?
0
"hallo"
101
-1
true
"33"
3.1415
Übung 1.32 Was müssten Sie tun, um einem Kreis-Objekt ein neues Daten-
feld, beispielsweise name, hinzuzufügen?
Übung 1.33 Schreiben Sie die Signatur für eine Methode senden, die einen
Parameter vom Typ String bekommt und keinen Wert zurückliefert.
Übung 1.34 Schreiben Sie den Kopf für eine Methode mittelwert, die zwei
Parameter vom Typ int bekommt und einen int-Wert zurückliefert.
Übung 1.35 Sehen Sie sich das Buch an, das Sie gerade lesen. Ist es ein Objekt
oder eine Klasse? Wenn es eine Klasse ist, dann nennen Sie einige Objekte.
Wenn es ein Objekt ist, dann nennen Sie seine Klasse.
Übung 1.36 Kann ein Objekt verschiedene Klassen haben? Diskutieren Sie.

49
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

2 Klassendefinitionen

Lernziele
Zentrale Konzepte in diesem Kapitel: Datenfelder, sondierende und ver-
ändernde Methoden, Konstruktoren, Zuweisung und bedingte Anweisung,
Parameter
Java-Konstrukte in diesem Kapitel: Datenfeld, Konstruktor, Kommentar,
Parameter, Zuweisung (=), Block, return, void, zusammengesetzter Zuwei-
sungsoperator (+=, -=), if

In diesem Kapitel befassen wir uns erstmals gründlich mit dem Quelltext einer Klasse.
Wir werden die Basiselemente von Klassendefinitionen diskutieren: Datenfelder,
Konstruktoren und Methoden. Methoden enthalten Anweisungen. Wir werden
zuerst Methoden betrachten, die lediglich einfache arithmetische Anweisungen und
Ausgabeanweisungen enthalten. Später führen wir bedingte Anweisungen ein, mit
denen eine Auswahl zwischen verschiedenen Aktionen innerhalb einer Methode
getroffen werden kann.
Wir beginnen, indem wir uns detailliert ein neues Projekt ansehen. Dieses Projekt
enthält eine naive Implementierung eines Ticketautomaten. Während wir die ele-
mentaren Konzepte von Klassen einführen, werden wir schnell merken, dass diese
Implementierung einige Schwächen hat. Entsprechend werden wir anschließend
eine weiterentwickelte Version des Ticketautomaten beschreiben, die etliche Ver-
besserungen aufweist. Schließlich, um die Konzepte dieses Kapitels noch einmal zu
wiederholen, werfen wir einen Blick auf die interne Struktur der Laborkurse aus
dem letzten Kapitel.

2.1 Ticketautomaten
Auf Bahnhöfen und U-Bahnstationen stehen üblicherweise Ticketautomaten, die
ein Ticket ausdrucken, sobald ein Kunde den korrekten Betrag eingeworfen hat.
In diesem Kapitel werden wir eine Klasse definieren, die einen solchen Automaten
modelliert. Da wir hier erstmalig die Struktur einer Klasse untersuchen, ist die Simu-
lation selbst zu Anfang absichtlich simpel gehalten. Auf diese Weise können wir
untersuchen, inwieweit solche Modelle von ihren Originalen in der Realität abwei-

51
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

chen und wie wir unsere Klassen ändern müssen, damit sich die erzeugten Objekte
ihren realen Vorlagen annähern.
Unser Ticketautomat arbeitet folgendermaßen: Ein Kunde wirft Geld ein und for-
dert ein Ticket an. Ein Automat hält die Information darüber, wie viel Geld während
seines Betriebs eingeworfen wurde. Reale Ticketautomaten bieten üblicherweise
eine Auswahl an verschiedenen Tickets, von denen der Kunde eines auswählt.
Unser Ticketautomat hat nur eine Ticketart mit einem Einheitspreis. Es ist um einiges
anspruchsvoller, eine Klasse für einen Automaten zu schreiben, der verschiedene
Ticketarten unterstützt. Aber immerhin können wir in der objektorientierten Pro-
grammierung mehrere Instanzen erzeugen, die jeweils einen eigenen Preis definie-
ren, um so verschiedene Ticketarten zu unterstützen.

2.1.1 Das Verhalten des naiven Ticketautomaten


Konzept Öffnen Sie das Projekt Naiver-Ticketautomat in BlueJ. Dieses Projekt enthält nur
eine Klasse – Ticketautomat –, die Sie ähnlich wie in Kapitel 1 untersuchen kön-
Objekterzeu-
nen. Wenn Sie eine Instanz von Ticketautomat erzeugen, werden Sie nach dem
gung: Einige
Objekte können Preis gefragt, den ein Ticket des neuen Automaten kostet soll. Dieser Preis soll in
nur dann erzeugt Cent angegeben werden, also wäre eine Angabe wie 500 ein realistischer Wert.
werden, wenn
zusätzliche Infor-
mationen bereit- Übung 2.1 Erzeugen Sie eine Instanz von Ticketautomat und untersuchen
gestellt werden.
Sie ihre Methoden. Sie sollten folgende Methoden sehen: geldEinwerfen,
gibBisherGezahltenBetrag, gibPreis, ticketDrucken. Rufen Sie die Methode
gibPreis auf. Sie sollten einen Ergebniswert bekommen, der dem Wert ent-
spricht, der bei der Erzeugung des Automaten angegeben wurde. Benutzen
Sie die Methode geldEinwerfen, um das Einwerfen von Geld zu simulieren.
Der Automat speichert den jeweiligen Stand nach jedem Münzeinwurf.
Rufen Sie anschließend gibBisherGezahltenBetrag auf, um zu überprüfen, ob
der Automat den eingeworfenen Betrag genau registriert hat. Sie können
mehrere unterschiedliche Beträge einwerfen, genau wie an einem echten
Automaten. Werfen Sie den exakten Betrag für ein Ticket ein und stellen Sie
mit gibBisherGezahltenBetrag sicher, dass korrekt hochgezählt wurde. Da
dies ein simpler Automat ist, wird das Ticket nicht automatisch ausgewor-
fen. Also rufen Sie die Methode ticketDrucken auf, sobald genügend Geld
eingeworfen wurde. In der BlueJ-Konsole sollte nun der Ausdruck für ein
Ticket erscheinen.
Übung 2.2 Welcher Wert wird geliefert, wenn Sie den bereits bezahlten Betrag
ausgeben lassen, nachdem das Ticket gedruckt wurde?
Übung 2.3 Experimentieren Sie mit unterschiedlichen Beträgen, bevor Sie
das Ticket drucken lassen. Verhält sich der Automat in irgendeiner Weise
eigenartig? Was passiert, wenn Sie zu viel Geld einwerfen – bekommen Sie
das Wechselgeld zurück? Was passiert, wenn Sie nicht genügend Geld ein-
werfen und dann versuchen, ein Ticket auszudrucken?

52
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.2 Eine Klassendefinition untersuchen

Übung 2.4 Versuchen Sie, das Verhalten des Automaten möglichst exakt zu
verstehen, indem Sie mit der Instanz auf der Objektleiste interagieren, bevor
wir uns im nächsten Abschnitt genauer ansehen, wie die Klasse Ticketautomat
implementiert ist.
Übung 2.5 Erzeugen Sie einen weiteren Ticketautomaten, der einen ande-
ren Preis verlangt. Denken Sie daran, dass Sie diesen Wert beim Erzeugen
der Automaten-Instanz angeben müssen. Kaufen Sie an diesem Automaten
ein Ticket. Sieht dieses Ticket anders aus als die Tickets, die von dem ersten
Automaten ausgegeben werden?

2.2 Eine Klassendefinition untersuchen


Die Übungen am Ende des vorherigen Abschnitts zeigen, dass sich die Objekte der
Klasse Ticketautomat nur dann sinnvoll verhalten, wenn wir exakt den geforderten
Ticketpreis einwerfen. Wenn wir uns nun das Innenleben der Klasse ansehen, wer-
den wir verstehen, warum das so ist.
Betrachten Sie den Quelltext der Klasse Ticketautomat, indem Sie einen Doppel-
klick auf deren Symbol im Klassendiagramm ausführen. Er sollte aussehen wie in
Abbildung 2.1.

Abbildung 2.1
Das Editorfenster
von BlueJ.

53
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Der komplette Quelltext ist in Listing 2.1 zu sehen. Indem wir diesen Text Schritt für
Schritt untersuchen, können wir die objektorientierten Konzepte herausschälen, die
wir in Kapitel 1 bereits besprochen haben. Die im Quelltext definierte Klasse enthält
etliche Elemente der Sprache Java, die uns wieder und wieder begegnen werden,
sodass sich das gründliche Studium dieser Klasse auf jeden Fall auszahlt.

Listing 2.1
Die Klassendefinition
der Klasse
Ticketautomat.

54
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.3 Der Kopf der Klasse

2.3 Der Kopf der Klasse


Der Quelltext einer Klasse kann in zwei Hauptbestandteile unterteilt werden: eine
kleine äußere Klammer, die die Klasse benennt (und auf einem grünen Hintergrund
dargestellt wird), und den meist viel umfangreicheren Innenteil, der die ganze
Arbeit erledigt. In diesem Fall sieht die äußere Klammer folgendermaßen aus:
public class Ticketautomat
{
Innenteil der Klasse hier ausgelassen
}
Die äußere Klammer sieht bei allen Klassen mehr oder weniger gleich aus; sie ent-
hält den Kopf der Klasse, dessen Hauptzweck darin besteht, die Klasse zu benen-
nen. Gemäß einer weitverbreiteten Konvention beginnen wir Klassennamen immer
mit einem Großbuchstaben. Konsequent angewendet erlaubt uns diese Konven-
tion, Klassennamen leicht von anderen Bezeichnungen zu unterscheiden – also z.B.
von Variablen- oder Methodennamen, auf die wir gleich noch eingehen werden.
Oberhalb des Kopfes der Klasse befindet sich ein Kommentar (in blauer Schrift),
der uns Informationen über die Klasse gibt.

Übung 2.6 Schreiben Sie auf, wie die äußeren Klammern der Klassen Student
und Laborkurs vermutlich aussehen – kümmern Sie sich nicht um den Innen-
teil.
Übung 2.7 Macht es einen Unterschiede, ob wir
public class Ticketautomat
oder
class public Ticketautomat

55
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

in der äußeren Klammer einer Klasse schreiben? Editieren Sie den Quelltext
der Klasse Ticketautomat auf diese Weise und schließen Sie dann das Editor-
fenster. Stellen Sie einen Unterschied im Klassendiagramm fest?
Welche Fehlermeldung erhalten Sie, wenn Sie nun den Knopf ÜBERSETZEN kli-
cken? Glauben Sie, dass diese Fehlermeldung klar erläutert, was falsch ist?
Nehmen Sie Ihre Änderung wieder zurück und achten Sie darauf, dass der
Fehler nach dem Übersetzen nicht mehr auftaucht.
Übung 2.8 Überprüfen Sie, ob es möglich ist, das Wort public in der äuße-
ren Klammer der Klasse Ticketautomat wegzulassen.
Übung 2.9 Fügen Sie das Wort public wieder ein und prüfen Sie dann, ob es
möglich ist, das Wort class wegzulassen, bevor Sie den Code erneut überset-
zen. Achten Sie darauf, beide Wörter wieder wie ursprünglich vorgegeben in
den Quelltext einzutragen, bevor Sie mit der Lektüre dieses Kapitels fortfahren.

2.3.1 Schlüsselwörter
Die Wörter „public“ und „class“ sind Teil der Programmiersprache Java, während
das Wort „Ticketautomat“ ein bestimmter Name ist, der von dem Autor der Klasse
frei gewählt wurde. Wir nennen Wörter wie „public“ und „class“ Schlüsselwörter
oder reservierte Wörter. Diese Begriffe tauchen häufig auf und bedeuten das Glei-
che. Es gibt in Java ungefähr 50 Schlüsselwörter und Sie werden schon bald die
meisten davon kennen. Ein Punkt, auf den wir Sie noch hinweisen möchten, ist,
dass Java-Schlüsselwörter niemals Großbuchstaben enthalten, während die Wör-
ter, die wir selbst wählen (wie „Ticketautomat“) oftmals eine Mischung aus Groß-
und Kleinbuchstaben darstellen.

2.4 Datenfelder, Konstruktoren


und Methoden
Im Innenteil einer Klasse werden die Datenfelder, Konstruktoren und Methoden
definiert, die den Instanzen der Klasse ihre speziellen Eigenschaften und ihr Ver-
halten geben. Wir können die grundlegenden Aufgaben dieser drei Komponenten
einer Klasse folgendermaßen zusammenfassen:
 Die Datenfelder speichern die Daten, die das jeweilige Objekt benutzt.
 Die Aufgabe der Konstruktoren ist es sicherzustellen, dass ein neu erzeugtes
Objekt in einen vernünftigen Anfangszustand gesetzt wird.
 Die Methoden implementieren das Verhalten eines Objekts; sie liefern seine
Funktionalität.
In BlueJ werden Datenfelder als Text auf einem weißen Hintergrund dargestellt,
während Konstruktoren und Methoden innerhalb eines gelben Kastens darge-
stellt sind.

56
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.4 Datenfelder, Konstruktoren und Methoden

In Java gibt es nur wenige Regeln über die Reihenfolge, in der diese Elemente in
einer Klassendefinition aufgeführt werden sollten. In der Klasse Ticketautomat haben
wir uns entschieden, zuerst die Felder, dann die Konstruktoren und schließlich die
Methoden aufzuführen (Listing 2.2). Diese Reihenfolge werden wir in allen unse-
ren Beispielen einhalten. Andere Autoren bevorzugen eine andere Reihenfolge,
und letztlich ist das eine Frage des Geschmacks. Unsere Konvention ist nicht
unbedingt besser als andere. Dennoch ist es wichtig, sich auf eine Konvention zu
einigen und diese dann konsequent durchzuhalten, weil Ihre Klassen dann einfa-
cher zu lesen und zu verstehen sind.

Listing 2.2
Unsere Reihenfolge für
Datenfelder, Konstruk-
toren und Methoden.

Übung 2.10 Durch das Experimentieren mit dem Ticketautomaten in BlueJ


erinnern Sie sich vielleicht noch an die Namen einiger Methoden – beispiels-
weise ticketDrucken. Sehen Sie sich die Klassendefinition in Listing 2.3 an
und benutzen Sie diese Information, zusammen mit den Hinweisen zur Rei-
henfolge, die wir oben gegeben haben, um eine Liste der Namen der Daten-
felder, Konstruktoren und Methoden der Klasse Ticketautomat zu erstellen.
Hinweis: Es gibt nur einen Konstruktor in der Klasse.
Übung 2.11 Welche zwei Eigenheiten des Konstruktors unterscheiden ihn
signifikant von den Methoden einer Klasse?

2.4.1 Datenfelder
Datenfelder speichern Daten dauerhaft in einem Objekt. Die Klasse Ticketautomat Konzept
hat drei Datenfelder: preis, bisherGezahlt, gesamtsumme. Diese Datenfelder werden
Datenfelder
auch als Instanzvariablen bezeichnet, da der Begriff Variable ein allgemeiner Aus-
speichern die Daten,
druck für Dinge ist, die Daten in einem Programm speichern. Wir haben sie gleich zu die ein Objekt
Anfang der Klassendefinition aufgeführt (Listing 2.3). Alle diese Variablen modellie- benutzt. Datenfel-
ren Geldbeträge, mit denen der Ticketautomat umgehen muss: der werden auch
 preis ist ein Datenfeld für den festgelegten Preis eines Tickets. als Instanzvariablen
bezeichnet.
 bisherGezahlt hält den Betrag, den der Benutzer bereits in den Automaten
gesteckt hat, bevor er sich ein Ticket drucken lässt.
 gesamtsumme speichert die Summe aller Geldbeträge, die in den Automaten seit
seiner Erzeugung von verschiedenen Benutzern eingeworfen wurden (ohne
Berücksichtigung des aktuellen Betrags im Feld bisherGezahlt). Die Idee dahinter
ist, dass nach Ausgabe eines Tickets der dafür gezahlte Betrag zu der Gesamt-
summe hinzuaddiert wird.

57
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Listing 2.3
Die Datenfelder
der Klasse
Ticketautomat.

Datenfelder sind kleine Bereiche innerhalb eines Objekts, in denen Daten dauerhaft
gespeichert werden können. Jedes Objekt reserviert bei seiner Erzeugung Platz für
jedes Datenfeld, das in seiner Klasse deklariert wurde. Abbildung 2.2 zeigt schema-
tisch eine Instanz der Klasse Ticketautomat mit ihren drei Datenfeldern. Den Daten-
feldern sind noch keine Werte zugewiesen; sobald sie Werte haben, können wir
den jeweiligen Wert an der Stelle eintragen, die das entsprechende Datenfeld reprä-
sentiert. Die Darstellung hier ist ähnlich zu der eines Objekts auf der Objektleiste in
BlueJ, enthält jedoch mehr Informationen. In BlueJ werden aus Platzgründen die
Datenfelder bei den Objekten auf der Objektleiste nicht angezeigt. Wir können sie
aber betrachten, wenn wir den Objektinspektor öffnen (Kapitel 1.7).

Abbildung 2.2
Ein Objekt der Klasse
Ticketautomat.

Jedes Datenfeld hat seine eigene Deklaration im Quelltext. Vor jeder Deklaration
haben wir eine Textzeile – einen Kommentar – eingefügt, der dem Leser den Zweck
des Datenfelds beschreiben soll:
// Der Preis eines Tickets dieses Automaten.
private int preis;

Konzept Ein einzeiliger Kommentar wird durch die beiden Zeichen // eingeleitet, zwischen
denen kein Leerzeichen stehen darf. Ausführlichere Kommentare, die sich über
Kommentare
mehrere Zeilen erstrecken, werden üblicherweise als Mehrzeilenkommentare for-
werden im Quell-
text einer Klasse matiert. Diese beginnen mit dem Zeichenpaar /* und enden mit dem Paar */. Ein
angegeben, um gutes Beispiel finden Sie vor dem Kopf der Klasse in Listing 2.1.
menschlichen
Die Definitionen der drei Datenfelder sind sehr ähnlich:
Lesern das Verste-
hen des Codes zu  Alle Definitionen besagen, dass sie private Datenfelder des Objekts sind; darauf
erleichtern. Kom- werden wir in Kapitel 6 noch ausführlich eingehen, aber vorläufig belassen wir
mentare haben es bei der Regel, dass wir Datenfelder immer als privat vereinbaren.
keinen Einfluss auf
 Alle drei Datenfelder sind vom Typ int. int ist ein weiteres Schlüsselwort, das
die Funktionalität
einer Klasse.
den Datentyp Integer repräsentiert. Dies besagt, dass jedes Datenfeld einen ganz-
zahligen Wert speichern kann. Das ist sinnvoll, da wir ja Zahlen speichern wollen,
die Geldbeträge in Cent darstellen.

58
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.4 Datenfelder, Konstruktoren und Methoden

Datenfelder können zu verschiedenen Zeitpunkten verschiedene Werte enthalten,


deshalb werden sie auch Variablen genannt. Im Bedarfsfall können wir den Anfangs-
wert, der in einem Datenfeld gespeichert ist, ändern. Wenn beispielsweise im Lauf
der Zeit immer mehr Geld in den Ticketautomaten eingeworfen wird, dann werden
wir den Wert im Datenfeld für die Gesamtsumme anpassen wollen. Es ist üblich,
Datenfelder zu haben, deren Werte sich häufig ändern (wie bisherGezahlt und
gesamtsumme), und solche, die sich nur selten oder gar nicht ändern (wie preis). Die
Tatsache, dass sich der Wert von preis nach dem Setzen nicht mehr ändert, ändert
nichts daran, dass es sich dennoch um eine Variable handelt. In den folgenden
Abschnitten werden wir noch andere Arten von Variablen kennenlernen, die jedoch
alle grundsätzlich den gleichen Zweck erfüllen, nämlich Daten zu speichern.
Die Datenfelder preis, bisherGezahlt und gesamtsumme sind Bestandteile eines
Objekts der Klasse Ticketautomat, die es benötigt, um seine Aufgaben erfüllen zu
können: Geld vom Kunden annehmen, Tickets drucken und eine Übersicht
behalten, wie viel Geld bereits in den Automaten gezahlt wurde. In den nächsten
Abschnitten werden wir untersuchen, wie diese Datenfelder vom Konstruktor und
den Methoden benutzt werden, um das Verhalten von naiven Ticketautomaten zu
implementieren.

Übung 2.12 Welchen Typ haben jeweils die folgenden Datenfelder Ihrer Mei-
nung nach?
private int zaehler;
private Student sprecher;
private Server zentral;
Übung 2.13 Was sind die Namen der folgenden Datenfelder?
private boolean lebendig;
private Person tutor;
private Spiel spiel;
Übung 2.14 Beurteilen Sie, aufbauend auf Ihren Kenntnissen der Namens-
konventionen für Klassen, welche der Typnamen in den Übungen 2.12 und
2.13 Klassennamen sind.

Übung 2.15 Ist es bei der folgenden Deklaration eines Datenfeldes aus der
Klasse Ticketautomat
private int preis;
wichtig, in welcher Reihenfolge die drei Wörter stehen? Editieren Sie die
Klasse Ticketautomat und probieren Sie verschiedene Reihenfolgen aus. Gibt
Ihnen das Aussehen des Klassendiagramms nach jeder Änderung einen Hin-
weis, ob andere Reihenfolgen möglich sind? Überprüfen Sie durch Klicken
auf ÜBERSETZEN, ob es eine Fehlermeldung gibt.
Stellen Sie sicher, dass nach Ihren Experimenten die Originalversion wieder-
hergestellt wird!

59
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.16 Ist es zwingend erforderlich, dass am Ende einer Datenfeld-


deklaration ein Semikolon steht? Experimentieren Sie auch hier mit dem Edi-
tor. Die Regel, die Sie dabei lernen, ist sehr wichtig, deshalb sollten Sie sich
diese einprägen.
Übung 2.17 Schreiben Sie die vollständige Deklaration eines Datenfeldes
mit dem Typ int und dem Namen status.

Aus den Definitionen der Datenfelder, die wir bisher kennengelernt haben, schält
sich ein Muster heraus, das sich auf jede Definition einer Datenfeldvariablen in einer
Klasse anwenden lässt.
 Sie beginnen normalerweise mit dem reservierten Wort private.
 Sie enthalten einen Typnamen (wie int, String, Person usw.).
 Sie enthalten einen vom Benutzer gewählten Namen für die Datenfeldvariable.
 Sie enden mit einem Semikolon.
Merken Sie sich diese vier Punkte. Sie werden Ihnen später, wenn Sie Ihre eigenen
Klassen schreiben, eine wertvolle Hilfe sein.
Wenn Sie sich die Mühe machen, den Quelltext verschiedener Klassen eingehender
zu studieren, werden Sie immer wieder auf Muster wie diese stoßen. Teil des Lern-
prozesses beim Programmieren ist es, nach solchen Mustern Ausschau zu halten
und sie dann in den eigenen Programmen zu verwenden. Das ist einer der Gründe,
warum die genaue Analyse von Quelltext in diesem Stadium so nützlich ist.

2.4.2 Konstruktoren
Konzept Konstruktoren haben eine ganz besondere Aufgabe: Sie sind verantwortlich dafür,
dass ein Objekt unmittelbar nach seiner Erzeugung in einen gültigen Zustand ver-
Konstruktoren
setzt wird, oder, um es anders auszudrücken, dass ein Objekt direkt nach seiner
ermöglichen, dass
ein Objekt nach Erzeugung verwendet werden kann. Dieser Prozess wird auch als Initialisierung
seiner Erzeugung bezeichnet.
in einen gültigen
In gewisser Hinsicht kann ein Konstruktor mit einer Hebamme verglichen wer-
Zustand versetzt
wird. den: Er ist dafür verantwortlich, dass das neue Objekt ordentlich ins Leben geru-
fen wird. Sobald ein Objekt erzeugt worden ist, spielt der Konstruktor im Leben
des Objekts keine weitere Rolle mehr und kann auch nicht noch einmal aufge-
rufen werden. Listing 2.4 zeigt den Konstruktor der Klasse Ticketautomat.
Eines der besonderen Merkmale eines Konstruktors ist, dass er genauso heißt
wie die Klasse, in der er definiert ist – in diesem Fall Ticketautomat. Der Name des
Konstruktors folgt direkt, d.h. ohne etwas dazwischen, auf das Wort public.1
Erwartungsgemäß sollte es eine enge Verbindung geben zwischen dem, was im
Rumpf eines Konstruktors geschieht und was in den Datenfelder der Klasse passiert.

1 Diese Behauptung vereinfacht geringfügig die entsprechende Java-Regel, ist aber für die Mehr-
zahl der Codebeispiele in diesem Buch gültig.

60
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.4 Datenfelder, Konstruktoren und Methoden

Der Grund ist, dass eine der Hauptaufgaben des Konstruktors darin besteht, die
Datenfelder zu initialisieren. Einige Datenfelder, wie beispielsweise bisherGezahlt
und gesamtsumme, können einen sinnvollen Wert bekommen, indem ihnen eine kons-
tante Zahl, in diesem Fall die Null, zugewiesen wird. Mit anderen, wie hier dem
Ticketpreis, ist das nicht ganz so einfach, weil wir den Preis eines Tickets nicht ken-
nen, bevor wir den Automaten erzeugt haben: Erinnern Sie sich, dass wir mögli-
cherweise mehrere Ticketautomaten mit unterschiedlichen Ticketpreisen haben
möchten, sodass kein fester Preis für alle festgelegt werden kann. Beim Erzeugen
von Ticketautomaten in BlueJ haben Sie ja festgestellt, dass Sie für jeden neuen
Ticketautomaten den Preis der Tickets festlegen mussten. Ein wichtiger Punkt an
dieser Stelle ist, dass der Preis eines Tickets an einer Stelle außerhalb des Ticketauto-
maten festgelegt wird und dass diese Information in den Automaten hineinge-
geben werden muss. In BlueJ entscheiden Sie sich für einen Wert und geben diesen
in ein Dialogfeld ein. Eine Aufgabe des Konstruktors ist, diesen Wert entgegenzu-
nehmen und ihn im Datenfeld preis des neu erzeugten Ticketautomaten abzu-
legen. Auf diese Weise kann der Automat den Wert behalten, ohne dass Sie ihn
immer wieder daran erinnern müssen.

Listing 2.4
Der Konstruktor
der Klasse
Ticketautomat.

Wir sehen daran, dass eine der wichtigsten Aufgaben eines Datenfelds das Spei-
chern von externen Informationen ist, damit diese Informationen dem Objekt
über seine gesamte Lebensdauer zur Verfügung stehen können. Datenfelder bie-
ten somit einen Ort, um Daten dauerhaft (d.h. persistent) zu speichern.

Abbildung 2.3
Eine Instanz der Klasse
Ticketautomat nach
ihrer Initialisierung
(erzeugt mit einem
Ticketpreis von
500 Cent).

61
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Abbildung 2.3 zeigt eine Instanz der Klasse Ticketautomat, nachdem der Konstruk-
tor ausgeführt wurde. Den Datenfeldern wurden nun Werte zugewiesen. Dem Dia-
gramm können wir entnehmen, dass dieser Automat mit einem Ticketpreis von 500
erzeugt worden ist.
Im nächsten Abschnitt diskutieren wir, wie ein Objekt Werte von außen entge-
gennehmen kann.

Hinweis
In Java werden alle Datenfelder automatisch mit einem vordefinierten Wert
belegt, falls sie nicht explizit gesetzt werden. Für Datenfelder vom Typ int ist
dieser Standardwert 0. Genau genommen hätten wir also ohne die Initialisie-
rung von bisherGezahlt und gesamtsumme mit 0 auskommen können, indem
wir uns auf die Standardinitialisierung mit gleichem Ergebnis verlassen hät-
ten. Wir bevorzugen aber die explizite Zuweisung. Sie hat keinen Nachteil,
dokumentiert jedoch deutlicher, was tatsächlich passiert. Wir verlassen uns
zum einen nicht darauf, dass der Leser die Standardwerte kennt. Zum ande-
ren dokumentieren wir, dass die Datenfelder tatsächlich den Wert 0 bekom-
men sollen und wir nicht einfach vergessen haben, sie zu initialisieren.

2.5 Datenübergabe mit Parametern


Konstruktoren und Methoden spielen ganz unterschiedliche Rollen im Leben eines
Objekts, aber die Art und Weise, wie beide Werte von außen erhalten, ist die glei-
che: über Parameter. Vielleicht erinnern Sie sich daran, dass wir Parameter bereits
kurz in Kapitel 1 (Abschnitt 1.4) kennengelernt haben. Parameter sind, wie Daten-
felder, lediglich eine andere Form von Variablen und dienen somit der Aufnahme
und Verwahrung von Daten. Parameter sind Variablen, die im Kopf eines Konstruk-
tors oder einer Methode definiert werden:
public Ticketautomat(int ticketpreis)
Dieser Konstruktor hat einen einzelnen Parameter, ticketpreis, der vom Typ int
ist – vom gleichen Typ wie das Datenfeld preis, dem dieser Wert zugewiesen wer-
den soll. Ein Parameter wird als eine Art temporärer Bote verwendet, der Daten
außerhalb des Konstruktors bzw. der Methode entgegennimmt und in den Kon-
struktor bzw. die Methode hineinreicht, sodass sie dort zur Verfügung stehen.
Abbildung 2.4 illustriert, wie Werte als Parameter übergeben werden. In diesem Fall
gibt ein Benutzer von BlueJ einen Wert in ein Dialogfeld ein, wenn er einen Ticket-
automaten erzeugt (links angedeutet). Dieser Wert wird in den Parameter ticket-
preis des Konstruktors des neuen Automaten kopiert. Dies ist in der Abbildung
durch den Pfeil (A) gekennzeichnet. Der Kasten in dem Ticketautomat-Objekt in
Abbildung 2.4 mit der Aufschrift „Ticketautomat (Konstruktor)“ stellt zusätzlichen
Platz dar, der nur während der Ausführung des Konstruktors zur Verfügung steht.
Wir nennen ihn hier den Konstruktorspeicher des Objekts (oder Methodenspei-
cher, wenn wir über Methoden sprechen, denn bei diesen verhält es sich analog).

62
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.5 Datenübergabe mit Parametern

Der Konstruktorspeicher bietet Platz für die Werte der Konstruktorparameter. In


unseren Diagrammen werden alle Variablen durch weiße Felder repräsentiert.
Wir unterscheiden zwischen den Parameternamen innerhalb eines Konstruktors
(oder einer Methode) und den Parameterwerten außerhalb, indem wir die Namen
innerhalb formale Parameter und die Werte von außerhalb aktuelle Parameter nen-
nen. Demnach ist beispielsweise ticketpreis ein formaler Parameter und ein Wert
wie 500 ist ein aktueller Parameter.

Abbildung 2.4
Parameterübergabe
(A) und Zuweisung (B).

Konzept
Durch die Sicht-
barkeit einer
Ein formaler Parameter steht einem Objekt nur im Rumpf des Konstruktors oder Variablen wird der
der Methode zur Verfügung, in dem der Parameter deklariert ist. Wir sagen, dass Bereich im Quell-
die Sichtbarkeit eines Parameters auf den jeweiligen Rumpf beschränkt ist. Im text definiert, von
dem aus auf eine
Gegensatz dazu ist der Sichtbarkeitsbereich eines Datenfelds die gesamte Klas-
Variable zugegrif-
sendefinition – es kann an allen Stellen in einer Klassendefinition benutzt wer- fen werden kann.
den. Dies ist ein sehr wichtiger Unterschied zwischen diesen beiden Arten von
Variablen.
Ein Konzept, das mit der Sichtbarkeit einer Variablen eng verwandt ist, ist das der Konzept
Lebensdauer. Die Lebensdauer eines formalen Parameters ist auf die Ausfüh-
Die Lebens-
rungszeit eines Konstruktors oder einer Methode beschränkt. Bei Aufruf eines
dauer einer Varia-
Konstruktors oder einer Methode wird zusätzlicher Speicher für die Parameter- blen legt fest, wie
variablen erzeugt und die externen Werte dort hinein kopiert. Nach der Ausfüh- lange sie existiert,
rung verschwinden die formalen Parameter und ihre Werte gehen verloren. Mit bevor sie zerstört
anderen Worten: Nach Ausführung des Konstruktors wird der Konstruktorspei- wird.
cher freigeräumt und mit ihm werden alle darin gehaltenen Parameterwerte ent-
fernt (Abbildung 2.4).
Im Gegensatz dazu entspricht die Lebensdauer eines Datenfelds der des zugehöri-
gen Objekts. Zusammen mit dem Objekt werden die Datenfelder erzeugt und sie
bestehen, solange das Objekt besteht. Daher müssen wir, wenn wir uns den im Para-
meter ticketpreis gehaltenen Ticketpreis merken wollen, diesen an einem beständi-
geren Ort abspeichern – im Datenfeld preis.

63
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Ebenso wie wir eine enge Verbindung zwischen einem Konstruktor und den Daten-
feldern seiner Klasse erwarten, erwarten wir eine enge Verbindung zwischen den
Parametern des Konstruktors und den Datenfeldern, da externe Werte oft benötigt
werden, um die Werte von einem oder mehreren dieser Datenfelder zu setzen. Wo
dies der Fall ist, stimmen die Parametertypen sehr eng mit den Typen der entspre-
chenden Datenfelder überein.

Übung 2.18 Zu welcher Klasse gehört der folgende Konstruktor?


public Student(String name)
Übung 2.19 Wie viele Parameter hat der folgende Konstruktor und welche
Typen haben diese?
public Buch(String titel, double preis)
Übung 2.20 Können Sie die Typen der Datenfelder in der Klasse Buch erra-
ten – und zwar aus den Parametern ihres Konstruktors? Können Sie etwas
über die Namen dieser Datenfelder sagen?

2.5.1 Variablennamen wählen


Sicher ist Ihnen aufgefallen, dass die Variablennamen, die wir für Datenfelder und
Parameter verwenden, eng mit dem Zweck der Variablen verbunden sind. Namen
wie preis, ticketpreis, titel und lebend verraten Ihnen etwas über die Informatio-
nen, die in dieser Variablen gespeichert sind. Das macht es leichter zu verstehen,
was im Programm abläuft. In Anbetracht der Tatsache, dass wir relativ frei in unserer
Wahl des Variablennamens sind, lohnt es sich, das Prinzip zu beherzigen, Namen zu
wählen, die den Zweck der Variablen kommunizieren, anstatt nur beliebig Buchsta-
ben und Zahlen aneinanderzureihen.

2.6 Zuweisungen
Im vorigen Abschnitt haben wir angemerkt, dass der kurzlebige Wert einer Parame-
tervariablen zu einer beständigeren Stelle kopiert werden sollte – zu einer Daten-
feldvariablen. Der Rumpf des Konstruktors enthält deshalb die folgende Zuweisung:
preis = ticketpreis;

Konzept Zuweisungen werden beim Programmieren häufig eingesetzt, um einen Wert in


einer Variablen zu speichern. Sie sind an einem Zuweisungsoperator erkennbar,
Zuweisungen
im obigen Beispiel =. Zuweisungen sind spezielle Anweisungen, die den Wert auf
speichern den Wert
auf der rechten der rechten Seite des Operators nehmen und ihn in der Variablen speichern, die
Seite eines Zuwei- auf der linken Seite des Operators steht. In Abbildung 2.4 ist dies durch den Pfeil
sungsoperators in (B) gekennzeichnet. Die rechte Seite einer Zuweisung wird Ausdruck genannt. In
der Variablen, die ihrer allgemeinsten Form sind Ausdrücke Konstrukte, die Werte berechnen. In
auf der linken Seite diesem Fall besteht der Ausdruck lediglich aus einer einzelnen Variablen, deren
genannt ist. Wert in die Variable preis kopiert wird. Weiter hinten in diesem Kapitel werden
wir noch kompliziertere Ausdrücke zu sehen bekommen.

64
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.7 Methoden

Eine Regel für Zuweisungen ist, dass der Typ des Ausdrucks auf der rechten Seite
zum Typ der Variablen auf der linken Seite passen muss, an die der Wert zugewiesen
wird. Bisher haben wir nur drei verschiedene Typen kennengelernt: int, String und
(nur sehr kurz) boolean. Die Regel besagt, dass wir beispielsweise keinen ganzzahli-
gen Wert an eine String-Variable zuweisen können. Die gleiche Regel gilt auch zwi-
schen einem formalen und einem aktuellen Parameter: Der Typ des aktuellen Para-
meters muss zum Typ der formalen Parametervariablen passen. Vorläufig können wir
sagen, dass die Typen gleich sein müssen, obwohl wir in späteren Kapiteln sehen
werden, dass dies nicht immer der Fall ist.

Übung 2.21 Nehmen Sie an, in einer Klasse Haustier gibt es ein Datenfeld
name vom Typ String. Schreiben Sie eine Zuweisung in den Rumpf des folgen-
den Konstruktors, sodass das Datenfeld name mit dem Wert des Konstruktor-
parameters initialisiert wird.
public Haustier(String seinName)
{
}
Übung 2.22 Zusatzaufgabe. Die folgende Objekterzeugung führt dazu, dass
der Konstruktor der Klasse Datum aufgerufen wird. Können Sie den Kopf des
Konstruktors aufschreiben?
new Datum("Maerz", 23, 1861);
Versuchen Sie, den Parametern bedeutungsvolle Namen zu geben.

2.7 Methoden
Die Klasse Ticketautomat hat vier Methoden: gibPreis, geldEinwerfen, gibBisher-
GezahltenBetrag und ticketDrucken. Im Quelltext der Klasse (Listing 2.1) sehen Sie
die Methoden auf gelbem Hintergrund dargestellt. Wir beginnen unsere Untersu-
chung des Quelltextes von Methoden, indem wir uns gibPreis näher ansehen (Lis-
ting 2.5).

Listing 2.5
Die Methode
gibPreis.

65
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Konzept Methoden bestehen aus zwei Teilen: einem Kopf und einem Rumpf. Der Kopf der
Methode gibPreis einschließlich eines vorangestellten Kommentars sieht folgen-
Methoden
dermaßen aus:
bestehen aus zwei
Teilen: einem Kopf /**
und einem Rumpf. * Liefere den Preis eines Tickets dieses Automaten (in Cent).
*/
public int gibPreis()
Es ist wichtig, zwischen dem Kopf einer Methode und der Deklaration eines
Datenfelds zu unterscheiden, da sich beide sehr ähnlich sehen können. Dass gib-
Preis eine Methode und kein Datenfeld ist, können wir an dem Klammerpaar ()
erkennen, denn der Kopf einer Methode enthält immer ein solches Klammer-
paar. Beachten Sie auch, dass am Ende des Kopfes kein Semikolon steht.
Der Rumpf der Methode ist alles das, was auf den Kopf folgt. Er ist immer in ein
Paar geschweifte Klammern eingeschlossen: { und }. Der Rumpf einer Methode
enthält Deklarationen und Anweisungen. Diese legen fest, was im Falle eines
Aufrufs der Methode passiert. Deklarationen dienen dazu, zusätzlich temporären
Speicherplatz für Variablen zu reservieren, während Anweisungen die Aktionen
der Methoden beschreiben. In gibPreis enthält der Rumpf der Methode nur eine
einzige Anweisung, aber wir werden bald Methodenrümpfe sehen, die aus etli-
chen Zeilen mit Deklarationen und Anweisungen bestehen.
Alle Deklarationen und Anweisungen, die zwischen geschweiften Klammern stehen,
werden zusammengefasst als Block bezeichnet. Entsprechend ist der Rumpf der
Klasse Ticketautomat ein Block und die Rümpfe des Konstruktors und der Methoden
der Klasse sind ebenfalls Blöcke.
Es gibt mindestens zwei entscheidende Unterschiede zwischen dem Kopf des Kon-
struktors und dem der Methode gibPreis:
public Ticketautomat(int ticketpreis)
public int gibPreis()
 Die Methode hat einen Ergebnistyp vom Typ int, der Konstruktor hingegen hat
keinen Ergebnistyp. Der Ergebnistyp wird unmittelbar vor den Methodennamen
geschrieben.
 Der Konstruktor hat einen formalen Parameter, ticketPreis, die Methode hin-
gegen hat keinen Parameter – nur ein Paar leere Klammern.
Es ist eine strikte Regel in Java, dass ein Konstruktor keinen Ergebnistyp definieren
darf. Aber sowohl Konstruktoren als auch Methoden dürfen eine beliebige Anzahl
an formalen Parametern definieren, also auch keine Parameter.
Im Rumpf von gibPreis steht eine einzelne Anweisung:
return preis;
Dies ist eine Rückgabeanweisung. Sie sorgt dafür, dass ein Wert vom Typ int zu-
rückgeliefert wird, ganz so, wie es im Kopf der Methode definiert ist. Wenn eine
Methode eine Rückgabeanweisung enthält, dann ist diese Anweisung immer die
letzte in der Methode, denn nach Ausführung der Rückgabeanweisung ist auch
die Ausführung einer Methode beendet.

66
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.8 Sondierende und verändernde Methoden

Ergebnistypen und Rückgabeanweisungen bedingen einander. Der Ergebnistyp int


von gibPreis ist eine Art Versprechen, dass der Rumpf der Methode etwas tut, das
schließlich mit der Rückgabe eines int-Werts endet. Sie können sich den Aufruf
einer Methode als eine Art Frage an ein Objekt vorstellen. Der Rückgabewert der
Methode ist dann die Antwort auf die Frage. In diesem Fall lautet die Frage, wenn
die Methode gibPreis an einem Ticketautomaten aufgerufen wird: „Was kostet
ein Ticket?“ Ein Ticketautomat muss für eine Antwort keine Berechnung durchfüh-
ren, weil er die Antwort im Datenfeld preis hält. Entsprechend kann die Methode
antworten, indem sie einfach den Wert dieser Variablen zurückliefert. Mit zuneh-
mender Komplexität unserer Klassen werden wir unausweichlich auch auf kompli-
ziertere Fragen stoßen, deren Beantwortung mehr Aufwand erfordert.

2.8 Sondierende und verändernde


Methoden
Wir nennen Methoden wie die beiden gib-Methoden von Ticketautomat (gibPreis Konzept
und gibBisherGezahltenBetrag) auch sondierende Methoden. Sie liefern dem Auf-
Sondierende
rufer Informationen über den Zustand des gerufenen Objekts – sie sondieren ihn.
Methoden lie-
Eine sondierende Methode enthält üblicherweise eine Rückgabeanweisung, mit fern Informationen
der ein bestimmter Wert zurückgeliefert wird. über den Zustand
Es herrscht oft Verwirrung über das, was mit „als Wert zurückgeben“ wirklich eines Objekts.
gemeint ist. Viele denken, dass damit das Programm angewiesen wird, etwas auszu-
geben. Dies trifft jedoch nicht zu – wir werden uns mit der Ausgabe beschäftigen,
wenn wir uns die Methode ticketDrucken näher anschauen. Das Zurückgeben eines
Werts bedeutet vielmehr, dass eine Information intern zwischen zwei verschiedenen
Programmteilen weitergereicht wird. Ein Teil des Programms hat vermittels eines
Methodenaufrufs Informationen von einem Objekt angefordert und über den Ergeb-
niswert hat das Objekt die Möglichkeit, diese Information an den Aufrufer zurückzu-
geben.

Übung 2.23 Vergleichen Sie Kopf und Rumpf der Methode gibBisher-
GezahltenBetrag mit Kopf und Rumpf der Methode gibPreis. Welche Unter-
schiede bestehen zwischen den beiden?
Übung 2.24 Wenn ein Aufruf von gibPreis der Frage „Was kostet ein
Ticket?“ entspricht, welcher Frage entspricht dann ein Aufruf von gibBisher-
GezahltenBetrag?

Übung 2.25 Wenn der Name von gibBisherGezahltenBetrag in gibBetrag


geändert würde, müsste dann auch die Rückgabeanweisung im Rumpf der
Methode geändert werden? Probieren Sie es in BlueJ aus. Welche Schlüsse
ziehen Sie daraus für den Namen einer sondierenden Methode und den
Namen des damit verbundenen Datenfelds?
Übung 2.26 Schreiben Sie eine sondierende Methode gibGesamtsumme in der
Klasse Ticketautomat. Die neue Methode sollte den Wert des Datenfelds
gesamtsumme zurückliefern.

67
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.27 Versuchen Sie, die Rückgabeanweisung aus dem Rumpf von
gibPreis zu entfernen. Welche Fehlermeldung sehen Sie, wenn Sie versuchen,
die Klasse zu übersetzen?
Übung 2.28 Vergleichen Sie die Köpfe der Methoden gibPreis und ticket-
Drucken in Listing 2.1. Worin unterscheiden sie sich, abgesehen von ihren
Namen?
Übung 2.29 Enthalten die Methoden geldEinwerfen und ticketDrucken Rück-
gabeanweisungen? Warum ist dies so? Ist an ihren Köpfen erkennbar,
warum sie keine Rückgabeanweisungen benötigen?

Konzept Die gib-Methoden des Ticketautomaten führen ähnliche Aufgaben aus – sie lie-
fern den Wert eines der Datenfelder des Objekts. Die übrigen Methoden –
Verändernde
geldEinwerfen und ticketDrucken – haben eine sehr viel wichtigere Rolle: Sie
Methoden
ändern den ändern den Wert eines oder mehrerer Datenfelder des Ticketautomaten, wenn
Zustand eines sie aufgerufen werden. Wir nennen sie deshalb verändernde Methoden.
Objekts. In der gleichen Weise, wie wir uns sondierende Methoden als Bitten um Informa-
tionen (Fragen) vorstellen können, können wir verändernde Methoden als Bitten
auffassen, dass das gerufene Objekt seinen Zustand ändern soll. Die Grundform
einer verändernden Methode ist eine Methode, die einen einzigen Parameter über-
nimmt, dessen Wert dazu dient, das, was in einem der Datenfelder des Objekts
gespeichert ist, zu überschreiben. Als direktes Komplement zu den gib-Methoden
wird dieser Typus oft setze-Methoden genannt, auch wenn es in der Klasse Ticket-
automat zurzeit noch keine gibt.
Eine charakteristische Eigenschaft einer verändernden Methode ist, dass ein Objekt
sich nach ihrem Aufruf anders verhält als vor dem Aufruf. Wir können dies mit
einer kleinen Übung illustrieren.

Übung 2.30 Erzeugen Sie einen Ticketautomaten mit einem Ticketpreis Ihrer
Wahl. Bevor Sie etwas anderes tun, rufen Sie an diesem Automaten zuerst die
Operation gibBisherGezahltenBetrag auf. Rufen Sie nun die Methode geldEin-
werfen auf (Listing 2.6) und geben Sie einen Betrag größer als null als aktuellen
Parameter an. Rufen Sie nun gibBisherGezahltenBetrag noch einmal auf. Die
beiden Aufrufe sollten unterschiedliche Ergebnisse liefern, weil der Aufruf von
geldEinwerfen den Effekt hatte, dass das Datenfeld bisherGezahlt verändert
wurde.

Der Kopf von geldEinwerfen definiert als Ergebnistyp void sowie einen formalen
Parameter betrag vom Typ int. Ein Ergebnistyp void besagt, dass eine Methode
keinen Wert an ihren Aufrufer zurückliefert. Dies ist ein grundlegender Unter-
schied zu allen anderen Ergebnistypen. In BlueJ wird dieser Unterschied dadurch
deutlich, dass nach dem Aufruf einer Methode mit Ergebnistyp void kein Ergeb-
nis-Dialog erscheint. Im Rumpf einer solchen Methode wird es dadurch deutlich,
dass keine Rückgabeanweisung angegeben ist.2

68
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.8 Sondierende und verändernde Methoden

Listing 2.6
Die Methode
geldEinwerfen.

Im Rumpf der Methode geldEinwerfen steht eine einzelne Anweisung, die eine wei-
tere Form der Zuweisung darstellt. Wir betrachten Zuweisungen immer, indem wir
uns zuerst die Berechnung auf der rechten Seite des Zuweisungssymbols ansehen.
Sie bewirkt hier, dass die Summe aus dem Wert des Parameters betrag und dem
Wert des Datenfeldes bisherGezahlt berechnet wird. Dieser kombinierte Wert wird
dann dem Datenfeld bisherGezahlt zugewiesen. Der Effekt ist hier also letztlich,
dass der Wert von bisherGezahlt um den Wert von betrag erhöht wird.3

Übung 2.31 Wie können wir bereits am Kopf von setzePreis erkennen,
dass dies eine Methode und kein Konstruktor ist?
public void setzePreis(int ticketpreis)
Übung 2.32 Vervollständigen Sie den Rumpf der Methode setzePreis so,
dass darin der Wert des Parameters an das Datenfeld preis zugewiesen wird.
Übung 2.33 Vervollständigen Sie den Rumpf der folgenden Methode, die die Auf-
gabe hat, das Datenfeld punktestand um den Wert des Parameters zu erhöhen.
/**
* Erhöhe den Punktestand um die angegebenen Punkte.
*/
public void erhoehen(int punkte)
{
...
}
Übung 2.34 Handelt es sich bei der Methode erhoehen um eine verändernde
Methode? Wenn ja, wie können Sie dies beweisen?
Übung 2.35 Vervollständigen Sie die folgende Methode, deren Aufgabe es
ist, das Datenfeld preis um den Wert des Parameters zu verringern?
/**
* Reduziere den Preis um den gegebenen Betrag.
*/
public void reduzieren(int betrag)
{
...
}

2 Tatsächlich erlaubt Java in Methoden mit Ergebnistyp void eine spezielle Rückgabeanweisung,
die keinen Rückgabewert angibt. Sie hat folgende Form:
return;
Sie bewirkt lediglich, dass die Ausführung der Methode beendet wird.
3 Das Hinzuaddieren eines Betrages zum Wert einer Variablen tritt so häufig auf, dass Java einen
speziellen zusammengesetzten Zuweisungsoperator dafür anbietet: +=. Beispielsweise:
bisherGezahlt += betrag;

69
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

2.9 Ausgaben in Methoden


Listing 2.7 zeigt die komplexeste Methode der Klasse Ticketautomat. Damit Sie der
folgenden Diskussion besser folgen können, sollten Sie diese Methode an einem
Ticketautomaten einmal aufgerufen haben. Folgendes sollte in der BlueJ-Konsole
ausgegeben werden:
##################
# Die BlueJ-Linie
# Ticket
# 500 Cent.
##################
Dies ist die längste Methode, die wir bisher betrachtet haben, wir sollten sie des-
halb Schritt für Schritt untersuchen:
 Der Kopf besagt, dass die Methode den Ergebnistyp void hat und keine Para-
meter annimmt.
 Der Rumpf enthält acht Anweisungen und einige Kommentare.
 Die ersten sechs Anweisungen sorgen für die Ausgabe, die Sie auf der BlueJ-
Konsole sehen: fünf Textzeilen und eine sechste Leerzeile.
 Die siebte Anweisung addiert den (durch Aufrufe von geldEinwerfen) bisher bezahl-
ten Betrag des Benutzers zur Gesamtsumme aller eingezahlten Beträge hinzu.
 Die achte Anweisung setzt den Wert des Datenfelds bisherGezahlt mit einer
einfachen Zuweisung auf null zurück, sodass der nächste Kunde Geld einwer-
fen kann.

Listing 2.7
Die Methode
ticketDrucken.

Konzept Durch einen Vergleich der Ausgaben in der Konsole mit den Anweisungen, die zu
diesen Ausgaben geführt haben, ist leicht zu erkennen, dass eine Anweisung wie
Die Methode Sys-
[Link] [Link]("# Die BlueJ-Linie");
gibt ihren Parameter
bewirkt, dass die in doppelten Anführungszeichen stehende Zeichenkette ausge-
auf der Konsole aus.
geben wird. Die grundlegende Form eines Aufrufs von println ist
[Link](etwas-das-wir-ausgeben-wollen);
wobei etwas-das-wir-ausgeben-wollen durch eine beliebige Zeichenkette ersetzt
werden kann, die in doppelten Anführungszeichen stehen muss. So hat zum Bei-

70
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.9 Ausgaben in Methoden

spiel das Zeichen # in dieser Zeichenkette keine besondere Bedeutung – es ist


lediglich eines der Zeichen, die ausgegeben werden sollen.
All diese Ausgabeanweisungen in der Methode ticketDrucken sind Aufrufe der
Methode println des Objekts [Link], das in die Sprache Java integriert ist.
Und das, was in den runden Klammern steht, ist erwartungsgemäß der Parame-
ter zu dem jeweiligen Methodenaufruf. Der aktuelle Parameter von println in
der vierten Anweisung ist etwas komplizierter und bedarf einer Erläuterung:
[Link]("# " + preis + " Cent.");
Dieser Methodenaufruf gibt den Preis des Tickets aus, mit zusätzlichen Zeichen zu
beiden Seiten des Betrags. Mithilfe der beiden +-Operatoren wird aus den drei nach-
folgend beschriebenen Bestandteilen ein einzelner aktueller Parameter zusammen-
gesetzt:
 der konstanten Zeichenkette "# " (beachten Sie das Leerzeichen hinter dem
Doppelkreuz);
 dem Wert des Datenfelds preis (beachten Sie, dass keine Anführungszeichen
um den Namen des Datenfelds angegeben sind, da wir den Wert des Daten-
feldes benötigen und nicht seinen Namen);
 der konstanten Zeichenkette " Cent." (beachten Sie das Leerzeichen vor dem
Wort Cent).
Wenn er mit einer Zeichenkette als Operand benutzt wird, dann ist + ein Opera-
tor zur Verkettung von Zeichenketten (zu einer neu erzeugten Zeichenkette) und
kein arithmetischer Operator zur Addition. Damit wird der numerische Wert von
preis in eine Zeichenkette umgewandelt, die mit den beiden Zeichenketten
rechts und links zu einer neuen Zeichenkette kombiniert wird.
Beachten Sie, dass der abschließende Aufruf von println keinen Parameter über-
gibt. Dies ist zulässig und das Ergebnis dieses Aufrufs ist die Ausgabe einer Leer-
zeile zwischen der vorigen Ausgabe und allen weiteren Ausgaben. Sie können
diese Leerzeile sehen, wenn Sie ein weiteres Ticket ausgeben lassen.

Übung 2.36 Schreiben Sie nieder, was genau die folgende Anweisung ausgibt.
[Link]("Meine Katze hat gruene Augen");
Übung 2.37 Fügen Sie der Klasse Ticketautomat eine Methode meldung
hinzu. Diese Methode sollte parameterlos sein und als Ergebnis void liefern.
Der Rumpf der Methode soll einen Text ausgeben, etwa in der Art:
Bitte werfen Sie den passenden Geldbetrag ein.
Übung 2.38 Was würde ausgegeben, wenn Sie die vierte Anweisung in der
Methode ticketDrucken so ändern würden, dass preis auch mit Anführungs-
zeichen umschlossen ist:
[Link]("# " + "preis" + " Cent.");
Übung 2.39 Wie sieht es mit folgender Version aus?
[Link]("# preis Cent.");

71
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.40 Könnte eine der beiden letzten Versionen benutzt werden, um
den Preis eines Tickets anderer Ticketautomaten auszugeben? Erläutern Sie
Ihre Antwort.
Übung 2.41 Fügen Sie der Klasse Ticketautomat eine Methode preisAusgeben
hinzu. Diese Methode sollte parameterlos sein und als Ergebnis void liefern.
Der Rumpf der Methode soll einen Text ausgeben, etwa in der Art:
Der Preis eines Tickets beträgt xyz Cent.
Hierbei sollte xyz durch den Wert ersetzt werden, der bei Aufruf der Methode
in dem Datenfeld preis gehalten wird.
Übung 2.42 Erzeugen Sie zwei Ticketautomaten mit unterschiedlichen Ticket-
preisen. Liefern Aufrufe ihrer preisAusgeben-Methoden unterschiedliche oder
gleiche Ergebnisse? Wie erklären Sie diesen Effekt?

2.10 Zusammenfassung der Methoden


Da Methoden ein grundlegender Bestandteil der Programme sind, die wir in die-
sem Buch schreiben und untersuchen werden, halten wir es für sinnvoll, hier eine
kurze Zusammenfassung zu geben. Methoden implementieren die Kernaktionen
eines jeden Objekts.
Eine Methode mit Parametern übernimmt Daten, die ihr vom Aufrufer der Methode
übergeben wurden, und verwendet diese Daten, um eine bestimmte Aufgabe aus-
zuführen. Es übernehmen jedoch nicht alle Methoden Parameter; viele Methoden
verwenden einfach die Daten, die in den Datenfeldern des Objekts gespeichert sind,
um ihre Aufgabe zu erfüllen.
Eine Methode, deren Ergebnistyp nicht void ist, liefert einen Wert dorthin zurück,
von wo sie aufgerufen wurde – und dieser Wert wird mit großer Sicherheit vom
Aufrufer für weitere Berechnungen oder Programmmanipulationen verwendet.
Viele Methoden sind jedoch vom Ergebnis void und liefern nichts zurück. Dennoch
erfüllen sie im Kontext ihres Objekts eine wichtige Aufgabe.
Sondierende Methoden haben einen Ergebnistyp ungleich void und liefern Infor-
mationen über den Zustand des Objekts zurück. Verändernde Methoden ändern
den Zustand eines Objekts. Sie übernehmen oft Parameter, deren Werte zur Ver-
änderung herangezogen werden, obwohl es auch möglich ist, eine verändernde
Methode zu schreiben, die keine Parameter übernimmt.

2.11 Zusammenfassung des naiven


Ticketautomaten
Wir haben die interne Struktur der Klasse des naiven Ticketautomaten nun aus-
führlich untersucht. Wir haben gesehen, dass die Klasse eine schmale äußere Hülle
hat, die der Klasse einen Namen gibt, sowie einen sehr viel umfangreicheren
Rumpf, der Datenfelder, einen Konstruktor und einige Methoden definiert. Daten-

72
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.12 Bewertung des Entwurfs des naiven Ticketautomaten

felder halten Werte und ermöglichen Objekten damit, einen Zustand zu halten, der
sich zwischen zwei Methodenaufrufen nicht ändert. Konstruktoren versetzen ein
Objekt in einen Anfangszustand, nachdem es erzeugt wurde. Ausgehend von die-
sem gültigen Anfangszustand ist das Objekt nach seiner Erzeugung in der Lage,
angemessen auf Methodenaufrufe zu reagieren. Methoden implementieren das
definierte Verhalten der Objekte einer Klasse. Sondierende Methoden liefern Infor-
mationen über den Objektzustand, verändernde Methoden ändern den Zustand.
Wir haben gesehen, dass Konstruktoren sich von Methoden dadurch unterschei-
den, dass sie den gleichen Namen tragen wie die Klasse, in der sie definiert sind.
Sowohl Konstruktoren als auch Methoden können Parameter entgegennehmen,
aber nur Methoden können einen Ergebnistyp definieren. Ergebnistypen, die
nicht void sind, erlauben uns, aus einer Methode ein Ergebnis zurückzuliefern.
Eine Methode mit einem Ergebnistyp ungleich void muss mindestens eine Rück-
gabeanweisung in ihrem Rumpf aufweisen; diese ist oft die letzte Anweisung.
Konstruktoren haben niemals einen Ergebnistyp – nicht einmal void.

Bevor Sie diese Übungen durchführen, sollten Sie sicher sein, dass Sie ein
solides Verständnis davon haben, wie sich die Ticketautomaten verhalten
und wie dieses Verhalten durch die Datenfelder, den Konstruktor und die
Methoden der Klasse implementiert ist.
Übung 2.43 Verändern Sie den Konstruktor in Ticketautomat so, dass er
keinen Parameter mehr bekommt. Stattdessen soll der Preis eines Tickets auf
1000 Cent festgelegt sein. Welchen Effekt hat diese Änderung, wenn Sie
Ticketautomaten in BlueJ erzeugen?
Übung 2.44 Definieren Sie zwei Konstruktoren für die Klasse. Einer soll
einen Parameter bekommen, der den Preis festlegt, der andere Konstruktor
soll keinen Parameter bekommen und den Preis auf einen Standardwert Ihrer
Wahl festlegen. Testen Sie Ihre Implementierung, indem Sie Automaten mit
beiden Konstruktoren erzeugen.
Übung 2.45 Implementieren Sie eine Methode leeren, mit der simuliert
wird, dass dem Automaten alles Geld entnommen wird. Diese Methode
sollte den Rückgabetyp void haben und in ihrem Rumpf lediglich das Daten-
feld gesamtsumme auf null setzen. Benötigt diese Methode irgendwelche Para-
meter? Testen Sie Ihre Methode, indem Sie einen Automaten erzeugen,
Geld einwerfen, einige Tickets ausgeben lassen, die Gesamtsumme über-
prüfen und dann den Automaten leeren. Ist leeren eine sondierende oder
eine verändernde Methode?

2.12 Bewertung des Entwurfs des


naiven Ticketautomaten
Nach unserer Untersuchung der internen Struktur der Klasse Ticketautomat sollte
Ihnen klar sein, wie unzulänglich solche Automaten im realen Einsatz wären. Die
Klasse weist mehrere Schwächen auf:

73
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

 Sie enthält keine Prüfung, ob der Kunde überhaupt genug Geld für ein Ticket
eingeworfen hat.
 Sie gibt kein Geld zurück, wenn der Kunde zu viel für ein Ticket bezahlt hat.
 Sie überprüft nicht, ob der Kunde sinnvolle Geldbeträge einwirft: Probieren Sie
beispielsweise aus, was passiert, wenn Sie negative Beträge angeben.
 Sie überprüft nicht, ob der Ticketpreis, der dem Konstruktor übergeben wird,
sinnvoll ist.
Wenn wir diese Probleme beseitigen könnten, dann hätten wir ein sehr viel rea-
listischeres Stück Software, das als Basis für den Betrieb eines echten Ticketauto-
maten dienen könnte.
In den nächsten Abschnitten werden wir die Implementierung eine Klasse für verbes-
serte Ticketautomaten untersuchen, die einige Schwächen der naiven Implementie-
rung behebt. Öffnen Sie als Erstes das Projekt Besserer-Ticketautomat. Wie vorher
enthält dieses Projekt eine einzige Klasse – Ticketautomat. Bevor wir uns die interne
Struktur dieser Klasse näher ansehen, experimentieren Sie mit der Klasse. Erzeugen
Sie einige Instanzen und stellen Sie fest, ob Sie einen Unterschied im Verhalten dieser
Version zu der Version des naiven Ticketautomaten feststellen.
Ein Unterschied ist, dass diese Version eine zusätzliche Methode wechselgeld-
Auszahlen hat. Beobachten Sie, was passiert, wenn Sie sie aufrufen.

Listing 2.8
Ein verbesserter
Ticketautomat.

74
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.12 Bewertung des Entwurfs des naiven Ticketautomaten

75
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

2.13 Entscheidungen treffen:


die bedingte Anweisung
Listing 2.8 zeigt den Quelltext der Klasse für verbesserte Ticketautomaten. Vieles
in dieser Klassendefinition wird Ihnen schon von unserer Diskussion der naiven
Ticketautomaten vertraut sein. Beispielsweise ist die äußere Hülle mit dem Klas-
sennamen gleich, weil wir dieser Klasse den gleichen Namen gegeben haben.
Außerdem enthält die Klasse die gleichen drei Datenfelder für den Objektzustand,
die auf die gleiche Weise definiert sind. Der Konstruktor und die beiden gib-
Methoden sind ebenfalls gleich.
Der erste signifikante Unterschied ist in der Methode geldEinwerfen zu finden. Wir
hatten festgestellt, dass eines der Hauptprobleme des naiven Ticketautomaten die
fehlende Überprüfung von Bedingungen war. Eine dieser Prüfungen fehlte beim
Einwerfen von Geld, denn der Kunde konnte negative Werte angeben. Wir haben
diesen Mangel behoben, indem wir mit einer bedingten Anweisung überprüfen,
ob der übergebene Wert größer als null ist:
if (betrag > 0) {
bisherGezahlt = bisherGezahlt + betrag;
}
else {
[Link]("Einen positiven Betrag verwenden, nicht: "
+ betrag);
}

Konzept Bedingte Anweisungen sind auch als if-Anweisungen bekannt, da if das Schlüs-
selwort ist, das in den meisten Programmiersprachen für bedingte Anweisungen
Eine bedingte
verwendet wird. Eine bedingte Anweisung erlaubt uns, abhängig vom Ergebnis
Anweisung
führt eine von zwei einer Prüfung eine von zwei Aktionen auszuführen. Wenn der Test positiv ausfällt
Aktionen aus, (true liefert), dann soll die eine Aktion ausgeführt werden, andernfalls die
abhängig vom andere. Eine bedingte Anweisung hat allgemein ein Format, wie es im folgenden
Ergebnis einer Pseudocode angedeutet ist:
Prüfung.
if (führe eine Prüfung durch, die true oder false liefert) {
auszuführende Anweisungen, wenn die Prüfung true geliefert hat
}
else {
auszuführende Anweisungen, wenn die Prüfung false geliefert hat
}
Bestimmte Teile dieses Pseudocodes sind echter Java-Code und in fast allen beding-
ten Ausdrücken zu finden: die Schlüsselwörter if und else, die runden Klammern
um den Test und die geschweiften Klammern um die beiden Blöcke. Der Rest, d.h.
der kursive Code, fällt je nach Situation unterschiedlich aus.
Es wird immer nur einer der beiden Anweisungsblöcke nach Auswertung des
Tests ausgeführt. Entsprechend wird im Beispiel der Methode geldEinwerfen nach
dem Test des eingeworfenen Betrags entweder der Betrag zum bereits gezahlten
Betrag hinzuaddiert, oder es wird eine Fehlermeldung ausgegeben. Der Test

76
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.13 Entscheidungen treffen: die bedingte Anweisung

benutzt den Größer-als-Operator, >, um den Wert von betrag mit null zu verglei-
chen. Wenn der Wert größer als null ist, dann wird er zum bisher bezahlten
Betrag hinzuaddiert. Wenn er nicht größer als null ist, dann wird eine Fehlermel-
dung ausgegeben. Durch die Verwendung einer bedingten Anweisung schützen
wir an dieser Stelle bisherGezahlt davor, durch einen ungültigen Wert verändert
zu werden. Einzelheiten zu anderen Java-Operatoren sind in Anhang C zu fin-
den. Die vielleicht nennenswertesten Operatoren sind < (kleiner- als) , <= (kleiner-
gleich) und >= (größer-gleich). All diese Operatoren werden, wie in der Methode
ticketDrucken, dazu verwendet, um zwei numerische Werte zu vergleichen.
Der Test, den wir in der bedingten Anweisung verwenden, ist ein Beispiel für Konzept
einen booleschen Ausdruck. In diesem Kapitel haben wir bereits arithmetische
Boolesche Aus-
Ausdrücke eingeführt, die numerische Ergebnisse liefern. Ein boolescher Aus-
drücke haben nur
druck hat nur zwei mögliche Ergebnisse, true oder false: Entweder ist der Wert zwei mögliche
von betrag größer als null (true) oder er ist nicht größer (false). Eine bedingte Werte: wahr (true)
Anweisung benutzt diese beiden möglichen Werte, um zwischen zwei mögli- und falsch (false).
chen Aktionen zu wählen. Sie werden oft
verwendet, um die
Auswahl zwischen
zwei Ausführungs-
Übung 2.46 Überprüfen Sie, ob das hier diskutierte Verhalten auch wirklich pfaden in einer
eingehalten wird, indem Sie eine Instanz von Ticketautomat erzeugen und bedingten Anwei-
die Methode geldEinwerfen mit unterschiedlichen Parametern aufrufen. sung zu treffen.
Überprüfen Sie die bisherige Bezahlung jeweils vor und nach den Aufrufen.
Ändert sich bisherGezahlt, wenn eine Fehlermeldung ausgegeben wird?
Versuchen Sie vorherzusagen, was im Falle eines Parameters mit dem Wert
null passiert, und überprüfen Sie dann, ob Sie recht hatten.
Übung 2.47 Sagen Sie voraus, was passiert, wenn Sie die Prüfung in geld-
Eingeben so verändern, dass der Größer-gleich-Operator verwendet wird:
if (betrag >= 0)
Überprüfen Sie Ihre Vorhersage durch einige Tests. In welcher Situation hat
diese Änderung einen Einfluss auf das Verhalten der Methode?
Übung 2.48 Schreiben Sie die if-else-Anweisung so um, dass sich die
Methode zwar immer noch korrekt verhält, aber die Fehlermeldung ausgege-
ben wird, wenn der boolesche Ausdruck wahr ist, beziehungsweise bisher-
Gezahlt erhöht wird, wenn der Ausdruck falsch ist. Hierzu werden Sie offen-
sichtlich die Bedingung umschreiben müssen.
Übung 2.49 Im Projekt Figuren in Kapitel 1 haben wir ein boolesches Daten-
feld benutzt, um eine Eigenschaft eines Kreis-Objekts zu kontrollieren. Was
war das für eine Eigenschaft? War die Modellierung dieser Eigenschaft durch
einen Typ mit nur zwei möglichen Werten angemessen?

77
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

2.14 Ein weiteres Beispiel für eine


bedingte Anweisung
Die Methode ticketDrucken enthält eine weitere bedingte Anweisung. Hier ist das
entsprechende Fragment aus dem Quelltext:
if (bisherGezahlt >= preis) {

Details der Ausgabe hier ausgelassen

// Die Gesamtsumme um den Ticketpreis erhöhen.


gesamtsumme = gesamtsumme + preis;
// Den Preis von der bisherigen Bezahlung abziehen.
bisherGezahlt = bisherGezahlt - preis;
}
else {
[Link]("Sie müssen noch mindestens " +
(preis - bisherGezahlt) + " Cent einwerfen.");
}
Mit dieser if-Anweisung beheben wir das Problem, dass die naive Version nicht
überprüft, ob der Kunde überhaupt genügend Geld für ein Ticket eingeworfen
hat, bevor gedruckt wird. Die neue Version überprüft, ob der Wert im Datenfeld
bisherGezahlt mindestens den Wert des Datenfelds preis hat. Ist dies der Fall,
dann kann ein Ticket ausgegeben werden. Wenn nicht, lassen wir eine Fehler-
meldung ausgeben.
Die Ausgabe der Fehlermeldung folgt genau dem gleichen Muster wie die Aus-
gabe des Ticketpreises in ticketDrucken. Der Code ist nur etwas ausführlicher.
[Link]("Sie müssen noch mindestens " +
(preis - bisherGezahlt) + " Cent einwerfen.");
Der einzige aktuelle Parameter an die Methode println besteht aus einer Anein-
anderreihung von drei Elementen: zwei konstante Zeichenketten links und rechts
eines numerischen Wertes. In diesem Fall ist der numerische Wert eine Subtrak-
tion, die in Klammern gesetzt wurde, um anzuzeigen, dass sie das Ergebnis lie-
fert, das wir mit den beiden Zeichenketten zusammensetzen möchten.

Übung 2.50 In dieser Version von ticketDrucken werden auch die Daten-
felder gesamtsumme und bisherGezahlt anders behandelt. Vergleichen Sie den
Quelltext in Listing 2.1 mit dem in Listing 2.8 und stellen Sie die Unter-
schiede fest. Überprüfen Sie Ihr Verständnis dann über Testaufrufe in BlueJ.
Übung 2.51 Ist es möglich, den else-Teil der if-Anweisung in der Methode
ticketDrucken wegzulassen (d.h. das Wort else und den dazugehörigen
Block)? Versuchen Sie es und probieren Sie, ob sich der Code noch überset-
zen lässt. Was passiert, wenn Sie anschließend versuchen, ein Ticket auszu-
geben, ohne vorher Geld einzuwerfen?

78
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.15 Hervorhebung von Sichtbarkeitsbereichen

Die Methode ticketDrucken vermindert den Wert von bisherGezahlt um den


Wert von preis. Als Konsequenz daraus bleibt, wenn der Kunde mehr als den
Ticketpreis einwirft, die Differenz als Geldbetrag übrig und kann für die Bezah-
lung des nächsten Tickets verwendet werden. Alternativ kann der Kunde sich
den Restbetrag auszahlen lassen, und das ist auch genau das, was die Methode
wechselgeldAuszahlen tut. Wir werden sie uns in Abschnitt 2.16 genauer anse-
hen.

2.15 Hervorhebung von


Sichtbarkeitsbereichen
Ihnen ist inzwischen sicher aufgefallen, dass der BlueJ-Editor den Quelltext mit
einigen zusätzlichen Dekorationen versieht: farbige Kästen, die bestimmte
Abschnitte wie Methoden und if-Anweisungen umschließen (siehe zum Beispiel
Listing 2.8).
Diese farbliche Unterstützung hebt die Sichtbarkeit hervor und hilft, logische Ein-
heiten in Ihren Programmen zu erkennen. Ein Sichtbarkeitsbereich (auch Block
genannt) ist eine Codeeinheit, die normalerweise durch ein Paar geschweifter
Klammern gekennzeichnet ist. Der ganze Rumpf einer Klasse ist ein Sichtbarkeits-
bereich, ebenso wie der Rumpf jeder Methode und die if- und else-Teile einer if-
Anweisung Sichtbarkeitsbereiche sind.
Wie Sie sehen, sind Sichtbarkeitsbereiche oft verschachtelt: Die if-Anweisung liegt
innerhalb einer Methode, die innerhalb einer Klasse liegt. Und BlueJ hilft Ihnen, die
verschiedenen Sichtbarkeitsbereiche zu erkennen, indem diese farblich unterlegt
werden.
Einer der häufigsten Fehler im Code von Programmieranfängern betrifft die ge-
schweiften Klammern – entweder werden sie falsch gesetzt oder es wird eine der
Klammern ganz vergessen. Es gibt zwei Möglichkeiten, die helfen, diese Art von
Fehler zu vermeiden:
 Achten Sie darauf, Ihren Code ordentlich einzurücken. Jedes Mal, wenn ein
neuer Sichtbarkeitsbereich beginnt (nach einer geöffneten geschweiften Klam-
mer), rücken Sie den folgenden Code um eine Ebene ein. Durch Schließen des
Sichtbarkeitsbereichs wird die Einrückung wieder zurückgenommen. Wenn Ihre
Einrückung vollständig deaktiviert ist, verwenden Sie die AUTO-LAYOUT-Funktion
von BlueJ (zu finden im BEARBEITEN-Menü), um dies zu beheben.
 Achten Sie auf die Hervorhebung der Sichtbarkeitsbereiche. Sie werden sich
schnell an das Erscheinungsbild von gut strukturiertem Code gewöhnen. Ver-
suchen Sie, eine geschweifte Klammer im Editor zu entfernen oder eine an
einer beliebigen Position zu ergänzen, und beachten Sie, wie sich die farbliche
Hervorhebung ändert. Gewöhnen Sie sich daran, zu erkennen, wann Sichtbar-
keitsbereiche nicht korrekt sind.

79
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.52 Könnte, nachdem ein Ticket ausgegeben wurde, der Wert des
Datenfelds bisherGezahlt einen negativen Wert annehmen, wenn der Wert
von preis abgezogen wird? Begründen Sie Ihre Antwort.
Übung 2.53 Bisher haben wir Ihnen zwei arithmetische Operatoren (+ und -)
vorgestellt, die in arithmetischen Ausdrücken in Java verwendet werden
können. Sehen Sie sich in Anhang C an, welche weiteren Operatoren ver-
fügbar sind.
Übung 2.54 Schreiben Sie eine Zuweisung, die das Ergebnis der Multiplika-
tion zweier Variablen, preis und rabatt, in einer dritten Variablen ersparnis
speichert.
Übung 2.55 Schreiben Sie eine Zuweisung, die den Wert in gesamt durch
den Wert in anzahl teilt und das Ergebnis in durchschnitt speichert.
Übung 2.56 Schreiben Sie eine if-Anweisung, die den Wert in preis mit dem
Wert in budget vergleicht. Wenn preis größer ist als budget, dann soll die Mel-
dung „zu teuer“ ausgegeben werden, ansonsten die Meldung „passt“.
Übung 2.57 Passen Sie Ihre Antwort aus der vorigen Übung so an, dass die
Meldung das aktuelle Budget anzeigt, wenn der Preis zu hoch ist.

2.16 Lokale Variablen


Bisher haben wir zwei verschiedene Arten von Variablen kennengelernt: Daten-
felder (Instanzvariablen) und Parameter. Jetzt wollen wir eine dritte Art einführen.
Allen Variablen ist gemeinsam, dass sie Daten speichern, aber jeder Variablenart
kommt eine bestimmte Rolle zu.
In Abschnitt 2.7 haben wir behauptet, dass ein Methodenrumpf (oder allgemein,
ein Block) sowohl Deklarationen als auch Anweisungen enthalten kann. Bis jetzt
haben wir noch keine Methode betrachtet, die irgendwelche Deklarationen ent-
hält. Die Methode wechselgeldAusgeben enthält drei Anweisungen und eine Dekla-
ration. Die Deklaration illustriert eine neue Variablenart:
public int wechselgeldAuszahlen()
{
int wechselgeld;
wechselgeld = bisherGezahlt;
bisherGezahlt = 0;
return wechselgeld;
}
Was für eine Variable ist wechselgeld? Wir wissen, dass es kein Datenfeld ist,
denn Datenfelder werden außerhalb von Methoden vereinbart. Es ist auch kein
Parameter, da diese immer im Kopf einer Methode deklariert werden. Die Variable
wechselgeld ist das, was man eine lokale Variable nennt, weil sie innerhalb einer
Methode vereinbart wird.

80
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.16 Lokale Variablen

Die Deklaration einer lokalen Variablen sieht der Deklaration eines Datenfelds Konzept
sehr ähnlich, eine lokale Variable wird aber niemals als private oder public defi-
Eine lokale Vari-
niert. Auch Konstruktoren können lokale Variablen definieren. Wie auch formale
able ist eine Vari-
Parameter haben lokale Variablen einen Sichtbarkeitsbereich, der auf die Anwei- able, die innerhalb
sungen der definierenden Methode beschränkt ist. Ihre Lebensdauer entspricht einer Methode
der Zeit, die für die Ausführung der Methode benötigt wird: Sie werden ange- deklariert und
legt, wenn die Methode aufgerufen wird, und entfernt, wenn die Methodenaus- benutzt wird. Sie ist
führung endet. nur innerhalb der
Methode zugreifbar
Vielleicht fragen Sie sich, wozu wir lokale Variablen benötigen, wo wir doch Daten- und ihre Lebens-
felder haben. Lokale Variablen werden primär zur Speicherung von temporären dauer entspricht
Daten verwendet, die benötigt werden, damit eine Methode ihre Aufgabe been- der der Methode.
den kann. Wir stellen sie uns als Datenspeicher für eine Methode vor. Im Gegen-
satz dazu werden Datenfelder eingesetzt, um Daten für die Lebensdauer eines
ganzen Objekts dauerhaft zu speichern. Die in den Datenfeldern gespeicherten
Daten sind für alle Methoden des Objekts zugreifbar. Wir versuchen es zu vermei-
den, Variablen als Datenfelder zu deklarieren, die eigentlich nur lokal (auf Metho-
denebene) verwendet werden und deren Werte nicht über einen Methodenaufruf
hinaus aufbewahrt werden müssen. Selbst wenn zwei oder mehr Methoden in der
gleichen Klasse lokale Variablen für den gleichen Zweck verwenden würden, ist es
nicht korrekt, sie als Datenfelder zu definieren, wenn ihre Werte nicht über das
Ende der Methoden hinaus bestehen müssten.
In der Methode wechselgeldAuszahlen wird wechselgeld nur kurz benutzt, um den
Wert von bisherGezahlt aufzubewahren, bevor dieses Datenfeld auf null gesetzt
wird. Die Methode liefert dann den alten Wert von bisherGezahlt zurück. Die fol-
genden Übungen machen deutlich, warum wir an dieser Stelle eine lokale Variable
brauchen – indem wir versuchen, ohne eine solche auszukommen.

Übung 2.58 Warum liefert die folgende Version von wechselgeldAuszahlen


nicht das gleiche Ergebnis wie das Original?
public int wechselgeldAuszahlen()
{
bisherGezahlt = 0;
return bisherGezahlt;
}
Übung 2.59 Was passiert, wenn Sie versuchen, die Klasse Ticketautomat mit
folgender Version von wechselgeldAuszahlen zu übersetzen?
public int wechselgeldAuszahlen()
{
return bisherGezahlt;
bisherGezahlt = 0;
}
Was wissen Sie über Rückgabeanweisungen, das Ihnen erklären könnte,
warum diese Version nicht übersetzt werden kann?

81
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.60 Was ist falsch an der folgenden Version des Konstruktors von
Ticketautomat?
public Ticketautomat(int ticketpreis)
{
int preis = ticketpreis;
bisherGezahlt = 0;
gesamtsumme = 0;
}
Setzen Sie diese Version in dem Projekt Besserer-Ticketautomat ein. Lässt
sich diese Version übersetzen? Erzeugen Sie ein Objekt und untersuchen Sie
dann seine Datenfelder. Fällt Ihnen auf, dass in dieser Version irgendetwas
im Inspektor komisch ist mit dem Wert des Datenfeldes preis? Können Sie
erklären, warum dies so ist?

Es ist ganz normal, lokale Variablen bei ihrer Deklaration zu initialisieren. Wir könnten
also die ersten zwei Anweisungen von wechelgeldAuszahlen wie folgt verkürzen:
int wechselgeldAuszahlen = bisherGezahlt;
Dennoch ist wichtig zu wissen, dass hier zwei Schritte zusammengefasst wurden:
Die Variable wechselgeldAuszahlen zu deklarieren und ihr einen Anfangswert
zuzuweisen.

Fallstrick
Eine lokale Variable mit dem gleichen Namen wie ein Datenfeld verhindert,
dass innerhalb des Konstruktors bzw. der Methode auf das Datenfeld zuge-
griffen wird. In Abschnitt 3.13.2 ist beschrieben, wie Sie dies bei Bedarf
umgehen können.

2.17 Datenfelder, Parameter und


lokale Variablen
Nach der Einführung von wechselgeld in der Methode wechselgeldAuszahlen kennen
wir nun drei Arten von Variablen: Datenfelder, formale Parameter und lokale Variab-
len. Es ist wichtig, sich die Unterschiede und Gemeinsamkeiten dieser verschiede-
nen Arten klarzumachen. Wir fassen ihre Eigenschaften hier zusammen:
 Alle drei Arten von Variablen können einen Wert halten, der ihrem jeweiligen
Typ entspricht. Eine Variable vom Typ int beispielsweise kann einen ganzzahli-
gen Wert halten.
 Datenfelder werden außerhalb von Konstruktoren und Methoden deklariert.
 Datenfelder halten Daten, die über die gesamte Lebensdauer eines Objekts er-
halten bleiben. Sie halten somit den Zustand eines Objekts. Sie haben die glei-
che Lebensdauer wie das Objekt, in dem sie definiert sind.

82
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.17 Datenfelder, Parameter und lokale Variablen

 Datenfelder haben klassenweite Sichtbarkeit: Sie sind in der gesamten Klasse


zugreifbar und können somit in jedem Konstruktor und jeder Methode ihrer
Klasse benutzt werden.
 Solange ein Datenfeld als private deklariert ist, kann darauf nicht von außer-
halb der definierenden Klasse zugegriffen werden.
 Formale Parameter und lokale Variablen existieren nur für die Dauer der Aus-
führung eines Konstruktors oder einer Methode. Ihre Lebensdauer beträgt nur
die Zeit der Ausführung, sodass ihre Werte zwischen den Aufrufen verloren
gehen. Sie dienen somit temporärer statt dauerhafter Datenhaltung.
 Formale Parameter werden im Kopf eines Konstruktors und einer Methode defi-
niert. Sie bekommen ihre Werte von außen, indem sie mit den Werten der aktu-
ellen Parameter des Konstruktors oder Methodenaufrufs initialisiert werden.
 Die Sichtbarkeit formaler Parameter ist auf den definierenden Konstruktor oder
die definierende Methode beschränkt.
 Lokale Variablen werden im Rumpf eines Konstruktors oder einer Methode
definiert. Sie können initialisiert werden und es kann nur im Rumpf des Kons-
truktors oder der Methode auf sie zugegriffen werden. Lokale Variablen müs-
sen explizit initialisiert werden, bevor sie in einem Ausdruck benutzt werden
können – sie werden nicht automatisch mit einem Standardwert initialisiert.
 Die Sichtbarkeit lokaler Variablen ist auf den Block beschränkt, in dem sie defi-
niert wurden. Sie sind außerhalb dieses Blocks nicht zugreifbar.
Programmierneulingen fällt es häufig schwer zu entscheiden, ob ein Variabe als
Datenfeld oder als lokale Variable definiert werden sollte. Die Versuchung ist groß,
alle Variablen als Datenfelder zu definieren, weil auf diese von überall in der Klasse
zugegriffen werden kann. Tatsächlich ist es viel besser, sich die umgekehrte Vorge-
hensweise zur Regel zu machen: Variablen lokal für eine Methode zu definieren,
außer sie gehören untrennbar zum dauerhaften Zustand eines Objekts. Selbst
wenn Sie erwarten, dieselbe Variable in zwei oder mehreren Methoden zu verwen-
den, sollten Sie für jede Methode eine eigene Version lokal definieren, solange Sie
sich nicht absolut sicher sind, dass die dauerhafte Speicherung gerechtfertigt ist.

Übung 2.61 Fügen Sie eine Methode entleeren hinzu, die das Entleeren des
Automaten simuliert. Diese Methode soll gesamtsumme auf null zurücksetzen,
aber vorher den Wert von gesamtsumme zurückliefern.
Übung 2.62 Überarbeiten Sie die Methode ticketDrucken so, dass sie eine
lokale Variable nochZuZahlen definiert. Diese sollte mit der Differenz zwischen
preis und bisherGezahlt initialisiert werden. Schreiben Sie die bedingte
Anweisung so um, dass sie den Wert von nochZuZahlen überprüft. Wenn die-
ser Wert kleiner oder gleich null ist, dann soll ein Ticket ausgegeben wer-
den, andernfalls soll eine Fehlermeldung ausgegeben werden, die den noch
zu zahlenden Betrag angibt. Testen Sie, ob sich diese Version genauso ver-
hält wie das Original. Achten Sie darauf, dass Sie die Methode mehrmals
und für unterschiedliche Maschinenzustände ausführen, damit beide Teile
der bedingten Anweisung ausgeführt werden.

83
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.63 Zusatzaufgabe. Nehmen Sie an, wir möchten, dass ein einzelner
Ticketautomat Tickets zu unterschiedlichen Preisen ausgeben kann. Beispiels-
weise könnten Benutzer eine Taste am realen Automaten drücken, um ein
rabattiertes Ticket auszuwählen. Welche weiteren Methoden und/oder Daten-
felder müssten zu Ticketautomat hinzugefügt werden, um diese Funktionalität
zu ermöglichen? Denken Sie, dass auch viele der bereits bestehenden Metho-
den geändert werden müssten?
Speichern Sie das Projekt Besserer-Ticketautomat unter einem neuen Namen
und implementieren Sie die Ihrer Meinung nach notwendigen Änderungen.

2.18 Zusammenfassung des besseren


Ticketautomaten
In der neuen Version der Klasse Ticketautomat haben wir die Schwächen der nai-
ven Version behoben. Dabei haben wir zwei neue Sprachkonzepte eingeführt:
die bedingte Anweisung und lokale Variablen.
 Die bedingte Anweisung gibt uns die Möglichkeit, eine Prüfung vorzunehmen
und abhängig vom Ergebnis dieser Prüfung eine von zwei möglichen Aktionen
durchzuführen.
 Mit lokalen Variablen können wir temporäre Werte in Konstruktoren und Metho-
den berechnen und speichern. Sie unterstützen das Verhalten der Methode, in
der sie definiert wurden, aber ihre Werte gehen verloren, sobald der Konstruktor
oder die Methode beendet ist.
Weitere Details über bedingte Ausführungen und darüber, wie ihre Prüfungen
aussehen können, finden Sie in Anhang D.

2.19 Übungen zur Selbstüberprüfung


Dieses Kapitel ist sehr umfangreich und wir haben viele neue Konzepte vorge-
stellt. In weiteren Kapiteln werden wir auf diesen Konzepten aufbauen, daher ist
es wichtig, dass Sie sie wirklich gut beherrschen. Überprüfen Sie durch das Bear-
beiten der folgenden Textaufgaben, ob Sie mit den neu vorgestellten Begriffen
vertraut geworden sind. Lassen Sie sich nicht davon abschrecken, dass Sie nur
Texte verfassen müssen, die nicht in BlueJ verarbeitet werden. Es übt ungemein,
Dinge auch einmal ohne einen Compiler auszuprobieren.

Übung 2.64 Schreiben Sie den Namen und den Ergebnistyp der folgenden
Methode auf:
public String gibName()
{
return name;
}

84
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.19 Übungen zur Selbstüberprüfung

Übung 2.65 Schreiben Sie den Namen der folgenden Methode sowie den
Namen und den Typ ihres Parameters auf:
public void setzeKreditrahmen(int rahmen)
{
kreditrahmen = rahmen;
}

Übung 2.66 Schreiben Sie die äußere Klammer einer Klasse Person. Denken Sie
an die geschweiften Klammern, die den Anfang und das Ende des Innenteils
markieren, aber lassen sie ihn ansonsten leer.
Übung 2.67 Schreiben Sie die Definitionen folgender Datenfelder auf:
 ein Datenfeld name vom Typ String
 ein Datenfeld vom Typ int, das alter heißt
 ein Datenfeld vom Typ String, das nummer heißt
 ein Datenfeld kreditrahmen vom Typ int
Übung 2.68 Schreiben Sie den Konstruktor einer Klasse Modul. Der Kons-
truktor soll einen einzelnen Parameter vom Typ String entgegennehmen,
der modulnummer heißt. Der Rumpf des Konstruktors sollte den Wert dieses
Parameters an ein Datenfeld namens nummer zuweisen. Sie müssen nicht die
Definition von nummer angeben, lediglich den Text des Konstruktors.

Übung 2.69 Schreiben Sie den Konstruktor für eine Klasse Person. Der Kons-
truktor soll zwei Parameter entgegennehmen. Der erste ist vom Typ String
und heißt meinName. Der zweite ist vom Typ int und heißt meinAlter. Der erste
soll zum Initialisieren eines Datenfeldes mit Namen name benutzt werden, der
zweite soll das Datenfeld alter setzen. Sie müssen nicht die Definition der
Datenfelder angeben, lediglich den Text des Konstruktors.
Übung 2.70 Korrigieren Sie den Fehler in dieser Methode:
public void gibAlter()
{
return alter;
}

Übung 2.71 Schreiben Sie eine sondierende Methode namens gibName, die
den Wert eines Datenfeldes name vom Typ String liefert.

Übung 2.72 Schreiben Sie eine verändernde Methode namens setzeAlter,


die einen einzelnen Parameter vom Typ int bekommt und den Wert eines
Datenfeldes alter setzt.

Übung 2.73 Schreiben Sie eine Methode namens detailsAusgeben in einer


Klasse, die ein Datenfeld name vom Typ String hat. Die Methode details-
Ausgeben sollte den String "Der Name dieser Person ist" ausgeben, gefolgt
vom Wert des Datenfeldes name. Wenn beispielsweise der Wert des Daten-
feldes name "Steffi" ist, dann würde detailsAusgeben ausgeben:
Der Name dieser Person ist Steffi

85
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Wenn Sie die meisten oder alle Aufgaben erfolgreich bearbeitet haben, dann
könnten Sie als Nächstes ein neues Projekt in BlueJ anlegen und darin eine Klas-
sendefinition für Personen erzeugen. Die Klasse könnte beispielsweise Datenfel-
der haben, die den Namen und das Alter einer Person halten. Wenn Sie sich bei
der Bearbeitung der Aufgaben unsicher gefühlt haben, dann sehen Sie sich die
früheren Abschnitte dieses Kapitels und den Quelltext der Klasse Ticketautomat
noch einmal an und machen Sie sich bewusst, was Ihnen unklar geblieben ist. Im
nächsten Abschnitt folgt weiteres Vertiefungsmaterial.

2.20 Vertrautes neu betrachtet


Bis zu diesem Punkt haben Sie etliche neue Konzepte kennengelernt. Um diese
Konzepte weiter zu vertiefen, sollten wir sie nun anhand eines anderen, aber schon
vertrauten Kontextes erneut betrachten. Doch Achtung! Dabei werden Ihnen ein
oder zwei weitere neue Konzepte begegnen, die wir in späteren Kapiteln eingehen-
der behandeln werden.
Öffnen Sie das Projekt Laborkurse, das wir in Kapitel 1 kennengelernt haben, und
öffnen Sie mit dem Editor die Klasse Student (Listing 2.9).

Listing 2.9
Die Klasse Student.

86
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.20 Vertrautes neu betrachtet

Zu den Daten, die wir in diesem kleinen Beispiel über einen Studenten speichern
möchten, gehören sein Name, seine Matrikelnummer und die Anzahl der Scheine,
die er bisher erworben hat. Alle diese Informationen sollen für die Zeit seines Stu-
dentendaseins dauerhaft gespeichert werden, auch wenn sich in diesem Zeit-
raum einige der Daten ändern (z.B. die Anzahl der Scheine). Wir speichern diese
Informationen deshalb in Datenfeldern, um den Zustand eines Studenten wider-
zuspiegeln.
Die Klasse definiert drei Datenfelder: name, matrikelnummer und scheine. Diese wer-
den alle im Konstruktor initialisiert. Die Anfangswerte der ersten beiden werden
durch die Werte der Parameter bestimmt, die an den Konstruktor übergeben
werden. Für jedes Datenfeld gibt es eine sondierende gib-Methode, aber nur
für name und scheine gibt es verändernde Methoden. Der Wert des Datenfelds
matrikelnummer bleibt somit konstant, nachdem das Objekt erzeugt wurde. Wenn
der Wert eines Datenfeldes nach der Initialisierung nicht mehr verändert werden
kann, sagen wir, das Datenfeld ist unveränderlich. Manchmal machen wir den
kompletten Zustand eines Objekts unveränderlich, sobald es erzeugt wurde. Die
String-Klasse ist ein wichtiges Beispiel hierfür.

87
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

2.21 Methoden aufrufen


Die Methode gibLoginName benutzt ein neues Konstrukt, das wir näher betrachten
wollen:
public String gibLoginName()
{
return [Link](0,4) + [Link](0,3);
}
Hier sehen wir zwei Dinge in Aktion:
 Der Aufruf einer Methode für ein anderes Objekt, wobei die Methode ein Ergeb-
nis zurückliefert.
 Die Verwendung des als Ergebnis zurückgelieferten Werts als Teil eines Ausdrucks.
Sowohl name als auch matrikelnummer sind Zeichenketten, also vom Typ String. Die
Klasse String definiert eine sondierende Methode substring mit dem folgenden
Kopf:
/**
* Liefere eine neue Zeichenkette, die die Zeichen von
* beginIndex bis (endIndex-1) dieser Zeichenkette enthält.
*/
public String substring(int beginIndex, int endIndex)
Ein Indexwert von null bezeichnet das erste Zeichen einer Zeichenkette, also nimmt
gibLoginName die ersten vier Zeichen aus der Zeichenkette name, die ersten drei Zei-
chen der matrikelnummer und verkettet sie zu einer neuen Zeichenkette. Diese neue
Zeichenkette wird als Ergebnis der Methode zurückgeliefert. Wenn beispielsweise
name den Wert "Leonardo da Vinci" hat und matrikelnummer den Wert "468366",
dann würde die Zeichenkette "Leon468" von der Methode zurückgeliefert.
In Kapitel 3 werden wir näher auf Methodenaufrufe zwischen Objekten eingehen.

Übung 2.74 Erstellen Sie ein ähnliches Diagramm wie in Abbildung 2.3 für
den Anfangszustand einer Instanz der Klasse Student, nachdem diese mit den
folgenden aktuellen Parametern erzeugt wurde:
new Student("Benjamin Jonson", "738321")
Übung 2.75 Was würde von gibLoginName zurückgeliefert für einen Student
mit name "Henry Moore" und matrikelnummer "557214"?
Übung 2.76 Erzeugen Sie eine Instanz von Student mit name "djb" und mat-
rikelnummer "859012". Was passiert, wenn an diesem Objekt gibLoginName
aufgerufen wird? Woran könnte das Ihrer Meinung nach liegen?
Übung 2.77 Die Klasse String definiert eine sondierende Operation length,
die die Länge einer Zeichenkette zurückliefert. Sie hat folgenden Kopf:
/**
* Liefere die Anzahl der Zeichen in diesem String zurück.
*/
public int length()

88
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.22 Ausdrücke testen: die Direkteingabe

Fügen Sie bedingte Anweisungen in den Konstruktor von Student ein, die
eine Fehlermeldung ausgeben, wenn der Parameter vollerName weniger als
vier Zeichen hat oder wenn matrNr weniger als drei Zeichen hat. In jedem Fall
soll der Konstruktor die übergebenen Werte benutzen, um die Datenfelder
name und matrikelnummer zu initialisieren, auch wenn eine Fehlermeldung
ausgegeben wird. Hinweis: Benutzen Sie eine if-Anweisung der folgenden
Form (also ohne einen else-Teil), um die Fehlermeldung auszugeben:
if (führe eine Prüfung an einem der Parameter aus) {
Fehlermeldung ausgeben, wenn die Prüfung true geliefert hat
}
Falls notwendig, können Sie die verschiedenen Formen einer bedingten Anwei-
sung in Anhang D nachlesen.
Übung 2.78 Zusatzaufgabe. Verändern Sie die Methode gibLoginName in
Student so, dass sie immer einen Login-Namen zurückliefert, auch wenn name
oder matrikelnummer nicht ausreichend lang sind. Für Zeichenketten, die kür-
zer sind, soll einfach die gesamte Zeichenkette genommen werden.

2.22 Ausdrücke testen: die Direkteingabe


In den vorangehenden Abschnitten haben wir verschiedene Ausdrücke zur Durch-
führung diverser Operationen kennengelernt: etwa die Addition gesamtsumme + preis
aus dem Ticketautomaten-Beispiel oder den Ausdruck [Link](0,4) im
Quelltext der Klasse Student.
Im weiteren Verlauf des Buches werden wir noch vielen solchen Operationen
begegnen, einige mit Operatorsymbolen (z.B. +), andere in Form von Methoden-
aufrufen (z.B. substring). Um sich mit der Funktionsweise neuer Operatoren und
Methoden vertraut zu machen, empfiehlt es sich, sie anhand einer Reihe von Bei-
spielen auszuprobieren.

Abbildung 2.5
Die Direkteingabe
von BlueJ.

89
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Die Direkteingabe, die wir bereits kurz in Kapitel 1 angesprochen haben, kann uns
helfen, mit Java-Ausdrücken zu experimentieren (Abbildung 2.5). Hier können wir
Ausdrücke direkt eintippen, um sie dann umgehend auszuwerten und die Ergeb-
nisse auszugeben. Dieses Werkzeug werden Sie beim Ausprobieren neuer Opera-
toren und Methoden nicht mehr missen wollen.

Übung 2.79 Betrachten Sie die folgenden Ausdrücke und versuchen Sie, das
jeweilige Ergebnis vorherzusagen. Tippen Sie die Ausdrücke danach in die
Direkteingabe ein und vergleichen Sie die Ergebnisse mit Ihren Vorhersagen.
99 + 3
"See" + "wolf"
"See" + 9
9 + 3 + "See"
"See" + 3 + 9
"Seewolf".substring(3,4)
"Seewolf".substring(3,8)
Hat Sie diese Übung etwas Neues gelehrt? Wenn ja, was?

Wenn die Auswertung eines Ausdrucks in der Direkteingabe ein Objekt ergibt (z.B.
ein String-Objekt), wird dieses durch ein kleines, rotes Objektsymbol neben der Zeile,
die das Ergebnis enthält, gekennzeichnet. Sie können auf dieses Symbol zur genaue-
ren Untersuchung doppelklicken oder es zur weiteren Verwendung in die Objekt-
leiste ziehen. Darüber hinaus können Sie in der Direkteingabe Variablen deklarieren
und ganze Anweisungen schreiben.
Wann immer Sie auf für Sie neue Operatoren oder Methodenaufrufen treffen,
sollten Sie diese in der Direkteingabe ausprobieren, um ein Gefühl für ihr Verhalten
zu bekommen.
Sie können in der Direkteingabe testen, wie Sie Variablen verwenden. Versuchen
Sie folgenden Code:
summe = 99 + 3;
Damit erhalten Sie folgende Fehlermeldung:
Error: cannot find symbol – variable summe
Der Grund ist, dass in Java jede Variable (in diesem Fall summe) einen Datentyp zuge-
teilt bekommen muss, bevor sie verwendet werden kann. Erinnern wir uns, dass
jedes Mal, wenn ein Datenfeld, ein Parameter oder eine lokale Variable zum ersten
Mal im Quelltext eingeführt wird, eine Typbezeichnung wie String oder int davor
stehen muss. Versuchen Sie jetzt Folgendes in die Direkteingabe einzugeben:
int summe = 0;
summe = 99 + 3;
Dieses Mal gibt es keine Beschwerden, da summe mit einer Typbezeichnung einge-
führt wurde und damit ohne Wiederholung des Datentyps verwendet werden
kann. Wenn Sie danach
summe

90
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.22 Ausdrücke testen: die Direkteingabe

allein auf einer Zeile eingeben (ohne Semikolon), sehen Sie den Wert, der gerade
darin gespeichert ist.
Versuchen Sie jetzt folgenden Code in der Direkteingabe:
String schwimmer = "katzen" + "fisch";
schwimmer
Hier haben wir der Variablen schwimmer ebenfalls einen passenden Typ vorange-
stellt, was uns erlaubt, eine Zuweisung vorzunehmen und herauszufinden, was die
Variable speichert. Dieses Mal haben wir uns dafür entschieden, der Variable direkt
bei der Deklaration auch gleich den gewünschten Wert zuzuweisen.
Welches Ergebnis würden Sie für folgenden Code erwarten?
String fisch = schwimmer;
fisch
Probieren Sie es aus. Was glauben Sie, was in der Zuweisung passiert?

Übung 2.80 Öffnen Sie die Direkteingabe im Projekt Besserer-Ticketauto-


mat. Tippen Sie folgenden Code in die Direkteingabe:
Ticketautomat t1 = new Ticketautomat(1000);
[Link]()
[Link]();
[Link]()
Tippen Sie diese Zeilen genauso ein, wie sie hier erscheinen. Achten Sie dabei
besonders auf die vorhandenen bzw. nicht vorhandenen Semikolons am Ende
der Zeile. Was liefern die Aufrufe von gibBisherGezahltenBetrag in jedem Fall
zurück?
Übung 2.81 Fügen Sie jetzt Folgendes über die Direkteingabe hinzu:
Ticketautomat t2 = t1;
Was, glauben Sie, liefert ein Aufruf von [Link] zurück?
Versuchen Sie es.
Übung 2.82 Fügen Sie Folgendes hinzu:
[Link](500);
Was, glauben Sie, liefert der folgende Code zurück? Überlegen Sie sorgfältig,
bevor Sie es ausprobieren, und achten Sie darauf, dieses Mal die Variable t2
zu verwenden.
[Link]()
Haben Sie die Antwort erhalten, die Sie erwartet haben? Können Sie zwi-
schen den Variablen t1 und t2 eine Verbindung finden, die erklären würden,
was passiert?

91
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Zusammenfassung
In diesem Kapitel haben wir die Grundlagen besprochen, die Sie zum Defi-
nieren von Klassen benötigen. Klassen enthalten Datenfelder, Konstruktoren
und Methoden, die die Zustände und das Verhalten von Objekten definie-
ren. Im Rumpf von Konstruktoren und Methoden implementieren Folgen
von Anweisungen diesen Teil ihres Verhaltens. Wir haben Zuweisungen und
bedingte Anweisungen behandelt, weitere Arten von Anweisungen werden
wir in späteren Kapiteln betrachten.

NEUE BEGRIFFE IN DIESEM KAPITEL


Datenfeld, Instanzvariable, Konstruktor, Methode, Kopf einer Methode,
Rumpf einer Methode, aktuelle Parameter, formale Parameter, sondie-
rende Methode, verändernde Methode, Deklaration, Initialisierung, Block,
Anweisung, Zuweisung, bedingte Anweisung, Rückgabeanweisung,
Ergebnistyp, Kommentar, Ausdruck, Operator, Variable, lokale Variable,
Sichtbarkeit, Lebensdauer.

92
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.22 Ausdrücke testen: die Direkteingabe

Zusammenfassung der Konzepte


 Objekterzeugung Einige Objekte können nur dann erzeugt werden, wenn
zusätzliche Informationen bereitgestellt werden.
 Datenfeld Datenfelder speichern die Daten, die ein Objekt benutzt. Daten-
felder werden auch als Instanzvariablen bezeichnet.
 Kommentar Kommentare werden im Quelltext einer Klasse angegeben,
um menschlichen Lesern das Verstehen des Codes zu erleichtern. Sie haben
keinen Einfluss auf die Funktionalität einer Klasse.
 Konstruktor Konstruktoren ermöglichen, dass ein Objekt nach seiner
Erzeugung in einen gültigen Zustand versetzt wird.
 Sichtbarkeit Durch die Sichtbarkeit einer Variablen wird der Bereich inner-
halb des Quelltextes definiert, in dem eine Variable zugreifbar ist.
 Lebensdauer Die Lebensdauer einer Variablen legt fest, wie lange sie
existiert, bevor sie zerstört wird.
 Zuweisung Zuweisungen speichern den Wert auf der rechten Seite eines
Zuweisungsoperators in der Variablen, die auf der linken Seite genannt ist.
 Sondierende Methode Sondierende Methoden liefern Informationen
über den Zustand eines Objekts.
 Verändernde Methode Verändernde Methoden ändern den Zustand
eines Objekts.
 println Die Methode [Link] gibt ihren Parameter auf der
Konsole aus.
 Bedingte Anweisung Eine bedingte Anweisung führt eine von zwei
Aktionen aus, abhängig vom Ergebnis einer Prüfung.
 Boolescher Ausdruck Boolesche Ausdrücke haben nur zwei mögliche
Werte: wahr (true) und falsch (false). Sie werden oft verwendet, um die
Auswahl zwischen zwei Ausführungspfaden in einer bedingten Anweisung
zu treffen.
 Lokale Variable Eine lokale Variable ist eine Variable, die innerhalb einer
Methode deklariert und benutzt wird. Sie ist nur innerhalb der Methode
zugreifbar und ihre Lebensdauer entspricht der ihrer Methode.

In den folgenden Übungen können Sie mit den Konzepten, die wir in diesem Kapi-
tel vorgestellt haben, experimentieren. In diesen Übungen werden Sie eigene Klas-
sen definieren, die Elemente wie Datenfelder, Konstruktoren, Methoden, Zuwei-
sungen und bedingte Anweisungen enthalten.

93
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.83 Unten sehen Sie das Grundgerüst einer Buch-Klasse, die Sie in
dem Projekt Buch-Aufgabe finden. Die Klasse definiert bereits zwei Daten-
felder und einen Konstruktor, der diese Felder initialisiert. In dieser und den
nächsten Übungen sollen Sie dieses Grundgerüst ausbauen.
Fügen Sie zwei sondierende Methoden hinzu – gibAutor und gibTitel –, die
den jeweiligen Wert der Datenfelder autor und titel zurückliefern. Testen
Sie Ihre Klasse, indem Sie einige Instanzen erzeugen und die Methoden auf-
rufen.
/**
* Eine Klasse, deren Exemplare Informationen über ein Buch halten.
* Dies könnte Teil einer größeren Anwendung sein, einer
* Bibliothekssoftware beispielsweise.
*
* @author (Ihren Namen hier eintragen.)
* @version (das heutige Datum eintragen.)
*/
class Buch
{
// Exemplarvariablen
private String autor;
private String titel;

/**
* Setze den Autor und den Titel, wenn ein Exemplar erzeugt wird.
*/
public Buch(String buchautor, String buchtitel)
{
autor = buchautor;
titel = buchtitel;
}

// weitere Methoden hier einfügen ...


}
Übung 2.84 Fügen Sie der Klasse Buch zwei Methoden autorAusgeben und
titelAusgeben hinzu. Diese Methoden sollen die entsprechenden Datenfelder
auf der Konsole ausgeben.
Übung 2.85 Fügen Sie ein weiteres Datenfeld seiten hinzu, das die Anzahl
der Seiten eines Buches speichert. Dieses Feld sollte vom Typ int sein und
seinen Anfangswert durch den Konstruktor bekommen, indem dieser neben
buchautor und buchtitel einen weiteren Parameter erhält. Fügen Sie auch eine
entsprechende sondierende Methode gibSeiten hinzu.
Übung 2.86 Sind die Buch-Objekte, die Sie implementiert haben, unveränder-
lich? Begründen Sie Ihre Antwort.

94
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.22 Ausdrücke testen: die Direkteingabe

Übung 2.87 Fügen Sie der Klasse Buch eine Methode detailsAusgeben hinzu.
Diese sollte den Autor, den Titel und die Anzahl der Seiten auf der Konsole
ausgeben. Es ist Ihnen überlassen, wie Sie die Ausgabe formatieren. Bei-
spielsweise können alle Angaben auf einer Zeile ausgegeben werden oder
jede Information auf einer eigenen Zeile. Sie können auch erläuternden Text
mit ausgeben, der die einzelnen Informationen einleitet, etwa in der Form:
Titel: Robinson Crusoe, Autor: Daniel Defoe, Seiten: 232
Übung 2.88 Fügen Sie in der Klasse Buch ein weiteres Datenfeld refNummer
für eine Referenznummer ein. Dieses Feld könnte beispielsweise eine ein-
deutige Kennzeichnung in einer Bibliothek modellieren. Es sollte vom Typ
String sein und im Konstruktor mit einer leeren Zeichenkette ("") initialisiert
werden, da es seinen Wert nicht als Konstruktorparameter bekommen soll.
Definieren Sie stattdessen eine verändernde Methode mit folgendem Kopf:
public void setzeRefNummer(String ref)
Im Rumpf dieser Methode sollte der Wert des Parameters dem Datenfeld
refNummer zugewiesen werden. Fügen Sie eine zugehörige Methode gibRef-
Nummer ein, mit deren Hilfe Sie testen können, ob die verändernde Methode
korrekt arbeitet.
Übung 2.89 Passen Sie Ihre Methode detailsAusgeben so an, dass sie auch
die Referenznummer ausgibt. Allerdings soll die Nummer nur dann ausge-
geben werden, wenn tatsächlich ein Wert eingetragen wurde – also wenn
das Datenfeld eine nichtleere Zeichenkette enthält. Falls das Datenfeld nicht
gesetzt wurde, geben Sie stattdessen "ZZZ" aus. Hinweis: Benutzen Sie eine
bedingte Anweisung, die in ihrer Prüfung die Methode length an refNummer
aufruft.
Übung 2.90 Verändern Sie die Methode setzeRefNummer so, dass sie den
Wert des Datenfelds nur ändert, wenn die übergebene Zeichenkette min-
destens drei Zeichen lang ist. Wenn der Parameter kürzer ist, lassen Sie eine
Fehlermeldung ausgeben und das Datenfeld unverändert.
Übung 2.91 Fügen Sie der Klasse Buch ein weiteres Datenfeld ausgeliehen
vom Typ int hinzu. Es soll einen Zähler halten, der angibt, wie häufig das Buch
bereits ausgeliehen wurde. Fügen Sie eine verändernde Methode ausleihen
hinzu, die den Zähler bei jedem Aufruf um eins erhöht. Fügen Sie eine
Methode gibAusgeliehen hinzu, die den Wert des neuen Datenfelds als Ergeb-
nis liefert. Erweitern Sie die Methode detailsAusgeben so, dass auch die Infor-
mation des neuen Felds mit einem erläuternden Text ausgegeben wird.
Übung 2.92 Fügen Sie zu der Klasse Buch ein boolesches Datenfeld namens
kursText. Dieses gibt an, ob ein Buch als Fachbuch zu einem Kurs verwendet
wird oder nicht. Dieses Datenfeld sollte über einen Parameter im Konstruk-
tor gesetzt werden und unveränderlich sein. Ergänzen Sie zu diesem Zweck
eine sondierende Methode namens istKursText.

95
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 2 Klassendefinitionen

Übung 2.93 Zusatzaufgabe. Erzeugen Sie in BlueJ ein neues Projekt Heizungs-
aufgabe. Editieren Sie den Text der Projektbeschreibung – diese ist symboli-
siert durch das Textsymbol in der linken oberen Ecke des Projektdiagramms.
Erzeugen Sie eine Klasse Heizung, die ein einzelnes Datenfeld temperatur ent-
hält vom Typ doppelte Fließkommazahl. (In Anhang B, Abschnitt B.1 finden Sie
die Typbezeichnung für diese Beschreibung.) Definieren Sie einen Konstruk-
tor ohne Parameter, der den Anfangswert von temperatur auf 15.0 setzt.
Definieren Sie verändernde Methoden waermer und kuehler, die die Tempera-
tur um jeweils 5 Grad erhöhen bzw. senken. Definieren Sie eine sondie-
rende Methode, die den Wert von temperatur liefert.
Übung 2.94 Zusatzaufgabe. Fügen Sie Ihrer Klasse Heizung drei neue Daten-
felder hinzu, die doppelte Fließkommazahlen aufnehmen: min, max und
schrittweite. Die Werte von min und max sollen durch Konstruktorparameter
initialisiert werden. Der Wert von schrittweite sollte im Konstruktor auf 5.0
gesetzt werden. Bevor Sie weitermachen, sollten Sie zuerst prüfen, ob alles
noch funktioniert.
Verändern Sie dann die Methode waermer so, dass sie die Temperatur nicht auf
einen Wert größer als max setzt. Entsprechend passen Sie auch die Methode
kuehler so an, dass sie temperatur nicht unter den Wert von min geraten lässt.
Überprüfen Sie, ob die Klasse noch funktioniert. Fügen Sie nun eine Methode
setzeSchrittweite mit einem int-Parameter ein, mit der schrittweite gesetzt
werden kann. Auch hier sollten Sie wieder testen, indem Sie einige Instanzen in
BlueJ erzeugen und diese interaktiv prüfen. Funktioniert alles noch korrekt,
wenn ein negativer Wert an die Methode setzeSchrittweite übergeben wird?
Fügen Sie in der Methode eine Prüfung ein, die das Setzen eines negativen
Werts verhindert.

96
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

3 Objektinteraktion

Lernziele
Zentrale Konzepte in diesem Kapitel: Abstraktion, Objektdiagramme,
Modularisierung, Methodenaufrufe, Objekterzeugung, Debugger
Java-Konstrukte in diesem Kapitel: Klassen als Typen, logische Operatoren
(&&, ||), Verkettung von Zeichenketten, Modulo-Operator (%), Objekterzeugung
(new), Methodenaufrufe (Punkt-Notation), this

In den vorherigen Kapiteln haben wir untersucht, was Objekte sind und wie sie
implementiert werden. Insbesondere haben wir Datenfelder, Konstruktoren und
Methoden diskutiert, als wir Klassendefinitionen betrachtet haben.
Wir werden nun einen Schritt weitergehen. Für die Konstruktion interessanter
Anwendungen reicht es nicht, individuell funktionierende Objekte zu erstellen.
Zusätzlich müssen diese Objekte miteinander kombiniert werden, damit sie gemein-
sam eine Aufgabe bearbeiten können. In diesem Kapitel werden wir eine kleine
Anwendung aus drei Objekten erstellen, in der einzelne Methoden andere Metho-
den aufrufen müssen, um ihre Aufgabe zu erfüllen.

3.1 Das Uhren-Beispiel


Das Projekt, das wir für die Diskussion von interagierenden Objekten benutzen
werden, modelliert die Anzeige einer Digitaluhr. Diese Anzeige zeigt Stunden und
Minuten, voneinander getrennt durch einen Doppelpunkt (Abbildung 3.1). Wir
gehen vorläufig von einer Uhr mit einer 24-Stunden-Anzeige aus. Die Anzeige
zeigt also Zeitpunkte von 00:00 (Mitternacht) bis 23:59 (eine Minute vor Mitter-
nacht). Eine Anzeige für eine 2-mal-12-Stunden-Zählung wäre ein wenig kompli-
zierter – deshalb schieben wir diese Variante bis zum Ende dieses Kapitels auf.

Abbildung 3.1
Die Anzeige einer
11:03 Digitaluhr.

97
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

3.2 Abstraktion und Modularisierung


Unser erster Ansatz könnte sein, die gesamte Anzeige in einer einzigen Klasse zu
realisieren. Schließlich ist es das, was wir bisher getan haben: Klassen erstellen,
die eine Aufgabe erfüllen.
Wir gehen das Problem jedoch etwas anders an. Wir werden untersuchen, ob wir
bei unserer Aufgabe Teilaufgaben identifizieren können, die wir mit eigenen Klas-
sen modellieren können. Der Grund für dieses Vorgehen ist Komplexität. Im Laufe
dieses Buches werden die Programmbeispiele, die wir untersuchen, immer kom-
plizierter werden. Einfache Aufgaben wie ein Ticketautomat können als Einzelauf-
gaben betrachtet werden. Man kann die gesamte Aufgabe betrachten und eine
Lösung mit nur einer Klasse vorschlagen. Für kompliziertere Aufgaben reicht dieses
Vorgehen nicht mehr aus. Mit zunehmendem Aufgabenumfang wird es auch
zunehmend schwieriger, alle Details des Problembereichs im Auge zu behalten.

Konzept Das Mittel, mit dem wir zu hohe Komplexität in Aufgaben angehen, ist Abstrak-
tion. Wir zerlegen eine Aufgabe in Teilaufgaben, diese wieder in Unterteilauf-
Abstraktion ist
gaben etc., bis die einzelnen Aufgaben klein genug für eine übersichtliche Lösung
die Fähigkeit,
Details von sind. Sobald wir eine Unteraufgabe gelöst haben, können wir diesen Teil als erle-
Bestandteilen zu digt betrachten und als Baustein für die nächste Aufgabe ansehen. Diese Technik
ignorieren, um den ist auch unter der Bezeichnung teile und herrsche bekannt.
Fokus der Betrach-
tung auf eine Wir wollen dies an einem Beispiel verdeutlichen. Stellen Sie sich Ingenieure bei
höhere Ebene einem Automobilhersteller vor, die ein neues Fahrzeug entwerfen sollen. Einige die-
lenken zu können. ser Ingenieure betrachten spezifische Aspekte des Fahrzeugs wie die äußere Form,
die Größe und die Position des Motors, die Anzahl und Größe der Sitze im Innen-
raum, den exakten Radstand etc. Ein anderer Ingenieur, der zum Team gehört, das
den Motor entwirft, denkt an die Teile, aus denen ein Motor besteht: die Zylinder,
die Einspritztechnik, der Vergaser, die Elektronik etc. Dieser Ingenieur betrachtet den
Motor nicht als eine Einheit, sondern als ein komplexes Gebilde aus vielen Einzeltei-
len. Eines dieser Teile ist eine Zündkerze.
Dann gibt es einen weiteren Ingenieur (vermutlich bei einem anderen Unterneh-
men), der Zündkerzen entwirft. Er wiederum sieht eine Zündkerze als ein auf-
wendiges Gebilde aus vielen Einzelteilen an. Er hat möglicherweise aufwendige
Studien betrieben, welches Metall für die Kontakte besonders geeignet ist oder
welches Material und welcher Fertigungsprozess für die Isolierung verwendet
werden soll.
Das Gleiche gilt für viele andere Teile. Ein Designer auf äußerster Ebene sieht einen
Reifen als einzelnen Bestandteil an. Ein Ingenieur, der in der Fertigungskette an
ganz anderer Stelle positioniert ist, kann hingegen Tage damit verbringen, die ide-
ale chemische Zusammensetzung für das Material der Reifen zu entwerfen. Für
den Reifeningenieur ist ein Reifen ein komplexes Gebilde. Das Automobilunterneh-
men hingegen kauft die Reifen lediglich ein und betrachtet sie als einzelnes Teil
eines Fahrzeugs. Das ist das Prinzip der Abstraktion.

98
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.3 Abstraktion in Software

Der Ingenieur beim Automobilhersteller abstrahiert von den Details der Reifen-
herstellung, um sich auf die Konstruktion der Räder eines Fahrzeugs konzentrie-
ren zu können. Der Fahrzeugdesigner wiederum abstrahiert von den technischen
Details der Räder und des Motors, um das Gesamtfahrzeug entwerfen zu kön-
nen (und ist dabei lediglich an der Größe von Motor und Rädern interessiert).
Dies gilt für alle Bestandteile. Während sich jemand mit dem Entwurf des Innen-
raums beschäftigt, entwickelt ein anderer den Stoff, mit dem die Sitze bezogen
werden.
Der entscheidende Punkt ist: Wenn man nur genau genug hinsieht, dann besteht
ein Fahrzeug aus so vielen Einzelteilen, dass es für eine einzelne Person praktisch
unmöglich ist, alle Details aller Teile zu kennen. Wenn das notwendig wäre, würde
kein Fahrzeug hergestellt werden können.
Ingenieure können so erfolgreich Fahrzeuge bauen, weil sie Modularisierung und Konzept
Abstraktion einsetzen. Sie zerteilen ein Fahrzeug in unabhängige Module (Rad,
Modularisie-
Motor, Getriebe, Sitz, Steuerrad etc.) und lassen verschiedene Personen parallel
rung ist der Pro-
an diesen verschiedenen Modulen arbeiten. Nachdem ein Modul erstellt wurde, zess der Zerlegung
verwenden sie Abstraktion. Sie sehen die Module als einzelne Komponenten an, eines Ganzen in
die zu komplexeren Komponenten zusammengesetzt werden. wohldefinierte
Teile, die getrennt
Modularisierung und Abstraktion sind somit komplementär. Modularisierung erstellt und unter-
bedeutet, große Dinge (Probleme, Aufgaben) in kleinere Teile zu zerlegen, während sucht werden kön-
Abstraktion der Prozess ist, Details zu ignorieren, um das Gesamtbild zu erfassen. nen und die in
wohldefinierter
Weise interagieren.

3.3 Abstraktion in Software


Die gerade diskutierten Prinzipien von Modularisierung und Abstraktion gelten in
gleicher Weise für die Softwareentwicklung. Um den Überblick in komplexen
Anwendungen zu behalten, versuchen wir, Subkomponenten zu identifizieren und
unabhängig zu implementieren. Dann versuchen wir, diese Subkomponenten als
einfache Bestandteile zu betrachten, ohne uns mit ihrer internen Komplexität zu
beschäftigen.
Bei der objektorientierten Programmierung sind diese Komponenten und Sub-
komponenten Objekte. Wenn wir ein Fahrzeug durch Software in einer objekt-
orientierten Sprache modellieren wollen, dann gehen wir wie die Kfz-Ingenieure
vor. Anstatt ein Fahrzeug als einen großen, monolithischen Block zu implemen-
tieren, würden wir zuerst getrennte Objekte für den Motor, das Getriebe, einen
Sitz etc. entwickeln und das Fahrzeug anschließend aus diesen kleineren Teilen
zusammensetzen.
Es ist nicht immer leicht, für ein gegebenes Problem herauszufinden, welche
Objekte (und damit auch Klassen) in einer Softwarelösung notwendig sind. Darauf
werden wir in späteren Abschnitten dieses Buches noch zu sprechen kommen. An
dieser Stelle wollen wir mit einem relativ einfachen Beispiel beginnen. Wenden wir
uns also wieder unserer Digitaluhr zu.

99
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

3.4 Modularisierung im Uhren-Beispiel


Wir wollen das Beispiel der Zeitanzeige genauer betrachten. Wir wollen die gerade
besprochenen Abstraktionskonzepte benutzen, um einen geeigneten Weg zu fin-
den, wie wir die Aufgabe mit einigen Klassen lösen können. Eine mögliche Sicht-
weise wäre zu sagen, dass die Anzeige aus vier Ziffern besteht (zwei für die Stun-
den, zwei für die Minuten). Wenn wir nun von dieser grundlegenden Sichtweise
etwas abstrahieren, dann können wir die Anzeige auch als zwei getrennte Anzei-
gen mit je einem Ziffernpaar ansehen (ein Paar für die Stunden, ein Paar für die
Minuten). Die eine Anzeige startet bei 0, wird jede Stunde um eins erhöht und
springt auf 0 zurück, sobald die 23 überschritten wird. Die andere springt nach
dem Überschreiten der 59 wieder auf 0 zurück. Die Ähnlichkeit im Verhalten dieser
beiden Anzeigen könnte uns wiederum dazu führen, von ihren Unterschieden zu
abstrahieren. Wir könnten sie als Objekte ansehen, die die Werte von 0 bis zu
einem bestimmten Wert anzeigen können. Der Wert der Anzeige kann inkremen-
tiert werden und sobald der Wert ein Limit überschreitet, springt er wieder auf 0
zurück. Nun haben wir möglicherweise eine angemessene Ebene der Abstraktion
erreicht, die wir durch eine Klasse repräsentieren können: eine Klasse für zweiziff-
rige Anzeigen.

Abbildung 3.2
Eine zweiziffrige
Nummernanzeige. 03

Für die Anzeige unserer Uhr sollten wir also zuerst eine Klasse für solche zweiziff-
rigen Anzeigen (Abbildung 3.2) entwickeln. Diese Klasse sollte eine sondierende
Methode haben, mit der der Wert der Anzeige abgefragt werden kann, und zwei
verändernde Methoden, um den Wert setzen und erhöhen zu können. Sobald
wir diese Klasse haben, können wir zwei Objekte dieser Klasse mit unterschied-
lichen Limits erzeugen und daraus die gesamte Uhrenanzeige zusammensetzen.

3.5 Implementierung der Uhrenanzeige


Nach der Diskussion im vorigen Abschnitt sollten wir zuerst eine zweiziffrige Num-
mernanzeige entwickeln. Eine solche Anzeige benötigt zwei Werte: das Limit, an
dem die Anzeige zurückspringen soll, und den aktuellen Anzeigewert. Wir wer-
den beide in unserer Klasse durch Datenfelder vom Typ int repräsentieren (Listing
3.1).

Listing 3.1
Die Klassendefinition
einer zweiziffrigen
Nummernanzeige.

100
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.6 Klassendiagramme und Objektdiagramme

Wir werden uns die weiteren Details dieser Klasse später ansehen. Wir wollen jetzt Konzept
erst einmal annehmen, dass wir die Klasse Nummernanzeige implementieren können,
und ein wenig mehr über die gesamte Uhrenanzeige nachdenken. Wir können eine Klassen defi-
nieren Typen:
komplette Uhrenanzeige implementieren, indem wir ein Objekt entwerfen, das
Ein Klassenname
intern zwei Nummernanzeigen enthält (eine für die Stunden, eine für die Minuten). kann als Typname in
Jede der Nummernanzeigen wäre ein Datenfeld in der Uhrenanzeige (Listing 3.2). einer Variablende-
An dieser Stelle machen wir von einem Konzept Gebrauch, das wir vorher schon klaration verwendet
einmal erwähnt haben: Klassen definieren Typen. werden. Variablen,
die als Typ eine
Klasse haben, kön-
nen Objekte dieser
Klasse halten.

Listing 3.2
Die Klasse Uhren-
Bei der Diskussion von Datenfeldern in Kapitel 2 haben wir gesagt, dass das Wort anzeige enthält zwei
private von einem Typ und einem Namen für das Datenfeld gefolgt wird. Hier Datenfelder vom Typ
benutzen wir nun die Klasse Nummernanzeige als den Typ für die Datenfelder stunden Nummernanzeige.
und minuten. Dies veranschaulicht, dass Klassen als Typen benutzt werden können.
Der Typ eines Datenfelds legt fest, welche Arten von Werten in dem Datenfeld ge-
speichert werden können. Wenn der Typ eine Klasse ist, kann das Datenfeld Objekte
dieser Klasse halten. Ein Datenfeld oder eine andere Variable eines Klassentyps zu de-
klarieren, erzeugt nicht automatisch ein Objekt dieses Typs – das Datenfeld ist im Ge-
genteil anfänglich leer. Wir haben jetzt noch kein Nummernanzeige-Objekt. Das ent-
sprechende Objekt muss explizit erzeugt werden und wir werden gleich sehen, wie
dies funktioniert, wenn wir uns dem Konstruktor der Klasse Uhrenanzeige zuwenden.

3.6 Klassendiagramme und


Objektdiagramme
Die im vorigen Abschnitt beschriebene Struktur (ein Objekt der Klasse Uhrenanzeige Konzept
enthält zwei Objekte der Klasse Nummernanzeige) kann in einem Objektdiagramm
Ein Klassendia-
visualisiert werden, wie in Abbildung 3.3a dargestellt. Aus diesem Diagramm kön-
gramm zeigt die
nen Sie ersehen, dass wir mit drei Objekten umgehen. Abbildung 3.3b zeigt das Klassen einer
Klassendiagramm für dieselbe Situation. Anwendung und
die Beziehungen
zwischen diesen
Klassen. Es liefert
Informationen über
den Quelltext. Es
präsentiert eine
statische Sicht auf
ein Programm.

Abbildung 3.3
Objektdiagramm und
Klassendiagramm für
eine Uhrenanzeige.

101
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Konzept Bemerkenswert ist, dass das Klassendiagramm lediglich zwei Klassen zeigt, wäh-
rend das Objektdiagramm drei Objekte zeigt. Dies liegt daran, dass wir von einer
Ein Objektdia-
Klasse mehrere Objekte erzeugen können. In diesem Fall erzeugen wir zwei Objekte
gramm zeigt die
Objekte und ihre der Klasse Nummernanzeige (Listing 3.3).
Beziehungen zu Diese beiden Diagramme bieten zwei unterschiedliche Sichtweisen in Bezug auf die-
einem bestimmten
selbe Anwendung. Das Klassendiagramm zeigt die statische Sicht. Es illustriert, wel-
Zeitpunkt während
der Ausführung che Bestandteile existieren, während das Programm geschrieben wird. Wir haben
einer Anwendung. zwei Klassen und der Pfeil besagt, dass die Klasse Uhrenanzeige die Klasse Nummern-
Es präsentiert eine anzeige benutzt (d.h., die Klasse Nummernanzeige wird im Quelltext der Klasse Uhren-
dynamische Sicht anzeige erwähnt). Wir sagen auch: Uhrenanzeige ist abhängig von Nummernanzeige.
auf ein Programm.
Um die Anwendung zu starten, erzeugen wir ein Objekt der Klasse Uhrenanzeige. Wir
werden die Uhrenanzeige so implementieren, dass sie automatisch zwei Objekte von
Nummernanzeige erzeugt. Das Objektdiagramm zeigt somit die Situation zur Laufzeit
(während das Programm ausgeführt wird). Diese Sicht wird auch die dynamische
Sicht genannt.

Konzept Das Objektdiagramm zeigt ein weiteres wichtiges Detail: Wenn eine Variable ein
Objekt enthält, dann ist dieses Objekt nicht direkt in der Variablen selbst abgelegt,
Objektrefe-
sondern die Variable enthält lediglich eine Referenz auf ein Objekt (eine Objekt-
renz: Variablen
von Objekt- referenz ). In dem Diagramm ist die Variable durch ein weißes Feld symbolisiert und
typen speichern die Referenz durch einen Pfeil. Das Objekt, das referenziert wird, ist außerhalb des
Referenzen auf Objekts gespeichert, das die Referenz hält. Die Objekte sind durch die Objektrefe-
Objekte. renz miteinander verbunden.
Der Unterschied zwischen diesen beiden Diagrammen und den damit verbunde-
nen Sichtweisen ist sehr wichtig. BlueJ zeigt lediglich die statische Sicht. Sie sehen
das Klassendiagramm im Hauptfenster eines Projekts. Um Java-Programme planen
und verstehen zu können, müssen Sie in der Lage sein, die Objektdiagramme auf
Papier oder in Ihrer Vorstellung zu zeichnen. Wenn wir darüber nachdenken, was
ein Programm tut, dann denken wir über die Struktur der beteiligten Objekte nach
und wie diese interagieren. Das Verständnis solcher Objektstrukturen ist in der
Objektorientierung unverzichtbar.

Übung 3.1 Denken Sie an das Projekt Laborkurse zurück, das wir in Kapitel 1
und Kapitel 2 diskutiert haben. Nehmen Sie an, wir erzeugen ein Laborkurs-
Objekt und drei Student-Objekte. Anschließend tragen wir die drei Studenten
in den Kurs ein. Versuchen Sie, ein Klassendiagramm und ein Objektdia-
gramm für diese Situation zu zeichnen. Zeigen Sie die Unterschiede zwischen
den Diagrammen auf und erläutern Sie diese.
Übung 3.2 Zu welchen Zeitpunkten kann sich ein Klassendiagramm ändern?
Wie wird es geändert?
Übung 3.3 Zu welchen Zeitpunkten kann sich ein Objektdiagramm ändern?
Wie wird es geändert?
Übung 3.4 Schreiben Sie eine Definition für ein Datenfeld tutor, das eine
Referenz auf ein Objekt des Typs Lehrender halten kann.

102
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.8 Der Quelltext im Projekt Zeitanzeige

3.7 Primitive Typen und Objekttypen


Java unterscheidet zwei sehr unterschiedliche Typarten: primitive Typen und Objekt- Konzept
typen. Sämtliche primitiven Typen in Java sind vordefiniert; int und boolean zählen
Die primitiven
beispielsweise zu diesen Typen. Eine komplette Liste aller primitiven Typen findet sich
Typen in Java sind
in Anhang B. Objekttypen sind die Typen, die durch Klassen definiert werden. Einige die Typen, die keine
Klassen sind in Java vordefiniert (wie String); andere schreiben wir selbst. Objekttypen sind.
Sowohl primitive Typen als auch Objekttypen können als Typen verwendet werden, Die gebräuchlichs-
ten primitiven
aber es gibt Situationen, in denen sie sich unterschiedlich verhalten. Ein Unterschied
Typen sind int,
ist, wie Werte gespeichert werden. Aus unseren Diagrammen konnten wir ersehen, boolean, char,
dass primitive Werte direkt in den Variablen gespeichert werden (wir haben die double und long.
Werte direkt in die Variablenfelder geschrieben, beispielsweise in Abbildung 2.3 in Primitive Typen
Kapitel 2). Objekte hingegen werden nicht direkt in den Variablen abgelegt, sondern haben keine
es wird nur eine Referenz auf ein Objekt gespeichert (symbolisiert durch einen Pfeil Methoden.
in unseren Diagrammen, Abbildung 3.3).
Wir werden später noch weitere Unterschiede zwischen primitiven Typen und
Objekttypen kennenlernen.

3.8 Die Klasse Nummernanzeige


Bevor wir den Quelltext des gesamten Zeitanzeige-Projekts analysieren, ist es hilf-
reich, sich zuerst die Klasse Nummernanzeige anzusehen, um zu verstehen, wie diese
zum Aufbau der Klasse Uhrenanzeige beiträgt. Sie finden diese Klasse im seperaten
Nummernanzeige-Projekt.
Listing 3.3 zeigt den kompletten Quelltext der Klasse Nummernanzeigen. Insgesamt
ist diese Klasse recht einfach zu verstehen, auch wenn sie ein paar neue Aspekte
von Java zeigt. Sie hat zwei Datenfelder, die bereits in Abschnitt 3.5 diskutiert
wurden, einen Konstruktor und vier Methoden (gibWert, setzeWert, gibAnzeigewert
und erhoehen).

Listing 3.3
Der Quelltext der
Klasse Nummern-
anzeige.

103
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Übung 3.5 Öffnen Sie das Projekt Nummernanzeige. Wählen Sie im Menü
ANSICHT den Befehl KONSOLE ANZEIGEN und dann METHODENAUFRUFE
PROTOKOLLIEREN, so wie Sie es schon mit dem Figuren-Projekt in Kapitel 1
gemacht haben. Dadurch haben Sie die Möglichekeit, das Ergebnis Ihrer
Interaktionen mit Objekten zu sehen – eine hiflreiche Funktion, wenn wir
uns das vollständige Uhrenanzeige-Projekt im Detail ansehen. Erzeugen Sie
nun ein Nummernanzeige-Objekt mit dem Namen stunden anstelle des von
BlueJ vorgegebenen Standardnamens. Verwenden Sie eine Überlaufgrenze
von 24. Öffnen Sie einen Objektinspektor für dieses Objekt. Rufen Sie die
Methode erhoehen des Objekts auf, während der Objektinspektor geöffnet
ist. Beobachten Sie, was im Objektinspektor angezeigt wird. Rufen Sie die
Methode erhoehen immer wieder auf, bis der Wert im Inspektor auf null
zurückspringt. (Wenn Sie ungeduldig sind, könnten Sie auch ein Nummern-
anzeige-Objekt mit kleinerem Limit erzeugen!)
Übung 3.6 Erzeugen Sie ein zweites Nummernanzeige-Objekt mit einem Limit
von 60, dem Sie den Namen minuten geben. Rufen Sie die erhoehen-
Methode auf und beobachten Sie, wie die Methode im Konsolenfenster
dargestellt wird. Stellen Sie sich vor, dass die stunden- und minuten-Objekte

104
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.8 Die Klasse Nummernanzeige

auf der Objektleiste die beiden Nummernanzeige-Objekte repräsentieren, die vom


Uhrenanzeige-Objekt verwaltet werden. Tatsächlich übernehmen Sie gerade die
Rolle des Uhrenanzeige-Objekts. Was sollten Sie jedes Mal tun, wenn Sie erhoe-
hen für minuten aufrufen, um zu entscheiden, ob es zurückgesprungen ist und
ob erhoehen daher für das stunden-Objekt aufgerufen werden sollte?
Übung 3.7 Wählen Sie im Menü ANSICHT den Befehl DIREKTEINGABE ANZEIGEN
aus. Erzeugen Sie in der Direkteingabe ein Nummernanzeige-Objekt mit dem
Limit 6:
Nummernanzeige na = new Nummernanzeige(6);
Rufen Sie anschließend in der Direkteingabe die Methoden gibWert, setzeWert
und erhoehen des Objekts auf (indem Sie beispielsweise [Link] eintippen).
Beachten Sie, dass Anweisungen (verändernde Methoden) im Gegensatz zu
Ausdrücken (sondierende Methoden) ein Semikolon am Ende erfordern. Der
Aufruf von setzeWert benötigt einen numerischen Parameterwert. Hier ist es
hilfreich, die aufgezeichneten Methodenaufrufe im Konsolenfenster zu ver-
wenden, um diese Methodenaufrufe korrekt zu schreiben.
Übung 3.8 Welche Fehlermeldung sehen Sie in der Direkteingabe, wenn
Sie die folgende Zeile eingeben?
[Link]();
Sehen Sie sich diese Fehldermeldung genau an und versuchen Sie, sie sich gut
einzuprägen – Ihnen werden wahrscheinlich in der Zukunft ähnliche Meldun-
gen bei vielen Gelegenheiten begegnen. Beachten Sie, dass der Fehler auf-
tritt, weil die Verwendung des Klassennamens, Nummernanzeige, anstatt der
Variablen na bei dem Versuch, die gibWert-Methode aufzurufen, falsch ist.
Übung 3.9 Welche Fehlermeldung sehen Sie in der Direkteingabe, wenn
Sie die folgende Zeile eingeben?
[Link](int 5);
Die Fehlermeldung ist tatsächlich nicht sehr hilfreich. Können Sie herausfin-
den, was an diesem Aufruf von setzeWert falsch ist, und ihn korrigieren?
Auch diese Fehlermeldung sollten Sie sich merken, da sie nach einem Fehler
aufaucht, der im frühen Lernstadium schnell passiert.

Der Konstruktor bekommt die Überlaufgrenze als Parameter. Wenn beispielsweise


24 als Limit angegeben wird, dann wird die Anzeige bei Erreichen dieses Werts auf
0 zurückspringen. Der Bereich, den die Anzeige dann anzeigen könnte, wäre 0 bis
23. Dies ermöglicht uns, die Anzeige sowohl für die Stunden als auch für die
Minuten zu verwenden. Für die Stundenanzeige werden wir eine Nummernanzeige
mit der Anzeigegrenze 24 erzeugen, für die Minutenanzeige eine mit der Grenze
60. Der Konstruktor speichert den übergebenen Wert in einem Datenfeld und
setzt anschließend den Wert der Anzeige auf 0.
Als Nächstes sehen wir eine einfache sondierende Methode, die uns den aktuellen
Anzeigewert liefert (gibWert). Diese erlaubt anderen Objekten, diesen Wert auszu-
lesen.

105
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

In den folgenden Abschnitten werden einige neue Funktionen besprochen, die in


den Methoden setzeWert, gibAnzeigewert und erhoehen verwendet wurden.

3.8.1 Die logischen Operatoren


Die folgende verändernde Methode setzeWert ist interessant, weil sie versucht
sicherzustellen, dass der Anfangswert eines Nummernanzeige-Objekts immer ein gül-
tiger Wert ist. Sie sieht folgendermaßen aus:
public void setzeWert(int ersatzwert)
{
if((ersatzwert >= 0) && (ersatzwert < limit)) {
wert = ersatzwert;
}
}
An dieser Stelle kann ein neuer Wert für die Anzeige als Parameter an die Methode
übergeben werden. Allerdings müssen wir, bevor wir den neuen Wert zuweisen,
seine Gültigkeit überprüfen. Der zugelassene Bereich für den Wert, wie oben disku-
tiert, geht von null bis eins unter dem Limit. Wir verwenden eine bedingte Anwei-
sung, um vor der Zuweisung zu prüfen, ob der Wert zulässig ist. Das Symbol ist der
logische Und-Operator. Er sorgt dafür, dass die Prüfung in der if-Anweisung nur
genau dann true liefert, wenn die Ausdrücke auf beiden Seiten des Operators true
liefern. Im folgenden Erläuterungskasten Logische Operatoren werden diese
Operatoren erläutert. Anhang C enthält eine komplette Tabelle aller logischen Ope-
ratoren in Java.

Logische Operatoren
Logische Operatoren arbeiten mit booleschen Werten (wahr und falsch bzw.
true und false) und liefern als Ergebnis wieder einen booleschen Wert. Die drei
wichtigsten logischen Operatoren sind und, oder und nicht. In Java werden
diese folgendermaßen notiert:
&& (und)
|| (oder)
! (nicht)
Der Ausdruck
a && b
ist dann wahr (liefert true), wenn sowohl a als auch b wahr sind, in allen ande-
ren Fällen ist er falsch (liefert false). Der Ausdruck
a || b
liefert true, wenn entweder a oder b oder beide true liefern, und false, wenn
beide false liefern. Der Ausdruck
!a
liefert true, wenn a false ist, und false, wenn a true ist.

106
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.8 Die Klasse Nummernanzeige

Übung 3.10 Was passiert, wenn Sie die Methode setzeWert mit einem ungül-
tigen Wert aufrufen? Ist das eine gute Lösung? Können Sie sich eine bessere
vorstellen?
Übung 3.11 Was würde passieren, wenn Sie den Operator >= in der Prü-
fung durch > ersetzen würden, und zwar in folgender Weise:
if((ersatzwert > 0) && (ersatzwert < limit))
Übung 3.12 Was würde passieren, wenn Sie den Operator && in der Prü-
fung durch || ersetzen würden, und zwar in folgender Weise:
if((ersatzwert >= 0) || (ersatzwert < limit))
Übung 3.13 Welche der folgenden Ausdrücke liefern true?
! (4 < 5)
! false
(2 > 2) || ((4 == 4) && (1 < 0))
(2 > 2) || (4 == 4) && (1 < 0)
(34 != 33) && ! false
Nachdem Sie Ihre Antworten auf Papier notiert haben, öffnen Sie die Direk-
teingabe von BlueJ und testen Sie die Ausdrücke. Vergleichen Sie die Ergeb-
nisse mit Ihren Antworten.
Übung 3.14 Schreiben Sie einen Ausdruck mit zwei booleschen Variablen a
und b, der true liefert, wenn entweder a und b beide true sind oder beide
false sind.
Übung 3.15 Schreiben Sie einen Ausdruck mit zwei booleschen Variablen a
und b, der true liefert, wenn nur genau eine von beiden true ist, und der
false liefert, wenn a und b beide false oder beide true sind (dies bezeichnet
man auch als exklusives Oder).
Übung 3.16 Betrachten Sie den Ausdruck (a && b). Schreiben Sie einen
äquivalenten Ausdruck (einen, der in exakt den gleichen Fällen für die glei-
chen Werte true liefert), ohne den Operator && zu benutzen.

3.8.2 Verkettung von Zeichenketten


Die nächste Methode, gibAnzeigewert, liefert ebenfalls den Wert der Anzeige
zurück, allerdings in einem anderen Format. Dies liegt daran, dass wir die Anzeige
als eine Zeichenkette aus zwei Zeichen anzeigen möchten, damit beispielsweise eine
Uhrzeit wie 3:05 als 03:05 angezeigt wird und nicht als 3:5. Um dies einfach zu
ermöglichen, haben wir die Methode gibAnzeigewert implementiert. Diese Methode
liefert den aktuellen Wert als Zeichenkette und fügt außerdem eine führende Null
ein, wenn der Wert unter 10 liegt. Der Quelltext sieht folgendermaßen aus:
if(value < 10) {
return "0" + wert;
}
else {
return "" + wert;
}

107
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Beachten Sie, dass die Null ("0") in doppelten Anführungszeichen gesetzt ist. Es
handelt sich also hier um die Zeichenkette 0, nicht um die ganze Zahl 0. Anschlie-
ßend werden in dem Ausdruck
"0" + wert
eine Zeichenkette und eine ganze Zahl „addiert“ (denn der Typ von wert ist int, also
eine ganze Zahl). Also ist der Plus-Operator an dieser Stelle wieder ein Verkettungs-
operator für Zeichenketten, wie wir bereits in Abschnitt 2.9 gesehen haben. Bevor
wir fortfahren, werden wir uns die Verkettung von Zeichenketten etwas genauer
ansehen.
Der Plus-Operator (+) hat unterschiedliche Bedeutungen, je nach dem Typ seiner
Operanden. Wenn beide Operanden Zahlen sind, dann repräsentiert er die erwar-
tete Addition. Also addiert
42 + 12
die beiden Zahlen und das Ergebnis ist 54. Wenn die Operanden hingegen Zei-
chenketten sind, dann ist die Bedeutung des Operators eine Verkettung von Zei-
chenketten, das Ergebnis ist dann eine einzelne Zeichenkette, die aus der Verket-
tung der beiden Operanden besteht. Das Ergebnis des Ausdrucks
"Java" + "mit BlueJ"
ist die einzelne Zeichenkette
"Javamit BlueJ"
Beachten Sie, dass nicht automatisch ein Leerzeichen zwischen den beiden Zei-
chenketten eingefügt wird. Wenn dort ein Leerzeichen stehen soll, dann muss es
explizit in einer der Zeichenketten angegeben werden.
Wenn einer der beiden Operanden eine Zeichenkette ist und der andere nicht,
dann wird der andere Operand automatisch in eine Zeichenkette umgewandelt
und anschließend eine Verkettung vorgenommen. Deshalb führt
"antwort: " + 42
zu der Zeichenkette
"antwort: 42"
Dies funktioniert für alle Typen. Ganz gleich, welchen Typ der hinzuaddierte Ope-
rand hat, er wird in eine Zeichenkette umgewandelt und dann angehängt.
Zurück zu den Anweisungen in unserer Methode gibAnzeigewert. Wenn wert eine
3 enthält, dann wird die Anweisung
return "0" + wert;
die Zeichenkette "03" zurückliefern. In dem Fall aber, in dem der Wert größer ist
als 9, haben wir einen kleinen Trick verwendet:
return "" + wert;
An dieser Stelle haben wir wert mit einer leeren Zeichenkette verknüpft. Das Ergeb-
nis ist, dass der Wert in eine Zeichenkette umgewandelt wird und ihm keine weite-

108
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.8 Die Klasse Nummernanzeige

ren Zeichen vorangestellt werden. Wir benutzen den Plus-Operator hier nur, um
einen ganzzahligen Wert vom Typ int in einen String umzuwandeln.

Übung 3.17 Arbeitet die Methode gibAnzeigewert in allen Fällen korrekt?


Welche Annahmen werden in ihr gemacht? Was passiert beispielsweise,
wenn Sie eine Nummernanzeige mit einem Limit von 800 erzeugen?
Übung 3.18 Gibt es unterschiedliche Ergebnisse, wenn in der Methode
gibAnzeigewert
return wert + "";
statt
return "" + wert;
geschrieben wird?
Übung 3.19 In Übung 2.79 sollten Sie (unter anderem) die Ausdrücke
9 + 3 + "See"
und
"See" + 3 + 9
untersuchen. Versuchen Sie noch einmal, das Ergebnis dieser beiden Aus-
drücke vorherzusagen, und testen Sie sie danach. (Sind Sie überrascht?)
Erklären Sie, warum die Ergebnisse so und nicht anders ausfallen.

3.8.3 Der Modulo-Operator


Die letzte Methode in der Klasse Nummernanzeige erhöht den Anzeigewert um 1. Sie
sorgt außerdem dafür, dass der Wert auf null zurückgesetzt wird, wenn das Limit
der Anzeige erreicht ist:
public void erhoehen()
{
wert = (wert + 1) % limit;
}
Diese Methode benutzt den Modulo-Operator (%). Dieser Operator berechnet den
Rest einer ganzzahligen Division. Beispielsweise kann das Ergebnis der Division
27 / 4
durch zwei ganze Zahlen ausgedrückt werden:
Ergebnis = 6, Rest = 3
Der Modulo-Operator liefert lediglich den Rest einer solchen Division. Das Ergeb-
nis des Ausdrucks (27 % 4) wäre demnach 3.

109
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Übung 3.20 Erklären Sie den Modulo-Operator. Möglicherweise benötigen


Sie dazu weitere Quellen (Java-Ressourcen online, andere Java-Bücher usw.),
um die Details nachzulesen.
Übung 3.21 Was ist das Ergebnis des Ausdrucks (8 % 3)?
Übung 3.22 Testen Sie den Ausdruck (8 % 3) in der Direkteingabe. Variieren
Sie die Zahlen. Was passiert, wenn Sie den Modulo-Operator mit negativen
Zahlen verwenden?
Übung 3.23 Welches sind die möglichen Werte des Ausdrucks (n % 5), wobei
n eine positive Variable vom Typ int ist?
Übung 3.24 Welches sind die möglichen Werte des Ausdrucks (n % m),
wobei n und m positive Variablen vom Typ int sind?
Übung 3.25 Erklären Sie ausführlich, wie die Methode erhoehen funktio-
niert.
Übung 3.26 Schreiben Sie die Methode erhoehen so um, dass sie statt des
Modulo-Operators eine if-Anweisung benutzt. Welche Lösung ist besser?

3.9 Die Klasse Uhrenanzeige


Nachdem wir nun eine Klasse schreiben können, die zweiziffrige Nummern anzei-
gen kann, sollten wir die Klasse Uhrenanzeige näher betrachten – die Klasse, die
zwei Nummernanzeigen erzeugt, um die komplette Uhrzeit anzuzeigen. Listing 3.4
zeigt den kompletten Quelltext der Klasse Uhrenanzeige, die zum Projekt Zeit-
anzeige gehört.

Listing 3.4
Der Quelltext der
Klasse Uhrenanzeige.

110
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.9 Die Klasse Uhrenanzeige

Übung 3.27 Öffnen Sie das Projekt Zeitanzeige und erzeugen Sie ein Objekt
der Klasse Uhrenanzeige, indem Sie den folgenden Konstruktor benutzen:
new Uhrenanzeige()
Rufen Sie an diesem Objekt die Methode gibUhrzeit auf, um die Anfangs-
zeit herauszufinden, auf die die Anzeige gesetzt wurde. Können Sie erklären,
warum sie gerade mit dieser Zeit startet?
Übung 3.28 Öffnen Sie den Objektinspektor für dieses Objekt. Rufen Sie,
während das Fenster geöffnet ist, die Methoden des Objekts auf. Beobach-
ten Sie das Datenfeld zeitanzeige in dem Fenster. Im Projektkommentar (Sie
öffnen diesen mit einem Doppelklick auf das Notiz-Symbol im Klassendia-
gramm) können Sie weitere Informationen finden.
Übung 3.29 Wie oft müssen Sie die Methode taktsignalGeben an einem neu
erzeugten Uhrenanzeige-Objekt aufrufen, damit die Anzeige den Wert 01:00
erreicht? Welchen anderen Weg könnte es geben, die Anzeige auf diesen
Wert zu bringen?

111
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

In diesem Projekt benutzen wir das Datenfeld zeitanzeige, um die tatsächliche


Anzeige der Uhr zu simulieren (wie Sie in Übung 3.28 gesehen haben). Wenn die
Software in einer echten Uhr laufen würde, dann würden wir die Ausgabe entspre-
chend auf ihrer Anzeige erzeugen. Diese Zeichenkette dient uns also als Software-
simulation einer echten Uhrenanzeige.1
Zusätzlich zur zeitanzeige hat die Klasse Uhrenanzeige zwei weitere Datenfelder:
stunden und minuten. Jedes kann eine Referenz auf ein Objekt vom Typ Nummern-
anzeige halten. Der logische Wert der Uhrenanzeige (die aktuelle Zeit) ist in diesen
Objekten gespeichert. Abbildung 3.4 zeigt ein Objektdiagramm dieser Anwen-
dung mit der aktuellen Zeit 15:23.

Abbildung 3.4
Ein Objektdiagramm
der Uhrenanzeige.

3.10 Objekte erzeugen Objekte


Die erste Frage, die wir uns stellen müssen, ist: Woher kommen die Nummernanzeige-
Objekte, die von Uhrenanzeige verwendet werden? Als Anwender einer Uhren-
anzeige nehmen wir bei der Erzeugung eines Objekts der Klasse Uhrenanzeige an,
dass unsere Uhrenanzeige Stunden und Minuten hat. Wir erwarten also, dass
durch die Erzeugung der Uhrenanzeige auch zwei Nummernanzeigen für die Stun-
den und Minuten erzeugt wurden.

Konzept Als Entwickler der Klasse Uhrenanzeige müssen wir jedoch dafür sorgen, dass das
tatsächlich passiert. Dazu schreiben wir Anweisungen in den Konstruktor der Klasse
Objekterzeu-
Uhrenanzeige, die zwei Nummernanzeigen erzeugen und abspeichern. Da der Kons-
gung: Objekte
können andere truktor automatisch ausgeführt wird, wenn ein Objekt der Klasse Uhrenanzeige
Objekte mit erzeugt wird, werden die Objekte von Nummernanzeige zur gleichen Zeit mit erzeugt.
dem new-Operator Hier sind die Anweisungen des Konstruktors, in denen das geschieht:
erzeugen.

1 Der Ordner Projekte umfasst eine Version dieses Projekts mit einer einfachen grafischen Benut-
zeroberfläche (GUI) namens Zeitanzeige-GUI. Der interessierte Leser kann mit diesem Projekt
experimentieren, auch wenn es nicht in diesem Buch besprochen wird.

112
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.10 Objekte erzeugen Objekte

public class Uhrenanzeige


{
private Nummernanzeige stunden;
private Nummernanzeige minuten;
private String zeitanzeige; // simuliert die tatsächliche Anzeige

restliche Datenfelder hier ausgelassen

/**
* Konstruktor für ein Exemplar von Uhrenanzeige.
* Mit diesem Konstruktor wird die Anzeige auf 00:00 initialisiert.
*/
public Uhrenanzeige()
{
stunden = new Nummernanzeige(24);
minuten = new Nummernanzeige(60);
anzeigeAktualisieren();
}

Methoden hier ausgelassen


}
In jeder der ersten beiden Zeilen wird ein Objekt von Nummernanzeige erzeugt und
einer Variablen zugewiesen. Die Syntax einer Anweisung zum Erzeugen eines
Objekts ist
new Klassenname ( Parameterliste )
Die new-Anweisung macht zwei Dinge:
1 Sie erzeugt ein neues Objekt der angegebenen Klasse (hier: Nummernanzeige).
2 Sie führt den Konstruktor dieser Klasse aus.
Wenn der Konstruktor der Klasse Parameter definiert, dann müssen aktuelle
Parameter in der new-Anweisung angegeben werden. Der Konstruktor der Klasse
Nummernanzeige beispielsweise ist mit einem int-Parameter definiert:
public Nummernanzeige (int anzeigeGrenze)
Deshalb muss die new-Anweisung, die diesen Konstruktor aufruft, einen aktuellen
Parameter vom Typ int übergeben, damit der Aufruf zum Kopf des Konstruktors
passt:
new Nummernanzeige (24);
Dies ist dasselbe Prinzip, das wir in Abschnitt 2.5 für Methoden besprochen haben.
Mit diesem Konstruktor haben wir erreicht, was wir erreichen wollten: Wenn jemand
ein Objekt der Klasse Uhrenanzeige erzeugt, dann wird der Konstruktor der Klasse
Uhrenanzeige automatisch ausgeführt und zwei Objekte der Klasse Nummernanzeige
werden erzeugt. Aus der Perspektive eines Anwenders der Klasse Uhrenanzeige ist die
Erzeugung des Objekts Nummernanzeige implizit enthalten. Aus der Perspektive der
Entwickler ist dies jedoch ein expliziter Vorgang, weil sie den dafür zuständigen Code
schreiben müssen. Die letzte Anweisung im Konstruktor ist ein Aufruf an die
Methode anzeigeAktualisieren, die das Datenfeld zeitanzeige einrichtet. Dieser Auf-
ruf wird in Abschnitt 3.12.1 erklärt. Dann ist die Uhrenanzeige in einem vernünftigen
Ausgangszustand.

113
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Übung 3.30 Schreiben Sie Java-Anweisungen, die eine Variable fenster vom
Typ Rechteck definieren, anschließend ein Rechteck-Objekt erzeugen und es
dann dieser Variablen zuweisen. Der Konstruktor der Klasse Rechteck hat zwei
int-Parameter.

3.11 Mehrere Konstruktoren


Sie haben möglicherweise beim Erzeugen des Uhrenanzeige-Objekts bemerkt, dass
das Kontextmenü Ihnen zwei Möglichkeiten dazu angeboten hat:
new Uhrenanzeige()
new Uhrenanzeige(stunde, minute)

Konzept Dies liegt daran, dass die Klasse Uhrenanzeige zwei Konstruktoren hat. Diese bieten
unterschiedliche Wege zur Initialisierung von Uhrenanzeige-Objekten an. Wenn der
Überladen: Eine
parameterlose Konstruktor benutzt wird, dann ist die Anfangszeit der Anzeige
Klasse kann mehr
als einen Konstruk- 00:00. Wenn Sie aber eine andere Startzeit für die Anzeige wählen möchten, dann
tor oder mehr als können Sie diese mit dem zweiten Konstruktor setzen. Es ist durchaus üblich, dass
eine Methode mit eine Klasse mehrere Versionen von Konstruktoren oder Methoden anbietet, die
dem gleichen unterschiedliche Wege zum Erfüllen einer Aufgabe über unterscheidbare Parameter-
Namen enthalten, sätze anbieten. Dies wird allgemein als Überladen eines Konstruktors oder einer
solange jede von Methode bezeichnet.
ihnen einen unter-
scheidbaren Satz
von Parameter-
typen definiert. Übung 3.31 Sehen Sie sich den zweiten Konstruktor im Quelltext der Klasse
Uhrenanzeige an. Erklären Sie, was dieser tut und wie er es tut.

Übung 3.32 Welche Gemeinsamkeiten haben die beiden Konstruktoren,


worin unterscheiden sie sich? Warum steht in dem zweiten Konstruktor bei-
spielsweise kein Aufruf der Methode anzeigeAktualisieren?

3.12 Methodenaufrufe

3.12.1 Interne Methodenaufrufe


Konzept Die letzte Zeile des ersten Konstruktors in Uhrenanzeige besteht aus der Anwei-
sung
Methoden können
andere Methoden anzeigeAktualisieren();
der eigenen Klasse
als Teil ihrer Imple-
Diese Anweisung ist ein Methodenaufruf. Wie wir bereits oben gesehen haben,
mentierung auf- hat die Klasse Uhrenanzeige eine Methode mit der folgenden Signatur:
rufen. Dies wird private void anzeigeAktualisieren()
als interner
Methodenauf-
ruf bezeichnet.

114
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.12 Methodenaufrufe

Der obige Methodenaufruf bewirkt die Ausführung dieser Methode. Da diese


Methode in derselben Klasse steht wie der Aufruf der Methode, nennen wir ihn
auch einen internen Methodenaufruf. Interne Methodenaufrufe haben folgende
Syntax:
Methodenname ( Parameterliste )
Ein interner Methodenaufruf hat keinen Variablennamen und keinen Punkt vor
dem Methodennamen, wie Sie es bei den aufgezeichneten Methodenaufrufen in
der Konsole beobachtet haben. Eine Variable wird nicht benötigt, weil bei einem
internen Methodenaufruf ein Objekt die Methode für sich selbst aufruft. Wir
stellen im nächsten Abschnitt interne und externe Aufrufe einander gegenüber.
In unserem Beispiel hat die Methode keine Parameter, deshalb ist die Parameter-
liste leer. Dies wird durch das leere Klammerpaar deutlich.
Wenn ein Methodenaufruf erfolgt, dann wird die passende Methode ausgeführt
und anschließend kehrt die Ausführung zur Aufrufstelle zurück und wird bei der
nächsten Anweisung nach dem Aufruf fortgesetzt. Damit eine Methode zu einem
Methodenaufruf passt, müssen sowohl der Name als auch die Parameterliste der
Methode passen. In diesem Beispiel sind beide Parameterlisten leer, daher passen sie.
Dass Methodenname und Parameterliste passen, ist sehr wichtig, denn es könnte
mehr als eine Methode mit dem gleichen Namen in der Klasse geben – wenn diese
überladen ist.
In unserem Beispiel ist der Zweck der Methode, die Zeichenkette für die Anzeige zu
aktualisieren. Nachdem die beiden Nummernanzeigen erzeugt wurden, wird diese
Zeichenkette auf den Wert gesetzt, den die beiden Anzeigeobjekte repräsentieren.
Die Implementierung der Methode anzeigeAktualisieren werden wir später disku-
tieren.

3.12.2 Externe Methodenaufrufe


Lassen Sie uns nun die nächste Methode untersuchen: taktsignalGeben. Die Defini-
tion lautet:
public void taktsignalGeben()
{
[Link]();
if([Link]() == 0) { // Limit wurde erreicht!
[Link]();
}
anzeigeAktualisieren();
}
Wenn diese Anzeige Teil einer echten Uhr wäre, dann würde diese Methode alle
60 Sekunden vom elektronischen Taktgeber der Uhr aufgerufen. Wir rufen sie hier
zu Testzwecken einfach von Hand auf.
Wenn die Methode taktsignalGeben aufgerufen wird, führt sie zuerst die Anwei-
sung
[Link]();

115
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Konzept aus. Diese Anweisung ruft die Methode erhoehen des minuten-Objekts auf. Somit
ruft, wenn eine der Methoden eines Uhrenanzeige-Objekts aufgerufen wird, diese
Methoden können
wiederum eine Methode eines anderen Objekts auf, um einen Teil der Arbeit von
Methoden von
anderen Objekten diesem erledigen zu lassen. Der Aufruf einer Methode eines anderen Objekts wird
über die Punkt- als externer Methodenaufruf bezeichnet. Die Syntax eines externen Methodenauf-
Notation aufrufen. rufs ist:
Dies wird als
Objekt . Methodenname ( Parameterliste )
externer
Methodenauf- Diese Syntax bezeichnet man auch als Punkt-Notation. Sie besteht aus einem Objekt-
ruf bezeichnet. namen, einem Punkt, dem Methodennamen und den Parametern für den Aufruf. Es
ist sehr wichtig zu beachten, dass an dieser Stelle der Name eines Objekts verwendet
wird und nicht der Name einer Klasse. Wir benutzen hier den Namen minuten statt
Nummernanzeige. (Das wesentliche Prinzip wurde in Übung 3.8 illustriert.)

Der Unterschied zwischen internen und externen Methodenaufrufen ist ein-


leuchtend – die Anwesenheit eines Objektnamens gefolgt von einem Punkt sagt
uns, dass die aufgerufene Methode zu einem anderen Objekt gehört. Beispiels-
weise ruft das Uhrenanzeige-Objekt innerhalb der Methode TaktSignalGeben die
Nummernanzeige-Objekte auf und überträgt diesen einen Teil der Gesamtaufgabe.
Mit anderen Worten wird die übergeordnete Aufgabe, den Takt zu halten, zwi-
schen den Klassen Uhrenanzeige und Nummernanzeige aufgeteilt. Hier haben wir
eine praktische Illustration des Teile-und-herrsche-Prinzips, das wir in unserer frü-
heren Diskussion der Abstraktion angesprochen haben.
Die Methode taktsignalGeben benutzt eine if-Anweisung, um zu überprüfen, ob die
Stunden ebenfalls erhöht werden müssen. Als Teil der Prüfung in der if-Anweisung
wird eine weitere Methode des minuten-Objekts aufgerufen: gibWert. Diese Methode
liefert den aktuellen Wert der Minuten. Wenn dieser Wert null ist, dann wissen wir,
dass gerade das Limit überschritten wurde und dass wir deshalb auch die Stunden
erhöhen sollten. Das ist genau das, was in diesem Quelltextabschnitt passiert.
Wenn der Wert der Minuten nicht null ist, dann sind wir fertig. In diesem Fall brau-
chen wir die Stunden nicht zu verändern. Deshalb hat die if-Anweisung keinen
else-Teil.

Wir sollten nun auch in der Lage sein, die anderen drei Methoden der Klasse Uhren-
anzeige zu verstehen (siehe Listing 3.4). Die Methode setzeUhrzeit fordert zwei
Parameter – die Stunden und die Minuten – und setzt die Anzeige auf den entspre-
chenden Wert. Wenn wir uns den Rumpf der Methode ansehen, dann sehen wir,
dass sie dies durch Aufrufe der Methode setzeWert an beiden Nummernanzeigen
vornimmt. Anschließend ruft sie anzeigeAktualisieren auf, um die Zeichenkette für
die Anzeige entsprechend zu aktualisieren, genau wie der Konstruktor dies tut.
Die Methode gibUhrzeit ist trivial – sie liefert lediglich den aktuellen Wert der Zei-
chenkette für die Anzeige. Da diese immer auf dem aktuellen Stand gehalten
wird, brauchen wir hier nichts weiter zu tun.
Die Methode anzeigeAktualisieren schließlich ist dafür zuständig, dass die Zeichen-
kette für die Anzeige korrekt den Wert anzeigt, der durch die beiden Nummer-
nanzeigen repräsentiert wird. Sie wird jedes Mal aufgerufen, wenn sich die
anzuzeigende Zeit ändert. Auch dies ist ein Beispiel für einen externen Metho-
denaufruf. Sie erfüllt ihre Aufgabe, indem sie an beiden Nummernanzeige-Objekten

116
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.12 Methodenaufrufe

die Methode gibWert aufruft. Diese Methodenaufrufe liefern die Werte der Num-
mernanzeigen. Zuletzt verknüpft die Methode anzeigeAktualisieren diese beiden
Werte über den Operator zur Verkettung von Zeichenketten, durch einen Dop-
pelpunkt getrennt, zu einer einzelnen Zeichenkette.

Übung 3.33 Gegeben sei eine Variable


Drucker d1;
die aktuell eine Referenz auf ein Drucker-Objekt hält, sowie zwei Methoden
in der Klasse Drucker mit den Signaturen
public void drucke(String dateiname, boolean doppelseitig)
public int gibStatus(int wartezeit)
Schreiben Sie zwei mögliche Aufrufe zu jeder dieser beiden Methoden.
Übung 3.34 Öffnen Sie das Projekt Haus aus Kapitel 1 und sehen Sie sich
die Klasse Bild noch einmal an. Welche Objekttypen werden vom Bild-Kon-
struktor erzeugt?
Übung 3.35 Zählen Sie alle externen Methodenaufrufe auf, die in der
zeichne-Methode von Bild auf dem Dreieck-Objekt dach ausgeführt werden.

Übung 3.36 Enthält die Klasse Bild interne Methodenaufrufe?


Übung 3.37 Entfernen Sie die beiden folgenden Anweisungen aus der
zeichne-Methode von Bild:
[Link]("schwarz")
[Link]("gelb")
und führen Sie die Farbeinstellung stattdessen mithilfe eines einzigen Auf-
rufs an eine interne Methode setzeFarbe durch (die Sie erstellen müssen).

3.12.3 Zusammenfassung der Uhrenanzeige


Wir sollten an dieser Stelle noch einmal kurz Revue passieren lassen, wie in diesem
Beispiel Abstraktion benutzt wird, um eine Aufgabe in kleinere Teilaufgaben zu zer-
legen. Ein Blick auf den Quelltext der Klasse Uhrenanzeige zeigt, dass wir dort ledig-
lich Nummernanzeige-Objekte erzeugen, ohne dass wir daran interessiert wären, wie
diese Objekte intern funktionieren. Wir rufen ihre Methoden auf (erhoehen, gib-
Wert), damit sie einen Teil der Arbeit für uns erledigen. Auf dieser Ebene nehmen wir
einfach an, dass erhoehen korrekt den Wert der Anzeige erhöht, ohne dass uns inte-
ressiert, wie sie dies tut.
In echten Softwareentwicklungsprojekten werden solche Klassen häufig von zwei
unterschiedlichen Personen geschrieben. Es ist Ihnen vermutlich inzwischen klar
geworden, dass diese beiden Personen sich lediglich darüber einig sein müssen, wel-
che Methodensignaturen eine Klasse haben sollte und was diese Methoden tun.
Dann kann die eine Person sich auf die Implementierung dieser Methoden konzent-
rieren, während die andere sie einfach nur benutzt.

117
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Die Menge der Methoden, die ein Objekt anderen Objekten zugänglich macht,
wird als seine Schnittstelle bezeichnet. Wir werden Schnittstellen in diesem Buch
noch sehr ausführlich diskutieren.

Übung 3.38 Zusatzaufgabe. Ändern Sie die 24-Stunden-Anzeige in eine


12-Stunden-Anzeige. Vorsicht: Dies ist nicht so einfach, wie es auf den ers-
ten Blick scheint. Bei einer 12-Stunden-Anzeige werden die Minuten nach
Mitternacht und nach dem Mittag nicht als 00:30, sondern als 12:30 ange-
zeigt. Somit zeigt die Minutenanzeige Werte von 0 bis 59, während die
Stundenanzeige Werte von 1 bis 12 anzeigt!
Übung 3.39 Es gibt (mindestens) zwei Wege, wie Sie eine 12-Stunden-
Anzeige realisieren können. Eine Möglichkeit ist, die Stundenwerte nur von
1 bis 12 zu speichern. Eine andere lässt die Anzeige intern nach wie vor als
24-Stunden-Anzeige arbeiten, passt aber die Zeichenkette zur Ausgabe auf
4:23 oder 4.23pm an, wenn der interne Wert 16:23 ist. Welche Version ist ein-
facher? Welche ist besser? Warum?
Übung 3.40 Angenommen, eine Klasse Baum besitzt ein Datenfeld vom Typ
Dreieck namens blaetter und ein Datenfeld vom Typ Quadrat namens stamm.
Der Konstruktor von Baum hat keine Parameter und sein Konstruktor erzeugt
die Dreieck- und Quadrat-Objekte für seine Datenfelder. Erstellen Sie, unter
Zuhilfenahme des Figuren-Projekts aus Kapitel 1, eine einfache Baum-Klasse,
zu der diese Beschreibung passt. Sie müssen keine Methoden in der Klasse
definieren und die Figuren müssen nicht sichtbar gemacht werden.
Übung 3.41 Zusatzaufgabe. Vervollständigen Sie die Klasse Baum aus der
vorigen Aufgabe: Der Konstruktor soll nun das Quadrat für den Stamm
unterhalb des Blätter-Dreiecks platzieren und dann beide Figuren sichtbar
machen. Definieren Sie dazu eine Methode in der Baum-Klasse mit Namen
einrichten und fügen Sie einen Aufruf an diese Methode im Baum-Konstruk-
tur hinzu. Verändern Sie die Größe des Dreicks, sodass der Baum eher wie
ein Tannenbaum aussieht.

3.13 Ein weiteres Beispiel für


Objektinteraktion
Konzept Wir werden nun dieselben Konzepte mit einem anderen Beispiel und mit anderen
Werkzeugen betrachten. Wir wollen noch immer ein gutes Verständnis davon
Ein Debugger ist
bekommen, wie Objekte andere Objekte erzeugen und wie diese Objekte gegensei-
ein Softwarewerk-
zeug, mit dessen tig ihre Methoden aufrufen. Im ersten Teil dieses Kapitels haben wir dazu die grund-
Hilfe die Ausfüh- legendste Technik zur Analyse eines Programms benutzt: das Lesen von Quelltext.
rung eines Pro- Die Fähigkeit, einen Quelltext zu lesen und zu verstehen, ist eine der wichtigsten
gramms untersucht Eigenschaften für einen Softwareentwickler, und wir werden diese Fähigkeit in jedem
werden kann. Es Projekt brauchen, in dem wir mitarbeiten. Manchmal kann es aber nützlich sein, mit-
kann benutzt hilfe von zusätzlichen Werkzeugen ein besseres Verständnis davon zu bekommen,
werden, um Fehler
(Bugs) zu finden.

118
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.13 Ein weiteres Beispiel für Objektinteraktion

was bei einer Programmausführung passiert. Ein solches Werkzeug, das wir jetzt
näher betrachten wollen, ist ein Debugger.
Ein Debugger ist ein Werkzeug, mit dem ein Entwickler ein Programm Schritt für
Schritt ausführen lassen kann. Er bietet üblicherweise Funktionen zum Stoppen und
Starten eines Programms an ausgewählten Stellen im Quelltext und zum Betrachten
der Werte von Variablen.

Der Begriff „Debugger“


Fehler in Computerprogrammen werden im Jargon häufig auch „Bugs“ (eng-
lisch für Käfer) genannt. Deshalb werden Programme, die bei der Beseiti-
gung von Fehlern helfen, „Debugger“ genannt.
Es ist nicht vollständig geklärt, worauf diese Verwendung des Begriffs
zurückgeht. Es gibt den berühmten Fall des „ersten Computer-Bugs“ – ein
echter Käfer (genauer eine Motte), der im Jahr 1945 im Innern des Mark-II-
Computers durch Grace Murray Hopper, einer frühen Computerpionierin,
gefunden wurde. Es existiert noch immer ein Logbuch im National Museum
of American History of the Smithsonian Institute, in dem bei einem Eintrag
die Motte eingeklebt ist, mit der Bemerkung „erster tatsächlicher Fall eines
gefundenen Bugs“. Diese Formulierung lässt allerdings vermuten, dass der
Begriff „Bug“ schon benutzt wurde, bevor dieser echte „Bug“ Probleme im
Mark II bereitet hat.
Für weitere Details suchen Sie im Web nach „first computer bug“ – Sie wer-
den sogar Bilder der Motte finden!

Debugger unterscheiden sich stark in ihrer Komplexität. Solche für die professio-
nelle Entwicklung bieten eine umfangreiche Funktionalität, um sehr detailliert die
unterschiedlichen Aspekte einer Anwendung ausleuchten zu können. Der Debug-
ger, der in BlueJ integriert ist, ist sehr viel simpler. Wir können damit die Ausfüh-
rung eines Programms anhalten, zeilenweise den Quelltext ausführen lassen und
die Werte von Variablen untersuchen. Trotz dieser abgespeckten Funktionalität ist
er leistungsfähig genug, um uns nützliche Informationen zu liefern.
Bevor wir den Debugger ausprobieren, werfen wir zuerst einen Blick auf das Bei-
spiel, mit dem wir ihn demonstrieren wollen: eine Simulation eines E-Mail-Sys-
tems.

3.13.1 Das Beispiel eines Mail-Systems


Wir beginnen, indem wir uns die Funktionalität des Projekts Mail-System anse-
hen. An dieser Stelle müssen wir noch keinen Quelltext lesen, wir starten das Sys-
tem einfach und untersuchen sein Verhalten.

119
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Übung 3.42 Öffnen Sie das Projekt Mail-System, das Teil des Zusatzmateri-
als zu diesem Buch ist. In diesem Projekt soll simuliert werden, dass Benut-
zer sich gegenseitig E-Mails in Form von Nachrichten schicken. Dazu ver-
wendet ein Benutzer einen sogenannten Mail-Client (ein Programm zum
Lesen und Schreiben von E-Mails), der Nachrichten an einen Server schickt,
der sie wiederum an den Mail-Client eines anderen Benutzers weiterleitet.
Erzeugen Sie zuerst ein Objekt der Klasse MailServer. Erzeugen Sie dann ein
Objekt der Klasse MailClient für einen der Benutzer. Bei der Erzeugung
eines Mail-Clients müssen Sie eine MailServer-Instanz als Parameter ange-
ben. Verwenden Sie dazu diejenige, die Sie gerade erzeugt haben. Sie müssen
außerdem einen Benutzernamen für den Mail-Client angeben. Erzeugen Sie
anschließend einen weiteren Mail-Client mit einem anderen Benutzernamen.
Experimentieren Sie mit den MailClient-Objekten herum. Sie können benutzt
werden, um Nachrichten von einem Mail-Client zum anderen zu schicken
(über die Methode sendeNachricht) und um Nachrichten zu empfangen (über
die Methoden gibNaechsteNachricht und naechsteNachrichtAusgeben).

Bei der Untersuchung des Projekts Mail-System stellen wir folgende Dinge fest:
 Es enthält drei Klassen: MailServer, MailClient und Nachricht.
 Ein MailServer-Objekt, das von allen Mail-Clients benutzt wird, muss erzeugt
werden. Es kümmert sich um den Austausch der Nachrichten.
 Es können mehrere Mail-Clients erzeugt werden. Jeder Mail-Client ist mit einem
Benutzernamen verknüpft.
 Nachrichten können von einem Mail-Client zu einem anderen Mail-Client über
eine Methode in der Klasse MailClient verschickt werden.
 Nachrichten können von einem Mail-Client durch eine Methode in MailClient
empfangen werden, die Nachrichten einzeln vom Server abholt.
 Der Benutzer erzeugt nicht explizit Objekte der Klasse Nachricht. Diese Klasse
wird intern in den beiden anderen Klassen verwendet, um Nachrichtentexte zu
erzeugen, zu speichern und auszutauschen.

Übung 3.43 Zeichnen Sie ein Objektdiagramm von der Situation unmittel-
bar nach der Erzeugung eines Mail-Servers und dreier Mail-Clients. Objekt-
diagramme wurden in Abschnitt 3.6 diskutiert.

Die drei Klassen sind unterschiedlich komplex. Die Klasse Nachricht ist recht trivial.
Wir werden nur ein kleines Detail diskutieren und die weitere Erkundung dem
Leser überlassen. Die Klasse MailServer ist an diesem Punkt der Diskussion noch
sehr komplex – sie benutzt Konzepte, die wir erst sehr viel später in diesem Buch
diskutieren werden. Wir werden sie deshalb hier nicht näher betrachten. Stattdes-
sen verlassen wir uns darauf, dass sie ihre Aufgabe erfüllt – ein weiteres Beispiel
dafür, dass Abstraktion uns ermöglicht, unnötige Details zu ignorieren.

120
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.13 Ein weiteres Beispiel für Objektinteraktion

Die Klasse MailClient ist am interessantesten, wir werden sie deshalb ausführlich
diskutieren.

3.13.2 Das Schlüsselwort this


Der einzige Abschnitt der Klasse Nachricht, den wir hier betrachten wollen, ist der
Konstruktor. Er benutzt ein Java-Konstrukt, das wir bisher noch nicht kennengelernt
haben. Der Quelltext ist in Listing 3.5 zu sehen.

Listing 3.5
Datenfelder und Kons-
truktor der Klasse
Nachricht.

Das neue Java-Konstrukt in diesem Quelltextabschnitt ist die Verwendung des


Schlüsselworts this:
[Link] = absender;
Die gesamte Zeile ist eine Zuweisung. Sie weist den Wert auf der rechten Seite
(absender) der Variablen auf der linken Seite ([Link]) zu.
Dieses Konstrukt wird benutzt, weil in dieser Situation ein Name überladen ist – der-
selbe Name wird für zwei verschiedene Dinge verwendet. Die Klasse enthält drei
Datenfelder, absender, empfaenger und nachricht. Der Konstruktor hat drei Parame-
ter, die ebenfalls absender, empfaenger und nachricht heißen!
Wie viele Variablen existieren dann, während der Konstruktor ausgeführt wird? Die
Antwort lautet sechs – drei Datenfelder und drei Parameter. An dieser Stelle ist sehr
wichtig zu verstehen, dass die Datenfelder und Parameter unterschiedliche Variab-
len sind, die unabhängig voneinander existieren, auch wenn sie die gleichen Namen
tragen. Ein Parameter und ein Datenfeld, die den gleichen Namen tragen, sind in
Java kein wirkliches Problem.
Wir haben allerdings das Problem, dass wir die sechs Variablen benennen und dabei
die beiden Sätze an Variablen unterscheiden wollen. Wenn wir einfach nur den Vari-
ablennamen absender im Konstruktor verwenden (beispielsweise in einer Anwei-
sung wie [Link](absender)), welche Variable wird dann benutzt – der
Parameter oder das Datenfeld?

121
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Die Spezifikation der Sprache Java gibt hier die Antwort. Sie spezifiziert, dass
immer die Definition des nächsten umschließenden Blocks verwendet wird. Da
der Parameter absender im Konstruktor definiert ist und das Datenfeld absender in
der Klasse, wird der Parameter verwendet. Seine Definition ist „näher“ an der
Anweisung, die ihn benutzt.
Alles, was wir nun brauchen, ist ein Mechanismus, mit dem wir auf ein Datenfeld
zugreifen können, wenn eine Variable mit gleichem Namen in größerer Nähe defi-
niert ist. Genau dafür wird das Schlüsselwort this verwendet. Der Ausdruck this
bezieht sich auf das aktuelle Objekt. Wenn wir [Link] schreiben, bezie-
hen wir uns auf das Datenfeld absender im aktuellen Objekt. Dieses Konstrukt
gibt uns somit die Möglichkeit, statt auf einen Parameter auf ein Datenfeld mit
gleichem Namen zuzugreifen. Wir können die Zuweisung nun erneut betrach-
ten:
[Link] = absender;
Diese Anweisung hat, wie wir nun wissen, den folgenden Effekt:
Datenfeld namens absender = Parameter namens absender;
Mit anderen Worten: Sie weist den Wert des Parameters an das Datenfeld mit dem
gleichen Namen zu. Und das ist genau das, was wir für die Initialisierung des Objekts
tun wollen.
Es bleibt eine letzte Frage: Warum tun wir das alles? Das ganze Problem könnte
leicht umgangen werden, wenn wir die Parameter und Datenfelder unterschied-
lich benennen würden. Der Grund ist die Lesbarkeit des Quelltextes.
Manchmal gibt es einen Namen, der die Bedeutung einer Variablen perfekt
beschreibt. Er passt so gut, dass wir uns keinen weiteren Namen für sie ausden-
ken wollen. Wir wollen ihn für den Parameter verwenden, weil er dem Aufrufer
einen Hinweis darauf gibt, was übergeben werden soll. Und wir wollen ihn für
das Datenfeld verwenden, weil er dem Entwickler der Klasse deutlich macht,
zu welchem Zweck es definiert ist. Wenn ein Name die Benutzung perfekt
beschreibt, dann sollte man ihn für beide verwenden und den geringen Mehr-
aufwand für das Einfügen des Schlüsselworts this in die Zuweisung in Kauf
nehmen.

3.14 Die Benutzung eines Debuggers


Die interessanteste Klasse in unserem Mail-System ist MailClient. Wir werden
diese nun mithilfe des Debuggers näher untersuchen. Sie hat drei Methoden:
gibNaechsteNachricht, naechsteNachrichtAusgeben und sendeNachricht. Wir werden
zuerst die Methode naechsteNachrichtAusgeben betrachten.
Bevor wir den Debugger starten, werden wir ein Szenario erstellen, das wir dann
näher untersuchen können (Übung 3.44).

122
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.14 Die Benutzung eines Debuggers

Übung 3.44 Erstellen Sie ein Szenario zur Untersuchung: Erzeugen Sie einen
Mail-Server, dann zwei Mail-Clients für die Benutzer „Sophie“ und „Juan“ (Sie
sollten auch die Instanzen so benennen, damit Sie sie auf der Objektleiste besser
unterscheiden können). Benutzen Sie dann Sophies Methode sendeNachricht,
um eine Nachricht an Juan zu schicken. Rufen Sie die Nachricht noch nicht ab.

Nach dem Aufbau in Übung 3.44 haben wir eine Situation, in der eine Nachricht für
Juan auf dem Server gespeichert ist, die abgerufen werden soll. Wir haben bereits
gesehen, dass die Methode naechsteNachrichtAusgeben diese Nachricht abruft und
sie auf der Konsole ausgibt. Nun wollen wir genau wissen, wie das abläuft.

3.14.1 Haltepunkte setzen


Um unsere Untersuchung zu starten, setzen wir zuerst einen Haltepunkt (Übung
3.45). Ein Haltepunkt ist ein Hinweis, der einer Zeile im Quelltext angeheftet wird.
Wenn bei der Ausführung einer Methode diese Stelle erreicht wird, dann wird
die Ausführung angehalten. Ein Haltepunkt ist im BlueJ-Editor durch ein kleines
Stoppzeichen symbolisiert (Abbildung 3.5).
Sie können einen Haltepunkt setzen, indem Sie den BlueJ-Editor öffnen, eine
geeignete Zeile selektieren (in unserem Beispiel die erste Zeile der Methode
naechsteNachrichtAusgeben) und aus dem Menü WERKZEUGE den Eintrag HALTEPUNKT
SETZEN/ENTFERNEN auswählen. Alternativ können Sie auch einfach in den Bereich
mit den Haltepunkten klicken, um dort einen Haltepunkt zu setzen oder zu ent-
fernen. Beachten Sie, dass die Klasse hierzu übersetzt sein muss.

Übung 3.45 Öffnen Sie den Editor für die Klasse MailClient und setzen Sie
einen Haltepunkt in die erste Zeile der Methode naechsteNachrichtAusgeben,
wie in Abbildung 3.5 gezeigt.

Abbildung 3.5
Ein Haltepunkt im
BlueJ-Editor.

123
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Nachdem Sie den Haltepunkt gesetzt haben, rufen Sie die Methode naechsteNach-
richtAusgeben an Juans Mail-Client auf. Das Editorfenster für die Klasse MailClient
und ein Debugger-Fenster werden in den Vordergrund geholt (Abbildung 3.6).
Am unteren Rand des Debugger-Fensters befinden sich einige Kontrollknöpfe. Diese
können benutzt werden, um die Ausführung eines Programms anzuhalten oder
fortzusetzen. (Eine ausführlichere Erläuterung der Kontrollmöglichkeiten des Debug-
gers finden Sie in Anhang F.)
Auf der rechten Seite des Debugger-Fensters befinden sich drei Bereiche für die
Anzeige von Variablen, benannt mit statische Variablen, Instanzvariablen und
lokale Variablen. Wir werden den Bereich für statische Variablen vorläufig ignorieren.
Statische Variablen werden erst später diskutiert, außerdem hat diese Klasse keine
statischen Variablen.
Abbildung 3.6
Das Debugger-Fenster,
Ausführung gestoppt
an Haltepunkt.

Wir sehen, dass dieses Objekt zwei Instanzvariablen (oder Datenfelder) hat, server
und benutzer, und wir können die aktuellen Werte dieser Variablen sehen. Die
Variable benutzer hält die Zeichenkette "Juan", und die Variable server hält eine
Referenz auf ein anderes Objekt. Die Objektreferenz haben wir in unseren Objekt-
diagrammen als Pfeil gezeichnet.
Beachten Sie, dass bis jetzt keine lokale Variable angezeigt wird. Dies liegt daran,
dass die Ausführung immer vor der Zeile mit dem Haltepunkt anhält. Da die Zeile
mit dem Haltepunkt die Deklaration der einzigen lokalen Variablen enthält und
diese Zeile noch nicht ausgeführt wurde, existiert zu diesem Zeitpunkt noch
keine lokale Variable.
Der Debugger ermöglicht uns nicht nur, ein laufendes Programm anzuhalten und
so Variablen zu inspizieren, mit ihm kann man das Programm auch schrittweise
weiter ausführen.

3.14.2 Einzelausführung
Wenn ein Haltepunkt erreicht wurde, führt ein Klick auf die Schaltfläche SCHRITT
ÜBER eine Zeile aus und die Ausführung stoppt erneut.

124
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.14 Die Benutzung eines Debuggers

Übung 3.46 Gehen Sie in der Ausführung der Methode naechsteNachrichtAus-


geben einen Schritt weiter, indem Sie auf die Schaltfläche SCHRITT ÜBER klicken.

In Abbildung 3.7 sehen Sie das Ergebnis der Ausführung der ersten Zeile von
naechsteNachrichtAusgeben. Wir können erkennen, dass die Ausführung einen
Schritt weitergegangen ist (ein kleiner schwarzer Pfeil neben der Quelltextzeile
markiert die aktuelle Position) und dass im Bereich für lokale Variablen im Debug-
ger-Fenster angezeigt wird, dass eine lokale Variable angelegt wurde und dieser
Variablen ein Objekt zugewiesen wurde.

Abbildung 3.7
Erneut gestoppt nach
einem einzelnen
Schritt.

Übung 3.47 Sagen Sie voraus, welche Zeile als die nächste auszuführende
Zeile markiert sein wird, wenn Sie einen Schritt weitergehen. Führen Sie die-
sen Schritt dann aus und überprüfen Sie Ihre Vorhersage. Lagen Sie richtig
oder falsch? Erklären Sie, was passiert ist und weshalb es passiert ist.

Wir können uns nun durch wiederholtes Klicken auf die SCHRITT ÜBER-Schaltfläche
an das Ende der Methode bewegen. Dies ermöglicht uns, den Ausführungspfad
für diese Methode nachzuvollziehen. Dies ist insbesondere bei bedingten Anwei-
sungen interessant: Wir können genau erkennen, welcher Zweig einer if-Anwei-
sung gewählt wird, und überprüfen, ob dies unseren Erwartungen entspricht.

Übung 3.48 Rufen Sie dieselbe Methode noch einmal auf. Bewegen Sie sich
wie vorher durch die Methode. Was beobachten Sie? Erklären Sie dieses
Verhalten.

125
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

3.14.3 Hineinschreiten in Methoden


Während wir uns durch die Methode naechsteNachrichtAusgeben schrittweise
bewegt haben, haben wir zwei Methodenaufrufe an Objekte unserer Klassen
gesehen. Die Zeile
Nachricht nachricht = [Link](benutzer);
enthält einen Aufruf der Methode gibNaechsteNachrichtFuer des server-Objekts.
Durch Prüfen der Instanzvariablen im Debugger-Fenster können wir sehen, dass
das server-Objekt von der Klasse MailServer ist.
Die Zeile
[Link]();
ruft die Methoden ausgeben an der Nachricht auf. Wir können in der ersten Zeile
der Methode naechsteNachrichtAusgeben sehen, dass nachricht von der Klasse
Nachricht deklariert ist.
Mit der Benutzung der SCHRITT ÜBER-Funktion im Debugger haben wir Abstraktion
eingesetzt: Wir haben die Methode ausgeben der Klasse Nachricht als eine einzelne
Anweisung angesehen und konnten beobachten, dass ihr Effekt die Ausgabe der
Details einer Nachricht (Absender, Empfänger und Inhalt) auf der Konsole war.
Wenn wir an mehr Details interessiert sind, können wir tiefer in den Ausführungs-
prozess hinabsteigen und auch die Methode ausgeben selbst Schritt für Schritt aus-
führen lassen. Dazu verwenden wir im Debugger statt der SCHRITT ÜBER-Funktion
die Funktion SCHRITT HINEIN. SCHRITT HINEIN schreitet in die Methode, die aufgerufen
wird, hinein und hält an der ersten Anweisung dieser Methode an.

Übung 3.49 Erzeugen Sie erneut die Ausgangsposition wie vorher, indem
Sie eine Nachricht von Sophie an Juan schicken. Rufen Sie dann wieder die
Methode naechsteNachrichtAusgeben am Mail-Client von Juan auf. Schreiten
Sie durch die Methode wie zuvor. Aber dieses Mal klicken Sie, wenn Sie die
Zeile
[Link]();
erreichen, nicht auf SCHRITT ÜBER, sondern auf SCHRITT HINEIN. Sorgen Sie dafür,
dass die Konsole sichtbar ist, wenn Sie nun weiterschreiten. Was beobachten
Sie? Erklären Sie es.

3.15 Mehr zu Methodenaufrufen


In unseren Experimenten in Abschnitt 3.14 haben wir ein weiteres Beispiel von
Objektinteraktion gesehen, das denen ähnlich ist, die wir schon kannten: Objekte
rufen Methoden von anderen Objekten auf. In der Methode naechsteNach-
richtAusgeben macht ein MailClient-Objekt einen Aufruf an ein MailServer-Objekt,

126
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.15 Mehr zu Methodenaufrufen

um die nächste Nachricht abzuholen. Diese Methode (gibNaechsteNachrichtFuer)


liefert einen Wert – ein Objekt der Klasse Nachricht. Außerdem gab es den Aufruf
der Methode ausgeben an einer Nachricht. Mithilfe von Abstraktion können wir die-
sen Aufruf als eine einzelne Anweisung betrachten. Oder wir können, wenn wir an
mehr Details interessiert sind, eine Abstraktionsstufe hinabsteigen und in die aus-
geben-Methode hineinsehen.

Auf ähnliche Weise können wir den Debugger benutzen, um zu beobachten, wie
ein Objekt ein anderes erzeugt. Die Methode sendeNachricht in der Klasse Mail-
Client liefert ein gutes Beispiel. In dieser Methode wird in der ersten Zeile ein
Objekt der Klasse Nachricht erzeugt:
Nachricht nachricht = new Nachricht(benuzter, empfaenger, text);
Der Grundgedanke ist, dass eine Nachricht eine Botschaft in einem Nachrichten-
system modelliert. Sie enthält Informationen über den Absender, den Empfänger
und den Text der Nachricht. Wenn eine Botschaft versendet werden soll, dann
kapselt ein Mail-Client diese Informationen in einem Objekt der Klasse Nachricht
und legt diese auf dem Mail-Server ab. Dort kann sie später vom Mail-Client des
Empfängers abgeholt werden.
In der Quelltextzeile oben sehen wir, wie das Schlüsselwort new für die Erzeugung
eines neuen Objekts verwendet wird, und wir sehen, wie die Parameter an den
Konstruktor übergeben werden. (Erinnern Sie sich: Bei der Konstruktion eines
Objekts geschehen zwei Dinge – das Objekt wird erzeugt und der Konstruktor
wird ausgeführt.) Der Aufruf eines Konstruktors ist dem Aufruf einer Methode
sehr ähnlich. Dies können wir beobachten, wenn wir die Funktion SCHRITT HINEIN
des Debuggers an der Stelle verwenden, an der das Objekt erzeugt wird.

Übung 3.50 Setzen Sie einen Haltepunkt in die erste Zeile der Methode sende-
Nachricht der Klasse MailClient. Rufen Sie dann diese Methode auf. Benutzen
Sie die Funktion SCHRITT HINEIN, um in den Konstruktor der Klasse Nachricht
zu schreiten. Im Debugger-Fenster können Sie nun die Instanzvariablen und
die lokalen Variablen gleichen Namens sehen, wie wir sie für ein Nachricht-
Objekt in Abschnitt 3.13.2 diskutiert haben. Schreiten Sie durch den Kons-
truktor und beobachten Sie, wie die Instanzvariablen initialisiert werden.
Übung 3.51 Benutzen Sie eine Kombination aus Lesen von Quelltext, Aus-
führen von Methoden, Haltepunkten und schrittweiser Ausführung, um sich
mit den Klassen Nachricht und MailClient vertraut zu machen. Beachten Sie,
dass Sie noch nicht genügend Informationen haben, um die Implementie-
rung der Klasse MailServer zu verstehen, sodass Sie sie vorläufig ignorieren
können. (Sie können natürlich trotzdem einen Blick darauf werfen, aber
seien Sie nicht überrascht, wenn Sie sie etwas verwirrend finden …) Erklären
Sie schriftlich, wie die Klassen MailClient und Nachricht miteinander inter-
agieren. Zeichnen Sie Objektdiagramme als Teil Ihrer Erklärung.

127
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Zusammenfassung
In diesem Kapitel haben wir diskutiert, wie man eine Aufgabe in Teilaufgaben
zerlegen kann. Wir können versuchen, die Subkomponenten in den Objekten
zu identifizieren, die wir modellieren wollen, und wir können diese Subkom-
ponenten in unabhängigen Klassen implementieren. Dieses Vorgehen hilft
uns, die Komplexität bei der Implementierung großer Anwendungen zu redu-
zieren, da es uns ermöglicht, die Klassen unabhängig voneinander zu imple-
mentieren, zu testen und zu warten.
Wir haben gesehen, dass ein solches Vorgehen zu Strukturen von Objekten
führt, die gemeinsam eine Aufgabe erledigen. Objekte können andere Objekte
erzeugen und sie können die Methoden anderer Objekte aufrufen. Ein Ver-
ständnis dieser Objektinteraktionen ist grundlegend sowohl für die Planung
und Umsetzung von Anwendungen als auch für die Fehlersuche in ihnen.
Wir können handgezeichnete Diagramme, das Lesen von Quelltext und Debug-
ger verwenden, um das Verhalten einer Anwendung zu verstehen und um Feh-
ler zu finden.

NEUE BEGRIFFE IN DIESEM KAPITEL


Abstraktion, Modularisierung, teile und herrsche, Klassendiagramm,
Objektdiagramm, Objektreferenz, Überladen, interner Methodenaufruf,
externer Methodenaufruf, Punkt-Notation, Debugger, Haltepunkt

Zusammenfassung der Konzepte


 Abstraktion Abstraktion ist die Fähigkeit, Details von Bestandteilen zu
ignorieren, um den Fokus der Betrachtung auf eine höhere Ebene lenken
zu können.
 Modularisierung Modularisierung ist der Prozess der Zerlegung eines
Ganzen in wohldefinierte Teile, die getrennt erstellt und untersucht wer-
den können und die in wohldefinierter Weise interagieren.
 Klassen definieren Typen Ein Klassenname kann als Typname in einer
Variablendeklaration verwendet werden. Variablen, die als Typ eine Klasse
haben, können Objekte dieser Klasse halten.
 Klassendiagramm Ein Klassendiagramm zeigt die Klassen einer Anwen-
dung und die Beziehungen zwischen diesen Klassen. Es liefert Informationen
über den Quelltext. Es präsentiert eine statische Sicht auf ein Programm.
 Objektdiagramm Ein Objektdiagramm zeigt die Objekte und ihre Bezie-
hungen zu einem bestimmten Zeitpunkt während der Ausführung einer
Anwendung. Es präsentiert eine dynamische Sicht auf ein Programm.
 Objektreferenz Variablen von Objekttypen speichern Referenzen auf
Objekte.

128
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.15 Mehr zu Methodenaufrufen

 Primitiver Typ Die primitiven Typen in Java sind die Typen, die keine Objekt-
typen sind. Die gebräuchlichsten primitiven Typen sind int, boolean, char,
double und long. Primitive Typen haben keine Methoden.
 Objekterzeugung Objekte können andere Objekte mit dem new-Opera-
tor erzeugen.
 Überladen Eine Klasse kann mehr als einen Konstruktor oder mehr als
eine Methode mit dem gleichen Namen enthalten, solange jede von ihnen
einen unterscheidbaren Satz von Parametertypen definiert.
 Interner Methodenaufruf Methoden können andere Methoden der
eigenen Klasse als Teil ihrer Implementierung aufrufen. Dies wird als inter-
ner Methodenaufruf bezeichnet.
 Externer Methodenaufruf Methoden können Methoden von anderen
Objekten über die Punkt-Notation aufrufen. Dies wird als externer Metho-
denaufruf bezeichnet.
 Debugger Ein Debugger ist ein Softwarewerkzeug, mit dessen Hilfe die
Ausführung eines Programms untersucht werden kann. Es kann benutzt
werden, um Fehler (Bugs) zu finden.

Übung 3.52 Benutzen Sie den Debugger, um das Projekt Zeitanzeige zu


untersuchen. Setzen Sie Haltepunkte in den Konstruktor von Uhrenanzeige
und in jede der Methoden und führen Sie sie dann Schritt für Schritt aus.
Verhält sich das Projekt so, wie Sie es erwarten? Konnten Sie neue Erkennt-
nisse gewinnen? Wenn ja, welche?
Übung 3.53 Benutzen Sie den Debugger, um die Methode geldEinwerfen des
Projekts Besserer-Ticketautomat aus Kapitel 2 zu untersuchen. Führen Sie
Tests durch, in denen beide Zweige der if-Anweisung ausgeführt werden.
Übung 3.54 Fügen Sie eine Betreffzeile für eine E-Mail in eine Nachricht aus
dem Projekt Mail-System ein. Lassen Sie auch bei der Ausgabe auf der Konsole
die Betreffzeile mitausgeben. Passen Sie den Mail-Client entsprechend an.
Übung 3.55 Gegeben sei die folgende Klasse (nur auszugsweise angegeben):
public class Bildschirm
{
public Bildschirm(int xAufloesung, int yAufloesung)
{ ...
}

public int anzahlBildpunkte()


{ ...
}

public void loeschen(boolean invertieren)


{ ...
}
}

129
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion

Schreiben Sie einige Zeilen Java-Code, die ein Bildschirm-Objekt erzeugen


und dann seine Methode loeschen nur genau dann aufrufen, wenn die
Anzahl der Bildpunkte größer als 2 Millionen ist (kümmern Sie sich nicht
darum, ob das logisch ist – das Ziel ist lediglich, dass etwas syntaktisch Kor-
rektes aufgeschrieben wird, also etwas, das sich von einem Compiler über-
setzen lassen könnte).
Übung 3.56 Beschreiben Sie die Veränderungen, die an der Uhrenanzeige-
Klasse vorgenommen werden müssten, um Stunden, Minuten und Sekun-
den anzuzeigen. Wie viele Nummernanzeige-Objekte würde ein Uhrenanzeige-
Objekt benötigen?
Übung 3.57 Schreiben Sie den Code für die Methode taktsignalGeben in
Uhrenanzeige, die Stunden, Minuten und Sekunden anzeigt – oder imple-
mentieren Sie sogar die ganze Klasse, wenn Sie möchten.
Übung 3.58 Diskutieren Sie, ob das aktuelle Design der Uhrenanzeige-Klasse
die Anzeige von Stunden, Minuten, Sekunden, Zehntelsekunden und Hun-
dertstelsekunden unterstützen würde. Wie sähe beispielsweise der Code für
die taktsignalGeben-Methode in diesem Fall aus?
Übung 3.59 Zusatzaufgabe. Im aktuellen Design von Uhrenanzeige ist ein
Uhrenanzeige-Objekt dafür verantwortlich festzustellen, wann ein Nummern-
anzeige-Objekt auf null zurückgesprungen ist, und dann ein anderes Num-
mernanzeige-Objekt zu inkrementieren. Mit anderen Worten, es gibt keine
direkte Verbindung zwischen Nummernanzeige-Objekten. Wäre es möglich,
dass ein Nummernanzeige-Objekt einem anderen mitteilt, wenn es auf null
zurückgesprungen ist, und dass sich dieses andere Nummernanzeige-Objekt
dann hochzählt? Dann würde zum Beispiel das minuten-Objekt dem stun-
den-Objekt mitteilen, dass eine weitere Stunde vergangen ist, ober das
sekunden-Objekt sagt dem minuten-Objekt, dass weitere sechzig Minuten
verstrichen sind. Mit welchem dieser Objekte müsste die taktsignalGeben-
Methode interagieren? Welche Datenfelder hätte ein Nummernanzeige-
Objekt? Was sollte das stunden-Objekt tun, wenn ein ganzer Tag abgelaufen
ist? Diskutieren Sie die Probleme dieses alternativen Designs und – wenn
Ihnen der Sinn nach einer Herausforderung steht – versuchen Sie, es zu
implementieren! Beachten Sie, dass Sie dazu eventuell etwas über das Java-
Schlüsselwort null herausfinden müssen, das wir erst am Ende von Kapitel 4
behandeln.

130
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

4 Objektsammlungen

Lernziele
Zentrale Konzepte in diesem Kapitel: Sammlungen, Iteratoren, Schleifen
Java-Konstrukte in diesem Kapitel: ArrayList, Iterator, for-each-Schleife,
while-Schleife, null, anonyme Objekte

In diesem Kapitel fokussieren wir auf die Einführung verschiedener Möglichkei-


ten, Objekte in Sammlungen zusammenzufassen. Insbesondere werden wir die
Klasse ArrayList als ein Beispiel für Sammlungen mit flexibler Größe diskutieren.
Eng verbunden mit Sammlungen ist der Bedarf, über alle Elemente einer Samm-
lung zu iterieren. Zu diesem Zweck stellen wir zwei neue Kontrollstrukturen vor:
die for-Schleife und die while-Schleife.
Dieses Kapitel ist lang und wichtig. Sie werden vermutlich nur dann ein erfolgreicher
Programmierer, wenn Sie den Inhalt dieses Kapitels voll und ganz verstanden haben.
Sie werden wahrscheinlich etwas länger für das Durcharbeiten benötigen als bei den
vorherigen Kapiteln. Doch erliegen Sie nicht der Versuchung, das Kapitel nur ober-
flächlich zu überfliegen; nehmen Sie sich die Zeit und studieren Sie es sorgfältig.

4.1 Themen aus Kapitel 3 vertiefen


Wir werden in diesem Kapitel nicht nur neue Themen wie Sammlungen und Iteration
einführen, sondern auch erneut auf zwei wichtige Themen aus Kapitel 3 eingehen:
Abstraktion und Objektinteraktion. Wir haben gelernt, dass die Abstraktion uns
erlaubt, ein Problem zu vereinfachen, indem wir es in einzelne Komponenten zerle-
gen. So können wir jede Komponente getrennt für sich betrachten, ohne uns in den
Details eines großen Problems zu verlieren. Wir werden dieses Prinzip in die Praxis
umsetzen, wenn wir die Bibliotheksklassen nutzen, die für Java verfügbar sind. Diese
Klassen sind zwar streng genommen nicht Teil der Sprache, doch einige von ihnen
sind so eng mit dem Schreiben von Java-Programmen verwoben, dass sie oft dafür
gehalten werden. Deshalb prüfen Java-Programmierer normalerweise immer erst die
Bibliotheken, um zu sehen, ob bereits jemand zu ihrem Problem eine Klasse geschrie-
ben hat, die ihnen von Nutzen sein könnte. Das spart viel Arbeit, die die Programmie-
rer besser in andere Teile des Programms investieren. Das gleiche Prinzip gilt für die
meisten anderen Programmiersprachen, zu denen es ebenfalls Bibliotheken mit nütz-

131
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

lichen Klassen gibt. Es macht sich also bezahlt, sich mit dem Inhalt der Bibliotheken
und den geläufigsten Klassen vertraut zu machen. Was die Abstraktion so stark
macht, ist, dass wir normalerweise nicht unbedingt (falls überhaupt) wissen müssen,
wie die Klasse im Innern aussieht, um sie effektiv nutzen zu können.
Wenn wir eine Bibliotheksklasse verwenden, folgt daraus, dass wir Code schreiben,
der Instanzen dieser Klasse erzeugt, und anschließend interagieren unsere Objekte
mit denen der Bibliotheksklasse. Deshalb spielt die Objektinteraktion in diesem
Kapitel eine so große Rolle.
Sie werden feststellen, dass die Kapitel in diesem Buch immer wieder Themen
aus vorherigen Kapiteln aufgreifen und vertiefen. Wir bezeichnen dies im Vorwort
als „iterativen Ansatz“. Ein besonderer Vorteil dieses Ansatzes ist, dass Sie damit
Ihr Wissen in den jeweiligen Bereichen peu à peu festigen, während Sie das Buch
durcharbeiten.
Schließlich werden wir in diesem Kapitel unsere Definition des Konzepts der Abs-
traktion erweitern. Wir werden lernen, dass Abstraktion nicht nur bedeutet,
Details zu verbergen, sondern auch, die gemeinsamen Eigenschaften und Muster
zu erkennen, die in einem Programm immer wieder auftauchen. Diese gemeinsa-
men Muster sind der Grund, warum wir eine bereits geschriebene Methode oder
Klasse teilweise oder ganz in einer neuen Situation wiederverwenden können.
Dies gilt vor allem, wenn wir es mit Sammlungen und Iteration zu tun haben.

4.2 Die Abstraktion Sammlung


Eine der Abstraktionen, mit denen wir uns in diesem Kapitel beschäftigen werden, ist
das Konzept der Sammlung, d.h. die Vorstellung, Dinge so zu gruppieren, dass wir
auf alle gemeinsam Bezug nehmen und sie zusammen verwalten können. Eine
Sammlung kann groß sein (alle Studenten einer Universität), klein sein (die Kurse, die
ein Student belegt) oder sogar leer sein (alle Bilder von Picasso, die mir gehören).
Briefmarken, Autogramme, Filmplakate, Schmuckstücke, Musik oder was auch
immer Sie sammeln – es wird immer einige Funktionen geben, die Sie hin und wie-
der auf die Sammlung ausführen wollen. Sie könnten der Sammlung etwas hinzu-
fügen wollen oder die Sammlung verkleinern, zum Beispiel wenn Sie etwas dop-
pelt haben oder Geld für Neuerwerbungen benötigen. Vielleicht wollen Sie auch
Ihre Sammlung umsortieren – zum Beispiel nach Erwerbsdatum oder Wert. Was
wir hier beschreiben sind typische Operationen auf eine Sammlung.
Im Programmierkontext wird die Abstraktion Sammlung zu einer bestimmten Art von
Klasse und die Operationen darauf zu den Methoden dieser Klasse. Eine bestimmte
Sammlung (meine Musiksammlung) wäre dann eine Instanz der Klasse. Und die in
einer Sammlungsinstanz gespeicherten Elemente wären ebenfalls Objekte.

Konzept Weitere Beispiele für Sammlungen, deren Beziehung zu Programmierprojekten


etwas offensichtlicher ist, wären zum Beispiel:
Sammlungen:
Sammlungsobjekte  Elektronische Kalender speichern Terminnotizen zu Verabredungen, Konferen-
sind Objekte, die eine zen, Geburtstagen usw. Werden weitere Termine verabredet, dann kommen
beliebige Anzahl neue Notizen hinzu; und alte Notizen werden gelöscht, wenn Einzelheiten zu
anderer Objekte vergangenen Ereignissen nicht länger benötigt werden.
enthalten können.

132
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.3 Ein Verwaltungssystem für Musikdateien

 Bibliotheken speichern Informationen über ihre Bücher und Zeitschriften. Der


Katalog ändert sich, wenn neue Bücher angeschafft oder alte Bücher ausge-
mustert oder eingelagert werden.
 Universitäten verwalten Datensätze über die immatrikulierten Studenten. Jedes
akademische Jahr kommen neue Datensätze zu der Sammlung hinzu, wäh-
rend die Datensätze derer, die die Uni verlassen, in eine Archiv-Sammlung ver-
schoben werden. Häufig tritt der Fall ein, dass Teilmengen der Sammlung aus-
gegeben werden: alle Studenten, die im ersten Semester Informatik studieren,
oder alle Studenten, die dieses Jahr am Abschlussexamen teilnehmen.
Die Anzahl der Elemente in einer Sammlung ändert sich von Zeit zu Zeit. Bisher
haben wir noch keine Funktionen von Java kennengelernt, die es uns erlaubt, eine
beliebige Anzahl von Elementen zusammenzufassen. Wir könnten beispielsweise
eine Klasse mit sehr vielen individuellen Datenfeldern definieren, um eine feste, aber
sehr große Anzahl an Elementen aufzunehmen. Allerdings benötigen Programme in
der Regel eine allgemeinere Lösung. Eine gute Lösung würde weder voraussetzen,
dass wir im Voraus wissen, wie viele Elemente die Sammlung enthalten soll, noch
verlangen, dass wir eine Obergrenze für die Anzahl angeben müssen.
Deshalb beginnen wir unsere Erkundung der Java-Bibliothek mit einer Klasse, die
die einfachste Form einer Objektsammlung darstellt: eine unsortierte, aber geord-
nete Liste flexibler Größe namens ArrayList. In den nächsten Abschnitten werden
wir als Beispiel eine persönliche Musiksammlung verwalten, um zu zeigen, wie wir
eine beliebige Anzahl von Objekten in nur einem Containerobjekt unterbringen.

4.3 Ein Verwaltungssystem für


Musikdateien
Wir werden eine Klasse schreiben, die uns bei der Organisation unserer Musik-
dateien auf einem Computer unterstützt. Unsere Klasse wird die Dateiinformatio-
nen nicht wirklich speichern, sondern sie wird die Verantwortung an die Standard-
bibliotheksklasse ArrayList delegieren, die uns viel Arbeit abnimmt. Warum müssen
wir dann überhaupt eine eigene Klasse schreiben? Wer mit Bibliotheksklassen arbei-
tet, sollte sich auf alle Fälle merken, dass diese nicht für ein spezielles Anwendungs-
szenario geschrieben wurden, denn es sind allgemeine Klassen. Beispielsweise
könnte ein ArrayList-Objekt Studentendatensätze als Objekte speichern, während
ein anderes Terminnotizen speichert. Das bedeutet, dass erst die Klassen, die wir für
die Nutzung der Bibliotheksklassen schreiben, szenariospezifische Operationen bie-
ten, zum Beispiel eine Datei abspielen, die in der Sammlung gespeichert ist, wenn
wir es mit Musikdateien zu tun haben.
Der Einfachheit halber arbeitet die erste Version dieses Projekts nur mit den Datei-
namen der einzelnen Musik-Tracks. Es gibt keine zusätzlichen Angaben zu Titel,
Interpret, Abspielzeit etc. Das bedeutet, wir bitten ArrayList lediglich, String-
Objekte zu speichern, die Dateinamen repräsentieren. Indem wir in diesem Stadium
die Dinge möglichst einfach halten, vermeiden wir, dass die Schlüsselkonzepte, die
wir veranschaulichen möchten, in den Hintergrund gedrängt werden: die Erzeu-
gung und die Nutzung eines Sammlungsobjekts. Später im Kapitel werden wir

133
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

unser Musiksammlung-Beispiel zu einem immer anspruchsvolleren Verwaltungs- und


Abspielsystem ausbauen.
Wir gehen davon aus, dass jede Musikdatei einen Musik-Track repräsentiert. Die
Beispieldateien zu diesem Projekt enthalten im Dateinamen den Namen des Inter-
preten und den Titel des Tracks – was wir uns später zunutze machen werden. Die
erste Version unseres Vewaltungssystems, die wir als Ausgangsbasis nutzen, unter-
stützt folgende grundlegenden Operationen:
 Es können Tracks zu der Sammlung hinzugefügt werden.
 Es gibt keine Obergrenze bezüglich der Anzahl der zu speichernden Tracks (es sei
denn durch die Speicherkapazität des Rechners).
 Wir erfahren, wie viele Tracks in der Sammlung sind.
 Es werden alle Tracks aufgelistet.
Es zeigt sich, dass die Klasse ArrayList es uns sehr leicht macht, diese Funktiona-
lität aus unserer eigenen Klasse heraus zu bieten.
Beachten Sie, dass wir in dieser ersten Version nicht allzu ehrgeizig sind. Diese Ope-
rationen veranschaulichen lediglich die Grundlagen der Erzeugung und Verwen-
dung der Klasse ArrayList. In späteren Versionen werden wir dann nach und nach
weitere Funktionen hinzufügen, bis wir eine etwas anspruchsvollere Version haben.
(Am wichtigsten wird es vielleicht sein, später die Möglichkeit vorzusehen, Musik-
dateien abzuspielen. Unsere erste Version ist dazu nicht in der Lage.) Dieser beschei-
dene inkrementelle Ansatz ist viel Erfolg versprechender als zu versuchen, alles auf
einmal zu implementieren.
Bevor wir den Quelltext analysieren, der benötigt wird, um eine solche Klasse nut-
zen zu können, ist es sinnvoll, das Startverhalten des Musik-Verwaltungssystems zu
untersuchen.

Übung 4.1 Öffnen Sie in BlueJ das Projekt Musiksammlung-v1 und erzeu-
gen Sie ein Objekt MusikSammlung. Speichern Sie darin die Namen einiger
Audiodateien – dies sind einfache Zeichenketten. Da wir in diesem Stadium
die Dateien noch nicht abspielen, sind beliebige Dateinamen möglich. Es gibt
jedoch im audio-Ordner des Kapitels eine Reihe von Audiodateien, die Sie
nutzen können.
Prüfen Sie, dass die Anzahl der Dateien, die von gibAnzahlDateien zurückge-
liefert wird, mit der Anzahl übereinstimmt, die Sie gespeichert haben. Wenn
Sie die Methode dateiAusgeben verwenden, müssen Sie einen Parameterwert
von 0 (null) verwenden, um die erste Datei auszugeben, 1 (eins), um die zweite
auszugeben, usw. Wir werden die Gründe für diese Form der Nummerierung
zu gegebener Zeit erläutern.
Übung 4.2 Was passiert, wenn Sie ein neues MusikSammlung-Objekt erzeugen
und dann entferneDatei(0) aufrufen, bevor Sie irgendwelche Dateien hinzu-
gefügt haben? Erhalten Sie eine Fehlermeldung? Erwarten Sie einen Fehler?

134
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.4 Eine Bibliotheksklasse verwenden

Übung 4.3 Erzeugen Sie ein MusikSammlung-Objekt und fügen Sie zwei
Dateinamen hinzu. Rufen Sie dateiAusgeben(0) und dateiAusgeben(1) auf, um
die beiden Dateien auszugeben. Rufen Sie jetzt entferneDatei(0) und dann
dateiAusgeben(0) auf. Was passiert? Entspricht dies Ihren Erwartungen? Kön-
nen Sie erklären, was passiert sein könnte, als Sie den Dateinamen aus der
Sammlung entfernt haben?

4.4 Eine Bibliotheksklasse verwenden


Listing 4.1 zeigt die volle Definition unserer Klasse MusikSammlung, die sich der Bib-
liotheksklasse ArrayList bedient. Beachten Sie, dass BlueJ die Bibliotheksklassen
nicht im Klassendiagramm anzeigt.

Klassenbibliotheken
Objektorientierte Sprachen sind unter anderem deshalb so mächtig, weil sie
oft von Klassenbibliotheken begleitet werden. Diese Bibliotheken enthalten
normalerweise viele Hunderte oder Tausende von verschiedenen Klassen, die
sich bereits in zahlreichen Projekten als sehr nützlich für die Entwickler erwie-
sen haben. In Java heißen diese Bibliotheken Pakete. Bibliotheksklassen wer-
den genauso verwendet wie eigene Klassen. Instanzen werden mit new erzeugt
und die Klassen haben Datenfelder, Konstruktoren und Methoden.

Listing 4.1
Die Klasse
MusikSammlung.

135
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

4.4.1 Eine Bibliotheksklasse importieren


Die erste Zeile illustriert, wie mit einer import-Anweisung in einem Java-Programm
der Zugriff auf Bibliotheksklassen deklariert wird:
import [Link];
Diese Anweisung macht die Klasse ArrayList aus dem Paket [Link] innerhalb
unserer Klassendefinition verfügbar. import-Anweisungen müssen immer vor den
Klassendefinitionen in einer Datei stehen. Sobald eine Klasse auf diese Weise
importiert wurde, kann sie wie eine selbst definierte Klasse verwendet werden.
Dies tun wir hier, indem wir ein Datenfeld dateien definieren:
private ArrayList<String> dateien;
Hier lernen wir mit String in spitzen Klammern ein neues Konstrukt kennen:
<String>. Auf den Grund dafür wurde bereits in Abschnitt 4.3 kurz angespielt,
als wir erwähnten, dass ArrayList eine allgemeine Sammlungsklasse ist (d.h. eine
Klasse, deren Speicherkapazität nicht beschränkt ist). Wenn wir eine ArrayList-
Instanz erzeugen, müssen wir jedoch genau angeben, welche Art von Objekten
in dieser speziellen Instanz gespeichert werden soll. Wir können jeden beliebigen
Typ speichern, müssen aber diesen Typ angeben, wenn wir eine ArrayList-Varia-
ble deklarieren. Klassen wie ArrayList, die mit einem zweiten Typ parametrisiert
werden, werden generische Klassen genannt. Später werden wir hierauf noch
näher eingehen.
Wenn wir Sammlungen benutzen, müssen wir immer zwei Typen angeben: den
Typ der Sammlung selbst (hier: ArrayList) und den Typ der Elemente, die wir in der
Sammlung speichern wollen (hier: String). Wir können die gesamte Typdefinition

136
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.4 Eine Bibliotheksklasse verwenden

ArrayList<String> lesen als Array-Liste von Strings. Wir benutzen diese Typdefini-
tion als den Typ für unser Datenfeld dateien.
Wie Sie inzwischen erwarten dürften, gibt es eine enge Verbindung zwischen dem
Rumpf des Konstruktors und den Datenfeldern der Klasse, da der Konstruktor für
die Initialisierung der Datenfelder jeder Instanz zuständig ist. Wie also die Klasse
Uhrenanzeige für ihre beiden Datenfelder Nummernanzeige-Objekte erzeugte, sehen
wir hier den Konstruktor der Klasse MusikSammlung, der ein Objekt vom Typ Array-
List erzeugt und in seinem Datenfeld dateien speichert.

4.4.2 Diamant-Notation
Bei der Erzeugung der ArrayList-Instanz haben wir die folgende Anweisung
geschrieben:
dateien = new ArrayList< >();
Dies ist die sogenannte Diamant-Notation (weil die beiden spitzen Klammern eine
diamantähnliche Form bilden), sie sieht ein wenig ungewöhnlich ein. Wir hatten
vorher gesehen, dass die new-Anweisung die folgende Form hat:
new Typname ( Parameter )
und außerdem, dass der vollständige Typname für unsere Sammlung
ArrayList<String>
lautet. Daher sollte die Anweisung zum Erzeugen eines neuen Sammlungs-
objekts mit einer leeren Parameterliste folgendermaßen aussehen:
dateien = new ArrayList<String>();
In der Tat würde diese Anweisung auch funktionieren. Die erste Version, die die
Diamant-Notation verwendet (und somit den String-Typ nicht nennt), ist eher
eine praktische Abkürzung. Wird die Erzeugung des Sammlungsobjekts mit einer
Zuweisung kombiniert, dann erkennt der Compiler den Typ der Sammlungsele-
mente anhand des Variablentyps auf der linken Seite der Zuweisung. Java erlaubt
uns auch hier wieder, die Definition wegzulassen. Der Elementtyp wird automa-
tisch aus dem Variablentyp gefolgert.
Die Verwendung dieser Form ändert nichts an der Tatsache, dass das zu erzeu-
gende Objekt nichts anderes als String-Objekte speichern kann. Es dient also ledig-
lich der Bequemlichkeit, weil es uns etwas Tipperei erspart und unseren Code ver-
kürzt.

4.4.3 Schlüsselmethoden von ArrayList


Die Klasse ArrayList definiert etliche Methoden; wir werden an dieser Stelle jedoch
nur vier von ihnen benutzen: add, size, get und remove.
Die Bedeutung der ersten beiden wird durch die Benutzung in den Methoden
dateiHinzufuegen und gibAnzahlDateien verdeutlicht. Mit der Methode add an einer
ArrayList wird der Liste ein Objekt hinzugefügt, die Methode size liefert die
Anzahl der bereits gespeicherten Elemente. Wir werden in Abschnitt 4.7 betrach-

137
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

ten, wie die Methoden get und remove funktionieren, obwohl Sie wahrscheinlich
schon vorher eine Vorstellung davon bekommen werden, wenn Sie den Code in
den Methoden dateiAusgeben und entferneDatei lesen.

4.5 Objektstrukturen mit Sammlungen


Um besser verstehen zu können, wie ein Sammlungsobjekt der Klasse ArrayList
arbeitet, sollten wir ein Objektdiagramm betrachten. Abbildung 4.1 zeigt, wie ein
Objekt der Klasse MusikSammlung aussehen könnte, wenn bereits zwei Dateinamen
gespeichert wurden. Vergleichen Sie Abbildung 4.1 mit Abbildung 4.2, in der ein
dritter Dateiname abgelegt ist.
Es gibt mindestens drei Eigenschaften der Klasse ArrayList, die Ihnen auffallen sollten:
 Sie kann ihre interne Kapazität bei Bedarf vergrößern: Wenn weitere Objekte
eingefügt werden, wird für diese einfach Platz geschaffen.
 Sie führt Buch über die Anzahl der aktuell gespeicherten Elemente. Die Methode
size liefert diese Anzahl.
 Sie behält die Reihenfolge bei, in der die Elemente eingefügt werden. Die add-
Methode speichert jedes neue Element am Ende der Liste. Später können die Ele-
mente in der gleichen Reihenfolge wieder aus der Liste herausgeholt werden.

Abbildung 4.1 meineMusik:


Eine Musiksammlung MusikSammlung
mit zwei Dateien.
dateien :ArrayList<String>

:String :String

“MorningBlues.mp3” “DontGo.mp3”

Wir stellen fest, dass die Klasse MusikSammlung sehr einfach aussieht – sie hat nur
ein Datenfeld, in dem ein Objekt vom Typ ArrayList<String> gespeichert ist. Der
schwierige Teil scheint durch das ArrayList-Objekt erledigt zu werden. Dies ist
einer der großen Vorteile von Bibliotheksklassen: Jemand anderes hat Zeit und
Aufwand investiert, um etwas Nützliches zu implementieren, und wir bekommen
quasi kostenlosen Zugriff auf diese Funktionalität, indem wir die Klasse benutzen.
An diesem Punkt brauchen wir uns noch nicht darum zu kümmern, wie ArrayList
diese Funktionalität implementiert. Es reicht uns, wenn wir diese nützliche Klasse
schlicht benutzen können. Erinnern wir uns: Diese Ausblendung der Details ist ein
Vorteil, den wir der Abstraktion verdanken: Es bedeutet, dass wir ArrayList zur
Implementierung von beliebig vielen Klassen verwenden können, die eine Speicher-
möglichkeit für eine nicht festgelegte Anzahl an Objekten benötigen.

138
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.6 Generische Klassen

meineMusik:
Abbildung 4.2
MusikSammlung Eine Musiksammlung
mit drei Dateien.

dateien :ArrayList<String>

:String :String :String

“MorningBlues.mp3” “DontGo.mp3” “MatchBoxBlues.mp3”

Die zweite Eigenschaft – dass ein ArrayList-Objekt einen eigenen Zähler der einge-
fügten Elemente hält – hat wichtige Konsequenzen für die Art, wie wir die Klasse
MusikSammlung implementieren. Obwohl eine Musiksammlung eine Methode
gibAnzahlDateien hat, haben wir kein eigenes Datenfeld definiert, um diese Infor-
mation zu speichern. Stattdessen delegiert eine Musiksammlung die Aufgabe, die
Anzahl der eingefügten Elemente im Auge zu behalten, an sein ArrayList-Objekt.
Auf diese Weise dupliziert eine Musiksammlung nicht solche Informationen, die es
auch von anderer Stelle bekommen kann. Wenn ein Benutzer die Musiksammlung
befragt, wie viele Dateinamen sie enthält, dann reicht die Musiksammlung diese
Frage an das dateien-Objekt weiter und liefert dann dessen Antwort zurück.
Duplizierung von Information oder Verhalten ist etwas, das wir so weit wie mög-
lich vermeiden sollten. Duplizierung deutet meist auf verschwendeten Aufwand
hin und kann zu Inkonsistenzen führen, wenn zwei Dinge, die eigentlich gleich
sein sollten, aufgrund eines Fehlers nicht mehr gleich sind. In den hinteren Kapi-
teln wird uns das Thema Duplizierung noch häufiger beschäftigen.

4.6 Generische Klassen


Die oben neu eingeführte Notation mit den spitzen Klammern müssen wir uns
noch etwas ausführlicher ansehen. Der Typ unseres Datenfeldes dateien wurde
deklariert als
ArrayList<String>
Die Klasse selbst, die wir hier verwenden, heißt einfach ArrayList, aber sie erfor-
dert die Angabe eines zweiten Typs als Parameter, wenn sie zur Deklaration von
Datenfeldern oder anderen Variablen verwendet wird. Klassen, die einen solchen
Parameter erfordern, heißen, wie gesagt, generische Klassen. Generische Klassen
definieren, im Gegensatz zu allen bisher betrachteten Klassen, nicht einen einzel-
nen Typ in Java, sondern potenziell viele Typen. Die Klasse ArrayList beispiels-
weise kann benutzt werden, um eine ArrayList von Strings, eine ArrayList von
Personen, eine ArrayList von Rechtecken oder eine ArrayList von jeder anderen
verfügbaren Klasse zu definieren. Jede spezielle ArrayList ist ein eigener Typ, der

139
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

in der Deklaration von Datenfeldern, Parametern und Methodenergebnissen ver-


wendet werden kann. Wir können beispielsweise die folgenden beiden Datenfel-
der definieren:
private ArrayList<Person> mitglieder;
private ArrayList<Ticketautomat> meineAutomaten;
Diese Definitionen besagen, dass mitglieder eine ArrayList hält, in der Person-
Objekte gespeichert werden können, während meineAutomaten eine ArrayList halten
kann, die Ticketautomat-Objekte speichert. Beachten Sie, dass ArrayList <Person>
und ArrayList<Ticketautomat> verschiedene Typen sind. Diese Datenfelder können
einander nicht zugewiesen werden, obwohl ihre Typen von derselben Klasse abge-
leitet wurden.

Übung 4.4 Schreiben Sie die Deklaration eines privaten Datenfeldes biblio-
thek, das eine ArrayList halten kann. Die Elemente der ArrayList sollen
vom Typ Buch sein.
Übung 4.5 Schreiben Sie die Deklaration einer lokalen Variablen cs101, die
eine ArrayList von Student-Objekten hält.
Übung 4.6 Schreiben Sie eine Deklaration eines privaten Datenfeldes
tracks, um eine Sammlung von Musiktrack-Objekten zu speichern.

Übung 4.7 Schreiben Sie Zuweisungen an die Variablen bibliothek, cs101


und tracks (die Sie in den drei vorangegangenen Übungen definiert haben),
um die entsprechenden ArrayList-Objekte zu erzeugen. Schreiben Sie sie ein-
mal mit Diamant-Notation und einmal ohne, indem Sie den vollständigen Typ
angeben.

Generische Klassen werden für verschiedene Zwecke benutzt; wir werden im


Lauf dieses Buches noch weitere kennenlernen. Vorläufig sind Sammlungen wie
ArrayList und einige weitere, die wir uns bald näher ansehen, die einzigen gene-
rischen Klassen, mit denen wir umgehen müssen.

4.7 Nummerierung in Sammlungen


Als wir das Musiksammlung-Projekt in den ersten paar Übungen untersucht haben,
haben wir festgestellt, dass wir für die Ausgabe und das Entfernen von Dateien
aus der Sammlung Parameter beginnend mit dem Wert null angeben mussten.
Der Grund für diese Anforderung ist, dass die Elemente in einer Sammlung vom
Typ ArrayList eine implizite Nummerierung oder Position haben, die mit null
beginnt. Die Position eines Elements in einer Sammlung wird üblicherweise sein
Index genannt. Das erste eingefügte Element in einer Sammlung hat den Index 0,
das zweite den Index 1 und so weiter. Abbildung 4.3 illustriert dieselbe Situation
wie oben, ergänzt um Indexnummern im ArrayList-Objekt.

140
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.7 Nummerierung in Sammlungen

Das heißt, dass dabei das letzte Element in einer Sammlung den Index größe-1
hat. Zum Beispiel hat in einer Liste von 20 Elementen das letzte den Index 19.

meineMusik:
Abbildung 4.3
MusikSammlung Indexnummern der
Elemente in einer
:ArrayList<String> Sammlung.
dateien
0 1 2

:String :String :String

“MorningBlues.mp3” “DontGo.mp3” “MatchBoxBlues.mp3”

Die Methoden dateiAusgeben und entferneDatei veranschaulichen beide, wie Index-


nummern verwendet werden, um Zugriff auf ein Element in einer ArrayList zu
erhalten: im ersten Fall über ihre get-Methode und im zweiten über ihre remove-
Methode. Beachten Sie, dass beide Methoden sicherstellen, dass ihr Parameter im
Bereich der gültigen Indexwerte [0…size()-1] liegt, bevor sie den Index in die Array-
List-Methoden hineinreichen. Diese Validierung ist guter Programmierstil, den Sie
möglichst übernehmen sollten, da er verhindert, dass der Aufruf einer Biblio-
theksklassenmethode aufgrund potenziell ungültiger Parameter fehlschlägt.

Fallstrick
Bei nachlässiger Benutzung kann es Ihnen passieren, dass Sie einen Zugriff auf
ein Element mit einem Index außerhalb der gültigen Indexgrenzen einer Array-
List versuchen. In einem solchen Fall erhalten Sie eine Fehlermeldung. Diesen
Fehler bezeichnen wir als Index-Grenzverletzung (index-out-of-bounds). In
Java werden Sie entsprechend eine Meldung über eine IndexOutOfBounds-
Exception zu sehen bekommen.

Übung 4.8 Wenn eine Sammlung 10 Elemente enthält, welchen Wert lie-
fert dann ein Aufruf ihrer Methode size?
Übung 4.9 Schreiben Sie eine Methode, die mithilfe der Methode get das
fünfte Objekt in einer Sammlung mit dem Namen elemente liefert.
Übung 4.10 Welchen Index hat das letzte Element in einer Sammlung von
15 Objekten?
Übung 4.11 Schreiben Sie eine Methode, die ein Objekt, auf das die Variable
lieblingstrack verweist, in die Sammlung dateien einfügt.

141
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

4.7.1 Elemente aus einer Sammlung entfernen


Nicht nur das Hinzufügen von Elementen zu einer Sammlung ist wichtig, sondern
auch das Entfernen von Elementen, wie wir bereits anhand der Methode entferne-
Datei in Listing 4.1 sehen konnten. Hierfür bietet die Klasse ArrayList eine
Methode remove, die den Index des zu entfernenden Elements als Parameter
bekommt. Eine Besonderheit beim Entfernen von Elementen ist, dass es die
Indexwerte der restlichen in der Sammlung gespeicherten Objekte ändert. Wenn
ein Element mit einer niedrigen Indexnummer entfernt wird, dann verschiebt die
Sammlung alle nachfolgenden Elemente um eine Position, um die Lücke zu füllen.
Folglich werden ihre Indexnummern um 1 verringert.
Abbildung 4.4 illustriert, wie die Indexwerte einiger Elemente in einer ArrayList sich
verschieben, wenn ein Element in der Mitte entfernt wird. Ausgehend von der Situ-
ation in Abbildung 4.3 wurde das Objekt mit der Nummer 1 entfernt. Als Ergebnis
hat sich der Index des Objekts mit dem ursprünglichen Index 2 auf 1 geändert, wäh-
rend der Index des Objekts mit der Nummer 0 unverändert geblieben ist.
Zusätzlich werden wir später noch sehen, dass es neben dem Anfügen am Ende
noch andere Möglichkeiten zum Einfügen von Elementen in eine ArrayList gibt.
Dies bedeutet, dass sich bei Elementen, die bereits in der Sammlung enthalten
sind, beim Einfügen eines neuen Elements ihre Indexnummern verschieben kön-
nen. Den Benutzern sollten solche möglichen Änderungen der Indizes durch Ein-
fügen oder Entfernen bewusst sein.

Abbildung 4.4 meineMusik:


Änderung von MusikSammlung
Indexnummern nach
Entfernen eines
dateien :ArrayList<String>
Elements.
0 1

:String :String

“MorningBlues.mp3” “MatchBoxBlues.mp3”

4.7.2 Vom allgemeinen Nutzen der Nummerierung


(von Sammlungen)
Die Verwendung von ganzzahligen Indexwerten, um auf Objekte in einer Samm-
lung zuzugreifen, wird uns immer wieder begegnen – und zwar nicht nur in
ArrayList-Instanzen, sondern auch in anderen Sammmlungstypen. Deshalb ist es
so wichtig, dass wir alles, was wir bisher gesehen haben, auch verstehen: dass
die Indexwerte bei null beginnen, dass die Objekte sequenziell durchnummeriert
werden und dass es in der Regel keine Lücken in den Indexwerten der aufeinan-
derfolgenden Objekte in der Sammlung gibt.

142
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.7 Nummerierung in Sammlungen

Ganze Zahlen als Indexwerte zu verwenden, macht es einfach, im Programmcode


Dinge wie „das nächste Element“ oder „das vorherige Element“ mit Bezug auf ein
Element in der Sammlung auszudrücken. Wenn ein Element an Indexposition p
steht, dann steht das nächste an Index (p+1) und das vorherige an Index (p-1). Wir
können auch eine Auswahl von Elementen in natürlicher Sprache wie „die ersten
drei“ bzw. „die Elemente 0, 1 und 2“ in einer Programmiersprache ausdrücken.
Oder wir könnten „die letzten vier“ Elemente ausdrücken als die Elemente an den
Indexpositionen „([Link]()-4) bis ([Link]()-1)“. Wir könnten uns sogar
vorstellen, eine ganze Sammlung durchzugehen, indem wir eine Indexvariable vom
Typ int einrichten, deren Wert am Anfang auf null gesetzt und anschließend suk-
zessive um 1 erhöht wird. Dann übergeben wir ihren Wert der get-Methode, um
auf jedes Element in der Liste der Reihe nach zuzugreifen, und hören auf, wenn
der letzte Indexwert überschritten wurde.
Doch wir greifen vor. Wir werden in Bälde im Zusammenhang mit Schleifen und
Iterationen sehen, wie all dies funktioniert.

Übung 4.12 Schreiben Sie eine Methode, die das dritte Objekt aus der
Sammlung verabredungen entfernt.
Übung 4.13 Nehmen Sie an, dass in einer Sammlung ein Objekt bei Index 6
gespeichert ist. Welchen Index wird dieses Objekt haben, nachdem die
Objekte bei Index 0 und Index 9 entfernt wurden?
Übung 4.14 Fügen Sie zu der Klasse MusikSammlung eine Methode indexPruefen
hinzu. Diese Methode übernimmt einen int-Parameter und prüft, ob er ein
gültiger Index für die aktuelle Sammlung ist. Um gültig zu sein, muss der
Parameter im Bereich 0 bis size()-1 liegen.
Wenn der Parameter nicht gültig ist, sollte die Methode eine Fehlermeldung
ausgeben, die Auskunft über den gültigen Indexbereich gibt. Ist der Index gül-
tig, sollte nichts ausgegeben werden. Testen Sie Ihre Methode auf der Objekt-
leiste sowohl mit gültigen als auch ungültigen Parametern. Funktioniert Ihre
Methode auch, wenn Sie den Index einer leeren Sammlung prüfen?
Übung 4.15 Schreiben Sie eine alternative Version von indexPruefen namens
gueltigerIndex. Diese Methode soll einen int-Parameter übernehmen und
einen booleschen Wert zurückliefern. Sie gibt nichts aus, sondern liefert
true zurück, wenn der Wert des Parameters ein gültiger Index für die aktu-
elle Sammlung ist, und andernfalls false. Testen Sie Ihre Methode auf der
Objektleiste sowohl mit gültigen als auch ungültigen Parametern. Testen Sie
auch für den Fall, dass die Sammlung leer ist.
Übung 4.16 Ändern Sie die Methoden dateiAusgeben und entferneDatei in
der Klasse MusikSammlung so, dass sie statt des booleschen Ausdrucks Ihre
Methode gueltigerIndex verwenden, um ihren Parameter zu prüfen. Sie soll-
ten nur dann get oder remove auf die ArrayList aufrufen, wenn gueltigerIndex
true zurückliefert.

143
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

4.8 Musikdateien abspielen


Es wäre nett, wenn unser Verwaltungssystem nicht nur eine Liste von Musikdateien
speichern würde, sondern zusätzlich die Möglichkeit bieten würde, Dateien abzu-
spielen. Auch hier kann uns die Abstraktion helfen. Wenn wir eine Klasse zur Ver-
fügung hätten, die speziell dem Abspielen von Audiodateien dient, müsste unsere
MusikSammlung-Klasse das nicht auch noch übernehmen. Sie würde lediglich den
Namen der Datei in die Abspiel-Klasse reichen und ihr den Rest überlassen.
Leider gibt es in der Java-Standardbibliothek keine Klasse, mit der das von uns
verwendete Audioformat .mp3 abgespielt werden kann. Doch glücklicherweise
gibt es viele Programmierer, die ständig eigene nützliche Klassen schreiben und
sie anderen Personen zur Verfügung stellen. Diese werden oft als „Bibliotheken
von Drittanbietern“ bezeichnet und genauso importiert und verwendet wie die
Klassen der Standard-Java-Bibliothek.
Für die nächste Version unseres Projekts haben wir eine Reihe von Klassen aus
[Link] herangezogen, um eine eigene Musikplayer-Klasse zu schreiben. Sie
finden sie in der Version Musiksammlung-v2. Die drei Methoden der Klasse
MusikPlayer, die wir verwenden, lauten dateiAnspielen, starteAbspielen und stop.
Die ersten beiden nehmen den Namen der abzuspielenden Audiodatei entgegen.
Die erste spielt einige Sekunden vom Anfang der Datei und kehrt zurück, wenn
sie mit dem Abspielen fertig ist, während die zweite das Abspielen im Hinter-
grund startet und dann die Kontrolle sofort wieder an das Verwaltungssystem
zurückgibt, was die Notwendigkeit für eine stop-Methode erklärt (falls Sie das
Abspielen abbrechen wollen). Listing 4.2 enthält die neuen Elemente der Klasse
MusikSammlung, die auf einige der Abspielfunktionen zugreifen.

Listing 4.2
Abspielfunktionen
der Klasse
MusikSammlung.

144
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.8 Musikdateien abspielen

Übung 4.17 Erzeugen Sie in der zweiten Version unseres Projekts ein Objekt
der Klasse MusikSammlung. Experimentieren Sie ein wenig herum, indem Sie
Dateien hinzufügen und abspielen.
Wenn Sie die Dateien nutzen wollen, die im Kapitelordner audio gespeichert
sind, müssen Sie den Namen des Ordners zusammen mit dem Dateinamen und
dem Suffix im dateiname-Parameter angeben. Um zum Beispiel die Datei Blind-
Blake-EarlyMorningBlues.mp3 aus dem audio-Ordner zu verwenden, müssen
Sie der Methode dateiHinzufuegen den String "../audio/BlindBlake-EarlyMor-
ningBlues.mp3" übergeben.

Sie können auch eigene mp3-Dateien verwenden. Sie müssen diese Dateien
nur in dem audio-Ordner ablegen. Denken Sie jedoch daran, den Ordner-
namen als Teil des Dateinamens anzugeben.
Experimentieren Sie auch mit Dateinamen, die nicht existieren. Was passiert
dann?

4.8.1 Zusammenfassung des Musiksammlung-


Projekts
Wir haben mit den grundlegenden Funktionen zur Organisation unserer Musik-
sammlung gute Fortschritte gemacht. Wir können die Namen einer beliebigen
Anzahl von Musikdateien speichern und sogar abspielen. Und der Programmier-
aufwand war viel geringer als gedacht, da wir die Möglichkeit hatten, von den
Funktionen der folgenden Bibliotheksklassen zu profitieren: ArrayList aus der
Java-Standardbibliothek und eine Musikplayer-Klasse, die eine Klassenbibliothek
eines Drittanbieters verwendet. Außerdem mussten wir kaum etwas von den
internen Abläufen in diesen Bibliotheksklassen wissen. Es reichte, die Namen,
Parametertypen und den Ergebnistyp der Schlüsselmethoden zu kennen.
Es fehlen jedoch noch einige wichtige Funktionen, wenn wir ein wirklich nützliches
Programm implementieren wollen. Am auffälligsten ist, dass es beispielsweise gar
keine Möglichkeit gibt, die ganze Sammlung auszugeben. Dies soll Thema des
nächsten Abschnitts sein, in dem wir die erste von mehreren Java-Kontrollstruktu-
ren einführen.

145
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

4.9 Komplette Sammlungen verarbeiten


Am Ende des letzten Abschnitts haben wir erwähnt, dass es nützlich wäre, eine
Methode in der Klasse MusikSammlung zu haben, die alle Dateinamen der Sammlung
ausgibt. Da wir wissen, dass jeder Dateiname der Sammlung mit einer eindeutigen
Indexnummer verbunden ist, besteht eine Möglichkeit, die Ausgabe umzusetzen,
darin, die Dateinamen, die bei aufeinanderfolgenden, zunehmenden Indexnum-
mern gespeichert sind, beginnend mit null auszugeben. Bevor Sie weiterlesen, soll-
ten Sie anhand der folgenden Übungen feststellen, ob Sie mit den Java-Mitteln, die
wir bereits kennen, eine solche Methode schreiben können.

Übung 4.18 Wie würde der Kopf einer Methode alleDateienAusgeben in der
Klasse MusikSammlung aussehen? Welchen Ergebnistyp sollte sie haben? Benö-
tigt sie Parameter?
Übung 4.19 Wir wissen, dass das erste Element einer ArrayList bei Index 0
gespeichert ist und die Liste die Dateinamen als Zeichenketten speichert.
Könnten wir den Rumpf der Methode alleDateienAusgeben ungefähr nach
folgendem Muster schreiben?
[Link]([Link](0));
[Link]([Link](1));
[Link]([Link](2));
usw.
Wie viele println-Aufrufe wären notwendig, um die Methode zu vervollstän-
digen?

Vermutlich haben Sie bemerkt, dass diese Frage nicht wirklich beantwortet werden
kann, weil die Antwort von der Anzahl der Dateien abhängt, die zum Zeitpunkt
der Ausgabe in der Liste enthalten sind. Bei drei Dateien würden wir drei println-
Aufrufe benötigen, bei vieren wären es vier usw. Die Methoden dateiAusgeben und
entferneDatei zeigen, dass der Bereich der gültigen Indexnummern zu jedem Zeit-
punkt von null bis zur Anzahl der zusammengetragenen Dateien minus eins defi-
niert ist. Entsprechend sollte eine Methode alleDateienAusgeben ebenfalls die dyna-
mische Größe der Liste berücksichtigen, um die Aufgabe korrekt auszuführen.
Wir haben es hier mit einer Situation zu tun, in der etwas mehrfach getan wer-
den soll; aber wie oft genau, das hängt von Umständen ab, die variieren können –
in diesem Fall der Größe der Sammlung. Diese Art von Problem werden wir bei
der Programmierung sehr häufig haben, und die meisten Programmiersprachen
bieten mithilfe von Schleifenanweisungen (die auch als iterative Kontrollstruktu-
ren bezeichnet werden) mehrere Wege, um es zu lösen.
Die erste Schleife, die wir zur Ausgabe der Dateien einführen wollen, ist eine der
besonderen Art für Sammlungen, da sie komplett auf eine Indexvariable verzich-
tet: die for-each-Schleife.

146
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.9 Komplette Sammlungen verarbeiten

4.9.1 Die for-each-Schleife


Eine for-each-Schleife ist eine Möglichkeit, eine Menge von Aktionen auf den Konzept
Elementen einer Sammlung zu wiederholen, ohne dass die Aktionen mehrfach in
Eine Schleife
den Quelltext geschrieben werden müssen, wie wir dies in Übung 4.19 gesehen
kann benutzt
haben. Die Struktur einer for-each-Schleife kann durch folgenden Pseudocode werden, um einen
dargestellt werden: Block von Anwei-
for (Elementyp element : sammlung) { sungen wiederholt
Schleifenrumpf ausführen zu lassen,
ohne dass die
}
Anweisungen mehr-
Das entscheidend neue Java-Konzept an dieser Stelle ist das Schlüsselwort for. Die fach in den Quell-
Sprache Java bietet zwei Varianten einer for-Schleife: Eine ist die for-each-Schleife, text geschrieben
die wir hier gerade näher betrachten, die andere heißt einfach for-Schleife und werden müssen.
wird in Kapitel 7 diskutiert.
Eine for-each-Schleife hat zwei Bestandteile: einen Schleifenkopf (die erste Zeile
der Schleifenanweisung) und einen darauf folgenden Schleifenrumpf. Der Rumpf
enthält die Anweisungen, die wir wiederholt ausführen wollen.
Die for-each-Schleife hat ihren Namen von der Art, wie wir sie lesen können („for
each“ lässt sich übersetzen als „für jedes“): Wenn wir das Schlüsselwort for
lesen als „für jedes“ und den Doppelpunkt als „in“, dann ergibt die Struktur im
oben angegebenen Pseudocode sinngemäß Folgendes:
für jedes element in sammlung tue: {
Schleifenrumpf
}
Wenn Sie diese Version mit der ersten vergleichen, dann sehen Sie, dass element
oben in Form einer Variablendeklaration als Elementtyp element geschrieben
wurde. Es wird auch tatsächlich eine Variable deklariert, die dann nacheinander
für alle Elemente der Sammlung verwendet wird. Aber bevor wir das weiter aus-
führen, sollten wir uns zuerst ein Beispiel mit echtem Java-Code ansehen.
Listing 4.3 zeigt eine Implementierung einer Methode alleDateienAusgeben, die
alle Dateien in der ArrayList der Musiksammlung mithilfe einer for-each-Schleife
auf die Konsole ausgibt.

Listing 4.3
Benutzung einer
for-each-Schleife zur
Ausgabe aller Dateien.

In dieser for-each-Schleife wird der Schleifenrumpf – der aus der einzelnen Anwei-
sung [Link] besteht – wiederholt ausgeführt, einmal für jedes Element
in der ArrayList-Sammlung dateien. Wenn beispielsweise vier Strings in der Liste
wären, würde die Anweisung viermal ausgeführt.

147
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Vor jeder Ausführung der Anweisung wird der Variablen dateiname eines der Ele-
mente in der Liste zugewiesen, zuerst das bei Index 0, dann das bei Index 1 und so
weiter. Auf diese Weise wird jedes Element in der Liste ausgegeben.
Lassen Sie uns diese Schleife noch etwas genauer ansehen. Das Schlüsselwort for
leitet die Schleife ein. Es wird gefolgt von einem Paar runder Klammern, in denen
die Details der Schleife definiert werden. Das erste Detail ist die Deklaration
String dateiname – dies deklariert eine neue lokale Variable dateiname, die nachein-
ander die Elemente der Liste enthält. Wir nennen diese Variable die Schleifen-
variable. Wir können den Namen dieser Variablen wie bei jeder Variablen frei
wählen, es muss nicht wie hier „dateiname“ sein. Der Typ der Schleifenvariablen
muss übereinstimmen mit dem deklarierten Typ der Elemente der Sammlung, die
wir benutzen wollen; in unserem Fall ist er String.
Es folgen ein Doppelpunkt und die Variable, die die Sammlung hält, die wir ver-
arbeiten wollen. Jedes Element dieser Sammlung wird nacheinander der Schleifen-
variablen zugewiesen und nach jeder dieser Zuweisungen wird der Schleifenrumpf
einmal ausgeführt. Im Schleifenrumpf können wir uns über die Schleifenvariable
auf jedes Element beziehen.
Um zu überprüfen, ob Sie die Arbeitsweise dieser Schleife verstehen, sollten Sie
die folgenden Übungen bearbeiten.

Übung 4.20 Implementieren Sie die Methode alleDateienAusgeben in Ihrer


Version des Musiksammlung-Projekts. (Eine Lösung, in der diese Methode
implementiert ist, finden Sie im Projekt Musiksammlung-v3, aber für einen
besseren Lerneffekt empfehlen wir, dass Sie die Methode selbst implemen-
tieren.)
Übung 4.21 Erzeugen Sie eine MusikSammlung und speichern Sie einige
Dateinamen darin. Benutzen Sie die Methode alleDateienAusgeben, um zu
prüfen, ob diese wie gewünscht funktioniert.
Übung 4.22 Erzeugen Sie in der Direkteingabe ein ArrayList<String>-Objekt,
indem Sie die folgenden zwei Zeilen eingeben:
import [Link];
new ArrayList<String>()
Wenn Sie die letzte Zeile nicht mit einem Semikolon abschließen, erscheint
das kleine rote Objektsymbol. Ziehen Sie dieses Symbol in die Objektleiste.
Informieren Sie sich über die zur Verfügung stehenden Methoden und ver-
suchen Sie, einige davon aufzurufen (z.B. add, remove, size, isEmpty). Rufen Sie
die gleichen Methoden über die Direkteingabe auf. Sie können aus der Direkt-
eingabe auf die Objekte in der Objektleiste zugreifen, indem Sie deren Namen
eingeben. Wenn Sie z.B. ein ArrayList über den Namen al1 auf die Objekt-
leiste gelegt haben, können Sie es in der Direkteingabe ansprechen mit
[Link]()

148
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.9 Komplette Sammlungen verarbeiten

Übung 4.23 Wenn Sie wollen, können Sie den Debugger verwenden, um zu
beobachten, wie die Anweisungen in der Schleife in alleDateienAusgeben wie-
derholt werden. Setzen Sie einen Haltepunkt unmittelbar vor die Schleife und
gehen Sie die Methode Schritt für Schritt durch, bis die Schleife alle Elemente
verarbeitet hat und die Methode endet.
Übung 4.24 Zusatzaufgabe. Die for-each-Schleife verwendet keine explizite
int-Variable, um nacheinander auf die Elemente in der Liste zuzugreifen.
Wenn wir also den Index von jedem Dateinamen mit ausgeben wollen, müs-
sen wir unsere eigene lokale int-Variable (z.B. position) deklarieren, sodass
wir im Rumpf der Schleife in etwa Folgendes schreiben können:
[Link](position + ": " + dateiname);
Versuchen Sie, hierfür eine Version von alleDateienAusgeben entsprechend zu
erweitern. Hinweis: Sie werden eine lokale Variable position in der Methode
deklarieren müssen, sowie eine Anweisung, die deren Wert innerhalb der
for-each-Schleife um eins aktualisiert.

Diese Übung veranschaulicht, dass die for-each-Schleife nicht wirklich für die
Verwendung einer separaten Indexvariablen geeignet ist.

Wir haben nun gesehen, wie wir mit einer for-each-Schleife eine bestimmte Opera-
tion (den Schleifenrumpf) auf jedes Element einer Sammlung anwenden können.
Das ist ein großer Schritt vorwärts, aber er löst noch nicht alle unsere Probleme.
Manchmal brauchen wir etwas mehr Kontrolle und Java bietet uns ein weiteres
Schleifenkonstrukt an, mit dem wir mehr machen können: die while-Schleife.

4.9.2 Eine Sammlung selektiv verarbeiten


Die Methode alleDateienAusgeben veranschaulicht, wie nützlich eine for-each-
Schleife ist: Sie bietet über die Variable, die im Schleifenkopf deklariert ist, nachei-
nander Zugriff auf jedes Element in einer Sammlung. Sie liefert uns zwar nicht die
Indexpositionen der einzelnen Elemente, aber diese werden nicht immer benötigt,
sodass das nicht notwendigerweise ein Problem darstellt.
Die Möglichkeit, auf jedes Element in der Sammlung zuzugreifen, bedeutet aller-
dings nicht, dass wir auf jedem Element das Gleiche ausführen müssen. Wir kön-
nen selektiver sein. Vielleicht wollen wir nur die Tracks eines bestimmten Interpre-
ten ausgeben oder suchen nach allen Tracks, die ein bestimmtes Wort im Titel
enthalten. Es gibt nichts, was uns davon abhalten sollte, da der Rumpf einer for-
each-Schleife nur ein normaler Block ist, in dem wir jede beliebige Java-Anweisung
verwenden können. Deshalb sollte es kein Problem bereiten, eine if-Anweisung im
Rumpf einzufügen, um die gewünschten Dateien auszuwählen.
Listing 4.4 zeigt eine Methode, die genau die Dateinamen der Sammlung ausgibt,
die eine bestimmte Zeichenkette beinhalten.

149
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Listing 4.4
Ausgewählte
Elemente der
Sammlung
ausgeben.

Mithilfe einer if-Anweisung und dem booleschen Ergebnis der contains-Methode


der Klasse String können wir „filtern“, welche Dateien ausgegeben werden sollen
und welche nicht. Wenn der Dateiname nicht mit den Filterkriterien (dem Test in
der if-Anweisung) übereinstimmt, wird er einfach übergangen – es wird kein else-
Teil benötigt. Das Filterkriterium können wir frei wählen.

Übung 4.25 Fügen Sie die Methode bestimmteDateienAusgeben aus Listing 4.4
Ihrer letzten Projektversion hinzu. (Wenn Sie noch keine eigene Version ange-
legt haben, können Sie auch Musiksammlung-v3 verwenden.) Vergewissern
Sie sich, dass die Methode nur Dateien ausgibt, die dem Kriterium entsprechen.
Versuchen Sie, einen Suchbegriff anzugeben, für den es keine Übereinstim-
mung in den Dateinamen gibt. Wird in diesem Fall irgendetwas ausgegeben?
Übung 4.26 Zusatzaufgabe. Gibt es in der Methode bestimmteDateienAusgeben
eine Möglichkeit, nach Beenden der for-each-Schleife eine Nachricht auszuge-
ben, die darüber informiert, dass kein Dateiname mit dem Suchbegriff über-
einstimmt? Hinweis: Verwenden Sie eine lokale boolesche Variable.
Übung 4.27 Schreiben Sie in Ihrer Projektversion eine Methode, die alle Tracks
eines bestimmten Interpreten hintereinander anspielt. Die Methode bestimmte-
DateienAusgeben veranschaulicht die Grundstruktur, die Sie für diese Methode
benötigen. Achten Sie darauf, einen Interpreten auszuwählen, von dem Sie
mehr als einen Track gespeichert haben. Verwenden Sie die MusikSammlung-
Methode dateiAnspielen (zu finden in Musiksammlung-v3). Die Methode
dateiAnspielen spielt den Anfang eines Stücks (ca. 15 Sekunden) und kehrt
dann zurück.
Übung 4.28 Schreiben Sie den Kopf einer for-each-Schleife, die eine Array-
List<Track> namens tracks verarbeitet. Machen Sie sich noch keine Gedan-
ken um den Schleifenrumpf.

4.9.3 Probleme bei der Verwendung von


Zeichenketten
Aus den obigen Übungen wird ersichtlich, dass allein Dateinamen, die alle Einzel-
heiten zu den Musikstücken enthalten, nicht wirklich befriedigend sind. Angenom-
men, Sie wollten nach allen Musikstücken suchen, die im Titel das Wort „Liebe“
aufweisen. Mit der einfachen Suchtechnik, wie wir sie oben beschrieben haben,

150
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.9 Komplette Sammlungen verarbeiten

finden wir auch Musikstücke, bei denen der Begriff „Liebe“ im Namen des Inter-
preten (z.B. Liebermann) vorkommt. Das mag auf den ersten Blick kein besonders
großes Problem sein, wirkt aber etwas „billig“. Das heißt, es sollte mit nur wenig
Mehraufwand möglich sein, eine bessere Version zu erstellen. Was wir dazu benö-
tigen, ist eine separate Klasse, sagen wir Track, die die Angaben zu Interpret und
Titel unabhängig vom Dateinamen speichert. Dann könnten wir einfacher die Titel
separat von den Interpreten abgleichen. Die ArrayList<String> würde dann zu
einer ArrayList<Track>.
Bei der Weiterentwicklung unseres Musiksammlung-Projekts in den folgenden
Abschnitten werden wir uns letztendlich durch Einführen einer Track-Klasse einer
besseren Struktur annähern.

4.9.4 Zusammenfassung der for-each-Schleife


Die for-each-Schleife wird immer verwendet, um über eine Sammlung zu iterieren.
Sie bietet die Möglichkeit, nacheinander jedes Element in der Sammlung anzu-
sprechen und auf beliebige Art und Weise zu verarbeiten. Wir können mit jedem
Element das Gleiche machen (wie bei der Ausgabe der ganzen Liste) oder die
Liste filtern und Elemente selektieren (wie bei der Ausgabe einer Teilmenge der
Sammlung). Der Rumpf der Schleife kann beliebig kompliziert sein.
Die Einfachheit dieses Konzepts weist gezwungenermaßen einige Beschränkun-
gen auf. Eine Beschränkung ist zum Beispiel, dass wir die Sammlung während
der Iteration weder durch Hinzufügen neuer Elemente erweitern noch durch Ent-
fernen von Elementen verkleinern können. Das heißt jedoch nicht, dass wir die
Zustände der Objekte, die bereits in der Sammlung sind, nicht ändern können.
Wir haben auch gesehen, dass die for-each-Schleife uns keine Indexwerte für die
Elemente in der Sammlung liefert. Wenn wir dies wünschen, müssen wir unsere
eigene lokale Variable deklarieren und verwalten. Der Grund dafür ist wie immer
die Abstraktion. In Zusammenhang mit Sammlungen und der Iteration darüber
sollten Sie sich zwei Dinge merken:
 Eine for-each-Schleife ist eine allgemeine Kontrollstruktur, mit der Sie über ver-
schiedene Arten von Sammlungen iterieren können.
 Es gibt einige Arten von Sammlungen, bei denen die darin gespeicherten Ele-
mente nicht automatisch mit ganzen Zahlen als Indexwerte verbunden wer-
den. Einige davon werden Sie in Kapitel 6 kennenlernen.
Die for-each-Schleife abstrahiert sozusagen die Aufgabe, eine vollständige Samm-
lung Element für Element zu verarbeiten, und zwar für verschiedene Arten von
Sammlungen. Die Einzelheiten darüber müssen wir nicht wissen.
Eine der Fragen, die wir noch nicht gestellt haben, ist, ob eine for-each-Schleife ver-
wendet werden kann, wenn wir mitten in der der Sammlung mit der Verarbeitung
aufhören. Angenommen wir spielen nicht jedes Musikstück des von uns gewählten
Interpreten, sondern nur das erste und gehen nicht weiter. Im Prinzip ist das mit
einer for-each-Schleife möglich. Aber Erfahrung hat uns gelehrt, dass es nicht rat-
sam ist, eine for-each-Schleife für Aufgaben zu verwenden, in der nicht die ganze
Sammlung verarbeitet werden muss. Mit anderen Worten, wir empfehlen die Ver-

151
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

wendung einer for-each-Schleife nur, wenn Sie definitiv die ganze Sammlung ver-
arbeiten wollen. Das bedeutet, sobald Sie die Schleife gestartet haben, wissen Sie
genau, wie oft der Rumpf ausgeführt wird – und zwar entsprechend der Größe der
Sammlung. Dieser Stil wird oft als bestimmte Iteration (definite iteration) bezeich-
net. Bei Aufgaben, die mittendrin abgebrochen werden sollen, gibt es geeignetere
Schleifen – zum Beispiel die while-Schleife, die wir im Folgenden vorstellen. In die-
sen Fällen ist die Anzahl, wie oft der Schleifenrumpf ausgeführt werden soll, nicht
so sicher; sie hängt oft davon ab, was während der Iteration passiert. Dieser Stil
wird of unbestimmte Iteration (indefinite iteration) genannt und ist Thema des
nächsten Abschnitts.

4.10 Unbestimmte Iteration


Mit der for-each-Schleife konnten wir erste Erfahrungen mit dem Prinzip der wie-
derholten Ausführung von Aktionen sammeln. Die Anweisungen im Schleifen-
rumpf werden nacheinander auf jedes Element der Sammlung ausgeführt. Der
Durchlauf ist beendet, wenn wir das Ende der Sammlung erreicht haben. Eine
for-each-Schleife bietet bestimmte Iteration, das heißt, wenn der Zustand einer
bestimmten Sammlung gegeben ist, wird der Schleifenrumpf genau so oft aus-
geführt, wie Elemente in der Sammlung gespeichert sind. Es gibt jedoch viele
Situationen, in denen wir wiederholt einige Aktionen ausführen wollen, ohne im
Voraus genau zu wissen, wie viele Durchläufe es geben wird. In diesen Fällen ist
eine for-each-Schleife keine große Hilfe.
Angenommen, Sie haben Ihre Schlüssel verloren und müssen sie finden, bevor
Sie das Haus verlassen können. Eine solche Suche ist ein Beispiel für eine unbe-
stimmte Iteration, da es viele Orte gibt, wo sich der Schlüssel befinden könnte,
aber Sie nicht im Voraus sagen können, wie viele Orte Sie durchsuchen müssen,
bevor Sie die Schlüssel finden. Wenn Sie das wüssten, könnten Sie ja auch gleich
zum vermeintlichen Fundort gehen! Stattdessen werden Sie wahrscheinlich im
Kopf eine Liste der möglichen Fundorte erstellen und diese nacheinander absu-
chen, bis Sie die Schlüssel haben. Nachdem Sie sie gefunden haben, werden Sie
sicherlich die Suche abbrechen wollen, anstatt die Liste weiter durchzugehen
(was sinnlos wäre).
Bei diesem Beispiel haben wir es mit einer unbestimmten Iteration zu tun: die
(Such-)Aktion wird beliebig oft ausgeführt, bis die Aufgabe gelöst ist. Szenarien
wie die Schlüsselsuche gibt es in der Programmierung häufig. Wir werden zwar
nicht immer nach etwas suchen, aber es gibt oft Situationen, in denen wir etwas
immer wieder tun wollen, bis die Wiederholung nicht länger benötigt wird.
Genau genommen treten sie so häufig auf, dass die meisten Programmierspra-
chen dafür zumindest ein, wenn nicht sogar mehrerer Schleifenkonstrukte zur
Verfügung stellen. Da das, was wir mit diesen Schleifenkonstrukten erreichen
wollen, normalerweise komplizierter ist, als von Anfang bis Ende über eine ganze
Sammlung zu iterieren, sind sie schwerer zu verstehen. Der Aufwand lohnt sich
jedoch, da Sie damit wesentlich mehr machen können. Wir legen den Schwer-
punkt hier auf Javas while-Schleife, weil sie sehr starke Ähnlichkeit mit Schleifen
in anderen Programmiersprachen aufweist.

152
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.10 Unbestimmte Iteration

4.10.1 Die while-Schleife


Eine while-Schleife besteht aus einem Schleifenkopf und einem Schleifenrumpf,
von denen der Rumpf mehrfach ausgeführt werden kann. Im Folgenden sehen
Sie die Struktur einer while-Schleife, bei der die boolesche Bedingung und der
Schleifenrumpf Pseudocode sind, der Rest hingegen Java-Syntax:
while(boolesche Bedingung) {
Schleifenrumpf
}
Wir erkennen, dass eine while-Schleife durch das Schlüsselwort while eingeleitet
wird, gefolgt von einer booleschen Bedingung. Diese Bedingung steuert letztend-
lich, wie oft eine Schleife ausgeführt wird. Die Bedingung wird ausgewertet,
wenn die Programmsteuerung das erste Mal in die Schleife eintritt; nach jeder
Ausführung des Schleifenrumpfes wird sie erneut ausgewertet. Dieser wiederhol-
ten Auswertung verdankt eine while-Schleife ihren unbestimmten Charakter.
Wenn die Auswertung der Bedingung true liefert, dann wird der Rumpf ausge-
führt; sobald die Auswertung false liefert, ist die Iteration beendet. In diesem Fall
wird der Schleifenrumpf übersprungen und die Ausführung mit dem fortgesetzt,
was nach dem Rumpf im Quelltext folgt. Beachten Sie, dass die Bedingung bereits
beim ersten Mal zu false ausgewertet werden kann, sodass der Rumpf überhaupt
nicht ausgeführt wird. Dies ist ein wichtiges Merkmal der while-Schleife: Der
Rumpf kann auch gar nicht ausgeführt werden, anstatt immer zumindest einmal.
Bevor wir ein richtiges Java-Beispiel betrachten, wollen wir unsere oben beschrie-
bene Schlüsseljagd in Pseudocode ausdrücken, um ein Gefühl dafür zu bekom-
men, wie eine while-Schleife funktioniert. Eine Möglichkeit, die Suche auszudrü-
cken, lautet:
while(die Schlüssel fehlen) {
suche am nächsten Ort
}
Wenn wir die Schleife das erste Mal erreichen, wird die Bedingung ausgewertet
und die Schlüssel fehlen. Das bedeutet, wir treten in den Rumpf der Schleife ein
und suchen am nächsten Ort auf unserer Liste. Nachdem wir das gemacht haben,
kehren wir zurück zu der Bedingung und werten sie erneut aus. Haben wir die
Schlüssel gefunden, ist die Schleife beendet und wir können den Schleifenrumpf
überspringen und das Haus verlassen. Sind die Schlüssel hingegen immer noch
unauffindbar, treten wir erneut in den Schleifenrumpf ein und suchen am nächsten
Ort. Dieser Prozess wiederholt sich so lange, bis die Schlüssel gefunden sind.1
Beachten Sie, dass wir genauso gut die Schleifenbedingung auch anders hätten
ausdrücken können:
while(nicht (die Schlüssel gefunden)) {
suche am nächsten Ort
}

1 An dieser Stelle wollen wir die Möglichkeit außer Acht lassen, dass die Schlüssel nicht gefun-
den werden. Allerdings wird die Berücksichtigung dieser Möglichkeit sehr wichtig, wenn wir
konkrete Java-Beispiele betrachten.

153
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Die Unterschiede sind nur geringfügig. In dem einen Pseudocode wird der Zustand
als etwas ausgedrückt, das zu ändern ist, im anderen als Ziel, das noch nicht
erreicht wurde. Nehmen Sie sich die Zeit und studieren Sie beide Versionen sorgfäl-
tig, um sicherzugehen, dass Sie alles verstanden haben. Beide sind gültig und spie-
geln Entscheidungen wider, die wir für unsere Ausdrücke treffen müssen, wenn
wir später konkrete Schleifen schreiben. In beiden Fällen führt das, was wir in den
Schleifenrumpf setzen, beim Finden der Schlüssel dazu, dass die Schleifenbedin-
gungen bei der nächsten Auswertung von true auf false umspringen.

Übung 4.29 Angenommen, wir drücken die erste Version der Schlüssel-
suche in Pseudocode wie folgt aus:
boolean fehlt = true;
while(fehlt) {
if(die Schlüssel sind am nächsten Ort) {
fehlt = false;
}
}
Versuchen Sie die zweite Version auszudrücken, indem Sie folgendes Gerüst
vervollständigen:
boolean gefunden = false;
while(...) {
if(die Schlüssel sind am nächsten Ort) {
...
}
}

4.10.2 Mithilfe einer Indexvariablen iterieren


Als erste while-Schleife in korrektem Java schreiben wir eine Version der Methode
alleDateienAusgeben aus Listing 4.3. Sie veranschaulicht zwar nicht den unbe-
stimmten Charakter von while-Schleifen, liefert aber einen nützlichen Vergleich mit
der gleichwertigen, schon bekannten for-each-Schleife. Die Version der while-
Schleife finden Sie in Listing 4.5. Das wichtigste Merkmal ist die int-Variable index,
die sowohl dazu verwendet wird, auf die Elemente der Liste zuzugreifen, als auch
um die Häufigkeit der Iterationen zu kontrollieren.

Listing 4.5
Benutzung einer
while-Schleife zur
Ausgabe der Tracks.

154
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.10 Unbestimmte Iteration

Es ist direkt ersichtlich, dass diese Version der while-Schleife programmiertech-


nisch etwas aufwendiger ist:
 Wir müssen eine Variable für den Listenindex deklarieren und wir müssen sie
selbst für das erste Listenelement mit 0 initialisieren. Die Variable muss außer-
halb der Schleife deklariert werden.
 Wir müssen herausfinden, wie wir die Schleifenbedingung so ausdrücken, dass
die Schleife sicher rechtzeitig beendet wird.
 Die Listenelemente werden nicht automatisch für uns aus der Sammlung geholt
und einer Variablen zugewiesen. Stattdessen müssen wir das selbst tun, mithilfe
der Methode get der Klasse ArrrayList. Die Variable dateiname ist lokal zum
Schleifenrumpf.
 Wir müssen daran denken, dass wir die Indexvariable (index) selbst erhöhen,
damit die Schleifenbedingung irgendwann false wird, wenn wir das Ende der
Liste erreicht haben.
Die letzte Anweisung im Rumpf der while-Schleife zeigt einen speziellen Opera-
tor zum Erhöhen einer numerischen Variablen um den Wert 1:
index++
Dies ist äquivalent zu
index = index + 1
Bisher war die for-each-Schleife klar besser für unsere Zwecke geeignet. Sie ist
leichter zu schreiben und sie ist sicherer. Sie ist deshalb sicherer, weil garantiert ist,
dass sie immer zu einem Ende kommt. In der Version mit der while-Schleife ist es
möglich, einen Fehler zu machen, der zu einer Endlosschleife führt. Wenn wir das
Erhöhen der Indexvariablen vergessen hätten (die letzte Zeile im Schleifenrumpf),
dann würde die Schleifenbedingung niemals false werden und die Schleife würde
endlos weiterlaufen. Das ist ein typischer Programmierfehler, der auch erfahre-
nen Programmierern immer einmal wieder unterläuft. Ein solches Programm wird
immer wieder von vorn ausgeführt. Wenn die Schleife in einer solchen Situation
keine Ausgabeanweisung enthält, dann scheint das Programm zu hängen: Es
scheint nichts zu tun und reagiert nicht auf Mausklicks oder Tastendrücke. Tatsäch-
lich macht das Programm aber sehr viel: Es führt wieder und wieder die Schleife
aus, aber wir können keine Auswirkungen sehen und das Programm scheint
gestorben zu sein. In BlueJ lässt sich dies oft daran erkennen, dass der rot-weiß
gestreifte Laufstatus-Anzeiger aktiv bleibt, während das Programm anscheinend
nichts macht.
Worin liegen also die Vorteile einer while-Schleife gegenüber einer for-each-
Schleife? Es gibt zwei: Erstens muss eine while-Schleife nicht mit einer Sammlung
verbunden sein – wir können Schleifen auf Basis jeder nur denkbaren Bedingung
formulieren, die sich als boolescher Ausdruck schreiben lässt. Zweitens müssen wir,
selbst wenn wir mit der Schleife eine Sammlung verarbeiten, nicht jedes Element
verarbeiten. Wir können die Schleife bei Bedarf früher beenden, indem wir eine
weitere Komponente in die Schleifenbedingung aufnehmen, die ausdrückt, warum
wir abbrechen wollen. Genau genommen drückt natürlich die Schleifenbedingung
aus, ob wir fortfahren wollen, und die Negation davon ist der Grund, warum die
Schleife stoppt.

155
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Der Vorteil einer expliziten Indexvariablen ist, dass wir deren Wert innerhalb und
außerhalb der Schleife verwenden können – eine Tatsache, auf die wir bei for-each-
Schleifen verzichten müssen. Wir können also den Index mit ausgeben, wenn wir
wollen. Das erleichtert uns die Auswahl eines Musikstücks anhand seiner Position
in der Ausgabe. Zum Beispiel:
int index = 0;
while(index < [Link]()) {
String dateiname = [Link](index);
// Vor dem Dateinamen den Track-Index ausgeben
[Link](index + ": " + dateiname);
index++;
}
Eine lokale Indexvariable zu haben, ist insbesondere für das Durchsuchen von Liste
hilfreich, da sie speichert, wo das Element sich befand – und auch dann noch ver-
fügbar ist, wenn die Schleife beendet ist. Doch dazu im nächsten Abschnitt mehr.

4.10.3 Eine Sammlung durchsuchen


Die Suche ist eine der wichtigsten Formen der Iteration, mit der Sie es zu tun bekom-
men werden. Deshalb ist es wichtig, die wesentlichen Elemente gut zu kennen. Die
Art von Schleifenstruktur tritt beim Programmieren in der Praxis immer wieder auf.
Das Hauptmerkmal einer Suche ist, dass sie mit einer unbestimmten Iteration ver-
bunden ist, was nicht verwunderlich ist, denn wenn wir genau wüssten, wo wir
suchen sollten, müssten wir gar nicht suchen! Stattdessen müssen wir eine Suche
einleiten, deren Erfolg von einer unbestimmten Anzahl von Iterationen abhängt.
Das bedeutet, dass eine for-each-Schleife sich nicht für das Suchen eignet, da sie
immer alle Iterationen durchläuft.2
In konkreten Suchsituationen müssen wir berücksichtigen, dass die Suche fehl-
schlagen kann: Es könnten uns die Suchorte ausgehen. Das heißt, wir müssen nor-
malerweise zwei Szenarien für das Beenden der Schleife ins Auge fassen, wenn wir
eine Suchschleife schreiben:
 Die Suche ist nach einer unbestimmten Anzahl von Iterationen erfolgreich.
 Die Suche schlägt fehl, nachdem alle Möglichkeiten ausprobiert wurden.
Beide Szenarien müssen beim Schreiben der Schleifenbedingung berücksichtigt
werden. Genauso wie die Schleifenbedingung zu true ausgewertet werden sollte,
wenn wir ein weiteres Mal über die Schleife iterieren wollen, sollte jedes der Been-
den-Kriterien für sich dazu führen, dass die Bedingung zu false ausgewertet wird,
um die Schleife zu stoppen. Die Tatsache, dass wir das Durchsuchen der ganzen
Liste beenden, wenn die Suche fehlschlägt, macht eine fehlgeschlagene Suche nicht
zu einem Beispiel der bestimmten Iteration. Das Hauptmerkmal einer bestimmten
Iteration ist, dass Sie die Anzahl der Iterationen zu Beginn der Schleife festlegen
können. Dies ist bei einer Suche nicht der Fall.

2 Auch wenn es leider recht häufig genutzte Möglichkeiten gibt, dieses Merkmal einer for-each-
Schleife zu unterlaufen, betrachten wir sie hier als schlechten Stil und benutzen sie in unseren
Beispielen nicht.

156
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.10 Unbestimmte Iteration

Wenn wir eine Indexvariable verwenden, um nacheinander die Elemente einer


Sammlung durchzugehen, ist eine fehlgeschlagene Suche leicht zu identifizieren:
Die Indexvariable ist über das letzte Element in der Liste hinaus inkrementiert
worden. Das ist genau die Situation, die in der Methode alleDateienAusgeben in
Listing 4.5 durch folgende Bedingung abgedeckt wird:
while(index < [Link]())
Die Bedingung drückt aus, dass wir fortfahren wollen, solange der Index im gülti-
gen Bereich der Sammlung liegt; sobald der Index über den Indexbereich hinaus
inkrementiert wurde, soll die Schleife aufhören. Diese Bedingung funktioniert
auch, wenn die Liste komplett leer ist. In diesem Fall wurde index mit 0 initialisiert
und der Aufruf der Methode size liefert ebenfalls 0 zurück. Da 0 nicht kleiner 0
ist, wird die Schleife nicht ausgeführt, was genau das ist, was wir wollen.
Darüber hinaus müssen wir zu der Bedingung einen zweiten Teil hinzufügen.
Seine Aufgabe ist es anzuzeigen, ob wir das gesuchte Element schon gefunden
haben, und in diesem Fall die Suche zu beenden. Wir haben in Abschnitt 4.10.1
und Übung 4.29 gesehen, dass wir dies über entsprechend gesetzte boolesche
Variablen oft positiv oder negativ ausdrücken können:
 Eine Variable amSuchen (oder fehlt), die am Anfang auf true gesetzt ist, könnte
die Suche so lange weiterführen, bis sie in der Schleife auf false gesetzt wird,
weil das Element gefunden wurde.
 Eine Variable gefunden, die am Anfang auf false gesetzt ist und in der Bedin-
gung als !gefunden verwendet wird, könnte die Suche so lange weiterführen,
bis sie auf true gesetzt wird, weil das Element gefunden wurde.
Im Folgenden finden Sie zwei entsprechende Quelltextabschnitte, die die kom-
plette Bedingung für beide Fälle ausdrücken:
int index = 0;
boolean amSuchen = true;
while(index < [Link]() && amSuchen)
oder
int index = 0;
boolean gefunden = false;
while(index < [Link]() && !gefunden)
Studieren Sie die Quelltextabschnitte sorgfältig. Beide bieten die gleiche Art der
Schleifensteuerung, werden aber leicht unterschiedlich ausgedrückt. Zur Erinne-
rung: Die Bedingung muss als Ganzes zu true ausgewertet werden, wenn wir
weitersuchen wollen, und zu false, wenn wir aus einem beliebigen Grund auf-
hören wollen. Wir haben den Und-Operator (&&) bereits in Kapitel 3 angespro-
chen. Er wird nur zu true ausgewertet, wenn beide seiner Operanden true sind.
Die volle Version einer Methode, die den ersten Dateinamen sucht, der einem
gegebenen Suchbegriff entspricht, ist in Listing 4.6 (Musiksammlung-v4) zu fin-
den. Die Methode liefert den Index des Elements als Ergebnis zurück. Beachten
Sie, dass wir eine Möglichkeit finden müssen, den Aufrufer der Methode darüber
zu informieren, falls die Suche fehlgeschlagen ist. Wir haben uns in diesem Fall
dafür entschieden, einen Wert zurückzugeben, der eindeutig keine gültige Posi-

157
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

tion in der Sammlung ist, da er negativ ist. Diese Vorgehensweise ist typisch für
Suchsituationen: Um einen Fehlschlag anzuzeigen, geben wir einen Wert zurück,
der außerhalb des Gültigkeitsbereichs liegt.

Listing 4.6
Das erste überein-
stimmende Element
in einer Liste suchen.

Es mag verlockend sein zu versuchen, nur eine Bedingung in die Schleife aufzu-
nehmen, auch wenn es zwei verschieden Gründe gibt, die Suche zu beenden.
Eine Möglichkeit hierfür wäre, dafür zu sorgen, dass der Wert des Index zu groß
wird, wenn wir das Gesuchte gefunden haben. Von dieser Praxis wollen wir aber
lieber abraten, da der Code dann die Beenden-Kriterien der Schleife nicht direkt
widerspiegelt und Klarheit immer unser oberstes Ziel ist.

4.10.4 Einige Beispiele ohne Sammlung


Schleifen werden nicht nur bei Sammlungen eingesetzt. Es gibt viele Situationen,
in denen wir Anweisungsblöcke wiederholen wollen, die nichts mit einer Samm-
lung zu tun haben. Im Folgenden sehen Sie ein Beispiel, der alle geraden Zahlen
von 0 bis 30 ausgibt:
int index = 0;
while(index <= 30) {
[Link](index);
index = index + 2;
}
Dies ist ein Beispiel für die Verwendung einer while-Schleife für eine bestimmte Ite-
ration, da von Anfang an klar ist, wie viele Zahlen ausgegeben werden. Wir kön-
nen in diesem Fall keine for-each-Schleife verwenden, da diese nur beim Iterieren

158
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.11 Verbesserung der Struktur – die Klasse Track

über Sammlungen zum Einsatz kommen. Später werden wir noch einer dritten,
verwandten Schleife (der for-Schleife) begegnen, die sich noch besser für dieses
bestimmte Beispiel eignet. Um zu überprüfen, ob Sie while-Schleifen verstanden
haben, die nicht bei Sammlungen eingesetzt werden, sollten Sie die folgenden
Übungen bearbeiten.

Übung 4.30 Schreiben Sie eine while-Schleife (beispielsweise in einer Methode


vielfacheVonFuenf), die alle Vielfachen von 5 zwischen 10 und 95 ausgibt.
Übung 4.31 Schreiben Sie eine while-Schleife, die die Werte von 1 bis 10
addiert und die Summe nach Beenden der Schleife ausgibt.
Übung 4.32 Schreiben Sie eine Methode summieren mit einer while-Schleife,
die alle Zahlen zwischen zwei Zahlen a und b aufsummiert. Die Werte von a
und b können der Methode als Parameter übergeben werden.
Übung 4.33 Zusatzaufgabe. Schreiben Sie eine Methode istPrim(int n), die
true liefert, wenn der Parameter n eine Primzahl ist, und false, wenn sie
keine ist. In der Implementierung der Methode können Sie eine while-
Schleife schreiben, die n durch alle Zahlen zwischen 2 und (n–1) teilt und
überprüft, ob die Division eine ganze Zahl liefert. Sie können für diesen Test
den Modulo-Operator (%) verwenden, indem Sie mit ihm überprüfen, ob die
int-Division zu einem Rest von null führt (siehe die Beschreibung des
Modulo-Operators in Abschnitt 3.8.3).
Übung 4.34 In der Methode findeErste fragt die Bedingung in der Schleife
die Sammlung dateien wiederholt, wie viele Dateien sie speichert. Ändert sich
der von size gelieferte Wert von einer Prüfung zur nächsten? Lautet Ihre Ant-
wort nein, dann schreiben Sie die Methode so um, dass die Anzahl der
Dateien nur einmal festgelegt und in einer lokalen Variablen gespeichert wird,
bevor die Schleife ausgeführt wird. Verwenden Sie dann diese Variable und
nicht den Aufruf von size in der Schleifenbedingung. Prüfen Sie, dass diese
Version die gleichen Ergebnisse liefert. Wenn Sie Schwierigkeiten haben,
diese Übung zu beenden, setzen Sie den Debugger ein, um zu sehen, wo Sie
Fehler gemacht haben.

4.11 Verbesserung der Struktur –


die Klasse Track
Wir haben bereits mehrmals erlebt, dass die Verwendung von Zeichenketten zur
Speicherung der ganzen Details der Musikstücke nicht gänzlich zufriedenstellend ist
und unseren Musikplayer relativ billig erscheinen lässt. Jeder handelsübliche Player
würde erlauben, Musikstücke nach Interpret, Titel, Album, Genre usw. zu suchen,
und außerdem weitere Funktionen bieten, wie z.B. die Anzeige von Dauer und
Nummer der Musikstücke. Eine der Stärken der Objektorientierung ist, dass sie uns
erlaubt, Klassen zu entwerfen, die die inhärente Struktur und die Verhaltensweisen
der realen Entitäten modellieren, die wir versuchen darzustellen. Dies erreichen wir,

159
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

indem wir Klassen schreiben, deren Datenfelder und Methoden denen der Attribute
entsprechen. Inzwischen wissen wir genug über das Schreiben von einfachen
Klassen mit Datenfeldern, Konstruktoren sowie sondierenden und verändernden
Methoden, dass wir leicht eine Klasse Track für Musikstücke entwerfen können, die
Datenfelder aufweist, um zum Beispiel getrennt Informationen über den Interpreten
und den Titel zu speichern. Auf diese Weise können wir mit den Objekten in der
Musiksammlung interagieren, wie es eher unserer natürlichen Vorgehensweise ent-
spricht.
Höchste Zeit also, mit dem Speichern der Trackinformationen als Zeichenketten
aufzuhören, da eine getrennte Klasse Track ein viel geeigneterer Weg ist, die wich-
tigsten Datenelemente (Tracks), die wir in dem Programm verwenden, darzustel-
len. Wir sind jedoch nicht allzu ehrgeizig. Eine der größeren Hürden, die wir über-
winden müssen, ist, wie wir an die verschiedenen Informationen gelangen, die wir
in jedem Track-Objekt speichern wollen. Eine Möglichkeit wäre, den Benutzer auf-
zufordern, beim Hinzufügen jeder Musikdatei zu der Sammlung Interpret, Titel,
Genre usw. anzugeben. Das wäre jedoch ziemlich langsam und arbeitsaufwendig.
Deshalb haben wir für dieses Projekt einen Satz von Musikdateien gewählt, deren
Dateinamen bereits Name des Interpreten und des Titels enthalten. Zusätzlich
haben wir eine Hilfsklasse (namens TrackReader) für unsere Anwendung geschrie-
ben, die nach allen Musikdateien in einem bestimmten Ordner Ausschau hält und
deren Dateinamen heranzieht, um Teile der entsprechenden Track-Objekte zu fül-
len. Wir kümmern uns im Moment noch nicht um die Einzelheiten der Vorgehens-
weise. (Weiter hinten im Buch besprechen wir die Techniken und Bibliotheksklas-
sen, die in der Klasse TrackReader verwendet werden.) Eine Implementierung dieses
Entwurfs finden Sie in Musiksammlung-v5.
Auf folgende Punkte sollten Sie in dieser Version achten:
 Die wichtigste neue Änderung erfolgt in der Klasse MusikSammlung. Es werden in
der ArrayList keine String-Objekte mehr, sondern Track-Objekte gespeichert
(Listing 4.7). Dies hat Einfluss auf die meisten der bisher entwickelten Metho-
den.
 Bei der Ausgabe der Trackinformationen in alleTracksAusgeben fordern wir das
Track-Objekt auf, einen String zu liefern, der die Details enthält. Das zeigt,
dass wir die Klasse Track so ausgelegt haben, dass sie dafür verantwortlich ist,
die auszugebenden Details wie Interpret und Titel zur Verfügung zu stellen. Es
ist ein Beispiel für den sogenannten Entwurf nach Zuständigkeiten, den wir in
einem Kapitel weiter hinten ausführlicher behandeln.
 In der Methode spieleTrack müssen wir dann den Dateinamen aus dem aus-
gewählten Track-Objekt abfragen, bevor wir ihn an den Player übergeben.
 In der Musikbibliothek haben wir Code hinzugefügt, der automatisch Datei-
namen aus dem Ordner audio ausliest, sowie einige Ausgabeanweisungen,
um die Informationen anzuzeigen.

160
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.11 Verbesserung der Struktur – die Klasse Track

Listing 4.7
Die Verwendung von
Track in der Klasse
MusikSammlung.

161
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Auch wenn ersichtlich ist, dass die Einführung einer Klasse Track einige der alten
Methoden etwas verkompliziert hat, führt die Arbeit mit speziellen Track-Objekten
letztendlich zu einer viel besseren Struktur des ganzen Programms und erlaubt uns,
die Klasse Track beliebig auszubauen, sodass sie mehr als nur Namen von Audio-
dateien repräsentiert. Die Information ist nicht nur besser strukturiert: die Verwen-
dung einer Track-Klasse ermöglicht es uns, umfassendere Informationen zu spei-
chern, wenn wir es möchten, wie zum Beispiel das Bild vom Cover des Albums.
Dank der besseren Strukturierung der Trackinformationen können wir jetzt viel
besser nach Tracks suchen, die bestimmten Kriterien entsprechen. Wenn wir zum
Beispiel alle Tracks suchen, die das Wort „Liebe“ im Titel haben, können wir dazu
folgenden Code verwenden, ohne Gefahr zu laufen, unerwünschte Treffer auf-
grund Übereinstimmungen mit den Interpretennamen zu erzielen:
/**
* Liste alle Tracks, die einen gegebenen Suchbegriff im Titel enthalten.
* @param suchbegriff der zu suchende Begriff.
*/
public void sucheInTitel(String suchbegriff)
{
for(Track track : tracks) {
String titel = [Link]();
if([Link](suchbegriff)) {
[Link]([Link]());
}
}
}

Übung 4.35 Fügen Sie zu der Klasse Track ein Datenfeld abspielFrequenz
hinzu. Ergänzen Sie Methoden, die die Zählung auf null setzen und um eins
inkrementieren.
Übung 4.36 Lassen Sie die Klasse MusikSammlung die Abspielfrequenz eines
Tracks bei jedem Abspielen inkrementieren.
Übung 4.37 Fügen Sie ein weiteres Datenfeld Ihrer Wahl zu der Klasse Track
hinzu sowie sondierende und verändernde Methoden, um es abzufragen
und zu manipulieren. Suchen Sie nach einer Möglichkeit, diese Information
in Ihrer Version des Projekts zu verwenden. Bauen Sie die Information bei-
spielsweise in die Ausgabe ein oder richten Sie in der Klasse MusikSammlung
eine Methode zum Setzen der Information ein.
Übung 4.38 Wenn Sie zwei Tracks abspielen, ohne den ersten zu stoppen,
werden beide gleichzeitig abgespielt. Das ist nicht besonders nützlich. Ändern
Sie Ihr Programm so, dass das Abspielen eines Tracks automatisch angehalten
wird, wenn ein anderer Track gestartet wurde.

162
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.12 Der Typ Iterator

4.12 Der Typ Iterator


Die Iteration ist ein wichtiges Werkzeug in fast jedem Programmierprojekt, sodass Konzept
es nicht Wunder nimmt, dass Programmiersprachen normalerweise eine ganze
Ein Iterator ist
Reihe von Funktionen bereitstellen, mit denen sie die Iteration unterstützen. Jede
ein Objekt, mit
dieser Funktionen verfügt über jeweils eigene individuelle Merkmale für verschie- dessen Hilfe über
dene Situationen. alle Elemente einer
Sammlung iteriert
Wir wollen hier für die Iteration über eine Sammlung eine dritte Variante vorstellen,
werden kann
die irgendwo zwischen der while-Schleife und der for-each-Schleife angesiedelt ist. (d.h. alle Elemente
Sie besteht aus einer while-Schleife für die Iteration und einem Iterator-Objekt durchlaufen wer-
anstelle einer int-Indexvariablen, um die Position in der Liste zu speichern. Wir müs- den können).
sen an dieser Stelle große Sorgfalt bei der Benennung walten lassen, da Iterator
(man beachte das großgeschriebene I) eine Java-Klasse ist, wir es aber auch mit
einer Methode namens iterator (kleingeschriebenes i) zu tun haben. Achten Sie
also sorgfältig auf diese Unterschiede, wenn Sie diesen Abschnitt lesen und später
eigenen Code schreiben.
Die Prüfung eines jeden Elements in einer Sammlung kommt so häufig vor, dass
wir bereits eine spezielle Kontrollstruktur – die for-each-Schleife – kennengelernt
haben, die genau für diesen Zweck entwickelt wurde. Zusätzlich bieten Javas
Sammlungsklassen einen speziellen, häufig verwendeten Typ, der ebenfalls Itera-
tion unterstützt; ArrayList ist hierfür ein typischer Vertreter.
Die Methode iterator der Klasse ArrayList liefert ein Iterator-Objekt. Iterator ist
ebenfalls im Paket [Link] deklariert, entsprechend müssen wir eine weitere
import-Anweisung in den Quelltext einfügen, um den Typ benutzen zu können:
import [Link];
import [Link];
Ein Iterator bietet nur vier Methoden, von denen zwei verwendet werden, um
über eine Sammlung zu iterieren: hasNext und next. Keine von ihnen nimmt einen
Parameter entgegen, aber beide haben einen Ergebnistyp, der nicht void ist, sodass
sie in Ausdrücken verwendet werden. Die übliche Art, einen Iterator zu benutzen,
kann mit folgendem Pseudocode veranschaulicht werden:
Iterator<Elementtyp> it = [Link]();
while ([Link]()) { solange es noch ein weiteres Element gibt
mit [Link]() das nächste Element abholen
etwas mit diesem Element tun
}
In diesem Fragment benutzen wir zuerst die Methode iterator der Klasse Array-
List, um ein Iterator-Objekt zu bekommen. Beachten Sie, dass Iterator ebenfalls
ein generischer Typ ist, sodass wir es mit dem Typ der Elemente in der Sammlung
parametrisieren, über die wir iterieren. Anschließend benutzen wir diesen Iterator,
um zu prüfen, ob es noch weitere Elemente gibt ([Link]()) und um dieses Ele-
ment dann auch zu bekommen ([Link]()). Ein wichtiger Punkt an dieser Stelle ist,
dass wir jeweils den Iterator nach dem nächsten Element fragen, nicht das Samm-
lungsobjekt. Genau genommen versuchen wir überhaupt nicht im Rumpf der
Schleife direkt auf die Sammlung Bezug zu nehmen. Alle Interaktionen mit der
Sammlung erfolgen über den Iterator.

163
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Mithilfe eines Iterator können wir eine Methode zur Ausgabe aller Tracks wie in
Listing 4.8 schreiben. Der Iterator beginnt am Anfang der Sammlung und arbeitet
sich, Objekt für Objekt, jedes Mal weiter, wenn wir die Methode next aufrufen.

Listing 4.8
Benutzung eines
Iterators zur Ausgabe
aller Tracks.

Nehmen Sie sich etwas Zeit, um diese Version der Version gegenüberzustellen,
die in Listing 4.7 eine for-each-Schleife verwendet, sowie sie mit den beiden Ver-
sionen von alleDateienAusgeben in Listing 4.3 und Listing 4.4 zu vergleichen.
Bemerkenswert an der letzten Version ist, dass wir eine explizite while-Schleife
verwenden, uns aber nicht mehr um die Indexvariable zu kümmern brauchen.
Das liegt daran, dass ein Iterator selbst die Information darüber verwaltet, wie
weit er bereits eine Sammlung durchlaufen hat. Auf diese Weise ist dem Iterator
bekannt, ob es noch weitere Elemente gibt (hasNext) und welches geliefert wer-
den muss (next), wenn es noch ein weiteres gibt.
Um zu verstehen, wie ein Iterator funktioniert, müssen Sie sich unter anderem
bewusst sein, dass der Aufruf von next den Iterator veranlasst, das nächste Element
in der Sammlung zu liefern und dann die Position hinter diesem Element einzu-
nehmen. Deshalb liefern mehrere aufeinanderfolgende Aufrufe von next auf einen
Iterator immer unterschiedliche Elemente. Nachdem next aufgerufen worden ist,
können Sie nicht mehr zurück zum vorherigen Element. Zum Schluss erreicht der
Iterator das Ende der Sammlung und liefert dann durch den Aufruf von hasNext
false zurück. Danach würde es zu einem Fehler kommen, wenn versucht würde,
next auf dieses bestimmte Iterator-Objekt aufzurufen – das Iterator-Objekt wurde
sozusagen „aufgebraucht“ und hat keinen weiteren Nutzen.

Abbildung 4.5 :ArrayList<String>


Ein Iterator nach einer
Iteration, der auf das
nächste zu verarbei-
tende Element zeigt.

:String :String :String

“MorningBlues.mp3” “DontGo.mp3” “MatchBoxBlues.mp3”

it: Iterator

164
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.12 Der Typ Iterator

Auf den ersten Blick scheint Iterator gegenüber den vorherigen Möglichkeiten,
über eine Sammlung zu iterieren, keine Vorteile zu bieten. Aber die folgenden
zwei Abschnitte sollen Ihnen zeigen, warum es so wichtig ist zu wissen, wie man
damit arbeitet.

4.12.1 Zugriff mit Index oder über Iteratoren


Wir haben gesehen, dass es mindestens drei Möglichkeiten gibt, um über eine
ArrayList zu iterieren: Wir können eine for-each-Schleife verwenden (wie in
Abschnitt 4.9.1), die Methode get mit einem Index einsetzen (wie in Abschnitt
4.10.2) oder wir können ein Iterator-Objekt verwenden (dieser Abschnitt).
Nach unserem bisherigen Verständnis scheinen alle Ansätze gleich gut zu sein. Der
erste Ansatz war möglicherweise etwas leichter zu verstehen, aber am wenigsten
flexibel.
Die Benutzung einer for-each-Schleife ist die Standardtechnik, wenn alle Elemente
einer Sammlung verarbeitet werden sollen (d.h. bestimmte Iteration), denn sie ist
für diesen Fall am klarsten. Die beiden letzten Versionen haben den Vorteil, dass
die Wiederholungen leichter in der Mitte der Verarbeitung gestoppt werden kön-
nen (unbestimmte Iteration), sodass sie vorzuziehen sind, wenn nur ein Teil der
Sammlung verarbeitet werden muss.
Bei einer ArrayList sind die letzten beiden Möglichkeiten mit den while-Schleifen
tatsächlich gleich gut. Dies ist jedoch nicht immer der Fall. Java bietet neben der
ArrayList noch etliche weitere Sammlungsklassen an. Wir werden einige davon
in den nächsten Kapiteln noch zu sehen bekommen. Für einige dieser Sammlun-
gen ist es entweder unmöglich oder sehr ineffizient, auf einzelne Elemente über
einen Index zuzugreifen. Unsere erste Lösung mit einer while-Schleife ist deshalb
angemessen für eine ArrayList, bei einigen anderen Sammlungstypen funktio-
niert sie aber möglicherweise nicht.
Die letzte Lösung mit einem Iterator hingegen wird von allen Sammlungstypen der
Java-Klassenbibliothek unterstützt; sie ist deshalb ein wichtiges Programmiermus-
ter, das wir in späteren Projekten noch häufiger verwenden werden.

4.12.2 Elemente entfernen


Bei unserer Entscheidung, welche Schleife am besten geeignet ist, sollten wir auch
berücksichtigen, ob wir bei den Schleifendurchläufen Elemente aus der Sammlung
entfernen müssen. Stellen wir uns zum Beispiel vor, wir wollten alle Tracks aus unse-
rer Sammlung entfernen, die von einem Interpreten stammen, der uns nicht mehr
interessiert.
Den zugehörigen Pseudocode haben wir schnell formuliert:
für jeden track in der sammlung {
wenn [Link]() nicht mehr gewünschte Interpret ist:
entferne ihn
}
Es stellt sich allerdings heraus, dass dies mit einer for-each-Schleife nicht möglich
ist. Wenn wir während eines Durchlaufs versuchen, die Sammlung mit einer ihrer

165
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

remove-Methoden zu verändern, erhalten wir vom System eine Fehlermeldung


über eine unzulässige Veränderung (eine ConcurrentModificationException). Dies
liegt daran, dass Änderungen an der Sammlung mitten in einem Durchlauf zu
recht schwierigen Situationen führen können. Was ist, wenn das Element ent-
fernt wird, an dem wir gerade arbeiten? Wie sollen wir danach das nächste Ele-
ment finden? Auf diese Fragen gibt es keine befriedigenden allgemeingültigen
Antworten. Aus diesem Grunde ist es nicht erlaubt, die remove-Methode einer
Sammlung während eines Durchlaufs mit einer for-each-Schleife zu verwenden.
Eine gute Lösung, um ein Element während eines Durchlaufs zu entfernen, ist,
einen Iterator zu verwenden, dessen dritte Methode (neben hasNext und next)
remove lautet. Diese Methode nimmt keine Parameter entgegen und hat einen
Ergebnistyp void. Der Aufruf von remove entfernt das Element, das vom letzten Auf-
ruf von next geliefert wurde. Der zugehörige Code könnte wie folgt aussehen:
Iterator<String> it = [Link]();
while ([Link]()) {
Track t = [Link]();
String interpret = [Link]();
if ([Link](zuEntfernenderInterpret)) {
[Link]();
}
}
Erneut sei darauf hingewiesen, dass wir die Sammlungsvariable tracks nicht im
Schleifenrumpf verwenden. Auch wenn ArrayList als auch Iterator über remove-
Methoden verfügen, müssen wir die remove-Methode von Iterator verwenden und
nicht die von ArrayList.
Die remove-Methode von Iterator ist weniger flexibel – wir können keine beliebi-
gen Elemente entfernen. Wir können nur das letzte Element entfernen, das wir
von der Iterator-Methode next erhalten haben. Die Methode remove hat jedoch
den Vorteil, dass man sie während eines Schleifendurchlaufs verwenden darf. Da
der Iterator über das Entfernen des Elements informiert ist (und das Entfernen für
uns erledigt), kann er dafür sorgen, dass der Durchlauf auch nach dem Entfernen
in Übereinstimmung mit dem aktuellen Zustand der Sammlung erfolgt.
Mit einer for-each-Schleife sind solche Löschoperationen nicht möglich, da uns darin
kein Iterator zur Verfügung steht, über den wir löschen könnten. Wir müssen also
für solche Fälle auf die Kombination von while-Schleife und Iterator zurückgreifen.
Technisch gesehen können wir Elemente entfernen, indem wir die get-Methode
der Sammlung mit einem Schleifenindex aufrufen. In der Praxis ist hiervon aller-
dings abzuraten, da sich die Indizes der Elemente ändern, wenn man Elemente
hinzufügt oder entfernt. Schnell hat man Schleifen programmiert, die nach Mani-
pulationen an der zugrunde liegenden Sammlung mit falschen Indizes arbeiten.
Durch die Verwendung eines Iterator lassen sich solche Fehler vermeiden.

Übung 4.39 Implementieren Sie in Ihrem Musiksammlung-Beispiel eine


Methode, die als Parameter eine Zeichenkette entgegennimmt und dann
alle Tracks entfernt, die diese Zeichenkette enthalten.

166
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.13 Zusammenfassung des Musiksammlung-Beispiels

4.13 Zusammenfassung des


Musiksammlung-Beispiels
Am Musiksammlung-Beispiel haben wir gesehen, wie wir ein ArrayList-Objekt mit-
hilfe einer Bibliotheksklasse erzeugen und zum Speichern einer beliebigen Anzahl
von Objekten in einer Sammlung verwenden können. Wir müssen nicht von vorn-
herein festlegen, wie viele Objekte wir speichern wollen, und das ArrayList-Objekt
hält automatisch die Information darüber, wie viele Elemente es bereits enthält.
Wir haben diskutiert, wie man mithilfe einer Schleife über alle Elemente einer
Sammlung iterieren kann. Java bietet mehrere Schleifenkonstrukte – wir haben
hier die for-each-Schleife und die while-Schleife verwendet. Wir verwenden in
der Regel eine for-each-Schleife, wenn wir die ganze Sammlung verarbeiten wol-
len, und die while-Schleife, wenn wir nicht im Voraus sagen können, wie viele
Durchläufe wir benötigen, oder wenn wir Elemente während der Durchläufe ent-
fernen müssen.
Bei einer ArrayList können wir auf die Elemente entweder mit einem Index zugrei-
fen oder wir können mit einem Iterator-Objekt alle Elemente durchlaufen. Es
lohnt sich, die verschiedenen Umstände zu prüfen, unter denen die verschiedenen
Schleifentypen (for-each und while) eingesetzt werden und warum ein Iterator
einem ganzzahligen Index vorzuziehen ist, weil Sie Entscheidungen dieser Art
immer wieder fällen müssen. Hier die richtige Wahl zu treffen, kann es viel einfa-
cher machen, ein bestimmtes Problem zu lösen.

Übung 4.40 Benutzen Sie das Projekt Verein für diese und die folgenden Übun-
gen. Ihre Aufgabe ist es, die Klasse Verein, von der bereits ein Grobgerüst vor-
handen ist, zu vervollständigen. Die Klasse Verein soll Mitgliedschaft-Objekte in
einer Sammlung mit flexibler Größe speichern.
Definieren Sie in Verein ein Datenfeld für eine ArrayList. Definieren Sie die not-
wendige import-Anweisung für dieses Datenfeld und überlegen Sie sich genau
den Elementtyp der Liste. Erzeugen Sie im Konstruktor das Sammlungsobjekt
und weisen Sie es dem Datenfeld zu. Stellen Sie sicher, dass alle Dateien des Pro-
jektes übersetzt werden können, bevor Sie mit der nächsten Übung beginnen.
Übung 4.41 Vervollständigen Sie die Methode anzahlMitglieder so, dass sie
die aktuelle Größe der Sammlung liefert. Bis Sie eine Methode zum Einfügen
von Objekten in die Sammlung haben, wird diese natürlich immer null liefern,
aber die Methode ist für spätere Tests vorbereitet.
Übung 4.42 Die Mitgliedschaft in einem Verein wird durch eine Instanz der
Klasse Mitgliedschaft repräsentiert. Eine vollständige Version der Klasse Mit-
gliedschaft ist im Projekt Verein bereits vorgegeben und sollte keine Verände-
rung mehr benötigen. Ein Mitgliedschaft-Objekt enthält Informationen über
den Namen einer Person sowie den Monat und das Jahr des Zeitpunktes, an
dem die Person dem Verein beigetreten ist. Alle Daten zu einer Mitgliedschaft
werden angegeben, wenn ein Mitgliedschaft-Objekt erzeugt wird. Ein neues
Mitgliedschaft-Objekt wird in die Sammlung eines Verein-Objektes über die
Methode eintreten eingefügt, die die folgende Signatur hat:

167
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

/**
* Füge ein neues Mitglied in die Mitgliederliste ein.
* @param mitglied Infos über das einzufügende Mitglied.
*/
public void beitreten (Mitgliedschaft mitglied)
Vervollständigen Sie die Methode beitreten.
Wenn Sie ein neues Mitgliedschaft-Objekt in ein Verein-Objekt von der Objekt-
leiste aus eintragen wollen, dann gibt es dazu zwei Möglichkeiten. Entweder
erzeugen Sie ein neues Mitgliedschaft-Objekt auf der Objektleiste, rufen die
Methode beitreten auf dem Verein-Objekt auf und klicken auf das Mitglied-
schaft-Objekt, um es als Parameter anzugeben; oder Sie rufen die beitreten-
Methode am Verein-Objekt auf und tippen Folgendes in das Parameterfeld des
Methodenaufruf-Dialogs:
new Mitgliedschaft ("Mitgliedsname ...", Monat, Jahr)
Prüfen Sie jedes Mal, wenn Sie ein Objekt eintragen, mit der Methode anzahl-
Mitglieder, ob die Methode beitreten wirklich in die Sammlung einfügt und
ob die Methode anzahlMitglieder selbst das richtige Ergebnis liefert.
Wir werden dieses Projekt noch weiter erkunden, indem wir später weitere
Übungen dazu durchführen.
Übung 4.43 Zusatzaufgabe. Diese und die folgenden Übungen sind eine Her-
ausforderung, da sie Kenntnisse in Themen voraussetzen, die wir noch nicht
explizit behandelt haben. Trotzdem sollten Sie sich daran wagen, wenn Sie das
bisher besprochene Material einigermaßen verstanden haben. Wir werden
etwas hinzufügen, über das die meisten Musikplayer verfügen: eine „Shuffle“-
beziehungsweise „Zufallsabspiel“-Funktion.
Das Paket [Link] enthält die Klasse Random, deren Methode nextInt eine
positive ganze Zahl innerhalb eines begrenzten Bereichs erzeugt. Schreiben Sie
eine Methode in der Klasse MusikSammlung, die einen zufälligen Track aus ihrer
Liste auswählt und abspielt.
Hinweis: Sie müssen Random importieren und ein Random-Objekt erzeugen –
und zwar entweder direkt in der neuen Methode oder im Konstruktor und
anschließend in einem Datenfeld speichern. Lesen Sie die API-Dokumentation
für die Random-Klasse, um festzustellen, welche der nextInt-Methoden Sie
wählen müssen. Alternativ können Sie auch das Kapitel 6 lesen, das sich mit
der Klasse Random befasst.
Übung 4.44 Zusatzaufgabe. Überlegen Sie, wie Sie mehrere Tracks in einer
zufälligen Reihenfolge abspielen könnten. Wollen Sie sicherstellen, dass alle
Tracks gleich oft gespielt werden oder Lieblingstracks bevorzugen? Inwie-
fern könnte ein Datenfeld zur Abspielfrequenz in der Klasse Track für diese
Aufgabe nützlich sein? Diskutieren Sie die verschiedenen Optionen.
Übung 4.45 Zusatzaufgabe. Schreiben Sie eine Methode, um jeden Track der
Liste genau einmal in einer Zufallsreihenfolge abzuspielen.

168
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem

Hinweis: Eine Möglichkeit wäre, die Reihenfolge der Tracks in der Liste – oder
besser in einer Kopie der Liste – neu zu mischen und dann die Liste von
Anfang bis Ende abzuspielen. Eine andere Möglichkeit wäre, eine Kopie der
Liste zu erstellen und dann wiederholt einen zufälligen Track aus der Liste aus-
zuwählen, abzuspielen und von der Liste zu entfernen, bis die Liste leer ist.
Versuchen Sie, einen dieser Ansätze zu implementieren. Wenn Sie den ersten
Ansatz verfolgen, stellt sich die Frage, wie einfach es ist, die Liste so zu
mischen, dass sie tatsächlich in einer neuen Zufallsreihenfolge vorliegt. Gibt es
irgendwelche Bibliotheksmethoden, die Ihnen dabei helfen könnten?

4.14 Ein weiteres Beispiel:


ein Auktionssystem
In diesem Abschnitt werden wir die Konzepte, die wir in diesem Kapitel neu ken-
nengelernt haben, aus einem anderen Zusammenhang heraus erneut betrachten.
Das Projekt Auktion modelliert einen Teil der Funktionalität eines Online-Auktions-
systems. Die Grundidee ist, dass eine Auktion aus einer Reihe von Gegenständen
besteht, die zum Verkauf angeboten werden. Diese Gegenstände werden Posten
genannt und jedem Posten wird eine eindeutige Nummer vom Programm zuge-
wiesen. Eine Person, die einen Posten ersteigern möchte, muss Geld dafür bieten.
Unsere Auktionen sind etwas anders als sonst üblich, weil alle Posten nur für
kurze Zeit angeboten werden.3 Am Ende dieser Frist wird die Auktion geschlossen.
Beim Ende einer Auktion bekommt die Person, die das höchste Gebot für einen
Posten abgegeben hat, den Zuschlag. Alle Posten, für die nicht geboten wurde,
bleiben unverkauft. Unverkaufte Posten können dann beispielsweise in der nächs-
ten Auktion wieder angeboten werden.

Abbildung 4.6
Die Klassenstruktur
des Projektes Auktion.

3 Zur Vereinfachung haben wir den Aspekt der Zeitbegrenzung in den hier betrachteten Klassen
nicht implementiert.

169
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Das Projekt Auktion enthält folgende Klassen: Auktion, Gebot, Posten und Person.
Ein genauerer Blick auf das Klassendiagramm für dieses Projekt (Abbildung 4.6)
zeigt, dass die Beziehungen zwischen den verschiedenen Klassen ein wenig kom-
plizierter sind als in vorherigen Projekten. Dies hat Auswirkungen auf die Art und
Weise, wie auf Informationen während der Auktion zugegriffen wird. Das Dia-
gramm zeigt beispielsweise, dass die Auktion-Objekte über alle anderen Objekt-
typen (Gebot, Posten und Person) informiert sind. Posten-Objekte wissen von Gebot-
Objekten, und Gebot-Objekte wissen von Person-Objekten. Was uns das Diagramm
allerdings nicht verrät, ist, wie genau auf die in einem Gebot-Objekt gespeicherten
Informationen beispielsweise von einem Auktion-Objekt aus zugegriffen wird.
Dafür müssen wir uns mit dem Code der jeweiligen Klasse befassen.

4.14.1 Erste Schritte


Zu diesem Zeitpunkt würde es sich anbieten, das Projekt Auktion zu öffnen und
den Quelltext zu untersuchen, bevor Sie weiterlesen. Neben Bekanntem wie die
Verwendung von ArrayList und Schleifen werden Ihnen einige Dinge begegnen,
die Sie am Anfang noch nicht ganz verstehen werden. Doch dies ist zu erwarten,
da wir uns neuen Ideen und Herangehensweisen zuwenden.
Ein Auktion-Objekt ist der Ausgangspunkt für unser Projekt. Personen, die etwas
verkaufen wollen, geben es mithilfe der Methode postenAnmelden in die Auktion,
doch sie liefern nur eine String-Beschreibung. Das Auktion-Objekt erzeugt darauf-
hin für jeden eingegebenen Posten ein Posten-Objekt. Dies modelliert die Abläufe
der realen Welt: Es ist beispielsweise die Auktions-Site und nicht der Verkäufer, die
den Posten Postennummern oder Kennungen zuweist. Demzufolge ist ein Posten-
Objekt die Repräsentation eines zum Verkauf stehenden Postens der Auktions-Site.
Um für Posten mitbieten zu können, müssen sich Interessenten beim Auktionshaus
registrieren. In unserem Programm wird ein potenzieller Bieter durch ein Person-
Objekt repräsentiert. Diese Objekte müssen unabhängig auf der BlueJ-Objektleiste
erzeugt werden. In unserem Projekt enthält ein Person-Objekt lediglich den Namen
der Person. Wenn jemand für einen Posten bieten möchte, ruft er die gibGebotAb-
Methode des Auktion-Objekts auf, unter Angabe der Postennummer des Person-
Objekts des Bieters und der Höhe des Gebots. Beachten Sie, dass die Postennum-
mer und nicht das Posten-Objekt übergeben wird. Die Posten-Objekte bleiben
Interna des Auktion-Objekts und werden von außen über ihre Postennummer refe-
renziert.
Genauso wie das Auktion-Objekt ein Posten-Objekt erzeugt, wandelt es ein finanzi-
elles Gebot in ein Gebot-Objekt um, das den Betrag und die bietende Person spei-
chert. Aus diesem Grund sehen wir im Klassendiagramm eine Verbindung von der
Gebot-Klasse zu der Person-Klasse. Beachten Sie jedoch, dass es keine Verbindung
von Gebot zu Posten gibt. Die Verbindung ist in dem Diagramm andersherum, da
ein Posten das bisher höchste Gebot für den Posten speichert. Das bedeutet, dass
das Posten-Objekt das bei ihm gespeicherte Gebot-Objekt ersetzt, sobald ein höhe-
res Gebot eingeht.
Was wir hier beschrieben haben, ist eine ziemlich verschachtelte Kette von Objekt-
referenzen. Auktion-Objekte speichern Posten-Objekte; jedes Posten-Objekt kann

170
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem

ein Gebot-Objekt speichern; jedes Gebot-Objekt speichert ein Person-Objekt. Solche


Ketten kommen in Programmen sehr häufig vor, sodass dieses Projekt eine gute
Gelegenheit bietet herauszufinden, wie sie in der Praxis funktionieren.

Übung 4.46 Erzeugen Sie eine Auktion mit einigen Posten, Personen und
Geboten. Verwenden Sie dann den Objektinspektor, um die Objektstruktur
zu untersuchen. Beginnen Sie mit dem Auktionsobjekt und untersuchen Sie
anschließend alle Objektreferenzen, die Ihnen in den Datenfeldern der
Objekte begegnen.

Da weder die Klasse Person noch die Klasse Gebot irgendeine Aktivität im Auk-
tionssystem initiieren, wollen wir hierauf nicht näher eingehen. Das Studium die-
ser Klassen bleibt dem interessierten Leser als Übung überlassen. Wir werden
stattdessen unser Hauptaugenmerk auf den Quelltext der Klassen Posten und
Auktion richten.

4.14.2 Das Schlüsselwort null


Aus der obigen Diskussion sollte klar sein, dass ein Gebot-Objekt nur erzeugt wird, Konzept
wenn tatsächlich jemand für einen Posten bietet. Das neu erzeugte Gebot-Objekt
Das Java-Schlüssel-
speichert dann die Person, die das Gebot abgegeben hat. Das wiederum bedeutet,
wort null hat die
dass das Person-Datenfeld eines Gebot-Objekts im Gebot-Konstruktor initialisiert Bedeutung „kein
werden kann und immer ein gültiges Person-Objekt enthält. Objekt“. null wird
in einer Objektvari-
Wenn hingegen ein Posten-Objekt erzeugt wird, bedeutet dies lediglich, dass der
ablen gespeichert,
Posten in die Auktion gegeben wurde und noch keinen Bieter gefunden hat. die nicht auf ein
Trotzdem hat dieses Objekt ein Datenfeld hoechstesGebot vom Typ Gebot, in dem konkretes Objekt
das höchste Gebot für den Posten abgelegt wird. Mit welchem Wert sollte dieses verweist. Eine
Datenfeld im Posten-Konstruktor initialisiert werden? Objektvariable, die
nicht explizit initia-
Was wir benötigen, ist ein Wert für das Datenfeld, aus dem klar hervorgeht, dass es lisiert wurde, ent-
zurzeit „kein Objekt“ gibt, auf das diese Variable verweist. Die Variable ist gewisser- hält als Standard-
maßen „leer“. Hierfür bietet uns Java das Schlüsselwort null. Folglich gibt es im wert null.
Konstruktor von Posten die folgende Anweisung:
hoechstesGebot = null;
Ein sehr wichtiges Prinzip der Programmierung mit Referenzen ist, dass über eine
Variable, die den null-Wert enthält, keine Methode aufgerufen werden sollte. Die
Erklärung liegt auf der Hand: Da Methoden zu Objekten gehören, können wir über
eine Variable, die auf kein Objekt verweist, auch keine Methode aufrufen. Das
bedeutet, dass wir manchmal eine if-Anweisung verwenden müssen, um zu tes-
ten, ob eine Variable null enthält oder nicht, bevor wir eine Methode auf diese
Variable aufrufen können. Wird dieser Test nicht gemacht, dann kommt es sehr
häufig zu einem Laufzeitfehler namens NullPointerException. Wir werden einige
Beispiele für diesen Test sowohl in der Klasse Posten als auch in der Klasse Auktion
sehen.

171
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Genau genommen erhält ein Datenfeld, das vom Typ eines Objekts ist, automa-
tisch den Wert null, wenn wir vergessen haben, es zu initialisieren. In diesem spe-
ziellen Fall ziehen wir es jedoch vor, die Zuweisung explizit vorzunehmen, sodass
der Leser des Codes genau weiß, dass wir bei der Erzeugung eines Posten-Objekts
davon ausgehen, dass hoechstesGebot den Wert null hat.

4.14.3 Die Klasse Posten


Die Klasse Posten speichert eine Beschreibung eines Postens, eine Postennummer
und die Details des bisher höchsten Gebots. Die umfangreichste Methode der
Klasse ist die Methode hoeheresGebot (Listing 4.9). Sie überprüft für ein neues
Gebot, ob es höher ist als alle bisherigen Gebote für den Posten. Wenn es höher
ist, dann wird es als das aktuell höchste Gebot im Posten vermerkt.

Listing 4.9
Die Behandlung
eines Gebots für
einen Posten.

Hier überprüfen wir zuerst, ob das Gebot tatsächlich das höchste ist. Das ist ent-
weder der Fall, wenn es vorher noch kein Gebot gegeben hat, oder wenn das
bisher höchste Gebot niedriger ist als das gegebene. Der erste Teil der Prüfung ist
der folgende Vergleich:
hoechstesGebot == null

172
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem

Dieser Ausdruck testet, ob die Variable hoechstesGebot momentan eine Referenz


auf ein Objekt enthält. Wie bereits im vorherigen Abschnitt beschrieben, enthält
das Datenfeld hoechstesGebot so lange den Wert null, bis ein Gebot für den Pos-
ten abgegeben wurde. Wenn es noch null ist, dann ist dies das erste Gebot für
einen bestimmten Posten und somit auch das höchste. Ist es nicht null, dann
müssen wir den Wert mit dem neuen Gebot vergleichen. Beachten Sie, dass ein
Fehlschlagen der ersten Prüfung uns einige sehr nützliche Informationen liefert:
Wir wissen jetzt ganz sicher, dass hoechstesGebot nicht null ist, sodass wir ohne
Bedenken eine Methode darüber aufrufen können. Wir müssen in dieser zweiten
Bedingung nicht erneut auf null testen. Die Werte der beiden Gebote zu verglei-
chen, erlaubt uns, ein höheres neues Gebot zu wählen oder das neue Gebot zu
verwerfen, wenn es nicht besser ist.

4.14.4 Die Klasse Auktion


In der Klasse Auktion (Listing 4.10) sehen wir eine weitere Verwendung der Kon-
zepte von ArrayList und for-each-Schleifen, die wir in diesem Kapitel bereits dis-
kutiert haben.

Listing 4.10
Die Klasse Auktion.

173
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Das Datenfeld posten ist eine ArrayList, die die Posten hält, die in dieser Auktion
angeboten werden. Posten werden in die Auktion eingebracht, indem der Methode
postenAnmelden eine einfache Beschreibung übergeben wird. Ein neuer Posten wird
erzeugt, indem diese Beschreibung zusammen mit einer eindeutigen Postennummer
an den Konstruktor der Klasse Posten übergeben wird. Das neue Posten-Objekt wird
dann in die Sammlung eingefügt. Die folgenden Abschnitte diskutieren einige
zusätzliche, häufig vorkommende Besonderheiten anhand der Klasse Auktion.

4.14.5 Anonyme Objekte


Die Methode postenAnmelden in der Klasse Auktion demonstriert ein häufig auftre-
tendes Programmiermuster – anonyme Objekte. Wir erkennen es in der folgenden
Anweisung:
[Link](new Posten(naechstePostennummer, beschreibung));

174
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem

Hier nehmen wir zwei Dinge vor:


 Wir erzeugen ein neues Posten-Objekt und
 wir übergeben dieses neue Objekt an die Methode add der ArrayList.
Wir hätten das Gleiche in zwei Zeilen schreiben können, um die beiden Schritte
expliziter zu machen:
Posten neuerPosten = new Posten(naechstePostennummer, beschreibung);
[Link](neuerPosten);
Beide Versionen sind äquivalent, aber wenn wir für die Variable neuerPosten keine
sonstige Verwendung haben, dann vermeidet die Originalfassung die Deklaration
einer Variablen mit so begrenztem Nutzen. Wir erzeugen also ein anonymes Objekt –
ein Objekt ohne Bezeichnung –, indem wir es direkt an die Methode übergeben, die
es verwendet.

Übung 4.47 Die Methode gibGebotAb umfasst die folgenden zwei Anwei-
sungen:
Gebot gebot = new Gebot(bieter, betrag);
boolean erfolgreich = [Link](gebot);
Die Variable gebot wird hier nur als Platzhalter für das neu erzeugte Gebot-
Objekt verwendet, bevor es direkt an die Methode hoeheresGebot übergeben
wird. Schreiben Sie diese Anweisungen so um, dass Sie auf die Variable gebot
verzichten können. Verwenden Sie dazu wie in der Methode postenAnmelden
ein anonymes Objekt.

4.14.6 Methodenaufrufe verketten


In unserer Einführung des Auktion-Projektes haben wir auf eine Kette von Objekt-
referenzen hingewiesen: Auktion-Objekte speichern Posten-Objekte; jedes Posten-
Objekt kann ein Gebot-Objekt speichern; jedes Gebot-Objekt speichert ein Person-
Objekt. Wenn das Auktion-Objekt identifizieren muss, wer gegenwärtig das höchste
Gebot für einen Posten abgegeben hat, dann muss es das Posten-Objekt auffor-
dern, das Gebot-Objekt für diesen Posten zurückzuliefern, dann das Gebot-Objekt
nach dem Person-Objekt fragen, von dem das Gebot stammt, und schließlich die-
ses Person-Objekt nach seinem Namen fragen.
Wenn wir die Möglichkeit von null-Objektreferenzen außer Acht lassen, könnten
wir in etwa folgende Folge von Anweisungen haben, um den Namen eines Bieters
auszugeben:
Gebot gebot = [Link]();
Person bieter = [Link]();
String name = [Link]();
[Link](name);

175
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Da die Variablen gebot, bieter und name hier nur als Zwischenspeicher verwendet
werden, um an den Namen des Bieters zu gelangen, kommt es häufig vor, dass sol-
che Anweisungsfolgen durch die Verwendung von anonymen Objektreferenzen
komprimiert werden. Wir erhalten beispielsweise den gleichen Effekt mit folgender
Zeile:
[Link]([Link]().gibBieter().gibName());
Das sieht aus, als ob Methoden Methoden aufrufen, aber so darf es nicht gelesen
werden. In Anbetracht der Tatsache, dass diese beiden Quelltextabschnitte gleich
sind, muss die Kette der Methodenaufrufe streng von links nach rechts gelesen
werden:
[Link]().gibBieter().gibName()
Der Aufruf von gibHoechstesGebot liefert ein anonymes Gebot-Objekt, auf das
anschließend die Methode gibBieter aufgerufen wird. Entsprechend liefert gib-
Bieter ein anonymes Person-Objekt, sodass gibName auf diese Person aufgerufen
wird.
Solche Ketten von Methodenaufrufen können kompliziert aussehen, doch sie las-
sen sich entwirren, wenn Sie die zugrunde liegenden Regeln kennen. Selbst wenn
Sie sich bei Ihrem eigenen Code gegen diese kompakte Schreibweise entscheiden,
sollten Sie wissen, wie er zu lesen ist, da Sie in den Quelltexten anderer Program-
mierer mit Sicherheit darauf treffen werden.

Übung 4.48 Fügen Sie der Klasse Auktion eine Methode beenden hinzu.
Diese sollte die Sammlung der Posten durchlaufen und die Details aller Pos-
ten ausgeben. Verwenden Sie dazu eine for-each-Schleife. Posten mit min-
destens einem Gebot sollten als verkauft angesehen werden, sodass Sie
nach Posten-Objekten Ausschau halten, deren Datenfeld hoechstesGebot
nicht null ist. Verwenden Sie eine lokale Variable in der Schleife, um den
Wert zu speichern, der durch Aufrufe der Methode gibHoechstesGebot
zurückgeliefert wird, und testen Sie diese Variable auf den Wert null.
Bei verkauften Posten sollten der Name des erfolgreichen Bieters und das
Höchstgebot ausgegeben werden, bei unverkauften Posten eine kurze Mel-
dung, die dies deutlich macht.
Übung 4.49 Fügen Sie der Klasse Auktion eine Methode gibUnverkaufte
hinzu, die die folgende Signatur hat:
public ArrayList<Posten> gibUnverkaufte();
Diese Methode soll über das Datenfeld posten iterieren und unverkaufte
Posten in einer neuen lokalen ArrayList-Variablen ablegen. Sie suchen nach
Posten-Objekten, deren Datenfeld hoechstesGebot den Wert null hat. Am
Ende soll diese Liste als Ergebnis der Methode zurückgegeben werden.

176
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem

Übung 4.50 Nehmen Sie an, die Klasse Auktion enthielte eine Methode, mit
der ein Posten aus einer Auktion herausgenommen werden kann. Wenn Sie
weiterhin annehmen, dass bei den übrig gebliebenen Posten das Datenfeld
postennummer nicht verändert wird, wenn ein Posten entfernt wird: Wie
würde sich die Möglichkeit zum Entfernen von Posten auf die Methode gib-
Posten auswirken?
Übung 4.51 Ändern Sie die Methode gibPosten so ab, dass diese sich nicht
darauf verlässt, dass ein Posten mit einer bestimmten Nummer an Indexposi-
tion (nummer-1) in der Sammlung gespeichert ist. Wenn zum Beispiel Posten-
nummer 2 entfernt wurde, dann wird Postennummer 3 von Indexposition 2
auf Indexposition 1 verschoben und alle höheren Postennummern werden
ebenfalls um eine Indexposition verschoben. Sie können jedoch weiterhin
davon ausgehen, dass die Posten nach aufsteigender Postennummer abgelegt
werden.
Übung 4.52 Fügen Sie der Klasse Auktion eine Methode entfernePosten mit
folgendem Kopf hinzu:
/**
* Entferne den Posten mit der gegebenen Postennummer.
* @param nummer die Nummer des Postens, der entfernt werden soll.
* @return den Posten mit der gegebenen Nummer, oder null, falls
* es einen solchen Posten nicht gibt.
*/
public Posten entfernePosten(int nummer)
Diese Methode soll nicht davon ausgehen, dass ein Posten mit einer bestimm-
ten Nummer an einer bestimmten Position innerhalb der Liste abgelegt ist.
Übung 4.53 Die Klasse ArrayList ist im Paket [Link] definiert. Dieses
Paket enthält auch eine Klasse LinkedList. Versuchen Sie, alles über die Klasse
LinkedList herauszufinden, und vergleichen Sie ihre Methoden mit denen von
ArrayList. Über welche Methoden verfügen beide Klassen und in welchen
unterscheiden sie sich?

4.14.7 Der Umgang mit Sammlungen


Die Sammlungsklasse ArrayList (und andere ähnliche Klassen) ist ein wichtiges
Hilfsmittel bei der Programmierung, weil viele Programmierprobleme flexible Samm-
lungen von Objekten betreffen. Bevor Sie sich den nächsten Kapiteln zuwenden,
sollten Sie sich mit ihrem Umgang vertraut machen. Die folgenden Übungen
ermöglichen Ihnen dies.

177
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Übung 4.54 Arbeiten Sie weiter mit dem Projekt Verein aus Übung 4.40.
Definieren Sie in der Klasse Verein eine Methode, die folgendermaßen be-
schrieben ist:
/**
* Liefere die Anzahl der Mitglieder, die im angegebenen Monat
* Mitglied geworden sind.
* @param monat der Monat des Beitritts.
* @return die Anzahl der Mitglieder, die im Monat beigetreten sind.
*/
public int beigetretenInMonat(int monat)
Wenn der Parameter monat außerhalb des gültigen Bereichs von 1 bis 12 liegt,
dann gibt die Methode eine Fehlermeldung aus und liefert 0 zurück.
Übung 4.55 Definieren Sie in der Klasse Verein eine Methode, die folgen-
dermaßen beschrieben ist:
/**
* Entferne alle Mitglieder aus der Sammlung des Vereins, die im
* angegebenen Monat Mitglied geworden sind, und liefere sie in
* einem getrennten Sammlungsobjekt zurück.
* @param monat der Monat des Beitritts.
* @param jahr das Jahr des Beitritts.
* @return die neuen Mitglieder des gegebenen Monats und Jahres.
*/
public ArrayList<Mitgliedschaft> entfernen(int monat, int jahr)
Wenn der Parameter monat außerhalb des gültigen Bereichs von 1 bis 12 liegt,
dann gibt die Methode eine Fehlermeldung aus und liefert ein Sammlungs-
objekt ohne Objekte zurück.
Hinweis: Die Methode entfernen ist deutlich schwieriger zu programmieren
als alle anderen Methoden dieser Klasse.
Übung 4.56 Öffnen Sie das Projekt Artikelbestand und vervollständigen Sie
die Klasse Bestandsverwalter in dieser und den folgenden Übungen. Der
Bestandsverwalter verwendet eine ArrayList, um Artikel zu speichern. Seine
Methode neuerArtikel führt bereits einen neuen Artikel in die Verwaltung
ein, aber die folgenden Methoden müssen noch vervollständigt werden:
aufnehmen, findeArtikel, alleArtikelAnzeigen und mengeImBestand.
Jeder Artikel, der von der Firma verkauft wird, ist durch ein Objekt der Klasse
Artikel repräsentiert, das den Namen, die Nummer und den aktuellen Bestand
dieses Artikels im Lager hält. Die Klasse Artikel definiert eine Methode erhoe-
heBestand, mit der der Bestand erhöht werden kann. Die Methode verkaufeEi-
nen vermerkt, wenn ein einzelner Artikel verkauft wurde, indem der Bestand
um 1 reduziert wird. Die Klasse Artikel ist vorgegeben und Sie sollten diese
nicht ändern müssen. Beginnen Sie, indem Sie die Methode alleArtikelAnzei-
gen implementieren und auf diese Weise feststellen, ob Sie über alle Elemente
der Sammlung iterieren können. Geben Sie einfach die Details jedes geliefer-
ten Artikels aus, indem Sie seine toString-Methode aufrufen.

178
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem

Übung 4.57 Implementieren Sie die Methode findeArtikel. Diese sollte die
Artikelsammlung nach einem Artikel mit passender Nummer durchsuchen.
Wenn ein passender Artikel gefunden wird, sollte er als Ergebnis des Aufrufs
zurückgeliefert werden. Wenn kein Artikel gefunden werden kann, wird null
geliefert.
Diese Methode unterscheidet sich von alleArtikelAnzeigen dadurch, dass
nicht notwendigerweise alle Elemente der Sammlung untersucht werden
müssen, bevor ein Artikel gefunden wird. Wenn die Artikelnummer bei-
spielsweise gleich auf den ersten Artikel in der Sammlung passt, dann kann
die Iteration abgebrochen und der Artikel geliefert werden. Andererseits
kann es passieren, dass die gesuchte Artikelnummer gar nicht bekannt ist.
In diesem Fall wird die gesamte Sammlung untersucht, ohne dass ein Artikel
gefunden wird. Dann sollte null geliefert werden.
Wenn Sie nach einem passenden Artikel suchen, werden Sie die Methode
gibNummer auf einem Artikel aufrufen müssen.
Übung 4.58 Implementieren Sie die Methode mengeImBestand. Diese sollte
einen Artikel mit passender Nummer in der Sammlung finden und die aktu-
ell vorhandene Menge als Methodenergebnis zurückliefern. Wenn kein Arti-
kel mit passender Nummer gefunden wird, soll 0 geliefert werden. Dies ist
relativ einfach zu implementieren, sobald die Methode findeArtikel zur Ver-
fügung steht. mengeImBestand könnte beispielsweise findeArtikel für die
Suche verwenden und dann auf dem Ergebnis die Methode gibBestand auf-
rufen. Beachten Sie aber den Fall, wenn ein Artikel nicht gefunden werden
kann.
Übung 4.59 Implementieren Sie die Methode aufnehmen nach einem ähnli-
chen Muster wie bei mengeImBestand. Sie sollte den Artikel mit der gegebe-
nen Nummer finden und dann auf diesem die Methode erhoeheBestand auf-
rufen.
Übung 4.60 Zusatzaufgabe. Implementieren Sie eine Methode im Bestands-
verwalter, die die Details aller Artikel ausgibt, deren Bestand unter einen
bestimmten Level gesunken ist (der als Parameter an die Methode über-
geben wird).
Verändern Sie die Methode neuerArtikel so, dass ein neuer Artikel nicht ein-
gefügt werden kann, wenn bereits ein Artikel mit dieser Nummer existiert.
Fügen Sie beim Bestandsverwalter eine Methode ein, mit deren Hilfe ein Arti-
kel nicht über seine Nummer, sondern über seinen Namen gefunden werden
kann:
public Artikel findeArtikel(String name)
Für die Lösung müssen Sie wissen, dass zwei String-Objekte s1 und s2 mit
dem booleschen Ausdruck
[Link](s2)
auf Gleichheit getestet werden können. Mehr zum Thema Vergleichen von
Strings finden Sie in Kapitel 6.

179
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen

Zusammenfassung
Wir haben gesehen, dass uns Klassen wie ArrayList auf einfache Weise
ermöglichen, Sammlungen mit einer beliebigen Anzahl von Elementen zu
erzeugen. Die Java-Bibliothek enthält weitere Sammlungen dieser Art, und
wir werden uns einige von ihnen in Kapitel 6 ansehen. Sie werden feststel-
len, dass ein souveränes Umgehen mit Sammlungen eine wichtige Fähigkeit
für das Entwickeln interessanter Programme ist. Es gibt praktisch keine
Anwendung, die nicht in irgendeiner Form Sammlungen verwendet.
Beim Umgang mit Sammlungen entsteht der Bedarf, über alle Elemente
einer Sammlung zu iterieren, um von jedem Objekt Gebrauch machen zu
können. Zu diesem Zweck haben wir uns die Benutzung von Schleifen und
Iteratoren angesehen.
Schleifen sind ebenfalls ein fundamentales Konzept der Programmierung, das
Sie von nun an in allen weiteren Projekten verwenden werden. Stellen Sie
sicher, dass Sie den Umgang mit Schleifen ausreichend geübt haben – ohne
Schleifen werden Sie nicht sehr weit kommen. Um zu entscheiden, welche
Art von Schleife in welcher Situation angebracht ist, ist es nützlich zu wissen,
ob die Aufgabe mit einer bestimmten oder unbestimmten Iteration verbun-
den ist. Ist die Anzahl der benötigten Durchläufe bekannt oder nicht?
Am Rande haben wir auch die Klassenbibliothek von Java erwähnt – eine
große Sammlung nützlicher Klassen, die wir einsetzen können, um unsere
eigenen Klassen leistungsfähiger zu machen. Wir sollten diese Bibliothek aus-
führlicher untersuchen, um zu sehen, welche weiteren Klassen wir kennen soll-
ten. Die Benutzung der Bibliothek ist deshalb Thema eines späteren Kapitels.

NEUE BEGRIFFE IN DIESEM KAPITEL


Sammlung, Iterator, for-each-Schleife, while-Schleife, Index, import-Anwei-
sung, Bibliothek, Paket, anonymes Objekt, bestimmte Iteration, unbe-
stimmte Iteration

180
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem

Zusammenfassung der Konzepte


 Sammlung Sammlungsobjekte sind Objekte, die eine beliebige Anzahl
anderer Objekte enthalten können.
 Schleife Eine Schleife kann benutzt werden, um einen Block von Anwei-
sungen wiederholt ausführen zu lassen, ohne dass die Anweisungen mehr-
fach in den Quelltext geschrieben werden müssen.
 Iterator Ein Iterator ist ein Objekt, mit dessen Hilfe über alle Elemente
einer Sammlung iteriert werden kann.
 null Das Java-Schlüsselwort null hat die Bedeutung „kein Objekt“. null
wird in einer Objektvariablen gespeichert, die nicht auf ein konkretes
Objekt verweist. Eine Objektvariable, die nicht explizit initialisiert wurde,
enthält als Standardwert null.

Übung 4.61 Java bietet einen weiteren Schleifentyp an: die do-while-Schleife.
Finden Sie heraus, wie diese Schleife funktioniert, und beschreiben Sie es.
Schreiben Sie eine do-while-Schleife, die die Zahlen von 1 bis 10 ausgibt. Um
etwas über diese Schleife nachzulesen, sollten Sie eine Sprachbeschreibung
von Java benutzen (beispielsweise [Link]
java/nutsandbolts/ im Abschnitt Control Flow Statements).
Übung 4.62 Schreiben Sie die Methode alleDateienAusgeben in der Klasse
MusikSammlung des Projekts Musiksammlung-v3 so um, dass eine do-while-
Schleife anstelle einer for-each-Schleife verwendet wird. Testen Sie Ihre Lösung
sorgfältig. Funktioniert sie auch, wenn die Sammlung leer ist?
Übung 4.63 Zusatzaufgabe. Schreiben Sie die Methode findeErste in der
Klasse MusikSammlung des Projekts Musiksammlung-v4 so um, dass eine do-
while-Schleife anstelle einer while-Schleife verwendet wird. Testen Sie Ihre
Lösung sorgfältig. Starten Sie Suchläufe, die erfolgreich sind, und solche, die
fehlschlagen. Starten Sie Suchläufe, in denen die gesuchte Datei die erste bzw.
die letzte in der Liste ist.
Übung 4.64 Informieren Sie sich über die switch-case-Anweisung von Java.
Welchen Zweck hat sie? Wie wird sie benutzt? Schreiben Sie ein Beispiel. (Dies
ist ebenfalls eine Anweisung zur Steuerung des Kontrollflusses (control flow),
daher werden Sie Informationen an ähnlichen Stellen finden wie für die do-
while-Schleife.)

181
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

5 Funktionale Verarbeitung von


Sammlungen (fortgeschrittene Konzepte)

Lernziele
Zentrale Konzepte in diesem Kapitel: Funktionale Programmierung,
Lambda-Ausdrücke, Streams, Pipelines
Java-Konstrukte in diesem Kapitel: Stream, Lambda-Ausdruck, filter,
map, reduce, ->

In diesem Kapitel führen wir die ersten der fortgeschrittenen Konzepte ein.
Abschnitte dieser Art (es wird später noch einige mehr geben) können beim
ersten Lesen getrost übersprungen werden. Wir werden in diesem Kapitel
einige alternative Techniken besprechen, mit denen dieselben Ziele erreicht wer-
den können wie mit den Methoden, die wir in den vorigen Kapiteln diskutiert
haben. Diese Techniken sind in Java erst ab Version 8 integriert.

5.1 Ein neuer Blick auf die Themen von


Kapitel 4
In Kapitel 4 haben wir uns Techniken angesehen, mit denen Sammlungen von
Objekten verarbeitet werden konnten. Der Umgang mit Sammlungen ist eine der
fundamentalsten Techniken im Programmieren; fast alle Computerprogramme
verarbeiten in irgendeiner Art Sammlungen und wir werden in diesem Buch noch
sehr vielen weiteren begegnen. Es ist eine entscheidende Fähigkeit, dies gut zu
beherrschen.
Das Programmierenlernen hinsichtlich Sammlungen ist jedoch in den letzten Jah-
ren durch die Einführungen von neuen Programmiertechniken zu den großen
Programmiersprachen komplizierter geworden. Diese Techniken können zwar Ihr
Programm vereinfachen, aber sie zu lernen ist aufwendiger – es gibt jetzt schlicht
mehrere unterschiedliche Konstrukte, die zu meistern sind.
Die „neuen“ Techniken sind nicht wirklich neu – sie sind nur neu innerhalb dieser
bestimmten Sprachart. Es gab immer unterschiedliche Arten von Sprachen: Java
ist beispielsweise eine imperative objektorientierte Sprache, und der Stil zur Ver-
arbeitung von Sammlungen, den wir in Kapitel 4 vorgestellt haben, ist eine typi-
sche imperative Herangehensweise.

183
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Eine weitere Sprachklasse sind funktionale Sprachen; es gibt sie schon eine lange
Zeit und sie wenden einen anderen Programmierstil an. Aus Gründen, die wir
später noch besprechen werden, hat der funktionale Programmierstil bei der Ver-
arbeitung von Sammlungen in einigen Situationen Vorteile gegenüber dem
imperativen Stil. Deshalb haben die Entwickler von Java irgendwann entschie-
den, einige Elemente der funktionalen Programmierung in Java aufzunehmen.
Dies ist in Java Version 8 geschehen (die 2014 veröffentlicht wurde). Die neu hin-
zugefügten Techniken sind Streams (Ströme) und Lambda-Ausdrücke (manchmal
auch Closures genannt). Dies macht Java technisch gesehen zu einer hybriden
Sprache: größtenteils imperativ, aber mit einigen funktionalen Elementen.
Für erfahrene Programmierer ist dies eine großartige Sache. Die neuen funktiona-
len Sprachkonstrukte erlauben es uns, elegantere und ausdrucksstärkere Pro-
gramme zu schreiben. Auf Anfänger kommt allerdings mehr Arbeit zu: Man
muss jetzt zwei unterschiedliche Wege lernen, um ein ähnliches Ziel zu erreichen.
Der neue funktionale Stil, den wir in diesem Kapitel behandeln, wird schnell
populär und wird wahrscheinlich innerhalb kurzer Zeit zum vorherrschenden Pro-
grammierstil für neuen Code in Java avancieren. Der imperative Stil, der im vori-
gen Kapitel behandelt wurde, wird jedoch weiterhin für die meisten heutigen
Programme verwendet und immer noch als „Standard-Java“ angesehen. Wenn
Sie mit Code arbeiten, der von anderen Programmierern stammt (was die meiste
Zeit der Fall sein wird), ist es unabdingbar, dass Sie den imperativen Stil verstehen
und damit arbeiten können. Beim Programmieren Ihres eigenen Codes können
Sie Ihren Stil auswählen: funktionaler und imperativer Stil sind zwei unterschiedli-
che Wege, um dasselbe Problem zu lösen, und können sich jeweils gegenseitig
ersetzen. Von nun an werden wir häufig den funktionalen Stil vorziehen, da er
prägnanter und eleganter ist.
Kurz gesagt, ein Anfänger muss sich mit beiden Stilrichtungen der Verarbeitung
von Sammlungen vertraut machen, um ein guter Programmierer zu werden. In
diesem Buch präsentieren wir immer zuerst den imperativen Stil und führen den
funktionalen Stil in den als „fortgeschritten“ markierten Abschnitten (wie diesem
Kapitel) ein.
Diese vertiefenden Kapitel können zunächst ausgelassen werden (Sie können
also jetzt mit Kapitel 6 fortfahren, wenn Sie möchten) – Sie werden trotzdem
Programmieren lernen und weiterhin in der Lage sein, die vorgestellten Aufga-
ben zu lösen. Diese Abschnitte können auch unabhängig von der chronologi-
schen Reihenfolge gelesen werden (also jetzt überspringen, aber später zurück-
kehren), wenn Sie gerade wenig Zeit haben und zuerst mit anderen Konstrukten
vorankommen möchten. Um einen Gesamteindruck zu erhalten, können Sie
diese Abschnitte so lesen, wie sie im Buch erscheinen – auf diese Art lernen Sie
die alternativen Konstrukte quasi parallel.

184
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.2 Monitoring von Tierpopulationen

5.2 Monitoring von Tierpopulationen


Wie bisher werden wir ein Beispielprogramm verwenden, um die neuen Konst-
rukte einzuführen und zu besprechen. Hier wollen wir uns ein System für das Moni-
toring von Tierpopulationen ansehen.
Ein wichtiges Element von Tierschutz und der Erhaltung gefährdeter Arten ist die
Überwachung von Populationsgrößen, um festzustellen, wann der Bestand gesund
ist und wann er rückläufig ist. Unser Projekt verarbeitet Sichtungsmeldungen von
unterschiedlichen Tierarten, die von Meldern von vielen unterschiedlichen Orten
eingesandt werden. Jeder Sichtungsbericht besteht aus dem Namen der Tierart,
die gesichtet wurde, der Anzahl der von dieser Art gesichteten Tiere, wer den
Bericht sendet (eine ID als ganze Zahl), in welchem Gebiet die Sichtung stattge-
funden hat (eine ganze Zahl) und eine Angabe darüber, wann die Sichtung
gemacht wurde (ein einfacher numerischer Wert, der zum Beispiel die Anzahl der
Tage bezeichnet, die seit Beginn des Experiments vergangen sind).
Die einfachen Datenanforderungen machen es für die Melder an entfernten
Orten leicht, verwertbare Informationen in relativ kleinen Einheiten – vielleicht in
Form einer Textnachricht – zu einer Basis zu senden, die diese Daten dann sam-
meln und Berichte erzeugen kann oder die Feldforscher in andere Gebiete aus-
sendet.
Dieses Konzept könnte sowohl bei Projekten eingesetzt werden, die intensiv ver-
waltet werden – zum Beispiel, wenn in einem Nationalpark Kameras durch Bewe-
gung ausgelöst werden und Angestellte die Kamerabilder auswerten – als auch
bei lose strukturierten Crowd-Sourcing-Aktivitäten – wie nationalen Vogelzähl-
tagen, bei denen eine große Gruppe von Freiwilligen Daten mittels Handy-Apps
einsendet.
Sie finden eine Teilimplementierung eines solchen Systems unter dem Namen
Tiermonitoring-v1 in den Buchprojekten.
Listing 5.1 zeigt die Klasse Sichtung, die wir benutzen, um Details jeder Sichtungs-
meldung von einem einzelnen Melder für ein bestimmtes Tier aufzuzeichnen,
nachdem die Sichtungsdetails verarbeitet wurden. In diesem Kapitel kümmern
wir uns nicht um das Format der eingesandten Daten. Da das Thema dieses Kapi-
tels die fortgeschritteneren Java-Konzepte sind, möchten wir Sie ermutigen, den
vollständigen Quelltext des Projekts zu lesen, um herauszufinden, was wir bis
jetzt noch nicht ausführlich untersucht haben, und dies für Ihre eigene Entwick-
lung zu nutzen. Die Klasse Sichtung ist jedoch noch relativ geradlinig.

185
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Listing 5.1
Die Klasse Sichtung.

Listing 5.2 zeigt Teile der Klasse Tiermonitor, mit deren Hilfe die einzelnen Sichtun-
gen in einer Liste zusammengefasst werden. Zu diesem Zeitpunkt werden alle Sich-
tungen von allen verschiedenen Meldern und aus allen unterschiedlichen Gebieten
in einer einzigen Sammlung gehalten. Unter anderem enthält die Klasse Sichtung
Methoden zum Ausgeben der Liste, zum Zählen der Gesamtzahl an Sichtungen
einer bestimmten Tierart, zum Auflisten aller Sichtungen durch einen bestimmten
Melder, zum Entfernen von leeren Datensätzen und so weiter. All diese Methoden
wurden mithilfe der grundlegenden Techniken zur Listenverarbeitung aus Kapitel 4
implementiert: Iteration über der gesamten Liste; Verarbeitung ausgewählter Listen-
elemente aufgrund bestimmter Bedingungen (Filtern); sowie das Entfernen von Ele-
menten. Die hier implementierten Methoden sind für eine Anwendung dieser Art
nicht vollständig, wir haben sie hier aber implementiert, um die unterschiedlichen
Arten der Listenverarbeitung zu demonstrieren.

186
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.2 Monitoring von Tierpopulationen

Listing 5.2
Die Klasse
Tiermonitor.

Bisher verwendet der Quelltext die imperativen Techniken zur Verarbeitung von
Sammlungen, die in Kapitel 4 eingeführt wurden. Wir wollen dies als Basis nutzen,
um schrittweise den Code zur Listenverarbeitung durch funktionale Konstrukte
zu ersetzen.

Übung 5.1 Öffnen Sie das Projekt Tiermonitoring-v1 und erzeugen Sie eine
Instanz der Klasse Tiermonitor. Die Datei [Link] im Projektordner
enthält eine kleine Sammlung von Sichtungsdatensätzen im CSV-Format
(Comma-Separated Values). Übergeben Sie den Namen dieser Datei (also
„[Link]“) der Methode sichtungenHinzufuegen im Tiermonitor-Objekt
und rufen Sie dann listeAusgeben auf, um Details der eingelesenen Daten
anzuzeigen.
Übung 5.2 Probieren Sie die anderen Methoden des Tiermonitor-Objekts aus,
benutzen Sie dazu Tiernamen, Melder- und Gebiet-IDs, die in der Ausgabe
von listeAusgeben gezeigt sind.

187
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Übung 5.3 Lesen Sie sich den vollständigen Quelltext der Klasse Tiermonitor
durch, um zu verstehen, wie die Methoden arbeiten. Die Klasse verwendet
nur Techniken, die wir in den vorigen Kapiteln behandelt haben, Sie sollten
daher in der Lage sein, die Details herauszufinden.
Übung 5.4 Warum verwendet die Methode entferneNullAnzahl eine while-
Schleife mit einem Iterator anstatt einer for-each-Schleife wie in den anderen
Methoden? Könnte die Methode auch mit einer for-each-Schleife geschrie-
ben werden? Könnten die anderen Methoden mit einer while-Schleife funk-
tionieren?

5.3 Lambda-Ausdrücke – ein erster Blick


Konzept Im Zentrum des neuen funktionalen Programmierstils zur Verarbeitung von Samm-
lungen steht das Konzept der Lambda-Ausdrücke.
Ein Lambda-
Ausdruck ist ein Die Grundidee besteht darin, einen Codeabschnitt als Parameter an eine Methode
Codeabschnitt, der zu übergeben, die dieses Codestück später ausführen kann, wenn es benötigt wird.
für eine spätere
Ausführung gespei- Bis jetzt haben wir verschiedene Parametertypen kennengelernt – ganze Zahlen,
chert werden kann. Zeichenketten, Objekte –, doch diese waren allesamt Bestandteile von Daten,
nicht von Code. Die Möglichkeit, Code als Parameter zu übergeben, ist mit Java
8 neu hinzugekommen, und erlaubt uns, einige sehr nützliche Dinge zu tun.

5.3.1 Verarbeitung einer Sammlung mit einem


Lambda-Ausdruck
Als wir unsere Sammlungen in Kapitel 4 verarbeitet haben, haben wir eine
Schleife verwendet, um die Elemente der Sammlung eines nach dem anderen zu
holen, und daraufhin haben wir etwas mit jedem Element gemacht. Die Struktur
ähnelt dabei immer dem folgenden Pseudocode:
loop (für jedes Element in der Sammlung):
nimm ein Element;
mache etwas mit dem Element;
ende loop
Es ist egal, ob wir eine for-Schleife, eine for-each-Schleife oder eine while-Schleife
verwenden oder ob wir einen Iterator oder einen Index benutzen, um auf die Ele-
mente zuzugreifen – das Prinzip ist dasselbe.
Beim Einsatz eines Lambda-Ausdrucks wird das Problem anders angegangen.
Wir übergeben ein paar Codezeilen an die Sammlung mit der Anweisung „Tue
dies mit jedem Element in der Sammlung“. Die Codestruktur sieht ungefähr so
aus:
[Link](ein wenig Code);

188
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.3 Lambda-Ausdrücke – ein erster Blick

Offensichtlich sieht der Code viel einfacher aus. Am wichtigsten ist jedoch: die Konzept
Schleife ist ganz verschwunden. Tatsächlich ist sie nicht weg – sie ist lediglich in die
Beim funktiona-
tueDiesMitJedemElement-Methode verschoben worden –, aber sie ist aus unserem
len Program-
eigenen Code verschwunden; wir müssen sie nicht mehr jedes Mal schreiben. mierstil zur
Die Sammlungen sind nun leistungsstärker: Anstatt nur ihre Elemente an uns Verarbeitung
von Sammlun-
aushändigen zu können, damit wir diese irgendwie bearbeiten, ist die Sammlung
gen holen wir
jetzt in der Lage, die Elemente für uns zu bearbeiten. Dies erleichtert uns das nicht jedes Ele-
Leben. ment einzeln, statt-
dessen übergeben
Wir können uns das folgendermaßen vorstellen: Angenommen, Sie müssen jedem
wir ein Codeseg-
Kind in einer Schulklasse die Haare schneiden. Wenn Sie die Aufgabe nach der alten ment an die Samm-
Art angehen, dann sagen Sie zu dem Lehrer: „Schicken Sie mir das erste Kind.“ lung, das auf jedes
Dann schneiden Sie dem Kind die Haare. Danach sagen Sie: „Schicken Sie mir das Element ange-
nächste Kind.“ Eine weitere Haarschneidesitzung folgt. Und so geht es weiter, bis wandt werden soll.
alle Kinder ihren Haarschnitt bekommen haben. Dies ist die Schleife im alten Stil von
„nächstes Element–Verarbeitung“.
Beim neuen Stil machen Sie Folgendes: Sie schreiben Anweisungen auf, wie man
Haare schneidet, und dann geben Sie dies dem Lehrer und sagen: „Führen Sie dies
bei jedem Kind in Ihrer Klasse durch.“ (Der Lehrer repräsentiert hier die Samm-
lung.) Tatsächlich ist ein interessanter Nebeneffekt dieses Ansatzes, dass wir noch
nicht einmal wissen müssen, wie der Lehrer diese Aufgabe bewältigen wird. Zum
Beispiel könnte er, anstatt die Haare selbst zu schneiden, sich entscheiden, für
jedes einzelne Kind eine andere Person zu beauftragen, sodass alle Haare gleich-
zeitig geschnitten werden. Der Lehrer würde also einfach die Anweisungen wei-
terreichen, die Sie ihm gegeben haben.
Ihr Leben ist plötzlich viel einfacher. Der Lehrer übernimmt den Großteil der Arbeit
für Sie. Dies ist genau das Prinzip, wie die neuen Sammlungen in Java 8 für Sie
arbeiten.

5.3.2 Grundlegende Syntax eines Lambda-


Ausdrucks
Ein Lambda-Ausdruck ähnelt in gewisser Weise einer Methode: es ist ein Code-
abschnitt, der für eine bestimmte Aufgabe definiert ist, der aber nicht unmittel-
bar ausgeführt wird. Anders als eine Methode jedoch gehört ein Lambda-Aus-
druck nicht zu einer Klasse und wir werden ihn nicht benennen. Es ist hilfreich,
die Syntax eines Lambda-Ausdrucks mit der einer Methode zu vergleichen, die
dieselbe Aufgabe ausführt.
Eine Methode, um die Details einer Sichtung auszugeben, könnte folgendermaßen
aussehen:
public void sichtungAusgeben(Sichtung datensatz)
{
[Link]([Link]());
}

189
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Der entsprechende Lambda-Ausdruck dazu lautet:


(Sichtung datensatz) ->
{
[Link]([Link]());
}
Sie können jetzt leicht die Unterschiede und die Gemeinsamkeiten sehen. Die
Unterschiede sind:
 Es gibt kein public- oder private-Schlüsselwort – die Sichtbarkeit wird als pub-
lic vorausgesetzt.
 Es wird kein Rückgabewert angegeben. Das liegt daran, dass der Compiler dies
aus der letzten Anweisung im Rumpf des Lambda-Ausdrucks herausfinden
kann. In diesem Fall ist die println-Methode void, das heißt der Rückgabetyp
des Lambda-Ausdrucks ist void.
 Der Lambda-Ausdruck ist nicht benannt; er beginnt mit der Parameterliste.
 Ein Pfeil (->) trennt die Parameterliste vom Rumpf des Lambda-Ausdrucks –
dies ist die unverwechselbare Schreibweise für einen Lambda-Ausdruck.
Sie werden feststellen, dass Lambda-Ausdrücke oft für relativ einfache, einmalig
auszuführende Aufgaben eingesetzt werden, die nicht die Komplexität einer
ganzen Klasse benötigen. Einige Informationen zu einem Lambda-Ausdruck wer-
den standardmäßig geliefert oder aus dem Kontext erschlossen – seine Sichtbar-
keit und der Rückgabetyp – und wir haben keinen Einfluss darauf. Und da ein
Lambda-Ausdruck keine ihm zugeordnete Klasse hat, gibt es außerdem keine
Instanzfelder und keinen Konstruktor. Dies führt zu einem sehr spezialisierten
Einsatz der Lambda-Ausdrücke. Aufgrund des fehlenden Namens sind Lambda-
Ausdrücke auch als anonyme Funktionen bekannt.
Wir werden den Lambda-Ausdrücken und Funktionsparametern in diesem Buch
noch später mehrmals begegnen, zum Beispiel wenn wir funktionale Interfaces
in Kapitel 12 einführen und wenn wir grafische Benutzerschnittstellen (GUIs) in
Kapitel 13 schreiben. Für den Moment werden wir jedoch untersuchen, wie Lam-
bda-Ausdrücke mit unseren Sammlungen eingesetzt werden.

5.4 Die forEach-Methode von


Sammlungen
Als wir oben das Konzept der Lambda-Ausdrücke eingeführt haben, haben wir
die Idee einer tueDiesMitJedemElement-Methode in der Sammlungsklasse ange-
schnitten. Diese Rolle nimmt in Java die forEach-Methode ein.
Wenn wir eine Sammlung namens meineListe haben, können wir schreiben:
[Link]( ... Code, der auf jedes Listenelement
angewandt wird ... )
Der Parameter für diese Methode wird ein Lambda-Ausdruck – ein Codesegment –
mit der oben gezeigten Syntax sein. Die forEach-Methode führt dann den Lambda-

190
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.4 Die forEach-Methode von Sammlungen

Ausdruck für jedes der obigen Listenelemente aus, wobei der Reihe nach jedes Lis-
tenelement als Parameter dem Lambda-Ausdruck übergeben wird.
Nun wollen wir uns den richtigen Code ansehen. Die Methode listeAusgeben aus
unserer Tiermonitor-Klasse sieht folgendermaßen aus:
/**
* Gib die Details aller Sichtungen aus.
*/
public void listeAusgeben()
{
for(Sichtung datensatz : sichtungen) {
[Link]([Link]());
}
}
Wir können jetzt die folgende Version dieser Methode schreiben, die auf Lambda-
Ausdrücken basiert:
/**
* Gib die Details aller Sichtungen aus.
*/
public void listeAusgeben()
{
[Link](
(Sichtung datensatz) –>
{
[Link]([Link]());
}
);
}
Der Rumpf dieser neuen Version von listeAusgeben besteht aus einer einzigen
Anweisung (selbst wenn sich diese über sechs Zeilen hinzieht), welche ein Aufruf
an die forEach-Methode der sichtungen-Liste ist. Zugegebenermaßen sieht es jetzt
noch nicht einfacher als vorher aus, aber wir werden gleich dort hinkommen. Es
ist wichtig zu bemerken, dass es in dieser Version, anders als in der vorigen, keine
explizite Schleifenanweisung gibt – die Iteration wird implizit von der forEach-
Methode durchgeführt.
Der Parameter der forEach-Methode ist ein Lambda-Ausdruck. Der Lambda-Aus-
druck selbst hat einen Parameter namens datensatz. Jedes Element der sichtun-
gen-Liste wird der Reihe nach als Parameter für diesen Lambda-Ausdruck verwen-
det und der Rumpf des Lambda-Ausdrucks wird ausgeführt. So werden die
Details von jedem Element ausgegeben.
Dieser syntaktische Stil unterscheidet sich völlig von allem, was Sie in den vorigen
Kapiteln dieses Buches kennengelernt haben. Nehmen Sie sich daher unbedingt
genügend Zeit, um dieses erste Beispiel eines Lambda-Ausdrucks in Aktion zu
verstehen. Wir werden in Kürze einige leichte Veränderungen an der hier verwen-
deten Syntax zeigen, doch dieses Beispiel illustriert den typischen Einsatz eines
Lambda-Ausdrucks und die grundlegenden Ideen finden sich in allen kommen-
den Beispielen wieder:

191
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

 Ein Lambda-Ausdruck beginnt mit einer Parameterliste.


 Ein Pfeilsymbol folgt auf die Parameter.
 Auszuführende Anweisungen kommen nach dem Pfeilsymbol.

Übung 5.5 Öffnen Sie das Projekt Tiermonitoring-v1 und schreiben Sie die
Methode listeAusgeben in Ihrer Verson der Tiermonitor-Klasse um, sodass nun
ein Lambda-Ausdruck verwendet wird, wie oben gezeigt. Testen Sie, ob die
Methode noch funktioniert und dasselbe Ergebnis wie bisher liefert.

5.4.1 Variationen der Syntax von Lambda-


Ausdrücken
Eines der Ziele der Lambda-Syntax in Java ist es, dass Code klar und präzise
geschrieben werden kann. Die Möglichkeit, den Zugriffsmodifikator public und
den Rückgabetyp auszulassen, stellt den Auftakt dazu dar. Der Compiler erlaubt
es uns jedoch, noch darüber hinauszugehen. Es können mehrere weitere Ele-
mente ausgelassen werden, wenn der Compiler diese selbst herausfinden kann.
Das erste Element ist der Parametertyp für den Lambda-Ausdruck (in unserem Fall
datensatz). Da der Typ des Lambda-Parameters immer mit dem Typ der Samm-
lungselemente übereinstimmen muss, kann der Compiler diesen Typ herausfinden,
und wir können ihn auslassen. Unser Code sieht dann folgendermaßen aus:
[Link](
(datensatz) –>
{
[Link]([Link]());
}
);

Die nächste Vereinfachung betrifft die Klammern, sowohl die runden als auch die
geschweiften:
 Wenn die Parameterliste nur einen einzigen Parameter enthält, dann können
die Klammern weggelassen werden.
 Enthält der Rumpf des Lambda-Ausdrucks nur eine einzige Anweisung, dann
können die geschweiften Klammern weggelassen werden.
Durch die Anwendung dieser Vereinfachungen auf unseren Code wird dieser
nun so kurz, dass er in eine einzige Zeile passt, und wir können den forEach-Auf-
ruf nun folgendermaßen schreiben:
[Link](datensatz) -> [Link]([Link]()));
Dies ist die Version, die wir ab nun in unserem Quelltext bevorzugt verwenden.
Auch an dieser Stelle möchten wir Sie noch einmal anregen sicherzustellen, dass
Sie verstanden haben, wie diese reduzierte Syntax der Beschreibung entspricht,
da in der Praxis Lambda-Ausdrücke oft in dieser Form geschrieben werden.

192
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams

Das Projekt Tiermonitoring-v2 ist im funktionalen Stil wie hier dargestellt geschrie-
ben worden und kann verwendet werden, falls Sie Ihre Lösungen von einigen der
Aufgaben überprüfen möchten. Machen Sie trotzdem die Übungen erst einmal
selbst und verwenden Sie dieses Projekt nur, wenn Sie wirklich nicht weiterkom-
men.

Vorteile von Lambda-Ausdrücken


Wenn wir Lambda-Ausdrücke in der Praxis verwenden, ist der Lambda-Code
häufig kurz und einfach und wir können die eben eingeführte abkürzende
Schreibweise benutzen. Dadurch wird unser Quelltext kurz und prägnant
und ist einfach zu schreiben. Hinzu kommt: Da wir die Schleife nicht mehr
selbst programmieren, machen wir wahrscheinlich auch weniger Fehler –
wenn wir die Schleife nicht schreiben, können wir sie nicht falsch machen.
Es gibt jedoch noch weitere Vorteile. Einer davon ist die Möglichkeit, Streams
zu verwenden, die im nächsten Abschnitt besprochen werden. Ein weiterer
Vorteil ist die Nebenläufigkeit (auch als parallele Programmierung bezeichnet).
Die meisten Rechner heute haben mehrere Kerne und sind grundsätzlich in
der Lage, parallel zu arbeiten. Es jedoch extrem schwierig, Code zu schreiben,
der mehrere Kerne effizient nutzt. Ein Großteil unserer Programme verwendet
meistens nur einen Kern, der Rest der verfügbaren Prozessorleistung wird ver-
schwendet. Code zu programmieren, der alle Kerne verwendet, ist ein sehr
fortgeschrittenes Thema, und nur wenige Programmierer kommen jemals so
weit.
Mithilfe der Lambda-Ausdrücke und durch das Verlagern der Schleife in die
Sammlung können wir nun potenziell Sammlungen dazu benutzen, die par-
allele Verarbeitung für uns zu übernehmen. Möchten wir also Parallelität
haben, so müssen wir nur eine entsprechende Sammlung verwenden und
der ganze schwer zu programmierende Code für die parallele Verarbeitung
wird jetzt innerhalb der Sammlungsklasse versteckt und das Schreiben des
Quelltextes dafür wird uns abgenommen. Unser Code sieht kurz und ein-
fach aus. Plötzlich wird es – mit nur sehr wenig zusätzlichem Aufwand –
möglich, alle Kerne in Ihrem Laptop zu benutzen.
Wir werden in diesem Buch parallele Programmierung nicht weiter bespre-
chen. Es ist jedoch interessant zu wissen, dass Sie gut darauf vorbereitet
sind, später nebenläufige Sammlungsklassen zu verwenden, wenn Sie den
Umgang mit Lambda-Ausdrücken und Streams gut beherrschen.

5.5 Streams
Die forEach-Methode der Klasse ArrayList (und verwandten Sammlungsklassen)
ist nur ein Beispiel für eine Methode, die Streams (Ströme) verwendet, welche
mit Java 8 eingeführt wurden. Streams sind ein bisschen wie Sammlungen, doch
mit einigen wichtigen Unterschieden:

193
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

 Auf die Elemente in einem Stream wird nicht über einen Index zugegriffen,
sondern in der Regel sequenziell.
 Die Inhalte und die Reihenfolge der Elemente des Streams können nicht ver-
ändert werden – für Änderungen muss ein neuer Stream erzeugt werden.
 Ein Stream kann eventuell unendlich sein!

Konzept Das Konzept der Streams wird eingesetzt, um die Verarbeitung von Datenmengen
zu vereinheitlichen, unabhängig davon, woher diese Daten stammen. Es könnte
Streams vereinen
die Verarbeitung von Elementen aus einer Sammlung sein (wie wir es besprochen
die Verarbeitung
von Elementen haben), von Nachrichten, die über ein Netzwerk hereinkommen, von Textzeilen aus
einer Sammlung einer Textdatei oder von allen Zeichen einer Zeichenkette. Es ist egal, aus welcher
und anderer Daten- Quelle die Daten kommen – wir können sie als einen Datenstrom ansehen und
mengen. Ein Stream dann darauf dieselben Techniken und Methoden anwenden, um die jeweiligen Ele-
stellt nützliche mente zu verarbeiten.
Methoden zur Ver-
fügung, um diese Mit den Streams kommt in Java eine weitere wichtige neue Funktion gegenüber vor-
Datenmengen zu herigen Möglichkeiten zur Verarbeitung von Sammlungen hinzu: die Fähigkeit zur
manipulieren. sicheren parallelen Verarbeitung. Es gibt viele Situationen, in denen die Reihenfolge,
in der die Elemente einer Sammlung verarbeitet werden, keine Rolle spielt oder in
denen die Verarbeitung eines Elements aus der Sammlung im Wesentlichen unab-
hängig von den anderen Elementen ist. Treffen diese Bedingungen zu, so kann die
Parallelisierung die Sammlungsverarbeitung bedeutend beschleunigen, wenn es um
große Datenmengen geht. Wir werden dies hier jedoch nicht weiter besprechen.
Eine ArrayList ist selbst kein Stream, aber wir können die ArrayList auffordern, die
Quelle eines Streams zur Verfügung zu stellen, indem wir ihre stream-Methode auf-
rufen. Der zurückgelieferte Stream wird alle Elemente der ArrayList in Indexreihen-
folge enthalten. Von der Parallelisierung einmal abgesehen, bieten Streams uns
eigentlich nichts, was wir nicht bereits vorher mit den bekannten Sprachmerkma-
len programmieren konnten. Doch Streams lösen elegant viele der am häufigsten
vorkommenden Aufgaben, die man auf Sammlungen durchführen kann, und zwar
in einer Form, die sowohl viel leichter zu programmieren als auch einfacher zu ver-
stehen ist, als wir es bisher gewohnt sind. Insbesondere werden wir sehen, dass
man durch den Einsatz von Streams keine expliziten Schleifen zur Verarbeitung
einer Sammlung schreiben muss – was wir jetzt mit einer Neufassung der liste-
Ausgeben-Methode in Übung 5.6 durchführen werden.

Übung 5.6 Modifizieren Sie Ihre eigene listeAusgeben-Methode, wie im Text


besprochen: Probieren Sie alle Syntaxvariationen für Lambda-Ausdrücke aus,
die wir vorgestellt haben. Kompilieren Sie Ihren Code und testen Sie das Pro-
jekt mit jeder Variation, um zu sicherzustellen, dass Ihre Syntax korrekt ist.

Wir haben bereits einiges davon mit der forEach-Methode gesehen; im Rest die-
ses Kapitels werden wir die Methoden filter, map und reduce näher untersuchen,
die auf Streams definiert sind.

194
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams

5.5.1 Filter-Map-Reduce
Wir haben oben gesagt, dass die Inhalte eines Streams nicht verändert werden
können, bei Änderungen muss also ein neuer Stream erzeugt werden. Mit nur
drei Funktionstypen kann eine Vielzahl unterschiedlicher Verarbeitungsarten rea-
lisiert werden: filter, map und reduce. Diese drei können variiert und kombiniert
werden, und damit können wir fast alles erreichen, was wir jemals vorhaben.
Bevor wir uns die konkreten Java-Methoden ansehen, wollen wir uns noch ein
wenig mit dem Grundgedanken aufhalten, der diesen Funktionen zugrunde liegt.
Alle werden auf einen Stream angewandt, um ihn auf eine bestimmte Weise zu
verarbeiten. Und sie sind alle sehr verbreitet und nützlich.

Die Funktion filter


Die Funktion filter erhält einen Stream, wählt einige seiner Elemente aus und Konzept
erzeugt einen neuen Stream, der nur aus den ausgewählten Elementen besteht
Wir können die
(Abbildung 5.1). Einige Elemente wurden herausgefiltert. Das Ergebnis ist ein Stream
Funktion filter
mit weniger Elementen (eine Teilmenge des ursprünglichen Streams). auf einen Stream
anwenden, um
nur bestimmte Ele-
mente auszuwählen.
A

B filter A Abbildung 5.1


Anwendung der Funk-
tion filter auf einen
C C Stream.

D F

Ein Beispiel aus unserer Sammlung der Tiersichtungen wäre es, nur die Sichtungen Konzept
von Elefanten aus allen aufgezeichneten Sichtungen zu selektieren. Wir beginnen Wir können die
mit allen Sichtungen, wenden einen Filter an, der nur Elefanten auswählt, und erhal- Funktion map
ten einen Stream aller Elefantensichtungen. anwenden, um
einen Stream auf
Die Funktion map einen neuen
Stream abzubil-
Die Funktion map erhält einen Stream und erzeugt einen neuen Stream, wobei den, wobei jedes
jedes Element des ursprünglichen Streams auf ein neues anderes Element im Element des
neuen Stream abgebildet wird (Abbildung 5.2). Der neue Stream hat genauso viele ursprünglichen
Elemente wie der alte, doch Typ und Inhalt jedes Elements können sich unterschei- Streams durch ein
neues Element
den – das neue Element wurde irgendwie vom ursprünglichen Element abgeleitet.
ersetzt wird, das
vom Original abge-
leitet wurde.

195
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Abbildung 5.2
Anwendung der
Funktion map auf A 3
einen Stream.
B map 1

C 4

D 1

E 2

F 3

Ein Beispiel aus unserem Projekt wäre, jedes gesichtete Objekt aus dem ursprüngli-
chen Stream mit der Anzahl der in dieser Sichtung entdeckten Tiere zu ersetzen.
Wir beginnen mit einem Stream der gesichteten Objekte und haben am Ende einen
Stream von ganzen Zahlen.

Die Funktion reduce

Konzept Die Funktion reduce erhält einen Stream und reduziert alle Elemente auf ein einzi-
ges Ergebnis (Abbildung 5.3). Dies kann auf verschiedene Arten erreicht werden,
Wir können die
zum Beispiel indem alle Elemente addiert werden oder indem einfach das kleinste
Funktion reduce
auf einen Stream Element aus dem Stream ausgewählt wird. Wir beginnen mit einem Stream und
anwenden, um den haben am Ende einen einzelnen Ergebniswert.
Stream zu reduzie-
ren. Das heißt, die
Funktion erhält den
gesamten Stream 3
als Eingabe und
gibt einen einzigen
Ergebniswert zurück.
1 reduce

Abbildung 5.3 4 14
Anwendung der
Funktion reduce auf
1
einen Stream.

196
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams

In unserem Tiermonitoring-Projekt könnte dies beispielsweise nützlich sein, wenn


wir zählen möchten, wie viele Elefanten wir gesehen haben: Sobald wir einen
Stream mit Elefantensichtungen haben, können wir reduce verwenden, um diese zu
addieren. (Zur Erinnerung: jede Instanz einer Sichtung kann aus mehreren Tierarten
bestehen.)

5.5.2 Pipelines
Streams und Stream-Funktionen werden sogar noch nützlicher, wenn mehrere Konzept
Funktionen zu einer Pipeline zusammengefasst werden. Eine Pipeline ist die Verket-
Eine Pipeline
tung von zwei oder mehreren dieser Funktionen, sodass eine nach der anderen
ist die Kombination
angewandt werden kann. von zwei oder mehr
Wir wollen annehmen, dass wir herausfinden möchten, wie viele Elefantensich- Stream-Funktionen
in einer Kette,
tungen wir aufgezeichnet haben. Wir können dazu die Funktionen filter, map und
wobei die einzel-
reduce hintereinander anwenden (Abbildung 5.4). nen Funktionen
nacheinander ange-
wandt werden.
"Elefant"
3

"Tiger"
"Elefant"
1 filter 3
map 3 reduce Abbildung 5.4
"Schlange" Eine Pipeline von
"Elefant" 8
3 1 Stream-Funktionen.
1
"Elefant"
1 "Elefant"
4
4
"Elefant"
4

"Papagei"
5

In diesem Beispiel wenden wir zuerst die filter-Funktion auf alle Sichtungen an, um
nur die Elefantensichtungen auszuwählen, dann bilden wir mit der map-Funktion
jedes Sichtungsobjekt auf die Anzahl der beobachteten Elefanten dieser Sichtung
ab und zum Schluss addieren wir diese Werte innerhalb einer reduce-Funktion.
Beachten Sie, dass wir es nach der Anwendung der map-Funktion nicht mehr mit
einem Stream von Sichtung-Objekten zu tun haben, sondern mit einem Stream von
ganzzahligen Werten. Diese Form der Transformation zwischen Eingabe-Stream und
Ausgabe-Stream kommt bei der map-Funktion sehr häufig vor.
In Pseudocode können wir dies folgendermaßen schreiben:
[Link](name == elefant).map(anzahl).reduce(addieren);
Dies ist keine korrekte Java-Syntax, aber Sie bekommen eine Vorstellung von der
Idee. Wir werden uns weiter unten den korrekten Java-Code ansehen.
Durch die verschiedenen Kombinationen dieser Funktionen kann man viele unter-
schiedliche Operationen bekommen. Wenn wir beispielsweise wissen möchten, wie
viele Berichte von einem bestimmten Melder an einem bestimmten Tag eingereicht
wurden, können wir schreiben:

197
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

[Link](melder == melderID)
.filter(zeitraum == tagID)
.reduce(anzahl elemente);
Pipelines beginnen immer mit einer Quelle (einem Stream), gefolgt von einer Reihe
von Operationen. Es gibt zwei Arten von Operationen: intermediäre Operationen
und terminale Operationen. Die letzte Operation einer Pipeline muss immer eine
terminale Operation sein und alle anderen müssen intermediäre Operationen sein:
quelle.inter1(...).inter2(...).inter3(...).terminal(...);

Übung 5.7 Schreiben Sie Pseudocode, um festzustellen, wie viele Elefanten ein
bestimmter Melder (melderID) an einem bestimmten Tag (tagID) gesehen hat.
Übung 5.8 Schreiben Sie Pseudocode, um einen Stream zu erzeugen, der
nur Sichtungen enthält, deren anzahl größer als 0 ist.
Übung 5.9 Öffnen Sie noch einmal das Projekt Musiksammlung aus Kapitel 4
und nehmen Sie an, dass jedes Track-Objekt der Sammlung eine Variable für
die Anzahl enthält, wie oft der Track abgespielt wurde. Schreiben Sie Pseu-
docode, um festzustellen, wie oft die Tracks des Künstlers Bill Broonzy ins-
gesamt abgespielt wurden.

Intermediäre Operationen erzeugen immer einen neuen Stream, auf den die nächste
Operation angewandt werden kann, während terminale Operationen einen Ergeb-
niswert liefern oder leer sind (wie forEach, das eine terminale Operation ist). Die
Dokumentation der Stream-Methode gibt Ihnen immer Auskunft darüber, ob eine
Operation intermediär oder terminal ist.
Wir werden im Folgenden die Java-Syntax der Methoden vorstellen, die diese
Funktionen implementieren. Hier treffen Streams und Lambda-Ausdrücke aufein-
ander: Der Parameter für diese Methoden ist ein Lambda-Ausdruck.

5.5.3 Die Methode filter


Wie bereits angesprochen, erzeugt die Methode filter eine Teilmenge des
ursprünglichen Streams. In der Regel wird diese Teilmenge aus denjenigen Elemen-
ten bestehen, die einer Bedingung genügen; das heißt, diese Elemente haben
irgendetwas, was wir bewahren möchten, sodass wir mehr mit diesen bestimmten
Elementen als mit den anderen weiterarbeiten wollen. Bei dem Musiksammlung-
Projekt möchten wir vielleicht alle Tracks eines bestimmten Künstlers finden. Beim
Tiermonitoring-Projekt möchten wir eventuell alle Sichtungen eines bestimmten
Tiers finden. Bei beiden wird über die jeweilige Liste iteriert und für jedes Element
getestet, um festzustellen, ob es einer bestimmten Anforderung entspricht. Falls die
geforderte Bedingung erfüllt wird, dann verarbeiten wir das Element auf irgendeine
Weise weiter.
Die filter-Methode eines Streams ist eine intermediäre Operation, die an ihren
Ausgabe-Stream nur diejenigen Elemente des Eingabe-Streams weitergibt, die eine

198
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams

gegebene Bedingung erfüllen. Die Methode bekommt einen Lambda-Ausdruck, der


ein Element erhält, und gibt einen boolean-Wert als Ergebnis zurück. (Ein Lambda-
Ausdruck, der ein Element entgegennimmt und true oder false zurückgibt, wird
Prädikat genannt.) Ist das Ergebnis true, dann wird das Element in den Ausgabe-
Stream aufgenommen, andernfalls nicht. Wenn wir beispielsweise nur diejenigen
Sichtung-Objekte selektieren möchten, die sich auf Elefanten beziehen, dann könnte
der Code dazu so aussehen:
[Link]()
.filter(s -> "Elefant".equals([Link]()))
Beachten Sie, dass wir zuerst einen Stream aus der Liste der Sichtungen erzeugen
müssen, um die filter-Funktion anwenden zu können. Es ist nicht nötig, den Para-
metertyp des Lambda-Ausdrucks anzugeben, da der Compiler weiß, dass Array-
List<Sichtung> einen Stream von Sichtung-Objekten liefern wird, und somit haben
die Parameter des Prädikats den Typ Sichtung.
Allgemeiner gesagt ist es sinnvoll, den Vorgang des Filterns zum Teil einer Methode
zu machen, die unterschiedliche Tierarten selektieren könnte. Wir können daher
die Methode sichtungenAusgebenUeber unseres ursprünglichen Projekts wie folgt als
Stream-basierte Version neu formulieren:
/**
* Gib die Details aller Sichtungen des angegebenen Tiers aus.
* @param tier die Tierart
*/
public void sichtungenAusgebenUeber(String tier)
{
[Link]()
.filter(s -> [Link]([Link]()))
.forEach(s -> [Link]([Link]()));
}
Es ist wichtig zu wissen, dass jede Stream-Operation ihren Eingabe-Stream unver-
ändert beibehält. Jede Operation erzeugt – basierend auf seiner Eingabe und der
ausgeführten Operation – einen neuen Stream, der an die nächste Operation in
der Folge weitergereicht wird. In diesem Fall ist die terminale Operation eine for-
Each-Methode, die als Ergebnis die Sichtungen liefert, die den vorhergehenden Fil-
terprozess „überlebt“ haben. Die forEach-Methode ist eine terminale Operation,
weil sie keinen weiteren Stream als Ergebnis hat – es ist eine void-Methode.

Übung 5.10 Schreiben Sie die Methode sichtungenAusgebenUeber in Ihrer


Tiermonitor-Klasse neu, sodass sie nun Streams und Lambda-Ausdrücke ver-
wendet, wie wir es oben gezeigt haben. Testen Sie die Klasse, um sicherzu-
stellen, dass das Projekt immer noch wie vorher funktioniert.
Übung 5.11 Schreiben Sie eine Methode in der Tiermonitor-Klasse, um Details
aller aufgezeichneten Sichtungen eines bestimmten Tags (tagID) auszugeben,
der als Parameter der Methode übergeben wird.

199
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Übung 5.12 Schreiben Sie eine Methode, die zwei filter-Aufrufe verwen-
det, um Details aller Sichtungen einer bestimmten Tierart an einem bestimm-
ten Tag auszugeben – die Methode bekommt den Tiernamen und die Tages-
ID als Parameter.
Übung 5.13 Hat die Reihenfolge der beiden filter-Aufrufe in Ihrer Lösung
der vorigen Aufgabe einen Einfluss? Begründen Sie Ihre Antwort.

5.5.4 Die Methode map


Die Methode map ist eine intermediäre Operation, die einen Stream mit unterschied-
lichen Objekten erzeugt, die möglicherweise auch unterschiedlichen Typs sind,
indem jeder der ursprünglichen Elemente auf ein neues Element im neuen Stream
abgebildet wird.
Dies wollen wir wieder am Beispiel illustrieren: Wenn wir die Details einer Sich-
tung ausgeben, sind wir eigentlich nur an der Zeichenkette interessiert, die von
der Methode gibDetails zurückgeliefert wird, und nicht am Sichtung-Objekt
selbst. Eine Alternative zum obigen Code (wo wir gibDetails() innerhalb von
[Link] aufgerufen haben) wäre, das Sichtung-Objekt zuerst auf den
String abzubilden, der von gibDetails kommt. Die terminale Operation kann
dann einfach diese Details ausgeben:
/**
* Gib alle Sichtungen des angegebenen Melders aus.
* @param melder die ID des Melders
*/
public void sichtungenAusgebenVon(int melder)
{
[Link]()
.filter(sichtung -> [Link]() == melder)
.map(sichtung -> [Link]())
.forEach(details -> [Link](details));
}
Dies ist äquivalent zu der bisherigen Version. Das andere Beispiel, das wir oben
erwähnt hatten – auf die Anzahl der gesichteten Tiere jeder Sichtung abzubilden,
sähe so aus:
[Link]()
.filter(sichtung -> [Link]() == melder)
.map(sichtung -> [Link]())
. ...
Dieses Codesegment würde als Ergebnis einen Stream von ganzen Zahlen liefern,
die dann weiter verarbeitet werden könnten.

200
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams

Übung 5.14 Schreiben Sie die Methode sichtungenAusgebenVon in Ihrem Pro-


jekt so wie oben beschrieben neu.
Übung 5.15 Schreiben Sie eine Methode, um die Anzahlen aller Sichtungen
einer bestimmten Tierart auszugeben. Ihre Methode sollte die map-Operation
als Teil der Pipeline einsetzen.
Übung 5.16 Wenn eine Pipeline eine filter- und eine map-Operation ent-
hält, ist die Reihenfolge der Operationen ausschlaggebend für das Endresul-
tat? Begründen Sie Ihre Antwort.
Übung 5.17 Schreiben Sie die Methode gefaehrdedAusgeben in Ihrem Projekt
neu, sodass sie nun Streams einsetzt, und testen Sie sie. (Um diese Methode
zu testen, kann es am einfachsten sein, eine Testmethode zu schreiben, die
eine ArrayList aus Tiernamen erzeugt und die gefaehrdedAusgeben-Methode
damit aufruft.)
Übung 5.18 Zusatzaufgabe. Die Methode sichtungenAusgebenVon von Tier-
monitor enthält die folgende terminale Operation:
forEach(details -> [Link](details));
Ersetzen Sie diese durch
forEach([Link]::println);
Achten Sie darauf, die korrekte Syntax zu benutzen. Verändert dies die
Funktion der Methode? Was können Sie aus Ihrer Antwort ableiten? Wenn
Sie sich nicht sicher sind, versuchen Sie herauszufinden, was :: bei der Ver-
wendung von Lambda-Ausdrücken in Java bedeutet. Wir werden uns dies in
den Fortgeschrittenenabschnitten von Kapitel 6 ansehen.

5.5.5 Die Methode reduce


Die intermediären Operationen, die wir bisher betrachtet haben, bekommen einen
Stream als Eingabe und geben einen Stream als Ausgabe zurück. Manchmal benö-
tigen wir jedoch eine Operation, die ihren Eingabe-Stream auf ein einziges Objekt
oder Wert „kollabieren“ lässt, und diese Rolle kommt der reduce-Methode als ter-
minaler Operation zu.
Der vollständige Code für das obige Beispiel – alle Tiere eines gegebenen Typs
auswählen, diese dann auf die Anzahl der Tiere in jeder Sichtung abbilden und
abschließend alle Zahlen addieren – lautet wie folgt:
public int gibAnzahl(String tier)
{
return [Link]()
.filter(sichtung> [Link]([Link]()))
.map(sichtung -> [Link]())
.reduce(0, (total, anzahl) -> total + anzahl);
}

201
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Wir sehen, dass die Parameter für die reduce-Methode ein wenig aufwendiger
aussehen als für die filter- und map-Methoden. Dafür gibt es zwei Gründe:
 Die reduce-Methode hat zwei Parameter. Der erste in unserem Beispiel ist 0, der
zweite ist ein Lambda-Ausdruck.
 Der hier verwendete Lambda-Ausdruck hat selbst wiederum zwei Parameter,
total und anzahl.

Um die Funktionsweise zu verstehen, ist es ganz hilfreich, wenn wir uns ansehen,
wie wir den Code schreiben müssten, wollten wir die Anzahlen auf die traditionelle
Art und Weise addieren. Die Schleife sähe dann so aus:
public int gibAnzahl(String tier)
{
int total =0;
for(Sichtung sichtung : sichtungen) {
if([Link]([Link]())) {
total = total + [Link]())) {
}
}
return total;
}
Die Berechnung der Summe umfasst die folgenden Schritte:
 Deklariere eine Variable für die Aufnahme des Endwerts und gib ihr den Anfangs-
wert 0.
 Iteriere über der Liste und stelle fest, welche Sichtungsdatensätze sich auf die
Tierart beziehen, an der wir interessiert sind (der Filter-Schritt).
 Erhalte die Anzahl der identifizierten Sichtungen (der Map-Schritt).
 Addiere die Anzahl zur Variablen.
 Gib die Summe zurück, die in der Variablen im Verlauf der Iteration summiert
wurde.
Der zentrale Punkt, der relevant für das Programmieren einer Stream-basierten Ver-
sion dieses Prozesses ist, ist zu erkennen, dass die Variable total als eine Art
„Akkumulator“ fungiert: Jedes Mal, wenn eine relevante Anzahl identifiziert wird,
wird diese zu dem Wert addiert, der bisher in total angelaufen ist. Dieser neue
Wert wird beim nächsten Schleifendurchlauf verwendet. Damit dies funktioniert,
muss total offensichtlich einen Anfangswert von 0 haben, auf den die erste
Anzahl addiert wird.
Die reduce-Methode hat zwei Parameter: einen Startwert und einen Lambda-
Ausdruck. Formal wird der Startwert Identität genannt. Es ist der Wert, mit dem
unsere laufende Variable total initialisiert wird. In unserem Code haben wir 0 für
diesen Parameter übergeben.
Der zweite Parameter ist ein Lambda-Ausdruck mit zwei Parametern: einer für
die laufende Variable total und einer für das aktuelle Element des Streams. Der
Lambda-Ausdruck in unserem Code lautet:
(total, anzahl) -> total + anzahl

202
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams

Der Rumpf des Lambda-Ausdrucks sollte den Wert liefern, der durch die Kombina-
tion der laufenden Variablen total mit dem Element resultiert. In unserem Beispiel
heißt „kombinieren“ einfach nur addieren. Die Anwendung dieser reduce-Methode
bewirkt dann, dass total mit null initialisiert wird, jedes Element des Streams hinzu-
addiert wird und zum Schluss total zurückgegeben wird.
Da es in unserem Code keine explizite Schleife mehr gibt, ist er anfangs vielleicht
ein wenig schwieriger zu verstehen, doch insgesamt werden dieselben Elemente
wie in der Version mit der forEach-Schleife verwendet.

Übung 5.19 Schreiben Sie Ihre gibAnzahl-Methode neu, indem Sie wie hier
gezeigt Streams verwenden.
Übung 5.20 Fügen Sie eine Methode zu Tiermonitor hinzu, die drei Parame-
ter hat, tier, melderID und tagID, und eine Anzahl zurückgibt, wie viele Sich-
tungen einer gegebenen Tierart von diesem Melder an einem bestimmten
Tag gemacht wurden.
Übung 5.21 Fügen Sie Tiermonitor eine Methode hinzu, die zwei Parameter
bekommt – melderID und tagID – und einen String zurückliefert, der die
Namen der gesichteten Tiere zurückgibt, die vom Melder am gegebenen Tag
gesehen wurden. Sie sollten nur Tiere aufnehmen, deren Sichtungszählung
größer als null ist, doch Sie müssen sich nicht um doppelt vorkommende
Namen kümmern, falls es mehrere nicht leere Sichtungsdatensätze von einem
bestimmten Tier gibt. Hinweis: Die Verwendung von reduce mit String-Ele-
menten und einem String-Ergebnis ist im Prinzip ganz ähnlich dem Vorgehen
mit ganzen Zahlen. Entscheiden Sie sich für die richtige Identität und formu-
lieren Sie einen Lambda-Ausdruck mit zwei Parametern, der die laufende
„Summe“ mit dem nächsten Element des Streams kombiniert.

5.5.6 Elemente aus einer Sammlung entfernen


Wir haben gesagt, dass die filter-Methode die zugrunde liegende Sammlung,
von der der Stream stammt, eigentlich nicht verändert. Prädikat-Lambda-Ausdrü-
cke machen es relativ leicht, alle Elemente aus der Sammlung zu entfernen, die
einer bestimmten Bedingung genügen. Nehmen wir zum Beispiel an, wir möch-
ten aus der sichtungen-Liste alle Sichtung-Datensätze löschen, deren Anzahl null
ist. Der folgende Code verwendet die removeIf-Methode der Sammlung:
/**
* Entferne aus der sichtungen-Liste alle Datensätze
* mit Anzahl null.
* @param melder die ID des Melders
*/
public void entferneNullAnzahl()
{
[Link](sichtung -> [Link]() == 0);
}

203
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Wieder einmal ist die Iteration implizit. Die removeIf-Methode übergibt jedes Ele-
ment der Liste nacheinander dem Prädikat-Lambda-Ausdruck, entfernt dann alle
Elemente, für die der Lambda-Ausdruck true liefert. Beachten Sie, dass dies eine
Methode der Sammlung ist (nicht eines Streams) und hiermit die ursprüngliche
Sammlung verändert wird.

Übung 5.22 Schreiben Sie die Methode entferneNullAnzahl neu, indem Sie
die removeIf-Methode verwenden, wie oben gezeigt.
Übung 5.23 Schreiben Sie eine Methode entferneMelder, der alle Daten-
sätze entfernt, die von einem gegebenen Melder stammen.

5.5.7 Weitere Stream-Methoden


Wir haben gesagt, dass viele der häufig vorkommenden Aufgaben mit diesen drei
Operationsarten – filter, map und reduce – bewerkstelligt werden können. In der
Praxis bietet Java viele weitere Stream-Methoden, einige davon werden wir später
noch kennenlernen. Die meisten sind allerdings lediglich Variationen der drei hier
vorgestellten Operationen, auch wenn sie andere Namen haben.
Die Klasse Stream hat beispielsweise Methoden namens count, findFirst und max –
alles Varianten einer reduce-Operation – sowie Methoden namens limit und
skip, die Beispiele einer filter-Operation sind.

Übung 5.24 Finden Sie die API-Dokumentation für Stream im Paket


[Link] und sehen Sie sich die Methoden count, findFirst und max
an. Diese können für jeden Stream aufgerufen werden.
Übung 5.25 Schreiben Sie eine Methode in der Klasse Tiermonitor, die
einen einzelnen Parameter, melderID, bekommt und eine Anzahl zurückgibt,
wie viele Sichtungsdatensätze von diesem Melder gemacht wurden. Ver-
wenden Sie dazu die Stream-Methode count.
Übung 5.26 Schreiben Sie eine Methode, die den Namen einer Tierart als Para-
meter hat und die größte Anzahl für dieses Tier in einem einzelnen Sichtung-
Datensatz zurückliefert.
Übung 5.27 Schreiben Sie eine Methode, die den Namen einer Tierart und
eine melderID als Parameter hat und das erste Sichtung-Objekt liefert, das in
der sichtungen-Sammlung für diese Kombination gespeichert ist.
Übung 5.28 Lesen Sie sich die Methoden limit und skip von Stream durch
und erfinden Sie selbst einige Methoden, um sie zu verwenden.

204
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams

Zusammenfassung
In diesem Kapitel haben wir einige neue Konzepte eingeführt, die relativ
fortgeschritten für dieses Stadium des Buchs sind, aber die sehr eng mit den
Konzepten verwandt sind, die wir in Kapitel 4 behandelt haben. Wir haben
uns einen funktionalen Stil zur Verarbeitung von Datenströmen angesehen.
Bei diesem Stil schreiben wir keine Schleifen, um die Elemente in einer
Sammlung zu verarbeiten. Stattdessen wenden wir eine Pipeline von Opera-
tionen auf einen Stream an, der aus einer Sammlung abgeleitet ist, um die
Folge in die gewünschte Form zu transformieren. Jede Operation in der
Pipeline definiert, wie ein einzelnes Element der Folge behandelt werden
soll. Die häufigsten Transformationen werden mittels der Methoden filter,
map und reduce durchgeführt.

Operationen in einer Pipeline haben häufig Lambda-Ausdrücke als Parameter,


um zu spezifizieren, was mit jedem Element der Folge getan werden soll. Ein
Lambda-Ausdruck ist eine anonyme Funktion, die keinen oder mehrere Para-
meter sowie einen Codeblock hat, der dieselbe Rolle wie ein Methodenrumpf
spielt.
Wenn Sie sich das Projekt Tiermonitoring-v1 ansehen, mit dem Sie gearbeitet
haben, dann haben Sie vielleicht bemerkt, dass es noch zwei Methoden gibt,
die forEach-Schleifen verwenden und die wir bisher noch nicht für die Ver-
wendung von Streams umgeschrieben haben. Dies sind Methoden, die neue
Sammlungen als Methodenergebnis zurückgeben; dieses Thema – eine neue
Sammlung aus einem Stream zu erzeugen – haben wir noch nicht behandelt.
Wir kommen darauf in Kapitel 6 zurück, wo wir sehen werden, wie eine neue
Sammlung aus der transformierten Folge gewonnen werden kann.

Zentrale Konzepte in diesem Kapitel: Funktionale Programmierung,


Lambda-Ausdruck, Stream, Pipeline, filter, map, reduce

205
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)

Zusammenfassung der Konzepte


 Lambda-Ausdruck Ein Lambda-Ausdruck ist ein Codesegment, das für
eine spätere Ausführung gespeichert werden kann.
 Funktionaler Stil Im funktionalen Stil der Verarbeitung von Sammlungen
entnehmen wir nicht jedes Element einzeln, um damit zu arbeiten. Stattdes-
sen übergeben wir der Sammlung ein Codesegment, das auf jedes Element
angewandt wird.
 Streams Streams vereinheitlichen die Verarbeitung von Elementen einer
Sammlung und anderer Datenmengen. Ein Stream bietet nützliche Metho-
den, um diese Datenmengen zu manipulieren.
 filter Wir können eine filter-Operation auf einen Stream anwenden,
um nur bestimmte Elemente auszuwählen.
 map Wir können einen Stream mithilfe der map-Operation auf einen ande-
ren Stream abbilden, wobei jedes Element des ursprünglichen Streams
durch ein neues Element ersetzt wird, das aus dem Original abgeleitet ist.
 reduce Wir können einen Stream mithilfe der reduce-Operation reduzie-
ren, das heißt, wir wenden eine Funktion an, die einen ganzen Stream
entgegennimmt und einen einzelnen Ergebniswert zurückgibt.
 Pipeline Eine Pipeline ist die Kombination von zwei oder mehreren Stream-
Funktionen in einer Kette, wobei jede Funktion der Reihe nach angewendet
wird.

Übung 5.29 Erstellen Sie eine Kopie des Projekts Musiksammlung-v5 aus
Kapitel 4. Schreiben Sie die Methoden alleTracksAusgeben und bestimmte-
TracksAusgeben der Klasse MusikSammlung neu, um Streams und Lambda-Aus-
drücke zu verwenden.

206
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

6 Bibliotheksklassen nutzen

Lernziele
Zentrale Konzepte in diesem Kapitel: Mit Bibliotheksklassen umgehen,
Dokumentation lesen, Dokumentation schreiben
Java-Konstrukte in diesem Kapitel: String, Random, HashMap, HashSet, Ite-
rator, static, final, Autoboxing, Wrapper-Klassen

In Kapitel 4 haben wir die Klasse ArrayList aus der Java-Klassenbibliothek vorge-
stellt. Wir haben gesehen, dass wir mit dieser Klasse Dinge tun können, die sonst
nur recht schwierig umzusetzen wären (in unserem Beispiel eine beliebige Anzahl
von Objekten speichern).
Dies war nur ein Beispiel für eine nützliche Klasse aus der Java-Bibliothek. Die Biblio-
thek besteht aus Tausenden von Klassen, von denen viele sehr nützlich für Ihre Arbeit
sein können, während Sie vermutlich viele andere niemals benötigen werden.
Für einen guten Java-Programmierer ist es wichtig, kompetent mit der Java-Biblio-
thek arbeiten zu können. Vor allem die Fähigkeit, die richtigen Klassen aus ihr aus-
zuwählen, ist unerlässlich. Sobald Sie mit der Bibliothek einigermaßen vertraut
sind, werden Sie merken, dass Sie vieles sehr viel einfacher und schneller mithilfe
der Bibliotheksklassen erledigen können als ohne sie. Der Umgang mit diesen Klas-
sen ist der Schwerpunkt in diesem Kapitel.
Bei den Elementen in der Bibliothek handelt es sich nicht um einen Satz von
beliebigen Klassen, die in keinem Zusammenhang stehen und die wir alle einzeln
lernen müssen, sondern sie werden oft nach bestimmte Kriterien zusammenge-
stellt, die auf Gemeinsamkeiten beruhen. Auch hier haben wir es wieder mit dem
Konzept der Abstraktion zu tun, die uns hilft, eine Fülle an Informationen zu ver-
arbeiten. Zu den wichtigsten Teilen der Bibliothek gehören die Sammlungen, zu
denen auch die ArrayList gehört. Wir werden in diesem Kapitel weitere Samm-
lungen besprechen und feststellen, dass sie viele Attribute gemeinsam haben,
sodass wir oft von den individuellen Details einer speziellen Sammlung abstrahie-
ren und über Sammlungsklassen im Allgemeinen sprechen können.
Wir werden neue Sammlungsklassen sowie einige andere nützliche Bibliotheks-
klassen vorstellen und diskutieren. Im Verlauf dieses Kapitels werden wir uns mit
der Erstellung einer einzelnen Anwendung beschäftigen (einem System für einen

207
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

technischen Kundendienst), die viele verschiedene Bibliotheksklassen benutzt.


Eine komplette Implementierung mit allen Entwurfsideen und Quelltexten, die
wir hier betrachten, samt einiger Zwischenversionen finden Sie in den Buchpro-
jekten. Obwohl Sie dadurch die komplette Lösung untersuchen können, schla-
gen wir vor, dass Sie die Übungen in diesem Kapitel Schritt für Schritt nachvoll-
ziehen. Nachdem wir einen kurzen Blick auf das komplette System geworfen
haben, werden wir von einer sehr einfachen Basisversion ausgehen und dann
nach und nach die komplette Lösung entwerfen und implementieren.
Die Anwendung benutzt mehrere Bibliotheksklassen und viele für Sie neue Techni-
ken, die alle für sich einige Aufmerksamkeit erfordern: Hash-Abbildungen, Mengen,
Zeichenkettenzerlegung und die weitere Verwendung von Zufallszahlen. Sie sollten
sich darauf einstellen, dass Sie dieses Kapitel nicht innerhalb eines Tages lesen und
verstehen können, sondern dass es etliche Abschnitte enthält, für die Sie einige
Tage intensiven Studiums investieren müssen. Dafür werden Sie am Ende, wenn Sie
alle Übungen programmiert haben, eine Vielzahl wichtiger Konzepte gelernt haben.

6.1 Die Dokumentation der


Bibliotheksklassen
Konzept Die Standardklassenbibliothek von Java ist riesig. Sie besteht aus Tausenden von
Klassen, jede mit vielen Methoden, mit oder ohne Parameter und mit oder ohne
Die Standard-
Ergebnistypen. Es ist unmöglich, sie alle mitsamt den Details ihrer Benutzung zu
klassenbiblio-
thek von Java lernen. Ein guter Java-Programmierer sollte stattdessen
enthält viele Klas-  einige der wichtigsten Klassen der Bibliothek und ihre Methoden namentlich
sen, die sehr nütz- kennen (ArrayList ist eine dieser wichtigen) und
lich sind. Es ist wich-
 wissen, wie er sich die anderen Klassen (mitsamt den Details ihrer Methoden
tig zu wissen, wie
man die Bibliothek
und Parameter) anlesen kann.
benutzen kann. In diesem Kapitel werden wir einige der wichtigsten Klassen der Bibliothek vorstel-
len, weitere werden in späteren Kapiteln behandelt werden. Aber viel wichtiger ist,
dass Sie auch lernen, wie Sie sich in der Bibliothek allein zurechtfinden können. Dies
wird Ihnen ermöglichen, sehr viel interessantere Programme zu schreiben. Glück-
licherweise ist die Java-Bibliothek recht gut dokumentiert. Diese Dokumentation liegt
im HTML-Format vor, sodass sie mit einem Webbrowser betrachtet werden kann.
Wir werden dies für unsere Erkundung der Bibliotheksklassen ausnutzen.
Lesen und Verstehen der Dokumentation ist der erste Teil unserer Einführung in die
Bibliotheksklassen. Wir gehen dann noch einen Schritt weiter, indem wir auch dis-
kutieren, wie Sie eigene Klassen so aufbereiten können, dass andere sie genauso
benutzen können, als wären sie Standardbibliotheksklassen. Dies ist für die reale
Softwareentwicklung sehr wichtig, bei der über einen längeren Zeitraum in Teams
an großen Projekten oder in der Softwarewartung gearbeitet wird.
Bei der Benutzung der Klasse ArrayList ist Ihnen möglicherweise aufgefallen, dass
wir sie einsetzen konnten, ohne einen Blick auf ihren Quelltext geworfen zu haben.
Wir haben nicht überprüft, wie sie implementiert ist. Dies war nicht notwendig, um
ihre Dienste in Anspruch zu nehmen. Wir kannten lediglich den Namen der Klasse,
die Namen der Methoden mit ihren Parametern und Ergebnistypen und wussten,

208
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.2 Das Kundendienstsystem

was die Methoden tun. Wir haben uns nicht dafür interessiert, wie sie ihre Aufga-
ben erfüllen. Dies ist typisch für die Benutzung von Bibliotheksklassen.
Dies gilt in gleicher Weise für andere Klassen in großen Softwareprojekten. Typischer-
weise kooperieren mehrere Entwickler in einem Projekt, indem sie an unterschiedli-
chen Teilen des Systems arbeiten. Jeder Entwickler sollte sich auf seinen Bereich kon-
zentrieren und nicht die Details der anderen Teile verstehen müssen (wir haben dies
bereits in Abschnitt 3.2 diskutiert, als wir über Abstraktion und Modularisierung
gesprochen haben). Letztlich sollte jeder Entwickler die Klassen der anderen Team-
mitglieder so benutzen können, als ob sie Bibliotheksklassen wären: also ihre Dienste
in Anspruch nehmen, ohne die Details der Realisierung kennen zu müssen.
Damit dies funktioniert, muss jedes Teammitglied seine Klassen in der Art doku-
mentieren, wie die Standardbibliothek dokumentiert ist. Diese Dokumentation
ermöglicht es dann, die Klasse zu benutzen, ohne den Quelltext zu lesen. Auch
dieses Thema werden wir in diesem Kapitel besprechen.

6.2 Das Kundendienstsystem


Wie immer werden wir diese Themen anhand eines Beispiels diskutieren. Diesmal
verwenden wir das Kundendienstsystem. Sie finden das Projekt im Begleitmate-
rial unter dem Namen Technischer-Kundendienst1.
Das Kundendienstsystem ist ein Programm, das den technischen Kundendienst
für die Kunden der fiktiven Softwarefirma SeltsamSoft abwickeln soll. Noch vor
einiger Zeit hatte SeltsamSoft eine Abteilung für den technischen Kundendienst,
in der Mitarbeiter an Telefonen saßen. Kunden konnten dort anrufen, um Rat und
Hilfe bei technischen Problemen mit den Softwareprodukten von SeltsamSoft zu
bekommen. In letzter Zeit ist das Geschäft allerdings nicht mehr sehr gut gelaufen
und SeltsamSoft hat aus Kostengründen beschlossen, die Abteilung aufzulösen.
Sie wollen nun ein Kundendienstsystem entwickeln, das den Eindruck vermitteln
soll, dass noch immer Kundendienst angeboten wird. Das System soll die Antwor-
ten simulieren, die ein Mitarbeiter im technischen Kundendienst geben könnte.
Die Kunden können mit dem System online kommunizieren.

6.2.1 Das Kundendienstsystem erkunden

Übung 6.1 Öffnen und starten Sie das Projekt Technischer-Kundendienst-


komplett. Sie starten es, indem Sie ein Objekt der Klasse Kundendienstsystem
erzeugen und seine starten-Methode aufrufen. Geben Sie einige Fragen zu
Problemen ein, die Sie mit Ihrer Software haben könnten, um das System aus-
zuprobieren. Beobachten Sie, wie es sich verhält. Tippen Sie „ade“ (es ist ein
schwäbisches System!), wenn Sie fertig sind. Sie brauchen an dieser Stelle
noch nicht den Quelltext zu untersuchen. Dieses Projekt enthält die komplette
Lösung, wie wir sie am Ende dieses Kapitels erstellt haben wollen. Der Zweck
dieser Übung ist lediglich, dass Sie einen Eindruck vom Zielsystem bekommen.

209
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Eliza
Die Idee für das Kundendienstsystem stammt von dem bahnbrechenden
Programm der Künstlichen Intelligenz, Eliza, das von Joseph Weizenbaum
am Massachusetts Institute of Technology in den 1960er Jahren entwickelt
wurde. Sie können mehr darüber erfahren, indem Sie im Web nach „Eliza“
und „Weizenbaum“ suchen.

Wir werden nun unsere ausführlichere Untersuchung beginnen, indem wir das
Projekt Technischer-Kundendienst1 öffnen. Es enthält eine erste rudimentäre
Implementierung unseres Systems. Wir werden es im Laufe dieses Kapitels aus-
bauen. Auf diese Weise kommen wir zu einem besseren Verständnis des Gesamt-
systems, als wenn wir nur die komplette Lösung betrachten würden.
Aus Übung 6.1 konnten Sie ersehen, dass das System im Kern einen Dialog mit
dem Benutzer führt. Der Benutzer kann eine Frage eingeben und das System ant-
wortet. Versuchen Sie nun das Gleiche mit unserem Prototyp des Projekts, Tech-
nischer-Kundendienst1.
In der kompletten Version schafft es das System, einigermaßen abwechslungsreiche
Antworten zu geben. Manchmal scheinen sie sogar sinnvoll zu sein! In dieser ersten
Version, die wir weiterentwickeln werden, sind die Reaktionen sehr viel einge-
schränkter (Abbildung 6.1). Sie werden schnell feststellen, dass es immer die gleiche
Antwort gibt:
Das klingt interessant. Erzählen Sie mehr ...

Abbildung 6.1
Ein erster Dialog mit
dem Kundendienst-
system.

210
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.2 Das Kundendienstsystem

Das ist tatsächlich überhaupt nicht interessant und auch nicht sehr überzeugend,
wenn eigentlich der Eindruck entstehen soll, dass am anderen Ende der Leitung ein
Mitarbeiter des technischen Kundendienstes sitzt. Wir werden bald versuchen,
diese Situation zu verbessern. Allerdings sollten wir zuerst etwas genauer unter-
suchen, was wir bisher vorliegen haben.
Das Projektdiagramm zeigt uns drei Klassen: Kundendienstsystem, Eingabeleser und
Beantworter (Abbildung 6.2). Kundendienstsystem ist die Hauptklasse, die einen Ein-
gabeleser für Eingaben von der Tastatur benutzt und einen Beantworter, der Ant-
worten erzeugt.

Abbildung 6.2
Das Klassendiagramm
von Technischer-
Kundendienst.

Untersuchen Sie den Eingabeleser genauer, indem Sie ein Objekt dieser Klasse
erzeugen und dann die Objektmethoden betrachten. Sie werden sehen, dass
es nur eine Methode anbietet, gibEingabe, die eine Zeichenkette zurückliefert.
Probieren Sie sie aus. Diese Methode lässt auf der Konsole eine Zeile eingeben
und liefert Ihre Eingabe als Aufrufergebnis zurück. Wir werden vorläufig nicht
im Detail untersuchen, wie dies funktioniert, sondern lediglich festhalten, dass
ein Eingabeleser eine Methode gibEingabe hat, die eine Zeichenkette zurücklie-
fert.
Machen Sie nun das Gleiche mit der Klasse Beantworter. Sie werden feststellen,
dass sie eine Methode generiereAntwort hat, die immer die Zeichenkette "Das
klingt interessant. Erzählen Sie mehr …" liefert. Das erklärt, was wir vorher im
Dialog beobachtet haben.
Lassen Sie uns nun einen genaueren Blick auf die Klasse Kundendienstsystem werfen.

6.2.2 Den Quelltext untersuchen


Der komplette Quelltext der Klasse Kundendienstsystem ist in Listing 6.1 zu sehen.
Listing 6.2 zeigt den Quelltext der Klasse Beantworter.

211
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Listing 6.1
Der Quelltext
der Klasse Kunden-
dienstsystem.

212
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.2 Das Kundendienstsystem

Listing 6.2
Der Quelltext der
Klasse Beantworter.

Ein Blick auf Listing 6.2 zeigt uns, dass die Klasse Beantworter trivial ist. Sie hat
nur eine Methode und diese liefert immer dieselbe Zeichenkette. Das ist etwas,
was wir später verbessern werden. Vorläufig konzentrieren wir uns auf die Klasse
Kundendienstsystem.

Kundendienstsystem deklariert zwei Datenfelder, um einen Eingabeleser und einen


Beantworter halten zu können, und initialisiert diese Datenfelder mit entsprechen-
den Objekten, die im Konstruktor erzeugt werden.
Am Ende hat die Klasse zwei Methoden, willkommenstextAusgeben und abschieds-
textAusgeben. Diese geben einfach kurze Texte aus – eine Begrüßung und einen
Abschiedstext.
Der interessanteste Abschnitt im Quelltext ist die Methode in der Mitte: starten.
Wir werden diese Methode genauer untersuchen.
Zu Anfang der Methode erfolgt ein Aufruf von willkommenstextAusgeben, am Ende
steht ein Aufruf von abschiedstextAusgeben. Diese Aufrufe sorgen für die entspre-
chenden Ausgaben zu den geeigneten Zeitpunkten. Der Rest der Methode besteht
aus der Deklaration einer booleschen Variablen und einer while-Schleife. Die Struktur
ist folgendermaßen:
boolean fertig = false;
while(!fertig) {
tue etwas
if(Ende-Bedingung) {
fertig = true;
}
else {
tue etwas anderes
}
}
Dieses Programmiermuster ist eine Abwandlung der while-Schleife, die wir in
Abschnitt 4.10 diskutiert haben. Wir benutzen fertig als einen Signalgeber, der

213
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

den Wert true bekommt, wenn wir die Schleife beenden wollen (und damit das
ganze Programm). Zu Anfang stellen wir sicher, dass fertig den Wert false hat.
(Erinnern Sie sich daran, dass ein Ausrufezeichen den Nicht-Operator darstellt!)
Der Hauptteil der Schleife – der Teil, der wiederholt ausgeführt wird, solange wir
noch nicht fertig sind – besteht aus drei Anweisungen, wenn wir die Prüfung der
Ende-Bedingung auslassen:
String eingabe = [Link]();
...
String antwort = [Link]();
[Link](antwort);
In dieser Schleife wird somit wiederholt
 eine Benutzereingabe gelesen
 der Beantworter um eine Antwort gefragt
 diese Antwort ausgegeben
(Sie werden sicherlich bemerkt haben, dass die Antwort überhaupt nicht von der
Eingabe abhängig ist! Das ist etwas, was wir ganz sicher ändern werden.)
Der letzte zu untersuchende Teil ist die Prüfung der Ende-Bedingung. Die Idee ist,
dass das Programm enden soll, sobald der Benutzer „ade“ eingegeben hat. Die
entsprechende Stelle im Quelltext lautet:
String eingabe = [Link]();
if([Link]("ade")) {
fertig = true;
}
Wenn Sie diese Abschnitte einzeln verstanden haben, dann sollten Sie noch einmal
einen Blick auf die Methode starten in Listing 6.1 werfen, um zu sehen, ob Sie
auch das Zusammenspiel verstehen.
In dem letzten Quelltextabschnitt, den wir oben betrachtet haben, wird eine
Methode startsWith benutzt. Da diese Methode an der Variablen eingabe aufge-
rufen wird, die auf ein Objekt vom Typ String verweist, muss dies wohl eine
Methode der Klasse String sein. Aber was tut diese Methode? Und wie finden
wir das heraus?
Wir könnten einfach durch Deuten des Namens startsWith (englisch für „beginnt
mit“) vermuten, dass sie prüft, ob die Zeichenkette mit dem Wort „ade“ beginnt.
Wir können dies durch Probieren belegen. Starten Sie das Kundendienstsystem
erneut und tippen Sie „ade, ade“ oder „adele“. Sie werden feststellen, dass beide
Versionen das System beenden. Beachten Sie aber, dass eine Eingabe wie „Ade“
oder „ ade“ – mit einem Großbuchstaben oder einem Leerzeichen am Anfang –
nicht als beginnend mit „ade“ erkannt wird. Das könnte etwas lästig für den
Benutzer sein, aber es wird sich herausstellen, dass wir dies recht leicht beheben
können, sobald wir etwas mehr über die Klasse String wissen.
Wie bekommen wir mehr Informationen über die Methode startsWith oder andere
Methoden der Klasse String?

214
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.3 Die Klassendokumentation lesen

6.3 Die Klassendokumentation lesen


Die Klasse String ist eine Klasse aus der Java-Standardbibliothek. Wir können mehr Konzept
über sie erfahren, indem wir die Bibliotheksdokumentation der Klasse String nach-
Die Dokumen-
lesen.
tation der
Wählen Sie dazu den Eintrag JAVA KLASSENBIBLIOTHEKEN aus dem Menü HILFE in Java-Standard-
BlueJ. Dies öffnet einen Webbrowser, der die Hauptseite der Java-API-Dokumenta- bibliothek zeigt
Informationen über
tion (API steht für Application Programmer Interface, die Schnittstelle für Anwen-
alle Klassen in der
dungsprogrammierer) anzeigt.1 Bibliothek. Die
Der Webbrowser zeigt drei Bereiche. In der linken oberen Ecke sehen Sie eine Benutzung dieser
Dokumentation ist
Liste der Pakete. Darunter sehen Sie eine Liste aller Klassen in der Java-Bibliothek.
unerlässlich für
Der große Bereich auf der rechten Seite zeigt Detailinformationen über das selek- einen vernünftigen
tierte Paket oder die selektierte Klasse. Gebrauch der
Bibliotheksklassen.
Suchen Sie in der Liste auf der linken Seite nach der Klasse String und selektieren
Sie sie. Der Bereich auf der rechten Seite zeigt daraufhin die Dokumentation der
Klasse String an (Abbildung 6.3).

Abbildung 6.3
Dokumentation
der Java-Standard-
bibliothek.

1 Per Voreinstellung greift diese Funktion auf die Dokumentation im Internet zu. Dies funktio-
niert nicht, wenn Ihr Computer keinen Netzzugang hat. BlueJ kann so konfiguriert werden,
dass es auf eine lokale Kopie der Java-API-Dokumentation zugreift. Dies ist zu empfehlen, da
es die Zugriffe beschleunigt und ohne einen Netzzugang funktioniert. Lesen Sie Anhang A für
weitere Details.

215
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Übung 6.2 Untersuchen Sie die Dokumentation von String. Sehen Sie sich
dann die Dokumentation einiger anderer Klassen an. Wie ist die Dokumen-
tation strukturiert? Welche Abschnitte haben alle Klassenbeschreibungen?
Welchem Zweck dienen sie?
Übung 6.3 Suchen Sie die Methode startsWith in der Dokumentation von
String. Es gibt sie in zwei Versionen. Beschreiben Sie mit eigenen Worten,
was sie tun und welche Unterschiede zwischen ihnen bestehen.
Übung 6.4 Gibt es eine Methode in der Klasse String, die testet, ob eine Zei-
chenkette mit einem gegebenen Suffix endet? Wenn ja, wie heißt sie und
welche Parameter und welchen Ergebnistyp hat sie?
Übung 6.5 Gibt es eine Methode in der String-Klasse, die die Anzahl der Zei-
chen in einer Zeichenkette liefert? Wenn ja, wie heißt sie und welche Parame-
ter hat sie?
Übung 6.6 Wenn Sie Methoden für die beiden vorigen Aufgaben gefunden
haben, wie haben Sie dies getan? Ist es einfach oder schwierig, nach Metho-
den zu suchen? Warum?

6.3.1 Schnittstellen versus Implementierungen


Konzept Sie werden feststellen, dass die Dokumentation unterschiedliche Informationen lie-
fert. Sie enthält, neben anderen Dingen:
Die Schnitt-
stelle einer Klasse  den Namen der Klasse
beschreibt, was  eine allgemeine Beschreibung des Zwecks der Klasse
eine Klasse leistet  eine Liste der Konstruktoren und Methoden der Klasse
und wie sie benutzt
werden kann, ohne  die Parameter und Ergebnistypen für jeden Konstruktor und jede Methode
dass ihre Imple-  eine Beschreibung des Zwecks jedes Konstruktors und jeder Methode
mentierung sicht-
bar wird. Diese Informationen werden zusammengefasst als die Schnittstelle der Klasse
bezeichnet. Beachten Sie, dass die Schnittstelle nicht den Quelltext zeigt, der die
Klasse implementiert. Wenn eine Klasse gut beschrieben ist (genauer, wenn ihre
Schnittstelle gut beschrieben ist), dann braucht ein Programmierer den Quelltext
der Klasse nicht zu sehen, um die Klasse zu benutzen. Die Schnittstelle liefert alle
notwendigen Informationen. Dies ist wieder einmal ein Fall von Abstraktion in
Aktion.

Konzept Der Quelltext hinter den Kulissen, der die Klasse ihren Dienst erfüllen lässt, wird als
die Implementierung der Klasse bezeichnet. Normalerweise arbeitet ein Program-
Der komplette
mierer an der Implementierung von genau einer Klasse, während er einige andere
Quelltext, der eine
Klasse definiert, Klassen über ihre Schnittstellen benutzt.
wird als die Die Unterscheidung zwischen der Schnittstelle und der Implementierung einer
Implementie-
Klasse ist ein sehr wichtiges Konzept, das uns in diesem und den folgenden Kapi-
rung dieser Klasse
bezeichnet. teln immer wieder begegnen wird.

216
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.3 Die Klassendokumentation lesen

Hinweis
Der Begriff Schnittstelle hat mehrere Bedeutungen im Bereich der Program-
mierung mit Java. Er wird benutzt, um den öffentlich sichtbaren Anteil einer
Klasse zu beschreiben (so haben wir ihn gerade kennengelernt), hat aber
auch noch andere Bedeutungen: Die Benutzungsschnittstelle (oft eine grafi-
sche Benutzungsschnittstelle) wird auch häufig einfach nur als die Schnitt-
stelle bezeichnet. Weiterhin verfügt Java noch über ein Sprachkonstrukt
namens Interface, das ins Deutsche übersetzt auch Schnittstelle heißt (die-
ses wird in Kapitel 12 besprochen). Wenn wir über das Java-Sprachkonst-
rukt reden, werden wir den englischen Begriff verwenden.
Es ist wichtig, die unterschiedlichen Bedeutungen des Begriffs Schnittstelle
in den verschiedenen Zusammenhängen auseinanderhalten zu können.

Der Begriff der Schnittstelle ist auch auf einzelne Methoden anwendbar. Beispiels-
weise zeigt uns die Dokumentation der Klasse String die Schnittstelle der Methode
length:
public int length()

Returns the length of this string. The length is equal to the number of
Unicode code units in the string.

Specified by:

length in interface CharSequence

Returns:

the length of the sequence of characters represented by this object.


Die Schnittstelle einer Methode besteht aus dem Kopf der Methode und einem
Kommentar (der hier kursiv angegeben ist). Zum Kopf einer Methode gehören
(in dieser Reihenfolge):
 ein Zugriffsmodifikator (hier public) – wir werden diesen noch diskutieren
 der Ergebnistyp der Methode (hier int)
 der Name der Methode
 eine Liste von Parametern (die in diesem Beispiel leer ist); der Name und die
Parameter werden zusammen auch als die Signatur der Methode bezeichnet.
Die Schnittstelle enthält all die Informationen, die wir für die Benutzung dieser
Methode benötigen.

6.3.2 Methoden von Bibliotheksklassen benutzen


Zurück zu unserem Kundendienstsystem. Wir wollen nun die Verarbeitung der Ein-
gabe etwas verbessern. Wir haben in der obigen Diskussion festgestellt, dass sich
das System nicht sehr tolerant verhält. Wenn wir beispielsweise „Ade“ oder „ade“

217
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

statt „ade“ eintippen, wird das Wort nicht erkannt. Eine Sache, die wir beachten
müssen, ist, dass ein String-Objekt nach seiner Erzeugung nicht mehr geändert wer-
den kann. Wir wollen dies ändern, indem wir den vom Benutzer eingegebenen Text
so anpassen, dass all diese Variationen als „ade“ erkannt werden.
Die Dokumentation der Klasse String gibt uns die Auskunft, dass sie über eine
Methode trim verfügt, mit der Leerzeichen zu Beginn und am Ende einer Zeichen-
kette abgeschnitten werden können. Wir können diese Methode verwenden, um
den zweiten Problemfall zu behandeln.

Übung 6.7 Finden Sie die Methode trim in der Dokumentation der Klasse
String. Schreiben Sie den Kopf dieser Methode auf. Schreiben Sie ein Bei-
spiel für den Aufruf dieser Methode an einer String-Variablen text auf.

Konzept Ein wichtiges Merkmal von String-Objekten ist, dass sie unveränderlich sind – das
heißt, sie können nicht mehr geändert werden, nachdem sie erzeugt wurden.
Ein Objekt wird als
Beachten Sie vor allem, dass die Methode trim beispielsweise eine neue Zeichen-
unveränder-
lich bezeichnet, kette zurückliefert und nicht die ursprüngliche Zeichenkette ändert. Deshalb soll-
wenn sein Inhalt ten Sie den nachfolgenden Einschub mit den Fallstricken sorgfältig lesen.
oder Zustand nicht
mehr geändert
werden kann, Fallstrick
nachdem
es erzeugt wurde. Häufig wird in Java der Fehler gemacht, eine Zeichenkette verändern zu wol-
String ist ein len – zum Beispiel durch folgende Anweisung
Beispiel für eine
Klasse, die unver- [Link]();
änderliche Objekte Dies ist nicht korrekt (Zeichenketten können nicht geändert werden), erzeugt
definiert. aber leider keinen Fehler. Die Anweisung hat einfach keine Auswirkung und
die Eingabezeichenkette bleibt unverändert.
Die Methode toUpperCase sowie andere String-Methoden ändern nicht die
ursprüngliche Zeichenkette, sondern liefern stattdessen eine neue Zeichen-
kette zurück, die gleich der alten ist einschließlich einiger Änderungen (hier
mit den Zeichen, die in Großbuchstaben geändert wurden). Wenn wir wol-
len, dass unsere Variable eingabe geändert wird, dann müssen wir dieses
neue Objekt wie folgt wieder der Variablen zuweisen (wobei wir uns der
alten Variablen entledigen):
eingabe = [Link]();
Das neue Objekt könnte auch einer anderen Variablen zugewiesen oder ander-
weitig verarbeitet werden.

Nachdem wir uns die Schnittstelle der Methode trim angesehen haben, wissen wir,
dass wir die Leerzeichen aus der Eingabe mit folgendem Aufruf entfernen können:
eingabe = [Link]();

218
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.3 Die Klassendokumentation lesen

Diese Anweisung veranlasst das String-Objekt, auf das eingabe verweist, eine neue
Zeichenkette zu erzeugen, in der führende und anhängende Leerzeichen entfernt
sind. Diese neue Zeichenkette wird dann in der Variablen eingabe gespeichert, da
wir die alte nicht mehr benötigen. Somit verweist eingabe nach Ausführung dieser
Zeile auf eine Zeichenkette, die an beiden Enden keine Leerzeichen mehr hat.
Wir können diese Zeile nun in unseren Quelltext einfügen, sodass er folgender-
maßen aussieht:
String eingabe = [Link]();
eingabe = [Link]();

if([Link]("ade")) {
fertig = true;
}
else {
... Quelltext hier ausgelassen
}
Die ersten beiden Zeilen können auch zu einer zusammengefasst werden:
String eingabe = [Link]().trim();
Der Effekt dieser Zeile ist identisch mit den ersten beiden Zeilen oben. Die rechte
Seite sollte gelesen werden, als wäre sie folgendermaßen geklammert:
([Link]()).trim();
Welche Version Sie bevorzugen, ist überwiegend eine Frage des Geschmacks. Die
Entscheidung sollte vor allem in Hinsicht auf Lesbarkeit getroffen werden: Benut-
zen Sie die Version, die Sie einfacher zu lesen und zu verstehen finden. Program-
mieranfänger bevorzugen häufig die zweizeilige Version, während erfahrene Pro-
grammierer sich an die einzeilige Version gewöhnt haben.

Übung 6.8 Implementieren Sie diese Verbesserung in Ihrer Version des Projekts
Technischer-Kundendienst1. Testen Sie, ob anschließend Leerzeichen vor oder
nach „ade“ toleriert werden.

Wir haben nun zwar das Problem mit den Leerzeichen um die Eingabe gelöst,
aber das Problem mit den Großbuchstaben besteht weiterhin. Eine weitere Unter-
suchung der Dokumentation der Klasse String führt jedoch zu einer möglichen
Lösung, denn dort ist eine Methode toLowerCase beschrieben.

Übung 6.9 Verbessern Sie die Implementierung der Klasse Kundendienstsys-


tem im Projekt Technischer-Kundendienst1 so, dass die Groß-/Kleinschreibung
bei der Eingabe ignoriert wird. Benutzen Sie dazu die Methode toLowerCase
der Klasse String. Beachten Sie auch hier wieder, dass diese Methode nicht
die Zeichenkette ändert, an der sie aufgerufen wird, sondern dass der Aufruf
zur Erzeugung einer neuen mit leicht geändertem Inhalt führt.

219
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

6.3.3 Zeichenketten auf Gleichheit prüfen


Eine alternative Lösung wäre gewesen, zu prüfen, ob die eingegebene Zeichenkette
die Zeichenkette „ade“ tatsächlich ist, und nicht, ob sie lediglich mit ade beginnt.
Ein (falscher!) Versuch, dies zu formulieren, könnte so aussehen:
if (eingabe == "ade") { // funktioniert nicht immer!
...
}
Das Problem ist, dass es mehrere String-Objekte geben kann, die alle denselben
Text repräsentieren. Zwei String-Objekte könnten beispielsweise beide aus den Zei-
chen „ade“ bestehen. Der Gleichheits-Operator (==) überprüft, ob seine beiden
Operanden sich auf dasselbe Objekt beziehen, nicht, ob sie den gleichen Wert
haben! Dies ist ein wichtiger Unterschied.
In unserem Beispiel wollen wir nicht wissen, ob die Variable eingabe und die Zeichen-
kettenkonstante „ade“ dasselbe Objekt bezeichnen, sondern, ob sie den gleichen
Wert haben. Die Benutzung des Operators == ist somit falsch. Sie könnte false lie-
fern, auch wenn der Wert der Variablen eingabe „ade“ ist.2
Die Lösung liegt in der Verwendung der Methode equals, die in der Klasse String
definiert ist. Diese Methode überprüft, ob zwei String-Objekte den gleichen Inhalt
haben. Der korrekte Quelltext sieht also so aus:
if([Link]("ade")) {
...
}
Dies kann natürlich auch mit den Methoden trim und toLowerCase kombiniert
werden.

Fallstrick
Das Vergleichen von String-Objekten mit dem Operator == kann zu uner-
warteten Ergebnissen führen. Als Grundregel gilt, dass Zeichenketten prak-
tisch immer mit equals statt mit == verglichen werden sollten.

Übung 6.10 Finden Sie die Methode equals in der Dokumentation der
Klasse String. Welchen Ergebnistyp hat diese Methode?
Übung 6.11 Ändern Sie Ihre Implementierung so, dass sie die Methode
equals anstelle von startsWith verwendet.

2 Leider führt aufgrund von Javas String-Implementierung der Vergleich zweier verschiedener
String-Objekte gleichen Inhalts mit == oft irreführenderweise zur „korrekten“ Antwort. Trotz-
dem sollten Sie zum Vergleichen der Inhalte zweier String-Objekte niemals == verwenden.

220
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.4 Zufälliges Verhalten einbringen

6.4 Zufälliges Verhalten einbringen


Bisher haben wir einige kleine Verbesserungen am Projekt Technischer-Kunden-
dienst vorgenommen, aber insgesamt ist das System noch sehr simpel. Eines der
Hauptprobleme ist, dass es immer dieselbe Antwort liefert, unabhängig von der
Eingabe durch den Benutzer. Wir werden dies nun verbessern, indem wir einige
plausible Antworten definieren. Aus diesen Antworten lassen wir zufällig eine
auswählen, wenn eine Anfrage gestellt wird. Dazu müssen wir die Klasse Beant-
worter in unserem Projekt erweitern.
Wir gehen folgendermaßen vor: Wir legen einige Antworten in einer ArrayList
ab, generieren eine Zufallszahl und benutzen diese als Index in der Antwortliste,
um eine der Antworten auszuwählen. In dieser Version werden wir die Antwort
noch nicht von der Eingabe des Benutzers abhängig machen (das kommt später),
aber zumindest sind die Antworten dann etwas abwechslungsreicher und sehen
deutlich besser aus.
Dazu müssen wir herausfinden, wie man Zufallszahlen generieren kann.

Zufall und Pseudozufall


Die Erzeugung von Zufallszahlen auf einem Computer ist nicht so einfach, wie
man es vermuten könnte. Da Computer nach wohldefinierten deterministi-
schen Verfahren arbeiten, die darauf beruhen, dass alle Berechnungen vor-
hersagbar und wiederholbar sind, gibt es eigentlich keinen Platz für zufälliges
Verhalten in Computern.
Wissenschaftler haben im Laufe der Zeit viele Algorithmen vorgeschlagen, mit
denen zufällig wirkende Folgen von Zahlen erzeugt werden können. Diese
Zahlen sind tatsächlich nicht zufällig, sondern folgen lediglich sehr komplizier-
ten Regeln. Deshalb werden sie auch Pseudozufallszahlen genannt.
In einer Sprache wie Java wurde die Erzeugung von Pseudozufallszahlen
glücklicherweise durch eine Bibliotheksklasse implementiert, sodass wir, um
Zufallszahlen zu bekommen, lediglich diese Klasse benutzen müssen.
Wenn Sie mehr zu diesem Thema lesen wollen, dann suchen Sie im Web nach
den Schlüsselwörtern „Zufallszahlen Computer“ oder auf Englisch „pseudo
random numbers“.

6.4.1 Die Klasse Random


Die Klassenbibliothek von Java enthält eine Klasse Random (englisch für „Zufall“), die
für unser Projekt nützlich ist.

Übung 6.12 Finden Sie die Klasse Random in der Dokumentation der Java-
Bibliothek. In welchem Paket ist sie zu finden? Was tut sie? Wie erzeugen
Sie eine Instanz? Wie generieren Sie eine Zufallszahl? Möglicherweise ver-
stehen Sie nicht alle Erläuterungen in der Dokumentation. Versuchen Sie
nur die Dinge herauszufinden, die Sie unmittelbar benötigen.

221
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Übung 6.13 Versuchen Sie, einen Quelltextabschnitt auf ein Blatt Papier zu
schreiben, der Ihnen mithilfe dieser Klasse eine Zufallszahl generiert.

Um eine Zufallszahl zu erzeugen, müssen wir


 eine Instanz der Klasse Random erzeugen
 an dieser Instanz eine Methode aufrufen, um eine Zufallszahl zu erhalten
Ein Blick auf die Dokumentation sagt uns, dass es mehrere Methoden next-
Irgendetwas gibt, die Zufallszahlen verschiedener Typen erzeugen. Diejenige zur
Erzeugung von ganzen Zufallszahlen heißt nextInt.
Der folgende Quelltextabschnitt zeigt, wie eine ganze Zufallszahl generiert und
ausgegeben werden kann:
Random zufallsgenerator;
zufallsgenerator = new Random();
int index = [Link]();
[Link](index);
In diesem Abschnitt wird eine neue Instanz der Klasse Random erzeugt und in der
Variablen zufallsgenerator abgelegt. Anschließend wird an diesem Objekt die
Methode nextInt aufgerufen, um eine Zufallszahl zu erhalten; diese wird in der
Variablen index abgelegt und schließlich ausgegeben.

Übung 6.14 Schreiben Sie Quelltext (in BlueJ), der die Erzeugung von Zufalls-
zahlen testet. Definieren Sie dazu eine Klasse ZufallszahlenTester. Sie können
diese Klasse im Projekt Technischer-Kundendienst anlegen oder ein eigenes
Projekt dafür anlegen. Implementieren Sie in der Klasse zwei Methoden: eine-
ZufallszahlAusgeben (die eine Zufallszahl ausgeben soll) und zufallszahlen-
Ausgeben(int anzahl) (die einen Parameter bekommt, der angibt, wie viele
Zufallszahlen generiert und ausgegeben werden sollen).

Beachten Sie, dass Ihre Klasse nur eine Instanz der Klasse Random erzeugen und
diese in einem Datenfeld ablegen sollte. Erzeugen Sie nicht für jede Zufallszahl
eine neue Instanz von Random.

6.4.2 Zufallszahlen mit eingeschränktem


Wertebereich
Die Zufallszahlen, die wir bisher gesehen haben, liegen im Wertebereich der gan-
zen Zahlen von Java (von –2147483648 bis 2147483647). Das ist zum Experimen-
tieren ganz okay, aber nicht sehr nützlich. Sehr häufig benötigen wir Zufallszahlen
aus einem definierten Wertebereich.
Die Klasse Random biete eine Methode an, die dies unterstützt. Sie heißt ebenfalls
nextInt, hat jedoch einen Parameter, mit dem der gewünschte Wertebereich ange-
geben werden kann.

222
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.4 Zufälliges Verhalten einbringen

Übung 6.15 Finden Sie die Methode nextInt in der Klasse Random, mit der Sie
den Wertebereich der Zufallszahlen angeben können. Aus welchem Wertebe-
reich stammen die erzeugten Zahlen, wenn Sie die Methode mit dem Parame-
ter 100 aufrufen?
Übung 6.16 Schreiben Sie eine Methode wuerfeln in Ihrer Klasse Zufalls-
zahlenTester, die Werte von 1 bis einschließlich 6 zurückliefert.
Übung 6.17 Schreiben Sie eine Methode gibAntwort, die zufällig eine der
Zeichenketten "ja", "nein" und "vielleicht" zurückliefert.
Übung 6.18 Erweitern Sie Ihre Methode gibAntwort so, dass sie eine Array-
List mit einer beliebigen Anzahl an Antworten benutzt, aus denen sie zufäl-
lig eine auswählt.

Wenn Sie eine Methode zur Erzeugung von Zufallszahlen aus einem bestimmten
Wertebereich benutzen, dann sollten Sie sehr genau darauf achten, ob die Gren-
zen des Wertebereichs als inklusiv oder exklusiv angesehen werden. Die Methode
nextInt(int n) in der Klasse Random beispielsweise spezifiziert, dass sie Zahlen aus
dem Bereich von 0 (inklusiv) bis n (exklusiv) erzeugt. Der Wert 0 ist somit ein mög-
liches Ergebnis eines Aufrufs, während der angegebene Wert für n niemals
zurückgeliefert wird. Die höchste gelieferte Zahl ist n-1.

Übung 6.19 Fügen Sie Ihrer Klasse ZufallszahlenTester eine Methode hinzu,
die einen Parameter max erhält und eine Zufallszahl aus dem Bereich 1 bis max
liefert (inklusive).
Übung 6.20 Fügen Sie Ihrer Klasse ZufallszahlenTester eine Methode hinzu,
die zwei Parameter min und max erhält und eine Zufallszahl aus dem Bereich
von min bis max liefert (inklusive). Schreiben Sie Ihre Methode aus der vorigen
Übung so um, dass sie nun diese neue Methode benutzt, um ihr Ergebnis zu
erzeugen. Beachten Sie, dass diese Methode nicht notwendigerweise mithilfe
einer Schleife implementiert werden muss.
Übung 6.21 Sehen Sie sich die Details der Klasse SecureRandom an, die im
[Link]-Paket definiert ist. Könnte diese Klasse anstelle der Random-
Klasse verwendet werden? Warum sind Zufallszahlen für kryptografische
Sicherheit wichtig?

6.4.3 Zufällige Antworten generieren


Wir können nun zur Klasse Beantworter zurückkehren, um diese zufällig eine Ant-
wort aus einer Menge von vorgegebenen Antworten auswählen zu lassen. Lis-
ting 6.2 zeigt den Quelltext der Klasse Beantworter in unserer ersten Version.
Wir werden diesen Quelltext nun um einige Zeilen anreichern, die
 ein Datenfeld vom Typ Random für einen Zufallszahlengenerator deklarieren
 ein Datenfeld vom Typ ArrayList für die möglichen Antworten deklarieren

223
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

 im Konstruktor von Beantworter ein Random- und ein ArrayList-Objekt erzeugen


 die Antwortliste mit einigen Antworten füllen
 eine Antwort zufällig auswählen und zurückliefern, wenn generiereAntwort
aufgerufen wird
Listing 6.3 zeigt eine Version der Klasse Beantworter mit diesen Ergänzungen.

Listing 6.3
Der Quelltext der
Klasse Beantworter
mit zufälligen
Antworten.

In dieser Version haben wir den Teil, der die Antwortliste befüllt, in eine eigene
Methode antwortlisteFuellen ausgelagert, die im Konstruktor aufgerufen wird.
Dies garantiert, dass die Antwortliste befüllt wird, sobald ein Beantworter-Objekt
erzeugt wird, doch durch diese Trennung wird der Quelltext der Klasse lesbarer
und übersichtlicher.

224
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.4 Zufälliges Verhalten einbringen

Der interessanteste Abschnitt in dieser Klasse ist die Methode generiereAntwort.


Ohne die Kommentare sieht sie folgendermaßen aus:
public String generiereAntwort()
{
int index = [Link]([Link]());
return [Link](index);
}
Die erste Zeile in dieser Methode erledigt dreierlei:
 Sie lässt sich über den Aufruf von size die Größe der Antwortliste geben.
 Sie generiert eine Zufallszahl zwischen 0 (inklusiv) und der Größe (exklusiv).
 Sie speichert die generierte Zufallszahl in der lokalen Variablen index.
Wenn Ihnen dies zu viel in einer Zeile ist, können Sie stattdessen auch schreiben:
int listengroesse = [Link]();
int index = [Link](listengroesse);
Diese Anweisungen sind äquivalent zur ersten Zeile oben. Welche Version Sie bevor-
zugen, hängt davon ab, welche Sie besser lesbar finden.
Beachten Sie, dass dieser Abschnitt Zufallszahlen aus dem Bereich von 0 bis
listengroesse-1 (inklusiv) erzeugt. Dies passt wunderbar zu den gültigen Indizes
für eine ArrayList. Sie erinnern sich, dass der Wertebereich für Indizes einer Array-
List mit der Größe listengroesse von 0 bis listengroesse-1 reicht. Die erzeugte
Zufallszahl liefert uns somit einen perfekt geeigneten Index, um zufällig auf einen
Eintrag in der Liste zugreifen zu können.
Die letzte Zeile der Methode lautet:
return [Link](index);
In dieser Zeile geschehen zwei Dinge:
 Sie fragt die Antwort an Position index mit der get-Methode ab.
 Sie liefert die gewählte Zeichenkette als Ergebnis der Methode mit der return-
Anweisung zurück.
Wenn Sie nicht vorsichtig sind, kann es leicht passieren, dass Ihr Programm Zufalls-
zahlen generiert, die außerhalb des gültigen Indexbereichs einer ArrayList liegen.
Wenn Sie mit einem solchen ungültigen Index versuchen, auf ein Element zuzu-
greifen, erhalten Sie eine IndexOutOfBoundsException.

6.4.4 Die Dokumentation generischer Klassen


Bisher haben wir Sie aufgefordert, sich die Dokumentation der Klasse String aus
dem Paket [Link] sowie die der Klasse Random aus dem Paket [Link] anzu-
sehen. Sie haben dabei möglicherweise bemerkt, dass einige Klassennamen in der
Dokumentation etwas anders aussehen, wie beispielsweise ArrayList<E> oder Hash-
Map<K,V>. Hinter den Klassennamen stehen hier Zusatzinformationen in spitzen
Klammern. Solche Klassen heißen parametrisierte Klassen oder generische Klas-
sen. Die Angaben in den spitzen Klammern sagen uns, welche zusätzlichen Typ-
namen wir zur Vervollständigung angeben müssen, um diese Klassen zu benutzen.

225
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Wir haben dies bereits in Kapitel 4 angewendet, als wir die Klasse ArrayList
benutzten, indem wir sie mit Typnamen wie String parametrisiert haben:
private ArrayList<String> notizen;
private ArrayList<Posten> studenten;
Weil wir eine ArrayList mit einem beliebigen Klassentyp parametrisieren können,
ist dies entsprechend in der Dokumentation der Java API angegeben. Wenn Sie
sich beispielsweise die Methodenliste der Klasse ArrayList<E> ansehen, entde-
cken Sie Methoden wie:
boolean add(E o)
E get(int index)
Hier sehen wir, dass der Typ der Objekte, die wir in eine ArrayList mit add einfügen
dürfen, von dem Typ abhängt, mit dem die Klasse parametrisiert wurde. Das Gleiche
gilt für den Ergebnistyp der Methode get. Im Endeffekt sagt die Dokumentation,
dass wir, wenn wir ein Objekt des Typs ArrayList<String> erzeugen, ein Objekt mit
den folgenden beiden Methoden erhalten:
boolean add(String o)
String get(int index)
Wenn wir hingegen ein Objekt vom Typ ArrayList<Student> erzeugen, dann hat
es diese beiden Methoden:
boolean add(Student o)
Posten get(int index)
Wir werden Sie später in diesem Kapitel noch auffordern, sich die Dokumentation
weiterer parametrisierter Typen anzusehen.

6.5 Pakete und Importe


Es gibt noch zwei Zeilen zu Anfang der Quelltextdatei, die wir hier ansprechen
müssen:
import [Link];
import [Link];
Wir haben die import-Anweisung zum ersten Mal in Kapitel 4 gesehen. Wir werden
sie nun etwas genauer untersuchen.
Die Java-Klassen in einer Klassenbibliothek stehen nicht automatisch zur Verfü-
gung, so wie die übrigen Klassen in unserem aktuellen Projekt. Stattdessen
müssen wir in unserem Quelltext deklarieren, dass wir eine Klasse aus der Bib-
liothek benutzen möchten. Dies wird Importieren der Klasse genannt und
geschieht entsprechend mit einer import-Anweisung. Die import-Anweisung hat
die Form
import qualifizierter-Klassenname;
Da die Java-Bibliothek aus mehreren Tausend Klassen besteht, ist ein Strukturie-
rungsmechanismus notwendig, mit dem der Umgang mit dieser Menge erleich-
tert wird. Java benutzt Pakete (package), um Bibliotheksklassen in zusammen-

226
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.6 Benutzung von Map-Klassen für Abbildungen

gehörigen Gruppen zu bündeln. Pakete können geschachtelt werden, d.h., ein


Paket kann weitere Pakete enthalten.
Die Klassen ArrayList und Random gehören beide zum Paket [Link]. Diese Infor-
mation steht in der Klassendokumentation. Der vollständige oder qualifizierte
Name einer Klasse besteht aus dem Namen seines Pakets, gefolgt von einem Punkt
und dem Klassennamen. Die qualifizierten Namen der beiden genannten Klassen
sind somit [Link] und [Link].
Java erlaubt auch, dass ganze Pakete mit Anweisungen der Form
import Paketname.*;
importiert werden. Die folgende Anweisung würde somit alle Klassennamen aus
dem Paket [Link] importieren:
import [Link].*;
Die Klassen einzeln aufzuführen, so wie in unserer ersten Version, bedeutet etwas
mehr Schreibarbeit, dokumentiert aber auch deutlicher, welche Klassen tatsächlich
benutzt werden. Deshalb werden wir überwiegend dem Stil des ersten Beispiels
folgen, also jede Klasse einzeln importieren.
Es gibt eine Ausnahme für die Importregeln: Einige Klassen werden so oft benö-
tigt, dass sie praktisch in jede Klasse importiert werden müssten. Diese Klassen
sind im Paket [Link] definiert und dieses Paket wird automatisch in jede
Klasse importiert. String ist ein Beispiel für eine Klasse aus [Link].

Übung 6.22 Implementieren Sie die hier diskutierte Lösung mit den zufälli-
gen Antworten in Ihrer Version des Projekts Technischer-Kundendienst.
Übung 6.23 Was passiert, wenn Sie weitere Antworten in die Antwortliste
einfügen (oder einige entfernen)? Wird die zufällige Auswahl einer Antwort
weiterhin funktionieren? Warum oder warum nicht?

Die Lösung für diese Aufgaben finden Sie auch im Begleitmaterial unter dem Namen
Technischer-Kundendienst2. Wir empfehlen jedoch, dass Sie diese Lösung selbst als
eine Erweiterung der Basisversion implementieren.

6.6 Benutzung von Map-Klassen für


Abbildungen
Wir haben nun eine Lösung für unser Kundendienstsystem, die zufällige Antwor-
ten generiert. Das ist besser als unsere erste Version, aber noch nicht wirklich
überzeugend. Insbesondere werden die Antworten nach wie vor nicht von den
Eingaben des Benutzers beeinflusst. Das wollen wir nun verbessern.
Die Idee ist, dass wir eine Menge von Wörtern definieren, die typischerweise in
Anfragen auftauchen, und diese Wörter mit entsprechenden Antworten ver-

227
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

knüpfen. Wenn die Eingabe des Benutzers eines der uns bekannten Wörter ent-
hält, dann können wir eine passende Antwort geben. Das ist immer noch eine
sehr ungenaue Methode, da sie weder auf den Inhalt einer Anfrage eingeht noch
den Kontext mit einbezieht. Sie kann dennoch überraschend effektiv sein und es
ist ein guter nächster Schritt.
Für diesen Schritt werden wir eine HashMap verwenden. Sie finden die Dokumenta-
tion der Klasse HashMap in der Java-Bibliothek. HashMap ist eine Spezialisierung von
Map, die ebenfalls dokumentiert ist. Sie werden sehen, dass Sie beide lesen müssen,
um zu verstehen, was eine HashMap ist und wie sie funktioniert.

Übung 6.24 Was ist eine HashMap? Welchen Zweck hat sie und wie benutzt
man sie?
Beantworten Sie diese Fragen schriftlich. Benutzen Sie die Dokumentation
von Map und HashMap in der Java-Bibliothek, um diese Fragen zu beantwor-
ten. Beachten Sie, dass es schwierig sein kann, alles zu verstehen, da die
Dokumentation dieser Klassen nicht sehr gut ist. Wir werden die Details in
diesem Kapitel noch besprechen, aber bevor Sie weiterlesen, sollten Sie ver-
suchen, möglichst viel auf eigene Faust herauszufinden.
Übung 6.25 HashMap ist eine parametrisierte Klasse. Benennen Sie die Metho-
den, die von den Typen abhängen, mit denen die Klasse parametrisiert wird.
Denken Sie, dass derselbe Typ für beide Typparameter benutzt werden kann?

6.6.1 Das Konzept einer Map


Konzept Eine Map ist eine Sammlung von Schlüssel-Wert-Paaren, wobei sowohl Schlüssel
als auch Werte Objekte sind. Wie auch eine ArrayList kann eine Map eine belie-
Eine Map ist eine
bige Anzahl von Einträgen haben. Ein Unterschied zwischen einer ArrayList und
Sammlung, die
Schlüssel-Wert- einer Map ist, dass bei einer Map ein Eintrag nicht ein einzelnes Objekt ist, sondern
Paare als Einträge ein Paar von Objekten. Ein solches Paar besteht aus einem Schlüssel-Objekt und
enthält. Ein Wert einem Wert-Objekt.
kann ausgelesen
werden, indem ein Bei einer Map verwenden wir nicht einen Index, um ein Element einer Sammlung
Schlüssel ange- zu referenzieren (wie wir es bei einer ArrayList tun), sondern wir nehmen das
geben wird. Schlüssel-Objekt, um das Wert-Objekt zu bekommen. Im Deutschen verwenden
wir den Begriff Abbildung für dieses Konzept: Ein Schlüssel wird auf einen Wert
abgebildet.
Ein Alltagsbeispiel für eine solche Abbildung ist eine Kontaktliste. Eine Kontakt-
liste enthält Einträge, die Paare aus einem Namen und einer Telefonnummer
sind. Wir verwenden eine Kontaktliste, indem wir einen Namen nachschlagen
und dann die zugeordnete Nummer verwenden. Wir benutzen keinen Index –
also die Position eines Eintrags in der Kontaktliste –, um sie zu finden.
Eine Map kann so strukturiert sein, dass das Nachschlagen eines Wertes für einen
Schlüssel einfach sein kann. Bei einer Kontaktliste beispielsweise ist dies durch

228
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.6 Benutzung von Map-Klassen für Abbildungen

die alphabetische Ordnung gewährleistet. Da die Einträge alphabetisch nach


ihren Schlüsseln sortiert sind, ist das Finden eines Schlüssels und damit des zuge-
ordneten Werts einfach. Die umgekehrte Suche (einen Schlüssel für einen Wert
suchen, also einen Namen zu einer Telefonnummer) ist mit einer solchen Abbil-
dung nicht so einfach. Wie im Falle einer Kontaktliste wäre die umgekehrte
Suche zwar möglich, aber im Vergleich extrem zeitaufwendig. Abbildungen sind
somit ideal für eine einseitig gerichtete Suche, wenn wir den Suchschlüssel ken-
nen und den zugeordneten Wert benötigen.

6.6.2 Die Benutzung einer HashMap


HashMap ist eine spezifische Implementierung einer Map. Die wichtigsten Methoden
einer HashMap sind put und get.
Die Methode put fügt der Abbildung einen Eintrag hinzu und get liefert für einen
gegebenen Schlüssel einen Wert. Der folgende Quelltextabschnitt erzeugt eine
HashMap und fügt drei Einträge ein. Jeder Eintrag ist ein Schlüssel-Wert-Paar,
bestehend aus einem Namen und einer Telefonnummer.
HashMap<String, String> kontakte = new HashMap<>();
[Link]("Günter Schmidt", "(0531) 392 4587");
[Link]("Harald Lorant", "(089) 7386 6632");
[Link]("Werner Jauch", "(030) 4532 7761");

Wie auch bei einer ArrayList müssen wir, wenn wir eine HashMap-Variable deklarie-
ren und ein HashMap-Objekt erzeugen, den Typ der Objekte angeben, die in der Map
enthalten sein sollen; zusätzlich müssen wir hier auch den Typ des Schlüssels ange-
ben. Für unsere Kontaktliste verwenden wir String sowohl für den Schlüsseltyp als
auch für den Elementtyp, aber die beiden Typen müssen im Allgemeinen nicht
gleich sein.
Wie wir bereits in Abschnitt 4.4.2 gesehen haben, müssen wir bei der Erzeugung
von Objekten generischer Klassen und deren Zuweisung an Variablen die generi-
schen Typen (hier <String, String>) nur einmal auf der linken Seite der Zuwei-
sung angeben und können den Diamant-Operator in der Objekterzeugung auf
der rechten Seite verwenden. Die generischen Typen für die Objekterzeugung
werden dann von der Variablendeklaration kopiert.

Die folgenden Anweisungen suchen die Telefonnummer von Harald Lorant und
geben sie aus.
String nummer = [Link]("Harald Lorant");
[Link](nummer);

Beachten Sie, dass hier der Schlüssel (der Name „Harald Lorant“) an die get-
Methode übergeben wird, um den Wert (die Telefonnummer) zu bekommen.

Lesen Sie die Dokumentation von get und put der Klasse HashMap erneut, um fest-
zustellen, ob diese Erklärungen Ihrem aktuellen Verständnis entsprechen.

229
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Übung 6.26 Wie überprüfen Sie, wie viele Einträge in einer Abbildung vor-
handen sind?
Übung 6.27 Legen Sie eine Klasse MapTester an (entweder in Ihrem aktuel-
len Projekt oder in einem eigenen). Verwenden Sie in diesem Projekt eine
HashMap, um eine Kontaktliste wie oben dargestellt zu implementieren. (Ver-
gessen Sie nicht, [Link] zu importieren.) Implementieren Sie zwei
Methoden in dieser Klasse:
public void nummerEintragen(String name, String nummer)
und
public String nummerSuchen(String name)
Diese Methoden sollten die Methoden put und get der Klasse HashMap ver-
wenden, um ihre Funktionalität zu implementieren.
Übung 6.28 Was passiert, wenn Sie bei einer solchen Abbildung einen Ein-
trag mit einem Schlüssel vornehmen, der bereits eingetragen ist?
Übung 6.29 Was passiert, wenn Sie bei einer solchen Abbildung zwei Ein-
träge mit demselben Wert, aber mit verschiedenen Schlüsseln vornehmen?
Übung 6.30 Wie prüfen Sie, ob ein gegebener Schlüssel bereits in einer
Abbildung eingetragen ist? (Geben Sie einen Java-Quelltext an.)
Übung 6.31 Was passiert, wenn Sie einen Wert suchen und der Schlüssel in
der Abbildung nicht existiert?
Übung 6.32 Wie geben Sie alle Schlüssel aus, die aktuell in einer Abbildung
gespeichert sind?

6.6.3 Benutzung einer Abbildung für das


Kundendienstsystem
Für unser Kundendienstsystem können wir eine solche Abbildung gut gebrauchen,
indem wir bekannte Wörter als Schlüssel nehmen und zugeordnete Antworten als
Werte. Listing 6.4 zeigt ein Beispiel, in dem eine HashMap namens antwortMap
erzeugt wird und drei Einträge vorgenommen werden. Beispielsweise ist das Wort
„langsam“ mit folgendem Text verknüpft:
Ich vermute, dass das mit Ihrer Hardware zu tun hat. Ein Upgrade für Ihren
Prozessor sollte diese Probleme lösen. Haben Sie ein Problem mit unserer
Software?
Wenn nun jemand eine Anfrage stellt, die das Wort „langsam“ enthält, können wir
diese Antwort suchen und ausgeben. Beachten Sie, dass die Antwort-Zeichenkette
im Quelltext über mehrere Zeilen geht, die einzelnen Teile aber mit + verknüpft sind
und deshalb als ein einzelner Wert in die HashMap eingetragen werden.

230
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.6 Benutzung von Map-Klassen für Abbildungen

Listing 6.4
Ausgewählte Stich-
wörter mit Antworten
verknüpfen.

Ein erster Versuch, eine Methode zum Erzeugen der Antworten zu schreiben, könnte
so aussehen wie die Methode generiereAntworten unten. Zur Vereinfachung nehmen
wir hier vorläufig an, dass der Benutzer lediglich ein einzelnes Wort (etwa „lang-
sam“) eingegeben hat.
public String generiereAntwort(String wort)
{
String antwort = [Link](wort);
if(antwort != null) {
return antwort;
}
else {
// Wenn wir hierher gelangen, wurde das Stichwort nicht erkannt.
// In diesem Fall wählen wir eine unserer Standardantworten.
return standardantwortAuswählen();
}
}
In diesem Quelltextabschnitt schlagen wir ein Wort, das der Benutzer eingegeben
hat, in der Map nach. Wenn wir dort einen Eintrag finden, liefern wir die zugeord-
nete Antwort. Wenn wir keinen Eintrag für das Wort finden, rufen wir die Methode
standardantwortAuswählen auf. Diese Methode kann die Anweisungen aus unserer
vorherigen Version von generiereAntwort enthalten, die zufällig eine der Standard-
antworten auswählt (wie in Listing 6.3 dargestellt). Die neue Verarbeitungslogik ist
somit, dass wir eine angemessene Antwort liefern, wenn wir ein Stichwort erken-
nen, ansonsten wählen wir zufällig eine unserer Standardantworten aus.

Übung 6.33 Implementieren Sie die hier diskutierten Veränderungen in


Ihrer Version des Kundendienstsystems. Testen Sie es, um ein Gefühl für die
Verbesserung zu bekommen.

231
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Dieser Ansatz, Stichwörter mit Antworten zu verknüpfen, funktioniert nur, solange


der Benutzer statt ganzer Sätze nur einzelne Wörter eingibt. Die letzte Verbesse-
rung unserer Anwendung wird sein, dass ein Benutzer wieder ganze Sätze einge-
ben kann und wir dann eine passende Antwort auswählen, wenn wir eines der
Wörter in der Anfrage erkennen.
Dies stellt uns vor das Problem, dass wir Stichwörter in dem Satz erkennen müssen,
den der Benutzer eingibt. In der momentanen Version wird die Benutzereingabe
durch den Eingabeleser als eine einzelne Zeichenkette geliefert. Wir werden dies
nun so abändern, dass der Eingabeleser eine Menge von Wörtern liefert. Technisch
wird das eine Menge von Zeichenketten sein, von denen jede ein Wort aus der
Benutzereingabe repräsentiert.
Wenn wir das hinbekommen, dann können wir diese Wortmenge an den Beant-
worter weiterreichen, der dann jedes Wort in der Menge daraufhin überprüft, ob
es bekannt ist und mit einer Antwort verknüpft ist.
Um dies in Java umzusetzen, benötigen wir zweierlei: Wir müssen wissen, wie
wir eine Zeichenkette mit einem ganzen Satz in einzelne Wörter zerlegen kön-
nen, und wir müssen mit Mengen umgehen können. Diese Punkte werden in
den nächsten beiden Abschnitten behandelt.

6.7 Der Umgang mit Mengen


Konzept Die Java-Bibliothek bietet verschiedene Varianten von Mengen (set ), die durch
unterschiedliche Klassen implementiert sind. Wir werden im Folgenden ein Hash-
Eine Menge
Set benutzen.
(Set) ist eine
Sammlung, in der
jedes Element nur
maximal einmal Übung 6.34 Was sind die Gemeinsamkeiten und die Unterschiede zwischen
enthalten ist. Die einem HashSet und einer ArrayList? Benutzen Sie die Beschreibungen von
Elemente einer Set, HashSet, List und ArrayList in der Dokumentation der Bibliothek für
Menge haben Ihre Untersuchung, da ein HashSet ein spezieller Fall eines Set ist und eine
keine spezifische ArrayList ein spezieller Fall einer List.
Ordnung.

Die beiden Dienstleistungen, die wir von einer Menge erwarten, sind das Eintra-
gen von Elementen und das spätere Abfragen dieser Elemente. Glücklicherweise
sind uns diese Anforderungen nicht neu. Betrachten Sie folgenden Quelltext-
abschnitt:
import [Link];
import [Link];
...
HashSet<String> meineMenge = new HashSet<>();
[Link]("eins");
[Link]("zwei");
[Link]("drei");

232
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.8 Zeichenketten zerlegen

Vergleichen Sie diese Anweisungen mit denen, die für das Einfügen der Elemente
in eine ArrayList notwendig sind. Es gibt praktisch keinen Unterschied, außer
dass wir diesmal ein HashSet erzeugen statt einer ArrayList. Nun wollen wir das
Iterieren über die Menge betrachten:
for (String eintrag : meineMenge) {
etwas mit diesem Eintrag tun
}
Auch hier sehen wir, dass dies die gleichen Anweisungen sind, die wir in Kapitel 4
für das Iterieren über eine ArrayList benutzt haben.
Kurz gesagt: Der Umgang mit den Sammlungen in Java ist für die verschiedenen
Sammlungstypen sehr ähnlich. Sobald Sie den Umgang mit einer verstanden
haben, können Sie alle benutzen. Die tatsächlichen Unterschiede liegen im Verhal-
ten der jeweiligen Sammlung. Eine Liste (List) beispielsweise hält ihre Elemente in
der gewünschten Reihenfolge, bietet den Zugriff über einen Index und kann das-
selbe Element mehrfach enthalten. Eine Menge (Set) hingegen definiert für ihre
Elemente keine Reihenfolge (die for-each-Schleife kann die Elemente in einer ande-
ren Reihenfolge liefern als die, in der sie eingefügt wurden) und garantiert, dass
jedes Element nur maximal einmal in der Menge vorkommt. Wenn ein Element ein
zweites Mal eingefügt wird, hat dies schlicht keinen Effekt.

6.8 Zeichenketten zerlegen


Nachdem wir den Umgang mit Mengen betrachtet haben, können wir nun unter-
suchen, wie wir eine Eingabezeile in mehrere Wörter zerlegen und diese in einer
Menge ablegen können. Die Lösung ist als eine Version der Methode gibEingabe
der Klasse Eingabeleser realisiert (Listing 6.5).

Listing 6.5
Die Methode gib-
Eingabe liefert eine
Menge von Wörtern.

Hier verwenden wir zusätzlich zur Klasse HashSet die Methode split der Klasse
String, die ebenfalls in der Java-Standardbibliothek definiert ist.

233
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Die Methode split kann einen String in getrennte Substrings aufteilen und diese
in einem String-Array zurückliefern (Arrays werden im nächsten Kapitel noch
ausführlich besprochen). Der Parameter der Methode split definiert, an welcher
Art von Zeichen der Original-String zerlegt werden soll. Wir haben definiert, dass
wir unseren String an jedem Leerzeichen zerlegen wollen.
Die nächsten Zeilen erzeugen ein HashSet und kopieren die Wörter aus dem Array
in dieses hinein, bevor das Set zurückgeliefert wird.3

Übung 6.35 Die Methode split ist mächtiger, als sie in diesem Beispiel auf
den ersten Blick aussieht. Wie können Sie exakt festlegen, wie ein String
zerlegt werden soll? Geben Sie einige Beispiele an.
Übung 6.36 Wie würden Sie die Methode split aufrufen, wenn Sie einen
String an Leerzeichen oder Tabulatorzeichen zerlegen wollen? Wie können
Sie einen String zerlegen, in dem die Wörter durch Doppelpunkte (:) getrennt
sind?
Übung 6.37 Worin unterscheidet sich das Ergebnis, wenn die Wörter statt
in einem HashSet in einer ArrayList zurückgeliefert werden?
Übung 6.38 Was passiert, wenn mehr als ein Leerzeichen zwischen zwei Wör-
tern steht (beispielsweise zwei oder drei Leerzeichen)? Gibt es ein Problem?
Übung 6.39 Zusatzaufgabe. Lesen Sie die Fußnote über die Methode
[Link]. Finden und lesen Sie die späteren Abschnitte über Klassen-
variablen und Klassenmethoden. Erklären Sie in Ihren eigenen Worten, wie
diese Lösung funktioniert.
Übung 6.40 Welche weiteren Methoden bietet die Klasse Arrays?
Übung 6.41 Erzeugen Sie eine Klasse namens SortierenTest. Erzeugen Sie
in dieser Klasse eine Methode, die ein Array von int-Werten als Parameter
entgegennimmt und die Elemente sortiert (kleinstes Element zuerst) auf der
Konsole ausgibt.

6.9 Abschluss des Kundendienstsystems


Um alles zusammensetzen zu können, müssen wir nun auch noch die Klassen
Kundendienstsystem und Beantworter so anpassen, dass sie mit einer Menge von
Worten statt mit einer einzelnen Zeichenkette umgehen können. Listing 6.6 zeigt

3 Es gibt einen kürzeren und noch eleganteren Weg. Sie können schreiben:
HashSet<String> woerter = new HashSet<>([Link](wortArray));
um alle vier Zeilen Quelltext zu ersetzen. Diese Lösung benutzt die Klasse Arrays aus der Stan-
dardbibliothek und eine statische Methode (häufig auch Klassenmethode genannt), die wir
hier noch nicht diskutieren wollen. Wenn Sie neugierig sind, dann lesen Sie den Abschnitt 6.16
über Klassenmethoden und probieren diese Version aus.

234
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.9 Abschluss des Kundendienstsystems

die neue Version der Methode starten aus der Klasse Kundendienstsystem. Sie hat
sich nicht sehr stark verändert. Die Änderungen sind im Einzelnen:
 Die Variable eingabe für das Ergebnis des Aufrufs [Link]() ist nun
vom Typ HashSet.
 Die Prüfung auf Beendigung der Anwendung wird mit der Methode contains
der Klasse HashSet vorgenommen statt mit einer String-Methode. (Lesen Sie
diese Methode in der Dokumentation nach.)
 Die Klasse HashSet muss mit einer import-Anweisung importiert werden (im Lis-
ting nicht gezeigt).

Listing 6.6
Die endgültige Ver-
sion der Methode
starten.

Schließlich müssen wir noch die Methode generiereAntwort in der Klasse Beant-
worter so erweitern, dass sie eine Menge von Wörtern als Parameter akzeptiert.
Sie muss dann über diese Wörter iterieren und bei jedem überprüfen, ob es in
der Map mit den bekannten Stichwörtern enthalten ist. Wenn eines der Wörter
erkannt wird, liefern wir sofort die entsprechende Antwort zurück. Wenn wir kei-
nes der Wörter erkennen, liefern wir wie vorher eine der Standardantworten. Lis-
ting 6.7 zeigt diese Lösung.

Listing 6.7
Die endgültige Ver-
sion der Methode
generiereAntwort.

Dies ist die letzte Änderung an dieser Anwendung, die wir in diesem Kapitel disku-
tieren. Die Lösung im Projekt Technischer-Kundendienst-komplett enthält all diese
Änderungen. Sie enthält auch eine größere Anzahl Verknüpfungen von Stichwör-
tern zu Antworten, als in diesem Kapitel gezeigt wird.
Es sind noch viele Verbesserungen dieser Anwendung denkbar. Wir werden diese
hier nicht diskutieren. Stattdessen geben wir Verbesserungsvorschläge in den
Übungen, die der Leser vornehmen kann. Einige davon sind recht anspruchsvolle
Programmieraufgaben.

235
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Übung 6.42 Implementieren Sie die oben diskutierten Änderungen in Ihrer


Version des Projekts.
Übung 6.43 Fügen Sie Ihrer Anwendung weitere Wort-Antwort-Paare hinzu.
Einige können Sie aus der gegebenen Lösung kopieren und einige selbst hin-
zufügen.
Übung 6.44 Stellen Sie sicher, dass die gleiche Standardantwort nicht zwei-
mal hintereinander ausgewählt wird.
Übung 6.45 Einige Wörter werden auf dieselbe Antwort abgebildet. Erwei-
tern Sie Ihr System so, dass es mit Synonymen und abgeleiteten Wörtern
explizit umgeht, damit Sie nicht mehrere Einträge in der Map für dieselbe
Antwort vornehmen müssen.
Übung 6.46 Identifizieren Sie auch mehrere passende Wörter in der Benut-
zereingabe und reagieren Sie mit einer passenderen Antwort.
Übung 6.47 Wenn kein Wort erkannt wird, können Sie auch auf andere Weise
auf Wörter aus der Eingabe reagieren, beispielsweise mit Standardantworten
auf Fragen mit „Wie“, „Warum“ und „Wer“.

6.10 Autoboxing und Wrapper-Klassen


Wir haben gesehen, dass die Sammlungsklassen (wie ArrayList) mit der geeigne-
ten Parametrisierung Objekte jedes Objekttyps speichern können. Es bleibt jedoch
ein Problem: Java hat auch einige Typen, die keine Objekttypen sind.
Wir erinnern uns, dass die primitiven Typen wie int, boolean, char etc. sich grund-
sätzlich von Objekttypen unterscheiden. Ihre Werte sind nicht Instanzen von
Klassen und können normalerweise nicht in Sammlungen eingetragen werden.
Das ist schade. Es sind Situationen denkbar, in denen wir beispielsweise Listen von
int-Werten oder Mengen von Zeichen (char) benötigen. Was können wir tun?

Die Lösung von Java für dieses Problem sind die sogenannten Wrapper-Klassen
(vom Englischen to wrap: einpacken, umwickeln). Für jeden primitiven Typ gibt
es eine korrespondierende Wrapper-Klasse, die den zugeordneten primitiven Typ
repräsentiert, aber ein echter Objekttyp ist. Die Wrapper-Klasse für int ist bei-
spielsweise Integer. Eine komplette Liste der primitiven Typen und ihrer Wrapper-
Klassen findet sich in Anhang B.
Die folgende Anweisung „wickelt“ den Wert der Variablen ix vom primitiven Typ
int in ein Integer-Objekt:
Integer iwrap = new Integer(ix);

236
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.10 Autoboxing und Wrapper-Klassen

Und nun kann iwrap beispielsweise in einer Sammlung vom Typ ArrayList<Integer> Konzept
gespeichert werden. Das Speichern von primitiven Werten in Objektsammlungen
Autoboxing
wird jedoch noch mehr durch ein Sprachkonstrukt erleichtert, das als Autoboxing
wird automatisch
(übersetzbar mit „automatischer Verpackung“) bezeichnet wird. durchgeführt, wenn
Immer dann, wenn der Wert eines primitiven Typs in einem Zusammenhang ver- der Wert eines pri-
mitiven Typs in
wendet wird, der einen Wrapper-Typ erfordert, verpackt der Compiler den Wert
einem Kontext ver-
des primitiven Typs automatisch in ein Objekt der passenden Wrapper-Klasse. wendet wird, in
Dies bedeutet, dass die Werte primitiver Typen direkt in eine Sammlung einge- dem ein Wrapper-
fügt werden können. Typ erwartet wird.
private ArrayList<Integer> notenListe;
...
public void speichereNoteInListe(int note)
{
[Link](note);
}
Die inverse Operation – das Unboxing – wird ebenfalls automatisch ausgeführt,
sodass das Auslesen aus einer Sammlung etwa folgendermaßen aussehen könnte:
int ersteNote = [Link](0);
Autoboxing wird ebenfalls angewendet, wenn der Wert eines primitiven Typs als
Parameter an eine Methode übergeben wird, die einen Wrapper-Typ erwartet,
und wenn der Wert eines primitiven Typs in einer Variablen eines Wrapper-Typs
gespeichert wird. In analoger Weise wird Unboxing angewendet, wenn der Wert
eines Wrapper-Typs als Parameter an eine Methode übergeben wird, die einen
primitiven Typ erwartet, und wenn er in einer Variablen mit einem primitiven Typ
gespeichert wird. Beachten Sie, dass diese Verfahrensweise dazu führt, dass es
fast so scheint, als ob man primitive Typen in Sammlungen speichern könnte.
Doch der Typ der Sammlung muss immer noch unter Verwendung des Wrapper-
Typs (z.B. ArrayList<Integer> und nicht ArrayList<int>) deklariert werden.

6.10.1 Verwalten der Nutzungszählung


Die Kombination einer Map mit Autoboxing bietet eine einfache Möglichkeit, die
Nutzungszählung von Objekten zu verwalten. Wir nehmen beispielsweise an,
dasss das Unternehmen, welches das System Technischer-Kundendienst verwen-
det, Beschwerden von seinen Kunden bekommt, weil einige der Antworten kei-
nen Bezug zu den gestellten Fragen haben. Das Unternehmen könnte dann ent-
scheiden, die in den Fragen verwendeten Wörter zu analysieren sowie weitere
gezielte Antworten für diejenigen Wörter hinzuzufügen, die am häufigsten vor-
kommen und für die es keine fertigen Antworten gibt. Listing 6.8 zeigt einen Teil
der Klasse Wortzaehler, die im Projekt Technischer-Kundendienst-Analyse zu fin-
den ist.

237
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Listing 6.8
Die Klasse Wortzaeh-
ler wird eingesetzt,
um Worthäufigkeiten
zu zählen.

Die Methode woerterHinzufuegen erhält dieselbe Wortmenge, die dem Beantwor-


ter übergeben wird, sodass jedem Wort eine Häufigkeit zugeordnet werden
kann. Die Häufigkeiten werden in einer Map gespeichert, die Zeichenketten auf
ganzen Zahlen abbildet.
Beachten Sie den Einsatz der HashMap-Methode getOrDefault, die zwei Parameter
besitzt: einen Schlüssel und einen vorgegebenen Wert. Wird der Schlüssel in der
Map bereits verwendet, dann gibt diese Methode den entsprechenden Wert
zurück. Falls aber der Schlüssel nicht verwendet wird, dann wird der vorgegebene
Wert anstelle von null geliefert. Dadurch wird vermieden, zwei unterschiedliche
Folgeaktionen zu schreiben, je nachdem, welcher Schlüssel in Benutzung ist und
welcher nicht. Wenn wir stattdessen get verwenden würden, müssten wir so etwas
wie Folgendes schreiben:
Integer zaehler = [Link](wort);
if(zaehler == null) {
[Link](wort, 1);
}
else {
[Link](wort, zaehler + 1);
}
Beachten Sie, wie Autoboxing und Unboxing mehrere Male in diesen Beispielen
eingesetzt werden.

238
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.11 Die Klassendokumentation schreiben

Übung 6.48 Was macht die Methode putIAbsent von HashMap?


Übung 6.49 Fügen Sie eine Methode zur Klasse Wortzaehler im Projekt
Technischer-Kundendienst-Analyse hinzu, um die Häufigkeit jedes Wortes
auszugeben, nachdem die „ade“-Nachricht ausgegeben wurde.
Übung 6.50 Geben Sie nur von denjeningen Wörtern die Häufigkeiten aus,
die noch keine Schlüssel in antwortMap im der Beantworter-Klasse sind. Sie
werden dazu eine sondierende Methode für antwortMap bereitstellen müssen.

6.11 Die Klassendokumentation schreiben


Während Sie an einem Projekt arbeiten, ist es wichtig, dass Sie neben der Entwick-
lung des Quelltextes auch die Dokumentation erstellen. Es ist leider nach wie vor
noch allgemein üblich, dass Programmierer das Dokumentieren nicht sehr ernst
nehmen, was später häufig zu ernsthaften Problemen führt.
Wenn Sie nicht genügend dokumentieren, dann kann es für einen anderen Pro-
grammierer (oder sogar für Sie selbst!) schwierig sein, Ihre Klassen zu verstehen.
Typischerweise müssen Sie dann die Implementierung untersuchen, um Ihre Arbeits-
weise zu verstehen. Das klappt eventuell noch in kleinen Studentenprojekten, aber
in realen Projekten führt das zu erheblichen Schwierigkeiten.
Es ist bei kommerziellen Anwendungen durchaus üblich, dass der Quelltext aus
mehreren Hunderttausend Zeilen in mehreren Tausend Klassen besteht. Stellen Sie
sich vor, Sie müssten all das lesen, um die Arbeitsweise zu verstehen! Das kann
eine einzelne Person nicht bewältigen.
Als wir Klassen wie HashSet oder Random aus der Java-Bibliothek benutzt haben, Konzept
haben wir uns ausschließlich auf die Dokumentation verlassen, um ihre Dienstleis-
Die Dokumen-
tungen zu verstehen. Wir haben uns nicht ein einziges Mal die Implementierung
tation einer Klasse
dieser Klassen angesehen. Das hat funktioniert, weil diese Klassen einigermaßen sollte genau die
gut dokumentiert sind (obwohl auch da noch Verbesserungen denkbar sind). Es Informationen
wäre sehr viel schwieriger für uns geworden, wenn wir uns die Implementierungen bieten, die andere
der Klassen hätten ansehen müssen, bevor wir sie benutzen können. Programmierer
benötigen, um die
In einem Entwicklungsteam für Software werden die Implementierungen der Klasse ohne einen
Klassen üblicherweise von mehreren Programmierern gemeinsam benutzt. Wäh- Blick auf ihre Imple-
rend Sie möglicherweise für die Klasse Kundendienstsystem aus unserem letzten mentierung benut-
Beispiel zuständig sind, implementiert ein anderer vielleicht die Klasse Eingabe- zen zu können.
leser. Es kann also vorkommen, dass Sie die eine Klasse implementieren und
dabei Methoden von anderen Klassen aufrufen.
Das Argument, das bei Bibliotheksklassen so schlüssig ist, gilt in gleicher Weise für
die Klassen, die Sie selbst erstellen: Wenn wir sie benutzen können, ohne dass wir
ihre Implementierung lesen und verstehen müssen, dann wird uns das die Arbeit
erleichtern. Wie bei den Bibliotheksklassen wollen wir nur die Schnittstelle einer
Klasse sehen, nicht ihre Implementierung. Deshalb ist eine gute Klassendokumen-
tation für Ihre eigenen Klassen ebenso wichtig.

239
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Java-Systeme bieten ein Werkzeug namens javadoc an, mit dem solche Schnitt-
stellenbeschreibungen aus Quelltexten generiert werden können. Die Dokumen-
tation der Standardbibliothek beispielsweise, die wir uns angesehen haben, ist
aus den Quelltexten der Klassen mithilfe von javadoc erzeugt worden.

6.11.1 javadoc in BlueJ nutzen


Die BlueJ-Umgebung benutzt javadoc, um für Ihre Klassen die Dokumentation
auf zwei Wegen zu erzeugen:
 Sie können die Dokumentation für eine einzelne Klasse betrachten, indem Sie
im aufklappbaren Listenfeld rechts oben im Editorfenster vom Eintrag QUELLTEXT
zum Eintrag DOKUMENTATION wechseln (oder im Menü WERKZEUGE den Befehl
ANSICHT IMPLEMENTIERUNG/DOKUMENTATION wählen).
 Sie können mit der Funktion DOKUMENTATION ERZEUGEN aus dem Menü WERKZEUGE
die Dokumentation für alle Klassen im Projekt erzeugen.
Sie können weitere Details dazu im Tutorial für BlueJ nachlesen. Sie finden das
Tutorial im Menü HILFE.

6.11.2 Die Elemente einer Klassendokumentation


Die Dokumentation einer Klasse sollte mindestens die folgenden Punkte enthalten:
 den Klassennamen
 einen Kommentar, der den allgemeinen Zweck und die Eigenschaften der Klasse
beschreibt
 eine Versionsnummer
 den (oder die) Autorennamen
 eine Dokumentation für jeden Konstruktor und jede Methode
Die Dokumentation jedes Konstruktors und jeder Methode sollte enthalten:
 den Namen der Methode
 den Ergebnistyp
 die Namen und Typen der Parameter
 eine Beschreibung des Zwecks und der Arbeitsweise der Methode
 eine Beschreibung jedes Parameters
 eine Beschreibung des Ergebnisses
Zusätzlich sollte jedes Projekt einen Projektkommentar enthalten, der oft in einer
„ReadMe“-Datei steht. In BlueJ ist dieser Projektkommentar über das Textblatt-
Symbol in der linken oberen Ecke des Klassendiagramms zugreifbar.

Übung 6.51 Benutzen Sie die Funktion DOKUMENTATION ERZEUGEN in BlueJ, um


die Dokumentation für Ihr Kundendienstsystem zu erstellen. Untersuchen Sie
das Ergebnis. Ist die Dokumentation korrekt? Ist sie vollständig? Welche Teile
sind nützlich, welche eher nicht? Finden Sie Fehler in der Dokumentation?

240
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.11 Die Klassendokumentation schreiben

Einige Elemente der Dokumentation, wie die Namen und die Parameter der Metho-
den, können immer aus dem Quelltext extrahiert werden. Andere Teile, wie bei-
spielsweise die Kommentare für die Klasse, die Methoden und die Parameter, brau-
chen mehr Aufmerksamkeit, da sie leicht vergessen werden oder unvollständig oder
inkorrekt sind.

In Java werden die Kommentare für javadoc mit einer speziellen Zeichenfolge ein-
geleitet:
/**
Dies ist ein javadoc-Kommentar.
*/

Die öffnenden Zeichen für einen Kommentar müssen zwei Sterne enthalten, damit
sie von javadoc erkannt werden können. Wenn ein solcher Kommentar unmittelbar
vor einer Klassendefinition steht, wird er als Klassenkommentar aufgefasst. Wenn
er unmittelbar vor der Signatur einer Methode steht, wird er als ein Methoden-
kommentar aufgefasst.

Die genauen Details, wie die Dokumentation erzeugt und formatiert wird, sind
für jede Programmiersprache und jede Programmierumgebung unterschiedlich.
Der Inhalt sollte jedoch mehr oder weniger immer der gleiche sein.

Für Java in Verbindung mit javadoc stehen einige Schlüsselwörter zur Verfügung,
mit denen die Dokumentation formatiert werden kann. Diese Schlüsselwörter
beginnen immer mit dem Symbol @:
@version
@author
@param
@return

Übung 6.52 Finden Sie Beispiele für die Schlüsselwörter von javadoc im Quell-
text des Projekts Technischer-Kundendienst. Wie beeinflussen diese die For-
matierung der Dokumentation?

Übung 6.53 Finden und beschreiben Sie weitere Schlüsselwörter von java-
doc. Ein guter Platz zum Nachforschen ist die Online-Dokumentation der
Java-Distribution von Sun Microsystems. Sie bietet ein Dokument mit dem
Titel javadoc – The Java API Documentation Generator (beispielsweise unter
http:// [Link]/ javase/8/docs/technotes/tools/windows/ [Link] ).
In diesem Dokument werden die Schlüsselwörter javadoc tags genannt.
Übung 6.54 Dokumentieren Sie alle Klassen in Ihrer Version des Kunden-
dienstsystems in angemessener Weise.

241
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

6.12 Öffentliche und private Eigenschaften


Konzept An dieser Stelle soll ein Aspekt von Klassen etwas näher beleuchtet werden, dem
wir bisher schon mehrfach begegnet sind, ohne viel darüber zu sagen: die Zugriffs-
Zugriffsmodifi-
modifikatoren.
katoren definie-
ren die Sichtbarkeit Die beiden Schlüsselwörter public oder private vor der Deklaration eines Daten-
eines Datenfelds, felds und vor der Signatur einer Methode sind Zugriffsmodifikatoren. Beispiels-
eines Konstruktors
weise:
oder einer Methode.
Öffentliche (public) // Datenfeld
Elemente sind private int anzahlSitzplaetze;
sowohl innerhalb
der definierenden // Methoden
Klasse zugreifbar public void setzeAlter(int neuesAlter)
als auch von ande- { ...
ren Klassen aus; }
private (private)
Elemente sind
private int berechneDurchschnitt()
ausschließlich
{ ...
innerhalb der defi-
}
nierenden Klasse
zugreifbar. Datenfelder, Konstruktoren und Methoden können entweder als öffentlich
(public) oder als privat (private) vereinbart werden, auch wenn wir bisher über-
wiegend private Datenfelder und öffentliche Methoden und Konstruktoren gese-
hen haben. Wir werden auf diesen Umstand zurückkommen.
Zugriffsmodifikatoren definieren die Sichtbarkeit eines Datenfelds, Konstruktors
oder einer Methode. Wenn eine Methode beispielsweise als public vereinbart ist,
kann sie innerhalb der definierenden Klasse und von jeder anderen Klasse aufge-
rufen werden. Private Methoden andererseits können nur innerhalb der Klasse
aufgerufen werden, in der sie definiert wurden. Sie sind für andere Klassen nicht
sichtbar.
Nachdem wir den Unterschied zwischen der Schnittstelle und der Implementie-
rung einer Klasse diskutiert haben (in Abschnitt 6.3.1), können wir nun sehr viel
leichter den Sinn dieser Schlüsselwörter verstehen.
Sie erinnern sich: Die Schnittstelle einer Klasse ist die Menge der Details, die ein
anderer Programmierer, der die Klasse benutzen will, benötigt. Sie liefert Infor-
mationen darüber, wie die Klasse benutzt werden kann. Die Schnittstelle schließt
die Konstruktoren, die Signaturen der Methoden und deren Kommentare mit
ein. Sie wird auch als der öffentliche Teil einer Klasse bezeichnet. Ihr Zweck ist zu
definieren, was die Klasse tut.
Die Implementierung ist der Teil der Klasse, der genau festlegt, wie die Klasse ihre
Aufgabe erfüllt. Die Methodenrümpfe mit den Java-Anweisungen und die meis-
ten Datenfelder sind Teil der Implementierung. Die Implementierung wird auch
als der private Teil einer Klasse bezeichnet. Der Benutzer einer Klasse muss die
Implementierung nicht kennen. Tatsächlich gibt es gute Gründe, weshalb ein
Benutzer davor geschützt werden sollte, etwas über die Implementierung zu wis-
sen (oder zumindest von diesem Wissen Gebrauch zu machen). Dieses Prinzip
wird auch als das Geheimnisprinzip bezeichnet (information hiding).

242
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.12 Öffentliche und private Eigenschaften

Das Schlüsselwort public deklariert für ein Element einer Klassendefinition (ein
Datenfeld oder eine Methode), dass es Teil der Schnittstelle ist (also öffentlich
sichtbar); das Schlüsselwort private deklariert es als einen Teil der Implementie-
rung (also als verborgen vor dem Zugriff von außen).

6.12.1 Das Geheimnisprinzip


In vielen objektorientierten Sprachen werden die Interna einer Klasse – ihre Imple- Konzept
mentierung – vor anderen Klassen verborgen. Dies hat zwei Aspekte: Erstens sollte
Das Geheim-
ein die Klasse benutzender Programmierer keine Informationen über ihren internen
nisprinzip
Aufbau benötigen; zweitens sollte ihm nicht erlaubt sein, diese internen Details zu besagt, dass die
kennen. internen Details der
Implementierung
Das erste Prinzip – keine Kenntnis über Interna benötigen – hat mit Abstraktion
einer Klasse vor
und Modularisierung zu tun, wie in Kapitel 3 diskutiert. Wenn es notwendig wäre, anderen Klassen
alle Interna von allen benötigten Klassen zu kennen, dann würden wir große Sys- verborgen sein soll-
teme niemals fertig bekommen. ten. Dies unter-
stützt eine bessere
Das zweite Prinzip – keine Kenntnis über Interna erlauben – hat eine andere Modularisierung
Bedeutung. Es hat zwar auch mit Modularisierung zu tun, aber in einem anderen von Anwendungen.
Zusammenhang. Die Programmiersprache erlaubt nicht den Zugriff auf die priva-
ten Anteile einer Klasse in den Anweisungen einer anderen Klasse. Dies garan-
tiert, dass eine Klasse nicht davon abhängt, dass eine andere Klasse exakt auf
diese Weise implementiert ist.
Dieser Aspekt ist sehr wichtig für die Wartung. Es kommt häufig vor, dass ein War-
tungsprogrammierer die Implementierung einer Klasse ändern oder erweitern
muss, um sie zu verbessern oder um Fehler zu beheben. Idealerweise sollte eine
Änderung in der Implementierung einer Klasse nicht auch eine Änderung in einer
anderen Klasse notwendig machen. Dieser Aspekt wird auch Kopplung genannt:
Wenn eine Änderung an einer Stelle eines Programms nicht auch Änderungen an
anderen Stellen impliziert, dann wird dies schwache oder lose Kopplung genannt.
Lose Kopplung ist gut, denn sie erleichtert die Aufgaben eines Wartungsprogram-
mierers sehr. Anstatt viele Klasse verstehen und ändern zu müssen, muss er nur
eine Klasse verstehen und ändern. Wenn beispielsweise ein Java-Programmierer
eine Verbesserung an der Implementierung der Klasse ArrayList vornimmt, dann
hoffen wir sehr, dass deshalb nicht alle Klassen geändert werden müssen, die diese
Klasse benutzen. Das funktioniert deshalb, weil wir in unseren Quelltexten keinen
Bezug auf die Implementierung der Klasse ArrayList nehmen.
Um es etwas genauer zu sagen: Die Regel – keine Kenntnis über Interna erlauben –
bezieht sich nicht auf den Programmierer einer benutzenden Klasse, sondern auf
die benutzende Klasse selbst. Es ist üblicherweise kein Problem, wenn ein Program-
mierer die Details einer anderen Klasse kennt, aber die von ihm implementierte
Klasse sollte keine Kenntnis über die Interna haben, also nicht von den Interna
einer anderen Klasse abhängen. Der Programmierer der beiden beteiligten Klassen
kann sogar derselbe sein, aber die Klassen sollten lose gekoppelt sein.
Die Konzepte lose Kopplung und Geheimnisprinzip sind sehr wichtig, und wir wer-
den in späteren Kapiteln auf sie zurückkommen.

243
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

An dieser Stelle ist wichtig zu verstehen, dass das Schlüsselwort private das
Geheimnisprinzip dadurch erzwingt, dass andere Klassen keinen Zugriff auf diese
privaten Anteile haben können. Dies ermöglicht lose Kopplung und macht eine
Anwendung modularer und leichter zu warten.

6.12.2 Private Methoden und öffentliche


Datenfelder
Die meisten Methoden, die wir bisher betrachtet haben, waren öffentlich. Dies
ermöglicht es anderen Klassen, diese Methoden aufzurufen. An manchen Stellen
haben wir allerdings schon private Methoden benutzt. In der Klasse Kundendienst-
system im Projekt Technischer-Kundendienst beispielsweise haben wir die Metho-
den willkommenstextAusgeben und abschiedstextAusgeben als privat deklariert.
Der Grund, beide Optionen für Methoden zu verwenden, liegt darin, dass Metho-
den tatsächlich für sehr unterschiedliche Zwecke eingesetzt werden. Sie werden
eingesetzt, um den Benutzern einer Klasse Operationen anzubieten (öffentliche
Methoden), und sie werden benutzt, um eine größere Aufgabe in kleinere zu zerle-
gen, um damit die größere Aufgabe leichter handhabbar zu machen. Im zweiten
Fall sind die Unteraufgaben nicht dafür vorgesehen, von außerhalb der Klasse auf-
gerufen zu werden, sondern sie werden nur für eine bessere interne Strukturierung
der Klasse in eigenen Methoden definiert. In einem solchen Fall sollten diese Metho-
den privat deklariert werden. willkommenstextAusgeben und abschiedstextAusgeben
sind Beispiele für diesen Fall.
Ein weiterer guter Grund für private Methoden ist, wenn eine bestimmte Dienst-
leistung (als eine Unteraufgabe) von mehreren Methoden in einer Klasse benö-
tigt wird. Statt diese Anweisungen mehrfach zu schreiben, können wir eine pri-
vate Methode mit diesen Anweisungen schreiben und diese Methode dann von
unterschiedlichen Stellen aus aufrufen. Wir werden später noch ein Beispiel für
diesen Fall sehen.
In Java können auch Datenfelder als privat oder als öffentlich deklariert werden.
Bisher haben wir kein Beispiel für öffentliche Datenfelder gesehen, und das hat
seinen Grund: Öffentliche Datenfelder brechen nämlich mit dem Geheimnisprin-
zip. Durch sie wird eine Klasse, die sich auf diese Informationen verlässt, verletz-
bar für Änderungen der Implementierung. Auch wenn Java erlaubt, dass Daten-
felder als öffentlich deklariert werden, betrachten wir dies als schlechten Stil und
machen von der Möglichkeit keinen Gebrauch. Einige andere objektorientierte
Sprachen erlauben öffentliche Datenfelder gar nicht.
Ein weiterer Grund für private Datenfelder ist, dass ein Objekt damit größere Kon-
trolle über seinen Zustand hat. Wenn die Zugriffe auf private Datenfelder aus-
schließlich über die sondierenden und verändernden Methoden möglich sind,
dann kann ein Objekt garantieren, dass kein Datenfeld auf einen Wert gesetzt
wird, der mit dem Gesamtzustand des Objekts nicht vereinbar ist. Dieser Grad an
Integrität kann mit öffentlichen Datenfeldern nicht garantiert werden.
Kurz gesagt: Datenfelder sollten immer privat sein.

244
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.13 Klassen über ihre Schnittstelle verstehen

Java definiert noch zwei weitere Zugriffsstufen. Die eine wird mit dem Schlüsselwort
protected deklariert, die andere wird wirksam, wenn gar kein Modifikator angege-
ben ist. Wir werden diese in späteren Kapiteln besprechen.

6.13 Klassen über ihre Schnittstelle


verstehen
Wir werden hier kurz ein weiteres Projekt besprechen, um die in diesem Kapitel
vorgestellten Konzepte zu vertiefen und Erfahrungen zu sammeln. Das Projekt
trägt den Namen Kritzeln und ist im Buchprojekte-Ordner zu Kapitel 6 zu finden.
Dieser Abschnitt führt keine neuen Konzepte ein; es besteht zum großen Teil aus
Übungen, in die vereinzelte Kommentare eingestreut sind.

6.13.1 Das Demo-Projekt Kritzeln


Das Projekt Kritzeln enthält drei Klassen: ZeichnenDemo, Stift und Canvas (Abbil-
dung 6.4).

ZeichnenDemo Abbildung 6.4


Das Projekt Kritzeln.

Stift

Canvas

Die Klasse Canvas (englisch für „Leinwand“) bietet ein Fenster auf dem Bildschirm
an, auf dem gezeichnet werden kann. Sie bietet Operationen zum Zeichnen von
Linien, Figuren und Text. Ein Canvas kann benutzt werden, indem interaktiv oder
von einem anderen Objekt aus eine Instanz erzeugt wird. Diese Klasse sollte kei-
ner Veränderungen bedürfen. Es ist wahrscheinlich am besten, sie als Biblio-
theksklasse zu behandeln:4 Öffnen Sie den Editor der Klasse und wechseln Sie in
die Ansicht Dokumentation. Dies zeigt die Schnittstelle der Klasse im javadoc-For-
mat.

4 Anmerkung des Übersetzers: Aufgrund dieser Rolle als Bibliotheksklasse wurde diese Klasse
bewusst nicht übersetzt.

245
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Die Klasse Stift liefert das Stift-Objekt, das dazu verwendet werden kann, durch
Bewegen des Stiftes auf dem Bildschirm zu zeichnen. Der Stift selbst ist unsichtbar,
zieht aber eine Linie, wenn er über die Leinwand bewegt wird.
Die Klasse ZeichnenDemo bietet mehrere kleine Beispiele für die Verwendung eines
Stift-Objekts für grafische Ausgaben.
Der beste Einstiegspunkt in dieses Projekt ist die Klasse ZeichnenDemo.

Übung 6.55 Erzeugen Sie ein Objekt der Klasse ZeichnenDemo und experi-
mentieren Sie mit seinen diversen Methoden. Lesen Sie den Quelltext von
ZeichnenDemo und beschreiben Sie (schriftlich) die Funktionsweise einer jeden
Methode.
Übung 6.56 Erzeugen Sie interaktiv ein Stift-Objekt unter Verwendung
seines Standardkonstruktors (der Konstruktor ohne Parameter). Experimen-
tieren Sie mit seinen Methoden. Achten Sie darauf, dass Sie währenddessen
ein Fenster geöffnet haben, das Ihnen die Dokumentation der Klasse Stift
zeigt (entweder das Editorfenster in der Dokumentationsansicht oder ein
Webbrowserfenster, das die Projektdokumentation zeigt). Vergewissern Sie
sich in der Dokumentation, dass Sie verstehen, was jede Methode macht.
Übung 6.57 Erzeugen Sie interaktiv eine Instanz der Klasse Canvas und versu-
chen Sie sich an einigen ihrer Methoden. Auch hier sollten Sie dabei Bezug
nehmen auf die Dokumentation der Klasse.

Einige der Methoden in den Klassen Stift und Canvas beziehen sich auf Parame-
ter vom Typ Color. Dieser Typ ist in der Klasse Color im Paket [Link] definiert
(das heißt, sein voll qualifizierter Name lautet [Link]). Die Klasse Color
definiert einige Farbkonstanten, die wir wie folgt ansprechen können:
[Link]
Um diese Konstanten zu verwenden, müssen Sie die Klasse Color in der verwen-
denden Klasse importieren.

Übung 6.58 Finden Sie einige Einsatzbereiche von Farbkonstanten im


Quelltext der Klasse ZeichnenDemo.
Übung 6.59 Schreiben Sie vier weitere Farbkonstanten nieder, die in der Klasse
Color verfügbar sind. Lesen Sie in der Klassendokumentation nach, um heraus-
zufinden, welche Konstanten es gibt.

Wenn wir interaktiv Methoden aufrufen, die Parameter der Klasse Color erwarten,
müssen wir ein wenig anders auf die Klasse Bezug nehmen. Da der interaktive Dia-

246
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.13 Klassen über ihre Schnittstelle verstehen

log keine import-Anweisung erlaubt (und damit die Klasse Color nicht automatisch
bekannt ist), müssen wir den voll qualifizierten Klassennamen schreiben, um auf
die Klasse Bezug zu nehmen (Abbildung 6.5). Auf diese Weise kann das Java-Sys-
tem die Klasse finden, ohne dass eine import-Anweisung verwendet werden muss.

Abbildung 6.5
Ein Methodenaufruf
mit einem voll qualifi-
zierten Klassennamen.

Da wir inzwischen wissen, wie wir die Farbe für Stifte und Leinwände ändern, kön-
nen wir uns weiteren Übungen zuwenden.

Übung 6.60 Erzeugen Sie ein Canvas. Zeichnen Sie unter interaktiver Ver-
wendung der Methoden von Canvas einen roten Kreis ungefähr in der Mitte
der Leinwand und anschließend ein gelbes Rechteck.
Übung 6.61 Wie löschen Sie die ganze Leinwand?

Wie Sie gesehen haben, können Sie entweder direkt auf die Leinwand zeichnen
oder sich dazu eines Stift-Objekts bedienen. Der Stift bietet uns eine Abstraktion,
die eine aktuelle Position, Rotation und Farbe hält, was die Erzeugung einiger
Arten von Zeichnungen einfacher macht. Lassen Sie uns noch ein wenig herum-
experimentieren. Dieses Mal wollen wir jedoch Code in einer Klasse schreiben,
anstatt interaktive Aufrufe zu verwenden.

Übung 6.62 Erzeugen Sie in der Klasse ZeichnenDemo eine neue Methode
namens dreieckZeichnen. Diese Methode sollte einen Stift erzeugen (wie in der
Methode quadratZeichnen) und dann ein grünes Dreieck zeichnen.
Übung 6.63 Schreiben Sie eine Methode fuenfeckZeichnen, die ein Fünfeck
zeichnet.
Übung 6.64 Schreiben Sie eine Methode polygonZeichnen(int n), die ein
gleichmäßiges Polygon mit n Seiten zeichnet (d.h., n=3 zeichnet ein Dreieck,
n=4 zeichnet ein Quadrat usw.).

Übung 6.65 Schreiben Sie eine Methode namens spirale, die eine Spirale
zeichnet (siehe Abbildung 6.6).

247
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Abbildung 6.6
Eine Spirale, die
auf eine Canvas
gezeichnet ist.

6.13.2 Code-Vervollständigung
Oft sind wir mit der Bibliotheksklasse, die wir benutzen, einigermaßen vertraut.
Dennoch können wir uns nicht immer genau an alle Namen ihrer Methoden oder
der Parameter erinnern. Für solche Fälle bieten Entwicklungsumgebungen in der
Regel Unterstützung in Form von Code-Vervollständigung.
Code-Vervollständigung ist eine Funktion, die im BlueJ-Editor zur Verfügung steht,
wenn der Cursor hinter dem Punkt eines Methodenaufrufs steht. Wenn Sie dann
(Strg)+(Leertaste) drücken, erscheint ein Kontextmenü, das alle Methoden in der
Schnittstelle des Objekts auflistet, das wir im Aufruf verwenden (Abbildung 6.7).

Abbildung 6.7
Code-Vervoll-
ständigung.

Wenn das Kontextmenü zur Code-Vervollständigung eingeblendet wird, können


wir den Anfang des Methodennamens eintippen und damit die Anzahl der ange-

248
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.13 Klassen über ihre Schnittstelle verstehen

zeigten Methoden in der Liste reduzieren. Durch Betätigen der Eingabetaste über-
nehmen Sie den ausgewählten Methodenaufruf in Ihren Quelltext. Die Code-Ver-
vollständigung kann auch ohne ein vorangehendes Objekt verwendet werden, um
lokale Methoden aufzurufen.
Die Code-Vervollständigung sollte kein Ersatz für das Lesen der Klassendokumen-
tation sein, da sie nicht alle Informationen umfasst (wie der einführende Klassen-
kommentar). Doch sobald wir eine Klasse recht gut kennen, ist die Code-Vervoll-
ständigung eine große Hilfe, um sich die Einzelheiten einer Methode schnell in
Erinnerung zu rufen und den Aufruf in den Quelltext zu übernehmen.

Übung 6.66 Geben Sie [Link] in Ihrer Klasse ZeichnenDemo ein und drü-
cken Sie dann (Strg) + (Leertaste) (wobei der Cursor unmittelbar hinter
dem eingegebenen Text steht), um die Code-Vervollständigung zu aktivieren.
Wie viele Methoden werden angezeigt?
Übung 6.67 Fügen Sie Ihrer Klasse ZeichnenDemo eine Methode hinzu, die
direkt ein Bild aufder Leinwand erzeugt (ohne ein Stift-Objekt zu verwen-
den). Das Bild kann irgendetwas Beliebiges sein, sollte aber mindestens einige
Formen, verschiedene Farben und Text enthalten. Nutzen Sie Code-Vervoll-
ständigung, während Sie Ihren Quelltext eingeben.

6.13.3 Das Demo-Projekt Baelle


Öffnen Sie das Projekt Baelle und stellen Sie fest, was es macht. Erzeugen Sie ein
BallDemo-Objekt und führen Sie seine Methode springenLassen aus.

Übung 6.68 Ändern Sie die Methode springenLassen in der Klasse BallDemo,
damit der Benutzer wählen kann, wie viele Bälle springen sollen.

Für diese Übungen sollten Sie zur Speicherung der Bälle eine Sammlung verwen-
den. So kann die Methode mit jeder beliebigen Anzahl Bälle – seien es 1, 3 oder
75 – ausgeführt werden. Die Bälle sollten am Anfang nebeneinander oben auf
der Leinwand angezeigt werden.
Welche Art Sammlung sollten wir wählen? Bis jetzt haben wir ArrayList, HashMap
und HashSet kennengelernt. Machen Sie erst die nächsten zwei Übungen, bevor
Sie Ihre Implementierung schreiben.

Übung 6.69 Welche Art Sammlung (ArrayList, HashMap und HashSet) ist am
besten geeignet, um die Bälle für die neue Methode springenLassen zu spei-
chern? Halten Sie Ihre Antwort schriftlich fest und begründen Sie Ihre Wahl.
Übung 6.70 Ändern Sie die Methode springenLassen, um die Bälle in der
oberen Hälfte des Bildschirms nach dem Zufallsprinzip anzuordnen.

249
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Übung 6.71 Schreiben Sie eine neue Methode namens kastenSpringen. Diese
Methode zeichnet ein Rechteck (den „Kasten“) auf den Bildschirm sowie einen
oder mehrere Bälle in diesen Kasten. Verwenden Sie für die Bälle nicht die
Klasse Ball, sondern erzeugen Sie eine neue Klasse KastenBall, wobei sich der
Ball im Kasten bewegt und von den Wänden abprallt, sodass der Ball immer im
Kasten bleibt. Die Anfangsposition sowie die Geschwindigkeit des Balls sollten
Zufallswerte sein. Die Methode kastenSpringen sollte einen Parameter über-
nehmen, der angibt, wie viele Bälle im Kasten erzeugt werden sollen.
Übung 6.72 Zeichnen Sie die Bälle in kastenSpringen mit Zufallsfarben.

6.14 Klassenvariablen und Konstanten


Bisher haben wir die Klasse Ball nicht näher betrachtet. Wenn Sie verstehen möch-
ten, wie die Animation funktioniert, sollten Sie sich auch diese Klasse genauer anse-
hen. Sie ist recht einfach – die einzige etwas kompliziertere Methode ist bewegen,
wobei der Ball seine neue Position auf seiner Bahn bekommt.
Wir überlassen es weitgehend Ihnen selbst, diese Methode zu verstehen, bis auf
ein Detail, das wir hier ansprechen möchten. Wir beginnen mit einer Übung.

Übung 6.73 In der Klasse Ball finden Sie die Definition der Gravitation (eine
einfache ganze Zahl). Vergrößern oder verkleinern Sie diesen Wert, überset-
zen Sie und starten Sie die Demo erneut. Beobachten Sie eine Veränderung?

Das interessanteste Detail in dieser Klasse ist die Zeile


private static final int GRAVITATION = 3;
Dies ist ein Konstrukt, das wir bisher noch nicht gesehen haben. Tatsächlich wer-
den hier zwei neue Schlüsselwörter eingesetzt, die gemeinsam verwendet wer-
den: static und final.

Konzept 6.14.1 Das Schlüsselwort static


Auch Klassen kön- Mit dem Schlüsselwort static werden in Java Klassenvariablen definiert. Klassen-
nen Datenfelder variablen sind Datenfelder, die in der Klasse selbst, nicht in den Objekten gespei-
haben. Diese wer- chert werden. Dies unterscheidet sie fundamental von den Instanzvariablen (die
den Klassen- Datenfelder, die wir bisher benutzt haben). Betrachten Sie folgenden Quelltext-
variablen oder abschnitt (ein Teil der Klasse Ball):
auch statische Vari-
ablen genannt. Von public class Ball
einer Klassenvariab- {
len existiert immer // Einfluss der Gravitation
nur genau eine private static final int GRAVITATION = 3;
Kopie, unabhängig
von der Anzahl der private int xPosition;
erzeugten Instanzen. private int yPosition;

250
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.14 Klassenvariablen und Konstanten

andere Datenfelder und Methoden hier ausgelassen


}
Nehmen Sie nun an, dass wir drei Instanzen der Klasse Ball erzeugen. Die resul-
tierende Situation ist in Abbildung 6.8 dargestellt.
Aus dem Diagramm können wir ersehen, dass die Instanzvariablen (xPosition und
yPosition) in jedem Objekt gespeichert sind. Da wir drei Objekte erzeugt haben,
haben wir auch drei voneinander unabhängige Kopien dieser Variablen.
Die Klassenvariable GRAVITATION hingegen ist in der Klasse selbst gespeichert. Als
Konsequenz gibt es immer nur genau eine Kopie dieser Variablen, unabhängig
von der Anzahl der erzeugten Instanzen.

Abbildung 6.8
Instanzvariablen und
eine Klassenvariable.
GRAVI-
TATION

Im Quelltext der Klasse kann auf diese Art der Variablen (lesend und schreibend)
genauso zugegriffen werden wie auf eine Instanzvariable. Auf die Klassenvariable
kann auch in allen Instanzen der Klasse zugegriffen werden. Die Konsequenz ist,
dass die Objekte diese Variable gemeinsam benutzen.
Klassenvariablen werden oft dann eingesetzt, wenn ein Wert für alle Instanzen einer
Klasse gleich sein soll. Anstatt diesen gleichen Wert in jedem einzelnen Objekt abzu-
legen (eine Verschwendung von Platz und möglicherweise schwierig zu koordinie-
ren), kann ein einzelner Wert von allen Instanzen geteilt werden.
Java unterstützt auch Klassenmethoden (auch als statische Methoden bekannt),
also Methoden, die zu einer Klasse gehören. Wir werden diese später in einem eige-
nen Abschnitt diskutieren.

251
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

6.14.2 Konstanten
Das Schlüsselwort static wird häufig bei der Definition von Konstanten verwen-
det. Konstanten sind ähnlich wie Variablen, können aber ihren Wert während der
Ausführung einer Anwendung nicht mehr ändern. In Java werden Konstanten
mit dem Schlüsselwort final definiert. Beispielsweise:
private final int GROESSE = 10;
Hier definieren wir eine Konstante GROESSE mit dem Wert 10. Wir stellen fest, dass
Konstantendeklarationen Variablendeklarationen sehr ähnlich sind, mit zwei Unter-
schieden:
 Sie enthalten das Schlüsselwort final vor dem Typnamen.
 Sie müssen bei ihrer Deklaration auch unmittelbar initialisiert werden.
Wenn eine Variable ihren Wert nicht ändern soll, dann sollte sie mit final dekla-
riert werden. Dies garantiert, dass sie später nicht aus Versehen geändert werden
kann. Der Versuch, ein konstantes Datenfeld zu ändern, führt zu einer Fehlermel-
dung bei der Übersetzung. Konstanten werden nach Konvention häufig in Groß-
buchstaben geschrieben. Wir folgen dieser Konvention in diesem Buch.
In der Praxis kommt es häufig vor, dass Konstanten für alle Instanzen einer Klasse
gelten sollen. In solchen Situationen verwendet man Klassenkonstanten. Klassen-
konstanten sind konstante Datenfelder einer Klasse. Ein Beispiel:
private static final int GROESSE = 10;
Die Definition von GRAVITATION in unserem Projekt Baelle ist ein Beispiel für eine sol-
che Konstante. Dies ist die Art, in der Konstanten meist benutzt werden. Instanz-
bezogene Konstanten kommen sehr viel seltener vor.
Wir haben zwei weitere Beispiele für Konstanten in dem Projekt Kritzeln kennen-
gelernt. Das erste Beispiel besteht aus zwei Konstanten in der Klasse Stift, die die
Größe des Zufallsgekritzel definieren (kehren Sie zurück zum Projekt und suchen
Sie danach!). Das zweite Beispiel war die Verwendung der Farbkonstanten in die-
sem Projekt wie [Link]. In diesem Fall haben wir die Konstanten nicht definiert,
sondern Konstanten verwendet, die in einer anderen Klasse definiert sind.
Der Grund, warum wir die Konstanten der Klasse Color verwenden konnten, ist,
dass wir sie als public deklariert haben. Im Gegensatz zu anderen Datenfeldern
(die, wie wir vorher berichtet haben, niemals als public deklariert werden sollten)
ist das Deklarieren von öffentlichen Konstanten im Allgemeinen unproblematisch
und manchmal nützlich.

Übung 6.74 Schreiben Sie Konstantendeklarationen für folgende Datenfelder:


 ein öffentliches Datenfeld für eine Toleranz bei Messwerten mit dem
Wert 0.001;
 ein privates Datenfeld für den Prozentsatz zum Bestehen einer Klausur
mit dem Wert 40;
 ein öffentliches Datenfeld vom Typ char, das den Buchstaben „h“ für das
Hilfe-Kommando festlegt.

252
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.15 Klassenmethoden

Übung 6.75 Welche Konstantennamen werden in der Klasse [Link]


definiert?
Übung 6.76 Geben Sie Gründe dafür an, warum es sinnvoll ist, in einem Pro-
gramm, das die Konstante 73,28166 an zehn verschiedenen Orten verwendet,
diesen Wert mit einem Variablennamen zu verknüpfen.

6.15 Klassenmethoden
Alle bisher betrachteten Methoden waren Instanzmethoden: Sie werden an einer
Instanz einer Klasse aufgerufen. Wir unterscheiden Klassenmethoden von Ins-
tanzmethoden dadurch, dass Klassenmethoden ohne eine Instanz aufgerufen
werden können – die Klasse selbst reicht aus.

6.15.1 Statische Methoden


In Abschnitt 6.14 haben wir Klassenvariablen diskutiert. Klassenmethoden sind
konzeptuell verwandt und benutzen eine verwandte Syntax (das Schlüsselwort
static in Java). Genau wie Klassenvariablen zu einer Klasse statt zu einer Instanz
gehören, gehören auch Klassenmethoden zu einer Klasse.
Eine Klassenmethode wird definiert, indem das Schlüsselwort static vor den Typ-
namen in einer Methodensignatur gestellt wird:
public static int gibAnzahlErzeugterInstanzen()
{
...
}
Solch eine Methode kann dann aufgerufen werden, indem in der üblichen Punkt-
schreibweise dem Punkt der Name der definierenden Klasse vorangestellt wird.
Wenn diese Methode beispielsweise in der Klasse Gegenstand definiert ist, dann
kann sie mit folgender Anweisung aufgerufen werden:
int anzahlGegenstände = [Link]();
Beachten Sie, dass der Name der Klasse vor dem Punkt steht – es musste hier kein
Objekt erzeugt werden, um die Methode aufzurufen.

Übung 6.77 Lesen Sie die Dokumentation der Klasse Math im Paket
[Link]. Sie enthält viele statische Methoden. Finden Sie die Methode, die
das Maximum zweier ganzer Zahlen ermittelt. Wie sieht ihr Kopf aus?
Übung 6.78 Was denken Sie, warum diese Methoden in der Klasse Math
statisch sind? Könnten sie auch als Instanzmethoden geschrieben werden?

253
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Übung 6.79 Schreiben Sie eine Testklasse mit einer Methode, die das Zählen
in einer Schleife von 1 bis 10000 misst. Sie können dazu die Methode cur-
rentTimeMillis aus der Klasse System verwenden, die bei Zeitmessungen hel-
fen kann.
Übung 6.80 Kann eine statische Methode aus einer Instanzmethode heraus
aufgerufen werden? Kann eine Instanzmethode aus einer statischen
Methode heraus aufgerufen werden? Kann eine statische Methode von
einer anderen statischen Methode aufgerufen werden? Beantworten Sie
diese Fragen schriftlich, erzeugen Sie dann ein Testprojekt, um Ihre Antwor-
ten zu überprüfen, und probieren Sie sie aus. Erklären Sie Ihre Antworten
und Beobachtungen detailliert.
Übung 6.81 Die Klasse Collections in den [Link]-Paketen enthält eine
Vielzahl an statischen Methoden, die mit Sammlungen wie ArrayList, Linked-
List, HashMap und HashSet benutzt werden können. Lesen Sie beispielsweise
die Klassendokumentation für die Methoden min, max und sort. Auch wenn Sie
eventuell die Erklärungen oder die Notation nicht vollständig verstehen, wird
das Wissen darum, dass es diese Methoden gibt, für Sie schon hilfreich sein.

6.15.2 Einschränkungen für Klassenmethoden


Weil Klassenmethoden mit einer Klasse statt mit einer Instanz verknüpft sind, gelten
zwei wichtige Einschränkungen für sie. Die erste Einschränkung ist, dass eine Klas-
senmethode nicht auf Instanzvariablen zugreifen kann, die innerhalb der Klasse
definiert sind. Das ist logisch, denn Instanzvariablen sind mit einzelnen Instanzen
verknüpft. Klassenmethoden können lediglich auf die Klassenvariablen ihrer Klasse
zugreifen. Die zweite Einschränkung ist der ersten ähnlich: Eine Klassenmethode
kann keine Instanzmethoden ihrer Klasse aufrufen. Eine Klassenmethode kann aus-
schließlich andere Klassenmethoden ihrer Klasse aufrufen.
Sie werden feststellen, dass wir in den Beispielen in diesem Buch nur sehr wenig
Gebrauch von Klassenmethoden machen.

6.16 Programmausführung ohne BlueJ


Wenn wir ein Programm fertig implementiert haben, möchten wir es möglicher-
weise an andere zum Ausprobieren weitergeben. Dazu wäre es schön, wenn
andere das Programm ausführen können, ohne BlueJ starten zu müssen. Um dies
tun zu können, müssen wir ein weiteres Konzept einführen: Klassenmethoden,
die in Java auch statische Methoden genannt werden.

6.16.1 Die Methode main


Wenn wir eine Java-Anwendung ohne BlueJ starten wollen, dann benötigen wir
dazu eine Klassenmethode. In BlueJ erzeugen wir typischerweise ein Objekt und

254
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.17 Weitere fortgeschrittene Konzepte

rufen eine seiner Methoden auf, aber ohne BlueJ muss eine Anwendung auch
ohne ein existierendes Objekt gestartet werden können. Klassen sind das Einzige,
das von Anfang an „da“ ist, entsprechend muss die erste aufzurufende Methode
eine Klassenmethode sein.
Die Java-Definition zum Starten einer Anwendung ist recht simpel: Der Benutzer
gibt die Klasse an, die den Startpunkt bilden soll, und das Java-System ruft an die-
ser Klasse die Methode main auf. Diese Methode muss eine fest definierte Signatur
haben:
public static void main(String[] args)
Wenn eine solche Methode in der Klasse nicht existiert, wird eine Fehlermeldung
ausgegeben. Anhang E beschreibt die Details dieser Methode und die notwendi-
gen Befehle, um ein Java-Programm ohne BlueJ starten zu können.

Übung 6.82 Fügen Sie eine main-Methode Ihrer Klasse Kundendienstsystem im


Projekt Technischer-Kundendienst hinzu. Diese Methode sollte ein Objekt der
Klasse Kundendienstsystem erzeugen und an diesem die Methode starten auf-
rufen. Testen Sie die Methode main, indem Sie sie innerhalb von BlueJ aufrufen.
Klassenmethoden können aus dem Kontextmenü einer Klasse aufgerufen
werden.
Übung 6.83 Starten Sie Ihr Programm ohne BlueJ.
Übung 6.84 Kann eine Klasse zählen, wie viele Instanzen von dieser Klasse
erzeugt wurden? Was benötigt man dazu? Schreiben Sie einige Codefrag-
mente, mit denen Sie die notwendigen Schritte illustrieren. Nehmen Sie an,
Sie möchten eine statische Methode namens anzahlInstanzen haben, die die
Anzahl der erzeugten Instanzen zurückgibt.

6.17 Weitere fortgeschrittene Konzepte


Im vorigen Kapitel haben wir Konzepte eingeführt, die wir als „fortgeschritten“
bezeichnet haben, und vorgeschlagen, diese Inhalte beim ersten Lesen eventuell
zu überspringen. In diesem Abschnitt bieten wir weitere vertiefende Inhalte an –
und geben denselben Rat.

6.17.1 Polymorphe Sammlungstypen


(fortgeschrittene Konzepte)
Bisher haben wir mehrere unterschiedliche Sammlungsklassen aus dem [Link]-
Paket vorgestellt, wie ArrayList, HashMap und HashSet. Jede Klasse hat einige charak-
teristische Funktionen, die sie für bestimmte Situationen geeignet macht. Die Klas-
sen besitzen jedoch auch sehr viele Ähnlichkeiten und gemeinsame Methoden. Dies
legt nahe, dass sie auf gewisse Weise miteinander verknüpft sind. In der objektori-
entierten Programmierung werden Beziehungen zwischen Klassen über das Kon-

255
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

zept der Vererbung ausgedrückt. Wir werden dieses Thema in den Kapiteln 10 und
11 ausführlich behandeln, aber es ist sinnvoll, es hier kurz anzureißen, um Ihr Ver-
ständnis für die unterschiedlichen Arten von Sammlungen zu vertiefen.
Wenn wir verstehen wollen, wie die unterschiedlichen Sammlungsklassen zu ver-
wenden sind, ist es hilfreich, deren Namen heranzuziehen. Es scheint nahezulie-
gen, aus dem Namen abzuleiten, dass ein HashSet in ähnlicher Weise zu benut-
zen ist wie eine HashMap. Tatsächlich haben wir jedoch gesehen, dass ein HashSet
in seiner Benutzung sehr viel enger mit einer ArrayList verwandt ist. Die Namen
bestehen aus zwei Teilen, zum Beispiel „Array“ und „List“. Die zweite Hälfte gibt
einen Hinweis auf die Art der Sammlung, mit der wir es zu tun haben (List, Map,
Set), und die erste Hälfte teilt uns mit, wie sie implementiert ist, zum Beispiel mit-
tels eines Arrays (wird in Kapitel 7 besprochen) oder durch Hashing (wird in Kapi-
tel 11 behandelt).
Bei der Programmierung mit Sammlungen ist die Art der Sammlung (der zweite
Teil) am wichtigsten. Wir haben bereits angesprochen, dass wir von der Imple-
mentierung abstrahieren können; wir müssen nicht groß darüber nachdenken.
So gesehen sind sich für unsere Zwecke HashSet und TreeSet sehr ähnlich. Beides
sind Mengen, daher verhalten sie sich entsprechend gleich. Der größte Unter-
schied liegt in ihrer Implementierung, was nur eine Rolle spielt, wenn Effizienz ins
Spiel kommt: Eine Implementierung wird einige Operationen viel schneller durch-
führen als die andere. Wir werden uns den Fragen der Effizienz allerdings erst
später zuwenden und nur, wenn wir es mit sehr großen Sammlungen oder
Anwendungen zu tun haben, für die die Leistung kritisch ist.
Eine Konsequenz aus dieser Ähnlichkeit ist, dass wir oft Details der Implementie-
rung ignorieren können, wenn wir eine Variable deklarieren, die sich auf ein
bestimmes Sammlungsobjekt bezieht. Mit anderen Worten, wir können die Vari-
able so deklarieren, als wäre sie von einem abstrakteren Typ, wie List, Map oder
Set, anstatt eines konkreteren, wie ArrayList, LinkedList, HashMap, TreeSet usw.
Zum Beispiel:
List<Track> tracks = new LinkedList<>();
oder
Map<String, String> antwortMap = new HashMap<>();
Hier ist die Variable als List-Typ deklariert, obwohl das erzeugte und gespei-
cherte Objekt vom Typ LinkedList ist (dasselbe gilt für Map und HashMap).
Variablen wie tracks und antwortMap in diesen Beispielen werden polymorphe
Variablen genannt, weil sie in der Lage sind, auf Objekte mit unterschiedlichen,
aber verwandten Typen zu verweisen. Die Variable tracks könnte entweder auf
eine ArrayList oder auf eine LinkedList verweisen, während antwortMap auf ein
HashMap oder ein HashTree verweisen könnte. Die grundlegende Idee, Variablen
auf diese Weise zu deklarieren, besteht darin, dass die Art, wie die Variablen im
restlichen Code verwendet werden, unabhängig vom exakten, konkreten Objekt-
typ ist, auf den sie verweisen. Ein Aufruf an add, clear, get, remove oder size auf
tracks hätte überall dieselbe Syntax, und alle haben dieselbe Auswirkung auf die
verknüpfte Sammlung. Der exakte Typ spielt selten jemals eine Rolle. Ein Vorteil
der Vewendung polymorpher Variablen ist: Falls wir jemals von einem konkreten

256
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.17 Weitere fortgeschrittene Konzepte

Typ zu einem anderen wechseln wollen, muss der Code nur an einer einzigen
Stelle geändert werden, nämlich dort, wo das Objekt erzeugt wird. Ein weiterer
Vorteil ist, dass unser Code nicht den konkreten Typ kennen muss, der mögli-
cherweise im Code, der von anderen Programmierern stammt, benutzt wird –
wir müssen nur den abstrakten Typ kennen: List, Map, Set usw. Beide Vorteile
reduzieren die Kopplung zwischen unterschiedlichen Teilen eines Programms,
was eine gute Sache ist – wir werden dies in Kapitel 8 vertiefen.
Wir werden die Mechanismen hinter polymorphen Variablen ausführlich in den
Kapiteln 10 und 11 untersuchen.

6.17.2 Die Methode collect aus Streams


(fortgeschritten)
In diesem Abschnitt führen wir die Diskussion zu Streams und Lambda-Ausdrücken
von Java 8 weiter, die wir in Kapitel 5 begonnen hatten. Dort hatten wir erklärt,
dass es wichtig zu wissen ist, dass jede Stream-Operation den Eingabe-Stream
unverändert lässt. Jede Operation erzeugt einen neuen Stream, basierend auf der
Eingabe und der Operation, der an die nächste Operation in der Folge weitergege-
ben wird. Wir werden jedoch häufig Stream-Operationen einsetzen wollen, die
eine neue Version der ursprünglichen Sammlung erzeugen, also eine veränderte
Kopie – in der Regel durch Filtern der Originalinhalte, indem entweder nur die
gewünschten Objekte ausgewählt oder diejenigen Elemente aussortiert werden,
die nicht benötigt werden. Dazu müssen wir die Methode collect auf den Stream
anwenden. Dies ist eine terminale Operation und wird daher als letzte Operation
in einer Pipeline erscheinen. Sie kann eingesetzt werden, um die Elemente, die aus
einer Stream-Operation resultieren, wieder einer neuen Operation zuzuführen.
Es gibt sogar zwei Versionen der collect-Methode, aber wir werden uns nur auf
die einfachere konzentrieren. Tatsächlich werden wir unser Augenmerk nur dar-
auf konzentrieren, wie wir ein neues Sammlungsobjekt als letzte Operation einer
Pipeline erzeugen können, und überlassen weitere Untersuchungen dem interes-
sierten Leser. Selbst die elementarste Nutzung bezieht mehrere Konzepte ein, die
für dieses relativ frühe Stadium des Buchs schon fortgeschritten sind: Streams,
statische Methoden und Polymorphismen.
Die collect-Methode hat als Parameter einen Collector. Dies ist im Wesentlichen
etwas, das schrittweise die Elemente seines Eingabe-Streams irgendwie akkumu-
lieren kann. Dies ähnelt der reduce-Operation auf Streams, die wir im vorigen
Kapitel behandelt haben. Dort haben wir gesehen, dass ein Stream aus Zahlen
auf eine einzige Zahl reduziert werden konnte. Hier werden wir einen Stream von
Objekten auf ein einzelnes Objekt reduzieren, das eine weitere Sammlung ist. Da
es uns häufig nicht besonders interessiert, ob eine Sammlung eine ArrayList,
eine LinkedList oder irgendeine andere Form von Listtyp ist, die wir nocht nicht
kennengelernt haben, wir können den Collector anweisen, die Sammlung in der
Form des polymorphen Typs List zurückzugeben.
Der einfachste Weg, einen Collector zu erhalten, ist eine der static-Methoden
zu verwenden, die in der Collectors-Klasse im Paket [Link] definiert
sind: toList, toMap und toSet.

257
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Listing 6.9 zeigt eine Methode aus dem Nationalpark-Projekt, das wir uns in
Kapitel 5 angesehen haben. Diese Methode filtert die vollständige List von Sich-
tung-Objekten, um eine neue List nur aus Sichtungen für ein bestimmtes Tier zu
erzeugen. In unserer Version im Tiermonitoring-v1-Projekt wurde diese Methode
im iterativen Stil mit einer for-each-Schleife geschrieben. Hier ist sie im funktio-
nalen Stil unter Verwendung der collect-Methode formuliert.

Listing 6.9
Eine funktionale Ver-
sion der Methode
gibSichtungUeber.

Ein heikler Punkt, den es zu beachten gilt, ist, dass die Methode toList von Collec-
ters gibt kein echtes List-Objekt zur Weitergabe an die collect-Methode zurück-
gibt. Stattdessen liefert sie einen Collector zurück, der schließlich zur Erzeugung
eines passenden konkreten List-Objekts führt.

Übung 6.85 Implementieren Sie diese Version der Methode gibSichtung-


Ueber in Ihrem eigenen Tiermonitoring-Projekt.

Übung 6.86 Schreiben Sie die Methode gibSichtungenInGebiet aus dem


ursprünglichen Tiermonitoring-Projekt im funktionalen Stil neu.

6.17.3 Methodenreferenzen (fortgeschritten)


Angenommen, wir wollten die Objekte aus einem gefilterten Stream in einem
bestimmten Sammlungstyp wie einer ArrayList zusammenfassen, anstatt einen
polymorphen Typ zu verwenden. Dazu muss der Ansatz, den wir im vorigen
Abschnitt genutzt haben, abgewandelt werden. Es ist immer noch notwendig, der
collect-Operation einen Collector zu übergeben, doch diesen müssen wir auf
eine andere Weise erlangen. Listing 6.10 zeigt, wie dies gemacht werden könnte.

Listing 6.10
Eine weitere Version
der Methode
gibSichtungUeber.

Dieses Mal haben wir die statische Methode toCollection von Collectors aufge-
rufen, die einen Parameter vom Typ Supplier besitzt. Der Parameter, den wir
benutzt haben, führt eine Notation ein, die Methodenreferenz genannt wird, zu
erkennen an den beiden aufeinanderfolgenden Doppelpunkten.

258
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.17 Weitere fortgeschrittene Konzepte

Methodenreferenzen sind eine bequeme Kurzschrift für einige Lambda-Ausdrü-


cke. Sie werden häufig verwendet, wenn der Compiler auf den vollständigen
Aufruf einer Methode einfach schließen kann, ohne dass diese im Detail explizit
spezifiziert werden müssen. Es gibt vier Fälle von Methodenreferenzen:
a Eine Referenz auf einen Konstruktor, so wie wir es hier verwendet haben:
ArrayList::new
(Beachten Sie, dass streng genommen ein Konstruktor nicht wirklich eine
Methode ist.) Dies ist eine Abkürzung für den Lambda-Ausdruck
() -> new ArrayList<>()
b Eine Referenz auf einen Methodenaufruf auf ein bestimmtes Objekt:
[Link]::println
Dies wäre eine Abkürzung für den Lambda-Ausdruck:
str -> [Link](str)
Beachten Sie, das [Link] eine public static Variable der Klasse System
ist und der Parameter str würde durch den Kontext geliefert, in dem die
Methodenreferenz verwendet wird, zum Beispiel als terminale Operation ei-
ner Stream-Pipeline:
forEach([Link]::println)
c Eine Referenz auf eine statische Methode:
Math::abs
Dies wäre eine Abkürzung für
x -> [Link](x)
Der Parameter x würde vom Kontext kommen, in dem die Methodenrefe-
renz verwendet wird, zum Beispiel als Teil der Map-Operation eines Streams:
map(Math::abs)
d Eine Referenz auf eine Instanzmethode eines bestimmten Typs:
String::length
Dies wäre eine Abkürzung für:
str -> [Link]()
Auch hier wird str wieder dem Kontext entnommen. Es ist wichtig, zwi-
schen dieser Nutzungsart, bei der es um eine Instanzmethode geht, und
Nutzung b), wo eine spezifische Instanz gemeint ist, sowie Nutzung c), die
eine statische Methode umfasst, zu unterscheiden.
Wir haben hier nur die einfachsten Beispiele für den Einsatz von Methodenrefe-
renzen aufgeführt. Komplexeren Fällen, die mehrere Parameter umfassen, wird
man auch in den allgemeineren Beispielen begegnen.

259
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Zusammenfassung
Der Umgang mit Bibliotheksklassen und Klassenschnittstellen ist essenziell
für einen kompetenten Programmierer. Dies hat zwei Aspekte: Zum einen
muss er die Beschreibung von Bibliotheksklassen lesen können (insbeson-
dere ihre Schnittstellen), zum anderen muss er sie auch erstellen können.
Es ist wichtig, einige zentrale Klassen der Java-Standardklassenbibliothek zu
kennen und zu wissen, wie man über weitere bei Bedarf etwas herausfin-
den kann. In diesem Kapitel haben wir einige der wichtigsten Klassen vor-
gestellt und gezeigt, wie man die Dokumentation der Bibliothek benutzt.
Es ist außerdem wichtig, in der Lage zu sein, jede selbst geschriebene Klasse
im Stil der Bibliotheksklassen zu dokumentieren, damit andere Programmie-
rer diese Klasse leicht benutzen können, ohne ihre Implementierung kennen
zu müssen. Diese Dokumentation sollte gute Kommentare für jedes Projekt,
jede Klasse und jede Methode enthalten. Mithilfe von javadoc können Sie
solche Dokumentationen erzeugen.

NEUE BEGRIFFE IN DIESEM KAPITEL


Schnittstelle, Implementierung, Abbildung (Map), Menge (Set), Autoboxing,
Wrapper-Klassen, javadoc, Zugriffsmodifikator, Geheimnisprinzip, Klassen-
variable, Klassenmethode, main-Methode, static, Konstante, final, poly-
morphe Variable, Methodenreferenz

260
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.17 Weitere fortgeschrittene Konzepte

Zusammenfassung der Konzepte


 Java-Bibliothek Die Standardklassenbibliothek von Java enthält viele
Klassen, die sehr nützlich sind. Es ist wichtig zu wissen, wie man die Bib-
liothek benutzen kann.
 Dokumentation einer Bibliothek Die Dokumentation der Standardbib-
liothek von Java zeigt Informationen über alle Klassen in der Bibliothek.
Die Benutzung dieser Dokumentation ist unerlässlich für einen vernünfti-
gen Gebrauch der Bibliotheksklassen.
 Schnittstelle Die Schnittstelle einer Klasse beschreibt, was eine Klasse
leistet und wie sie benutzt werden kann, ohne dass ihre Implementierung
sichtbar wird.
 Implementierung Der komplette Quelltext, der eine Klasse definiert, wird
als die Implementierung dieser Klasse bezeichnet.
 Unveränderlich Ein Objekt wird als unveränderlich bezeichnet, wenn
sein Inhalt oder Zustand nicht mehr geändert werden kann, nachdem es
erzeugt wurde. String ist ein Beispiel für eine Klasse, die unveränderliche
Objekte definiert.
 Map Eine Map ist eine Sammlung, die Schlüssel-Wert-Paare als Einträge
enthält. Ein Wert kann ausgelesen werden, indem ein Schlüssel angegeben
wird.
 Set Eine Menge (Set) ist eine Sammlung, in der jedes Element nur maxi-
mal einmal enthalten ist. Die Elemente einer Menge haben keine spezifi-
sche Ordnung.
 Autoboxing Autoboxing wird automatisch durchgeführt, wenn der Wert
eines primitiven Typs in einem Kontext verwendet wird, in dem ein Wrap-
per-Typ erwartet wird.
 Dokumentation Die Dokumentation einer Klasse sollte genau die Infor-
mationen bieten, die andere Programmierer benötigen, um die Klasse
ohne einen Blick auf ihre Implementierung benutzen zu können.
 Zugriffsmodifikatoren Zugriffsmodifikatoren definieren die Sichtbarkeit
eines Datenfelds, eines Konstruktors oder einer Methode. Öffentliche
(public) Elemente sind sowohl innerhalb der definierenden Klasse zugreif-
bar als auch von anderen Klassen aus; private (private) Elemente sind
ausschließlich innerhalb der definierenden Klasse zugreifbar.
 Geheimnisprinzip Das Geheimnisprinzip besagt, dass die internen Details
der Implementierung einer Klasse vor anderen Klassen verborgen sein soll-
ten. Dies unterstützt eine bessere Modularisierung von Anwendungen.
 Klassenvariablen, statische Variablen Auch Klassen können Datenfel-
der haben. Diese werden Klassenvariablen oder auch statische Variablen
genannt. Von einer Klassenvariablen existiert immer nur genau eine Kopie,
unabhängig von der Anzahl der erzeugten Instanzen.

261
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen

Übung 6.87 Im Internet geht das Gerücht um, dass George Lucas (der Schöp-
fer der Star-Wars-Filme) eine Formel benutzt, um Namen für die Charaktere in
seinen Geschichten zu erzeugen (Jar Jar Binks, Obiwan Kenobi etc.). Diese For-
mel lautet – angeblich:
Ihr Star-Wars-Vorname:
1 Nehmen Sie die ersten drei Buchstaben Ihres Nachnamens.

2 Fügen Sie diesen die ersten beiden Buchstaben Ihres Vornamens hinzu.
Ihr Star-Wars-Nachname:
1 Nehmen Sie die ersten beiden Buchstaben vom Mädchennamen Ihrer
Mutter.
2 Fügen Sie diesen die ersten drei Buchstaben Ihrer Geburtsstadt hinzu.
Ihre Aufgabe lautet nun: Erzeugen Sie ein BlueJ-Projekt namens Star-Wars.
Legen Sie darin eine Klasse Namensgenerator an. Diese Klasse soll eine Methode
generiereStarWarsNamen haben, die einen Namen nach dem oben angegebe-
nen Schema produziert. Sie werden dazu die Methode substring der Klasse
String benötigen, mit der Teile aus einer Zeichenkette herausgeschnitten wer-
den können.
Übung 6.88 Die folgende Methode versucht, einen String in Großbuchsta-
ben auszugeben:
public void inGrossbuchstabenAusgeben(String s)
{
[Link](); [Link](s);
}
Diese Methode funktioniert jedoch nicht. Finden Sie heraus, warum nicht,
und erklären Sie es. Wie sollte es richtig aussehen?
Übung 6.89 Nehmen Sie an, wir wollen die Werte zweier int-Variablen a
und b vertauschen. Dazu schreiben wir die Methode
public void tausche(int i1, int i2)
{
int tmp = i1;
i1 = i2;
i2 = tmp;
}
Anschließend rufen Sie diese Methode mit unseren Variablen a und b auf:
tausche(a,b);
Sind a und b nach diesem Aufruf vertauscht? Wenn Sie es testen, werden
Sie sehen, dass sie es nicht sind! Warum funktioniert dies nicht? Erklären Sie
es detailliert.

262
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

7 Sammlungen mit fester


Größe – Arrays

Lernziele
Zentrale Konzepte in diesem Kapitel: Arrays, for-Schleife
Java-Konstrukte in diesem Kapitel: Array, for-Schleife, ++, Konditional-
operator, ?:
Der Hauptfokus dieses Kapitels ist die Verwendung von Array-Objekten für
Sammlungen fester Größe und die for-Schleife.

7.1 Sammlungen fester Größe


In Kapitel 4 haben wir uns angesehen, in welchen Bibliotheksklassen, zum Beispiel
ArrayList, wir Sammlungen von Objekten verwalten können. Wir sind in Kapitel 6
auch weiteren Sammlungstypen wie HashSet und HashMap begegnet. Ein wesentli-
ches Merkmal all dieser Sammlungstypen ist ihre flexible Kapazität – wir müssen
nie festlegen, wie viele Elemente eine konkrete Sammlung aufnehmen wird, und
die Anzahl der gespeicherten Elemente kann während der Lebensdauer der Samm-
lung beliebig variieren.
In diesem Kapitel geht es nun um Sammlungen, die keine flexible, sondern eine feste
Kapazität besitzen – zu dem Zeitpunkt, an dem das Sammlungsobjekt erzeugt
wird, müssen wir die maximale Anzahl der Elemente angeben, die gespeichert
werden können, und diese Anzahl kann auch nicht mehr geändert werden. Das
mag zunächst wie eine unnötige Einschränkung erscheinen. Es gibt allerdings
Situationen, in denen wir im Voraus wissen, wie viele Elemente wir in einer
Sammlung ablegen wollen, und diese Anzahl bleibt dann über die Lebenszeit
der Sammlung konstant. Für solche Situationen haben wir die Möglichkeit, eine
Sammlung mit einer festen Größe zum Speichern der Objekte zu wählen.

Konzept
7.2 Arrays Ein Array ist eine
Eine Sammlung mit einer festen Größe wird Array genannt. Obwohl die feste Größe besondere Art von
bei Arrays in vielen Situationen ein deutlicher Nachteil sein kann, haben sie gegen- Sammlung, die eine
festgelegte Anzahl
über Sammlungen mit flexibler Größe mindestens zwei Vorteile:
von Elementen
 Der Zugriff auf die Elemente in einem Array ist häufig sehr viel effizienter als halten kann.
auf die Elemente einer vergleichbaren Sammlung mit flexibler Größe.

263
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

 In Arrays können neben Objekten auch Werte der primitiven Typen abgelegt wer-
den. Sammlungen mit flexibler Größe können ausschließlich Objekte speichern.1
Ein weiterer Unterschied ist, dass es für Arrays eine spezielle syntaktische Unterstüt-
zung in Java gibt – der Zugriff auf ihre Elemente erfolgt mit einer besonderen Syntax,
die von den üblichen Methodenaufrufen abweicht. Dies hat in erster Linie historische
Gründe: Arrays sind die älteste Sammlungsstruktur in Programmiersprachen, die Syn-
tax für den Umgang mit Arrays hat sich über Jahrzehnte hinweg entwickelt. In Java
wurde die gleiche Syntax beibehalten, die sich in anderen Sprachen bereits durchge-
setzt hatte. Dadurch sollte ein Umstieg für Programmierer anderer Sprachen erleichtert
werden, obwohl diese Syntax nicht konsistent mit dem Rest der Syntax von Java ist.
In den folgenden Abschnitten werden wir zeigen, wie Arrays benutzt werden kön-
nen, um Sammlung mit fester Größe zu verwalten. Wir werden auch ein neues
Schleifenkonstrukt vorstellen, das im Zusammenhang mit Arrays eine wichtige Rolle
spielt – die for-Schleife. (Beachten Sie, dass die for-Schleife sich von der for-each-
Schleife unterscheidet.)

7.3 Die Analyse einer Logdatei


Webserver verwalten üblicherweise einige Logdateien, in denen sie Informationen
über die Zugriffe auf die Webseiten ablegen, die sie zur Verfügung stellen. Mit
geeigneten Werkzeugen können die Betreiber der Webdienste daraus nützliche
Informationen extrahieren, beispielsweise:
 welche die beliebteste Seite im Angebot ist
 über welche Sites die Benutzer auf die eigene Site geleitet wurden
 ob andere Webseiten defekte Links auf die eigenen Seiten haben
 wie viele Daten an Kunden geliefert wurden
 zu welchen Zeiten über einen Tag, eine Woche oder einen Monat die Zugriffe
besonders zahlreich sind
Solche Informationen können den Administratoren bei der Entscheidung helfen, ob
beispielsweise leistungsfähigere Server angeschafft werden müssen oder welche Zei-
ten aufgrund der geringen Zugriffe am besten für Wartungsarbeiten geeignet sind.
Das Projekt Weblog-Auswertung enthält eine Anwendung, die eine Analyse der
Daten eines Webservers durchführt. Der Server schreibt bei jedem Zugriff auf
eine Seite eine Zeile in eine Logdatei. Ein Beispiel für eine solche Logdatei mit dem
Namen [Link] ist im Projektordner enthalten. Jede Zeile enthält das Datum
und die Uhrzeit eines Zugriffs in folgendem Format:
Jahr Monat Tag Stunde Minute
Die folgende Zeile beispielsweise protokolliert einen Zugriff um 03:45 Uhr am
7. Juni 2017:
2017 06 07 03 45

1 Auch wenn die Autoboxing-Funktion, die wir in Kapitel 6 beschrieben haben, einen Mechanis-
mus bietet, mit dem wir auch primitive Typen in flexible Sammlungen einfügen, können den-
noch nur Arrays sie direkt speichern.

264
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.3 Die Analyse einer Logdatei

Das Projekt besteht aus fünf Klassen: ProtokollAuswerter, LogdateiLeser, Logeintrag,


LogzeilenZerleger und LogdateiErzeuger. Wir werden überwiegend die Klasse Pro-
tokollAuswerter betrachten, da diese Beispiele für die Erzeugung und Benutzung
eines Arrays enthält (Listing 7.1). Spätere Übungen werden Sie dazu ermuntern,
auch die Klasse Logeintrag zu untersuchen und zu modifizieren, da diese eben-
falls ein Array benutzt. Die Klassen LogdateiLeser und LogzeilenZerleger benutzen
Konzepte der Sprache Java, die wir bisher noch nicht betrachtet haben, wir wer-
den sie deshalb nicht im Detail untersuchen. Die Klasse LogdateiErzeuger erlaubt es
Ihnen, eigene Logdateien mit Zufallsdaten zu erzeugen.

Listing 7.1
Die Auswertung
einer Logdatei für
Webzugriffe.

265
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Die Auswertung benutzt momentan lediglich Teile der Informationen, die in einer
Logzeile stehen. Die Auswertung liefert Informationen darüber, welche Stunden des
Tages durchschnittlich die meisten und die wenigsten Zugriffe haben. Dies
geschieht, indem für jeden Zeitabschnitt von einer Stunde die Zugriffe gezählt wer-
den, die in der Logdatei vermerkt sind.

Übung 7.1 Erkunden Sie das Projekt Weblog-Auswertung, indem Sie ein
ProtokollAuswerter-Objekt erzeugen und seine Methode analysiereStun-
dendaten aufrufen. Rufen Sie anschließend die Methode stundendatenAusge-
ben auf, die das Ergebnis der Analyse ausgibt. Welche sind die aktivsten
Stunden eines Tages?

In den nächsten Abschnitten werden wir untersuchen, wie diese Klasse ihre Auf-
gabe mithilfe eines Arrays erfüllt.

7.3.1 Array-Variablen deklarieren


Die Klasse ProtokollAuswerter enthält ein Datenfeld mit einem Array-Typ:
private int[] zugriffeInStunde;
Das Erkennungsmerkmal bei der Deklaration einer Array-Variablen sind die ecki-
gen Klammern als Teil des Typnamens: int[]. Dies besagt hier, dass die Variable
zugriffeInStunde den Typ Array von Ganzzahlen hat. Wir sagen, dass int der
Basistyp dieses speziellen Arrays ist, was bedeutet, dass das Array-Objekt Werte
vom Typ int speichert. Die Deklaration einer Array-Variablen sollte nicht mit der
ähnlich aussehenden Deklaration einer einfachen Variablen verwechselt werden:
int stunde; // eine einfache Variable vom Typ int
int[] zugriffeInStunde; // eine Array-Variable vom Typ int
Die Variable stunde kann den Wert einer einzelnen ganzen Zahl speichern, wäh-
rend zugriffeInStunde ein Array-Objekt referenzieren wird, sobald dieses erzeugt
wurde. Durch die Deklaration einer Array-Variablen wird noch kein Objekt erzeugt.
Dies geschieht erst als getrennter Schritt durch eine new-Anweisung, wie bei ande-
ren Objekten auch.
Wir müssen uns diese ungewöhnliche Syntax noch einmal genauer ansehen. Die
Deklaration int[] würde in konventioneller Syntax vermutlich eher als Array<int>
erscheinen. Das hat, wie bereits gesagt, eher historische als logische Gründe. Sie
sollten sich angewöhnen, es auf die gleiche Weise zu lesen: als „Array von ints“.

Übung 7.2 Schreiben Sie eine Deklaration für eine Array-Variable leute, die auf
ein Array von Person-Objekten verweisen kann.
Übung 7.3 Schreiben Sie eine Deklaration für eine Array-Variable verfügbar,
die auf ein Array von boolean-Werten verweisen kann.

266
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.3 Die Analyse einer Logdatei

Übung 7.4 Untersuchen Sie den Quelltext der Klasse ProtokollAuswerter


und finden Sie alle Stellen, an denen das Datenfeld zugriffeInStunde ver-
wendet wird. Kümmern Sie sich an dieser Stelle noch nicht darum, wozu
diese unterschiedlichen Benutzungen dienen, da wir dies in den nächsten
Abschnitten noch erläutern werden. Beachten Sie, wie häufig ein Paar eckige
Klammern verwendet wird.
Übung 7.5 Was ist falsch an den folgenden Array-Deklarationen? Korrigieren Sie.
[]int zaehler;
boolean[5000] belegt;

7.3.2 Array-Objekte erzeugen


Der nächste Schritt ist nun, eine Array-Variable mit einem Array-Objekt zu ver-
knüpfen.
Der Konstruktor der Klasse ProtokollAuswerter enthält eine Anweisung zur Erzeu-
gung eines Arrays von ganzen Zahlen:
zugriffeInStunde = new int[24];
Auch hier fällt auf, dass die Syntax von der normalen Objekterzeugung abweicht.
So gibt es beispielsweise keine runden Klammern um die Parameter des Kons-
truktors, da ein Array-Objekt keinen Konstruktor hat. Der Code erzeugt ein
Array-Objekt, in dem 24 unterschiedliche int-Werte gespeichert werden können,
und lässt die Array-Variable zugriffeInStunde darauf verweisen. Der Wert 24 gibt
die Größe des Arrays an und ist kein Konstruktorparameter. Abbildung 7.1 ver-
anschaulicht das Ergebnis dieser Zuweisung.

Abbildung 7.1
Ein Array für 24
int-Werte.

Die allgemeine Form für die Erzeugung eines Array-Objekts ist


new Typ[int-Ausdruck]
Die Auswahl des Typs legt fest, welchen Typ die Elemente des Arrays haben wer-
den. Der int-Ausdruck legt die Größe des Arrays fest, also die feste Anzahl der
Elemente, die in dem Array gespeichert werden können.
Wenn einer Array-Variablen ein Array-Objekt zugewiesen wird, dann muss der
Typ dieses Objekts zum Typ der Variablen passen. Die Zuweisung an zugriffeIn-

267
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Stunde ist zulässig, weil das Array-Objekt ein Array von ganzen Zahlen ist und
zugriffeInStunde eine Variable für ein Array von ganzen Zahlen. Die folgende
Zeile deklariert eine Variable für ein Array von Zeichenketten und lässt diese auf
ein Array verweisen, das eine Kapazität für zehn Zeichenketten hat:
String[] namen = new String[10];
Es ist wichtig zu verstehen, dass durch die Erzeugung des Arrays, das der Variablen
namen zugewiesen wird, nicht 10 Zeichenketten erzeugt werden. Es wird lediglich
eine Sammlung mit fester Größe erzeugt, in der 10 Zeichenketten gespeichert
werden können. Diese Zeichenketten werden möglicherweise an einer anderen
Stelle in der Klasse erzeugt, zu der namen gehört. Unmittelbar nach seiner Erzeu-
gung kann ein Array-Objekt als leer angesehen werden. Wenn es ein Array für
Objekte ist, dann enthält das Array null-Werte für alle Elemente, und wenn es ein
Array von ganzen Zahlen ist, dann sind alle Elemente auf null gesetzt. Im nächsten
Abschnitt sehen wir uns an, wie Elemente in einem Array abgelegt (und wieder
abgefragt) werden können.

Übung 7.6 Gegeben seien folgende Variablen-Deklarationen:


double[] temperaturen;
String[] urls;
Ticketautomat[] automaten;
Schreiben Sie Zuweisungen, die folgende Aufgaben ausführen: (a) die Varia-
ble temperaturen auf ein Array von 60 double-Werten verweisen lassen; (b)
die Variable urls auf ein Array verweisen lassen, das 90 Zeichenketten spei-
chern kann; (c) die Variable automaten auf ein Array verweisen lassen, das fünf
Ticketautomat-Objekte halten kann.

Übung 7.7 Wie viele Zeichenketten werden durch die folgende Deklaration
erzeugt?
String[] bezeichnungen = new String[20];
Übung 7.8 Was ist falsch an der folgenden Array-Erzeugung? Korrigieren Sie sie.
double[] preise = new double(50);

7.3.3 Array-Objekte benutzen


Auf die einzelnen Elemente eines Arrays wird über einen Index zugegriffen. Ein Index
ist ein ganzzahliger Ausdruck zwischen eckigen Klammern, der auf den Namen der
Variablen folgt. Beispielsweise:
bezeichnungen[6]
automaten[0]
leute[x + 10 - y]
Die Menge der gültigen Werte für einen Index-Ausdruck hängt von der Größe des
Arrays ab, auf das dieser Index angewendet wird. Array-Indizes beginnen immer
mit null und laufen bis zur Größe des Arrays minus eins. Die gültigen Indizes für
das Array zugriffeInStunde sind somit die Werte von 0 bis inklusive 23.

268
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.3 Die Analyse einer Logdatei

Fallstrick
Zwei Fehler, die sehr häufig gemacht werden, sind zum einen die Annahme,
dass die gültigen Indizes eines Arrays bei 1 beginnen, und zum anderen der
Zugriff auf ein Element mit dem Index, der der Größe des Arrays entspricht.
Die Benutzung von Indizes außerhalb der gültigen Grenzen eines Arrays führt
zu einem Laufzeitfehler, der ArrayIndexOutOfBoundsException genannt wird.

Ausdrücke, die ein Element aus einem Array auswählen, können an allen Stellen
stehen, an denen ein Wert des Array-Basistyps erwartet wird. Solche Ausdrücke
können auf beiden Seiten einer Zuweisung verwendet werden. Hier sind einige Bei-
spiele, in denen Array-Ausdrücke in verschiedenen Situationen verwendet werden:
bezeichnungen[5] = "Ende";
double haelfte = temperaturen[0] / 2;
[Link](leute[3].gibName());
automaten[0] = new Ticketautomat(500);
Die Benutzung eines Array-Index auf der linken Seite einer Zuweisung ist das Array-
Gegenstück zum Aufruf einer verändernden Methode (oder setze-Methode), denn
der Inhalt des Arrays wird dabei verändert. Die Benutzung an jeder anderen Stelle ist
das Array-Gegenstück zu einer sondierenden Methode (oder gib-Methode).

7.3.4 Die Auswertung der Logdatei


Das Array zugriffeInStunde, das im Konstruktor der Klasse ProtokollAuswerter
erzeugt wird, wird zum Speichern der Analysedaten der Zugriffswerte verwendet.
Die Daten werden in der Methode analysiereStundendaten im Array abgelegt und
in der Methode stundendatenAusgeben ausgelesen. Da die Aufgabe der analysiere-
Methode ist, für jeden Stundenabschnitt die Anzahl der Zugriffe zu zählen, benö-
tigt das Array 24 Plätze – einen für jeden Stundenabschnitt eines 24-Stunden-Tags.
Der Auswerter delegiert das Einlesen der Logdatei an einen LogdateiLeser.
Die Klasse LogdateiLeser ist recht komplex, deshalb empfehlen wir, dass Sie nicht
zu viel Zeit mit der Untersuchung ihrer Implementierung verbringen. Ihre Auf-
gabe ist das Aufteilen der Zeilen in einzelne Werte. Wir können von ihrer Imple-
mentierung abstrahieren, indem wir lediglich zwei ihrer Methoden betrachten:
public boolean hasNext()
public Logeintrag next()
Diese beiden Methoden entsprechen genau denen, die wir vom Iterator-Typ her
kennen, und ein LogdateiLeser kann genauso verwendet werden, außer dass in
diesem Fall nicht die Methode remove verwendet werden darf. Die Methode has-
Next sagt dem Auswerter, ob mindestens ein weiterer Eintrag in der Logdatei
existiert, und die Methode next liefert ein LogEintrag-Objekt, das die Werte der
nächsten Zeile enthält.
Von jedem Logeintrag lässt sich die Methode analysiereStundendaten den Stun-
denwert geben:
int stunde = [Link]();

269
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Wir wissen, dass der Wert der lokalen Variablen stunde immer im Bereich von 0
bis 23 liegt, und dies entspricht genau den gültigen Indizes im Array zugriffeIn-
Stunde. Jeder Eintrag in diesem Array wird als Zugriffszähler für den entsprechen-
den Stundenabschnitt benutzt. Wir wollen also jedes Mal, wenn ein Stunden-
wert gelesen wurde, den Zähler für die entsprechende Stunde erhöhen. Wir
haben dies im Quelltext so formuliert:
zugriffeInStunde[stunde]++;
Beachten Sie, dass der im Array-Element gespeicherte Wert erhöht wird und
nicht die Variable stunde. Die folgenden Alternativen sind beide äquivalent zu
dieser Anweisung, da wir ein Array-Element genauso wie eine normale Variable
verwenden können:
zugriffeInStunde[stunde] = zugriffeInStunde[stunde] + 1;
zugriffeInStunde[stunde] += 1;
Nach Ende der Ausführung der Methode analysiereStundendaten haben wir einen
kompletten Satz von aufaddierten Zählern für jeden Stundenabschnitt des proto-
kollierten Zeitraums.
Im nächsten Abschnitt sehen wir uns die Methode stundendatenAusgeben näher
an, da diese eine neue Kontrollstruktur verwendet, die gut für das Iterieren über
ein Array geeignet ist.

7.4 Die for-Schleife


Java definiert zwei Varianten von for-Schleifen, beide eingeleitet durch das
Schlüsselwort for. In Abschnitt 4.9 haben wir die erste Variante, die for-each-
Schleife, als ein geeignetes Mittel eingeführt, um über eine Sammlung mit flexib-
ler Größe zu iterieren. Die zweite Variante, die for-Schleife, ist eine weitere itera-
tive Kontrollstruktur,2 die besonders geeignet ist, wenn
 wir einige Anweisungen wiederholen wollen und die Anzahl der Wiederho-
lungen von vornherein feststeht;
 wir innerhalb der Schleife eine Variable benötigen, deren Wert sich bei jedem
Durchlauf um einen festgelegten Wert – typischerweise um 1 – verändern soll.
Die for-Schleife eignet sich gut für Situationen, in denen eine bestimmte Iteration
erforderlich ist. Beispielsweise benutzen wir eine for-Schleife häufig, wenn wir
jedes Element eines Arrays bearbeiten wollen. Dies passt zu den Kriterien, denn die
feststehende Häufigkeit korrespondiert mit der festen Größe des Arrays, und die
Variable wird benötigt, um einen ansteigenden Index für das Array zu haben.
Eine for-Schleife hat die folgende allgemeine Form:
for(Initialisierung; Bedingung; Aktion nach dem Rumpf) {
Anweisungen, die wiederholt werden sollen
}

2 Es kommt vor, dass Personen, die den Unterschied zur for-each-Schleife verdeutlichen wollen, die
for-Schleife als die „alte for-Schleife“ bezeichnen, weil sie von Anfang an in der Sprache Java de-
finiert war. Die for-each-Schleife wird manchmal auch als „verbesserte for-Schleife“ (enhanced
for loop) bezeichnet.

270
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.4 Die for-Schleife

Das folgende konkrete Beispiel ist der Methode stundendatenAusgeben des Proto-
kollauswerters entnommen:
for(int stunde = 0; stunde < [Link]; stunde++) {
[Link](stunde + ": " + zugriffeInStunde[stunde]);
}
Das Ergebnis dieser Anweisung ist, dass der Wert jedes Elements im Array ausge-
geben wird, wobei die entsprechende Stundennummer vorangestellt wird. Bei-
spielsweise:
0: 149
1: 149
2: 148
...
23: 166
Wenn wir diese for-Schleife mit der for-each-Schleife vergleichen, dann sehen wir
den syntaktischen Unterschied in dem Bereich zwischen den runden Klammern im
Schleifenkopf. In dieser for-Schleife stehen dort drei Abschnitte, die jeweils durch
ein Semikolon voneinander getrennt sind.
Aus einer Designsicht auf die Programmiersprache wäre es schöner gewesen,
wenn zwei verschiedene Schlüsselwörter für diese beiden Schleifenarten verwen-
det worden wären, etwa for und foreach. Dass für beide for verwendet wird, ist,
wie gesagt, auch hier wieder historisch bedingt. Ältere Versionen der Sprache
Java kannten die for-each-Schleife noch nicht. Als sie schließlich eingeführt
wurde, wollten die Designer der Sprache nicht zusätzlich ein neues Schlüsselwort
einführen, da dies Probleme mit bestehenden Programmen verursachen kann.
Deshalb haben sie sich entschieden, das gleiche Schlüsselwort für beide Schleifen
zu verwenden. Dies macht es uns etwas schwerer, diese beiden Schleifen vonein-
ander zu unterscheiden, aber wir werden mit der Zeit die unterschiedlichen
Kopfstrukturen auseinanderhalten können.
Auch wenn die for-Schleife oft für die bestimmte Iteration verwendet wird, bedeu-
tet die Tatsache, dass sie von einem allgemeinen booleschen Ausdruck gesteuert
wird, dass sie der while-Schleife näher steht als der for-each-Schleife. Wir können
die Ausführung einer for-Schleife veranschaulichen, indem wir ihre allgemeine
Form durch eine äquivalente while-Schleife formulieren:
Initialisierung
while(Bedingung) {
Anweisungen, die wiederholt werden sollen
Anweisung nach dem Rumpf
}
Entsprechend sähe der Rumpf von stundendatenAusgeben folgendermaßen aus:
int stunde = 0;
while(stunde < [Link]) {
[Link](stunde + ": " + zugriffeInStunde[stunde]);
stunde++;
}
Aus dieser umgeschriebenen Version können wir ersehen, dass die „Anweisung
nach dem Rumpf“ tatsächlich erst nach den Anweisungen im Schleifenrumpf

271
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

ausgeführt wird, obwohl diese Anweisung im Kopf der Schleife steht. Außerdem
können wir sehen, dass die Initialisierung nur genau einmal ausgeführt wird –
unmittelbar bevor die Bedingung das erste Mal geprüft wird.
In beiden Versionen ist die Anweisung
stunden < [Link]
zu beachten. Sie zeigt zwei wichtige Aspekte:
 Alle Arrays enthalten ein Datenfeld length, das die festgelegte Größe eines
Arrays hält. Der Wert dieses Datenfeldes wird immer dem Wert entsprechen,
mit dem das Array-Objekt erzeugt wurde. In diesem Fall ist der Wert also 24.
 Die Bedingung benutzt den Kleiner-Operator <, um den Wert von stunde mit
der Länge des Arrays zu vergleichen. In diesem Fall wird die Schleife also wie-
derholt, solange stunde niedriger ist als 24. Für den allgemeinen Fall, dass wir
auf jedes Element in einem Array zugreifen wollen, hat der Kopf der for-
Schleife folgende allgemeine Form:
for(int index = 0; index < [Link]; index++)
Dies ist korrekt, denn wir wollen keinen Index verwenden, der gleich der Größe
des Arrays ist – solch ein Element wird es bei einem Array niemals geben.

7.4.1 Arrays und die for-each-Schleife


Könnten wir die oben vorgestellte for-Schleife auch als for-each-Schleife schrei-
ben? Die Antwort lautet: beinahe. Ein möglicher Ansatz für eine solche Schleife
könnte beispielsweise folgendermaßen aussehen:
for(int wert : zugriffeInStunde) {
[Link](": " + wert);
}
Dieser Code lässt sich übersetzen und ausführen (versuchen Sie es!). Wir können
daran sehen, dass Arrays genauso wie Sammlungen in for-each-Schleifen verwendet
werden können. In diesem konkreten Fall gibt es allerdings ein Problem: Die Stunde
vor dem Doppelpunkt lässt sich auf diese Weise nicht so leicht ausgeben. Das Quell-
textfragment verzichtet daher auf die Ausgabe der Stunde und gibt nur den Doppel-
punkt und den Wert aus. Dies liegt schlicht daran, dass die for-each-Schleife keinen
Schleifenzähler anbietet, der uns das Ausgeben der Stunde ermöglichen würde.
Um das Problem zu beheben, müssten wir unsere eigene Zählervariable definie-
ren (ähnlich dem Beispiel mit der while-Schleife). Wir ziehen es aber vor, die tradi-
tionelle for-Schleife zu verwenden, die in diesem Fall prägnanter ist.

7.4.2 Die for-Schleife und Iteratoren


In Abschnitt 4.12.2 haben wir gesehen, dass wir zum Entfernen von Elementen aus
einer Sammlung einen Iterator benötigen. Es gibt allerdings eine bestimmte Form
der Verwendung einer for-Schleife mit einem Iterator, die für derlei Aufgaben eben-
falls geeignet ist. Angenommen wir möchten aus unserer Musiksammlung jeden
Track entfernen, der von einem bestimmten Interpreten stammt. Dazu müssen wir

272
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.4 Die for-Schleife

jeden Track in der Sammlung prüfen, was für den Einsatz einer for-each-Schleife
spricht. Wie wir jedoch gelernt haben, ist dies hier nicht möglich. Stattdessen ver-
wenden wir eine for-Schleife:
for(Iterator<Track> it = [Link](); [Link](); ) {
Track track = [Link]();
if (trackgibInterpret().equals(interpret)) {
[Link]();
}
}
Der springende Punkt hierbei ist, dass im Schleifenkopf kein „Aktion nach dem
Rumpf“-Teil vorgesehen ist – sein Platz bleibt einfach leer. Dies ist erlaubt, wir müs-
sen jedoch darauf achten, nach der Schleifenbedingung das Semikolon zu setzen.
Die Verwendung einer for-Schleife anstelle einer while-Schleife macht klarer, dass
wir jedes Element der Liste durchlaufen wollen.

Welche Schleife soll ich benutzen?


Wir haben drei verschiedene Schleifen kennengelernt: die for-Schleife, die
for-each-Schleife und die while-Schleife. Wie Sie gesehen haben, können wir
jede von ihnen benutzen, um ein bestimmtes Problem zu lösen. Normaler-
weise kann jede der Schleifen durch eine andere ersetzt werden. Für welche
sollen Sie sich also jeweils entscheiden? Hier kommen ein paar Richtlinien:
 Wenn Sie alle Elemente in einer Sammlung bearbeiten wollen, dann ist
die for-each-Schleife fast immer die eleganteste Lösung. Sie ist klar und
knapp (bietet aber keinen Schleifenzähler).
 Wenn Sie eine Schleife benötigen, die sich nicht auf eine Sammlung bezieht
(sondern irgendeine andere Aktion wiederholt ausführen soll), ist die for-
each-Schleife nicht hilfreich. Sie müssen sich dann zwischen der for-
Schleife und der while-Schleife entscheiden. Die for-each-Schleife ist aus-
schließlich für Sammlungen vorgesehen.
 Die for-Schleife ist gut geeignet, wenn Sie zu Beginn der Schleife wissen,
wie viele Wiederholungen Sie benötigen. Diese Information kann in einer
Variablen vorliegen, sollte aber während der Schleifenausführungen nicht
geändert werden. Die for-Schleife ist auch gut geeignet, wenn Sie den
Schleifenzähler explizit benötigen.
 Die while-Schleife sollte bevorzugt eingesetzt werden, wenn Sie zu Beginn
der Schleife nicht wissen, wie oft die Schleife durchlaufen werden soll.
Das Ende der Schleife kann während der Ausführung aufgrund einer Bedin-
gung ermittelt werden (beispielsweise: eine Zeile aus einer Datei einlesen –
wiederholt – bis wir das Ende der Datei erreichen).
 Wenn Sie während der Schleifendurchläufe Elemente aus einer Sammlung
entfernen müssen, haben Sie zwei Alternativen: Wollen Sie die ganze
Sammlung durchlaufen, sollten Sie eine for-Schleife zusammen mit einem
Iterator wählen. Wenn Sie möglicherweise fertig werden, bevor Sie das
Ende der Sammlung erreicht haben, können Sie auf eine while-Schleife
zurückgreifen.

273
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Übung 7.9 Überprüfen Sie, was passiert, wenn in der Bedingung der for-
Schleife von stundendatenAusgeben fälschlich der Kleiner-gleich-Operator ver-
wendet wird:
for(int stunde = 0; stunde <= [Link]; stunde++)
Übung 7.10 Schreiben Sie den Rumpf von stundendatenAusgeben so um, dass
die for-Schleife durch eine äquivalente while-Schleife ersetzt wird. Rufen Sie die
geänderte Methode auf, um zu prüfen, ob sie noch dasselbe Resultat liefert.
Übung 7.11 Korrigieren Sie alle Fehler in der folgenden Methode.
/**
* Gib alle Werte im Array noten aus, die höher als der
* mittelwert sind.
* @param noten ein Array mit Notenwerten.
* @param mittelwert die Durchschnittsnote.
*/
public void hoehereAusgeben(double noten, double mittelwert)
{
for(index = 0; index <= [Link]; index++) {
if(noten[index] > mittelwert)
{ [Link](noten[index]);
}
}
}
Übung 7.12 Ändern Sie die Klasse ProtokollAuswerter so, dass ihr Konstruk-
tor den Namen der zu analysierenden Protokolldatei übernehmen kann.
Dieser Konstruktor soll den Dateinamen an den Konstruktor der Klasse Log-
dateiLeser übergeben. Erzeugen Sie anschließend mit der Klasse Logdatei-
Erzeuger Ihre eigene Datei mit zufälligen Protokolleinträgen und analysieren
Sie die Daten.
Übung 7.13 Vervollständigen Sie die folgende Methode anzahlZugriffe, die
die Gesamtanzahl aller Zugriffe berechnet, die in der Logdatei protokolliert
sind. Benutzen Sie dazu eine for-Schleife, die über zugriffeInStunde iteriert:
/**
* Liefere die Anzahl aller Zugriffe, die in der Logdatei
* protokolliert sind.
*/
public int anzahlZugriffe()
{
int gesamt = 0;
// Addiere den Wert jedes Elements in zugriffeInStunde
// zu gesamt hinzu.
...
return gesamt;
}

274
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.4 Die for-Schleife

Übung 7.14 Fügen Sie eine Methode anzahlZugriffe in die Klasse Protokoll-
Auswerter ein und prüfen Sie, ob sie das richtige Ergebnis liefert.

Hinweis: Sie können die Prüfung vereinfachen, indem Sie den Auswerter
eine Datei mit nur wenigen Zeilen lesen lassen. Auf diese Weise werden Sie
leichter feststellen können, ob Ihre Methode das richtige Ergebnis liefert.
Die Klasse LogdateiLeser hat einen Konstruktor mit dem folgenden Kopf,
um aus einer bestimmten Datei lesen zu können:
/**
* Erzeuge einen LogdateiLeser, der Einträge aus einer
* Datei mit dem angegebenen Namen liest.
* @param dateiname der Name der Logdatei.
*/
public LogdateiLeser(String dateiname)
Übung 7.15 Fügen Sie ProtokollAuswerter eine Methode aktivsteStunde
hinzu, die die Stunde mit den meisten Zugriffen liefern soll. Sie können dies
implementieren, indem Sie das Array zugriffeInStunde nach dem Element
mit dem höchsten Wert durchsuchen. Hinweis: Müssen Sie jedes Element
überprüfen, um den höchsten Wert zu finden? Falls ja, dann verwenden Sie
eine for-Schleife oder eine for-each-Schleife. Welche ist besser geeignet für
diesen Fall?
Übung 7.16 Fügen Sie zu ProtokollAuswerter eine Methode ruhigsteStunde
hinzu, die die Stunde mit den wenigsten Zugriffen liefern soll. Hinweis: Dies
klingt fast identisch zur vorigen Übung, aber hier gibt es eine kleine Falle für
die Unaufmerksamen. Gehen Sie sicher, dass Ihre Methode mit Daten arbei-
tet, bei denen jede Stunde einen Wert größer als null hat.
Übung 7.17 Welche Stunde wird von Ihrer Methode aktivsteStunde zurück-
geliefert, wenn mehr als eine Stunde den größten Wert hat?
Übung 7.18 Fügen Sie ProtokollAuswerter eine Methode hinzu, die den
aktivsten zweistündigen Abschnitt findet. Lassen Sie die Methode den Wert
der ersten Stunde dieses Abschnitts liefern.
Übung 7.19 Zusatzaufgabe. Speichern Sie das Projekt Weblog-Auswertung
unter einem anderen Namen, damit Sie eine neue Version erstellen können, in
der die gegebenen Daten ausführlicher ausgewertet werden. Zum Beispiel
könnte die Information nützlich sein, welche Tage ruhiger sind als andere. Ist
beispielsweise ein zyklisches Muster über die sieben Wochentage erkennbar?
Um Auswertungen von Tages-, Monats- oder Jahresdaten durchführen zu
können, müssen Sie einige Änderungen an der Klasse LogEintrag vornehmen.
Diese speichert zwar alle Werte einer Logzeile, macht aber nur die Stunde und
die Minute über sondierende Methoden zugänglich. Fügen Sie weitere
Methoden hinzu, die auch die anderen Werte zugreifbar machen. Fügen Sie
dann weitere Analysemethoden hinzu.

275
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Übung 7.20 Zusatzaufgabe. Wenn Sie die vorige Aufgabe bearbeitet haben,
können Sie das Format der Logdatei um weitere numerische Einträge erwei-
tern. Beispielsweise speichern Webserver üblicherweise eine numerische
Kennung, mit der der Zugriffserfolg protokolliert wird: Der Wert 200 steht
für einen erfolgreichen Zugriff, 403 besagt, dass der Zugriff nicht erlaubt ist,
und 404 bedeutet, dass die angeforderte Seite nicht gefunden werden
konnte. Lassen Sie den Protokollauswerter Informationen über erfolgreiche
und nicht erfolgreiche Zugriffe liefern. Diese Aufgabe ist sehr anspruchsvoll,
da Sie jede Klasse des Projekts anpassen müssen.
Übung 7.21 Im Projekt Laborkurse, das wir in vorherigen Kapiteln unter-
sucht haben, enthält die Klasse Laborkurs ein Datenfeld studenten, das eine
Liste von Studenten verwaltet. Lesen Sie sich den Quelltext dieser Klasse durch,
um die Konzepte wiederzuerkennen, die wir in diesem Kapitel vorgestellt
haben.
Übung 7.22 Die Klasse Laborkurs schreibt eine Maximalzahl an Studenten
vor, die sich in einen Kurs einschreiben können. Denken Sie, dass es unter
diesem Gesichtspunkt angemessener wäre, statt der Sammlung mit flexibler
Größe ein Array mit fester Größe für das Datenfeld studenten zu verwen-
den? Diskutieren Sie Vor- und Nachteile beider Alternativen.
Übung 7.23 Schreiben Sie die Methode alleDateienAusgeben in der Klasse
MusikSammlung des Projekts Musiksammlung-v3 so um, dass eine for-Schleife
anstelle einer for-each-Schleife verwendet wird.

7.5 Das Projekt Automat


In den folgenden Abschnitten werden wir unsere Behandlung der Arrays auswei-
ten, um einige weitere typische Verwendungsmöglichkeiten zu zeigen. Wir werden
zwei Projekte einsetzen, die auf zellulären Automaten basieren. Dies sind relativ
einfache Rechenmodelle, die sowohl interessante als auch verblüffende „Verhaltens-
muster“ aufweisen. Wir werden mit einigen eindimensionalen Beispielen anfangen
und das Modell dann auf zwei Dimensionen erweitern, um den Einsatz von zweidi-
mensionalen Arrays zu untersuchen.
Ein zellulärer Automat besteht aus einem Gitter von „Zellen“. Jede Zelle besitzt einen
einfachen Status, der aus einem begrenzten Wertebereich besteht – oft einfach
durch die Werte 0 und 1 dargestellt. Diese Werte könnten zum Beispiel als „an“ und
„aus“ interpretiert werden als oder „tot“ und „lebend“ – aber die Bedeutung ist
eigentlich beliebig. Wir könnten eine Zelle zwar als Klassendefinition implemen-
tieren, die ein einziges boolesches Feld sowie eine sondierende und eine verän-
dernde Methode enthält, dies wäre aber vermutlich für einen einfachen Automa-
ten eine Nummer zu groß. Deshalb repräsentieren wir den Automaten lieber als
ein Array vom Typ int[]. Die Wahl von int anstelle von boolean wird es einfacher
machen, die Veränderungen der Zustandswerte zu berechnen, die bei dieser spe-
ziellen Anwendung nötig sind.

276
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.5 Das Projekt Automat

In jedem Schritt des Modells wird für jede Zelle ein neuer Zustand basierend auf
seinem vorhergehenden Zustand und den Zuständen seiner Nachbarn berechnet.
Für einen eindimensionalen Automaten sind die Nachbarn die beiden Zellen links
und rechts. Mit anderen Worten, für eine Zelle mit Index i sind die Nachbarn die
Zellen mit den Indizes (i-1) und (i+1). Wir werden diese Zelle an Index i als die
Zelle im „Zentrum“ bezeichnen. Automaten sind deshalb so interessant, weil die
verschiedenartigen Regeln zum Berechnen neuer Zustände, basierend auf diesen
drei Werten, nach mehreren Schritten ganz unterschiedliche Auswirkungen auf
das Gesamtverhalten des Automaten haben.
Listing 7.2 zeigt einen ersten Versuch, einen Automaten aufgrund dieser Prinzipien
zu implementieren. Sie finden die Klasse im Projekt Automat-v1. Die verwendete
Regel zur Berechnung des neuen Zellzustands besteht darin, die Zustandswerte der
beiden Nachbarn zum Zustandswert der Zelle zu addieren und auf diese Summe
dann eine Modulo-2-Operation anzuwenden, sodass das Ergebnis entweder 0
oder 1 sein muss. Abbildung 7.2 zeigt das Ergebnis der wiederholten Anwen-
dung dieser Aktualisierungsregel auf alle Zellen, wobei es im Ausgangszustand
(Schritt 0) nur eine einzige Zelle mit Wert 1 gab und alle anderen Zellen mit 0 ini-
tialisiert waren. Nach dem ersten Schritt behält die ursprüngliche 1-Zelle den
Wert 1, aber die beiden 0-Nachbarn erhalten jetzt auch den Wert 1, während
alle anderen Zellen auf 0 bleiben. Die durch die folgenden Schritte hervorgerufe-
nen Änderungen lassen ein symmetrisches Muster entstehen.

Listing 7.2
Die Klasse Automat.

277
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Abbildung 7.2 Schritt Zellzustände – leere Zellen sind im Zustand 0


Die ersten Schritte des
Automaten, der in 0 *
Automat-v1 imple- 1 * * *
mentiert ist.
2 * * *
3 * * * * *
4 * * *
5 * * * * * * * * *
6 * * * * *
7 * * * * * * * * * * *

Übung 7.24 Öffnen Sie das Projekt Automat-v1 und erzeugen Sie ein Objekt
AutomatController. Es sollte eine Zeile mit einem einzelnen * auf dem Bild-
schirm ausgegeben werden, diese stellt den Anfangszustand des Automaten
dar. Rufen Sie die Methode schritt ein paar Mal auf, um zu sehen, wie sich
die Zustände entwickeln. Dann versuchen Sie die Methode starten.
Übung 7.25 Nachdem Sie den Automaten ausgeführt haben, rufen Sie die
Methode zuruecksetzen auf und wiederholen Sie den Prozess aus der vorigen
Aufgabe. Ergeben sich genau dieselben Muster?

278
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.5 Das Projekt Automat

Übung 7.26 Wie viele Versionen der fill-Methode gibt es in der Klasse
[Link], die einen Parameter vom Typ int[] haben? Welchen Zweck
haben diese Methoden? Wie wird eine davon in der zuruecksetzen-Methode
von Automat eingesetzt?
Übung 7.27 Verändern Sie den Konstruktor von Automat, sodass am Anfang
mehr als eine Zelle im 1-Zustand ist. Treten dieses Mal unterschiedliche Muster
zutage? Sind diese Muster deterministisch – das heißt, führt ein bestimmter
Startzustand des Automaten immer zum selben Muster?

7.5.1 Der Konditionaloperator


Während die Version des Automaten in Automat-v1 funktioniert und interessante
Resultate produziert, gibt es einige Verbesserungen, die wir an der Implementie-
rung machen können. Wir beginnen, indem wir einen neuen Operator einführen,
der häufig eingesetzt werden kann, um den Ausdruck von if-else-Anweisungen
zu vereinfachen.
Sie haben vermutlich bemerkt, dass es in Listing 7.2 drei if-else-Anweisungen gibt,
deren Struktur ungefähr so aussieht:
if(Bedingung) {
tue etwas;
}
else {
tue etwas Ähnliches;
}
In der Methode ausgeben haben wir zum Beispiel
if(zellenwert == 1) {
[Link]("*");
}
else {
[Link](" ");
}
und in der Methode aktualisieren gibt es zwei if-else-Anweisungen, um den
Variablen links und rechts Werte zuzuweisen, wobei sich die alternativen Aktio-
nen nur in den Anweisungen auf der rechten Seite unterscheiden.
In solchen Situationen können wir häufig einen Konditionaloperator verwenden,
der in Java aus den beiden Zeichen ? und : besteht. Dieser Operator wird als ter-
närer Operator bezeichnet, weil er drei Operanden besitzt, im Gegensatz zu den
bisher behandelten binären Operatoren wie + und *, die zwei Operanden haben.
Dieser Operator wird eingesetzt, um einen von zwei alternativen Werten aufgrund
der Auswertung eines booleschen Ausdrucks auszuwählen. Die allgemeine Form
lautet:
Bedingung ? Wert1 : Wert2

279
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Wenn die Bedingung true ist, dann wird der Wert des gesamten Ausdrucks Wert1
sein, anderenfalls wird er Wert2 sein. Zum Beispiel könnten wir den Rumpf der for-
Schleife in der Methode ausgeben wie folgt umschreiben:
String wasAuszugebenIst = zellenWert == 1 ? "*" : " ";
[Link](wasAuszugebenIst);
oder noch kürzer:
[Link](zellenWert == 1 ? "*" : " ");

Übung 7.28 Schreiben Sie die beiden if-else-Anweisungen in der Schleife der
Methode aktualisieren der Automat-Klasse von Automat-v1 um, sodass für die
Zuweisungen an links und rechts Konditionaloperatoren verwendet werden.
Übung 7.29 Warum haben wir in der aktualisieren-Methode ein neues
Array, naechsterZustand, erzeugt, anstatt das Array zustand direkt zu verwen-
den? Verändern Sie die Zuweisung an naechsterZustand im Schleifenrumpf,
sodass es nun eine Zuweisung an zustand wird, um festzustellen, ob es irgend-
einen Unterschied im Verhalten des Automaten auslöst. Falls dies so ist, kön-
nen Sie erklären, warum?
Übung 7.30 Können Sie eine Lösung für diese Probleme finden, die in der
vorherigen Übung aufgezeigt wurden, ohne ein neues Array zu verwenden?
Hinweis: Wie viele alte Werte der Zelle müssen bei jeder Iteration beibehal-
ten werden? Welche Version ist Ihrer Meinung nach besser: ein vollständi-
ges Array benutzen oder unsere Lösung? Begründen Sie Ihre Antwort.

7.5.2 Erste und letzte Iteration


Die bedeutendste Verbesserung, die wir an der Automat-Klasse in Listing 7.2 vor-
nehmen können, ist die Struktur des Rumpfes der for-Schleife in der Methode
aktualisieren zu überarbeiten. Im Moment ist der Code dort sehr seltsam, weil wir
sehr vorsichtig beim Bearbeiten der ersten Zelle ([0]) und der letzten Zelle ([anzahl-
Zellen-1]) sein müssen, um einen möglichen Zugriff auf nicht vorhandene Ele-
mente des Arrays zu vermeiden ([-1] links von der ersten Zelle und [anzahlZellen]
rechts von der letzten Zelle). Für diese nicht existierenden Elemente setzen wir
einen Zustandswert von 0 ein, sodass wir bei der Berechnung des nächsten Zellzu-
stands immer drei Werte haben.
Grundsätzlich gilt: Wenn innerhalb einer Schleife auf den Spezialfall der ersten oder
letzten Iteration getestet wird, sollten wir diese Codesegmente sorgfältig überprü-
fen, denn oft gibt es eine andere Lösung für diese Spezialfälle, mit der diese Tests
vermieden werden können. Eigentlich möchten wir im Rumpf einer Schleife nur
Anweisungen haben, die bei jedem Durchlauf ausgeführt werden, und Spezialfälle
wollen wir so weit wie möglich vermeiden. Es wird nicht immer möglich sein, alle
Ausnahmefälle zu beseitigen, aber diejenigen, die nur bei dem ersten oder letzten
Schleifendurchlauf angewendet werden, werden vermutlich am ehesten zu vermei-
den sein.

280
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.5 Das Projekt Automat

Sie haben sicher auch bemerkt, dass im Laufe des Iterationsprozesses die Zellen
im „Zentrum“ in der nächsten Runde immer zum „linken“ Nachbarn werden;
und der „rechte“ Nachbar wird zur nächsten Zelle im „Zentrum“. Dadurch eröff-
net sich die Möglichkeit, die nächsten Werte von links und zentrum am Ende des
Schleifenrumpfs so einzurichten, dass sie schon für den nächsten Durchlauf vor-
bereitet sind; das heißt:
naechsterZustand[i] = (links + zentrum + rechts) % 2;
links = zentrum;
zentrum = rechts;
Damit muss nur noch rechts explizit vom Array zustand in der nächsten Iteration
eingerichtet werden. Interessanterweise liefert diese Variante auch einen Weg, das
Problem zu lösen, wie die Zuweisung an links im der ersten Schleifendurchlauf zu
behandeln ist. Die obige Änderung bedeutet: wird der Schleifenrumpf – mit Aus-
nahme der ersten Iteration – betreten, dann sind links und zentrum bereits vom
vorherigen Schleifendurchlauf auf die Werte gesetzt worden, die sie zur Berech-
nung des nächsten Zustands haben sollten. Wir müssen also nur sicherstellen, dass
links für die nicht vorhandene Zelle schon auf 0 gesetzt und zentrum unmittelbar
vor dem ersten Betreten der Schleife auf zustand[0] gesetzt wurde:
int links = 0;
int zentrum = zustand[0];
for(int i = 0; i < [Link]; i++) {
int rechts = i + 1 < [Link] ? zustand[i+1] : 0;
naechsterZustand[i] = (links + zentrum + rechts) % 2;
links = zentrum;
zentrum = rechts;
}
Beachten Sie, dass die Variablendeklarationen für links und zentrum nun nicht
mehr innerhalb der Schleife stehen, sodass deren Werte zwischen den Durchläu-
fen erhalten bleiben.
In der nächsten Übung werden Sie aufgefordert, diese Änderungen in Ihrer Version
des Projekts zu implementieren. Eine Version, die all diese Änderungen bereits
erhält, finden Sie im Projekt Automat-v2, falls Sie Schwierigkeiten haben oder Ihre
Lösung überprüfen möchten; versuchen Sie jedoch unbedingt zuerst, die Aufga-
ben selbst durchzuführen, bevor Sie nachsehen.

Übung 7.31 Schreiben Sie die Methode aktualisieren so um, wie wir es
oben besprochen haben.
Übung 7.32 Fügen Sie eine Methode berechneNaechstenZustand zur Klasse
Automat hinzu, die drei Werte – links, zentrum und rechts – entgegennimmt
und die Berechnung des Werts für den nächsten Zustand zurückgibt. Der
nächste Zustand einer Zelle wurde bisher mit der folgenden Codezeile
berechnet:
naechsterZustand[i] = (links + zentrum + rechts) % 2;
Verändern Sie diese Zeile, um stattdessen Ihre neue Methode zu verwenden.

281
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Übung 7.33 Experimentieren Sie mit unterschiedlichen Wegen, den nächs-


ten Zustand einer Zelle zu berechnen, indem die Werte von links, zentrum
und rechts kombiniert werden. Stellen Sie immer sicher, dass Sie das Ergeb-
nis noch einer Modulo-2-Operation unterziehen. Sie müssen nicht alle drei
Werte in die Berechnung einbeziehen. Hier sind ein paar Möglichkeiten, die
Sie vielleicht ausprobieren möchten:
(links + rechts) % 2
(zentrum + rechts + zentrum * rechts + links * zentrum * rechts) % 2
Wie viele unterschiedliche Mengen eindeutiger Regeln gibt es Ihrer Meinung
nach, um den nächsten 0- oder 1-Zustand einer Zelle zu berechnen, ausge-
hend von den drei binären Werten in links, zentrum und rechts? Gibt es
unendlich viele Möglichkeiten?

Die letzte Verbesserung, die wir noch am Automaten machen möchten, ist, den
Test auf das Ende des Arrays zu vermeiden, bevor wir eine Zuweisung an rechts
vornehmen. Dafür gibt es eine häufig genutzte Lösung – das Array um ein Ele-
ment erweitern! Diese Lösung hört sich zunächst ein bisschen nach Mogeln an,
doch die Erweiterung eines Arrays um ein einzelnes Element kann manchmal zu
klarerem Code führen, wenn es gut durchdacht und richtig eingesetzt wird. Die
Idee hier ist, dass das zusätzliche Element den Wert 0 enthält, um als rechter
Nachbar der letzten Zelle zu dienen. Dieser Nachbar wird selbst jedoch nicht als
Zelle in den Automat integriert. Er wird niemals geändert. Die überarbeitete Ver-
sion des Rumpfs von aktualisieren lautet:
int links = 0;
int zentrum = zustand[0];
for(int i = 0; i < anzahlZellen; i++) {
int rechts = zustand[i + 1];
naechsterZustand[i] = berechneNaechstenZustand(links, zentrum, rechts);
links = zentrum;
zentrum = rechts;
}
Beachten Sie bitte, dass die Schleifenbedingung verändert wurde, weil die Index-
variable nicht mehr mit der Länge des (erweiterten) Arrays verglichen werden sollte,
sondern mit der tatsächlichen Anzahl der Zellen im Automaten. Diese Änderung ist
wichtig, um sicherzustellen, dass [i +1] niemals über die Grenzen des Arrays hin-
ausgeht, wenn der Wert von rechts gesetzt wird.
Eine Projektversion einschließlich dieses Codes können Sie auch im Projekt Auto-
mat-v3 finden.

Übung 7.34 Führen Sie diese Verbesserungen in Ihrem eigenen Code durch.

282
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.5 Das Projekt Automat

7.5.3 Zuordnungstabellen
In Kapitel 6 haben wir die Klasse HashMap besprochen, die es ermöglicht, Verbindun-
gen zwischen Objekten in der Form eines Schlüssel-Werte-Paars zu erzeugen. Eine
Map ist eine allgemeine Form einer Zuordnungstabelle: der Schlüssel wird verwen-
det, um einen ihr zugeordneten Wert in einer Tabelle, der Map, zu finden. Arrays
sind häufig eine praktische und effiziente Möglichkeit, um spezielle Zuordnungs-
tabellen zu implementieren, bei denen der Schlüssel kein Objekt, sondern ein Wert
aus einem ganzzahligen Wertebereich ist. Dieser Bereich sollte als kleinsten Wert
0 haben, damit es keine Konflikte mit der Indexierungsart für Sammlungen gibt.
Natürlich könnte ein Bereich, dessen kleinster Wert ungleich 0 ist, leicht in einen
0-basierten Bereich umgewandelt werden, indem eine positive oder negative Zahl
(Offset) zu allen Werten addiert wird. Solange die Größe des Wertebereichs bekannt
ist, kann ein Array fester Größe erzeugt werden, um die Werte der (Schlüssel-Wert)-
Paare aufzunehmen, wobei der Schlüssel ein 0-basierter ganzzahliger Index ist.
Das Konzept einer Zuordnungstabelle wollen wir mithilfe des zellulären Automaten
illustrieren. Wenn Sie mit verschiedenen Formeln für die Berechnung des nächsten
Zustands einer Zelle experimentiert haben, dann ist Ihnen vielleicht schon aufgefal-
len, dass der eindimensionale Automat, den wir implementiert haben, nicht wirk-
lich unendlich viele Möglichkeiten hat, den nächsten Zustand einer Zelle zu bestim-
men. Diese Begrenzung resultiert aus den folgenden beiden Eigenschaften:
 Es gibt nur acht mögliche unterschiedliche Kombinationen von linken, zentra-
len und rechten Werten von 1 und 0: 000, 001, 010, ..., 111.
 Für jede der acht Kombinationen kann das Ergebnis nur eine von zwei Mög-
lichkeiten sein: 0 oder 1.
Zusammengenommen bedeuten diese Eigenschaften, dass es tatsächlich nur 256
(d.h. 28) Möglichkeiten gibt, wie sich der Automat verhalten kann. In der Tat ist
die Anzahl der eindeutigen Verhaltensmöglichkeiten noch beträchtlich kleiner –
88 gesamt.

Wolfram-Codes
1983 hat Stephen Wolfram eine Studie über alle möglichen 256 elementaren
zellulären Automaten veröffentlicht. Er stellte ein numerisches System vor, mit
dem das Verhalten jedes Automatentyps definiert werden kann, und der
jedem Automaten zugeordnete Code ist als Wolfram-Code bekannt. Ist der
numerische Code gegeben, so kann man sehr leicht die anwendbare Regel für
Zustandsänderungen herausfinden, wenn die Werte einer Zelle und seiner bei-
den Nachbarn vorliegen, weil die Regel im Code selbst verschlüsselt ist. Um zu
sehen, wie dies in der Praxis funktioniert, empfehlen wir Ihnen Übung 7.36
oder führen Sie eine Websuche nach „elementarer zellulärer Automat“ durch.

Diese relativ bescheidene Anzahl an Möglichkeiten erlaubt es, die Determinierung


des nächsten Zustands einer Zelle zu implementieren, und zwar über eine Zuord-
nungstabelle anstelle einer Funktionsauswertung. Zwei Dinge sind dazu nötig:

283
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

 Eine Möglichkeit, ein (links, zentrum, rechts)-Triplet in eine Zahl aus dem Bereich
0–7 umzuwandeln. Diese Werte werden als Indexschlüssel der Zuordnungstabelle
verwendet.
 Ein Integer-Array mit 8 Elementen, das den Wert für den nächsten Zustand für
die acht Tripel-Kombinationen speichert. Dies wird die Tabelle sein.
Der folgende Ausdruck zeigt, wie wir die Dreiergruppe in eine ganze Zahl aus dem
Bereich 0–7 umwandeln können:
links * 4 + zentrum * 2 + rechts
Und eine Zustandstabelle kann wie folgt eingerichtet werden:
int[] zustandstabelle = new int[] {
0, 1, 0, 0, 1, 0, 0, 1,
};
Dies illustriert den Einsatz eines Array-Initialisierers – eine Liste der Werte, die in
einem neu erzeugten Array gespeichert werden sollen, in geschweiften Klammern
eingefasst. Beachten Sie, dass die Größe des erzeugten Arrays nicht in den eckigen
Klammern von new int[] angegeben werden muss, weil der Compiler zählen
kann, wie viele Elemente in dem Initialisierer sind, daher kann er das Array-Objekt
in exakt dieser Größe erzeugen. Beachten Sie auch das abschließende Komma
nach dem letzten Wert im Initialisierer; dieses Komma ist optional. Eine Implemen-
tierung dieser Version finden Sie in Automat-v4.

Übung 7.35 Experimentieren Sie mit unterschiedlichen Mustern der Zuord-


nungstabelle in Automat-v4.
Übung 7.36 Zusatzaufgabe. In dieser Übung geht es darum, die Zuordnungs-
tabelle einzurichten, die auf einem zusätzlichen Integer-Parameter basiert, der
dem Konstruktor von Automat übergeben wird. Die acht 1/0-Werte in der
Zuordnungstabelle könnten als acht Binärziffern interpretiert werden, die
einen numerischen Wert aus dem Bereich 0–255 verschlüsseln, ganz ähnlich
wie wir das Triplet von Zellzuständen als eine ganze Zahl im Bereich von 0–7
interpretiert haben. Das niedrigstwertige Bit wäre der Wert in zustands-
tabelle[0] und das höchstwertige Bit in zustandstabelle[7]. Damit wäre das
Muster, das wir oben bei der Initialisierung von zustandstabelle verwendet
haben, eine Verschlüsselung des Dezimalwerts 146 (128 + 16 + 2). Und tat-
sächlich werden die 256 Wolfram-Codes genau so verwendet – ein Code
aus dem Bereich 0–255 wird in seine binäre 8-Bit-Binärdarstellung verwandelt
und die Binärziffern werden zum Einrichten der Zustandstabelle benutzt.
Fügen Sie einen Integer-Parameter zum Konstruktor von Automat hinzu, der
einen Wolfram-Code enthält, und benutzen Sie diesen, um zustandstabelle
zu initialisieren. Dazu müssen Sie herausfinden, wie die einzelnen Binärziffern
aus einer ganzen Zahl gewonnen werden. Verwenden Sie das niedrigstwer-
tige Bit, um Element 0 in der Zuordnungstabelle einzurichten, das nächste Bit,
um Element 1 einzurichten, und so weiter. Hinweis: Sie könnten dies entwe-
der mithilfe von Integer-Operationen wie % oder / tun oder mit Bit-Manipu-
lationsoperatorern wie >> und &.

284
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.6 Arrays mit mehr als einer Dimension (fortgeschritten)

7.6 Arrays mit mehr als einer Dimension


(fortgeschritten)
Dieser Abschnitt gehört zu den Teilen dieses Buchs mit fortgeschrittenen Konzep-
ten. Sie können ihn im Moment gefahrlos überspringen, ohne irgendetwas zu
verpassen, was Sie für spätere Kapitel benötigen würden. Wir werden zweidi-
mensionale Arrays besprechen, eine recht spezielle, aber nützliche Datenstruktur.
Wenn Sie das oben diskutierte Automat-Projekt interessiert hat, wird Ihnen diese
reichere Variante Spaß machen.
Arrays mit mehr als einer Dimension sind einfach eine Erweiterung des eindimen-
sionalen Konzepts. Zum Beispiel ist ein zweidimensionales Array ein eindimensio-
nales Array von eindimensionalen Arrays. Es hat sich durchgesetzt, dass die erste
Dimension eines zweidimensionalen Array die Reihen eines Gitters repräsentiert
und die zweite Dimension die Spalten. Die Tatsache, dass zweidimensionale
Arrays eigentlich Arrays von Arrays sind, bedeutet, dass zweidimensionale Arrays
nicht unbedingt rechteckig sein müssen: Jede Reihe könnte eine andere Anzahl
an Elementen besitzen. Diese besondere Form werden wir allerdings nicht weiter
untersuchen. In diesem Abschnitt begrenzen wir unsere Diskussion der mehr-
dimensionalen Arrays auf den zweidimensionalen Fall, da weitere Dimensionen
auf dieselbe Art zustande kommen.

7.6.1 Das Projekt Brain


Die Regeln, die wir beim eindimensionalen Automaten kennengelernt haben,
lassen sich auf vielerlei Weisen auf zwei Dimensionen erweitern. Das vermutlich
berühmteste Beispiel eines zweidimensionalen Automaten ist Conways Spiel des
Lebens, bei dem die Zellzustände auch wieder binär sind (also entweder 1 oder
0). Wir werden einen leicht anspruchsvolleren zellulären Automaten implemen-
tieren, der unter dem Namen Brian’s Brain bekannt ist, weil er von Brian Silver-
man erfunden wurde. Hierbei werden wir eine Klasse verwenden, die die Zellen
repräsentiert, weil das Verhalten und die Interaktionen erheblich komplexer als
bei denen des elementaren Automaten sind, den wir uns im vorigen Abschnitt
angesehen haben.

Übung 7.37 Öffnen Sie das Projekt Brain, erzeugen Sie ein Umgebung-Objekt
und verwenden Sie die GUI-Steuerelemente, um eine zufällige Anfangsein-
stellung für den Automaten zu erzeugen. Führen Sie dann den Automaten
entweder schrittweise aus oder mithilfe der Start-/Pause-Buttons, um ein
Gefühl dafür zu bekommen, wie sich der Automat verhält (Abbildung 7.3).

285
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Abbildung 7.3
Ein zweidimensionaler
Automat aus dem
Brain -Projekt.

Die Zellen in Brian’s Brain befinden sich immer in einem von drei möglichen Zustän-
den, die als lebend, sterbend oder tot (alive, dying, dead) bezeichnet werden. Diese
Zustände werden in unserem Projekt durch verschiedene Farben dargestellt. Wir
möchten jedoch noch einmal deutlich machen, dass dies einfach nur übliche
Bezeichnungen sind, die keine objektive Bedeutung haben. Wir werden die will-
kürlich gewählten Werte 0, 1 und 2 verwenden, um diese Zustände dazustellen,
obwohl wir – anders als beim eindimensionalen Automaten – keinen Ausdruck
damit berechnen werden. Der nächste Zustand einer Zelle wird auf der Basis seines
eigenen Zustands und der Zustände seiner Nachbarn berechnet. Bei einem zweidi-
mensionalen Automaten besteht die Nachbarschaft in der Regel aus den acht
direkten Nachbarn der Zelle. Diese sind unter dem Namen Moore-Nachbarschaft
bekannt (Abbildung 7.4).

Abbildung 7.4
Die äußeren grauen Zel-
len stellen die Moore-
Nachbarschaft der Zelle
im Zentrum dar.

Die Regeln zum Aktualisieren des Zellzustands sind die folgenden:


 Eine Zelle, die lebend ist, verändert ihren Zustand zu sterbend.
 Eine Zelle, die sterbend ist, verändert ihren Zustand zu tot.
 Eine Zelle, die tot ist, verändert ihren Zustand zu lebend, wenn genau zwei
ihrer Nachbarn lebend sind, andernfalls bleibt sie tot.
Listing 7.3 zeigt die Klasse Zelle, die diese Konzepte implementiert.

286
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.6 Arrays mit mehr als einer Dimension (fortgeschritten)

Listing 7.3
Eine Zelle eines
zweidimensionalen
Automaten.

Jede Zelle verwaltet eine Liste seiner Nachbarn in einem Array. Das Array wird
nach einem Aufruf der Methode setzeNachbarn eingerichtet. Diese Methode erhält
eine Liste von Zellen und kopiert diese Liste in ein Array-Objekt mithilfe der toArray-
Methode der Liste.

287
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

7.6.2 Einrichten des Arrays


Listing 7.4 zeigt einige dieser Elemente aus der Klasse Umgebung, die das zweidi-
mensionale Array der Zelle-Objekte einrichtet, um den Automaten zu modellieren.

Listing 7.4
Die Klasse Umgebung.

Die Syntax zum Deklarieren einer Array-Variable mit mehr als einer Dimension ist
eine Erweiterung des eindimensionalen Falls – ein Paar von leeren eckigen Klam-
mern für jede Dimension:
Zelle[][] zellen;
Ähnlich legt die Erzeugung des Array-Objekts mithilfe von new die Länge jeder
Dimension fest:
zellen = new Zelle[anzahlZeilen][anzahlSpalten]:
Denken Sie daran, dass zu diesem Zeitpunkt nicht tatsächlich Zelle-Objekte erzeugt
werden – sondern nur das Array-Objekt, das in der Lage sein wird, die Zelle-
Objekte zu speichern, sobald diese erzeugt wurden.
Die Methode setup verwendet ein typisches Codemuster, um über alle Elemente
eines zweidimensionalen Arrays zu iterieren: eine geschachtelte for-Schleife. Das
Muster sieht ungefähr so aus:
for(int zeile = 0; zeile < anzahlZeilen; zeile++) {
for(int spalte = 0; spalte < anzahlSpalten; spalte++) {
}
}

288
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.6 Arrays mit mehr als einer Dimension (fortgeschritten)

In dieser Schleife wird dann jede Zelle eingerichtet, indem das Zelle-Objekt erzeugt
und der aktuellen Position im Array zugewiesen wird:
zellen[zeile][spalte] = new Zelle();
Schließlich ruft die Methode setup eine separate Methode setupNachbarn auf, um
jeder Zelle ihre Moore-Nachbarschaft von benachbarten Zellen zu übergeben.
Sie werden sich aus der Diskussion des eindimensionalen Arrays erinnern, dass wir am
linken und rechten Ende des Automaten aufpassen mussten, damit auch die Randzel-
len die volle Anzahl an Nachbarn bekamen. Dasselbe gilt nun auch beim zweidimen-
sionalen Automaten. Zum Beispiel hat die Zelle an Position zellen[0][0] nur drei
Nachbarn anstelle von acht. Ein möglicher Ansatz wäre es, das zweidimensionale
Array um jeweils eine zusätzliche Zeile oben und unten sowie eine zusätzliche Spalte
links und rechts zu erweitern – wodurch ein (anzahlZeilen+2)×(anzahl-Spalten+2)-
Automat entsteht – und alle Zellen in diesen zusätzlichen Position als tot anzuneh-
men. Doch wir ziehen einen „Rundum“-Ansatz zur Berechnung der Nachbarschaft
vor – mit anderen Worten, Zellen in der obersten Zeile haben als Nachbarn die Zellen
aus der untersten Reihe und Zellen in der ganz linken Spalte haben als Nachbarn die
Zellen in der ersten rechten Spalte (und jeweils umgekehrt). Dies wird auch ringför-
mige oder toroidale Anordnung genannt. Auf diese Weise besitzt jede Zelle im Auto-
maten die vollständige Moore-Nachbarschaft von acht Zellen. Jetzt müssen wir noch
einen Weg finden, die Fälle entlang der Ränder zu behandeln, ohne auf einen Code
für Spezialfälle zurückzugreifen. Wenn wir die Ränder im Moment einmal noch igno-
rieren, kann man die Nachbarn der Zelle an Position [zeile][spalte] an allen Positio-
nen mit Abstand +1 und -1 von zeile und spalte finden; z.B. [zeile+1][spalte],
[zeile+1][spalte+1], [zeile-1][spalte] und so weiter. Um eine Liste der Nachbarn
zu erstellen, könnten wir also Code nach folgendem Muster schreiben:
Zelle zelle = zellen[zeile][spalte];
for(int dz = -1; dz <= 1; dz++) {
for(int ds = -1; ds <= 1; ds++) {
füge den Nachbarn an zellen[zeile+dz][spalte+ds] hinzu;
}
}
Beachten Sie, dass wenn sowohl dz als auch ds null sind, dann ist der „Nachbar“ in
Wirklichkeit zelle selbst und sollte nicht in die Liste der Nachbarn aufgenommen
werden. Dies müssen wir für unsere Lösung also noch im Hinterkopf behalten.
Nun müssen wir sicherstellen, dass wir den Wert 0 erhalten, wenn zeile + 1 ==
anzahlZeilen ist, und wir(anzahlZeilen-1) erhalten, wenn zeile + -1 == -1 ist
(dasselbe gilt für spalte). Die Lösung der ersten Anforderung ist relativ einfach –
der Modulo-Operator liefert genau das. Doch damit sind nicht beide Anforderun-
gen abgedeckt: (-1 % anzahlZeilen) ergibt -1 anstatt (anzahlZeilen - 1). Hier
können wir einen kleinen Trick anwenden, der auf der Tatsache beruht, dass
((x + anzahlZeilen) % anzahlZeilen) == (x % anzahlZeilen), wenn x nicht negativ
ist, was bedeutet, dass ((zeile + 1 + anzahlZeilen) % anzahlZeilen) zum selben
Ergebnis führt wie ((zeile + 1) % anzahlZeilen). Ist x jedoch negativ, funktioniert
dies nicht, und wenn wir anzahlZeilen hinzufügen, bevor wir den Modulo-Opera-
tor anwenden, dann ergibt ((-1 + anzahlZeilen) % anzahlZeilen) wunschgemäß
(anzahlZeilen - 1). Der Ausdruck ((zeile + dz + anzahlZeilen) % anzahlZeilen)
liefert also den Index, den wir sowohl in positiven wie in negativen Fällen benöti-
gen, um die Nachbarschaft einer Zelle in einem umlaufenden Gitter festzulegen.

289
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Listing 7.5 zeigt die vollständige Version der Methode setupNachbarn aus der Umge-
bung-Klasse, die diese Technik einsetzt. Beachten Sie, dass eine Zelle aus ihrer
eigenen Nachbarschaftsliste entfernt wird, bevor die Liste der Zelle übergeben
wird – womit der Fall, dass dz und ds beide null sind, behandelt wird.

Listing 7.5
Einrichten der
Nachbarzellen.

Übung 7.38 Conways Spiel des Lebens ist ein deutlich einfacherer Automat als
Brian’s Brain. Beim Spiel des Lebens besitzt eine Zelle nur zwei Zustände: leb-
end und tot. Der nächste Zustand einer Zelle wird folgendermaßen bestimmt:
 Es werden alle Nachbarn der Zelle gezählt, die lebend sind.
 Ist die Zelle tot und hat sie genau drei lebende Nachbarn, dann ist ihr Fol-
gezustand lebend, ansonsten ist der nächste Zustand tot.
 Ist die Zelle lebend, dann hängt ihr nächster Zustand auch von der
Anzahl der lebenden Nachbarn ab: Sind es weniger als zwei oder mehr
als drei, dann ist der Folgezustand tot, andernfalls lebend.
Speichern Sie eine Kopie des Brain-Projekts unter dem Namen Spiel-des-
Lebens und verändern Sie die Klasse Zelle so, dass diese die Regeln von
Spiel des Lebens implementiert. Es sollte nicht nötig sein, irgendwelche
anderen Klassen zu verändern.
Übung 7.39 Bearbeiten Sie die Klasse Umgebung in Ihrer Spiel-des-Lebens-Version,
sodass die Umgebung nicht mehr ringförmig ist. Mit anderen Worten, der
nächste Zustand einer Zelle an einer äußeren Reihe oder Spalte der Umgebung
wird bestimmt, indem angenommen wird, dass „Nachbarn“ jenseits der äußeren
Grenzen der Umgebung immer tot sind. Stellen Sie einen großen Unterschied
zwischen dem Verhalten dieser Version und der ringförmigen Variante fest?
Übung 7.40 Verwenden Sie wahlweise entweder eine Kopie des Brain-Pro-
jekts oder Ihres Spiel-des-Lebens-Projekts, um ein neues Projekt zu implemen-
tieren, bei dem die Zellen die Zustände lebend oder tot annehmen können.
Lebende Zellen sollten den Eindruck erwecken, als „wanderten“ sie durch die

290
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.7 Arrays und Streams (fortgeschritten)

zweidimensionale Umgebung. Weisen Sie den Zellen eine Bewegungsrichtung


zu. Eine lebende Zelle sollte sich bei jedem Schritt in die zugeordnete Rich-
tung „bewegen“ – mit anderen Worten sollte eine lebende Zelle einen ihrer
Nachbarn dazu bringen, im nächsten Schritt lebend zu werden, während
die Zelle selbst tot wird – dies ergibt die Illusion der Bewegung. Wenn eine
lebende Zelle am Rand der Umgebung versuchen würde, sich über den
Rand hinaus zu „bewegen“, sollte sie stattdessen ihre Richtung zufällig
ändern und im nächsten Schritt noch lebend bleiben; d.h., sie wird in die-
sem Fall nicht den Zustand eines Nachbarn ändern.
Überprüfen Sie Ihre Implementierung dieser Regeln, indem Sie anfangs nur
eine einzige lebende Zelle in die Umgebung setzen. Haben Sie mehrere
lebende Zellen, dann müssen Sie sich überlegen, was passieren soll, wenn
eine Zelle versucht, sich auf eine Position zu „bewegen“, die gerade erst
von einer anderen Zelle verlassen wurde, oder wenn zwei Zellen versuchen,
dieselbe Nachbarzelle zu besetzen.

7.7 Arrays und Streams (fortgeschritten)


Die Prinzipien zur Stream-Verarbeitung, die wir in Kapitel 5 eingeführt haben, kön-
nen auf Arrays ebenso gut wie auf flexible Sammlungen angewandt werden. Man
erhält einen Stream aus den Inhalten eines Arrays, indem das Array an die statische
Methode stream von [Link] übergeben wird. Wenn das Array vom Typ
int[] ist, dann wird ein Objekt vom Typ [Link] zurückgeliefert:
int[] zahlen = { 1, 2, 3, 4, 5 }
IntStream stream = [Link](zahlen);
Es gibt außerdem noch DoubleStream- und LongStream-Typen für die entsprechen-
den Stream-Versionen von double[]- und long[]-Arrays. In den anderen Fällen
wird für ein Array mit Typ T ein Stream vom Typ Stream<T> erzeugt.
Die Standardoperationen auf Streams wie filter, limit, map, reduce, skip und so
weiter stehen natürlich alle für den resultierenden Stream zur Verfügung, sodass
die Daten in einer Sammlung fester Größe auf genau dieselbe Weise wie in den
flexiblen Sammlungen verarbeitet werden können, sobald sie in einen Stream
umgewandelt wurden.
Die statische Methode range der Klasse IntStream wird häufig verwendet, um
einen Stream aus den Integer-Indexen einer Sammlung zu erzeugen; zum Beispiel:
IntStream indizes = [Link](0, [Link]);
Der Wert von [Link] wird nicht in den Stream aufgenommen. Das folgende
Beispiel zeigt, wie man die Indizes von Werten eines Arrays identifizieren könnten,
die über einem bestimmten Schwellenwert liegen. Beachten Sie, dass wir in diesem
speziellen Fall an den Indizes und nicht an den Werten interessiert sind:
int[] ueber = [Link](0, [Link])
.filter(i -> stundenzaehler[i] > schwellwert).toArray();
Die terminale toArray-Methode des Streams gibt ein Array zurück, welches die
Indexwerte enthält, die nach dem Filtern zurückbleiben.

291
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 7 Sammlungen mit fester Größe – Arrays

Zusammenfassung
In diesem Kapitel haben wir Arrays als Möglichkeit diskutiert, um Samm-
lungsobjekte fester Größe zu erzeugen. Eindimensionale Arrays sind ein
Sammlungstyp, der gut geeignet für solche Situationen ist, in denen die
Anzahl der zu speichernden Elemente festgelegt und im Voraus bekannt ist.
Sie sind auch syntaktisch ein wenig leichter als Sammlungen flexibler Größe
wie ArrayList, insbesondere wenn sie zur Speicherung von Werten der pri-
mitiven Typen verwendet werden. Abgesehen von diesen speziellen Anwen-
dungen bieten sie jedoch keine Funktionen, die nicht auch bereits mit
Sammlungen wie Listen, Maps und Sets verfügbar wären.

NEUE BEGRIFFE IN DIESEM KAPITEL


Array, for-Schleife, Konditionaloperator, Zuordnungstabelle

Zusammenfassung der Konzepte


 Array Ein Array ist eine besondere Art von Sammlung, die eine festge-
legte Anzahl von Elementen halten kann.
 for-Schleife Eine iterative Steuerungsstruktur, die häufig verwendet wird,
wenn eine Indexvariable benötigt wird, um Folgeelemente aus einer
Sammlung auszuwählen, wie eine ArrayList oder ein Array.

Übung 7.41 Schreiben Sie den folgenden Code neu, indem Sie die statische
Methode arraycopy aus der Klasse System verwenden:
int[] kopie = new int[[Link]];
for[int i = 0; i < [Link]; i++) {
kopie[i] = original[i];
}
Übung 7.42 Beschreiben Sie, was die folgenden statischen Methoden der
Klasse [Link] machen: asList, binarySearch, fill und sort. Wenn
es mehrere Versionen einer Methode gibt, verwenden Sie eine, die auf einem
Integer-Array arbeitet.
Übung 7.43 Versuchen Sie, ein paar Beispielcodesegmente zu schreiben,
mit dem Sie jede der Methoden aus der vorigen Übung benutzen können.
Übung 7.44 Schreiben Sie Anweisungen, um alle Elemente eines zweidi-
mensionalen Arrays, original, in ein neues Array, kopie, zu kopieren. Sie
sollten auch Anweisungen zur Erzeugung des kopie-Arrays schreiben.
Übung 7.45 Untersuchen Sie einige andere zweidimensionale zelluläre
Automaten und implementieren Sie deren Regeln im Brain-Projekt.

292
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

8 Klassenentwurf

Lernziele
Zentrale Konzepte in diesem Kapitel: Entwurf nach Zuständigkeiten,
Kohäsion, Kopplung, Refactoring
Java-Konstrukte in diesem Kapitel: Aufzählungstypen, switch

In diesem Kapitel untersuchen wir die Faktoren, die den Entwurf einer Klasse beein-
flussen. Wann ist ein Klassenentwurf gut, wann ist er schlecht? Ein guter Klassen-
entwurf kann kurzfristig mehr Aufwand bedeuten als ein schlechter, aber auf lange
Sicht zahlt sich dieser Mehraufwand fast immer aus. Um gute Klassen zu schreiben,
kann man sich an einige Entwurfsprinzipien halten. Insbesondere stellen wir hier die
Sichtweise vor, dass ein Klassenentwurf sich an Zuständigkeiten orientieren sollte
und dass Klassen ihre Daten kapseln sollten.
Dieses Kapitel orientiert sich, wie die vorherigen Kapitel auch, an einem Projekt.
Sie können es einfach nur lesen und seiner Argumentation folgen, oder Sie kön-
nen es sehr viel intensiver bearbeiten, indem Sie die Übungen am Projekt parallel
zum Text bearbeiten.
Die Projektarbeit ist in drei Teile unterteilt. Im ersten Teil diskutieren wie einige
notwendige Änderungen am Quelltext und entwickeln und zeigen komplette
Lösungen zu den Übungen. Die Gesamtlösung für diesen Teil ist auch als ein
eigenes Projekt im Begleitmaterial zu finden. Der zweite Teil schlägt zusätzliche
Änderungen und Erweiterungen vor, die auf der Ebene des Klassenentwurfs dis-
kutiert werden; die konkrete Umsetzung in der Programmierung überlassen wir
jedoch dem Leser.
Der dritte Teil schlägt dann noch weitergehende Verbesserungen in Form von Übun-
gen vor. Hier liefern wir keine Lösungen – die Übungen wenden das vorgestellte
Material dieses Kapitels an.
Die Implementierung aller Teile ist ein gutes Programmierprojekt, das über meh-
rere Wochen laufen kann. Sie kann auch sehr gut in Gruppenarbeit vorgenom-
men werden.

293
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

8.1 Einführung
Eine Anwendung kann implementiert werden und ihre Aufgabe erfüllen, obwohl
ihr Klassenentwurf schlecht ist. Allein durch das Ausführen einer Anwendung kann
man üblicherweise nicht darauf schließen, ob ihr Quelltext gut oder schlecht struk-
turiert ist.
Die Probleme treten meist dann auf, wenn ein Wartungsprogrammierer Ände-
rungen an einer bestehenden Anwendung vornehmen muss. Wenn ein Program-
mierer beispielsweise einen Fehler beheben oder neue Funktionalität hinzufügen
will, dann kann diese Aufgabe bei gut entworfenen Klassen sehr einfach und
gradlinig umsetzbar sein, während sie bei einem schlechten Klassenentwurf viel
Zeit und Energie kosten kann.
Bei großen Anwendungen tritt dieser Effekt schon während der Erstimplementie-
rung auf. Wenn eine Implementierung schon mit einem schlechten Entwurf
beginnt, dann kann die Arbeit an ihr schließlich sehr komplex werden, und die
gesamte Anwendung wird dann entweder nicht fertig, enthält Fehler oder braucht
sehr viel länger. In der Praxis warten, erweitern und verkaufen Unternehmen eine
Anwendung oft über viele Jahre. Es ist heutzutage keine Seltenheit, dass die Imple-
mentierung einer Software, die wir im Laden kaufen können, bereits vor 10 Jahren
begonnen wurde. In einer solchen Situation kann sich eine Softwarefirma schlecht
strukturierten Quelltext schlicht nicht leisten.
Da die meisten Effekte von schlechtem Klassenentwurf am offensichtlichsten
werden, wenn man eine Anwendung anpassen oder erweitern will, werden wir
genau das tun. In diesem Kapitel verwenden wir Die Welt von Zuul, eine rudi-
mentäre Implementierung eines textbasierten Adventure-Games. In seiner vorge-
gebenen Form ist es nicht sonderlich spannend: Es ist insbesondere noch unvoll-
ständig. Am Ende dieses Kapitels werden Sie jedoch so weit sein, dass Sie Ihre
Phantasie einsetzen und Ihre eigene Version des Spiels entwerfen und implemen-
tieren können, die spannender und interessanter ist.

Die Welt von Zuul


Unsere Version des Spiels basiert auf einem Spiel namens Adventure, das in
den frühen Siebzigern von Will Crowther entwickelt und von Don Woods
erweitert wurde. Das Original ist auch unter dem Namen Colossal Cave
Adventure bekannt. Für seine Zeit war es ein wunderbar kreatives und auf-
wendig gestaltetes Spiel, in dem man seinen Weg durch ein komplexes Sys-
tem von Gewölben finden musste, dabei versteckte Schätze heben,
geheime Parolen anwenden und andere spannende Dinge tun musste, um
letztlich die maximale Punktzahl zu erzielen. Sie können mehr über das Spiel
erfahren unter [Link] und unter
[Link] oder führen Sie eine Websuche nach
„Colossal Cave Adventure“ durch.

294
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.1 Einführung

Während wir die vorgegebene Anwendung erweitern, werden wir einige Aspekte
ihres Klassenentwurfs ansprechen. Wir werden feststellen, dass die Implementie-
rung, auf der wir aufsetzen, einige schlechte Entwurfsentscheidungen enthält, und
wir werden untersuchen, inwieweit diese unsere Aufgabe beeinflussen und wie
wir sie beheben können.
Im Begleitmaterial zu diesem Buch werden Sie zwei Versionen des Zuul-Projekts
finden: Zuul-schlecht und Zuul-besser. Beide implementieren die gleiche Funktio-
nalität, aber sie unterscheiden sich an einigen Punkten in der Klassenstruktur. Die
eine Version ist schlecht entworfen, die andere besser. Die Tatsache, dass wir die-
selbe Funktionalität entweder gut oder schlecht strukturiert umsetzen können,
verdeutlicht, dass ein schlechter Entwurf üblicherweise nicht durch die Komplexi-
tät des zu lösenden Problems entschuldigt werden kann. Ein schlechter Entwurf
hängt von den Entscheidungen ab, die wir bei der Lösung eines Problems treffen.
Wir können nicht behaupten, dass es keinen anderen Weg zur Lösung des Prob-
lems gegeben hat, um einen schlechten Entwurf zu rechtfertigen.
Deshalb werden wir das Projekt mit dem schlechten Entwurf untersuchen und auf-
zeigen, warum er schlecht ist, um ihn dann anschließend zu verbessern. In der bes-
seren Version des Projekts sind die Änderungen implementiert, die wir nun disku-
tieren werden.

Übung 8.1 Öffnen Sie das Projekt Zuul-schlecht. (Dieses Projekt heißt
„schlecht“, weil es einige schlechte Entwurfentscheidungen beinhaltet, und
wir wollen keinen Zweifel daran lassen, dass dieses Projekt nicht als ein Bei-
spiel für gute Programmierpraxis genutzt werden soll.) Starten Sie die Anwen-
dung und erkunden Sie diese. Im Projektkommentar finden Sie Hinweise zum
Starten.
Während Sie die Anwendung erkunden, beantworten Sie folgende Fragen:
 Was tut diese Anwendung?
 Welche Befehle akzeptiert das Spiel?
 Was bewirken die einzelnen Befehle?
 Wie viele Räume gibt es in der virtuellen Umgebung?
 Zeichnen Sie eine Karte der gegebenen Räume.
Übung 8.2 Nachdem Sie herausgefunden haben, was die Anwendung ins-
gesamt tut, sollten Sie nun die individuellen Klassen erkunden. Schreiben
Sie für jede Klasse ihren Einsatzzweck auf. Dazu müssen Sie sich den Quell-
text der Klassen ansehen. Beachten Sie, dass Sie möglicherweise nicht jedes
Detail verstehen werden (und auch nicht müssen). Häufig reicht es, die
Kommentare zu lesen und die Methodenköpfe zu untersuchen.

295
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

8.2 Die Welt von Zuul


Durch Übung 8.1 werden Sie festgestellt haben, dass das Spiel nicht sonderlich
abenteuerlich ist. Tatsächlich ist es in diesem Zustand ziemlich langweilig. Aber es
bietet uns eine gute Basis, um ein Spiel zu entwerfen und zu implementieren,
das hoffentlich etwas interessanter sein wird.
Wir untersuchen zuerst die gegebenen Klassen und versuchen ihre Funktionsweise
herauszufinden. Das Klassendiagramm ist in Abbildung 8.1 zu sehen.

Abbildung 8.1
Das Klassendiagramm
für Zuul.

Das Diagramm zeigt fünf Klassen: Parser, Befehlswoerter, Befehl, Raum und Spiel.
Eine Untersuchung des Quelltextes zeigt, dass diese Klassen recht gut dokumen-
tiert sind und wir schon einen guten Überblick über die Anwendung bekommen
können, indem wir nur die Klassenkommentare lesen. (Dies zeigt uns auch, dass
schlechter Entwurf etwas Grundlegenderes ist, das nicht am Aussehen oder an
der Dokumentation der Klassen erkennbar ist.) Wir verstehen das besser, wenn
wir im Quelltext untersuchen, welche Methoden die einzelnen Klassen haben
und was diese Methoden tun. Wir fassen den Zweck der einzelnen Klassen hier
kurz zusammen:
 Befehlswoerter: Die Klasse Befehlswoerter definiert die gültigen Befehle für das
Spiel. Sie hält dazu ein Array von string-Objekten für die Befehlswörter.
 Parser: Der Parser liest Eingabezeilen von der Konsole und versucht, sie als
Befehle zu interpretieren. Er erzeugt Objekte der Klasse Befehl, die die einge-
gebenen Befehle repräsentieren.
 Befehl: Ein Befehl-Objekt repräsentiert einen Befehl, den der Benutzer eingege-
ben hat. Es bietet Methoden, mit denen wir leicht herausfinden können, ob es
ein gültiger Befehl ist und aus welchen Wörtern sich der Befehl zusammensetzt.
 Raum: Ein Raum-Objekt repräsentiert einen Ort im Spielgeschehen. Räume kön-
nen Ausgänge haben, die zu anderen Räumen führen.
 Spiel: Die Klasse Spiel ist die Hauptklasse des Spiels. Sie initialisiert das Spiel
und liest dann in einer Schleife Befehle ein und führt sie aus. Die Klasse ent-
hält auch die Implementierung der einzelnen Befehle.

296
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.3 Kopplung und Kohäsion

Übung 8.3 Entwerfen Sie ein eigenes Szenario für ein Spiel. Tun Sie das ohne
Computer. Denken Sie dabei nicht über Implementierungen, Klassen oder
überhaupt Programmieren im Allgemeinen nach. Erfinden Sie einfach ein span-
nendes Spiel. Sie können dies auch in Gruppenarbeit tun.
Zugelassen ist alles, bei dem ein Spieler sich durch unterschiedliche Räum-
lichkeiten bewegt. Hier ein paar Anregungen:
 Sie sind ein weißes Blutkörperchen, das durch einen Körper fließt und auf
der Suche nach angreifenden Viren ist …
 Sie haben sich in einem Einkaufszentrum verlaufen und müssen den Aus-
gang finden …
 Sie sind ein Maulwurf in seiner Höhle und erinnern sich nicht, an welcher
Stelle Sie Ihre Futterreserven für den Winter versteckt haben …
 Sie sind ein Abenteurer, der ein Verließ voller Monster und anderer Krea-
turen durchsucht …
 Sie gehören zu einem Sprengstoffkommando und müssen eine Bombe
finden und entschärfen, bevor sie explodiert …
Stellen Sie sicher, dass Ihr Spiel ein Ziel hat (sodass es ein Ende gibt und der
Spieler „gewinnen“ kann). Integrieren Sie viele Elemente, die das Spiel inte-
ressant machen können (Falltüren; magische Gegenstände; Charaktere, die
nur helfen, wenn sie gefüttert werden; Zeitbeschränkungen – alles, was
Ihnen in den Sinn kommt). Lassen Sie Ihrer Phantasie freien Lauf.
Sorgen Sie sich an diesem Punkt nicht darum, wie diese Dinge implemen-
tiert werden könnten.

8.3 Kopplung und Kohäsion


Um die Aussage zu stützen, dass manche Entwürfe besser sind als andere, müs- Konzept
sen wir einige Begriffe definieren, mit deren Hilfe wir die wichtigen Eigenschaf-
Der Begriff Kopp-
ten von Klassenentwürfen diskutieren können. Zwei Begriffe spielen eine zen-
lung beschreibt
trale Rolle bei der Qualität von Klassenentwürfen: Kopplung und Kohäsion. den Grad der
Der Begriff Kopplung bezieht sich auf die Art, wie Klassen miteinander verknüpft Abhängigkeiten
zwischen Klassen.
sind. Wir haben bereits in früheren Kapiteln betont, dass wir Anwendungen als
Wir streben für ein
Mengen von kooperierenden Klassen entwerfen wollen, die über wohldefinierte System eine mög-
Schnittstellen miteinander kommunizieren. Der Grad der Kopplung gibt an, wie lichst lose Kopp-
eng diese Klassen miteinander verknüpft sind. Wir streben einen möglichst nied- lung an – also ein
rigen Grad an Kopplung an, eine möglichst lose Kopplung. System, in dem jede
Klasse weitgehend
Der Grad der Kopplung bestimmt, wie schwierig Änderungen an einer Anwen- unabhängig ist und
dung sind. In einer eng gekoppelten Klassenstruktur kann eine Änderung an einer mit anderen Klassen
Klasse viele Änderungen an anderen Klassen nach sich ziehen. Das ist genau der nur über möglichst
Effekt, den wir vermeiden wollen, denn auf diese Weise zieht sich eine Änderung schmale, wohldefi-
schnell durch eine ganze Anwendung. Zusätzlich kann es schwierig und zeitauf- nierte Schnittstellen
kommuniziert.
wendig sein, alle betroffenen Stellen zu finden und anzupassen.

297
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

In einem lose gekoppelten System hingegen können wir häufig Änderungen an


einer einzelnen Klasse vornehmen, ohne dass irgendeine andere Klasse geändert
werden muss, und die Anwendung läuft weiterhin. Wir werden einige konkrete
Beispiele von loser und enger Kopplung in diesem Kapitel zeigen.

Konzept Der Begriff Kohäsion bezieht sich auf die Anzahl und Vielfalt der Aufgaben, für
die eine einzelne Einheit in einer Anwendung zuständig ist. Kohäsion ist wichtig
Der Begriff
für einzelne Klassen und auch für einzelne Methoden.1
Kohäsion
beschreibt, wie gut Idealerweise sollte eine Programmeinheit für genau eine in sich geschlossene
eine Programmein- Aufgabe zuständig sein (also für eine Aufgabe, die als eine zusammenhängende,
heit eine logische logische Einheit angesehen werden kann). Eine Methode sollte eine logische
Aufgabe oder Ein-
Operation implementieren und eine Klasse sollte genau einen Typ von Objekt
heit abbildet. In
einem System mit
modellieren. Der Hauptanlass für Kohäsion ist die Wiederverwendung: Wenn
hoher Kohäsion ist eine Methode oder eine Klasse für genau eine sehr klar definierte Aufgabe ver-
jede Programmein- antwortlich ist, dann ist die Wahrscheinlichkeit höher, dass diese Einheit auch in
heit (eine Methode, anderen Zusammenhängen eingesetzt werden kann. Ein ergänzender Vorteil
eine Klasse oder beim Befolgen dieses Prinzips ist, dass im Falle von Änderungen die Stellen, die
ein Modul) verant- von diesen Änderungen betroffen sind, eher in einer Einheit zu finden sind.
wortlich für genau
eine wohldefinierte Wir werden nun anhand einiger Beispiele zeigen, wie Kohäsion die Qualität eines
Aufgabe oder Ein- Klassenentwurfs beeinflussen kann.
heit. Ein guter Klas-
senentwurf weist
einen hohen Grad
an Kohäsion auf.
Übung 8.4 Zeichnen Sie (auf Papier) eine Karte für das Spiel, das Sie in
Übung 8.3 erfunden haben. Öffnen Sie das Projekt Zuul-schlecht und spei-
chern Sie es unter einem anderen Namen (Zuul beispielsweise). An diesem
Projekt werden Sie im Laufe dieses Kapitels Veränderungen und Erweiterun-
gen vornehmen. Sie können das Suffix -schlecht weglassen, da dieses Pro-
jekt (hoffentlich) bald nicht mehr ganz so schlecht ist.
Als ersten Schritt sollen Sie die Methode raeumeAnlegen so ändern, dass sie
die Räume und Ausgänge für Ihr Spiel anlegt.

8.4 Code-Duplizierung
Konzept Die Duplizierung von Quelltextabschnitten (Code-Duplizierung) ist ein Indiz
schlechten Entwurfs. Die Klasse Spiel in Listing 8.1 enthält ein Beispiel für eine
Code-Duplizie-
solche Duplizierung. Das Problem mit Code-Duplizierung ist, dass Änderungen
rung (ein Quell-
textabschnitt an der einen Stelle immer auch an der anderen durchgeführt werden müssen,
erscheint mehr als damit keine Inkonsistenzen entstehen. Dies erhöht den Aufwand für einen War-
einmal in einer tungsprogrammierer und die Gefahr von Fehlern. Es kommt häufig vor, dass ein
Anwendung) ist Wartungsprogrammierer die eine Kopie des Abschnitts findet, den Fehler behebt
ein Indiz für einen und dann glaubt, alles sei erledigt. Es gibt keinen Hinweis darauf, dass eine
schlechten Ent- zweite Kopie des Abschnitts existiert, und diese kann weiterhin im falschen
wurf. Sie sollte ver-
Zustand verbleiben.
mieden werden.

1 Wir verwenden teilweise auch den Begriff Modul (oder Paket in Java), um uns auf Einheiten zu
beziehen, die aus mehreren Klassen bestehen. Kohäsion ist auch auf dieser Ebene wichtig.

298
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.4 Code-Duplizierung

Listing 8.1
Ausgewählte
Abschnitte aus der
(schlecht entworfe-
nen) Klasse Spiel.

299
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Sowohl die Methode willkommenstextAusgeben als auch die Methode wechsleRaum


enthalten den folgenden Quelltextabschnitt:
[Link]("Sie sind " + [Link]());
[Link]("Ausgänge: ");
if([Link] != null) {
[Link]("north ");
}
if([Link] != null) {
[Link]("east ");
}
if([Link] != null) {
[Link]("south ");
}
if([Link] != null) {
[Link]("west ");
}
[Link]();
Code-Duplizierung ist ein Zeichen für schlechte Kohäsion. Die Wurzel des Prob-
lems liegt hier darin, dass beide Methoden zweierlei Dinge tun: willkommens-
textAusgeben gibt einen Willkommenstext aus und liefert Informationen über den
aktuellen Raum, während wechsleRaum den Raum wechselt und dann Informatio-
nen über den (neuen) aktuellen Raum ausgibt.
Beide Methoden geben Informationen über den aktuellen Raum aus, aber keine
kann die andere aufrufen, weil sie auch noch andere Dinge erledigen. Das ist ein
schlechter Entwurf.

300
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.5 Erweiterungen für Zuul

Ein besserer Entwurf würde eine separate Methode mit verbesserter Kohäsion
benutzen, deren einzige Aufgabe ist, Informationen über den aktuellen Raum aus-
zugeben (Listing 8.2). Sowohl willkommenstextAusgeben als auch wechsleRaum kön-
nen dann diese Methode aufrufen, wenn sie diese Informationen ausgeben lassen
wollen. So wird vermieden, dass der gleiche Text zweimal geschrieben werden
muss, und wir brauchen bei einer Änderung nur eine Stelle zu korrigieren.

Listing 8.2
rauminfoAusgeben
als eigene Methode.

Übung 8.5 Implementieren und benutzen Sie eine eigene Methode raum-
infoAusgeben in Ihrem Projekt, so wie oben beschrieben. Testen Sie Ihre
Änderungen.

8.5 Erweiterungen für Zuul


Das Projekt Zuul-schlecht funktioniert. Wir können es ausführen und es macht all
das, was es tun soll. Aber an einigen Stellen ist es schlecht entworfen. Eine gut ent-
worfene Alternative würde genauso funktionieren. Indem wir lediglich das Pro-
gramm ausführen, würden wir keinen Unterschied feststellen.
Sobald wir aber Änderungen am Projekt vornehmen wollen, stellen wir fest, dass
es erheblich aufwendiger ist, ein schlecht entworfenes System zu ändern als ein
gut entworfenes System. Wir werden dies verdeutlichen, indem wir einige Ände-
rungen am Projekt vornehmen. Während wir dies tun, werden wir immer wieder
auf schlechte Entwurfsentscheidungen im bestehenden Quelltext hinweisen und
sie beheben, bevor wir die Änderungen vornehmen.

8.5.1 Die Aufgabe


Als Erstes werden wir versuchen, neue Bewegungsrichtungen in das Spiel einzu-
bauen. Momentan kann sich ein Spieler in vier Richtungen bewegen: north, east,
south und west. Wir wollen nun auch mehrstöckige Gebäude ermöglichen (oder
Keller oder Verliese oder was Sie Ihrem Spiel sonst noch hinzufügen möchten)
und deshalb up und down als mögliche Richtungen einführen.

301
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

8.5.2 Ermitteln der betroffenen Quelltextstellen


Eine Untersuchung der gegebenen Klassen zeigt uns, dass mindestens zwei Klassen
von dieser Änderungen betroffen sind: Spiel und Raum.
Raum ist die Klasse, die (neben anderen Dingen) die Ausgänge eines Raums spei-
chert, und in Spiel werden, wie wir in Listing 8.1 gesehen haben, diese Informa-
tionen benutzt, um dem Benutzer mögliche Ausgänge melden zu können und
um den Raum zu wechseln.
Die Klasse Raum ist recht kurz. Ihr Quelltext wird in Listing 8.3 gezeigt. Beim Lesen der
Klassendefinition stellen wir fest, dass die Ausgänge an zwei Stellen erwähnt wer-
den: Sie werden zu Anfang als Datenfelder aufgezählt und dann in der Methode
setzeAusgänge gesetzt. Um zwei neue Richtungen (etwa deckenausgang und boden-
ausgang) einzufügen, müssten wir sie an diesen Stellen einführen.

Listing 8.3
Quelltext der
(schlecht entworfenen)
Klasse Raum.

302
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.6 Kopplung

Es ist etwas aufwendiger, die betroffenen Stellen in der Klasse Spiel zu finden. Der
Quelltext ist etwas länger (er ist hier nicht vollständig aufgeführt) und das Finden
der betroffenen Stellen erfordert einige Geduld und Vorsicht.
Wenn wir den Quelltext in Listing 8.1 betrachten, dann sehen wir, dass sich die
Klasse Spiel sehr stark auf die Informationen über die Ausgänge bezieht. Ein Spiel-
Objekt hält eine Referenz auf einen Raum in der Variablen aktuellerRaum und greift
häufig auf die Ausgangsinformationen dieses Raums zu:
 In der Methode raeumeAnlegen werden die Ausgänge definiert.
 In der Methode willkommenstextAusgeben werden die Ausgänge des aktuellen
Raums ausgegeben, damit ein Spieler beim Spielstart weiß, in welche Richtun-
gen er gehen kann.
 In der Methode wechsleRaum werden die Ausgänge benutzt, um den nächsten
Raum zu finden. Sie werden dann noch einmal benutzt, um die Ausgänge des
Raums auszugeben, den wir gerade betreten haben.
Wenn wir nun die beiden neuen Ausgangsrichtungen up und down hinzufügen
wollen, dann müssen wir die Option an all diesen Stellen einbauen. Bevor Sie dies
tun, sollten Sie jedoch den nächsten Abschnitt lesen.

8.6 Kopplung
Der Umstand, dass alle Ausgänge an so vielen Stellen im Quelltext aufgezählt wer-
den, ist symptomatisch für einen schlechten Klassenentwurf. Bei der Deklaration der
Variablen für die Ausgänge müssen wir eine Variable pro Ausgang definieren; in der
Methode setzeAusgaenge gibt es eine if-Anweisung für jeden Ausgang; in der
Methode wechsleRaum gibt es eine if-Anweisung für jeden Ausgang; in der Methode
rauminfoAusgeben gibt es eine if-Anweisung für jeden Ausgang; und so weiter. Diese
Entwurfsentscheidung bürdet uns nun einiges an Arbeit auf: Beim Einführen neuer
Ausgänge müssen wir alle Stellen finden und diese beiden neuen Fälle einfügen.
Stellen Sie sich den Aufwand vor, wenn wir uns entschieden hätten, Richtungen wie
northwest, southwest etc. einzuführen!
Um diese Situation zu verbessern, entscheiden wir uns statt einzelner Variablen für
eine HashMap zur Speicherung der Ausgänge. Auf diese Weise sollten wir Quelltext
schreiben können, der mit einer beliebigen Anzahl von Ausgängen umgehen kann
und nicht so viele Änderungen nach sich zieht. Die HashMap enthält eine Abbildung
eines Richtungsnamens (etwa "north") auf den Raum, der in dieser Richtung liegt
(ein Raum-Objekt). Jeder Eintrag besteht somit aus einer Zeichenkette als Schlüssel
und einem Raum-Objekt als Wert.
Dies ändert die Art, wie ein Raum Informationen über seine Nachbarräume hält.
Theoretisch sollte eine solche Änderung nur die Implementierung der Klasse betref-
fen (wie die Informationen über die Ausgänge gespeichert werden), aber nicht ihre
Schnittstelle (was ein Raum speichert).
Idealerweise sollten andere Klassen nicht durch eine Änderung an der Implemen-
tierung in einer Klasse betroffen sein. Dann hätten wir wirklich eine lose Kopp-
lung.

303
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

In unserem Beispiel klappt das nicht. Wenn wir die Datenfelder in der Klasse Raum
entfernen und durch eine HashMap ersetzen, lässt sich die Klasse Spiel nicht mehr
übersetzen. Sie nimmt an vielen Stellen Bezug auf die Datenfelder für die Aus-
gänge in der Klasse Raum, die folglich alle Fehler produzieren würden.
Wir sehen, dass wir hier einen Fall von enger Kopplung haben. Um dies zu berei-
nigen, werden wir vor der Einführung der HashMap die beiden Klassen entkoppeln.

8.6.1 Kapselung zur Reduzierung der Kopplung


Eines der Hauptprobleme in diesem Beispiel ist die Benutzung von öffentlichen
Datenfeldern. Die Datenfelder für die Ausgänge sind in der Klasse Raum alle als public
deklariert. Diese Klasse wurde also offenbar von einer Person erstellt, die sich nicht
an eine bereits vorgestellte Grundregel gehalten hat („Definieren Sie Datenfelder
niemals öffentlich!“). Wir werden nun sehen, was das für Folgen haben kann. Die
Klasse Spiel kann direkt auf diese Felder zugreifen (und tut dies auch ausführlich).
Dadurch, dass die Klasse Raum ihre Datenfelder öffentlich anbietet, hat sie nicht nur
den Umstand offengelegt, dass sie über Ausgänge verfügt; sie hat außerdem auch
offengelegt, wie diese Ausgänge genau implementiert sind. Dies bricht mit einem
fundamentalen Prinzip für guten Klassenentwurf: der Kapselung.

Konzept Die Richtlinie für Kapselung (Informationen über die Implementierung verbergen)
besagt, dass nur Informationen über das Was einer Klasse (was sie leistet) nach
Saubere Kapse-
außen sichtbar sein sollten, nicht aber das Wie (ihre Realisierung). Das hat einen
lung reduziert die
Kopplung und führt entscheidenden Vorteil: Wenn keiner anderen Klasse bekannt ist, wie wir unsere
deshalb zu besse- Implementierung vornehmen, dann können wir die Implementierung ändern,
ren Entwürfen. ohne dass andere Klassen beeinträchtigt werden.
Wir können diese Trennung zwischen dem Was und dem Wie erzwingen, indem
wir die Datenfelder privat deklarieren und eine sondierende Methode für den
Zugriff auf sie definieren. Die erste Stufe unserer geänderten Klasse Raum ist in Lis-
ting 8.4 gezeigt.

Listing 8.4
Eine sondierende
Methode, um
die Kopplung zu
reduzieren.

304
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.6 Kopplung

Nach dieser Änderung an der Klasse Raum müssen wir auch die Klasse Spiel anpas-
sen. An allen Stellen, an denen bisher direkt auf die Datenfelder zugegriffen wurde,
verwenden wir nun die sondierende Methode. Beispielsweise schreiben wir statt
naechsterRaum = [Link];
nun
naechsterRaum = [Link]("east");
Dies macht auch einen Abschnitt in der Klasse Spiel viel einfacher. In der Methode
wechsleRaum führt diese Änderung zu folgendem Quelltext:
Raum naechsterRaum = null;
if ([Link]("north")) {
naechsterRaum = [Link]("north");
}
if ([Link]("east")) {
naechsterRaum = [Link]("east");
}
if ([Link]("south")) {
naechsterRaum = [Link]("south");
}
if ([Link]("west")) {
naechsterRaum = [Link]("west");
}
Dieser lange Abschnitt kann nun durch die folgende Anweisung ersetzt werden:
Raum naechsterRaum = [Link](richtung);

Übung 8.6 Führen Sie die beschriebenen Änderungen an den Klassen Raum
und Spiel durch.
Übung 8.7 Führen Sie eine ähnliche Änderung auch an der Methode raum-
infoAusgeben in der Klasse Spiel durch, sodass die Details der Ausgänge nun
von Raum und nicht mehr von Spiel geliefert werden. Definieren Sie dazu
eine Methode in der Klasse Raum mit folgendem Kopf:
/**
* Liefere eine Beschreibung der Ausgänge dieses Raumes,
* beispielsweise "Ausgänge: north west".
* @return eine Beschreibung der verfügbaren Ausgänge.
*/
public String gibAusgaengeAlsString()

Bisher haben wir noch nicht die Repräsentation der Ausgänge in der Klasse Raum
geändert. Wir haben lediglich die Schnittstelle etwas aufgeräumt. Die Änderung
in der Klasse Spiel ist minimal – statt dem Zugriff auf ein Datenfeld benutzen wir
einen Methodenaufruf –, aber der Gewinn ist dramatisch. Wir können nun sehr
leicht die Art der Speicherung von Ausgängen im Raum ändern, ohne uns darum
sorgen zu müssen, dass in der Klasse Spiel dadurch etwas nicht mehr funktio-

305
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

niert. Die interne Repräsentation wurde komplett von der Schnittstelle entkop-
pelt. Nachdem wir den Entwurf so geändert haben, wie er von Anfang an hätte
sein sollen, ist der Austausch der Datenfelder für die Ausgänge durch eine Hash-
Map leicht. Der geänderte Quelltext ist in Listing 8.5 zu sehen.

Listing 8.5
Quelltext der
Klasse Raum.

Beachten Sie, dass wir diese Änderung nun vornehmen können, ohne prüfen zu
müssen, ob an einer anderen Stelle etwas schiefgehen kann. Da wir nur private
Aspekte der Klasse Raum verändert haben, die per Definition nicht von anderen

306
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.6 Kopplung

Klassen benutzt werden können, kann diese Änderung keine andere Klasse betref-
fen. Die Schnittstelle bleibt unverändert.
Ein Nebenprodukt unserer Änderung ist, dass die Klasse Raum nun sogar kürzer
geworden ist. Anstelle von vier einzelnen Datenfeldern haben wir nur noch
eines. Zusätzlich wurde die Methode gibAusgang erheblich vereinfacht.
Rufen Sie sich noch einmal in Erinnerung, dass wir all diese Änderungen nur des-
halb begonnen haben, weil unsere ursprüngliche Absicht war, zusätzliche Aus-
gänge nach oben und nach unten zu definieren. Dies ist jetzt erheblich einfacher
geworden. Da wir die Ausgänge nun in einer HashMap speichern, können wir die
beiden zusätzlichen Ausgänge ohne jede weitere Änderung hinzufügen. Wir
können auch problemlos die Information über einen Ausgang mit der Methode
gibAusgang abfragen.

Die einzige Stelle im Quelltext, an der nach wie vor Wissen über die vier Ausgänge
(north, east, south, west ) „fest verdrahtet“ ist, ist in der Methode setzeAusgaenge.
Dies ist der letzte Teil, der noch verbessert werden muss. Momentan sieht der Kopf
folgendermaßen aus:
public void setzeAusgaenge(Raum norden, Raum osten, Raum sueden, Raum westen)
Diese Methode ist Teil der Schnittstelle der Klasse Raum. Eine Änderung an ihrer
Signatur führt deshalb unweigerlich dazu, dass aufgrund der Kopplung auch
andere Klassen von Änderungen betroffen sind. Wir können die Klassen einer
Anwendung niemals vollständig voneinander entkoppeln, denn dann würden sie
gar nicht mehr miteinander interagieren. Stattdessen versuchen wir den Grad der
Kopplung so niedrig wie möglich zu halten. Wenn wir sowieso eine Änderung an
der Methode setzeAusgaenge vornehmen wollen, um weitere Richtungen zu
ermöglichen, dann ziehen wir vor, die Methode vollständig durch die folgende zu
ersetzen:
/**
* Definiere einen Ausgang aus diesem Raum.
* @param richtung die Richtung, in der der Ausgang liegen soll.
* @param nachbar der Raum, der über diesen Ausgang erreicht wird.
*/

public void setzeAusgang(String richtung, Raum nachbar)


{
[Link](richtung, nachbar);
}
So können die Ausgänge eines Raums einer nach dem anderen definiert werden
und es kann eine beliebige Richtungsbenennung für einen Ausgang gewählt
werden. In der Klasse Spiel wirken sich die Änderungen an der Schnittstelle von
Raum folgendermaßen aus: Statt
[Link](draussen, buero, null, null);
schreiben wir nun
[Link]("north", draussen);
[Link]("east", buero);

307
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Insgesamt haben wir die Beschränkung, dass ein Raum nur vier Ausgänge haben kann,
komplett aus dem Quelltext entfernt. Die Klasse Raum ist nun bereit, auch Ausgänge
wie up und down oder beliebige andere aufzunehmen (northwest, southeast etc.).

Übung 8.8 Implementieren Sie die in diesem Abschnitt beschriebenen Ände-


rungen in Ihrem eigenen Zuul-Projekt.

8.7 Entwurf nach Zuständigkeiten


Im vorigen Abschnitt haben wir gesehen, dass wir mit sauberer Kapselung die
Kopplung und damit den Aufwand bei Änderungen in einer Anwendung erheb-
lich reduzieren können. Kapselung ist jedoch nicht der einzige Faktor, der den
Grad der Kopplung beeinflusst. Ein anderer Aspekt wird unter dem Begriff Ent-
wurf nach Zuständigkeiten (responsibility-driven design) gefasst.
Entwurf nach Zuständigkeiten basiert auf der Idee, dass jede Klasse für den Umgang
mit ihren Daten zuständig sein sollte. Wenn wir einer Anwendung neue Funktionali-
tät hinzufügen, fragen wir uns oft, welcher Klasse wir die Methode mit der neuen
Funktion hinzufügen sollten. Welche Klasse ist verantwortlich für diese Aufgabe? Die
Antwort lautet: Wenn eine Klasse verantwortlich für bestimmte Daten ist, dann ist
sie auch verantwortlich für den Umgang mit diesen Daten.
Wenn wir stark an Zuständigkeiten orientiert entwerfen, dann beeinflusst dies
den Grad der Kopplung und damit auch den Aufwand für Änderungen oder
Erweiterungen an einer Anwendung. Wie üblich werden wir dies ausführlicher
an einem Beispiel demonstrieren.

Konzept 8.7.1 Zuständigkeiten und Kopplung


Entwurf nach Die Änderungen der Klasse Raum, die wir in Abschnitt 8.6.1 diskutiert haben, machen
Zuständig- es recht leicht, neue Richtungen für Bewegungen nach oben oder unten zu definie-
keiten ist ein Ent- ren. Wir untersuchen dies anhand eines Beispiels: Nehmen Sie an, wir wollen einen
wurfsprozess, bei neuen Raum (einen Keller) unter dem Büro definieren. Wir müssen dazu lediglich
dem jeder Klasse einige kleine Änderungen in der Methode raeumeAnlegen in der Klasse Spiel vorneh-
eine klare Verant-
men, um den neuen Raum zu erzeugen und zwei Ausgänge zu setzen. Etwa so:
wortung zugewie-
sen wird. Dieser private void raeumeAnlegen()
Prozess kann {
benutzt werden, Raum draussen, hoersaal, cafeteria, labor, buero, keller;
um festzulegen, ...
welche Klasse für keller = new Raum("im Keller");
welche Funktionen ...
in einer Anwen-
[Link]("down", keller);
dung zuständig
[Link]("up", buero);
sein soll.
}
Mit der neuen Schnittstelle von Raum klappt das ohne Probleme. Diese Änderung
ist nun sehr einfach und zeigt uns, dass der Entwurf besser wird.

308
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.7 Entwurf nach Zuständigkeiten

Wir können diesen Eindruck noch verstärken, indem wir die ursprüngliche Version
der Methode rauminfoAusgeben aus Listing 8.2 mit der Methode gibAusgaengeAls-
String vergleichen, die eine Lösung für Übung 8.7 ist.

Listing 8.6
Die Methode
gibAusgaenge
AlsString in
der Klasse Raum.

Weil die Informationen über seine Ausgänge nun ausschließlich im Raum selbst
abgelegt sind, ist dieser auch für Informationen über Ausgänge zuständig. Der
Raum kann diese Aufgabe sehr viel besser erfüllen als jedes andere Objekt, da er
alle Details der internen Speicherung der Ausgänge kennt. Innerhalb der Klasse
Raum können wir Gebrauch von dem Wissen machen, dass die Ausgänge in einer
HashMap abgelegt sind, indem wir für die Ausgabe der Ausgänge über die Map
iterieren.
Konsequenterweise ersetzen wir deshalb die Version von gibAusgaengeAlsString in
Listing 8.6 mit der Version in Listing 8.7. Diese Methode findet alle Namen für Aus-
gänge in der HashMap (die Schlüssel in der HashMap sind die Namen der Ausgänge)
und verkettet sie zu einer Zeichenkette, die zurückgeliefert wird. (Wir müssen die
Klasse Set von [Link] importieren, damit dies funktioniert.)

Listing 8.7
Eine überarbeitete
Fassung von gib-
AusgaengeAlsString.

Übung 8.9 Schlagen Sie die Methode keySet in der Dokumentation von
HashMap nach. Was tut sie?

Übung 8.10 Erläutern Sie ausführlich und schriftlich, wie die Methode gib-
AusgaengeAlsString in Listing 8.7 arbeitet.

309
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Wir haben nach wie vor das Ziel, die Kopplung in unserem System zu reduzieren.
Dies erfordert unter anderem, dass Änderungen an der Klasse Raum die Klasse
Spiel möglichst wenig beeinflussen sollten. Das ist noch verbesserungswürdig.

Momentan ist in der Klasse Spiel immer noch fest verdrahtet, dass die Informatio-
nen über einen Raum aus einer Raumbeschreibung und einer Beschreibung der
Ausgänge bestehen:
[Link]("Sie sind " + [Link]());
[Link]([Link]());
Was passiert, wenn wir unseren Räumen Gegenstände hinzufügen? Oder Monster?
Oder andere Spieler?
Wenn wir beschreiben, was im Raum zu sehen ist, dann sollte die Liste von Gegen-
ständen, Monstern und anderen Spielern Teil der Beschreibung des Raums sein. Wir
müssten aber nicht nur Änderungen an der Klasse Raum vornehmen, um dies hinzu-
zufügen, sondern auch an dem obigen Quelltextabschnitt, in dem diese Informatio-
nen ausgegeben werden.
Auch dies ist eine Verletzung des Prinzips des Entwurfs nach Zuständigkeiten. Da
die Klasse Raum die Informationen über einen Raum hält, sollte sie auch für die Aus-
gabe dieser Informationen zuständig sein. Wir können dies verbessern, indem wir
der Klasse Raum die folgende Methode hinzufügen:
/**
* Liefere eine lange Beschreibung dieses Raums, in der Form:
* Sie sind in der Küche.
* Ausgänge: north west
* @return eine lange Beschreibung dieses Raumes.
*/
public String gibLangeBeschreibung()
{
return "Sie sind " + beschreibung + ".\n" + gibAusgaengeAlsString();
}
In der Klasse Spiel schreiben wir dann
[Link]([Link]());
Diese „lange Beschreibung“ eines Raums schließt nun die Beschreibung des Raums
selbst und die der Ausgänge ein und kann zukünftig um weitere Informationen
ergänzt werden, die sich durch Erweiterungen ergeben. Wenn wir diese Erweite-
rungen vornehmen, brauchen wir nur noch die Klasse Raum zu ändern.

Übung 8.11 Implementieren Sie die in diesem Abschnitt beschriebenen


Änderungen in Ihrem eigenen Zuul-Projekt.
Übung 8.12 Zeichnen Sie ein Objektdiagramm, das alle Objekte Ihres Spiels
zu dem Zeitpunkt zeigt, zu dem das Spiel gerade gestartet wurde.
Übung 8.13 Wie ändert sich das Objektdiagramm, wenn Sie den Befehl go
ausführen?

310
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.8 Änderungen lokal halten

8.8 Änderungen lokal halten


Ein weiterer Aspekt bei den Prinzipien von Entkopplung und Zuständigkeiten ist, Konzept
dass Änderungen lokal gehalten werden sollten. Wir streben einen Entwurf an,
Eines der Haupt-
bei dem spätere Änderungen leicht gemacht werden, weil sie in ihren Auswir-
ziele eines guten
kungen lokal beschränkt bleiben. Klassenentwurfs
Idealerweise sollte nur eine Klasse für eine Änderung modifiziert werden müssen. lautet, Änderun-
gen lokal hal-
Manchmal müssen mehrere Klassen geändert werden, aber dann streben wir an,
ten: eine Ände-
dass es möglichst wenige Klassen sind. Zusätzlich sollten die notwendigen Ände- rung an einer
rungen an anderen Klassen offensichtlich, leicht zu finden und leicht durchzufüh- Klasse sollte einen
ren sein. möglichst geringen
Einfluss auf andere
Wir können diesen Anspruch weitgehend erfüllen, indem wir nach Zuständigkei- Klassen haben.
ten entwerfen und nach loser Kopplung und hoher Kohäsion streben. Zusätzlich
sollten wir aber auch mögliche Änderungen und Erweiterungen bereits berück-
sichtigen, wenn wir eine Anwendung entwerfen. Es ist wichtig, mögliche Ände-
rungen an Teilen einer Anwendung vorherzusehen, damit diese Änderungen
eventuell erleichtert werden können.

8.9 Implizite Kopplung


Wir haben gesehen, dass die Verwendung von öffentlichen Datenfeldern zu einer
unnötig engen Kopplung zwischen Klassen führen kann. Bei einer solch engen
Kopplung kann es passieren, dass mehr als eine Klasse geändert werden muss,
obwohl die Änderung selbst eigentlich einfach sein sollte. Öffentliche Datenfelder
sollten deshalb vermieden werden. Es gibt allerdings eine weitere Form der Kopp-
lung, die noch unangenehmer ist: die implizite Kopplung.
Implizite Kopplung tritt dann auf, wenn sich eine Klasse auf interne Informatio-
nen einer anderen Klasse abstützt, diese Abhängigkeit aber nicht offensichtlich
erkennbar ist. Die enge Kopplung durch die öffentlichen Datenfelder war nicht
gut, aber zumindest war sie offensichtlich: Wenn wir die öffentlichen Datenfel-
der in einer Klasse ändern und die andere Klasse vergessen, dann lässt sich die
Anwendung nicht mehr übersetzen und der Compiler meldet den Fehler. Bei
impliziter Kopplung hingegen kann eine notwendige Änderung, die übersehen
wurde, unentdeckt bleiben.
Wir können dieses Problem erkennen, wenn wir versuchen, dem Spiel weitere
Befehle hinzuzufügen.
Nehmen Sie an, wir wollen den Befehl look in den Satz der gültigen Befehle auf-
nehmen. Die Funktion von look soll lediglich sein, dass die Beschreibung des
Raums und seiner Ausgänge erneut ausgegeben wird (wir sehen uns – englisch
to look – also im Raum um). Das kann dann nützlich sein, wenn wir in einem
Raum schon einige Befehle eingegeben haben und die Beschreibung bereits aus
dem sichtbaren Bereich des Fensters herausgeschoben wurde, wir aber nicht
mehr wissen, welche Ausgänge dieser Raum hat.

311
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Wir können ein neues Befehlswort einführen, indem wir es einfach in das Array
gueltigeBefehle der bekannten Befehlswörter in der Klasse Befehlswoerter einfügen:
// ein konstantes Array mit den gültigen Befehlswörtern
private static final String gueltigeBefehle[] = {
"go", "quit", "help", "look"
};

Dies ist ein gutes Beispiel für hohe Kohäsion: Statt die Befehlswörter in der Klasse
Parser zu definieren, einer durchaus passenden Stelle, hat der Autor eine sepa-
rate Klasse angelegt, die ausschließlich die Befehlswörter definiert. Dies macht es
uns jetzt sehr leicht, die Stelle zu finden, in der die Befehlswörter definiert sind,
und den neuen Befehl einzufügen. Der Autor hat offensichtlich vorausgedacht
und angenommen, dass noch weitere Befehle hinzukommen werden, und ent-
sprechend eine Struktur definiert, die dies erleichtert.

Wir können diese Änderung sofort testen. Tatsächlich passiert nichts, wenn wir
das Spiel starten und look eingeben. Dies entspricht nicht dem Verhalten bei
unbekannten Befehlen: Wenn wir einen unbekannten Befehl eingeben, sehen
wir als Antwort
Ich weiß nicht, was Sie meinen ...

Somit wird der Befehl anscheinend erkannt, aber es passiert nichts, weil wir noch
keine Aktion für diesen Befehl implementiert haben.

Wir können dies nun tun, indem wir für den Befehl look in die Klasse Spiel eine
neue Methode einfügen:
private void umsehen()
{
[Link]([Link]());
}

Sie sollten natürlich einen Kommentar für diese Methode hinzufügen. Anschlie-
ßend müssen wir nur noch eine Fallunterscheidung in der Methode verarbeite-
Befehl einfügen, die umsehen aufruft, wenn der Befehl look erkannt wurde:
if([Link]("help")) {
hilfstextAusgeben();
}
else if([Link]("go")) {
wechsleRaum(befehl);
}
else if([Link]("look")) {
umsehen();
}
else if([Link]("quit")) {
moechteBeenden = beenden(befehl);
}
Versuchen Sie es – es wird funktionieren.

312
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.9 Implizite Kopplung

Übung 8.14 Fügen Sie Ihrem eigenen Zuul-Projekt den Befehl look hinzu.
Übung 8.15 Fügen Sie dem Spiel einen weiteren Befehl hinzu. Als einfachen
Anfang könnten Sie einen Befehl wie eat wählen, der, wenn er aufgerufen
wird, zu der Meldung „Sie haben nun gegessen und sind nicht mehr hung-
rig.“ führt. Später können wir dies verbessern, indem der Spieler tatsächlich
im Laufe der Zeit immer hungriger wird und etwas zu essen finden muss.

Die Kopplung zwischen den Klassen Spiel, Parser und Befehlswoerter schien bisher
sehr gut zu sein – die Änderung fiel leicht und wir haben sie schnell zum Laufen
gebracht. Das bereits erwähnte Problem der impliziten Kopplung wird erst offen-
sichtlich, wenn wir nun den Befehl help eingeben. Die Ausgabe lautet dann:
Sie haben sich verlaufen. Sie sind allein. Sie irren
auf dem Unigelände herum.
Ihnen stehen folgende Befehle zur Verfügung:
go quit help
Hier erkennen wir ein kleines Problem. Der Hilfstext ist unvollständig: Der neue
Befehl look ist nicht aufgeführt.
Das können wir anscheinend leicht beheben: Wir verändern einfach den Hilfstext
in der Methode hilfstextAusgeben in Spiel: Das ist schnell erledigt und scheint
kein großes Problem zu sein. Was aber, wenn wir den Fehler nicht bemerkt hät-
ten? Haben Sie an dieses Problem gedacht, bevor Sie es gerade gelesen haben?
Dies ist ein grundsätzliches Problem. Jedes Mal, wenn ein neuer Befehl einge-
führt wird, muss der Hilfstext angepasst werden, und das kann leicht vergessen
werden. Das Programm lässt sich übersetzen und alles scheint in Ordnung zu
sein. Ein Wartungsprogrammierer könnte durchaus glauben, dass seine Aufgabe
erledigt ist, und das Programm freigeben, das dann einen Fehler enthält.
Dies ist ein Beispiel für implizite Kopplung: Wenn sich die Befehle ändern, dann muss
der Hilfstext angepasst werden (Kopplung), aber im Quelltext gibt es keinen Hinweis
darauf, dass diese Abhängigkeit besteht (deshalb implizit).
Bei einer gut entworfenen Klasse wird diese Form von Kopplung vermieden,
indem den Regeln des Entwurfs nach Zuständigkeiten gefolgt wird: Da die Klasse
Befehlswoerter verantwortlich ist für die Befehlswörter, sollte sie auch für die Dar-
stellung der Befehle zuständig sein. Deshalb fügen wir der Klasse Befehlswoerter
die folgende Methode hinzu:
/*
* Gib alle gültigen Befehlswörter auf der Konsole aus.
*/
public void alleAusgeben()
{
for(String befehl : gueltigeBefehle) {
[Link](befehl + " ");
}
[Link]();
}

313
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Die Idee ist dann, dass die Methode hilfstextAusgeben in der Klasse Spiel nicht
einen fest definierten Text mit den Befehlswörtern ausgibt, sondern eine Methode
an der Klasse Befehlswoerter aufruft, die alle Befehlswörter ausgibt. Dies bewirkt,
dass immer die korrekten Befehlswörter ausgegeben werden und der Hilfstext
auch nach Einführung eines neuen Befehls korrekt ist.
Als letztes Problem bleibt nur noch, dass das Spiel-Objekt keine Referenz auf das
Befehlswoerter-Objekt hat. Sie sehen im Klassendiagramm (Abbildung 8.1), dass
es keinen Pfeil von Spiel zu Befehlswoerter gibt. Dies zeigt uns, dass die Klasse
Spiel nichts von der Existenz der Klasse Befehlswoerter weiß. Stattdessen benutzt
sie einen Parser, und der hat Befehlswörter.
Wir könnten nun dem Parser eine Methode hinzufügen, mit der ein Spiel-Objekt das
Befehlswoerter-Objekt vom Parser erfragen kann. Dies würde aber den Grad der
Kopplung in unserem System erhöhen: Spiel würde dann von Befehlswoerter abhän-
gen, was vorher nicht der Fall war. Wir würden den Effekt im Klassendiagramm
sehen können: Es würde zusätzlich ein Pfeil von Spiel zu Befehlswoerter auftauchen.
Die Pfeile im Klassendiagramm sind tatsächlich ein guter erster Hinweis darauf,
wie eng gekoppelt ein Programm ist. Je mehr Pfeile, desto enger die Kopplung.
Als Richtlinie für guten Klassenentwurf sollten wir deshalb Diagramme mit mög-
lichst wenigen Pfeilen anstreben.
Also ist es gut, dass Spiel keine Referenz auf Befehlswoerter hat! Wir sollten das
nicht ändern. Aus Sicht von Spiel ist die Tatsache, dass der Parser eine Klasse
Befehlswoerter benutzt, ein Implementierungsdetail des Parsers. Der Parser liefert
Befehle, und ob er dazu die Hilfe von weiteren Klassen benötigt, ist vollständig
seiner Implementierung überlassen.
In einem guten Entwurf kommuniziert Spiel mit dem Parser und dieser dann wie-
derum mit Befehlswoerter. Wir können dies umsetzen, indem wir die folgenden
Zeilen in der Methode hilfstextAusgeben in Spiel einfügen:
[Link]("Ihnen stehen folgende Befehle zur Verfügung:");
[Link]();
Es fehlt dann nur die Methode zeigeBefehle in der Klasse Parser, die diese Anfrage
an die Klasse Befehlswoerter weiterleitet. Hier ist die vollständige Methode für die
Klasse Parser:
/**
* Gib eine Liste der bekannten Befehlswörter aus.
*/
public void zeigeBefehle()
{
[Link]();
}

Übung 8.16 Implementieren Sie die verbesserte Version der Ausgabe der Be-
fehle, wie wir sie in diesem Abschnitt beschrieben haben.
Übung 8.17 Wenn Sie nun einen weiteren Befehl hinzufügen: Müssen Sie
immer noch die Klasse Spiel ändern? Warum?

314
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.10 Vorausdenken

Die vollständige Implementierung aller in diesem Kapitel diskutierten Änderun-


gen finden Sie im Begleitmaterial im Projekt Zuul-besser. Wenn Sie alle Übungen
durchgeführt haben, können Sie das Projekt ignorieren und weiter mit Ihrem
eigenen arbeiten. Wenn Sie die Übungen nicht gemacht haben, aber die folgen-
den Übungen als Programmieraufgabe durchführen wollen, dann können Sie das
Projekt Zuul-besser als Startpunkt nehmen.

8.10 Vorausdenken
Der Entwurf, den wir nun erstellt haben, stellt eine erhebliche Verbesserung
gegenüber der ursprünglichen Version dar. Er kann aber immer noch verbessert
werden.

Eine Eigenschaft eines guten Softwareentwicklers ist die Fähigkeit zum Voraus-
denken. Was könnte sich ändern? Welche Teile werden voraussichtlich während
der Lebenszeit des Programms unverändert bleiben?

Eine Annahme, die wir in den meisten unserer Klassen hart verdrahtet haben, ist,
dass wir dieses Spiel textbasiert mit der Ein- und Ausgabe über die Konsole spielen
werden. Aber wird das immer so bleiben?

Es könnte eine interessante Erweiterung sein, später eine grafische Benutzungs-


oberfläche mit Menüs, Knöpfen und Bildern hinzuzufügen. In dem Fall würden
wir die Ausgabe nicht mehr über das Terminal vornehmen wollen. Wir könnten
immer noch Befehlswörter haben und sie immer noch ausgeben wollen, wenn
ein Spieler den Befehl help eingibt. Aber dann eher in einem Textfeld innerhalb
eines Fensters statt einer Ausgabe über [Link].

In einem guten Entwurf sollten alle Informationen über die Benutzungsschnitt-


stelle in einer Klasse oder zumindest in einer klar definierten Menge von Klas-
sen gekapselt sein. Unsere Lösung in Abschnitt 8.9 beispielsweise, die Methode
alleAusgeben in der Klasse Befehlswoerter, folgt dieser Entwurfsregel nicht. Es
wäre schön zu definieren, dass Befehlswoerter zuständig ist für das Produzie-
ren (aber nicht das Ausgeben!) der Befehlsliste, aber die Klasse Spiel sollte da-
rüber entscheiden, wie diese Information dem Spieler gegenüber dargestellt
wird.

Wir können dies leicht ändern, indem wir die Methode alleAusgeben so ändern,
dass sie die Befehlswörter als Zeichenkette zurückliefert, statt sie selbst auszu-
geben. (Wir sollten sie dann besser in gibBefehlsliste umbenennen.) Diese Zei-
chenkette kann dann in der Methode hilfstextAusgeben in Spiel ausgegeben
werden.

Beachten Sie, dass uns diese Änderung momentan keinen Gewinn liefert, wir
aber möglicherweise in der Zukunft von diesem besseren Entwurf profitieren
könnten.

315
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Übung 8.18 Implementieren Sie die vorgeschlagene Änderung. Stellen Sie


sicher, dass das Programm wie vorher abläuft.
Übung 8.19 Finden Sie etwas über das Entwurfsmuster Model-View-Con-
troller heraus. Sie können eine Websuche vornehmen, um an Informationen
zu kommen, oder eine beliebige andere Quelle benutzen. Wie steht dieses
Muster mit der hier geführten Diskussion in Beziehung? Was schlägt das
Muster vor? Wie könnte es auf dieses Projekt angewendet werden? (Disku-
tieren Sie seine Anwendung in diesem Projekt lediglich, da eine tatsächliche
Implementierung eine anspruchsvolle Zusatzaufgabe wäre.)

8.11 Kohäsion
Konzept Wir haben den Grundgedanken von Kohäsion in Abschnitt 8.3 vorgestellt: Eine
Programmeinheit sollte immer nur für genau eine Aufgabe zuständig sein. Wir
Kohäsion von
werden dieses Prinzip nun näher beleuchten und uns einige Beispiel ansehen.
Methoden: Eine
Methode mit hoher Das Prinzip der Kohäsion kann auf Klassen und auf Methoden angewendet wer-
Kohäsion ist verant- den: Sowohl Klassen als auch Methoden sollten einen hohen Grad an Kohäsion
wortlich für genau
aufweisen.
eine wohldefinierte
Aufgabe.

8.11.1 Kohäsion von Methoden


Wenn wir über die Kohäsion von Methoden sprechen, dann wollen wir das Ideal
formulieren, dass eine Methode nur für genau eine wohldefinierte Aufgabe zustän-
dig ist.
Wir können einige Beispiele für Methoden mit hoher Kohäsion in der Klasse
Spiel sehen. Diese Klasse hat eine private Methode willkommenstextAusgeben für
den Eröffnungstext, die aufgerufen wird, wenn ein Spiel in der Methode spielen
gestartet wird (Listing 8.8).
Aus funktionaler Sicht hätten wir die Anweisungen aus der Methode willkom-
menstextAusgeben auch direkt in der Methode spielen angeben können; das hätte
das gleiche Ergebnis geliefert und wäre mit einem Methodenaufruf weniger aus-
gekommen. Das Gleiche kann übrigens auch für die Methode verarbeiteBefehl
gesagt werden, die ebenfalls aus der Methode spielen heraus aufgerufen wird:
Auch diese Anweisungen hätten direkt in der Methode spielen angegeben wer-
den können.
Ein Quelltextabschnitt ist jedoch sehr viel leichter zu verstehen und Änderungen
sind leichter vorzunehmen, wenn kurze Methoden mit hoher Kohäsion verwendet
wurden. In der gewählten Struktur der Methoden sind die einzelnen Methoden
relativ kurz und auch gut zu verstehen, ihre Namen geben ihre Funktion deutlich
wieder. Diese Eigenschaften sind für Wartungsprogrammierer eine große Hilfe.

316
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.11 Kohäsion

Listing 8.8
Zwei Methoden mit
einem hohen Grad
an Kohäsion.

8.11.2 Kohäsion von Klassen


Die Regel für die Kohäsion von Klassen besagt, dass jede Klasse genau eine wohl- Konzept
definierte Einheit aus dem Anwendungsbereich modellieren sollte.
Kohäsion von
Als ein Beispiel für die Kohäsion von Klassen werden wir nun eine weitere Erwei- Klassen: Eine
terung am Zuul-Projekt diskutieren. Wir wollen Gegenstände einführen. Jeder Klasse mit hoher
Kohäsion repräsen-
Raum kann einen Gegenstand enthalten und jeder Gegenstand hat eine Beschrei-
tiert genau eine
bung und ein Gewicht. Das Gewicht eines Gegenstands kann später darüber ent- wohldefinierte
scheiden, ob er aufgehoben werden kann oder nicht. Einheit.
Ein naiver Ansatz wäre, zwei neue Datenfelder in der Klasse Raum einzuführen:
gegenstandBeschreibung und gegenstandGewicht. Wir könnten dann die Details der
Gegenstände in den Räumen festlegen und sie ausgeben lassen, sobald ein Raum
betreten wird.

Dieser Ansatz würde aber nicht zu einem hohen Grad an Kohäsion führen: Die
Klasse Raum würde dann sowohl einen Raum als auch einen Gegenstand beschrei-
ben. Das würde auch bedeuten, dass ein Gegenstand an einen bestimmten
Raum gebunden ist, und das soll vielleicht nicht der Fall sein.

Ein besserer Entwurf würde eine neue Klasse für Gegenstände einführen, die
man Gegenstand nennen könnte. Diese Klasse würde Datenfelder für die Beschrei-
bung und das Gewicht definieren und ein Raum würde einfach eine Referenz auf
einen solchen Gegenstand enthalten.

317
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Übung 8.20 Erweitern Sie entweder Ihr eigenes Projekt oder das vorgege-
bene Projekt Zuul-besser so, dass jeder Raum einen Gegenstand enthält.
Gegenstände haben eine Beschreibung und ein Gewicht. Bei der Erzeugung
der Räume und dem Setzen der Ausgänge sollten auch Gegenstände für
das Spiel erzeugt werden. Wenn ein Spieler einen Raum betritt, sollten auch
die Informationen über den Gegenstand in diesem Raum angezeigt werden.
Übung 8.21 Wie sollten die textbasierten Informationen über einen Gegen-
stand in einem Raum erzeugt werden? Welche Klasse sollte die Zeichen-
kette erzeugen, die den Gegenstand beschreibt? Welche Klasse sollte diese
ausgeben? Warum? Erklären Sie das schriftlich. Wenn Sie beim Beantwor-
ten dieser Fragen das Gefühl bekommen, dass Sie Ihre Implementierung
anpassen sollten, dann tun Sie das ruhig.

Der eigentliche Vorteil der Trennung von Raum und Gegenstand in unserem Entwurf
wird erst deutlich, wenn wir die Anforderungen an die Erweiterung etwas abändern.
In einer weiteren Version des Spiels wollen wir nicht nur einen einzelnen Gegenstand
in einem Raum zulassen, sondern eine beliebige Anzahl. In einem Entwurf mit einer
eigenen Klasse für Gegenstände ist dies einfach: Wir erzeugen mehrere Gegenstand-
Objekte und legen sie in einer Sammlung von Gegenständen im Raum ab.
Mit dem ersten, naiven Ansatz wäre es fast unmöglich, diese Änderung durchzu-
führen.

Übung 8.22 Modifizieren Sie das Projekt so, dass ein Raum beliebig viele
Gegenstände enthalten kann. Benutzen Sie dafür eine Sammlung. Stellen Sie
sicher, dass ein Raum eine Methode gegenstandAblegen hat, mit der ein Gegen-
stand in einem Raum abgelegt werden kann. Sorgen Sie auch dafür, dass alle
Gegenstände angezeigt werden, wenn ein Spieler einen Raum betritt.

8.11.3 Kohäsion für bessere Lesbarkeit


Hohe Kohäsion verbessert einen Entwurf in vielerlei Hinsicht. Die beiden wich-
tigsten Aspekte sind Lesbarkeit und Wiederverwendung.
Die Methode willkommenstextAusgeben, die in Abschnitt 8.11.1 erwähnt wurde, ist
ein deutliches Beispiel dafür, dass die Erhöhung der Kohäsion den Quelltext les-
barer und damit einfacher zu verstehen und zu warten macht.
Das Beispiel für Kohäsion von Klassen in Abschnitt 8.11.2 enthält auch einen Lesbar-
keitsaspekt. Wenn eine eigene Klasse Gegenstand gegeben ist, dann kann ein War-
tungsprogrammierer leicht die Stelle finden, an der die Eigenschaften von Gegen-
ständen geändert werden können. Die Kohäsion von Klassen erhöht ebenfalls die
Lesbarkeit eines Programms.

318
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.11 Kohäsion

8.11.4 Kohäsion für Wiederverwendbarkeit


Der zweite große Vorteil von Kohäsion ist ein hohes Potenzial für Wiederverwen-
dung.
Das Beispiel für Kohäsion von Klassen in Abschnitt 8.11.2 dient hier als Beispiel:
Indem wir eine eigene Klasse Gegenstand definieren, können wir mehrere Gegen-
stände erzeugen und somit den gleichen Quelltext für mehr als einen Gegenstand
verwenden.
Wiederverwendung ist auch ein wichtiger Aspekt bei der Kohäsion von Metho-
den. Stellen Sie sich eine Methode in der Klasse Raum mit folgendem Kopf vor:
public Raum verlasseRaum(String richtung)
Diese Methode könnte den Raum in der angegebenen Richtung liefern (sodass er
als neuer aktueller Raum verwendet werden kann) und außerdem eine Beschrei-
bung des Raums ausgeben, den wir gerade betreten.
Das ist ein möglicher Entwurf und er kann auch funktionieren. In unserer Version
haben wir diese Aufgabe auf zwei Methoden aufgeteilt:
public Raum gibAusgang(String richtung)
public String gibLangeBeschreibung()
Die erste Methode ist zuständig für die Lieferung des nächsten Raums, während
die zweite eine Beschreibung des Raums liefert.
Der Vorteil an diesem Entwurf ist, dass die getrennten Vorgänge besser wiederver-
wendet werden können. Die Methode gibLangeBeschreibung beispielsweise wird
nicht nur in der Methode wechsleRaum benutzt, sondern auch in der Methode will-
kommenstextAusgeben und in der Implementierung für den Befehl look. Das ist nur
deshalb möglich, weil sie einen hohen Grad an Kohäsion aufweist. Ihre Wiederver-
wendung wäre nicht möglich in der Version mit der Methode verlasseRaum.

Übung 8.23 Implementieren Sie den Befehl back. Dieser Befehl hat kein
zweites Wort. Die Eingabe des Befehls back soll den Spieler in den Raum
zurückbringen, in dem er zuletzt gewesen ist.
Übung 8.24 Testen Sie Ihren neuen Befehl. Verhält er sich wie erwartet? Tes-
ten Sie auch für den Fall, dass der Befehl nicht korrekt verwendet wird. Was
macht zum Beispiel Ihr Programm, wenn ein Spieler nach back noch ein zwei-
tes Wort angibt? Verhält es sich vernünftig?
Übung 8.25 Was macht Ihr Programm, wenn back zweimal eingegeben
wird? Ist dieses Verhalten sinnvoll?
Übung 8.26 Zusatzaufgabe. Implementieren Sie den Befehl back so, dass
eine wiederholte Anwendung den Spieler mehrere Räume zurückversetzt,
bei genügend häufiger Anwendung sogar an den Spielanfang. Benutzen Sie
dazu einen Stack. (Sie müssen vermutlich herausfinden, was ein Stack ist.
Sehen Sie in der Dokumentation der Java-Bibliothek nach.)

319
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

8.12 Refactoring
Konzept Beim Entwurf von Anwendungen sollten wir versuchen, vorausschauend zu den-
ken und mögliche zukünftige Änderungen vorherzusehen; wir sollten lose gekop-
Refactoring ist
pelte Klassen mit hoher Kohäsion entwerfen sowie Methoden, die Änderungen
eine Aktivität, bei
der ein bestehender leicht machen. Das ist ein hehres Ziel, aber wir können nicht alle zukünftig mögli-
Entwurf restruktu- chen Änderungen vorhersehen, und es ist nicht möglich, sich auf alle denkbaren
riert wird, um die Erweiterungen einzustellen.
Qualität des Klassen-
entwurfs in Hinsicht Deshalb ist Refactoring wichtig.
auf vorzuneh-
Refactoring wird die Aktivität genannt, bei der bestehende Klassen und Metho-
mende Änderungen
und Erweiterungen den restrukturiert werden, um sie geänderten Umständen und Anforderungen
zu erhalten. anzupassen. Während der Lebensdauer einer Anwendung wird Funktionalität oft
erst nach und nach hinzugefügt. Sehr häufig wachsen dadurch Methoden und
Klassen als Nebeneffekt in ihrer Länge.
Die Versuchung für einen Wartungsprogrammierer ist groß, neue Anweisungen in
bestehende Klassen und Methoden einzufügen. Nach einiger Zeit leidet aber der
Grad der Kohäsion darunter. Wenn immer mehr Anweisungen zu einer Methode
oder einer Klasse hinzugefügt werden, dann ist irgendwann die Wahrscheinlichkeit
sehr hoch, dass sie mehr als eine klar definierte Aufgabe ausfüllt bzw. nicht mehr
eine klare Einheit darstellt.

Refactoring ist das Überdenken und Restrukturieren von Klassen- und Metho-
denstrukturen. Sehr häufig wird dabei eine Klasse in zwei Klassen aufgespalten
oder eine Methode in zwei oder mehr Methoden aufgeteilt. Refactoring schließt
auch das Zusammenfassen mehrerer Klassen oder Methoden zu einer einzelnen
ein, aber dieser Fall ist seltener.

8.12.1 Refactoring und Testen


Bevor wir ein Beispiel für Refactoring betrachten, sollten wir kurz über die Tat-
sache nachdenken, dass wir ein Programm meist dann restrukturieren, wenn wir
eine größere Änderung an etwas vornehmen wollen, das bereits läuft. Wenn
etwas geändert wird, dann besteht die Möglichkeit neuer Fehler. Deshalb sollten
wir sehr vorsichtig vorgehen und vor der Restrukturierung einen Satz Tests für die
laufende Anwendung vorliegen haben. Wenn noch keine Tests existieren, dann
sollten wir erst entscheiden, wie wir auf vernünftige Art und Weise die Funktio-
nalität des Programms testen können, und diese Tests aufzeichnen (zum Beispiel
indem wir sie schriftlich festhalten), sodass wir die gleichen Tests später wieder-
holen können. Wir werden auf das Testen im nächsten Kapitel noch näher einge-
hen. Wenn Sie bereits mit automatisiertem Testen vertraut sind, sollten Sie auto-
matisierte Tests verwenden. Andernfalls reicht manuelles (systematisches) Testen
durchaus im Moment aus.

Sobald Sie sich auf einen Satz Tests geeinigt haben, sollten Sie mit der Umstruk-
turierung beginnen. Idealerweise folgt das Refactoring dann in zwei Schritten:

320
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.12 Refactoring

 Im ersten Schritt werden Umstrukturierungen vorgenommen, um die interne


Struktur des Quelltextes zu verbessern, aber ohne Änderungen an der Funk-
tionalität der Anwendung vorzunehmen. Anders gesagt: Das Programm sollte
sich bei der Ausführung genauso verhalten wie zuvor. Nachdem dieser Schritt
abgeschlossen ist, sollten die zuvor erstellten Tests wiederholt werden, um
sicherzustellen, dass keine unbeabsichtigten neuen Fehler eingeführt wurden.
 Der zweite Schritt wird erst dann ausgeführt, wenn wir die Basisfunktionalität in
der restrukturierten Version wiederhergestellt haben. Wir können dann beruhigt
Erweiterungen am Programm vornehmen. Nachdem dies geschehen ist, sollte
natürlich auch die neue Version gründlich getestet werden.
Mehrere gleichzeitige Änderungen (restrukturieren und neue Funktionalität ein-
führen) machen es schwieriger, eventuell auftauchende Fehler zu lokalisieren.

Übung 8.27 Welche grundlegenden Tests für die Basisfunktionalität der


aktuellen Version unseres Spiels sollten wir erstellen?

8.12.2 Ein Beispiel für Refactoring


Als ein Beispiel sollten wir mit der Erweiterung unseres Spiels um Gegenstände fort-
fahren. In Abschnitt 8.11.2 haben wir Gegenstände eingeführt und eine Struktur
vorgeschlagen, in der jeder Raum beliebig viele Gegenstände enthalten kann. Ein
logischer nächster Schritt wäre nun, wenn ein Spieler diese Gegenstände aufheben
und mit sich herumtragen könnte. Dies könnte informell etwa so spezifiziert werden:
 Ein Spieler kann Gegenstände im aktuellen Raum aufheben.
 Ein Spieler kann beliebig viele Gegenstände bei sich tragen, jedoch nur bis zu
einem Maximalgewicht.
 Einige Gegenstände können nicht aufgehoben werden.
 Der Spieler kann Gegenstände im aktuellen Raum ablegen.
Um dies zu erreichen, können wir Folgendes tun:
 Falls noch nicht geschehen, führen wir eine Klasse Gegenstand in unser Projekt
ein. Ein Gegenstand hat, wie bereits vorher beschrieben, eine Beschreibung
(als Zeichenkette) und ein Gewicht (eine ganze Zahl).
 Wir sollten für einen Gegenstand auch einen Namen einführen. Dies ermög-
licht dem Spieler, über einen kürzeren Namen als über die Beschreibung auf
den Gegenstand Bezug zu nehmen. Wenn beispielsweise im aktuellen Raum
ein Buch liegt, dann könnten die Datenfelder des Gegenstands so aussehen:
name: Buch
beschreibung: ein altes, staubiges Buch mit Ledereinband
gewicht: 1200
Wenn wir einen Raum betreten, können wir die Beschreibung eines Gegenstands
ausgeben, damit der Spieler informiert ist. Für Befehle ist ein Name jedoch einfa-
cher zu benutzen. Beispielsweise kann ein Spieler dann take Buch eintippen, um
das Buch aufzuheben.

321
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

 Wir können sicherstellen, dass ein Spieler einen Gegenstand nicht aufheben
kann, indem wir ihn sehr schwer machen (schwerer, als ein Spieler tragen kann).
Oder sollten wir ein weiteres boolesches Datenfeld aufhebbar definieren? Wel-
ches ist Ihrer Meinung nach der bessere Entwurf? Ist das wichtig? Versuchen Sie
dies zu beantworten, indem Sie überlegen, welche Änderungen zukünftig noch
an dem Spiel vorgenommen werden könnten.
 Wir fügen die Befehle take und drop ein, um Gegenstände aufheben bzw. wie-
der ablegen zu können. Beide Befehle haben als zweites Wort den Namen eines
Gegenstands.
 An irgendeiner Stelle müssen wir ein Datenfeld (für eine Sammlung) einführen,
um die aktuell vom Spieler getragenen Gegenstände zu halten. Wir müssen auch
ein Datenfeld einführen, das die maximale Tragkraft eines Spielers festlegt, damit
wir es bei jedem Versuch, etwas aufzuheben, überprüfen können. Wo sollten
diese Datenfelder definiert werden? Denken Sie auch hier wieder an mögliche
zukünftige Erweiterungen, um Ihre Entscheidung zu erleichtern.
Die letztgenannte Aufgabe wollen wir nun etwas ausführlicher besprechen, um
die Idee des Refactoring zu illustrieren.
Wenn ein Spieler Gegenstände bei sich tragen können soll, dann lautet die erste
Entwurfsfrage: An welcher Stelle sollten wir die Datenfelder einfügen, in denen
das aktuelle Gepäck und die maximale Tragkraft gehalten wird? Ein schneller
Blick auf die bestehenden Klassen zeigt uns, dass Spiel eigentlich die einzige
Klasse ist, in die diese Daten passen. Diese Informationen können nicht in Raum,
Gegenstand oder Befehl gespeichert werden, da es von diesen Klassen im Laufe
eines Spiels viele Instanzen geben wird, die nicht immer zugreifbar sind. In Parser
oder Befehlswoerter passen sie auch nicht.
Ein weiteres Argument für die Platzierung in Spiel ist, dass dort bereits der aktu-
elle Raum (in dem sich der Spieler gerade befindet) gehalten wird, sodass auch
die aktuellen Gegenstände (die der Spieler gerade bei sich trägt) dort hinzupas-
sen scheinen.
Dieser Ansatz ist denkbar. Er führt allerdings nicht zu einem guten Entwurf. Die
Klasse Spiel ist schon recht umfangreich, es gibt einige Hinweise, dass sie bereits
zu groß ist. Zusätzliche Erweiterungen würden das nicht verbessern.
Wir sollten uns noch einmal fragen, zu welcher Klasse oder welchem Objekt die
genannten Informationen gehören. Wenn wir genauer darüber nachdenken, was
hier verwaltet werden soll (aufgenommene Gegenstände, Tragkraft), dann erken-
nen wir, dass es Informationen über einen Spieler sind! Die logische Konsequenz
ist deshalb (dem Entwurf nach Zuständigkeiten folgend), eine neue Klasse Spieler
einzuführen. Wir können die genannten Daten dann in der Klasse Spieler definie-
ren und zum Spielstart ein Objekt dieser Klasse erzeugen, das die Daten verwaltet.
Das bestehende Datenfeld aktuellerRaum hält ebenfalls eine Information über den
Spieler: seine aktuelle Position. Konsequenterweise sollte dieses Datenfeld auch
in die Klasse Spieler wandern.
Wenn wir diesen Entwurf betrachten, dann können wir sagen, dass er besser dem
Prinzip von klaren Zuständigkeiten entspricht. Wer sollte verantwortlich sein für
Informationen über Spieler? Natürlich die Klasse Spieler.

322
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.12 Refactoring

In der ursprünglichen Version hatten wir nur eine Information über einen Spieler zu
speichern – den aktuellen Raum. Man kann diskutieren, ob dies nicht bereits eine
Klasse Spieler erfordert hätte. Es gibt Argumente dafür und dagegen. Es wäre ein
besserer Entwurf gewesen, also hätten wir es bereits tun können. Aber eine Klasse
mit nur einem Datenfeld und keinen echten eigenen Methoden könnte man auch
als zu aufwendig bezeichnen.
Manchmal gibt es solche Grauzonen, in denen für oder gegen eine Entwurfsent-
scheidung argumentiert werden kann. Nachdem wir nun aber die neuen Daten-
felder hinzufügen wollen, ist klar, dass wir gute Argumente für eine Klasse Spie-
ler haben. Diese kann die genannten Daten speichern und Methoden anbieten
wie gegenstandAblegen und gegenstandAufnehmen (die eine Prüfung des Gewichts
einschließen und false liefern könnte, wenn der Gegenstand zu schwer ist).
Wenn wir die Klasse Spieler einführen und das Datenfeld aktuellerRaum aus Spiel
in Spieler verlagern, nehmen wir ein Refactoring vor. Wir haben die Daten so re-
strukturiert, dass der Gesamtentwurf den geänderten Anforderungen besser ge-
recht wird.
Weniger gut ausgebildete (oder einfach faule) Programmierer hätten das Datenfeld
aktuellerRaum möglicherweise am alten Platz gelassen, da das Programm auch
dann offensichtlich noch läuft und kein zwingender Anlass für diese Änderung
besteht. Dieser Programmierer würde sich mit einem schlechten Entwurf abfinden.
Den Effekt dieser Änderung merken wir erst, wenn wir einen Schritt weiter den-
ken. Nehmen Sie an, dass wir das Spiel nun so erweitern wollen, dass es mehrere
Spieler geben kann. Mit unserem neuen Entwurf ist das plötzlich sehr einfach. Wir
haben bereits eine Klasse Spieler (die Klasse Spiel hält eine Instanz dieser Klasse)
und können sehr leicht mehrere Spieler-Objekte erzeugen, die wir dann in einer
Sammlung in der Klasse Spiel halten können. Jedes Spieler-Objekt hätte seinen
eigenen aktuellen Raum, eigene Gegenstände und eine eigene Tragkraft. Verschie-
dene Spieler könnten eine unterschiedliche Tragkraft haben, wodurch die Tür auf-
gestoßen wird für weitere Änderungen, bei denen unterschiedliche Spieler unter-
schiedliche Fähigkeiten haben – mit der Tragkraft als einer von vielen.
Der faule Programmierer hingegen, der das Datenfeld aktuellerRaum in der Klasse
Spiel gelassen hat, hat nun ein ernsthaftes Problem: Da das gesamte Spiel nur
einen aktuellen Raum hat, können die aktuellen Positionen der verschiedenen
Spieler nicht leicht gespeichert werden. Ein schlechter Entwurf fällt oft später
wieder auf uns zurück und kann dann eine Menge Mehrarbeit bedeuten.
Gutes Refactoring ist ebenso eine Frage der Einstellung wie der technischen Fähig-
keiten. Während wir eine Anwendung ändern oder erweitern, sollten wir uns stän-
dig fragen, ob der ursprüngliche Entwurf noch immer die beste Entscheidung dar-
stellt. Wenn sich die Funktionalität ändert, ändern sich auch die Argumente für oder
gegen eine Entwurfsentscheidung. Ein guter Entwurf für eine einfache Anwendung
kann zu einem schlechten werden, wenn die Anwendung erweitert wird.
Das Erkennen notwendiger Änderungen und das tatsächliche Durchführen des
Refactoring am Quelltext kann uns letztlich eine Menge Zeit und Aufwand erspa-
ren. Je eher wir einen Entwurf aufräumen, desto mehr Aufwand ersparen wir uns
üblicherweise.

323
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Wir sollten ständig bereit sein, Methoden zu extrahieren (eine Reihe von Anweisun-
gen aus dem Rumpf einer Methode in eine neue, unabhängige Methode auslagern)
und Klassen zu extrahieren (Teile einer Klasse ausschneiden und in eine eigene Klasse
auslagern). Solche Refactorings halten unsere Entwürfe sauber und sparen letztlich
Arbeit. Eines ist dabei klar: Refactoring kann uns auf lange Sicht das Leben schwer
machen, wenn wir es nicht schaffen, die überarbeitete Version im Vergleich zur Ori-
ginalversion zu testen. Wir sollten immer, wenn wir uns auf ein größeres Refactoring
einlassen, vorher sicherstellen, dass wir vor und nach der Änderung gut getestet
haben. Manuelle Tests (durch interaktives Erzeugen und Testen von Objekten) erwei-
sen sich schon sehr schnell als mühsam. Wir werden im nächsten Kapitel näher dar-
auf eingehen, wie wir unsere Tests (durch Automatisieren) verbessern können.

Übung 8.28 Restrukturieren Sie Ihr Projekt für eine neue Klasse Spieler. Ein
Spieler-Objekt sollte mindestens den aktuellen Raum des Spielers halten,
aber Sie können auch weitere Informationen wie etwa den Namen des Spie-
lers vorsehen.
Übung 8.29 Implementieren Sie eine Erweiterung, bei der ein Spieler genau
einen Gegenstand aufnehmen kann. Dazu müssen Sie zwei weitere Befehle
implementieren: take und drop.
Übung 8.30 Erweitern Sie Ihre Implementierung so, dass ein Spieler belie-
big viele Gegenstände aufnehmen kann.
Übung 8.31 Führen Sie die Beschränkung ein, dass ein Spieler nur Gegen-
stände bis zu einem maximalen Gewicht an sich nehmen kann. Die Tragkraft
eines Spielers sollte eines seiner Attribute sein.
Übung 8.32 Implementieren Sie einen Befehl status, der alle aufgenomme-
nen Gegenstände auflistet und ihr Gesamtgewicht anzeigt.
Übung 8.33 Legen Sie in einem Raum einen magischen Muffin ab. Fügen Sie
den Befehl eat muffin hinzu. Wenn ein Spieler den Muffin findet und isst,
dann erhöht dies seine maximale Tragkraft. (Sie können dies so anpassen,
dass es gut in Ihren Spielentwurf hineinpasst.)

8.13 Refactoring für Sprachunabhängigkeit


Ein Detail des Zuul-Spiels, zu dem wir bisher noch nichts gesagt haben, ist die Ver-
mischung von Deutsch und Englisch in der Benutzungsschnittstelle. Die Kom-
mandos werden in Englisch angegeben, die Beschreibungen der Räume sind aber
in Deutsch. Möglicherweise hat es sich der Übersetzer des Originalspiels nur leicht
machen wollen; oder die Kommandostruktur wurde beibehalten, weil der Befehl
„go east“ einfach griffiger und korrekter ist als „gehe ost“ (statt „gehe nach
Osten“ oder „gehe ostwärts“). Auf alle Fälle sind die englischen Befehle eingebet-
tet sowohl in die Klasse Befehlswoerter (hier werden die erlaubten Befehle gehal-
ten) als auch in der Klasse Spiel, in der explizit jeder eingegebene Befehl mit einer
Menge von englischen Wörtern abgeglichen wird. Wenn wir die Schnittstelle kom-
plett auf deutsche Kommandos (oder die einer anderen Sprache) umstellen woll-

324
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.13 Refactoring für Sprachunabhängigkeit

ten, dann müssten wir alle Stellen im Quelltext finden und ändern, an denen
Befehlswörter verwendet werden. Dies ist ein weiteres Beispiel für implizite Kopp-
lung, die wir in Kapitel 8.9 diskutiert haben.
Wenn das Programm sprachunabhängig sein soll, dann sollte es idealerweise nur
genau eine Stelle im Quelltext geben, an der die Befehlswörter gespeichert sind,
damit an allen anderen Stellen auf sprachunabhängige Befehle Bezug genom-
men werden kann. Ein Programmiersprachenkonstrukt, das dies ermöglicht, sind
Aufzählungstypen (enumerated types oder kurz enums). Wir werden es uns mit-
hilfe der Projekte Zuul-mit-Enums ansehen.

8.13.1 Aufzählungstypen
Listing 8.9 zeigt die Java-Definition eines Aufzählungstyps Befehlswort.

Listing 8.9
Ein Aufzählungstyp
für Befehlswörter.

In seiner simpelsten Form besteht ein Aufzählungstyp aus einer äußeren Klammer,
in der das Schlüsselwort enum anstelle von class verwendet wird, und einem Rumpf,
in dem einfach die Namen von Elementen aufgezählt werden, die die Werte des
Typs darstellen. Nach Konvention werden diese Namen in Großbuchstaben angege-
ben. Wir erzeugen niemals Objekte eines Aufzählungstyps. Jeder Name innerhalb
der Typdefinition repräsentiert eine eindeutige Instanz des Typs, die bereits für uns
erzeugt wurde. Wir beziehen uns auf diese Instanzen mit [Link], Befehls-
[Link] usw. Auch wenn die Syntax ihrer Verwendung sehr ähnlich ist, sollten wir
diese Werte nicht mit den numerischen Klassenkonstanten aus Abschnitt 6.14 ver-
wechseln. Trotz der einfachen Definition sind die Werte eines Aufzählungstyps
echte Objekte und nicht das gleiche wie int-Werte.
Wie können wir diesen Typ Befehlswort nun verwenden, um die Spiellogik inner-
halb von Zuul von einer spezifischen Sprache etwas mehr zu entkoppeln? Eine
der ersten möglichen Verbesserungen können wir an den folgenden Tests vor-
nehmen, die in der Methode verarbeiteBefehl der Klasse Spiel stehen:
if([Link]()) {
[Link]("Ich weiss nicht, was Sie meinen ...");
return false;
}
String befehlswort = [Link]();
if ([Link]("help")) {
hilfstextAusgeben();
}
else if ([Link]("go")) {
wechsleRaum(befehl);
}

325
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

else if ([Link]("quit")) {
moechteBeenden = beenden(befehl);
}
Wenn befehlswort vom Typ Befehlswort und nicht als String deklariert wird, dann
können die Tests folgendermaßen umformuliert werden:
if(befehlswort == [Link]) {
[Link]("Ich weiss nicht, was Sie meinen ...");
return false;
}
else if(befehlswort == [Link]) {
hilfstextAusgeben();
}
else if(befehlswort == [Link]) {
wechsleRaum(befehl);
}
else if(befehlswort == [Link]) {
moechteBeenden = beenden(befehl);
}
Nachdem wir jetzt den Typ in Befehlswort geändert haben, könnten wir auch eine
switch-Anweisung anstelle einer Reihe von if-Anweisungen verwenden. Dadurch
wird die Absicht des Quelltextes ein bisschen deutlicher.2
switch(befehlswort) {
case UNKNOWN:
[Link]("Ich weiss nicht, was Sie meinen ...");
break;

case HELP:
hilfstextAusgeben();
break;

case GO:
wechsleRaum(befehl);
break;

case QUIT:
moechteBeenden = beenden(befehl);
break;
}

Konzept Die switch-Anweisung übernimmt nach dem Schlüsselwort switch in Klammern die
Variable (in unserem Fall befehlswort) und vergleicht sie mit jedem der Werte, die
Eine switch-
nach den case-Schlüsselwörtern aufgeführt werden. Wenn eine Übereinstimmung
Anweisung
wählt aus verschie- festgestellt wird, wird der nachfolgende Quelltext ausgeführt. Die break-Anwei-
denen möglichen sung bricht die switch-Anweisung an der aktuellen Stelle ab und die Ausführung
Anweisungsfolgen wird nach der switch-Anweisung fortgesetzt. Eine genauere Beschreibung der
eine Folge zur switch-Anweisung finden Sie in Anhang D.
Ausführung aus.

2 Tatsächlich können String-Literale ebenfalls als case-Werte in switch-Anweisungen verwendet


werden, um eine lange Folge von if-else-if-Vergleichen zu vermeiden.

326
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.13 Refactoring für Sprachunabhängigkeit

Nun müssen wir uns nur noch darum kümmern, dass die vom Benutzer eingetipp-
ten Befehle auf die entsprechenden Werte von Befehlswort abgebildet werden.
Öffnen Sie das Projekt Zuul-mit-Enums-V1 und sehen Sie sich an, wie wir dies
umgesetzt haben. Die wichtigste Änderung ist in der Klasse Befehlswoerter zu fin-
den. Anstatt ein Array von Strings für die Definition der gültigen Befehle zu ver-
wenden, nehmen wir nun eine Abbildung von Strings auf Befehlswort-Objekte:
public Befehlswoerter()
{
gueltigeBefehle = new HashMap<>();
[Link]("go", [Link]);
[Link]("help", [Link]);
[Link]("quit", [Link]);
}
Der vom Benutzer eingetippte Befehl kann nun leicht in den entsprechenden Wert
des Aufzählungstyps umgewandelt werden.

Übung 8.34 Untersuchen Sie im Quelltext des Projekts Zuul-mit-Enums-V1,


wie der Typ Befehlswort verwendet wird. Die Klassen Befehl, Befehlswoerter,
Spiel und Parser wurden alle aus der Version in Zuul-besser angepasst, um
diese Änderungen nachzuvollziehen.
Übung 8.35 Fügen Sie dem Spiel einen Befehl look hinzu, so wie in Abschnitt
8.9 beschrieben.
Übung 8.36 Übersetzen Sie im Spiel die verschiedenen Bezeichnungen für
die Befehlswörter GO und QUIT von go und quit in deutsche Bezeichnungen.
Müssen Sie ausschließlich die Klasse Befehlswoerter ändern, um dies zu tun?
Übung 8.37 Übersetzen Sie nun auch den Hilfe-Befehl von help ins Deut-
sche und überprüfen Sie, ob alles noch klappt. Was bemerken Sie beim Will-
kommenstext, der beim Starten des Spiels ausgegeben wird?
Übung 8.38 Definieren Sie in einem neuen Projekt Ihren eigenen Aufzäh-
lungstyp Richtung mit den Werten NORD, SUED, OST und WEST.

8.13.2 Weitere Entkopplung der Befehlsschnittstelle


Der Aufzählungstyp Befehlswort hat es uns ermöglicht, die Sprache an der Benut-
zungsschnittstelle deutlich von der Spiellogik zu entkoppeln. Es ist fast vollständig
möglich, die Befehlswörter in eine andere Sprache zu übersetzen, indem wir aus-
schließlich die Klasse Befehlswoerter editieren. (An irgendeinem Punkt sollten wir
auch überlegen, die Raumbeschreibungen und andere Texte aus einer Datei einzule-
sen, aber das lassen wir für später.) Einen weiteren Entkopplungsschritt können wir
aber noch machen. Momentan müssen wir mit jedem neuen Befehlswort einen
neuen Wert im Typ Befehlswort definieren und eine Abbildung vom Benutzerbefehl
auf diesen Wert in der Klasse Befehlswoerter definieren. Es wäre hilfreich, wenn wir
den Typ Befehlswort selbstständiger machen würden; dazu müssten wir die Abbil-
dung von Text auf Wert aus der Klasse Befehlswoerter nach Befehlswort bewegen.

327
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Java erlaubt bei der Definition von Aufzählungstypen sehr viel mehr als nur eine
Aufzählung der Werte eines Typs. Wir werden diese Möglichkeiten nicht im Detail
untersuchen, wollen Ihnen aber zumindest einen Eindruck vermitteln. Listing 8.10
zeigt eine erweiterte Version des Typs Befehlswort, die einer normalen Klassendefi-
nition sehr ähnlich sieht. Sie ist im Projekt Zuul-mit-Enums-V2 zu finden.

Listing 8.10
Verknüpfen von
Benutzerbefehlen
mit Werten eines
Aufzählungstyps.

Die wichtigsten Punkte an dieser neuen Version von Befehlswort sind:


 Jeder Wert des Typs hat einen Parameterwert – in diesem Fall den Text des
Benutzerbefehls, der mit dem Wert verknüpft sein soll.
 Im Unterschied zur Version in Listing 8.9 ist ein Semikolon am Ende der Liste
mit den Werten des Typs nötig.
 Die Typdefinition enthält einen Konstruktor. Dieser fasst in seinem Kopf nicht
das Schlüsselwort public. Die Konstruktoren von Aufzählungstypen sind niemals
öffentlich, weil man von ihnen keine Instanzen erzeugt. Der Parameter, der bei
jedem Wert angegeben wurde, wird an diesen Konstruktor übergeben.
 Die Typdefinition enthält ein Datenfeld, befehlswort. Im Konstruktor wird der
Befehlsstring in diesem Datenfeld gespeichert.
 Die Methode toString liefert den Text, der auf diese Weise mit einem Wert des
Typs verknüpft wurde.
Da der Text der Befehle nun im Typ Befehlswort definiert ist, wird in der Klasse
Befehlswoerter in Zuul-mit-Enums-V2 ein anderer Weg zur Abbildung von Text
auf Aufzählungswerte genommen:

328
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.14 Entwurfsregeln

gueltigeBefehle = new HashMap<String, Befehlswort>();


for(Befehlswort befehl : [Link]()) {
if(befehl != [Link]) {
[Link]([Link](), befehl);
}
}
Jeder Aufzählungstyp definiert eine Methode values, die ein Array der Wert-
Objekte des Typs zurückliefert. Der Quelltext oben iteriert über dieses Array und ruft
jeweils die Methode toString auf, um die verknüpfte Zeichenkette abzufragen.

Übung 8.39 Fügen Sie Ihren eigenen Befehl look in Zuul-mit-Enums-V2 ein.
Müssen Sie lediglich den Typ Befehlswort ändern?
Übung 8.40 Ändern Sie das Wort, das mit dem Befehl HELP verknüpft ist. Ist
diese Änderung automatisch im Willkommenstext sichtbar, wenn Sie das
Spiel starten? Untersuchen Sie in der Methode willkommenstextAusgeben der
Klasse Spiel, wie dies erreicht wurde.

8.14 Entwurfsregeln
Programmieranfängern werden häufig Tipps gegeben wie „Lassen Sie eine Methode
nicht zu viel tun“ oder „Packen Sie nicht alles in eine Klasse“. Beide Vorschläge
haben ihre Berechtigung, führen aber leicht zu Fragen wie „Wie lang soll eine
Methode denn sein?“ oder „Wie lang soll eine Klasse sein?“.

Nach der Diskussion in diesem Kapitel können diese Fragen nun mit Blick auf
Kopplung und Kohäsion beantwortet werden. Eine Methode ist zu lang, wenn
sie mehr als eine Aufgabe erfüllt. Eine Klasse ist zu komplex, wenn sie mehr als
eine logische Einheit modelliert.

Sie merken, dass diese Antworten keine klar definierten Regeln liefern, was genau
zu tun ist. Ein Begriff wie eine einzelne logische Aufgabe ist immer noch stark
interpretierbar und verschiedene Programmierer entscheiden in vielen Situationen
unterschiedlich.
Diese Hinweise sind Richtlinien, keine in Stein gemeißelten Regeln. Mit diesen
Richtlinien im Hinterkopf werden Sie ihre Klassenentwürfe jedoch erheblich ver-
bessern können, sodass Sie komplexere Aufgaben bearbeiten und bessere und
spannendere Programme schreiben können.

329
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Die folgenden Übungen sollen als Anregungen verstanden werden, nicht als
fest definierte Aufgaben. Das Spiel kann auf viele Arten erweitert werden –
implementieren Sie auch eigene Ideen. Sie müssen nicht alle Übungen
durchführen, um ein spannendes Spiel zu erstellen; Sie können aber auch
mehr oder andere Ideen implementieren.
Übung 8.41 Führen Sie eine Zeitbegrenzung für Ihr Spiel ein. Wenn eine
bestimmte Aufgabe nicht rechtzeitig erledigt wird, verliert der Spieler. Eine
Begrenzung kann leicht eingebaut werden, indem die Raumwechsel oder die
eingegebenen Befehle gezählt werden. Sie müssen keine Echtzeit berücksich-
tigen.
Übung 8.42 Implementieren Sie irgendwo eine Falltür (oder eine andere Art
von Tür, die nur in einer Richtung durchschritten werden kann).
Übung 8.43 Fügen Sie dem Spiel einen Beamer hinzu. Ein Beamer ist ein
Gerät, das man laden und auslösen kann. Wenn Sie den Beamer laden, dann
registriert er den aktuellen Raum. Wenn Sie den Beamer auslösen, dann trans-
portiert er Sie augenblicklich in den Raum zurück, in dem er geladen wurde.
Der Beamer kann entweder zur Standardausrüstung gehören oder der Spieler
muss ihn erst finden. Offensichtlich benötigen Sie auch Befehle zum Laden
und Auslösen des Beamers.
Übung 8.44 Fügen Sie Ihrem Spiel geschlossene Türen hinzu. Der Spieler muss
einen Schlüssel finden (oder anderweitig erwerben), um eine Tür zu öffnen.
Übung 8.45 Fügen Sie einen Teleporter-Raum hinzu. Jedes Mal, wenn ein
Spieler diesen Raum betritt, wird er in einen zufällig ausgewählten anderen
Raum „gebeamt“. Hinweis: Ein guter Entwurf für diese Aufgabe ist nicht
leicht. Es kann interessant sein, mit anderen Studenten Entwurfsalternativen
zu diskutieren. (Wir werden einige Entwurfsalternativen für diese Aufgabe
am Ende von Kapitel 11 diskutieren. Unternehmungslustige oder fortge-
schrittene Leser können schon einmal einen Blick darauf werfen.)

330
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.14 Entwurfsregeln

Zusammenfassung
In diesem Kapitel haben wir die sogenannten nichtfunktionalen Aspekte
einer Anwendung diskutiert. Es ging hier nicht primär darum, eine Anwen-
dung eine Aufgabe ausführen zu lassen, sondern darum, dies mit gut ent-
worfenen Klassen zu tun.
Ein guter Klassenentwurf kann einen großen Unterschied ausmachen, wenn
eine Anwendung korrigiert, geändert oder erweitert werden muss. Er erlaubt
auch oft, dass Teile der Anwendung in anderen Zusammenhängen (beispiels-
weise in anderen Projekten) wiederverwendet werden und wir auch später
noch von ihm profitieren können.
Es gibt zwei Kernkonzepte, mit denen Klassenentwürfe bewertet werden
können: Kopplung und Kohäsion. Kopplung bezieht sich auf die Abhängig-
keiten zwischen Klassen und Kohäsion auf die Zerlegung in angemessene
Einheiten. Einen guten Entwurf zeichnen eine lose Kopplung und eine hohe
Kohäsion aus.
Eine gute Methode, um zu einer guten Struktur zu kommen, ist der Entwurf
nach Zuständigkeiten. Bei jeder Funktion, die wir einer Anwendung hinzu-
fügen wollen, versuchen wir festzulegen, welche Klasse für welchen Teil der
Aufgabe zuständig sein sollte.
Wenn wir ein Programm erweitern, führen wir regelmäßig ein Refactoring
durch, um den Entwurf an die geänderten Anforderungen anzupassen und
dafür zu sorgen, dass weiterhin alle Klassen und Methoden eine hohe Kohä-
sion aufweisen und möglichst lose gekoppelt sind.

NEUE BEGRIFFE IN DIESEM KAPITEL


Code-Duplizierung, Kopplung, Kohäsion, Kapselung, Entwurf nach
Zuständigkeiten, implizite Kopplung, Refactoring

331
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf

Zusammenfassung der Konzepte


 Kopplung Der Begriff Kopplung beschreibt den Grad der Abhängigkei-
ten zwischen Klassen. Wir streben für ein System eine möglichst lose
Kopplung an – also ein System, in dem jede Klasse weitgehend unabhän-
gig ist und mit anderen Klassen nur über möglichst schmale, wohldefi-
nierte Schnittstellen kommuniziert.
 Kohäsion Der Begriff Kohäsion beschreibt, wie gut eine Programmein-
heit eine logische Aufgabe oder Einheit abbildet. In einem System mit
hoher Kohäsion ist jede Programmeinheit (eine Methode, eine Klasse
oder ein Modul) verantwortlich für genau eine wohldefinierte Aufgabe
oder Einheit. Ein guter Klassenentwurf weist einen hohen Grad an
Kohäsion auf.
 Code-Duplizierung Code-Duplizierung (ein Quelltextabschnitt erscheint
mehr als einmal in einer Anwendung) ist ein Indiz für einen schlechten
Entwurf. Sie sollte vermieden werden.
 Kapselung Saubere Kapselung reduziert die Kopplung und führt des-
halb zu besseren Entwürfen.
 Entwurf nach Zuständigkeiten ist ein Entwurfsprozess, bei dem jeder
Klasse eine klare Verantwortung zugewiesen wird. Dieser Prozess kann
benutzt werden, um festzulegen, welche Klasse für welche Funktionen in
einer Anwendung zuständig sein soll.
 Änderungen lokal halten Eines der Hauptziele eines guten Klassenent-
wurfs lautet: Änderungen lokal halten; eine Änderung an einer Klasse
sollte einen möglichst geringen Einfluss auf andere Klassen haben.
 Kohäsion von Methoden Eine Methode mit hoher Kohäsion ist verant-
wortlich für genau eine wohldefinierte Aufgabe.
 Kohäsion von Klassen Eine Klasse mit hoher Kohäsion repräsentiert
genau eine wohldefinierte Einheit.
 Refactoring Refactoring ist eine Aktivität, bei der ein bestehender Ent-
wurf restrukturiert wird, um die Qualität des Klassenentwurfs in Hinsicht
auf vorzunehmende Änderungen und Erweiterungen zu erhalten.
 Eine switch-Anweisung wählt aus verschiedenen möglichen Anwei-
sungsfolgen eine Folge zur Ausführung aus.

332
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.14 Entwurfsregeln

Übung 8.46 Zusatzaufgabe. In der Methode verarbeiteBefehl in der Klasse


Spiel gibt es eine switch-Anweisung (oder eine Sequenz von if-Anweisungen),
mit denen der richtige Befehl für ein erkanntes Befehlswort ermittelt wird. Das
ist kein sehr guter Entwurf, da wir jedes Mal, wenn ein neuer Befehl hinzuge-
fügt wird, hier eine zusätzliche Fallunterscheidung implementieren müssen.
Können Sie diesen Entwurf verbessern? Entwerfen Sie Klassen, mit denen die
Behandlung von Befehlen modularer wird und durch die Befehle leichter hinzu-
gefügt werden können. Implementieren Sie diesen Entwurf. Testen Sie.
Übung 8.47 Fügen Sie dem Spiel Personen hinzu. Personen sind Gegen-
ständen sehr ähnlich, aber sie können reden. Sie sprechen einen Text, wenn
ein Spieler sie das erste Mal trifft, und sie können ihm Hinweise geben,
wenn dieser ihnen den richtigen Gegenstand gibt.
Übung 8.48 Führen Sie bewegliche Personen ein. Diese sind wie andere Per-
sonen auch, bewegen sich jedoch mit jedem eingegebenen Befehl in einen
angrenzenden Raum.
Übung 8.49 Fügen Sie Ihrem Projekt eine Klasse Hauptspiel hinzu. Definie-
ren Sie darin nur eine main-Methode, die ein Spiel-Objekt erzeugt und des-
sen spielen-Methode aufruft.

333
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

9 Fehler vermeiden

Lernziele
Zentrale Konzepte in diesem Kapitel: Testen, Modultests, Fehler beseiti-
gen, Tests automatisieren
Java-Konstrukte in diesem Kapitel: (In diesem Kapitel werden keine neuen
Java-Konstrukte vorgestellt.)

9.1 Einführung
Wenn Sie die vorherigen Kapitel gelesen und die vorgeschlagenen Übungen imple-
mentiert haben, dann haben Sie inzwischen etliche Klassen geschrieben. Mögli-
cherweise haben Sie dabei festgestellt, dass der erste Entwurf einer Klasse selten
perfekt ist. Normalerweise ist der Quelltext anfangs nicht vollständig, sodass meist
noch einiges getan werden muss, um die Implementierung abzuschließen.
Die Probleme, die Sie dabei haben, werden sich über die Zeit verschieben. Program-
mieranfänger kämpfen typischerweise mit Syntaxfehlern. Syntaxfehler sind die Feh-
ler, die in der Struktur des Quelltextes liegen. Sie sind leicht zu finden, weil der Com-
piler sie entdeckt und eine Fehlermeldung ausgibt.
Erfahrene Programmierer, die kompliziertere Aufgaben angehen, haben üblicher-
weise weniger Probleme mit der Syntax einer Sprache. Sie beschäftigen sich statt-
dessen eher mit logischen Fehlern.
Ein logischer Fehler liegt vor, wenn ein Programm ohne Probleme übersetzt und aus-
geführt werden kann, aber nicht das gewünschte Ergebnis liefert. Logische Prob-
leme sind sehr viel schwieriger zu finden als Syntaxfehler. Tatsächlich ist oft kaum zu
erkennen, dass ein Fehler vorliegt.
Syntaktisch korrekte Programme zu schreiben ist relativ leicht zu erlernen, denn
es gibt gute Werkzeuge (wie Compiler), die Syntaxfehler entdecken und aufzei-
gen. Logisch korrekte Programme zu schreiben ist hingegen sehr schwierig für
nahezu jedes nichttriviale Problem, und der Beweis für ihre Korrektheit kann für
den allgemeinen Fall nicht automatisiert werden. Es ist tatsächlich so schwierig,
dass der Großteil der kommerziell vertriebenen Software bekanntermaßen eine
signifikante Anzahl von Fehlern enthält.

335
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Somit ist es für einen kompetenten Softwaretechniker unverzichtbar, Probleme im


Zusammenhang mit Korrektheit zu verstehen und die Anzahl der Fehler in einer
Klasse reduzieren zu können.

Konzept In diesem Kapitel werden wir eine Reihe von Praktiken vorstellen, mit denen die
Korrektheit von Software verbessert werden kann. Dazu gehören Testen, Fehler-
Testen dient der
beseitigung und die Erstellung wartbarer Quelltexte.
Überprüfung, ob
ein Stück Software Mit Testen wird herausgefunden, ob ein Stück Software Fehler enthält. Gutes Tes-
(eine Methode, ten ist nicht einfach, denn es gibt sehr viel dabei zu bedenken.
eine Klasse oder
ein Programm) das Die Fehlerbeseitigung kommt nach dem Testen: Wenn Fehler durch Tests aufge-
gewünschte Verhal- zeigt wurden, verwenden wir Techniken zur Fehlerbeseitigung, um die Ursache
ten zeigt. dieser Fehler zu entdecken und sie zu beseitigen. Dies kann erheblichen Aufwand
bedeuten, denn zwischen dem Wissen, dass ein Fehler existiert, dem Finden der
Ursache und dem Beheben ist es oft ein weiter Weg.

Konzept Die Erstellung wartbarer Quelltexte ist vermutlich der grundlegendste Ansatz. Es
gilt, Quelltexte so zu schreiben, dass Fehler von vornherein vermieden werden
Fehlerbeseiti-
oder zumindest, wenn sie sich doch einschleichen, leicht zu finden sind. Der Pro-
gung bezeichnet
die Suche nach grammierstil und die Kommentierung haben hierauf einen starken Einfluss. Idea-
der Ursache eines lerweise sollte Quelltext so einfach zu verstehen sein, dass der ursprüngliche Ent-
Fehlers und seine wickler Fehler vermeidet und der Wartungsprogrammierer mögliche Fehler leicht
Beseitigung. entdecken kann.
In der Praxis ist das alles nicht immer so einfach. Aber auf jeden Fall gibt es deut-
liche Unterschiede zwischen gut und schlecht geschriebenen Programmen, sowohl
in der Anzahl der Fehler als auch dem Aufwand ihrer Beseitigung.

9.2 Testen und Fehlerbeseitigung


Testen und Fehlerbeseitigung sind grundlegende Tätigkeiten bei der Software-
entwicklung. Sie werden häufig Ihre Programme auf Fehler untersuchen und
dann gegebenenfalls die Fehlerursache feststellen wollen. Außerdem werden Sie
möglicherweise auch für das Testen oder Anpassen der Programme anderer
zuständig sein. Im letzteren Fall ist die Fehlerbeseitigung oft eng verknüpft mit
dem Verstehen der Quelltexte anderer, sodass die Techniken für beides sich stark
überschneiden. In den nächsten Abschnitten werden wir folgende Techniken und
Werkzeuge zum Testen und zur Fehlerbeseitigung untersuchen:
 manuelle Modultests mit BlueJ
 automatisierte Tests
 manuelle Ausführung
 print-Anweisungen
 Debugger
Wir werden die ersten beiden Testtechniken im Zusammenhang mit Klassen
untersuchen, die Sie selbst geschrieben haben könnten, und die übrigen Techni-
ken zur Fehlerbeseitigung in einem Kontext, in dem Sie fremde Quelltexte verste-
hen müssen.

336
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.3 Modultests in BlueJ

9.3 Modultests in BlueJ


Der Begriff Modultest (unit test) bezieht sich auf Tests einzelner Einheiten einer Konzept
Anwendung, im Gegensatz zu Akzeptanztests, die eine ganze Anwendung testen.
Modultests
Die Größe einer solchen Einheit kann stark variieren. Es kann eine Gruppe von
beziehen sich auf
Klassen sein, eine einzelne Klasse oder sogar nur eine einzelne Methode. Modul- Tests einzelner Teile
tests können lange vor der Fertigstellung einer Anwendung durchgeführt werden. einer Anwendung,
Jede einzelne Methode kann (und sollte), sobald sie geschrieben und übersetzt zum Beispiel auf
wurde, getestet werden. Methoden und
Klassen.
Weil BlueJ uns den interaktiven Umgang mit individuellen Objekten ermöglicht,
gibt es uns gute Möglichkeiten für das Testen von Klassen und Methoden. Beach-
ten Sie auch, dass es nie zu früh zum Testen ist. Frühes Experimentieren und Testen
hat viele Vorteile. Erstens macht es das Finden und Beheben von Fehlern in einem
System einfacher und billiger, als wenn sie erst sehr viel später im Laufe der System-
entwicklung gefunden werden. Zweitens können wir auf diese Weise eine Reihe
von Testfällen und Ergebnissen sammeln, die wir immer wieder einsetzen können,
während das System wächst. Jedes Mal, wenn wir eine Änderung an dem System
vornehmen, erlauben uns diese Testfälle zu prüfen, ob wir mit diesen Änderungen
ungewollt Fehler eingeführt haben.
Um diese Art des Testens zu demonstrieren, werden wir das Projekt OnlineShop
verwenden. Es repräsentiert den frühen Status bei der Entwicklung einer Software
für einen elektronischen Onlineshop (wie [Link]). Unser Projekt umfasst nur
einen sehr kleinen Teil dieser Anwendung, und zwar den, der sich mit den Kunden-
kommentaren zu Verkaufsartikel befasst.
Öffnen Sie das Projekt OnlineShop. Es enthält in diesem Stadium zwei Klassen:
Produkt und Kommentar. Folgende Funktionen sind in diesem Teil der Anwendung
enthalten, wenn wir uns ausschließlich auf die Bearbeitung der Kundenkommen-
tare konzentrieren:
 Es können Produkte mit einer Beschreibung und einem Preis erzeugt werden.
 Es können Kundenkommentare zu den Produkten hinzugefügt und entfernt
werden.
 Kommentare bestehen aus einem Kommentartext, dem Namen des Autors
und einer Bewertung. Die Bewertung liegt im Bereich 1 bis 5 (inklusive).
 Jede Person kann nur einen Kommentar abgeben. Nachfolgende Versuche,
einen erneuten Kommentar abzugeben, werden abgeblockt.
 Die Benutzungsschnittstelle (die in diesem Projekt noch nicht implementiert ist)
umfasst die Frage „War dieser Kommentar hilfreich?“ einschließlich der Knöpfe
JA und NEIN. Durch Anklicken von JA oder NEIN stimmt der Benutzer dem Kom-
mentar zu oder lehnt ihn ab. Das Verhältnis der Zustimmungen und Ablehnun-
gen wird für die Kommentare gespeichert, sodass die nützlichsten Kommentare
(die mit dem höchsten Stimmenverhältnis) ganz oben angezeigt werden können.
Die Klasse Kommentar in unserem Projekt speichert Informationen über einen ein-
zelnen Kommentar. Zum Testen konzentrieren wir uns auf die Klasse Produkt aus
Listing 9.1. Objekte dieser Klasse repräsentieren jeweils ein Produkt, einschließ-
lich aller dafür abgegebener Kommentare.

337
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Zum Testen gehört, dass wir mehrere Teile der beabsichtigten Funktionen prüfen,
einschließlich:
 Können Kommentare zu einem Produkt hinzugefügt oder entfernt werden?
 Zeigt die Methode infoZeigen korrekt alle Informationen zu einem Produkt
an?
 Werden die Beschränkungen (Bewertungen müssen zwischen 1 und 5 liegen,
nur ein Kommentar pro Person) korrekt umgesetzt?
 Können wir korrekt den hilfreichsten Kommentar finden (den mit den meisten
Stimmen)?
Wir werden feststellen, dass wir all dies komfortabel mithilfe der Objektleiste von
BlueJ testen können. Außerdem werden wir feststellen, dass BlueJ einige Testabläufe
vereinfacht, da gezielt Änderungen an den zu testenden Klassen vorgenommen wer-
den können.

Listing 9.1
Die Klasse Produkt.

338
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.3 Modultests in BlueJ

339
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

9.3.1 Objektinspektoren nutzen


Beim interaktiven Testen ist der Einsatz von Objektinspektoren oft sehr hilfreich. Als
Vorbereitung für die Tests erzeugen Sie ein Produkt-Objekt auf der Objektleiste und
öffnen seinen Objektinspektor, indem Sie die Funktion INSPIZIEREN aus dem Kontext-
menü des Objekts auswählen. Selektieren Sie das Datenfeld kommentare und öffnen
Sie auch dessen Inspektor (Abbildung 9.1). Prüfen Sie, ob die Liste erzeugt wurde
(nicht null ist) ist und am Anfang die Größe 0 hat. Prüfen Sie auch, dass die Liste

340
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.3 Modultests in BlueJ

beim Hinzufügen von Kommentaren anwächst. Lassen Sie den Kommentarlisten-


Inspektor als Hilfe für die folgenden Tests geöffnet.

Abbildung 9.1
Objektinspektor für die
Liste kommentare.

Ein wichtiger Fall beim Testen von Klassen, die Datenstrukturen benutzen, ist die
Überprüfung, ob sie sich bei einer leeren und bei einer vollständig gefüllten Datens-
truktur korrekt verhalten. Das Testen voller Datenstrukturen ist nur auf solche Struk-
turen anzuwenden, die eine Obergrenze angegeben haben, wie beispielsweise
Arrays. In unserem Fall verwenden wir eine ArrayList, sodass nicht geprüft werden
kann, ob die Liste voll ist, weil die Liste nach Bedarf erweitert werden kann. Eine
leere Liste zu testen, ist wichtig, da dies einen Sonderfall darstellt, der besonders
behandelt wird.
Der erste Test, der auf Produkt ausgeführt werden kann, besteht darin, ihre Methode
infoZeigen aufzurufen, bevor Kommentare hinzugefügt werden. Diese sollte korrekt
die Beschreibung und den Preis des Produkts ohne irgendwelche Kommentare anzei-
gen.
Ein Schlüsselelement beim Testen ist das Überprüfen von Grenzfällen, weil gerade
dort häufig etwas schiefgeht. Ein Grenzfall für die Klasse Produkt ist beispielsweise
die leere Kommentarliste. Die Grenzfälle für die Klasse Kommentar ergeben sich z.B.
durch die Beschränkung der Bewertung auf den Bereich 1 bis 5. Bewertungen
ganz oben und unten in diesem Bereich sind Grenzfälle. Es ist wichtig, nicht nur die
Bewertungen in der Mitte dieses Bereichs zu prüfen, sondern auch die höchst- und
kleinstmögliche Bewertung.
Um Tests nach diesen Überlegungen durchführen zu können, erzeugen Sie ein
Produkt-Objekt auf der Objektleiste und führen Sie die folgenden Übungen als
erste Tests der Kommentarfunktionen durch. Wenn Sie sorgfältig testen, sollten
Sie zwei Fehler in Ihrem Quelltext finden.

Übung 9.1 Fügen Sie mehrere Kommentare zu den Produkten hinzu, wäh-
rend Sie den Inspektor für die Kommentarliste im Blick haben. Achten Sie
darauf, dass sich die Liste wie erwartet verhält (d.h., sie sollte länger wer-
den). Vielleicht wollen Sie auch das Datenfeld elementData des ArrayList-
Objekts untersuchen.

341
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Übung 9.2 Überprüfen Sie, ob die Methode infoZeigen die Produktinforma-


tion, einschließlich der Kommentare, korrekt anzeigt. Versuchen Sie es sowohl
für Elemente mit und ohne Kommentare.
Übung 9.3 Prüfen Sie, ob die Methode gibAnzahlKommentare wie erwartet
funktioniert.
Übung 9.4 Prüfen Sie anschließend, ob Zweifach-Autoren korrekt behandelt
werden – d.h., dass weitere Kommentare des gleichen Autors abgelehnt wer-
den. Wenn versucht wird, einen Kommentar eines Autors hinzuzufügen, von
dem bereits ein Kommentar existiert, sollte die Methode kommentarHinzufuegen
den booleschen Wert false zurückliefern. Prüfen Sie ebenfalls, dass der Kom-
mentar nicht der Liste hinzugefügt wurde.
Übung 9.5 Prüfen Sie die Grenzfälle der Bewertungswerte. Das heißt, erzeu-
gen Sie nicht nur Kommentare mit mittlerer Bewertung, sondern auch mit sehr
hohen und sehr niedrigen Bewertungen. Funktioniert alles korrekt?
Übung 9.6 Gutes Testen der Grenzfälle schließt auch Tests ein, die bewusst
außerhalb des gültigen Bereichs liegen. Testen Sie 0 und 6 als Bewertungs-
werte. In beiden Fällen sollte der Kommentar abgelehnt werden (kommentar-
Hinzufuegen sollte false zurückliefern und der Kommentar sollte nicht zu der
Kommentarliste hinzugefügt werden).
Übung 9.7 Testen Sie die Methoden kommentarZustimmen und kommentar-
Ablehnen. Stellen Sie sicher, dass das Stimmenverhältnis korrekt gezählt wird.

Übung 9.8 Verwenden Sie die Methoden kommentarZustimmen und kommentar-


Ablehnen, um einige Kommentare als sehr oder weniger hilfreich zu markie-
ren. Testen Sie daraufhin die Methode findeHilfreichstenKommentar. Diese
Methode sollte den Kommentar zurückliefern, der als am hilfreichsten
gewählt wurde. Sie werden feststellen, dass die Methode eine Objektrefe-
renz zurückliefert. Mit der INSPIZIERE-Funktion in dem Dialogfenster Methoden-
ergebnisse können Sie prüfen, ob der korrekte Kommentar zurückgeliefert
wurde. Natürlich müssen Sie wissen, welches der korrekte Kommentar ist,
um zu prüfen, ob Sie das richtige Ergebnis haben!
Übung 9.9 Führen Sie Grenzbereichtests für die Methode findeHilfreichsten-
Kommentar aus. Das heißt, rufen Sie diese Methode auf, wenn die Kommen-
tarliste leer ist (noch keine Kommentare hinzugefügt wurden). Funktioniert
dies wie erwartet?
Übung 9.10 Die Tests in den vorherigen Übungen sollten zwei Fehler in unse-
rem Quelltext ans Tageslicht bringen. Beheben Sie sie. Können Sie anschlie-
ßend mit Sicherheit annehmen, dass alle vorherigen Tests wie zuvor funktio-
nieren? In Abschnitt 9.4 werden einige Probleme beim Testen diskutiert, die
sich durch das Korrigieren oder Erweitern von Software ergeben.

342
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.4 Tests automatisieren

Anhand dieser Übungen ist zu sehen, dass interaktive Methodenaufrufe und die
Objektinspektoren nützlich sind, um unmittelbar Feedback über den Zustand eines
Objekts zu bekommen. Sie ersparen häufig den Einsatz von print-Anweisungen
(Abschnitt 9.9) beim Testen und der Fehlerbeseitigung.

9.3.2 Positives und negatives Testen


Wenn wir Tests planen, unterscheiden wir häufig positive und negative Testfälle. Konzept
Positives Testen überprüft die Funktionalität, die wir erwarten. Das Hinzufügen
Positives Tes-
eines Kommentars von einem neuen Autor mit einer gültigen Bewertung ist bei-
ten ist das Testen
spielsweise ein positiver Test. Bei der Überprüfung positiver Testfälle müssen wir der Fälle, die funk-
uns davon überzeugen, dass die getestete Einheit auch wirklich das tut, was wir tionieren sollten.
erwarten. Negatives Tes-
ten ist das Testen
Negatives Testen überprüft die Fälle, deren Scheitern wir erwarten. Die Verwen-
der Fälle, die fehl-
dung einer ungültigen Bewertung oder der Versuch, einen zweiten Kommentar schlagen sollten.
vom gleichen Autor zu speichern, sind beides Beispiele für negative Tests. Wenn
wir negative Testfälle überprüfen, erwarten wir, dass das Programm diese in kon-
trollierter Weise handhabt.

Fallstrick
Ein von Anfängern häufig gemachter Fehler ist es, ausschließlich positive
Tests durchzuführen. Negatives Testen – das Prüfen, ob die Fälle, die fehl-
schlagen sollten, auch tatsächlich fehlschlagen, und das in möglichst orga-
nisierter Weise – ist ein wichtiger Bestandteil jedes guten Testvorgehens.

Übung 9.11 Welche der in den vorigen Übungen vorgeschlagenen Testfälle


sind positive, welche sind negative Testfälle? Erstellen Sie eine Tabelle für jede
Kategorie. Fallen Ihnen weitere positive Tests ein? Gibt es weitere negative
Tests?

9.4 Tests automatisieren


Gründliches Testen wird unter anderem deshalb häufig vernachlässigt, weil Testen Konzept
eine zeitaufwendige und relativ langweilige Tätigkeit ist, wenn sie von Hand aus-
Automatisie-
geführt wird. Ihnen ist bei den Übungen des vorherigen Abschnitts sicherlich auf-
ren von Tests
gefallen, dass gründliches Testen sehr schnell lästig werden kann. Das bekommt vereinfacht den
insbesondere dann Gewicht, wenn Tests nicht nur einmal, sondern möglicherweise Prozess des Regres-
hundertmal oder tausendmal ausgeführt werden müssen. Glücklicherweise gibt es sionstestens.
Techniken, die das wiederholte Testen automatisieren und dem Testen somit vieles
von seiner Eintönigkeit nehmen. Im nächsten Abschnitt betrachten wir Testauto-
matisierung im Zusammenhang mit Regressionstests.

343
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

9.4.1 Regressionstests
Es wäre schön, wenn wir uns darauf verlassen könnten, dass durch das Korrigieren
von Fehlern die Qualität eines Programms verbessert wird. Leider zeigt die Erfah-
rung, dass beim Modifizieren von Software nur allzu leicht weitere Fehler gemacht
werden. Das Beheben eines Fehlers an einer Stelle kann leicht einen neuen Fehler
an einer anderen Stelle mit sich führen.
Deshalb ist es wünschenswert, dass wir nach jeder Änderung an einer Software
sogenannte Regressionstests durchführen. Ein Regressionstest ist ein Test, der
bereits erfolgreich gelaufen ist und wiederholt wird, um eine neue Version der Soft-
ware zu überprüfen. Die Wahrscheinlichkeit, dass Regressionstests wirklich durchge-
führt werden, steigt, wenn sie in geeigneter Weise automatisiert werden können.
Einer der einfachsten Wege, einen Regressionstest zu automatisieren, ist das Schrei-
ben von Programmen, die als Testgerüste dienen können.

9.4.2 Automatisierte Tests mit JUnit


BlueJ sowie viele andere Entwicklungsumgebungen unterstützen Regressions-
tests durch ein Testsystem namens JUnit. JUnit ist ein von Erich Gamma und Kent
Beck entwickeltes Test-Rahmenwerk für Java, doch inzwischen gibt es ähnliche
Systeme für viele weitere Programmiersprachen.

JUnit ist ein populäres Test-Rahmenwerk zur Unterstützung von wohlstruktu-


rierten Modultests und Regressionstests in Java. Es ist unabhängig von spezifi-
schen Entwicklungsumgebungen erhältlich, aber auch in vielen Umgebungen
bereits integriert. JUnit wurde von Erich Gamma und Kent Beck entwickelt.
Sie können die Software selbst und eine Menge Informationen darüber fin-
den unter [Link]

Um an einem eigenen Beispiel zu sehen, wie Regressionstests durchgeführt werden,


öffnen Sie das Projekt OnlineShop-JUnit. Dieses Projekt enthält die gleichen Klassen
wie das vorherige Beispiel, plus einer zusätzlichen Klasse ProduktTest.
ProduktTest ist eine Testklasse. Das erste, was an dieser Klasse auffällt, ist, dass ihr
Erscheinungsbild von denen der bisherigen Klassen abweicht (Abbildung 9.2). Sie
ist mit dem Zusatz <<unit test>> versehen, sie unterscheidet sich farblich von den
gewöhnlichen Klassen im Diagramm und sie ist mit der Klasse Produkt verbunden
(sie wird mit dieser Klasse verschoben, wenn die Klasse Produkt im Diagramm ver-
schoben werden sollte).
Auch dürfte Ihnen in Abbildung 9.2 aufgefallen sein, dass im Hauptfenster unter
dem ÜBERSETZEN-Knopf einige weitere Steuerelemente zu finden sind. Diese erlauben
Ihnen, von den integrierten Regressionstestwerkzeugen Gebrauch zu machen. Wenn
Sie JUnit noch nicht in BlueJ verwendet haben, sind diese Werkzeuge deaktiviert und
die Knöpfe sind in Ihrem System noch nicht zu sehen. Sie sollten diese Testwerk-
zeuge jetzt aktivieren. Öffnen Sie dazu im Dialogfeld EINSTELLUNGEN die Registerseite
INTERFACE und stellen Sie sicher, dass die Option TESTWERKZEUGE ANZEIGEN markiert ist.

344
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.4 Tests automatisieren

Abbildung 9.2
Ein Projekt mit
einer Testklasse.

Ein weiterer Unterschied zeigt sich im Kontextmenü, wenn Sie die Testklasse mit
der rechten Maustaste anklicken (Abbildung 9.3). Es gibt drei neue Abschnitte in
diesem Menü anstelle einer Liste von Konstruktoren.

Abbildung 9.3
Das Kontextmenü für
eine Testklasse.

Mithilfe von Testklassen können wir unsere Regressionstests automatisieren. Die


Testklasse enthält Quelltext, um eine Reihe von vorbereiteten Tests durchzuführen
und ihre Ergebnisse zu prüfen. Dadurch wird das häufig wiederholte Testen mit
den gleichen Tests stark vereinfacht.
Eine Testklasse ist normalerweise einer normalen Projektklasse zugeordnet. In die-
sem Fall gehört die Klasse ProduktTest zu der Klasse Produkt und wir sagen, dass
Produkt die Bezugsklasse für ProduktTest ist.

In unserem Projekt wurde die Testklasse zuvor erzeugt; sie enthält bereits einige
Tests. Wir können diese Tests ausführen, indem wir auf den Knopf TESTS STARTEN
klicken.

345
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Übung 9.12 Klicken Sie auf den Knopf TESTS STARTEN, um die Tests für Ihr Pro-
jekt auszuführen. Sie sollten ein Fenster wie in Abbildung 9.4 erhalten, das die
Ergebnisse der Tests zusammenfasst.

Abbildung 9.4
Das Fenster
Testergebnisse.

Abbildung 9.4 zeigt das Ergebnis der Ausführung von drei Tests, die testKommentar-
Hinzufuegen, testInit und testUngueltigeBewertung heißen und in der Testklasse
definiert sind. Der Haken links neben den Namen der Tests zeigt an, dass die Tests
erfolgreich waren. Sie erhalten die gleichen Ergebnisse, wenn Sie die Option ALLES
TESTEN aus dem Kontextmenü der Textklasse aufrufen oder die Tests einzeln aus
dem Kontextmenü auswählen.
Testklassen unterscheiden sich deutlich von gewöhnlichen Klassen, und wenn Sie
sich den Quelltext der Klasse ProduktTest ansehen, dann werden Sie einige neue
Dinge bemerken. An dieser Stelle des Buches wollen wir nicht im Detail erklären,
wie Testklassen funktionieren, aber es sollte erwähnt werden, dass der Quelltext
von ProduktTest, obwohl er von einer Person geschrieben sein könnte, tatsächlich
innerhalb von BlueJ automatisch generiert wurde. Anschließend wurden lediglich
einige Kommentare eingefügt, um den Zweck der Tests zu dokumentieren.
Jede Testklasse enthält normalerweise Tests zu den Funktionen ihrer Bezugs-
klasse. Eine Testklasse wird erzeugt, indem auf einer potenziellen Bezugsklasse
ein Rechtsklick ausgeführt wird und aus dem Kontextmenü der Eintrag TESTKLASSE
ERZEUGEN selektiert wird. Beachten Sie, dass Produkt bereits eine Testklasse besitzt,
sodass dieser zusätzliche Eintrag nicht im Kontextmenü der Klasse erscheint. Im
Kontextmenü der Klasse Kommentar hingegen erscheint er, da dieser Klasse momen-
tan keine Testklasse zugeordnet ist.
Der entscheidende Punkt an einer Testklasse ist, dass sie sowohl Quelltext ent-
hält, der Tests auf einer Bezugsklasse ausführt, als auch Code, mit dem der Erfolg
dieser Tests überprüft wird. Hier ist beispielsweise eine Anweisung aus testInit,
die überprüft, dass der Preis des Produkts zu diesem Zeitpunkt 1000 ist:
assertEquals(1000, [Link]());

346
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.4 Tests automatisieren

Wenn solche Tests ausgeführt werden, kann BlueJ die Ergebnisse in dem Fenster
anzeigen, das in Abbildung 9.4 gezeigt wird.
Im nächsten Abschnitt werden wir diskutieren, wie BlueJ Sie bei der automatischen
Erstellung von Regressionstest unterstützt, sodass Sie Ihre eigenen automatisierten
Tests erstellen können.

Übung 9.13 Erzeugen Sie eine Testklasse für die Klasse Kommentar im Projekt
OnlineShop-JUnit.
Übung 9.14 Welche Methoden werden automatisch erzeugt, wenn eine
neue Testklasse angelegt wird?

9.4.3 Tests aufzeichnen


Wir haben bereits zu Beginn von Abschnitt 9.4 gesagt, dass Testautomatisierung
wünschenswert ist, weil das manuelle Durchführen und Neu-Durchführen von
Tests ein zeitaufwendiger Prozess ist. BlueJ ermöglicht, die Effektivität von inter-
aktiv durchgeführten Tests mit der Stärke automatisierter Tests zu kombinieren,
indem manuell durchgeführte Tests aufgezeichnet und später für Regressions-
tests wieder abgespielt werden können. Die Klasse ProduktTest aus dem Projekt
OnlineShop-JUnit wurde auf diese Weise erstellt.
Nehmen Sie an, wir möchten die Methode kommentarHinzufuegen der Klasse Produkt
gründlich testen. Diese Methode versucht, Kundenkommentare hinzuzufügen,
wenn sie gültig sind. Wir möchten verschiedene Tests durchführen, beispielsweise
wollen wir
 einen ersten Kommentar zu einer leeren Kommentarliste hinzufügen (positiv)
 weitere Kommentare zu bereits bestehenden Kommentaren hinzufügen (positiv)
 versuchen, einen Kommentar von einem Autor hinzuzufügen, der bereits einen
Kommentar abgegeben hat (negativ)
 versuchen, einen Kommentar mit einer ungültigen Bewertung abzugeben (negativ).
Der erste Test existiert bereits in der Klasse ProduktTest. Im Folgenden werden wir
beschreiben, wie wir den nächsten Test unter Verwendung des Projekts Online-
Shop-JUnit erzeugen.
In BlueJ wird ein Test aufgezeichnet, indem eine Aufzeichnung gestartet wird, der
Test manuell ausgeführt wird und dann das Ende der Aufzeichnung signalisiert wird.
Der erste Schritt erfolgt über das Kontextmenü der Testklasse. Auf diese Weise weiß
BlueJ, in welcher Klasse der neue Test gespeichert werden soll. Selektieren Sie
ERZEUGE TESTMETHODE… aus dem Kontextmenü der Klasse ProduktTest. Sie werden
daraufhin nach dem Namen für die Testmethode gefragt. Per Konvention beginnen
wir den Namen mit dem Präfix test. Bei der Erzeugung einer Methode, die das Hin-
zufügen von zwei Kommentaren prüft, könnten wir beispielsweise als Methoden-
namen testZweiKommentare vergeben.1

1 Frühere Versionen von JUnit (bis Version 3) schrieben vor, dass die Methodennamen mit dem
Präfix „test“ beginnen mussten. In aktuellen Versionen ist dies nicht mehr erforderlich.

347
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Sobald Sie einen Namen eingegeben und OK geklickt haben, erscheint ein roter
Kreis links vom Klassendiagramm und die Knöpfe BEENDEN und ABBRECHEN werden
aktiviert. Mit BEENDEN können Sie eine Testerzeugung abschließen, mit ABBRECHEN
können Sie sie verwerfen.

Konzept Sobald eine Aufzeichnung gestartet wurde, führen wir einfach die Aktionen durch,
die wir in einem normalen manuellen Test vornehmen würden.
Eine Zusiche-
rung ist ein boole-  Erzeugen eines Produkt-Objekts.
scher Ausdruck für  Einen Kommentar zu dem Produkt hinzufügen.
eine Bedingung,
Sobald kommentarHinzufuegen aufgerufen wurde, erscheint ein neues Dialogfenster
von der wir erwar-
ten, dass sie wahr
(Abbildung 9.5). Dieses ist eine erweiterte Version des normalen Fensters für Metho-
ist. Wenn sie nicht denergebnisse und spielt eine wichtige Rolle bei der Erzeugung automatisierter
wahr ist, dann Tests. In diesem Fenster können Sie angeben, welches Ergebnis der Methodenaufruf
sagen wir, dass die haben sollte. Eine in dieser Weise formalisierte Erwartung wird als Zusicherung
Zusicherung fehl- (assertion) bezeichnet.
schlägt. Dies weist
auf einen Fehler in
unserem Pro-
gramm hin.

Abbildung 9.5
Der Dialog für Metho-
denergebnisse mit
einer Unterstützung
für Zusicherungen.

In diesem Fall erwarten wir, dass die Methode den Wert true zurückliefert, ent-
sprechend wollen wir bei unserem Test eine Überprüfung durchführen, dass dies
wirklich so ist. Wir selektieren also die Checkbox SICHERE ZU, DASS, geben true in
den Dialog ein und klicken den SCHLIEßEN-Knopf.
 Fügen Sie zu Ihrem Produkt einen zweiten Kommentar hinzu. Achten Sie darauf,
dass die Kommentare gültig sind (sie stammen jeweils von unterschiedlichen
Autoren und ihre Bewertung ist gültig). Sichern Sie zu, dass das Ergebnis auch
für den zweiten hinzugefügten Kommentar true ist.
 Wir gehen jetzt davon aus, dass zwei Kommentare existieren. Um zu testen,
dass dies tatsächlich der Fall ist, rufen Sie die Methode gibAnzahlKommentare auf
und sichern Sie zu, dass das Ergebnis 2 ist.
Da dies der letzte Schritt unseres Tests ist, klicken wir anschließend den BEENDEN-
Knopf um die Aufzeichnung zu beenden. An dieser Stelle fügt BlueJ nun Quell-
text für unsere neue Testmethode testZweiKommentare in die Klasse ProduktTest
ein, übersetzt die Klasse und löscht die Objektleiste. Die resultierende generierte
Methode ist in Listing 9.2 zu sehen.
Es ist zu erkennen, dass die Methode Anweisungen enthält, die die Aktionen repro-
duzieren, die während der Aufzeichnung durchgeführt wurden: Ein Objekt der Klasse

348
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.4 Tests automatisieren

Produkt wird erzeugt und die Methoden kommentarHinzufuegen und gibAnzahlKommen-


tare werden aufgerufen. Der Aufruf assertEquals überprüft, ob das von diesen
Methoden zurückgelieferte Ergebnis zum erwarteten Wert passt. Darüber hinaus
sehen Sie vor der Methode ein neues Konstrukt @Test. Damit wird gekennzeichnet,
dass es sich bei dieser Methode um eine Testmethode handelt.

Listing 9.2
Eine automatisch
generierte Test-
methode.

In den folgenden Übungen können Sie diesen Prozess selbst ausprobieren. In einer
Übung sehen Sie auch, was passiert, wenn der tatsächliche Wert nicht dem erwar-
teten entspricht.

Übung 9.15 Erzeugen Sie einen Test, der prüft, dass kommentarHinzufuegen den
Wert false liefert, wenn ein Kommentar vom selben Autor bereits existiert.
Übung 9.16 Erstellen Sie den folgenden negativen Test für die Grenzfälle bei
den Bewertungswerten. Das heißt, testen Sie die Werte 0 und 6 als Bewertung
(die gerade außerhalb des gültigen Bereichs liegen). Wir erwarten, dass diese
false liefern, sodass Sie im Dialogfenster METHODENERGEBNIS false zusichern
müssen. Sie werden feststellen, dass einer davon tatsächlich (fälschlicherweise)
true liefert. Dies ist einer der Fehler, den wir zuvor durch interaktives Testen fest-
gestellt haben. Achten Sie darauf, dass Sie trotzdem false zusichern. Die Zusi-
cherung gibt das erwartete Ergebnis an und nicht das tatsächliche Ergebnis.
Übung 9.17 Führen Sie alle Tests erneut durch. Im Dialogfenster TESTERGEBNIS
können Sie sich davon überzeugen, dass der fehlgeschlagene Test angezeigt
wird. Selektieren Sie den fehlgeschlagenen Test in der Liste. Welche Optionen
stehen Ihnen zur Verfügung, um die Details des fehlgeschlagenen Tests zu
untersuchen?
Übung 9.18 Erzeugen Sie eine Testklasse, die Kommentar als ihre Bezugsklasse
hat. Erzeugen Sie einen Test, der prüft, ob der Autor und die Bewertungs-
details nach der Erzeugung korrekt gespeichert wurden. Zeichnen Sie getrennte
Testmethoden darin auf, die überprüfen, dass die Methode ablehnen und
zustimmen wie erwartet funktionieren.
Übung 9.19 Erstellen Sie Tests für die Klasse Produkt, die testet, ob die
Methode findeHilfreichstenKommentar wie erwartet arbeitet. Beachten Sie,
dass diese Methode ein Kommentar-Objekt liefert. Während des Testens kön-
nen Sie den Knopf HOLE im Dialogfenster Methodenergebnis dazu nutzen,
um das Ergebnisobjekt in die Objektleiste zu „holen“, wo Sie dann weitere
Methodenaufrufe machen und Zusicherungen für dieses Objekt hinzufügen
können. Das erlaubt es, dass zurückgelieferte Kommentarobjekt zu identifi-
zieren (z.B. durch Prüfen seines Autors). Sie können auch zusichern, dass das
Ergebnis null beziehungsweise nicht null ist (je nachdem, was Sie erwarten).

349
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

9.4.4 Testgerüste
Konzept Bei der Erstellung von Testmethoden werden Sie häufig beobachten, dass Sie immer
wieder ähnliche Objekte für jede Testmethode erstellen. Beispielsweise benötigen Sie
Ein Testgerüst
für jeden Test der Klasse Produkt mindestens ein Objekt der Klasse Produkt, das sie
besteht aus einer
Menge von Objek- initialisieren müssen, oft indem Sie ein oder mehrere Kommentare hinzufügen. Ein
ten in einem defi- Objekt oder eine Gruppe von Objekten, die in mehr als einem Test verwendet wer-
nierten Zustand, den, werden als Testgerüst bezeichnet. Zwei Menüeinträge einer Testklasse ermögli-
die als Grundlage chen uns, in BlueJ mit Testgerüsten zu arbeiten: OBJEKTZUSTAND SPEICHERN und
für einen Modultest OBJEKTZUSTAND WIEDERHERSTELLEN. Erzeugen Sie in Ihrem Projekt zwei Produkt-
dienen. Objekte auf der Objektleiste. Eines davon soll keine Kommentare und das andere
zwei Kommentare erhalten. Selektieren Sie nun den Eintrag OBJEKTZUSTAND
SPEICHERN im Kontextmenü von ProduktTest. Die Objekte verschwinden von der
Objektleiste und im Quelltext der Klasse ProduktTest können Sie erkennen, dass die
Methode setUp ungefähr so aussieht wie in Listing 9.3, in dem produkt1 und pro-
dukt2 als Datenfelder der Testklasse definiert sind.

Listing 9.3
Der Aufbau eines
Testgerüsts.

Die Methode setUp wird automatisch jedes Mal aufgerufen, bevor eine Test-
methode aufgerufen wird. (Die Kennzeichnung @Before über dem Methodenkopf
stellt dies sicher.) Dies bedeutet, dass eine individuelle Testmethode nun nicht
mehr ihr eigenes Testgerüst aufzubauen braucht.
Sobald wir ein Testgerüst für eine Testklasse definiert haben, vereinfachen sich auch
die Aufzeichnungen weiterer Tests: Wenn wir eine neue Testmethode erzeugen,
werden die Objekte des Testgerüsts automatisch erzeugt und auf die Objektleiste
gelegt – es besteht keine Notwendigkeit mehr, jedes Mal neue Testobjekte interaktiv
zu erzeugen.
Wenn wir ein Testgerüst um weitere Objekte ergänzen wollen, brauchen wir ledig-
lich OBJEKTZUSTAND WIEDERHERSTELLEN zu selektieren, die zusätzlichen Objekte erzeu-
gen und dann OBJEKTZUSTAND SPEICHERN auszuführen. Natürlich können wir auch
einfach die Methode setUp editieren und weitere Datenfelder direkt in der Testklasse
einfügen.
Testautomatisierung ist ein mächtiges Konzept, denn es macht es wahrschein-
licher, dass überhaupt Tests geschrieben werden, und es macht wahrscheinlicher,
dass diese im Laufe der Programmentwicklung ausgeführt und wiederholt wer-
den. Sie sollten sich angewöhnen, Testklassen so früh wie möglich bei der Arbeit
an einem Projekt zu schreiben und diese auch ständig aktuell zu halten. In Kapi-
tel 14 werden wir das Konzept der Zusicherungen im Zusammenhang mit Fehler-
behandlung erneut aufgreifen.

350
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.5 Refactoring bei Streams einsetzen (fortgeschritten)

Übung 9.20 Fügen Sie in Ihrem Projekt so lange weitere automatisierte


Tests ein, bis Sie ausreichend Vertrauen in die korrekte Arbeitsweise der zu
testenden Klassen haben. Testen Sie sowohl positiv als auch negativ. Wenn
Sie Fehler entdecken sollten, dann erstellen Sie Tests, die Sie vor dem erneu-
ten Auftreten dieser Fehler in einer späteren Version schützen.

Im Abschnitt 9.6 beschäftigen wir uns mit Debugging – die Aktivität, die einsetzt,
wenn wir feststellen, dass es einen Fehler gibt, den wir finden und beheben müssen.

9.5 Refactoring bei Streams einsetzen


(fortgeschritten)
In den vorherigen Abschnitten dieses Buchs über fortgeschrittene Konzepte haben
wir Streams als Alternative zu Schleifen bei der Verarbeitung von Sammlungen ein-
geführt. Das OnlineShop-Projekt bietet die Gelegenheit, das Umformen traditio-
neller Schleifen durch den Einsatz von Streams weiter zu üben. Wenn Sie die vori-
gen Übungen zu den fortgeschrittenen Themen durchgearbeitet haben, wird
Ihnen vielleicht auch die folgende Spaß machen.

Übung 9.21 Schreiben Sie alle Schleifen in der Klasse Produkt unter Verwen-
dung von Streams neu. Wenn Sie fertig sind, führen Sie Ihre Tests erneut aus,
um sich davon zu überzeugen, dass auch nach Ihren Veränderungen die Klasse
noch wie beabsichtigt funktioniert.

9.6 Debugging
Testen ist wichtig und gutes Testen trägt dazu bei, Fehler festzustellen. Doch Testen
allein reicht nicht aus. Nachdem wir einen Fehler festgestellt haben, müssen wir
seine Ursache finden und diese beseitigen. Hier kommt das Debugging ins Spiel.
Für unsere Diskussion der verschiedenen Ansätze zum Debuggen bedienen wir uns
eines hypothetischen Szenarios. Angenommen, Sie wurden gebeten, in einem Pro-
jektteam mitzuarbeiten, das die Implementierung eines Softwarerechners vornimmt
(Abbildung 9.6). Sie werden ausgewählt, weil ein Kernmitglied des Programmier-
teams – Hacker T. Largebrain – gerade auf eine Managementposition in einem
anderen Projekt versetzt wurde. Bevor er Ihr neues Team verlassen hat, hat er beteu-
ert, dass seine Implementierung von dem Teil des Rechners, für den er verantwort-
lich ist, abgeschlossen ist und vollständig getestet wurde. Er hat sogar etwas Test-
software geschrieben, mit der dies belegt werden soll. Sie wurden gebeten, diese
Klasse zu übernehmen und sicherzustellen, dass sie ausreichend kommentiert ist,
bevor sie mit den Klassen der anderen Teammitglieder integriert wird.

351
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Die Software für den Rechner wurde sorgfältig entworfen, um die Benutzungs-
schnittstellen von der Rechnerlogik zu trennen, sodass der Rechner später auch in
anderen Kontexten verwendet werden kann. Die erste Version, die wir hier betrach-
ten, wird mit einer grafischen Benutzungsschnittstelle wie in Abbildung 9.6 ausge-
führt. In späteren Erweiterungen zu dem Projekt wollen wir jedoch erreichen, dass
eben diese Rechnerimplementierung in einem Webbrowser oder auf einem mobilen
Gerät ausgeführt werden kann. Vorbereitend haben wir deshalb die Anwendung in
mehrere Klassen geteilt, vor allem GrafischeSchnittstelle, die die grafische Benut-
zungsschnittstelle implementiert, und Recheneinheit, die die Rechenlogik implemen-
tiert. Für eben diese letzte Klasse war Hacker verantwortlich. Diese Klasse sollte
unverändert bleiben, wenn der Rechner mit einer anderen Benutzungsschnittstelle
ausgeführt wird.

Abbildung 9.6
Die Benutzungs-
schnittstelle eines
Softwarerechners.

Um festzustellen, wie die Klasse Recheneinheit von anderen Klassen verwendet wird,
ist es hilfreich, einen Blick auf ihre Schnittstelle zu werfen – wobei hier nicht die
Benutzungsschnittstelle, sondern die Klassenschnittstelle gemeint ist. Diese dop-
pelte Bedeutung des Begriffs Schnittstelle ist bedauernswert, aber es ist wichtig,
beide Bedeutungen zu verstehen.

Die Klassenschnittstelle ist die Zusammenfassung der öffentlichen Methodenköpfe,


die andere Klassen benutzen können. Genau das können andere Klassen sehen und
darüber können sie mit unserer Klasse interagieren. Die Schnittstelle wird in der
javadoc-Dokumentation einer Klasse sowie in der Ansicht Dokumentation im Editor
angezeigt. Listing 9.4 zeigt die Schnittstelle der Klasse Recheneinheit.

Listing 9.4
Die Schnittstelle
einer arithmetisch-
logischen Einheit.

352
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.7 Kommentierung und Programmierstil

Solch eine Schnittstelle kann geschrieben werden, bevor die vollständigen Klas-
sen implementiert sind. Sie bildet so etwas wie einen simplen Vertrag zwischen
der Klasse Recheneinheit und den anderen Teilen des Programms, die diese Klasse
benutzen wollen. Die Klasse Recheneinheit bietet die Implementierung dieser
Schnittstelle. Die Schnittstelle beschreibt den minimalen Satz an Methoden, die
in der logischen Komponente implementiert werden, und für jede Methode wer-
den der Ergebnistyp und die Parameter definiert. Beachten Sie, dass die Schnitt-
stelle nichts darüber aussagt, was in der implementierenden Klasse intern abläuft,
wenn beispielsweise die Plus-Taste gedrückt wurde; das bleibt den Programmie-
rern überlassen, die dies implementieren. Zusätzlich kann die implementierende
Klasse weitere Methoden enthalten, die hier nicht aufgeführt sind.
In den folgenden Abschnitten werden wir uns Hackers Implementierungsversuch für
diese Schnittstelle ansehen. In diesem Fall entscheiden wir uns, bevor wir mit dem
Dokumentieren beginnen, dass wir den besten Einblick in Hackers Software bekom-
men, wenn wir den Quelltext untersuchen und das Verhalten der Objekte analysieren.

9.7 Kommentierung und Programmierstil


Öffnen Sie das Projekt Recheneinheit, um die Klassen zu untersuchen. Es handelt sich
dabei um Hackers Version des Projekts, das nur die Recheneinheit und eine Testklasse
enthält. Die Klasse RecheneinheitTester übernimmt an diesem Punkt der Entwick-
lung die Rolle der Benutzungsschnittstelle. Dies demonstriert einen weiteren Vorteil
von wohldefinierten Schnittstellen zwischen Klassen: Es wird einfacher, Stellvertreter-
klassen für andere Klassen, sogenannte Mock-Ups, für Testzwecke zu definieren.
Wenn Sie sich die Klasse Recheneinheit ansehen, dann werden Sie feststellen, dass
ihr Autor Wert auf einige wichtige Aspekte von gutem Programmierstil gelegt hat:
 Die Klasse hat einen mehrzeiligen Klassenkommentar bekommen, der den Zweck
der Klasse beschreibt. Auch Autor und Versionsnummer sind angegeben.
 Jede Methode der Schnittstelle hat einen Kommentar, der den Zweck der
Methode, ihre Parameter und ihren Ergebnistyp beschreibt. Dies macht es sicher-
lich leichter, eine Projektdokumentation für die Schnittstelle so zu generieren, wie
in Kapitel 6 besprochen.
 Das Layout der Klasse ist einheitlich, mit angemessenen Einrückungen für die
geschachtelten Blöcke und Kontrollstrukturen.
 Es wurden sinnvolle Namen für Variablen und Methoden gewählt.
Auch wenn das Einhalten dieser Konventionen während der Implementierung etwas
Zeit kostet, können sie doch von enormem Nutzen sein, wenn jemand anderes die-
sen Quelltext verstehen muss (so, wie wir in diesem Szenario) oder wenn der Autor
selbst sich nach einer längeren Pause wieder an den Zweck der Klasse erinnern muss.

353
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Dabei fällt ein weiteres Detail ins Auge, das verdächtig erscheint: Hacker hat keine
spezielle Modultest-Klasse verwendet, um seine Tests durchzuführen, sondern seine
eigene Testklasse geschrieben. Da wir wissen, dass BlueJ Modultests unterstützt,
fragen wir uns natürlich warum.
Eigene Testklassen zu schreiben muss kein Nachteil sein und selbst geschriebene
Testklassen müssen nicht notwendigerweise schlechter sein. Trotzdem sind wir
jetzt etwas misstrauisch geworden. Wusste Hacker wirklich, was er tat? Wir wer-
den bald auf diese Frage zurückkommen.
Nun, möglicherweise ist Hacker wirklich so gut, wie er von sich selbst glaubt, und
Sie haben nicht viel zu tun, um diese Klasse mit den übrigen zu integrieren! Füh-
ren Sie die folgenden Übungen durch, um dies zu überprüfen.

Übung 9.22 Stellen Sie sicher, dass alle Klassen übersetzt sind, und erzeugen
Sie dann in BlueJ ein Objekt der Klasse RecheneinheitTester. Rufen Sie die
Methode allesTesten auf. Was wird auf der Konsole ausgegeben? Glauben
Sie der Aussage in der letzten Zeile?
Übung 9.23 An dem Objekt der letzten Übung sollen Sie nun die Methode
testPlus aufrufen. Welches Ergebnis liefert sie? Ist dies das gleiche Ergebnis
wie bei allesTesten? Rufen Sie testPlus noch einmal auf. Welches Ergebnis
wird nun geliefert? Sollte die Methode immer die gleiche Antwort liefern?
Wenn ja, wie sollte diese Antwort lauten? Überprüfen Sie dazu im Quelltext
die Umsetzung der Methode testPlus.
Übung 9.24 Wiederholen Sie die vorige Übung mit der Methode testMinus.
Liefert diese immer das gleiche Ergebnis?

Durch diese Experimente sollten bei Ihnen nun die Alarmglocken läuten und Sie
warnen, dass mit der Klasse Recheneinheit etwas nicht stimmt. Es sieht so aus, als
ob die Klasse Fehler enthält. Aber welche Fehler sind das und wo sind sie ver-
steckt? In den folgenden Abschnitten werden wir einige Wege aufzeigen, wie
wir Fehler in einer Klasse lokalisieren können.

9.8 Manuelle Ausführung


Konzept Manuelle Ausführung (walkthrough) ist eine Technik, die eindeutig zu wenig einge-
setzt wird. Das liegt möglicherweise daran, dass sie eine vergleichsweise „untechni-
Eine manuelle
sche“ Technik zum Testen und zur Fehlerbeseitigung ist. Sie ist allerdings trotzdem –
Ausführung ist
das zeilenweise oder gerade deshalb – sehr nützlich. Eine manuelle Ausführung erfordert, dass Sie
Durchgehen durch den Quelltext der zu untersuchenden Klasse ausdrucken und sich dann weit weg
einen Quelltext- von Ihrem Computer mit dem Ausdruck hinsetzen! Es ist sehr leicht, sehr viel Zeit
abschnitt, bei dem vor einem Computerbildschirm sitzend zu verbringen und nur wenig Fortschritt bei
Zustandsänderun- einem Programmierproblem zu machen. Ein Ortswechsel und ein Wechsel der
gen und das Ver- Betrachtung können bei einer Blockade oft helfen, das Problem aus einer völlig
halten einer
anderen Sicht anzugehen. Wir haben schon sehr oft erlebt, dass uns auf dem Weg
Anwendung beob-
achtet werden. zum Mittagessen oder auf der Fahrt nach Hause die Lösung eingefallen ist, nach der
wir in ermüdenden Stunden an der Tastatur gesucht haben.

354
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.8 Manuelle Ausführung

Eine manuelle Ausführung schließt sowohl das Lesen von Klassentexten als auch
das Verfolgen des Kontrollflusses zwischen Klassen und Objekten ein. Dies hilft
uns, die Interaktion der Objekte und ihre interne Arbeitsweise zu verstehen.
Letztendlich ist eine manuelle Ausführung eine Papier-und-Bleistift-Simulation
eines Programmablaufs auf einem Computer. In der Praxis sollte dabei nur ein
kleiner Teil einer Anwendung betrachtet werden, etwa eine logische Einheit von
Aktionen oder sogar nur ein einzelner Methodenaufruf.

9.8.1 Eine Ausführung auf erster Ebene


Wir werden die Technik der manuellen Ausführung anhand des Projekts Rechenein-
heit demonstrieren. Sie können sich die Klassendefinitionen von Recheneinheit und
RecheneinheitTester ausdrucken, um die folgende Diskussion nachzuvollziehen.
Wir beginnen mit einer Untersuchung der Methode testPlus der Klasse Rechen-
einheitTester, da diese eine logische Einheit von Aktionen darstellt. Sie sollte uns
erleichtern zu verstehen, wie die einzelnen Methoden der Klasse Recheneinheit
zusammenspielen, um der Rolle der Recheneinheit eines Taschenrechners gerecht
zu werden. Während wir uns durch den Quelltext arbeiten, werden wir die Fragen
notieren, die uns dabei in den Sinn kommen.
1 Für diese erste Stufe wollen wir nicht zu sehr ins Detail gehen. Wir wollen
einfach nur sehen, wie die Methode testPlus ein Recheneinheit-Objekt ver-
wendet, ohne die Interna des Objekts zu betrachten. Durch unsere vorheri-
gen Experimente wissen wir, dass es Fehler gibt, aber wir wissen nicht, ob
diese im Tester oder der Recheneinheit liegen. Im ersten Schritt wollen wir
deshalb untersuchen, ob der Tester die Recheneinheit korrekt benutzt.
2 Wir stellen fest, dass die erste Anweisung in testPlus davon ausgeht, dass das
Datenfeld recheneinheit bereits eine gültige Referenz auf ein Objekt enthält:
[Link]();
Wir können diese Annahme bestätigen, indem wir uns den Konstruktor der
Testklasse ansehen. Ein häufig gemachter Fehler bei Datenfeldern ist, dass sie
nicht korrekt initialisiert werden, weder bei ihrer Deklaration noch in einem
Konstruktor. Wenn wir ein Datenfeld benutzen wollen, das keine Referenz auf
ein Objekt enthält, erhalten wir üblicherweise eine NullPointerException.
3 Der Aufruf von clear als erste Anweisung scheint der Versuch zu sein, die
Recheneinheit in einen gültigen Anfangszustand zu versetzen, damit sie Ins-
truktionen für das Durchführen von Rechenoperationen entgegennehmen
kann. Das scheint vernünftig zu sein – vergleichbar mit dem Drücken einer
Reset- oder Clear-Taste an einem echten Taschenrechner. An diesem Punkt
betrachten wir die Rechner-Klasse nicht näher, um die exakte Arbeitsweise
der Methode clear herauszufinden. Das kann warten, bis wir genügend
Vertrauen in die Aktionen des Testers haben. Stattdessen machen wir uns
nur eine kurze Notiz, dass wir noch prüfen müssen, ob die Methode clear
den Rechner wie erwartet in einen korrekten Anfangszustand versetzt.
4 Die nächste Anweisung in testPlus ist die Eingabe einer Ziffer über die Me-
thode zifferGetippt:
[Link](3);

355
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Auch dies scheint vernünftig, da der erste Schritt einer Berechnung die Ein-
gabe des ersten Operanden ist. Auch hier: Wir überprüfen nicht, was die Re-
cheneinheit mit der Ziffer macht. Wir nehmen einfach an, dass sie diese ir-
gendwo speichert, um sie für die spätere Berechnung zu verwenden.
5 Die nächste Anweisung ruft plus auf. Somit wissen wir, dass der volle Wert
des ersten Operanden 3 ist. Wir können dies kurz auf dem Ausdruck notieren
oder wir können einen Haken an die Zusicherung in einem der Kommentare
von testPlus machen. Außerdem sollten wir notieren, dass die auszuführende
Operation die Addition ist. Dies scheint trivial zu sein, aber es kommt sehr
leicht vor, dass der Kommentar in einer Klasse nicht mit dem kommentierten
Quelltext übereinstimmt. Das gleichzeitige Überprüfen der Kommentare kann
uns somit davor schützen, später von ihnen in die Irre geleitet zu werden.
6 Als Nächstes wird eine weitere einzelne Ziffer als der rechte Operand einge-
geben, wieder durch einen Aufruf von zifferGetippt.
7 Die Ausführung der Addition wird durch den Aufruf von gleich angestoßen.
Wir notieren hier kurz, dass diese Methode, so, wie sie in testPlus benutzt
wird, scheinbar nicht das erwartete Ergebnis der Berechnung zurückliefert.
Das ist ein weiterer Punkt, den wir bei der näheren Betrachtung von Rechen-
einheit prüfen sollten.

8 Die letzte Anweisung in testPlus fordert den Wert an, der in der Anzeige des
Taschenrechners erscheinen sollte:
return [Link]();
Anscheinend soll dies das Ergebnis der Berechnung sein, aber wir können es
nicht wissen, bevor wir uns nicht ausführlich die Klasse Recheneinheit ange-
sehen haben. Auch hier machen wir eine kurze Notiz, dass wir das überprü-
fen sollten.
Nachdem wir uns die Methode testPlus angesehen haben, haben wir ausreichend
Vertrauen in ihre Implementierung gewonnen: Sie benutzt die Recheneinheit kor-
rekt, indem sie eine Reihe von Tastendrücken simuliert, die eine komplette Berech-
nung durchführen. Wir könnten anmerken, dass diese Methode nicht sonderlich
ehrgeizig implementiert ist – beide Operanden bestehen aus nur einer einzelnen Zif-
fer und es wird nur ein einzelner Operator verwendet. Dies ist jedoch nicht unge-
wöhnlich für Testmethoden, denn die Basisfunktionalität sollte auf jeden Fall getes-
tet werden, bevor komplexere Kombinationen untersucht werden. Trotzdem sollten
noch einige komplexere Tests hinzugefügt werden.

Übung 9.25 Führen Sie selbst eine ähnliche manuelle Ausführung mit der
Methode testMinus durch. Kommen Sie dadurch auf weitere Fragen, die Sie
bei der Untersuchung der Klasse Recheneinheit klären wollen?

356
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.8 Manuelle Ausführung

Bevor wir uns die Klasse Recheneinheit ansehen, sollten wir kurz einen Blick auf die
Methode allesTesten werfen, um zu sehen, wie diese die von uns bereits unter-
suchten Methoden testPlus und testMinus benutzt. Dabei können wir Folgendes
beobachten:
1 Die Methode allesTesten besteht aus einer einfachen Sequenz von print-
Anweisungen.

2 Sie enthält jeweils einen Aufruf der Methoden testPlus und testMinus, deren
Rückgabewerte zur Betrachtung ausgegeben werden. Wir stellen fest, dass es
keinen Hinweis für den Benutzer gibt, welche Werte korrekt wären. Dies
macht es dem Benutzer schwer zu entscheiden, ob die Ergebnisse korrekt sind.

3 Wir stellen fest, dass die letzte Anweisung kühn verkündet:


Alle Tests erfolgreich.
Aber die Methode enthält keine Prüfung, die die Korrektheit dieser Aussage
absichert! Es sollten aber unbedingt zum einen die erwarteten Werte ange-
zeigt werden und zum anderen sollte überprüft werden, ob die berechneten
Werte mit diesen übereinstimmen. Das ist etwas, das wir unbedingt korri-
gieren sollten, wenn wir zu dieser Klasse zurückkehren.
An dieser Stelle sollten wir uns durch den letzten Punkt nicht dazu verleiten lassen,
Änderungen vorzunehmen, die nicht mit dem eigentlichen Problem zusammen-
hängen. Wenn wir solche Änderungen machen würden, könnten wir das eigent-
liche Problem möglicherweise verdecken. Eine der zentralen Voraussetzungen für
erfolgreiche Fehlerbeseitigung ist, den gesuchten Fehler leicht und wiederholbar
auftreten lassen zu können. Sobald das erreicht ist, fällt die Beurteilung einer mög-
lichen Korrektur viel leichter.
Nachdem wir die Testklasse untersucht haben, können wir uns nun dem Quell-
text der Klasse Recheneinheit zuwenden. Wir sind ausgerüstet mit einer Reihe zu
untersuchender Methodenaufrufe aus der manuellen Ausführung der Methode
testPlus und mit einigen Fragen, die dabei aufgetreten sind.

9.8.2 Den Zustand kontrollieren


Ein Objekt der Klasse Recheneinheit unterscheidet sich in seiner Art recht deutlich
von seinem Tester, denn es ist ein vollständig passives Objekt. Es stößt von sich
aus keine Aktionen an, sondern reagiert lediglich auf externe Aufrufe von
Methoden. So etwas ist ein typisches Server-Verhalten (englisch für „Dienstleis-
ter“). Server-Objekte machen häufig primär von ihrem internen Zustand abhän-
gig, wie sie auf Methodenaufrufe reagieren. Dies gilt insbesondere für unsere
Recheneinheit. Deshalb ist ein wichtiger Bestandteil einer manuellen Ausführung
die Kontrolle ihres internen Zustands. Eine Möglichkeit dazu ist das Notieren der
Datenfelder und ihrer Werte in einer handgeschriebenen Tabelle (Tabelle 9.1).
Neue Zeilen können hinzugefügt werden, sodass ein Protokoll der Werte nach
jedem Methodenaufruf entsteht.

357
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Tabelle 9.1
Informelle Tabel- Aufgerufene anzeigewert linker- rechter-
lierung der Zustände Methode Operand Operand
eines Objekts.
Anfangszustand 0 0 ''
clear 0 0 ''
zifferGetippt(3) 3 0 ''

Diese Technik unterstützt sehr leicht die Überprüfung, ob etwas schiefgeht. Sie
ermöglicht auch, die Zustände nach zwei Aufrufen derselben Methode zu über-
prüfen.
1 Zu Beginn unserer Ausführung der Recheneinheit halten wir ihren Anfangs-
zustand fest, wie in der ersten Zeile in Tabelle 9.1. Alle Datenfelder der Einheit
werden im Konstruktor initialisiert. Wir haben bei der Ausführung des Testers
festgestellt, dass die Objektinitialisierung wichtig ist, und halten an dieser Stelle
fest, dass wir die Initialisierung noch überprüfen sollten – insbesondere, weil der
Anfangswert von letzterOperator keinen gültigen Operator darzustellen
scheint. Außerdem fragen wir uns, ob es einen letzten Operator geben sollte,
bevor wirklich ein Operator in einer Berechnung auftritt. Durch das Aufschrei-
ben dieser Fragen müssen wir sie nicht unmittelbar beantworten, sondern kön-
nen sie wieder aufgreifen, wenn wir mehr über die Klasse wissen.
2 Im nächsten Schritt prüfen wir, ob ein Aufruf von clear den Zustand der Rechen-
einheit ändert. Die zweite Zeile in Tabelle 9.1 zeigt an, dass der Zustand an die-
ser Stelle unverändert bleibt, weil anzeigewert bereits den Wert null hatte. Aber
wir notieren eine weitere Frage: Warum wird nur eines der Datenfelder in dieser
Methode gesetzt? Wenn diese Methode eine Art „Reset“ implementieren soll,
warum werden dann nicht alle Datenfelder zurückgesetzt?

3 Als Nächstes wird der Aufruf von zifferGetippt mit dem aktuellen Parame-
ter 3 untersucht. Diese Methode multipliziert einen bereits bestehenden
Wert von anzeigewert mit 10 und addiert dann die neue Ziffer. Dies model-
liert korrekt das Anfügen einer weiteren Ziffer an das rechte Ende einer be-
reits existierenden Zahl. Die Methode verlässt sich darauf, dass anzeigewert
einen vernünftigen Anfangswert von null hat, wenn die erste Ziffer einge-
tippt wird, und unsere Untersuchung von clear liefert uns die Gewissheit,
dass dies der Fall ist. Diese Methode scheint in Ordnung zu sein.
4 Den Aufrufen in der Methode testPlus folgend, betrachten wir nun die Me-
thode plus. Die erste Anweisung ist ein Aufruf der Methode letztenOperator-
Anwenden. An dieser Stelle müssen wir uns entscheiden, ob wir weiterhin ge-
schachtelte Aufrufe übergehen wollen oder ob wir das Vorgehen ändern und
gleich ihre Implementierung betrachten. Ein kurzer Blick auf die Methode
letztenOperatorAnwenden zeigt uns, dass sie recht kurz ist. Außerdem scheint
sie offensichtlich den Zustand der Recheneinheit zu verändern, sodass wir den
Zustand nicht weiter verfolgen könnten, wenn wir diesem Aufruf nicht nach-
gingen. Gehen wir ihm also nach. Wir sollten im Auge behalten, von wo wir

358
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.8 Manuelle Ausführung

gekommen sind, also markieren wir die Stelle in plus, an der wir der letz-
tenOperatorAnwenden-Methode folgen. Wenn das Verfolgen eines geschachtel-
ten Aufrufs zu weiteren geschachtelten Aufrufen führt, brauchen wir mehr als
eine einfache Markierung, um unseren Weg zurückzufinden. In einem solchen
Fall ist es besser, die Aufrufpunkte mit aufsteigenden Nummern zu versehen –
und diese Nummern nach einem Rücksprung wieder zu verwenden.
5 Die Methode letztenOperatorAnwenden gibt uns Aufschluss darüber, wie das
Datenfeld letzterOperator verwendet wird. Sie scheint auch eine unserer frü-
heren Fragen zu beantworten: ob ein Leerzeichen als Anfangswert für den
letzten Operator ausreichend ist. Die Methode überprüft explizit, ob letzter-
Operator ein + oder ein - enthält, bevor sie ihn anwendet. Ein anderer Wert
führt somit nicht zur Anwendung einer falschen Operation. Am Ende dieser
Methode wird sich der Wert von linkerOperand verändert haben, also halten
wir seinen neuen Wert in der Zustandstabelle fest.

6 Wieder zurück in der Methode plus, erhalten die beiden anderen Datenfelder
ihre Werte, sodass die nächste Zeile in unserer Zustandstabelle (Tabelle 9.1)
folgende Werte enthält:
plus03'+'
Die Ausführung der Recheneinheit kann in ähnlicher Weise fortgesetzt wer-
den: Zustandsänderungen notieren, Verhalten verstehen, neue Fragen stellen.
Die folgenden Übungen sollten Ihnen beim Abschluss der manuellen Ausfüh-
rung helfen.

Übung 9.26 Vervollständigen Sie die Zustandstabelle auf Basis der folgen-
den Aufrufsequenz, die der Methode testPlus entnommen ist:
zifferGetippt(4);
gleich();
gibAnzeigewert();
Übung 9.27 Bei der Ausführung der Methode gleich: Sind Sie hier ähnlich
beruhigt über den Anfangswert von letzterOperator, wie wir es in der
Methode letztenOperatorAnwenden waren?
Übung 9.28 Führen Sie manuell einen Aufruf der Methode clear unmittelbar
nach dem Aufruf von gibAnzeigewert am Ende Ihrer Zustandstabelle aus und
notieren Sie den neuen Zustand. Befindet sich die Recheneinheit im gleichen
Zustand wie nach dem letzten Aufruf von clear? Wenn nicht: Welchen Ein-
fluss könnte das Ihrer Meinung nach auf nachfolgende Berechnungen haben?
Übung 9.29 Nach den Erkenntnissen Ihrer Ausführung: Welche Änderungen
sollten Ihrer Meinung nach an der Klasse Recheneinheit vorgenommen wer-
den? Führen Sie diese Änderungen an Ihrer Papierversion durch und machen
Sie dann eine weitere Ausführung. Sie brauchen nicht die Klasse Rechenein-
heitTester auszuführen, wiederholen Sie einfach die Aktionen ihrer Methode
allesTesten.

359
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Übung 9.30 Versuchen Sie eine Ausführung der folgenden Aufrufsequenz


an Ihrer korrigierten Version der Recheneinheit:
clear();
zifferGetippt(9);
plus();
zifferGetippt(1);
minus();
zifferGetippt(4);
gleich();
Welches Ergebnis sollte das ergeben? Verhält sich die Recheneinheit korrekt
und hinterlässt den korrekten Wert in anzeigewert?

9.8.3 Mündliche Ausführung


Eine weitere Anwendung von Ausführungen zur Fehlerbeseitigung kann darin
bestehen, einer anderen Person die Arbeitsweise einer Klasse oder einer Methode
zu erklären. Das kann zu ganz unterschiedlichen Ergebnissen führen:
 Die Person, der Sie den Quelltext erläutern, entdeckt den Fehler.
 Allein schon der Versuch, das gewünschte Verhalten eines Programmstücks in
Worte zu fassen, macht Ihnen selbst klar, warum es sich nicht so verhält.
Dieser zweite Effekt ist so häufig zu beobachten, dass es oft nützlich sein kann,
einer Person, die den Quelltext überhaupt nicht kennt, seine Arbeitsweise zu
erklären – nicht mit der Erwartung, dass diese Person den Fehler findet, sondern
Sie selbst!

9.9 Ausgabeanweisungen
Die vermutlich am häufigsten eingesetzte Technik zum Verstehen und zur Fehler-
beseitigung in Programmen – selbst unter erfahrenen Programmierern – ist das
temporäre Einfügen von Ausgabeanweisungen. Ausgabeanweisungen sind sehr
beliebt, weil es sie in den meisten Programmiersprachen gibt, weil sie jedem zur
Verfügung stehen und weil sie sehr leicht mit jedem Editor eingefügt werden
können. Es werden keine weiteren Sprachkonstrukte oder zusätzliche Software
benötigt, um sie einzusetzen. Während der Ausführung eines Programms liefern
diese zusätzlichen Ausgabeanweisungen dem Benutzer Informationen wie:
 welche Methoden aufgerufen wurden
 die Werte von Parametern
 die Reihenfolge, in der Methoden aufgerufen wurden
 die Werte von Datenfeldern und lokalen Variablen an strategisch wichtigen
Stellen
Listing 9.5 zeigt ein Beispiel, wie die Methode zifferGetippt mit zusätzlichen
Ausgabeanweisungen aussehen könnte. Solche Informationen sind insbesondere
dann nützlich, wenn man sich ein Bild davon verschaffen will, wie der Objekt-

360
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.9 Ausgabeanweisungen

zustand durch verändernde Methoden beeinflusst wird. Um dies noch besser zu


unterstützen, kann eine Methode speziell zur Fehlersuche eingefügt werden, die
die aktuellen Werte aller Datenfelder ausgibt Listing 9.6 zeigt eine solche Methode
zustandAusgeben für die Klasse Recheneinheit.

Listing 9.5
Zusätzliche Ausgabe-
anweisungen zur
Fehlersuche.

Listing 9.6
Eine Methode, die
den Objektzustand
ausgibt.

Wenn jede Methode der Recheneinheit eine Ausgabeanweisung zu Anfang ste-


hen hat und am Ende einen Aufruf von zustandAusgeben, dann zeigt Abbildung
9.7 die Ausgabe, die sich durch einen Aufruf der Methode testPlus am Tester
ergibt. (Diese Ausgabe wurde durch eine Version von Recheneinheit erzeugt, die
im Projekt Recheneinheit-mit-Ausgaben zu finden ist.) Durch solche Ausgaben
können wir uns ein Bild vom Kontrollfluss zwischen den Methoden machen. Bei-
spielsweise können wir an der Reihenfolge der Zustandsausgaben erkennen,
dass plus einen geschachtelten Aufruf von letztenOperatorAnwenden enthält.

Abbildung 9.7
clear wurde aufgerufen.
Zusätzliche Ausgabe-
anzeigewert: 0 linkerOperand: 0 letzterOperator: am Ende von clear
anweisungen zur
zifferGetippt aufgerufen mit: 3 Fehlersuche.
anzeigewert: 3 linkerOperand: 0 letzterOperator: am Ende von ziffer...
plus wurde aufgerufen.
letztenOperatorAnwenden wurde aufgerufen
anzeigewert: 3 linkerOperand: 3 letzterOperator: am Ende von letzte...
anzeigewert: 0 linkerOperand: 3 letzterOperator: + am Ende von plus
zifferGetippt aufgerufen mit: 4
anzeigewert: 4 linkerOperand: 3 letzterOperator: + am Ende von ziffer...
gleich wurde aufgerufen.
anzeigewert: 7 linkerOperand: 0 letzterOperator: + am Ende von gleich

Ausgabeanweisungen können sehr nützlich sein zum Verstehen von Program-


men oder bei der Suche nach Fehlern, aber sie haben auch eine Reihe von Nach-
teilen:

361
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

 Es ist nicht sehr praktikabel, jede Methode mit Ausgabeanweisungen zu verse-


hen. Sie sind erst dann wirklich nützlich, wenn die richtigen Methoden angerei-
chert werden.
 Zu viele Ausgabeanweisungen führen zu einer Informationsflut. Eine große
Menge an Ausgaben kann es schwierig machen, die nützlichen zu erkennen.
Insbesondere Ausgabeanweisungen in Schleifen sind ein Kandidat für dieses
Problem.
 Sobald sie ihren Zweck erfüllt haben, kann es recht umständlich sein, sie wie-
der zu entfernen.
 Es kann auch vorkommen, dass sie wieder benötigt werden, obwohl sie gerade
entfernt wurden. Es ist dann sehr frustrierend, sie erneut eingeben zu müssen!

Übung 9.31 Öffnen Sie das Projekt Recheneinheit-mit-Ausgaben und ver-


vollständigen Sie das Einfügen von Ausgabeanweisungen in allen Methoden
und dem Konstruktor.
Übung 9.32 Erzeugen Sie eine Instanz von RecheneinheitTester im Projekt
und rufen Sie die Methode allesTesten auf. Helfen Ihnen die resultierenden
Ausgaben bei der Suche nach der Ursache des Problems?
Übung 9.33 Haben Sie den Eindruck, dass die Ausgaben, die durch die
angereicherte Klasse Recheneinheit erzeugt werden, zu knapp, zu umfang-
reich oder gerade angemessen sind? Wenn Sie den Eindruck haben, dass sie
zu umfangreich oder zu knapp sind, dann fügen Sie entweder weitere Aus-
gabeanweisungen hinzu oder entfernen Sie unnötige Ausgaben.
Übung 9.34 Was sind jeweils die Vor- und die Nachteile von manuellen Aus-
führungen und von Ausgabeanweisungen bei der Fehlersuche und -beseiti-
gung? Diskutieren Sie diese.

9.9.1 Ausgabeanweisungen an- und ausschalten


Wenn eine noch in der Entwicklung befindliche Klasse um Ausgabeanweisungen
angereichert wird, dann sollen die Ausgaben nicht bei jeder Benutzung der Klasse
erscheinen. Am besten ist, wenn wir die Ausgaben je nach Situation an- oder
abschalten können. Üblicherweise wird dies durch ein zusätzliches boolesches
Datenfeld für Testausgaben erreicht, von dessen Wert die Ausgaben abhängig
gemacht werden. Listing 9.7 illustriert diese Idee.
Listing 9.7
Zusätzliche Ausgabe-
anweisungen zur
Fehlersuche.

362
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.10 Debugger

Eine etwas ökonomischere Variante besteht darin, die direkten Ausgabeanwei-


sungen durch Aufrufe einer der Klasse speziell hinzugefügten Ausgabemethode
zu ersetzen.2 Diese Ausgabemethode würde nur dann etwas ausgeben, wenn
das Datenfeld testausgaben den Wert true hat. Auf diese Weise müssten die Auf-
rufe von Ausgabeanweisungen nicht in einer bedingten Anweisung stehen. Lis-
ting 9.8 verdeutlicht diesen Ansatz. Beachten Sie, dass diese Version annimmt,
dass die Methode zustandAusgeben entweder selbst das boolesche Datenfeld
abfragt oder die neue Methode testausgabe benutzt.

Listing 9.8
Eine Methode zur
Steuerung von
Testausgaben.

Wie Sie anhand dieser Experimente sehen, benötigt man ein wenig Praxis, um he-
rauszufinden, welches die ideale Detailtiefe für ein sinnvolles Ausgeben ist. In der
Praxis werden Ausgabeanweisungen häuig nur einer Methode bzw. einer kleinen
Anzahl an Methoden hinzugefügt, wenn man eine grobe Vorstellung davon hat, in
welchem Bereich des Programms ein Fehler versteckt sein könnte.

9.10 Debugger
In Kapitel 3 haben wir einen Debugger benutzt, um uns eine bestehende Anwen-
dung und die Interaktion ihrer Objekte zu erschließen. Auf sehr ähnliche Weise kön-
nen wir einen Debugger auch benutzen, um Fehler zu finden.
Ein Debugger ist letztendlich ein Softwarewerkzeug, mit dem wir eine kontrollierte
Ausführung eines Quelltextabschnitts durchführen können. Wir setzen typischer-
weise einen Haltepunkt an die Stelle, an der die Ausführung begonnen werden soll,
und benutzen dann die Funktionen SCHRITT oder SCHRITT HINEIN, um eine Sequenz
schrittweise zu durchlaufen.
Ein Vorteil dabei ist, dass der Debugger automatisch die Zustände der beteiligten
Objekte verfolgt, und dies ist schneller und weniger fehlerträchtig als eine manu-
elle Ausführung. Ein Nachteil ist aber auch, dass ein Debugger kein Protokoll der

2 Wir könnten diese Methode sogar in eine spezielle Klasse für die Unterstützung der Fehler-
suche auslagern, aber wir wollen die Dinge in dieser Diskussion einfach halten.

363
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Objektzustände führt und man dadurch nicht ohne Weiteres einen früheren
Objektzustand für einen Vergleich heranziehen kann.
Ein Debugger liefert üblicherweise auch Informationen über die Aufrufsequenz,
auch Stack genannt, zu jedem Zeitpunkt. Die Aufrufsequenz zeigt den Namen
der Methode, deren Anweisungen aktuell ausgeführt werden, den Namen der
Methode, von der aus die aktuelle Methode aufgerufen wurde, den Namen der
Methode, von der aus wiederum diese Methode aufgerufen wurde, und so weiter.
Die Aufrufsequenz zeigt uns also ein Protokoll aller Methoden, die momentan aktiv
und noch nicht beendet sind – ähnlich unserem Vorgehen bei der manuellen Aus-
führung, als wir Markierungen an die Aufrufanweisungen gemacht haben.
In BlueJ wird die Aufrufsequenz im linken Bereich des Debugger-Fensters angezeigt
(Abbildung 9.8). Jeder Methodenname in dieser Liste kann selektiert werden, um
die lokalen Variablen der jeweiligen Methode anzeigen zu lassen.

Abbildung 9.8
Das Debugger-Fenster
von BlueJ, bei dem die
Ausführung bei einem
Haltepunkt gestoppt
wird.

Übung 9.35 Öffnen Sie das Projekt Recheneinheit, setzen Sie einen Halte-
punkt in der ersten Zeile der Methode testPlus der Klasse RecheneinheitTester.
Führen Sie diese Methode aus. Wenn der Debugger erscheint, gehen Sie Schritt
für Schritt durch den Quelltext. Experimentieren Sie sowohl mit den Knöpfen
SCHRITT ÜBER sowie SCHRITT HINEIN.
Übung 9.36 Zusatzaufgabe. In der Praxis könnten Sie zu dem Entschluss
kommen, dass die Version der Recheneinheit von Hacker T. Largebrain zu feh-
lerhaft ist, um repariert zu werden. Schreiben Sie also stattdessen eine völlig
neue Implementierung. Das Projekt Rechner-GUI enthält Klassen, die die gra-
fische Benutzungsschnittstelle aus Abbildung 9.6 anbieten. Sie können dieses
Projekt als Basis für Ihre eigene Implementierung der Klasse Recheneinheit
nutzen. Dokumentieren Sie Ihre neue Klasse sorgfältig und implementieren Sie

364
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.11 Das Debugging von Streams (fortgeschritten)

eine Testklasse mit gründlich durchdachten Testfällen. Die Erfahrungen, die Sie
mit dem Quelltext von Hacker gemacht haben, sollten Ihrem Nachfolger
erspart bleiben! Achten Sie darauf, zum Testen eine spezielle Modultest-Klasse
zu verwenden, anstatt Tests in eine Standardklasse zu schreiben. Wie Sie gese-
hen haben, macht dies das Zusichern von korrekten Ergebnissen viel leichter.

9.11 Das Debugging von Streams


(fortgeschritten)
Alle Debugging-Techniken, die wir in diesem Kapitel beschrieben haben, lassen
sich ebenso gut auf Code anwenden, der Streams verwendet. Der einzige kleine
Unterschied ist die Komplexität der Anweisungen, die Streams benutzen. Es ist
nicht ungewöhnlich, dass eine einzelne Anweisung drei oder mehr Operationen
innerhalb einer Pipeline hintereinanderschaltet, und die Reihenfolge und der logi-
sche Fluss zwischen diesen Operationen sind entscheidend für das korrekte Ergeb-
nis der Anweisung. Daher ist es oft eine gute Idee, den Zustand eines Streams an
Zwischenpunkten der Pipeline zu untersuchen.
Die Methode peek ist eine Stream-Operation, die äquivalent zur Benutzung von
print-Anweisungen ist und es ermöglicht, den Zustand einer Pipeline an ver-
schiedenen Punkten zu untersuchen. Es ist eine intermediäre Operation, die an
jedem beliebigen Punkt innerhalb der Pipeline eingefügt werden kann. Als Para-
meter bekommt peek einen Consumer-Lambda-Ausdruck3, reicht aber auch die
Elemente ihres Eingabe-Streams unverändert an ihren Ausgabe-Stream durch.
Nehmen wir beispielsweise an, Sie überarbeiten im Projekt Tiermonitor einen
einigermaßen komplexen Code – der von jemand anderem geschrieben wurde.
Der Code soll eine Liste von Sichtungen für ein bestimmtes Tier, ein Gebiet und
einen Melder erzeugen. Möglicherweise wurde dies folgendermaßen in einer
einzigen Anweisung untergebracht:
List<Sichtung> ergebnis =
[Link]()
.filter(record -> tier == [Link]() &&
gebiet == [Link]() &&
melder == [Link]())
.collect([Link]());
Stellen Sie sich vor, dass die Liste, die dieser Code erzeugt, immer leer ist, auch
wenn Sie Testdaten mit Sichtungsdatensätzen verwenden, die definitiv überein-
stimmen müssten. Offensichtlich stimmt irgendetwas mit der filter-Operation
nicht, aber Sie könnnen das Problem nicht direkt ausfindig machen. Einer der
leichtesten Wege, der Sache auf den Grund zu gehen, wäre es, das Filtern in drei
seperate Operationen aufzuspalten und die Datenfelder der verbleibenden Ele-
mente nach jedem Filterschritt auszugeben. Die folgende Version macht genau
dieses:

3 Zum Typ Consumer, siehe Abschnitt 12.6.7.

365
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

List<Sichtung> ergebnis =
[Link]()
.filter(datensatz -> tier == [Link]())
.peek(r -> [Link]([Link]())
.filter(datensatz -> gebiet == [Link]()
.peek(r -> [Link]([Link]())
.filter(datensatz -> melder == [Link]())
.peek(r -> [Link]([Link]())
.collect([Link]());
Mit dieser Version werden Sie recht wahrscheinlich herausfinden, dass der erste
Filterschritt überhaupt keine Datensätze an die nächste Operation weitergibt,
weil der Test auf textuelle Übereinstimmung der Zeichenketten durch die equals-
Methode nicht durchgeführt wurde. Sobald dies behoben ist, können die Tests
erneut ausgeführt werden, dann kann die peek-Methode entfernt werden.
Eine peek-Operation eignet sich auch, um eine Position in der Mitte einer Pipe-
line-Folge als Haltepunkt für einen Debugger zu setzen. In diesem Fall sind Sie
vermutlich eher an den Zuständen der Objekte interessiert, anstatt daran etwas
auszugeben. Daher würde man hier einen Consumer-Lambda-Ausdruck, der nichts
macht, als Parameter für peek benutzen, zum Beispiel:
peek(r -> { })

9.12 Die Wahl der richtigen Teststrategie


Wie Sie sehen konnten, gibt es verschiedene Teststrategien: die handschriftliche
oder mündliche Ausführung, das Einfügen von Ausgabeanweisungen (temporär
oder permanent und durch Schalter gesteuert), das interaktive Testen mithilfe der
Objektleiste, das Schreiben eigener Testklassen oder den Einsatz einer speziellen
Modultest-Klasse.
In der täglichen Praxis verwenden wir je nach Situation unterschiedliche Teststrate-
gien. Manuelle Ausführung, Ausgabeanweisungen und interaktives Testen eignen
sich insbesondere für das Testen von neu geschriebenen Quelltexten, das Analysie-
ren der Funktionsweise eines Programmabschnitts oder für die Fehlersuche. Der
Vorzug dieser Techniken ist, dass sie schnell und leicht einzusetzen sind, in jeder
Programmiersprache eingesetzt werden können und (mit Ausnahme des interakti-
ven Testens) nicht von der verwendeten Umgebung abhängig sind. Ihr größter
Nachteil ist, dass sie nicht leicht zu wiederholen sind. Das ist für die Fehlersuche in
Ordnung, aber zum Testen benötigen wir etwas Besseres: Wir benötigen einen
Mechanismus, der uns erlaubt, die Regressionstests schnell zu wiederholen. Die
Verwendung von Modultest-Klassen hat dagegen den Vorteil, dass die einmal ein-
gerichteten Tests beliebig oft wiederholt werden können.
Der von Hacker eingeschlagene Weg, seine eigenen Testklassen zu schreiben, war
also ein Schritt in die richtige Richtung. Er war nur zu kurz gegriffen. Denn wie wir
mittlerweile wissen, genügt es nicht, wenn die Klassen ausreichend Methodenaufrufe
zum Testen enthalten, die Klassen müssen die Ergebnisse der Methode auch aus-
werten und erkennen, wenn ein Test gescheitert ist. Durch Einsatz einer passenden
Modultest-Klasse hätte dieses Problem besser und leichter gelöst werden können.

366
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.13 Techniken umsetzen

Übung 9.37 Öffnen Sie Ihr Projekt erneut und verbessern Sie die Testunter-
stützung, indem Sie die Testklasse von Hacker durch eine Modultest-Klasse
ersetzen, die Recheneinheit zugeordnet ist. Richten Sie ähnliche Tests ein,
wie Hacker sie nutzte, fügen Sie gegebenenfalls weitere Tests hinzu, die Sie
für sinnvoll erachten, und achten Sie auf korrekte Zusicherungen.

9.13 Techniken umsetzen


Die in diesem Kapitel vorgestellten Techniken können benutzt werden, um ein
neues Programm zu verstehen oder um es auf Fehler zu untersuchen. Das Projekt
Ziegelsteine bietet Ihnen die Möglichkeit, diese Techniken in einem neuen Szena-
rio auszuprobieren. Das Projekt enthält Teile einer Anwendung für eine Firma, die
Ziegelsteine herstellt. Ziegelsteine werden an Kunden auf Paletten (als Stapel von
Ziegeln) ausgeliefert. Die Klasse Palette bietet Methoden, mit denen die Höhe
und das Gewicht einer Palette in Abhängigkeit von der Anzahl der Ziegel auf der
Palette abgefragt werden können.

Übung 9.38 Öffnen Sie das Projekt Ziegelsteine. Testen Sie es. Es gibt min-
destens vier Fehler in diesem Projekt. Versuchen Sie, diese zu finden und zu
beheben. Welche Techniken haben Sie zur Fehlersuche angewendet? Wel-
che Technik war am hilfreichsten?

367
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden

Zusammenfassung
Wenn wir Software schreiben, dann sollten wir damit rechnen, dass sie logi-
sche Fehler enthält. Aus diesem Grund ist es wichtig, sowohl das Testen als
auch die Fehlerbeseitigung als normale Aktivitäten im gesamten Entwick-
lungsprozess anzusehen. BlueJ unterstützt interaktive Modultests von Klas-
sen und Methoden. Wir haben uns auch einige grundlegende Techniken für
die Automatisierung von Tests und für die einfache Fehlerbeseitigung ange-
sehen.
Gute JUnit-Tests für unsere Klassen zu schreiben, stellt sicher, dass Fehler
früh entdeckt werden, und sie geben einen guten Hinweis darauf, in wel-
chem Teil des Systems ein Fehler liegen könnte, was die anschließende Feh-
lersuche viel einfacher macht.

NEUE BEGRIFFE IN DIESEM KAPITEL


Syntaxfehler, logischer Fehler, Testen, Fehlerbeseitigung, Modultest, JUnit,
positives Testen, negatives Testen, Regressionstest, manuelle Ausführung,
Aufrufsequenz

Zusammenfassung der Konzepte


 Testen Testen dient der Überprüfung, ob ein Stück Software (eine Methode,
eine Klasse oder ein Programm) das gewünschte Verhalten zeigt.
 Fehlerbeseitigung Fehlerbeseitigung bezeichnet die Suche nach der
Ursache eines Fehlers und seine Beseitigung.
 Modultests bezeichnen die Tests von einzelnen Teilen einer Anwen-
dung, wie Methoden und Klassen.
 Positives Testen Positives Testen ist das Testen der Fälle, die funktionie-
ren sollten.
 Negatives Testen Negatives Testen ist das Testen der Fälle, die fehlschla-
gen sollten.
 Testautomatisierung vereinfacht das Durchführen der Regressionstests.
 Zusicherung Ein boolescher Ausdruck für eine Bedingung, von der
erwartet wird, dass sie wahr ist.
 Testgerüst Eine Menge von Objekten, die als Grundlage für einen Modul-
test dienen.
 Manuelle Ausführung Eine manuelle Ausführung ist das zeilenweise
Durchgehen durch einen Quelltextabschnitt, bei dem Zustandsänderun-
gen und das Verhalten einer Anwendung beobachtet werden.

368
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
TEIL

II Anwendungsstrukturen

10 Bessere Struktur durch Vererbung ................................................. 371


11 Mehr über Vererbung.......................................................................... 403
12 Weitere Techniken zur Abstraktion ................................................ 429
13 Grafische Benutzungsoberflächen .................................................. 475
14 Fehlerbehandlung ................................................................................ 527
15 Entwurf von Anwendungen .............................................................. 573
16 Eine Fallstudie ....................................................................................... 597

Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.


Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

10 Bessere Struktur durch


Vererbung

Lernziele
Zentrale Konzepte in diesem Kapitel: Vererbung, Subtyping, Ersetzbar-
keit, polymorphe Variablen
Java-Konstrukte in diesem Kapitel: extends, super (in Konstruktoren),
Cast-Operator, Object

In diesem Kapitel stellen wir einige weitere objektorientierte Konzepte vor, mit
denen wir die Struktur unserer Anwendungen verbessern können. Die wichtigsten
dieser Konzepte sind Vererbung und Polymorphie.
Beide Konzepte sind von zentraler Bedeutung für die Objektorientierung. Sie
werden später feststellen, dass sie in unterschiedlicher Form in allen folgenden
Betrachtungen auftauchen werden. Es sind jedoch nicht nur die folgenden Kapi-
tel, die auf diesen Konzepten aufsetzen. Viele der Konstrukte und Techniken aus
den vorigen Kapiteln sind durch Aspekte von Vererbung und Polymorphie beein-
flusst; wir werden einige Punkte aus den vorigen Abschnitten wieder aufgreifen,
um ein besseres Verständnis der Zusammenhänge zwischen den verschiedenen
Teilen der Programmiersprache Java zu bekommen.
Vererbung ist ein mächtiges Konstrukt, mit dem Lösungen zu einer Vielfalt von Pro-
blemen formuliert werden können. Wie immer werden wir die wichtigen Aspekte
anhand eines Beispiels diskutieren. Bei diesem Beispiel werden wir anfangs nur
einige der Probleme ansprechen, die mit Vererbungsstrukturen gelöst werden kön-
nen, und weitere Benutzungsformen und Vorteile von Vererbung und Polymorphie
später aufgreifen.
Das Beispiel, das wir hier diskutieren werden, heißt Netzwerk.

371
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

10.1 Das Beispiel „Netzwerk“


Das Projekt Netzwerk implementiert einen Prototyp von einem kleinen Teil einer
Anwendung für ein soziales Netzwerk. Wir konzentrieren uns hier auf den News-
feed-Teil – die Liste der Nachrichten, die auf dem Bildschirm erscheinen sollte,
wenn ein Benutzer die Hauptseite des Netzwerkes öffnet.
Wir fangen klein und einfach an und haben dabei im Blick, dass wir die Anwen-
dung später erweitern und ausbauen werden. Am Anfang erscheinen in unserem
Newsfeed nur zwei Arten von Einsendungen: Texteinsendungen (die wir später
als Nachrichten bezeichnen) und Fotoeinsendungen, die aus einem Foto und einer
Überschrift bestehen.
Für den Teil der Anwendung, der diese Einsendungen speichert und anzeigt, wollen
wir hier einen Prototyp erstellen. Die Funktionalität dieses Prototyps sollte min-
destens Folgendes umfassen:
 Er sollte uns erlauben, Text- und Fotoeinsendungen zu erzeugen.
 Texteinsendungen bestehen aus einer Nachricht beliebiger Länge, möglicher-
weise mehrzeilig. Fotoeinsendungen bestehen aus einem Bild und einer Über-
schrift. Zu jeder Einsendung werden zusätzliche Details gespeichert.
 Diese Informationen sollten permanent gespeichert werden, damit sie später
weiterverwendet werden können.
 Eine Suchfunktion sollte uns erlauben, beispielsweise alle Einsendungen von
einem bestimmten Benutzer oder alle Fotos eines bestimmten Datumsbereichs
zu finden.
 Listen sollten ausgegeben werden können, etwa eine Liste der letzten Einsen-
dungen oder eine Liste aller Einsendungen von einem bestimmten Benutzer.
 Informationen sollten auch wieder gelöscht werden können.
Folgende Details einer Nachrichteneinsendung wollen wir erfassen:
 den Benutzernamen des Autors
 den Text der Nachricht
 einen Zeitstempel (Versendezeitpunkt)
 wie vielen Lesern die Einsendung gefällt
 eine Liste der Kommentare zu dieser Einsendung von anderen Benutzern
Und folgende Details sollen für eine Fotoeinsendung erfasst werden:
 der Benutzername des Autors
 der Dateiname des anzuzeigenden Bildes
 die Überschrift für das Foto (eine Textzeile)
 ein Zeitstempel (Versendezeitpunkt)
 wie vielen Lesern die Einsendung gefällt
 eine Liste der Kommentare zu dieser Einsendung von anderen Benutzern

372
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“

10.1.1 Die Klassen und Objekte in Netzwerk


Um diese Anwendung zu implementieren, müssen wir uns zuerst überlegen, mit
welchen Klassen wir sie modellieren. Einige der Klassen sind in diesem Fall leicht
zu identifizieren. Wir können recht gradlinig eine Klasse NachrichtenEinsendung
für Nachrichten definieren und eine Klasse FotoEinsendung für Fotos.
Die Objekte dieser Klassen sollten dann all die Daten kapseln, die wir über diese
Objekte speichern wollen (Abbildung 10.1).

Abbildung 10.1
: NachrichtenEinsendung : FotoEinsendung Die Datenfelder
in Nachrichten-
benutzername benutzername
Einsendung- und
nachricht dateiname FotoEinsendung-
zeitstempel ueberschrift Objekten.

gefielWieOft zeitstempel

kommentare gefielWieOft

kommentare

Für einige dieser Datenwerte sollten wir auch verändernde und sondierende Metho-
den definieren (Abbildung 10.2).1 Für unsere Zwecke ist es momentan nicht so
wichtig, dies in allen Details vorzunehmen, sondern es geht nur um einen ersten
Eindruck vom Entwurf der Anwendung. In dieser Abbildung haben wir sondierende
und verändernde Methoden für die Felder definiert, die sich im Laufe der Zeit ver-
ändern können (eine Einsendung als „gefällt“ oder „gefällt nicht“ markieren und
einen Kommentar hinzufügen), und nehmen an, dass alle anderen Werte im Kons-
truktor gesetzt werden. Wir haben auch eine Methode anzeigen definiert, die die
Details eines NachrichtenEinsendung- oder eines FotoEinsendung-Objekts zeigt.
Abbildung 10.2
NachrichtenEinsendung FotoEinsendung
Details der Klassen
benutzername benutzername
Datenfelder im
Nachrichten-
nachricht dateiname
zeitstempel ueberschrift oberen Bereich Einsendung und
gefielWieOft zeitstempel FotoEinsendung.
kommentare gefielWieOft
kommentare
gefaellt
gefaelltNicht gefaellt
erfasseKommentar gefaelltNicht
gibtText erfasseKommentar
gibZeitstempel gibtBilddateiname
anzeigen gibUeberschrift
gibZeitstempel Methoden im
anzeigen unteren Bereich

1 Die Notation für Klassendiagramme, die in diesem Buch und in BlueJ verwendet wird, benutzt
eine Untermenge der weitverbreiteten Notation UML. Obwohl wir (bei Weitem) nicht alles aus
der UML benutzen, verwenden wir die UML-Notation für all die Teile, die wir tatsächlich zeigen.
Die UML definiert, wie Datenfelder und Methoden in einem Klassendiagramm gezeigt werden:
Das Klassensymbol wird in drei Teile unterteilt, die (von oben nach unten) den Klassennamen,
die Datenfelder und die Methoden enthalten.

373
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Sobald wir die Klassen NachrichtenEinsendung und FotoEinsendung definiert haben,


können wir so viele Objekte davon erzeugen, wie wir möchten – eines für jede
Nachrichten- oder Fotoeinsendung, die wir speichern wollen. Abgesehen von die-
sen Klassen brauchen wir noch eine weitere Klasse, von der wir ein Objekt erzeugen
können, und zwar ein Objekt für alle Newsfeeds, das eine Sammlung von Nachrich-
teneinsendungen und eine Sammlung von Fotoeinsendungen halten kann. Dafür
werden wir eine Klasse namens NewsFeed erzeugen.
Das NewsFeed-Objekt selbst könnte zwei Sammlungen halten (beispielsweise von
den Typen ArrayList<NachrichtenEinsendung> und ArrayList<FotoEinsendung>). Die
eine könnte dann alle Nachrichteneinsendungen halten, die andere alle Fotoein-
sendungen. Ein Objektdiagramm für dieses Modell zeigt Abbildung 10.3.
Abbildung 10.3
: NewsFeed ArrayList<NachrichtenEinsendung>
Die Objekte in der
nachrichten
Anwendung Netzwerk.
fotos
: NachrichtenEinsendung : NachrichtenEinsendung : NachrichtenEinsendung : NachrichtenEinsendung

ArrayList<FotoEinsendung>

FotoEinsendung FotoEinsendung FotoEinsendung FotoEinsendung

Das zugehörige Klassendiagramm, im Stil wie es ähnlich auch von BlueJ angezeigt
wird, ist in Abbildung 10.4 zu sehen. Beachten Sie, dass BlueJ ein leicht vereinfach-
tes Diagramm zeigt: Klassen aus der Standardbibliothek (ArrayList in diesem Fall)
werden nicht gezeigt. Stattdessen konzentriert sich das Diagramm auf benutzer-
definierte Klassen. Außerdem werden von BlueJ keine Namen von Datenfeldern
und Methoden im Diagramm angezeigt.
Abbildung 10.4
NewsFeed
Das Klassendiagramm
für Netzwerk.

NachrichtenEinsendung

FotoEinsendung

In der Praxis würden wir, um die ganze Netzwerk-Anwendung zu implementie-


ren, einige weitere Klassen benötigen, die sich um das Speichern der Daten in
Dateien kümmern und eine Benutzungsschnittstelle anbieten. Diese sind nicht so
wichtig für die aktuelle Diskussion, weshalb wir sie vorläufig nicht weiter ausfüh-
ren werden; stattdessen konzentrieren wir uns auf eine detaillierte Betrachtung
der beschriebenen Kernklassen.

374
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“

10.1.2 Der Quelltext von Netzwerk


Bis jetzt war der Entwurf der drei Klassen (NachrichtenEinsendung, FotoEinsendung
und NewsFeed) recht gradlinig. Die Übersetzung dieses Entwurfs in Java-Quelltext ist
ähnlich einfach. Listing 10.1 zeigt den Quelltext der Klasse NachrichtenEinsendung.
Er definiert die notwendigen Datenfelder, setzt alle Werte, die sich über die Zeit
nicht verändern sollen, im Konstruktor und bietet verändernde und sondierende
Methoden. Er implementiert auch eine erste, sehr einfache Version der Methode
anzeigen, die Einsendungen auf der Konsole zeigt.

Listing 10.1
Der Quelltext der
Klasse Nachrichten-
Einsendung.

375
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Auf einige Details wollen wir hier eingehen:


 Es wurde einiges vereinfacht. Zum Beispiel werden Kommentare für eine Einsen-
dung als Zeichenketten gespeichert. In einer vollständigeren Version würden wir

376
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“

wahrscheinlich eine eigene Klasse für Kommentare erzeugen, da auch Kommen-


tare zusätzliche Merkmale aufweisen, wie einen Autor und eine Zeit. Die Anzahl
gefielWieOft wird als einfache ganze Zahl gespeichert. Wir halten im Moment
nicht fest, welchem Benutzer eine Einsendung gefiel. Auch wenn unser Prototyp
aufgrund dieser Vereinfachungen unvollständig ist, sind die Vereinfachungen für
unser Hauptanliegen hier nicht von Belang, sodass wir im Moment alles so belas-
sen.
 Der Zeitstempel wird als eine Zahl vom Typ long gespeichert. Dies ist allgemein
üblich. Wir können uns leicht vom Java-System die Systemzeit als long-Wert in
Millisekunden beschaffen. Außerdem haben wir eine kurze Methode zeitString
geschrieben, um diese Zahl in einen relativen Zeitstring umzuwandeln, wie „vor
5 Minuten“. In der endgültigen Fassung müsste das System die Echtzeit anstelle
der Systemzeit verwenden; doch auch hier gilt, dass die Systemzeit für unseren
bisherigen Prototyp absolut ausreicht.
Beachten Sie, dass wir uns momentan nicht bemühen, die Implementierung in
irgendeiner Weise zu komplettieren. Sie dient lediglich dazu, uns ein Gefühl für
eine solche Klasse zu vermitteln. Wir werden sie als Basis für die folgende Diskus-
sion über Vererbung verwenden. Lassen Sie uns nun den Quelltext der Klasse
NachrichtenEinsendung mit dem Quelltext der Klasse FotoEinsendung vergleichen,
der in Listing 10.2 gezeigt wird. Wir stellen schnell fest, dass sie sehr ähnlich sind.
Das ist nicht überraschend, denn ihre Zwecke sind sehr ähnlich: Beide werden zur
Speicherung von Informationen über Newsfeed-Einsendungen benutzt (und die
verschiedenen Arten von Einsendungen haben viel gemeinsam). Sie unterscheiden
sich nur in Details, beispielsweise in einigen ihrer Datenfelder und den dazugehöri-
gen sondierenden Methoden sowie in den Rümpfen der anzeigen-Methoden.

Listing 10.2
Der Quelltext
der Klasse
FotoEinsendung.

377
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

378
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“

Als Nächstes sehen wir uns den Quelltext der Klasse NewsFeed an (Listing 10.3). Auch
dieser ist sehr übersichtlich. Er definiert zwei Listen (beide basierend auf der Klasse
ArrayList) für das Halten der Nachrichteneinsendungen und der Fotoeinsendungen.
Im Konstruktor werden leere Listen angelegt. Die Klasse bietet dann zwei Metho-
den zum Erfassen von Elementen – eine zum Erfassen von Nachrichteneinsen-
dunge, die andere zum Erfassen von Fotoeinsendungen. Die letzte Methode, zei-
gen, gibt eine Liste aller Nachrichten- und Fotoeinsendungen auf der Konsole aus.
Beachten Sie, dass dies bei Weitem noch keine fertige Anwendung ist. Sie hat
noch keine Benutzungsschnittstelle (und ist deshalb außerhalb von BlueJ nicht
benutzbar) und die eingegebenen Daten werden noch nicht im Dateisystem oder
in deiner Datenbank gespeichert. Das bedeutet, dass alle eingegebenen Daten
beim Beenden der Anwendung verloren gehen. Es gibt keine Funktionen, um die
angezeigten Liste von Einsendungen z.B. nach Datum und Zeit oder nach Wich-
tigkeit zu sortieren. Zurzeit erhalten wir immer zuerst die Nachrichten, und zwar
in der Reihenfolge, in der sie eingegeben wurden, und danach die Fotos. Außer-
dem sind die Möglichkeiten zur Eingabe und Bearbeitung von Daten sowie die
Such- und Anzeigefunktionen nicht flexibel genug für reale Anforderungen.
Aber für unsere Zwecke ist das nicht wichtig. Wir können diese Punkte später ver-
bessern. Die grundlegende Struktur ist jedoch vorhanden und funktioniert. Dies
reicht uns, um die Probleme und möglichen Verbesserungen dieses Entwurfs zu
diskutieren.

379
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Listing 10.3
Der Quelltext der
Klasse NewsFeed.

380
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“

Übung 10.1 Öffnen Sie das Projekt Netzwerk-V1. Es enthält genau die Klas-
sen, die wir bisher diskutiert haben. Erzeugen Sie einige NachrichtenEinsen-
dung- und FotoEinsendung-Objekte. Erzeugen Sie ein NewsFeed-Objekt. Geben
Sie die Einsendungen in den Newsfeed ein und lassen Sie dann den Inhalt
des Newsfeed auflisten.
Übung 10.2 Versuchen Sie Folgendes: Erzeugen Sie ein NachrichtenEinsen-
dung-Objekt. Tragen Sie es in den Newsfeed ein und lassen Sie dann dessen
Inhalt anzeigen. Sie sehen, dass für die Einsendung kein Kommentar ange-
geben ist. Fügen Sie dem NachrichtenEinsendung-Objekt (das Sie in den
Newsfeed eingetragen haben) auf der Objektleiste nun einen Kommentar
hinzu. Wenn Sie anschließend den Newsfeed erneut auflisten lassen: Hat
die aufgeführte Einsendung dann einen Kommentar? Probieren Sie es aus.
Erklären Sie das Verhalten, das Sie beobachten.

10.1.3 Diskussion über Netzwerk


Obwohl unsere Anwendung noch nicht vollständig ist, haben wir den wichtigsten
Teil vorliegen. Wir haben den Kern der Anwendung definiert: die Datenstruktur, die
alle wichtigen Informationen hält.

Das war bisher recht einfach, wir könnten jetzt fortfahren und die fehlenden Teile
entwerfen. Bevor wir dies tun, sollten wir allerdings noch einmal die Qualität unserer
bisherigen Lösung betrachten.
Es gibt einige fundamentale Probleme mit der vorliegenden Lösung. Das offen-
sichtlichste Problem ist die Code-Duplizierung.

Wir haben oben bereits festgestellt, dass die Klassen NachrichtenEinsendung und
FotoEinsendung einander sehr ähnlich sind. Tatsächlich sind ihre Quelltexte, bis auf
wenige Ausnahmen, weitgehend identisch. Wir haben die Probleme mit Code-
Duplizierung bereits in Kapitel 8 angesprochen. Abgesehen von dem ärgerlichen
Umstand, dass wir alles zweimal schreiben müssen (oder kopieren und einfügen
und dann die Unterschiede anpassen), gibt es häufig Probleme bei der Wartung
von dupliziertem Code. Viele mögliche Änderungen müssen an zwei Stellen vorge-
nommen werden. Wenn beispielsweise der Typ des Kommentars von Array-
List<String> auf ArrayList<Comment> geändert wird (damit mehr Details gespeichert
werden können), dann muss diese Änderung einmal in der Klasse Nachrichten-
Einsendung vorgenommen werden und einmal in der Klasse FotoEinsendung. Ver-
bunden mit der Wartung von dupliziertem Quelltext ist immer auch die Gefahr von
Fehlern, denn einem Wartungsprogrammierer ist möglicherweise nicht bewusst,
dass er die gleiche Änderung an weiteren Stellen vornehmen muss.
Es gibt eine weitere Stelle mit Code-Duplizierung: in der Klasse NewsFeed. Wir sehen,
dass in dieser Klasse alles doppelt getan wird – einmal für Nachrichteneinsendungen
und einmal für Fotoeinsendungen. Die Klasse definiert zwei Listenvariablen, erzeugt
zwei Listenobjekte, bietet zwei Methoden zum Erfassen und hat zwei fast identi-
sche Quelltextabschnitte in der Methode zeigen.

381
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Die Probleme mit dieser Duplizierung werden offensichtlich, wenn wir untersu-
chen, welche Änderungen für die Einführung einer weiteren Einsendung not-
wendig wären. Nehmen Sie an, wir wollen nicht nur Informationen über Nach-
richteneinsendungen und Fotoeinsendungen verwalten, sondern auch über
Aktivitätseinsendungen. Aktivitätseinsendungen können automatisch erzeugt
werden und informieren uns über eine Aktivität einer unserer Kontakte, wie
„Fred hat sein Profilfoto geändert“ oder „Jakob ist jetzt Freund von Laura“. Akti-
vitätseinsendungen scheinen ähnlich genug zu sein, um mit vertretbarem Auf-
wand integriert werden zu können. Wir würden eine weitere Klasse Aktivitaet-
Einsendung einführen und eine dritte Version des Quelltextes erstellen, den wir
schon in den Klassen NachrichtenEinsendung und FotoEinsendung haben. Dann
müssten wir uns durch den Quelltext der Klasse NewsFeed arbeiten und eine wei-
tere Listenvariable einführen sowie ein weiteres Listenobjekt, eine weitere
Methode und eine weitere Schleife in der Methode zeigen.

Das Gleiche müssten wir für eine vierte Einsendung tun. Je weiter wir dies treiben,
desto ärger wird das Problem der Code-Duplizierung und damit der Aufwand für
spätere Änderungen. Wenn wir uns mit einer solchen Situation nicht wohlfühlen,
dann ist das ein Hinweis darauf, dass es vielleicht eine bessere Alternative gibt. Für
diesen speziellen Fall bieten objektorientierte Sprachen ein besonderes Konzept an,
das große Auswirkungen auf Programme hat, in denen sich stark ähnelnde Klassen
vorkommen. In den folgenden Abschnitten werden wir dieses Konzept vorstellen:
Vererbung.

10.2 Einsatz von Vererbung


Konzept Vererbung ist ein Mechanismus, mit dem wir unser Problem der Code-Duplizie-
rung lösen können. Die Idee ist einfach: Statt die beiden Klassen Nachrichten-
Vererbung
Einsendung und FotoEinsendung völlig unabhängig voneinander zu definieren, defi-
erlaubt uns, eine
Klasse als Erweite- nieren wir zuerst eine Klasse, die die Gemeinsamkeiten der beiden Klassen
rung einer anderen zusammenfasst. Wir nennen diese Klasse Einsendung. Wir können dann definieren,
zu definieren. dass eine NachrichtenEinsendung eine Einsendung ist und dass eine FotoEinsendung
eine Einsendung ist. Schließlich fügen wir die spezifischen Eigenschaften einer
Nachrichteneinsendung in die Klasse NachrichtenEinsendung ein und die spezifischen
Fotoeinsendung-Eigenschaften in die Klasse FotoEinsendung. Der grundlegende Vor-
teil dieser Technik ist, dass wir gemeinsame Eigenschaften nur einmal beschrei-
ben müssen.
Abbildung 10.5 zeigt ein Klassendiagramm mit der neuen Struktur. Es zeigt die
Klasse Einsendung, die alle Datenfelder und Methoden definiert, die Nachrichtenein-
sendungen und Fotoeinsendungen gemein sind. Unterhalb der Klasse Einsendung
sind die Klassen NachrichtenEinsendung und FotoEinsendung zu sehen, die nur dieje-
nigen Datenfelder und Methoden definieren, die spezifisch für die jeweilige Einsen-
dung sind.

382
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.2 Einsatz von Vererbung

Abbildung 10.5
Einsendung Nachrichten-
benutzername Einsendung und
zeitstempel FotoEinsendung
gefielWieOft erben von
kommentare
Einsendung.
gefaellt
gefaelltNicht
erfasseKommentar
gibZeitstempel
anzeigen

NachrichtenEinsendung FotoEinsendung

nachricht dateiname
ueberschrift
gibText
gibtBilddateiname
gibUeberschrift

Für dieses neu eingeführte Konzept der objektorientierten Programmierung brau- Konzept
chen wir auch eine passende Terminologie. In einer Situation wie dieser sagen
Eine Super-
wir: „Die Klasse NachrichtenEinsendung erbt von der Klasse Einsendung.“ Die Klasse
klasse ist eine
FotoEinsendung erbt ebenfalls von der Klasse Einsendung. Wenn wir über Java-Pro- Klasse, die von
gramme sprechen, sagen wir auch manchmal, dass die Klasse NachrichtenEinsendung anderen Klassen
die Klasse Einsendung erweitert, da in Java das Schlüsselwort extends (englisch für erweitert wird.
„erweitert“) für die Definition dieser Beziehung verwendet wird (wie wir bald
sehen werden). Die Pfeile im Klassendiagramm (üblicherweise mit einer nicht gefüll-
ten Pfeilspitze gezeichnet) repräsentieren diese Vererbungsbeziehung.
Die Klasse Einsendung (von der die beiden anderen Klassen erben) wird Superklasse Konzept
oder Oberklasse genannt. Die erbenden Klassen (NachrichtenEinsendung und Foto-
Eine Subklasse
Einsendung) werden als Subklassen oder Unterklassen bezeichnet. In diesem Buch
ist eine Klasse, die
werden wir die Begriffe Superklasse und Subklasse verwenden, wenn wir uns auf eine andere Klasse
Klassen in einer Vererbungsbeziehung beziehen. erweitert bzw. von
dieser Klasse erbt.
Die Vererbungsbeziehung wird manchmal auch als Ist-ein-Beziehung bezeichnet,
Sie erbt alle Daten-
weil eine Subklasse als eine Spezialisierung ihrer Superklasse angesehen werden felder und Metho-
kann. Wir können sagen, „eine Nachrichteneinsendung ist eine Einsendung“ den von ihrer
und „eine Fotoeinsendung ist eine Einsendung“. Superklasse.
Der Grund für die Verwendung von Vererbung ist nun ziemlich offensichtlich. Die
Instanzen der Klasse NachrichtenEinsendung werden alle Datenfelder haben, die in
der Klasse NachrichtenEinsendung und der Klasse Einsendung definiert sind (Nachrich-
tenEinsendung erbt die Datenfelder von Einsendung). Instanzen der Klasse FotoEin-
sendung werden alle Datenfelder der Klassen FotoEinsendung und Einsendung haben.
Wir erzielen also das gleiche Ergebnis wie vorher, müssen die Datenfelder benut-
zername, zeitstempel, gefielWieOft und kommentare jedoch nur einmal definieren
(können sie aber in zwei verschiedenen Zusammenhängen benutzen).

383
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Das Gleiche gilt für Methoden: Instanzen von Subklassen verfügen über alle Metho-
den, die in der Superklasse und der Subklasse definiert sind. Allgemein können
wir sagen: Da eine Nachrichteneinsendung eine Einsendung ist, hat eine Nach-
richteneinsendung alle Eigenschaften einer Einsendung und einige mehr. Und
weil eine Fotoeinsendung eine Einsendung ist, hat sie auch alle Eigenschaften
einer Einsendung und einige mehr.
Vererbung erlaubt uns somit, zwei Klassen mit großen Ähnlichkeiten zu definieren,
ohne dass wir die gemeinsamen Teile zweimal formulieren müssen. Vererbung bie-
tet einige weitere Vorteile, die wir später diskutieren werden. Vorher werfen wir
aber noch einen ausführlicheren Blick auf Vererbungshierarchien.

10.3 Vererbungshierarchien
Konzept Vererbung kann sehr viel allgemeiner eingesetzt werden, als wir es in unserem Bei-
spiel tun. Es können mehr als zwei Klassen von einer Superklasse erben und eine
Klassen, die über
Subklasse kann wiederum selbst Superklasse weiterer Subklassen sein. Die Klassen
Vererbungsbezie-
hungen miteinan- bilden dann eine Vererbungshierarchie.
der verknüpft sind, Das bekannteste Beispiel einer Vererbungshierarchie ist vermutlich die Taxonomie
bilden eine Ver- der Arten in der Biologie. Ein kleiner Ausschnitt wird in Abbildung 10.6 gezeigt.
erbungshie-
Wir können erkennen, dass ein Pudel ein Hund ist, ein Hund ein Säugetier und
rarchie.
ein Säugetier wiederum ein Tier.

Abbildung 10.6
Ein Beispiel für eine
Vererbungshierarchie.

Wir wissen einige Dinge über Pudel, beispielsweise dass sie lebendig sind, bellen
können, Fleisch fressen und lebend gebären. Bei näherer Betrachtung erkennen
wir, dass wir einige dieser Dinge nicht deshalb wissen, weil sie Pudel sind, son-
dern Hunde, Säugetiere und Tiere. Eine Instanz der Klasse Pudel (ein spezifischer
Pudel) besitzt alle Eigenschaften eines Pudels, eines Hundes, eines Säugetiers und
eines Tiers, denn ein Pudel ist ein Hund, ein Säugetier und so weiter.

384
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.4 Vererbung in Java

Das Prinzip ist einfach. Vererbung ist ein Abstraktionsmittel, mit dem wir Klassen
von Objekten nach bestimmten Kriterien kategorisieren und die Charakteristika
dieser Klassen festlegen können.

Übung 10.3 Zeichnen Sie eine Vererbungshierarchie der Personen in der Ins-
titution, in der Sie lernen oder arbeiten. Wenn Sie beispielsweise Student an
einer Uni sind, dann wird es vermutlich Studenten geben (Studenten im Grund-
und Hauptstudium, Diplomanden, …), Professoren, Tutoren, Verwaltungsmit-
arbeiter usw.

10.4 Vererbung in Java


Bevor wir uns Vererbung genauer ansehen, wollen wir kurz betrachten, wie Verer-
bung in Java ausgedrückt wird. Hier ist ein Ausschnitt aus dem Quelltext der Klasse
Einsendung:
public class Einsendung
{
private String benutzername; // Benutzername des Senders
private long zeitstempel;
private int gefielWieOft;
private ArrayList<String> kommentare;
// Konstruktoren und Methoden hier ausgelassen
}
An dieser Klasse ist so weit nichts außergewöhnlich. Sie beginnt wie jede Klassen-
definition und legt auf die übliche Weise ihre Datenfelder fest. Wir werfen nun
einen Blick auf den Quelltext der Klasse NachrichtenEinsendung:
public class NachrichtenEinsendung extends Einsendung
{
private String nachricht;
// Konstruktoren und Methoden hier ausgelassen
}
Zwei Dinge sind hier anzumerken. Erstens wird die Vererbungsbeziehung durch das
Schlüsselwort extends definiert. Die Klausel extends Einsendung gibt an, dass diese
Klasse eine Subklasse der Klasse Einsendung ist. Zweitens definiert die Klasse Nach-
richtenEinsendung nur die Datenfelder, die spezifisch für NachrichtenEinsendung-
Objekte sind (in diesem Fall nur nachricht). Die Datenfelder von Einsendung werden
geerbt und brauchen nicht erneut aufgeführt zu werden. Objekte der Klasse Nach-
richtenEinsendung haben dennoch Datenfelder wie benutzername, zeitstempel etc.

Als Nächstes werfen wir einen Blick auf den Quelltext der Klasse FotoEinsendung:
public class FotoEinsendung extends Einsendung
{
private String dateiname;
private String ueberschrift;
// Konstruktoren und Methoden hier ausgelassen
}

385
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Diese Klasse folgt dem gleichen Muster wie die Klasse NachrichtenEinsendung. Sie
deklariert sich über das Schlüsselwort extends als eine Subklasse von Einsendung
und definiert ihre eigenen zusätzlichen Datenfelder.

10.4.1 Vererbung und Zugriffsrechte


Für Objekte anderer Klassen erscheinen Objekte der Klassen NachrichtenEinsendung
und FotoEinsendung wie alle anderen Objekte auch. Als Konsequenz daraus sind
die mit public gekennzeichneten Anteile von sowohl der Superklasse als auch von
der jeweiligen Subklasse für Objekte anderer Klassen sichtbar, während private
deklarierte Anteile nicht zugreifbar sind.
Tatsächlich ist es sogar so, dass es auch eine Privatsphäre zwischen Super- und Sub-
klasse gibt: Eine Subklasse kann nicht auf die privaten Anteile ihrer Superklasse
zugreifen. Daraus wiederum folgt, dass, wenn eine Subklasse auf die privaten
Datenfelder ihrer Superklasse zugreifen will, die Superklasse hierfür entsprechende
sondierende und/oder verändernde Methoden anbieten muss. Ein Objekt einer
Subklasse kann jede öffentliche Methode aufrufen, die in ihrer Superklasse defi-
niert ist – ganz so, als wenn die Methode lokal in der Subklasse definiert wäre. Eine
Variable wird nicht benötigt, da die Methoden alle Teil desselben Objekts sind.
Wir werden das Thema der Zugriffsrechte wieder aufgreifen, wenn wir in Kapitel 11
den Zugriffsmodifikator protected vorstellen.

Übung 10.4 Öffnen Sie das Projekt Netzwerk-V2. Dieses Projekt enthält
eine Version von Netzwerk, in der Vererbung wie bisher besprochen einge-
setzt ist. Sie sehen, dass in dem Diagramm die Vererbungsbeziehung ange-
zeigt wird. Öffnen Sie den Quelltext der Klasse NachrichtenEinsendung und
entfernen Sie die Wörter „extends Einsendung“. Schließen Sie den Editor. Wel-
che Veränderung beobachten Sie am Klassendiagramm? Fügen Sie anschlie-
ßend „extends Einsendung“ wieder ein.
Übung 10.5 Erzeugen Sie ein NachrichtenEinsendung-Objekt. Rufen Sie
einige seiner Methoden auf. Können Sie auch die geerbten Methoden auf-
rufen (beispielsweise erfasseKommentar)? Was beobachten Sie bei den geerb-
ten Methoden?

Übung 10.6 Um zu veranschaulichen, dass eine Subklasse ohne eine beson-


dere Syntax auf die nicht privaten Elemente ihrer Superklasse zugreifen
kann, nehmen Sie folgende (etwas künstliche) Änderung an den Klassen
NachrichtenEinsendung und Einsendung vor. Erstellen Sie in der Klasse Nach-
richtenEinsendung eine Methode namens kurzinfoAusgeben, deren Aufgabe
es sein soll, nur die Kurzinfo „Nachrichteneinsendung von NAME“ auszugeben,
wobei NAME der Name des Autors sein sollte. Da jedoch benutzername in der
Klasse Einsendung ein privates Datenfeld ist, müssen wir Einsendung um eine
öffentliche gibBenutzername-Methode erweitern. Rufen Sie diese Methode aus
kurzinfoAusgeben heraus auf, um den Benutzernamen abzufragen. Denken Sie

386
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.4 Vererbung in Java

daran, dass keine besondere Syntax erforderlich ist, wenn eine Subklasse die
Methode einer Superklasse aufruft. Testen Sie Ihre Lösung, indem Sie ein Nach-
richtenEinsendung-Objekt erzeugen. Implementieren Sie eine ähnliche
Methode in der FotoEinsendung-Klasse.

10.4.2 Vererbung und Initialisierung


Wenn wir ein Objekt erzeugen, dann sorgt ein Konstruktor dafür, dass alle Daten-
felder des Objekts in einen vernünftigen Zustand versetzt werden. Wir müssen uns
etwas näher ansehen, wie dies in erbenden Klassen erfolgt.

Wenn wir ein NachrichtenEinsendung-Objekt erzeugen, dann übergeben wir meh-


rere Parameter an den Konstruktor von NachrichtenEinsendung: den Namen des
Autors und den Nachrichtentext. Einer davon hat einen Wert für ein Datenfeld,
das in der Klasse Einsendung definiert wurde, der andere für ein Datenfeld der
Klasse NachrichtenEinsendung. All diese Datenfelder müssen korrekt initialisiert
werden und Listing 10.4 zeigt die Quelltextabschnitte, mit denen dies in Java
geschieht.

Listing 10.4
Initialisierung von
Datenfeldern der
Super- und Subklasse.

387
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Mehrere Dinge sind hier anzumerken. Erstens hat die Klasse Einsendung einen
Konstruktor, obwohl wir nicht vorhaben, direkt eine Instanz der Klasse Einsendung
zu erzeugen.2 Dieser Konstruktor bekommt die Parameter, die für die Initialisie-
rung der Datenfelder in Einsendung benötigt werden, und enthält die Anweisun-
gen, die diese Initialisierung vornehmen. Zweitens bekommt der Konstruktor von
NachrichtenEinsendung alle Parameter, die für die Initialisierung von Datenfeldern
in den beiden Klassen Einsendung und NachrichtenEinsendung benötigt werden. Er
enthält dann folgende Anweisung:
super(autor);
Das Schlüsselwort super bewirkt hier – ausgehend vom Konstruktor der Subklasse –
einen Aufruf des Konstruktors der Superklasse. Der Konstruktor von Einsendung
wird also als Teil der Ausführung des NachrichtenEinsendung-Konstruktors ausge-
führt. Wenn wir eine Nachrichteneinsendung erzeugen, dann wird der Konstruktor
von NachrichtenEinsendung aufgerufen, der wiederum als erste Anweisung den
Konstruktor der Klasse Einsendung aufruft. Der Konstruktor in Einsendung initiali-
siert die Datenfelder in Einsendung, anschließend wird in den Konstruktor von Nach-
richtenEinsendung zurückgekehrt, der dann das noch verbliebene Datenfeld in der
NachrichtenEinsendung-Klasse initialisiert. Damit dies funktioniert, müssen die Para-
meter, die für die Initialisierung der Datenfelder von Einsendung benötigt werden,
an den Konstruktor der Superklasse über den super-Aufruf weitergegeben werden.

Konzept In Java muss im Konstruktor einer Subklasse immer als erste Anweisung der Kons-
truktor der Superklasse aufgerufen werden. Wenn Sie nicht explizit einen solchen
Konstruktor
Aufruf in den Quelltext schreiben, fügt der Compiler automatisch einen Aufruf über
der Super-
klasse: Im Kons- super ein, damit die Datenfelder der Superklasse korrekt initialisiert werden. Der
truktor einer Sub- eingefügte Aufruf ist äquivalent zu:
klasse muss immer super();
als erste Anwei-
sung der Konstruk- Dieser automatisch eingefügte Konstruktoraufruf funktioniert allerdings nur, wenn
tor der Superklasse die Superklasse einen Konstruktor ohne Parameter hat (da der Compiler kaum erra-
aufgerufen werden. ten könnte, welche Werte er für mögliche Parameter angeben sollte). Andernfalls
Wenn im Quelltext gibt es eine Fehlermeldung. Es ist im Allgemeinen eine gute Idee, den Konstruktor
kein solcher Aufruf der Superklasse auch dann explizit aufzurufen, wenn der Compiler diesen Aufruf
angegeben ist,
selbst einfügen könnte. Wir betrachten dies als guten Programmierstil, denn es ver-
versucht Java auto-
matisch, einen para-
meidet, dass ungeübte Leser, die sich der automatischen Einfügung durch den Com-
meterlosen Aufruf piler nicht bewusst sind, irritiert werden oder den Quelltext fehlinterpretieren.
einzufügen.

Übung 10.7 Setzen Sie einen Haltepunkt in der ersten Zeile des Konstruktors
der Klasse NachrichtenEinsendung. Erzeugen Sie dann ein NachrichtenEinsen-
dung-Objekt. Wenn das Debugger-Fenster erscheint, benutzen Sie die Schaltflä-
che SCHRITT HINEIN, um den Quelltext zu durchlaufen. Beobachten Sie die
Datenfelder und ihre Initialisierung. Beschreiben Sie Ihre Beobachtungen.

2 Momentan hält uns nichts davon ab, tatsächlich eine Instanz der Klasse Einsendung zu erzeu-
gen, auch wenn wir das eigentlich nicht so wollten. In Kapitel 12 werden wir Techniken ken-
nenlernen, mit denen wir sicherstellen können, dass nur Instanzen von NachrichtenEinsendung
oder FotoEinsendung erzeugt werden können, aber nicht von Einsendung.

388
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.5 Weitere Einsendungen für Netzwerk

10.5 Weitere Einsendungen für Netzwerk


Nachdem wir nun die Vererbungshierarchie für unser Netzwerk-Projekt so gestaltet
haben, dass die gemeinsamen Elemente der Einsendungen in der Klasse Einsendung
definiert sind, ist es sehr viel einfacher, einen neuen Typ von Einsendung einzufüh-
ren. Beispielsweise könnten wir Informationen über Ereigniseinsendungen hinzu-
fügen wollen, die aus einer Beschreibung eines Standardereignisses bestehen (z.B.
„Fred ist der Gruppe der Neal-Stephenson-Fans beigetreten“). Standardereignisse
können ein Benutzer sein, der einer Gruppe beitritt, ein Benutzer, der eine Freund-
schaftsanfrage bestätigt oder ein Benutzer, der sein Profilbild ändert. Dazu müssen
wir allerdings eine neue Subklasse von Einsendung namens EreignisEinsendung defi-
nieren (Abbildung 10.7). Da EreignisEinsendung eine Subklasse von Einsendung ist,
erbt sie automatisch alle Datenfelder und Methoden, die in Einsendung definiert
sind. Somit haben EreignisEinsendung-Objekte bereits einen Benutzernamen, einen
Zeitstempel, einen Gefällt-Zähler und Kommentare. Wir können uns dann auf die
Datenfelder konzentrieren, die spezifisch für Ereigniseinsendungen sind, beispiels-
weise der Ereignistyp. Der Ereignistyp kann als eine Aufzählungskonstante (siehe
Kapitel 8) oder als Zeichenkette, die das Ereignis beschreibt, gespeichert werden.

Abbildung 10.7
Einsendung
Einsendungen für
benutzername Netzwerk mit einer
zeitstempel
gefielWieOft
Klasse Ereignis-
kommentare * Methoden ausgelassen Einsendung.

NachrichtenEinsendung FotoEinsendung EreignisEinsendung

nachricht dateiname ereignisTyp


ueberschrift
* *
*

Dies ist ein Beispiel dafür, wie wir mit Vererbung bereits geleistete Arbeit wiederver- Konzept
wenden können. Wir können den Quelltext, den wir für die Klassen Nachrichten-
Vererbung erlaubt
Einsendung und FotoEinsendung in der Klasse Einsendung geschrieben haben, so wie-
die Wiederver-
derverwenden, dass er auch für die Klasse EreignisEinsendung funktioniert. Die wendung bereits
Möglichkeit zur Wiederverwendung bestehender Softwareeinheiten ist einer der erstellter Klassen in
wichtigsten Vorteile von Vererbung. Wir werden dies später noch weiter diskutieren. neuen Zusammen-
hängen.
Diese Art der Wiederverwendung bewirkt, dass wir sehr viel weniger programmie-
ren müssen, wenn wir weitere Einsendungstypen einführen. Da zusätzliche Einsen-
dungen als Subklassen von Einsendung definiert werden können, müssen nur die
Dinge zusätzlich programmiert werden, die sich von Einsendung unterscheiden.
Nehmen Sie nun an, dass wir die Anforderungen etwas ändern: Zu den Ereignis-
einsendungen unseres Netzwerk-Projekts gibt es weder einen „Gefällt“-Knopf
noch Kommentarmöglichkeiten. Diese Einsendungen dienen allein der Informa-
tion. Wie erreichen wir das? In dieser Version erbt EreignisEinsendung als Subklasse

389
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

von Einsendung automatisch die Datenfelder gefaelltWieOft und kommentare. Stellt


dies ein Problem dar?
Wir könnten alles so lassen, wie es ist, und uns dafür entscheiden, die Kommentare
und Gefällt-Zählung für Ereigniseinsendungen nie anzuzeigen, d.h., die Datenfelder
einfach zu ignorieren. Doch dies fühlt sich nicht richtig an. Die Datenfelder zur Ver-
fügung zu haben, aber nicht zu nutzen, öffnet Problemen Tür und Tor. Irgendwann
wird ein Wartungsprogrammierer, der nicht erkennt, dass diese Datenfelder nicht
verwendet werden sollten, versuchen, sie zu verarbeiten.
Oder wir könnten EreignisEinsendung schreiben, ohne sie von Einsendung zu erben.
Doch dann landen wir wieder bei der Code-Duplizierung für die Datenfelder benut-
zername und zeitstempel und ihren Methoden.
Die Lösung ist ein Refactoring der Vererbungshierarchie. Wir können eine neue
Superklasse für alle Einsendungen mit Kommentaren einführen (namens Kommen-
tierteEinsendung), die eine Subklasse von Einsendung ist (Abbildung 10.8). Anschlie-
ßend verschieben wir die Datenfelder gefaelltWieOft und kommentare von der
Klasse Einsendung in die neue Klasse. NachrichtenEinsendung und FotoEinsendung
sind dann Subklassen der neuen Klasse KommentierteEinsendung, während Ereignis-
Einsendung direkt von Einsendung erbt. NachrichtenEinsendung-Objekte erben alles
von beiden Superklassen und haben die gleichen Datenfelder und Methoden wie
zuvor. Objekte der Klasse EreignisEinsendung erben die Datenfelder benutzername
und zeitstempel, jedoch ohne die Kommentare.

Abbildung 10.8
Einsendung
Weitere Einsendungs-
typen für Netzwerk. benutzername
zeitstempel * Methoden ausgelassen

KommentierteEinsendung EreignisEinsendung

gefaelltWieOft ereignisTyp
kommentare
*
*

NachrichtenEinsendung FotoEinsendung

nachricht dateiname
ueberschrift
*
*

Eine solche Situation kommt beim Entwurf von Klassenhierarchien recht häufig vor.
Wenn die Hierarchie nicht zu passen scheint, müssen wir die Hierarchie umgestalten.
Klassen, die nicht selbst für die Erzeugung von Instanzen benutzt werden, sondern
ausschließlich als Superklassen für andere Klassen dienen (wie Einsendung und Kom-
mentierteEinsendung), werden abstrakte Klassen genannt. Wir werden solche Klas-
sen ausführlicher in Kapitel 12 untersuchen.

390
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.6 Vorteile durch Vererbung (bis hierher)

Übung 10.8 Öffnen Sie das Projekt Netzwerk-V2. Fügen Sie eine Klasse für
Ereigniseinsendungen hinzu. Erzeugen Sie einige Objekte der neuen Klasse
und testen Sie, ob alle Methoden wie erwartet funktionieren.

10.6 Vorteile durch Vererbung (bis hierher)


Wir haben einige Vorteile durch die Verwendung von Vererbung in unserer Netz-
werk-Anwendung gesehen. Bevor wir weitere Aspekte von Vererbung untersu-
chen, sollten wir die bisher erkannten Vorteile verallgemeinert zusammenfassen:
 Vermeidung von Code-Duplizierung: Die Verwendung von Vererbung ver-
meidet, dass gleiche oder weitgehend ähnliche Quelltextabschnitte doppelt
(oder mehrfach) geschrieben werden müssen.
 Wiederverwendung von Quelltext: Bestehender Quelltext kann wiederver-
wendet werden. Wenn bereits eine Klasse existiert, die unseren Anforderun-
gen sehr nahekommt, dann können wir manchmal eine Subklasse dieser Klasse
definieren und ihre Implementierung wiederverwenden, anstatt alles erneut
definieren zu müssen.
 Einfachere Wartung: Die Wartung der Anwendung wird vereinfacht, denn
die Beziehungen zwischen den Klassen sind explizit ausgedrückt. Eine Ände-
rung an Datenfeldern oder Methoden, die zwischen mehreren Subklassen
geteilt werden, muss nur einmal vorgenommen werden.
 Erweiterbarkeit: Durch Vererbung wird es einfacher, eine bestehende Anwen-
dung zu erweitern.

Übung 10.9 Ordnen Sie die folgenden Begriffe in einer Vererbungshierarchie


an: Apfel, Eiscreme, Brot, Frucht, Nahrungsmittel, Haferflocken, Orange, Des-
sert, Schokopudding, Baguette.
Übung 10.10 In welcher Vererbungsbeziehung stehen ein Touchpad und
eine Maus? (Wir reden hier über Eingabegeräte am Computer, nicht über das
kleine, haarige Säugetier.)
Übung 10.11 Manchmal sind Dinge schwieriger, als sie auf den ersten Blick
scheinen. Betrachten Sie folgendes Beispiel: In welcher Vererbungsbezie-
hung stehen Rechteck und Quadrat zueinander? Wie rechtfertigen Sie Ihre
Antwort? Diskutieren Sie.

10.7 Subtyping
Bisher haben wir noch nicht untersucht, inwieweit sich unsere Umstellung auf
Vererbung auf den Quelltext der Klasse NewsFeed ausgewirkt hat. Listing 10.5
zeigt den vollständigen Quelltext der geänderten Klasse NewsFeed. Wir können ihn
mit der ursprünglichen Fassung in Listing 10.3 vergleichen.

391
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Listing 10.5
Der Quelltext der
Klasse NewsFeed
(zweite Version).

Wir sehen, dass der Quelltext durch die Umstellung auf Vererbung erheblich kür-
zer und einfacher geworden ist. Während in der ersten Version (Listing 10.3) alles
doppelt getan werden musste, wird hier alles nur einmal getan. Wir haben nur
eine Sammlung, nur eine Methode zum Einfügen von Einsendungen und eine
Schleife in der Methode zeigen.

Konzept Wir konnten den Quelltext in dieser Form verkürzen, weil wir in der neuen Ver-
sion an allen Stellen den Typ Einsendung benutzen können, an denen wir vorher
Subtyp: Analog
FotoEinsendung und NachrichtenEinsendung benutzen mussten. Wir sehen uns dies
zur Klassenhierar-
chie bilden die zunächst anhand der Methode EinsendungHinzufuegen genauer an.
Objekttypen eine In der ersten Version hatten wir zwei Methoden, um dem Newsfeed Einsendun-
Typhierarchie. Der
gen hinzuzufügen. Diese hatten die folgenden Köpfe:
Typ, der durch eine
Subklasse definiert public void erfasseNachricht(NachrichtenEinsendung nachricht)
ist, ist ein Subtyp des public void erfasseFoto(FotoEinsendung foto)
Typs, der durch die
zugeordnete Super- In unserer neuen Version haben wir für denselben Zweck nur eine Methode:
klasse definiert wird. public void erfasseEinsendung(Einsendung einsendung)

392
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.5 Weitere Einsendungen für Netzwerk

Die Parameter in der ursprünglichen Version waren durch die Typen FotoEinsendung
und NachrichtenEinsendung definiert; dadurch wurde sichergestellt, dass nur Nach-
richteneinsendungen und Fotoeinsendungen eingefügt werden konnten, da die
Typen von aktuellen Parametern zu den Typen ihrer formalen Parameter passen
müssen. Bisher haben wir „passen“ immer als „den gleichen Typ haben“ aufge-
fasst; als die Annahme, dass der Typname eines aktuellen Parameters identisch sein
muss mit dem Typnamen des zugehörigen formalen Parameters. Tatsächlich ist dies
aber nur die halbe Wahrheit, denn Objekte von Subklassen können an allen Stellen
verwendet werden, an denen etwas vom Typ einer ihrer Superklassen erwartet wird.

10.7.1 Subklassen und Subtypen


Wir haben in den vorigen Kapiteln bereits erwähnt, dass Klassen Typen definie-
ren. Der Typ eines Objekts der Klasse FotoEinsendung ist FotoEinsendung. Wir haben
gerade dargestellt, dass Klassen Subklassen haben können. Entsprechend kön-
nen Typen, die durch Klassen definiert wurden, Subtypen haben. In unserem Bei-
spiel ist FotoEinsendung ein Subtyp des Typs Einsendung.

10.7.2 Subtypen und Zuweisungen


Wenn wir an eine Variable ein Objekt zuweisen wollen, dann muss der Typ des Konzept
Objekts zum Typ der Variablen passen. Beispielsweise ist
Variablen und
Auto meinAuto = new Auto(); Subtypen: Eine
Variable kann ein
eine gültige Zuweisung, weil ein Objekt vom Typ Auto einer Variablen vom Typ Auto Objekt halten, des-
zugewiesen wird. Nachdem wir nun Vererbung kennengelernt haben, müssen wir sen Typ entweder
die Typregel für Zuweisungen etwas erweitern: Eine Variable kann Objekte halten, gleich dem dekla-
deren Typ entweder gleich dem deklarierten Typ der Variablen oder ein beliebiger rierten Typ der Vari-
Subtyp des deklarierten Typs ist. ablen oder ein belie-
biger Subtyp des
deklarierten Typs ist.

Abbildung 10.9
Eine Vererbungs-
hierarchie.

Nehmen Sie an, wir haben eine Klasse Fahrzeug mit zwei Subklassen Auto und
Fahrrad (Abbildung 10.9). In diesem Fall sind nach der neuen Typregel alle folgen-
den Zuweisungen zulässig:
Fahrzeug f1 = new Fahrzeug();
Fahrzeug f2 = new Auto();
Fahrzeug f3 = new Fahrrad();
Der Typ einer Variablen legt fest, welche Objekte sie halten kann. Wenn eine Vari-
able vom Typ Fahrzeug deklariert wird, dann kann sie Fahrzeuge halten. Da ein
Auto ein Fahrzeug ist, ist es also absolut zulässig, in einer Variablen, die ein Fahr-
zeug halten soll, ein Auto zu halten. (Stellen Sie sich die Variable wie eine Garage

393
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

vor: Wenn Ihnen jemand sagt, dass Sie in einer Garage Fahrzeuge abstellen kön-
nen, dann würden Sie vermuten, dass Sie dort sowohl ein Auto als auch ein Fahr-
rad abstellen können.)

Konzept Dieses Prinzip wird Ersetzbarkeit genannt. In einer objektorientierten Sprache


können wir, wenn ein Objekt einer Superklasse erwartet wird, dieses durch ein
Ersetzbarkeit:
Objekt einer Subklasse ersetzen, denn ein Objekt einer Subklasse ist ein spezieller
Objekte von Sub-
typen können an Fall der Superklasse. Wenn uns beispielsweise jemand nach einem Stift fragt,
allen Stellen ver- dann können wir ohne Weiteres einen Bleistift oder einen Kugelschreiber anbie-
wendet werden, an ten. Bleistift und Kugelschreiber sind Subklassen von Stift, also erfüllen beide die
denen ein Supertyp Anforderung nach einem Stift.
erwartet wird. Die-
ses Prinzip nennen
Umgekehrt funktioniert dies jedoch nicht:
wir Ersetzbarkeit. Auto meinAuto = new Fahrzeug(); // dies ist ein Fehler!
In dieser Zuweisung wird versucht, einer Auto-Variablen ein Fahrzeug-Objekt zuzuwei-
sen. Java lässt dies nicht zu; es wird eine Fehlermeldung ausgegeben, wenn Sie diese
Zuweisung übersetzen wollen. Die Variable wurde deklariert, um Autos aufzuneh-
men. Ein Fahrzeug hingegen kann ein Auto sein oder auch nicht – wir wissen es
nicht. Da diese Zuweisung deshalb falsch sein kann, wird sie nicht zugelassen.
Gleichermaßen:
Auto a2 = new Fahrrad();// dies ist ein Fehler!
Auch dies ist eine illegale Anweisung. Ein Fahrrad ist kein Auto (oder etwas formaler:
der Typ Fahrrad ist kein Subtyp von Auto), diese Zuweisung ist deshalb nicht zulässig.

Übung 10.12 Folgende vier Klassen seien gegeben: Person, Dozent, Student
und Promotionsstudent. Dozent und Student sind Subklassen von Person, Promo-
tionsstudent ist eine Subklasse von Student.

a Welche der folgenden Zuweisungen sind zugelassen? Begründen Sie Ihre


Antworten.
Person p1 = new Student();
Person p2 = new Promotionsstudent();
Promotionsstudent pro1 = new Student();
Dozent d1 = new Person();
Student s1 = new Promotionsstudent();

b Angenommen wir haben die folgenden zulässigen Deklarationen und


Zuweisungen:
Person p1 = new Person();
Person p2 = new Person();
Promotionsstudent pro1 = new Promotionsstudent();
Dozent d1 = new Dozent();
Student s1 = new Student();
Welche der folgenden Zuweisungen sind angesichts der obigen Vorgaben
zulässig? Begründen Sie Ihre Antworten.
s1 = p1;
s1 = p2;

394
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.7 Subtyping

p1 = s1;
d1 = s1;
s1 = pro1;
pro1 = s1;
Übung 10.13 Überprüfen Sie Ihre Antworten der letzten Übung, indem Sie
Grundgerüste der genannten Klassen in BlueJ anlegen und die Anweisun-
gen ausprobieren.

10.7.3 Subtyping und Parameterübergabe


Die Übergabe eines Parameters (also die Zuweisung eines aktuellen Parameters an
eine formale Parametervariable) verhält sich exakt wie eine Zuweisung an eine Vari-
able. Deshalb können wir ein Objekt vom Typ NachrichtenEinsendung an eine
Methode übergeben, die einen Parameter vom Typ Einsendung erwartet. Wir haben
folgende Definition der Methode erfasseEinsendung in der Klasse NewsFeed:
public void erfasseEinsendung(Einsendung einsendung)
{
...
}
Wir können diese Methode nun verwenden, um Fotoeinsendungen und Nach-
richteneinsendungen zu erfassen:
NewsFeed feed = new NewsFeed ();
NachrichtenEinsendung nachricht = new NachrichtenEinsendung(...);
FotoEinsendung foto = new FotoEinsendung(...);
[Link](nachricht);
[Link](foto);
Aufgrund der Subtyp-Regeln benötigen wir nur eine Methode (mit einem Para-
meter vom Typ Einsendung), um Nachrichteneinsendungen und Fotoeinsendun-
gen erfassen zu können. Wir werden im nächsten Kapitel noch ausführlicher auf
die Subtyp-Regeln zu sprechen kommen.

10.7.4 Polymorphe Variablen


Variablen für Objekttypen sind in Java polymorphe Variablen. Der Term poly-
morph (wörtlich: vielgestaltig) bezieht sich auf den Umstand, dass eine Variable
Objekte von verschiedenen Typen (dem deklarierten Typ der Variablen und den
Subtypen dieses deklarierten Typs) halten kann. Polymorphie tritt in objektorien-
tierten Sprachen in unterschiedlichen Zusammenhängen auf – polymorphe Variab-
len sind nur das erste Beispiel. Wir werden andere Formen von Polymorphie aus-
führlicher im nächsten Kapitel besprechen.
An dieser Stelle stellen wir lediglich fest, wie sehr eine polymorphe Variable uns
das Leben in unserer Methode zeigen erleichtert. Der Rumpf der Methode sieht
folgendermaßen aus:
for(Einsendung einsendung : einsendungen) {
[Link]();
[Link](); // leere Zeile zwischen den Einsendungen
}

395
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Wir durchlaufen hier die Liste der Einsendungen (die in einer ArrayList in der
Variablen einsendungen gehalten werden). Wir lassen uns nacheinander jede Ein-
sendung liefern und rufen ihre anzeigen-Methode auf. Beachten Sie, dass die
Objekte, die wir tatsächlich geliefert bekommen, vom Typ NachrichtenEinsendung
oder FotoEinsendung sind, nicht vom Typ Einsendung. Wir können jedoch eine
Schleifenvariable vom Typ Einsendung benutzen, weil Variablen polymorph sind.
Die Variable einsendung kann Objekte der Klassen NachrichtenEinsendung und
FotoEinsendung halten, weil diese Subtypen von Einsendung sind.
Damit hat die Verwendung von Vererbung in der Methode zeigen die Notwendig-
keit für zwei Schleifen beseitigt. Vererbung vermeidet also nicht nur Code-Duplizie-
rung in den Dienstleisterklassen, sondern auch bei den Klienten dieser Klassen. (Für
Dienstleister und Klient werden manchmal auch die englischen Begriffe Server und
Client verwendet.)

Hinweis
Wenn Sie die Übungen durchführen, werden Sie feststellen, dass die Methode
zeigen ein Problem hat: Es werden nicht alle Details ausgegeben. Die Lösung die-
ses Problems bedarf einiger Erläuterungen – diese geben wir im nächsten Kapitel.

Übung 10.14 Was muss in der Klasse NewsFeed geändert werden, wenn wir
eine Klasse für eine weitere Einsendung (beispielsweise eine Klasse Ereignis-
Einsendung) einführen? Warum?

10.7.5 Der Cast-Operator


In manchen Situationen ist die Regel, dass wir nicht von einem Supertyp auf einen
Subtyp zuweisen können, restriktiver als notwendig. Wenn wir wissen, dass eine
Supertyp-Variable ein Objekt von einem Subtyp hält, dann könnte die Zuweisung
eigentlich erlaubt werden. Beispielsweise:
Fahrzeug f;
Auto a = new Auto();
f = a; // korrekt
a = f; // Fehler
Diese Anweisungen lassen sich nicht übersetzen: Wir erhalten einen Compiler-Feh-
ler in der letzten Zeile, weil die Zuweisung einer Fahrzeug-Variablen an eine Auto-
Variable (Supertyp an Subtyp) nicht zugelassen ist. Wir wissen jedoch, wenn wir
diese Anweisungen in dieser Reihenfolge ausführen, dass die Zuweisung zugelas-
sen werden könnte. Wir sehen, dass die Variable f tatsächlich ein Objekt vom Typ
Auto enthält, sodass die Zuweisung an a okay wäre. Der Compiler ist jedoch nicht
so schlau. Er übersetzt den Quelltext Zeile für Zeile, betrachtet deshalb auch die
letzte Zeile isoliert für sich, ohne zu „wissen“, was gerade in der Variablen f
gespeichert ist. Diesen Effekt nennt man Verlust an Typinformation. Der Typ des
Objekts in f ist tatsächlich Auto, aber der Compiler weiß dies nicht.

396
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.8 Die Klasse Object

Wir können dieses Problem umgehen, indem wir dem Typsystem explizit mittei-
len, dass die Variable f ein Objekt der Klasse Auto enthält. Wir machen dies mit
dem Cast-Operator:
a = (Auto)f; // ok
Der Cast-Operator besteht aus dem Namen eines Typs (hier: Auto), der in runden
Klammern vor eine Variable oder einen Ausdruck geschrieben wird. Dies veran-
lasst den Compiler dazu, zu glauben, dass das Objekt ein Auto ist, sodass er kei-
nen Fehler meldet. Zur Laufzeit wird das Java-System allerdings prüfen, ob es
wirklich ein Auto ist. Wenn wir vorsichtig waren und das stimmt, dann ist alles in
Ordnung. Wenn das Objekt in f jedoch einen anderen Typ hat, dann wird das
Laufzeitsystem einen Fehler melden (eine sogenannte ClassCastException) und
das Programm wird abgebrochen.3
Betrachten wir nun das folgende Quelltextfragment, in dem Fahrrad ebenfalls
eine Subklasse von Fahrzeug ist:
Fahrzeug f; Auto a; Fahrrad r;
a = new Auto();
f = a; // ok
r = (Fahrrad) a; // Fehler zur Übersetzungszeit!
r = (Fahrrad) f; // Fehler zur Laufzeit!
Die letzten beiden Zuweisungen werden fehlschlagen. Der Versuch, a an r zuzu-
weisen (auch mit dem Cast-Operator), schlägt zur Übersetzungszeit fehl. Der
Compiler erkennt, dass Auto und Fahrrad nicht in einer Supertyp-Subtyp-Relation
zueinander stehen, a also niemals ein Fahrrad-Objekt halten kann – die Zuwei-
sung wird niemals funktionieren.
Der Versuch, f an r zuzuweisen (mit dem Cast-Operator), wird zur Übersetzungs-
zeit zugelassen, schlägt aber zur Ausführungszeit fehl. Fahrzeug ist eine Super-
klasse von Fahrrad, sodass f potenziell ein Fahrrad-Objekt halten könnte. Zur
Laufzeit stellt sich jedoch heraus, dass das Objekt in f nicht ein Fahrrad, sondern
ein Auto ist, und das Programm wird vorzeitig abbrechen.
Der Cast-Operator sollte so weit wie möglich vermieden werden, weil er zu Lauf-
zeitfehlern führen kann, die wir unbedingt vermeiden wollen. Der Compiler kann
uns in solchen Fällen nicht garantieren, dass alles korrekt ist.
In der Praxis wird der Cast-Operator in gut strukturierten objektorientierten Pro-
grammen nur sehr selten benutzt. In fast allen Fällen, in denen Sie den Cast-Opera-
tor in Ihrem Quelltext verwenden, könnten Sie seine Verwendung durch eine Res-
trukturierung des Quelltextes vermeiden und ein besser entworfenes Programm
erhalten. Üblicherweise geschieht dies, indem statt des Cast-Operators ein poly-
morpher Methodenaufruf verwendet wird (mehr dazu im nächsten Kapitel).
Konzept
Alle Klassen ohne
10.8 Die Klasse Object explizit deklarierte
Superklasse haben
Alle Klassen haben eine Superklasse. Bisher hat es so ausgesehen, als ob die meis- Object als ihre
ten Klassen keine Superklasse hätten, lediglich Klassen wie FotoEinsendung oder Superklasse.
NachrichtenEinsendung, die eine andere Klasse erweitern. Tatsächlich ist es jedoch

3 Exceptions werden ausführlich in Kapitel 14 diskutiert.

397
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

so, dass alle Klassen, die nicht explizit eine Superklasse deklarieren, implizit von der
Klasse Object erben.
Object ist eine Klasse aus der Java-Standardbibliothek, die als Superklasse aller
Klassen dient. Eine Deklaration wie
public class Person
{
...
}
ist äquivalent zu
public class Person extends Object
{
...
}
Der Java-Compiler fügt in alle Klassendefinitionen ohne explizite extends-Deklaration
Object als Superklasse ein, sodass Sie das niemals selbst tun müssen. Jede Klasse (mit
Ausnahme der Klasse Object selbst) erbt von Object, entweder direkt oder indirekt.
Abbildung 10.10 zeigt einige zufällig gewählte Klassen, die dies illustrieren.
Abbildung 10.10
Alle Klassen erben
von Object.

Eine gemeinsame Superklasse für alle Klassen dient zwei Zwecken: Zum einen kön-
nen wir polymorphe Variablen vom Typ Object deklarieren, in denen wir jedes
beliebige Objekt halten können. Variablen, die ein beliebiges Objekt halten kön-
nen, sind nicht sehr oft sinnvoll, aber es gibt Situationen, in denen sie hilfreich sind.
Zum anderen kann die Klasse Object einige Methoden definieren, die dann auto-
matisch jedem Objekt zur Verfügung stehen. Von besonderer Bedeutung sind in
diesem Zusammenhang die Methoden toString, equals und hashCode. Der zweite
Punkt wird noch eine Rolle spielen, die wir im nächsten Kapitel diskutieren.

10.9 Die Hierarchie der Sammlungstypen


Die Java-Bibliothek benutzt Vererbung ausführlich dazu, die Sammlungsklassen
selbst zu strukturieren. Die Klasse ArrayList beispielsweise erbt von einer Klasse
namens AbstractList, die wiederum von AbstractCollection erbt. Wir werden diese
Hierarchie hier nicht diskutieren, da sie bereits ausführlich an gut zugänglichen
Stellen beschrieben ist. Eine gute Beschreibung findet sich auf der Webseite von
Oracle unter [Link]

398
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.9 Die Hierarchie der Sammlungstypen

Beachten Sie, dass einige Details dieser Hierarchie Wissen über Java-Interfaces
voraussetzen. Diese werden wir in Kapitel 12 diskutieren.

Übung 10.15 Benutzen Sie die Dokumentation der Standardklassenbibliothek


von Java, um etwas über die Vererbungshierarchie der Sammlungsklassen he-
rauszubekommen. Zeichnen Sie ein Diagramm, das die Hierarchie wiedergibt.

Zusammenfassung
In diesem Kapitel haben wir einen ersten Eindruck von Vererbung vermittelt.
Alle Klassen in Java sind in eine Vererbungshierarchie eingeordnet. Jede
Klasse kann eine explizit deklarierte Superklasse haben oder erbt implizit
von der Klasse Object.
Subklassen repräsentieren üblicherweise Spezialisierungen von Superklas-
sen. Die Vererbungsbeziehung wird deshalb auch oft als Ist-ein-Beziehung
bezeichnet („ein Auto ist ein Fahrzeug“).
Subklassen erben alle Datenfelder und Methoden ihrer Superklasse. Objekte
von Subklassen haben alle Datenfelder und Methoden ihrer eigenen Klasse
und der all ihrer Superklassen. Vererbungsbeziehungen können benutzt wer-
den, um Code-Duplizierung zu vermeiden, um existierenden Code wiederzu-
verwenden und um eine Anwendung besser wartbar und erweiterbar zu
machen.
Subklassen definieren auch Subtypen und dies führt zu polymorphen Varia-
blen. Objekte von Subtypen können Objekte von Superklassen ersetzen und
Variablen können Objekte halten, die Instanzen von Subtypen ihres dekla-
rierten Typs sind.
Vererbung erlaubt den Entwurf von Klassenstrukturen, die leichter zu pfle-
gen und zu warten sind. Dieses Kapitel hat einen ersten Einblick in die
Benutzung von Vererbung zur Verbesserung von Programmstrukturen gege-
ben – weitere Anwendungsmöglichkeiten von Vererbung und ihre Vorteile
werden wir in den folgenden Kapiteln besprechen.

NEUE BEGRIFFE IN DIESEM KAPITEL


Vererbung, Superklasse (Oberklasse), Subklasse (Unterklasse), ist-ein,
Vererbungshierarchie, abstrakte Klasse, Subtyp, Ersetzbarkeit, polymor-
phe Variable, Verlust von Typinformation, Cast-Operator

399
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung

Zusammenfassung der Konzepte


 Vererbung Vererbung erlaubt uns, eine Klasse als Erweiterung einer
anderen zu definieren.
 Superklasse Eine Superklasse ist eine Klasse, die von anderen Klassen
erweitert wird.
 Subklasse Eine Subklasse ist eine Klasse, die eine andere Klasse erweitert
bzw. von dieser Klasse erbt. Sie erbt alle Datenfelder und Methoden von
ihrer Superklasse.
 Vererbungshierarchie Klassen, die über Vererbungsbeziehungen mitei-
nander verknüpft sind, bilden eine Vererbungshierarchie.
 Konstruktor der Superklasse Im Konstruktor einer Subklasse muss
immer als erste Anweisung der Konstruktor der Superklasse aufgerufen
werden. Wenn im Quelltext kein solcher Aufruf angegeben ist, versucht
Java automatisch, einen parameterlosen Aufruf einzufügen.
 Wiederverwendung Vererbung erlaubt die Wiederverwendung bereits
erstellter Klassen in neuen Zusammenhängen.
 Subtyp Analog zur Klassenhierarchie bilden die Objekttypen eine Typhie-
rarchie. Der Typ, der durch eine Subklasse definiert ist, ist ein Subtyp des
Typs, der durch die zugeordnete Superklasse definiert wird.
 Variablen und Subtypen Eine Variable kann ein Objekt halten, dessen
Typ entweder gleich dem deklarierten Typ der Variablen oder ein beliebi-
ger Subtyp des deklarierten Typs ist.
 Ersetzbarkeit Objekte von Subtypen können an allen Stellen verwendet
werden, an denen ein Supertyp erwartet wird. Dieses Prinzip nennen wir
Ersetzbarkeit.
 Object Alle Klassen ohne explizit deklarierte Superklasse haben Object als
ihre Superklasse.

400
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.9 Die Hierarchie der Sammlungstypen

Übung 10.16 Gehen Sie zurück zum Projekt Laborkurse aus Kapitel 1. Fügen
Sie dem Projekt Dozenten hinzu (jeder Laborkurs hat viele Studenten und
einen Dozenten). Verwenden Sie Vererbung zur Vermeidung von Code-Dupli-
zierung zwischen Studenten und Dozenten (beide haben einen Namen, Kon-
taktinformationen etc.)
Übung 10.17 Zeichnen Sie eine Vererbungshierarchie, die die Teile eines Com-
putersystems in Beziehung zueinander setzt (Prozessor, Speicher, Festplatte,
CD-Laufwerk, Drucker, Scanner, Tastatur, Maus etc.).
Übung 10.18 Sehen Sie sich den folgenden Quelltext an. Sie haben vier
Klassen
(O, X, T und M) und von jeder Klasse eine Variable.
O o;
X x;
T t;
M m;
Die folgenden Zuweisungen sind allesamt legal (sie lassen sich übersetzen):
m = t;
m = x;
o = t;
Die folgenden Zuweisungen sind allesamt illegal (sie führen zu Übersetzungs-
fehlern):
o = m;
o = x;
x = o;
Was können Sie über die Beziehungen dieser Klasse zueinander sagen?
Übung 10.19 Zeichnen Sie eine Vererbungshierarchie mit AbstractList und
all ihren (direkten und indirekten) Subklassen, wie sie in der Java-Standard-
bibliothek definiert sind.

401
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

11 Mehr über Vererbung

Lernziele
Zentrale Konzepte in diesem Kapitel: Methoden-Polymorphie, statischer
und dynamischer Typ, Überschreiben von Methoden, dynamische Metho-
densuche
Java-Konstrukte in diesem Kapitel: super (in Methoden), toString, protec-
ted, instanceof

Im letzten Kapitel wurden die Kernkonzepte der Vererbung anhand des Netzwerk-
Beispiels eingeführt. Daneben gibt es aber noch etliche wichtige Details zu unter-
suchen. Vererbung ist von grundlegender Bedeutung für das Verständnis und die
Programmierung mit objektorientierten Sprachen; deshalb ist es für die weiteren
Abschnitte sehr wichtig, Vererbung bis in die Details hinein verstanden zu haben.
In diesem Kapitel werden wir weiter mit Netzwerk arbeiten, um uns die wichtigs-
ten der übrigen Aspekte rund um Vererbung und Polymorphie anzueignen.

11.1 Das Problem: die Methode zum


Anzeigen
Als Sie mit den Netzwerk-Beispielen in Kapitel 10 experimentiert haben, haben
Sie möglicherweise festgestellt, dass die zweite Version – die mit Vererbung – ein
Problem hat: Die Methode anzeigen zeigt nicht alle Details einer Einsendung an.
Betrachten wir ein Beispiel. Nehmen wir an, wir erzeugen ein NachrichtenEinsendung-
und ein FotoEinsendung-Objekt mit den folgenden Daten:
Die Nachrichteneinsendung:
Leonardo da Vinci
Hatte heute Morgen eine grossartige Idee. Doch jetzt habe ich vergessen,
was es war.
Es hatte irgendwas mit Fliegen zu tun …
vor 40 Sekunden. 2 Person(en) gefällt dies.
Keine Kommentare.

403
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Die Fotoeinsendung:
Alexander Graham Bell
[[Link]] Ich denke, ich werde dies Ding „Telefon“ nennen.

Vor 12 Minuten. 4 Person(en) gefaellt dies.


Keine Kommentare.
Wenn wir diese in den Newsfeed1 eintragen und dann die erste Version der
Methode zeigen aufrufen (die ohne Vererbung), wird Folgendes ausgegeben:
Leonardo da Vinci
Hatte heute Morgen eine grossartige Idee.
Doch jetzt habe ich vergessen, was es war. Es hatte irgendwas mit
Fliegen zu tun …
vor 40 Sekunden. 2 Person(en) gefällt dies.
Keine Kommentare.
Alexander Graham Bell
[[Link]]
Ich denke, ich werde dies Ding 'Telefon' nennen.
vor 12 Minuten. 4 Person(en) gefaellt dies.
Keine Kommentare.
Die Formatierung ist zwar nicht besonders ansprechend (da uns in der Textkon-
sole keine Formatierungsoptionen zur Verfügung stehen), aber alle Informatio-
nen sind vorhanden und wir können uns vorstellen, wie die Methode zeigen spä-
ter angepasst werden kann, um die Daten in einer anderen Benutzerschnittstelle
netter zu formatieren.
Vergleichen Sie dies mit der zweiten Version von Netzwerk (die mit Vererbung),
die lediglich ausgibt:
Leonardo da Vinci
Vor 40 Sekunden – 2 Person(en) gefällt dies
Keine Kommentare
Alexander Graham Bell
Vor 12 Sekunden – 4 Person(en) gefällt dies
Keine Kommentare
Wir sehen, dass der Text der Nachrichteneinsendung sowie der Bilddateiname und
die Überschrift der Fotoeinsendung fehlen. Der Grund ist einfach. Die Methode
anzeigen ist in dieser Version in der Klasse Einsendung implementiert, nicht in Nach-
richtenEinsendung und FotoEinsendung (Abbildung 11.1). In der Methode in Einsen-
dung sind jedoch nur die Datenfelder verfügbar, die auch in Einsendung definiert sind.
Wenn wir in der Methode anzeigen in Einsendung versuchen würden, auf das Daten-
feld nachricht von NachrichtenEinsendung zuzugreifen, würde es eine Fehlermel-
dung geben. Dies illustriert das wichtige Prinzip, dass Vererbung als eine Einbahn-
straße betrachtet werden sollte: NachrichtenEinsendung erbt die Datenfelder von
Einsendung, aber Einsendung weiß nichts über die Datenfelder ihrer Subklassen.

1 Der Text für die Nachrichteneinsendung ist eine zweizeilige Zeichenkette. Sie können einen
mehrzeiligen Text in eine Zeichenkette eingeben, indem Sie das Zeichen \n als Zeilenumbruch
einfügen.

404
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.2 Statischer und dynamischer Typ

NewsFeed Abbildung 11.1


Ausgabe, Version 1:
die Methode anzeigen
in der Superklasse.

Einsendung

...
anzeigen

NachrichtenEinsendung FotoEinsendung

11.2 Statischer und dynamischer Typ


Der Versuch, eine vollständig polymorphe Methode anzeigen zu entwickeln, führt
uns in eine Diskussion über statische und dynamische Typen und Methodensuche.
Aber fangen wir langsam an.
Ein erster Versuch zur Lösung des Problems könnte sein, die Methode anzeigen in
die Subklassen zu verschieben (Abbildung 11.2). Auf diese Weise könnten die
beiden Methoden anzeigen, die dann zu den Klassen NachrichtenEinsendung und
FotoEinsendung gehören, jeweils auf die Datenfelder von NachrichtenEinsendung
und FotoEinsendung zugreifen. Sie könnten auch auf die geerbten Datenfelder
zugreifen, indem sie sondierende Methoden aus der Klasse Einsendung aufrufen.
Das sollte diesen Methoden ermöglichen, alle notwendigen Details auszugeben.
Setzen Sie diesen Ansatz um, indem Sie die folgende Übung durchführen.

NewsFeed Abbildung 11.2


Ausgabe, Version 2:
die Methode anzeigen
in den Subklassen.

Einsendung

NachrichtenEinsendung FotoEinsendung

... ...
anzeigen anzeigen

405
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Übung 11.1 Öffnen Sie Ihre letzte Version des Netzwerk-Projekts. (Sie kön-
nen Netzwerk-V2 benutzen, wenn Sie noch kein eigenes Projekt haben.)
Entfernen Sie die Methode anzeigen aus der Klasse Einsendung und verschie-
ben Sie sie in die Klassen FotoEinsendung und NachrichtenEinsendung. Überset-
zen Sie. Was beobachten Sie?

Wenn wir die Methode anzeigen einfach in die Unterklassen verschieben, stellen
wir fest, dass sich das Projekt nicht mehr übersetzen lässt. Es gibt zwei funda-
mentale Probleme:
 Wir bekommen Fehlermeldungen in den Klassen NachrichtenEinsendung und
FotoEinsendung, weil wir dort nicht auf die Datenfelder der Superklasse zugrei-
fen können.
 Wir bekommen eine Fehlermeldung in der Klasse NewsFeed, weil dort die
Methode anzeigen nicht gefunden werden kann.
Der Grund für die erste Fehlerart ist, dass die Datenfelder in Einsendung als privat
deklariert und deshalb für alle anderen Klassen nicht zugreifbar sind – inklusive
aller Subklassen. Da wir die Kapselung nicht durchbrechen wollen, indem wir diese
Datenfelder öffentlich deklarieren, ist die nächstliegende Lösung, die Datenfelder
über öffentliche sondierende Methoden zugreifbar zu machen. (Hinweis: In
Abschnitt 11.9 werden wir noch eine weitere Zugriffsart vorstellen, die speziell für
die Beziehung zwischen einer Super- und ihren Subklassen definiert wurde.)
Der zweite Fehler erfordert eine etwas ausführlichere Erklärung, die wir im fol-
genden Abschnitt geben.

11.2.1 Aufrufe von anzeigen in NewsFeed


Wir untersuchen zuerst das Problem, dass anzeigen in der Klasse NewsFeed nicht auf-
gerufen werden kann. Die betroffenen Zeilen im Quelltext der Klasse NewsFeed sind:
for(Einsendung einsendung : einsendungen) {
[Link]();
[Link]();
}
Die for-each-Anweisung fragt jede Einsendung aus der Sammlung ab; die erste
Anweisung im Schleifenrumpf versucht, die Methode anzeigen an der Einsendung
aufzurufen. Der Compiler meldet, dass er die Methode anzeigen nicht finden kann.
Einerseits erscheint dies logisch: Einsendung hat tatsächlich keine Methoden
anzeigen mehr (Abbildung 11.2).
Andererseits erscheint es unlogisch und ist ärgerlich. Wir wissen, dass jedes Ein-
sendung-Objekt, das in der Datenbank eingetragen ist, entweder ein Nachrichten-
Einsendung- oder ein FotoEinsendung-Objekt ist, und beide haben eine Methode
anzeigen. Also könnte ein Aufruf [Link]() eigentlich funktionieren.
Egal, ob es ein NachrichtenEinsendung- oder ein FotoEinsendung-Objekt ist, wir wis-
sen, dass eine Methode anzeigen vorhanden ist.

406
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.2 Statischer und dynamischer Typ

Um im Detail zu verstehen, warum dies nicht funktioniert, sollten wir uns die Typen
näher ansehen. Betrachten wir folgende Anweisung:
Auto a1 = new Auto();
Wir sagen: Der Typ von a1 ist Auto. Bevor wir Vererbung kennengelernt haben, hat
es keine Rolle gespielt, ob wir mit „Typ von a1“ gemeint haben „der Typ der Varia-
blen a1“ oder „der Typ des Objekts, das in a1 gespeichert ist“. Das war nicht wich-
tig, weil der Typ der Variablen und der Typ des Objekts immer gleich waren.
Da wir jetzt das Konzept von Subtypen kennen, müssen wir etwas präziser wer- Konzept
den. Betrachten Sie folgende Anweisung:
Der statische
Fahrzeug f1 = new Auto(); Typ einer Variab-
len v ist der Typ,
Was ist der Typ von f1? Das hängt davon ab, was wir mit „Typ von f1“ meinen. mit dem die Vari-
Der Typ der Variablen f1 ist Fahrzeug; der Typ des Objekts, auf das f1 verweist, ist able im Quelltext
jedoch Auto. Durch die Regeln von Subtypen und Ersetzbarkeit haben wir hier der Klasse dekla-
eine Situation, in der der Typ der Variablen und der Typ des dahinterliegenden riert wurde.
Objekts nicht mehr gleich sind.
Um besser über diesen Tatbestand reden zu können, führen wir einige Begriffe ein: Konzept
 Wir nennen den deklarierten Typ einer Variablen ihren statischen Typ, denn er
Der dynami-
wird im Quelltext deklariert – der statischen Repräsentation eines Programms. sche Typ einer
 Wir nennen den Typ des Objekts, auf das eine Variable verweist, ihren dynami- Variablen v ist der
schen Typ, denn dieser hängt von Zuweisungen zur Laufzeit ab – vom dynami- Typ des Objekts,
schen Verhalten des Programms. das zurzeit in der
Variablen v gehal-
Somit können wir die obige Anweisung jetzt präziser beschreiben: Der statische ten wird.
Typ von f1 ist Fahrzeug, der dynamische Typ von f1 ist Auto. Wir können nun auch
unser Problem mit dem Aufruf von anzeigen in der Klasse NewsFeed umformulie-
ren. Beim Aufruf
[Link]();
ist der statische Typ von einsendung hier Einsendung, während der dynamische Typ
entweder NachrichtenEinsendung oder FotoEinsendung ist (Abbildung 11.3). Wir
wissen nicht, welcher von beiden es ist, wenn wir davon ausgehen, dass sowohl
NachrichtenEinsendung- als auch FotoEinsendung-Objekte in den Feed eingetragen
wurden.

Einsendung einsendung Abbildung 11.3


: FotoEinsendung
Eine Variable vom Typ
Einsendung enthält
ein Objekt vom Typ
FotoEinsendung.

Der Compiler meldet einen Fehler, weil bei der Typüberprüfung ausschließlich der
statische Typ berücksichtigt wird. Da der dynamische Typ häufig erst zur Laufzeit
bekannt ist, hat der Compiler gar keine andere Wahl: Zur Übersetzungszeit kann
er nur den statischen Typ berücksichtigen. Der statische Typ von einsendung ist
Einsendung, und Einsendung verfügt nicht über eine Methode anzeigen. Das Verhal-
ten des Compilers ist vernünftig, denn er hat keine Garantie, dass alle Subklassen
von Einsendung tatsächlich eine Methode anzeigen definieren; in der Praxis ist das
schlicht nicht prüfbar.

407
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Mit anderen Worten: Damit dieser Aufruf funktioniert, benötigt Einsendung eine
Methode anzeigen; damit sind wir wieder bei unserem ursprünglichen Problem,
scheinbar, ohne einen Schritt weitergekommen zu sein.

Übung 11.2 Fügen Sie in Ihrem Netzwerk-Projekt wieder eine Methode


anzeigen in die Klasse Einsendung ein. Lassen Sie im Rumpf der Methode vor-
läufig nur den Benutzernamen ausgeben. Modifizieren Sie dann die anzei-
gen-Methoden in den Klassen NachrichtenEinsendung und FotoEinsendung so,
dass die NachrichtenEinsendung-Version nur die Nachricht ausgibt und die
FotoEinsendung-Version nur den Überschrift. Dies beseitigt die oben aufge-
tretenen Fehler (auf die wir noch zurückkommen werden).
Sie sollten nun eine Situation wie in Abbildung 11.4 haben: eine Methode
anzeigen in drei Klassen. Übersetzen Sie Ihr Projekt. (Wenn es Fehler gibt,
sollten Sie diese beseitigen; der Entwurf sollte funktionieren.)
Bevor Sie den neuen Entwurf ausführen, sollten Sie vorhersagen, welche der
anzeigen-Methoden aufgerufen wird, wenn Sie die Methode zeigen des News-
feed aufrufen.
Versuchen Sie es: Tragen Sie ein NachrichtenEinsendung und ein FotoEinsendung-
Objekt in die Datenbank ein und rufen Sie die Methode auflisten auf. Welche
anzeigen-Methode wurde ausgeführt? War Ihre Vorhersage korrekt? Versuchen
Sie Ihre Beobachtungen zu erklären.

Abbildung 11.4 NewsFeed


Ausgabe, Version 3:
eine Methode
anzeigen in den
Subklassen und der
Superklasse. Einsendung

...
anzeigen

NachrichtenEinsendung FotoEinsendung

... ...
anzeigen anzeigen

11.3 Überschreiben von Methoden


Als Nächstes diskutieren wir den Entwurf, in dem sowohl die Super- als auch die
beiden Subklassen eine Methode anzeigen haben (Abbildung 11.4). Der Kopf
aller drei anzeigen-Methoden ist exakt gleich.
Listing 11.1 zeigt die relevanten Details der Quelltexte der drei Klassen. Die
Klasse Einsendung hat eine Methode anzeigen, die alle Datenfelder der Klasse Ein-

408
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.3 Überschreiben von Methoden

sendung ausgibt (die sowohl für Nachrichteneinsendungen als auch für Fotoein-
sendungen gelten), die Subklassen NachrichtenEinsendung und FotoEinsendung haben
Methoden, die die Daten der jeweiligen Klasse ausgeben.
Listing 11.1
Quelltext der
anzeigen-Methoden
in allen drei Klassen.

Konzept
Überschreiben
einer Methode:
Eine Subklasse
kann die Imple-
mentierung einer
Methode über-
schreiben. Dazu
deklariert die
Subklasse eine
Dieser Entwurf funktioniert etwas besser. Er lässt sich übersetzen und kann aus- Methode mit der
geführt werden, auch wenn er noch nicht perfekt ist. Eine Implementierung die- gleichen Signatur
ses Entwurfs liefert das Projekt Netzwerk-V3. (Wenn Sie die Übung 11.2 durch- wie in der Super-
klasse, implemen-
geführt haben, dann haben Sie bereits eine ähnliche Umsetzung dieses Entwurfs
tiert diese jedoch
in Ihrem eigenen Projekt.) mit einem anderen
Die Technik, die hier angewendet wird, nennt sich Überschreiben (und wird manch- Rumpf. Die über-
mal auch als Redefinieren bezeichnet). Überschreiben tritt auf, wenn eine Methode schreibende
in einer Superklasse definiert ist (die Methode anzeigen in der Klasse Einsendung in Methode wird dann
bei Aufrufen an
diesem Beispiel) und eine Methode mit der exakt gleichen Signatur in einer Sub-
Objekte der Unter-
klasse definiert wird. Man kann @Override vor die Version der Subklasse setzen, klasse vorgezogen.
um deutlich zu machen, dass hier eine neue Version einer geerbten Methode
definiert wird.

409
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

In einer solchen Situation haben die Objekte der Subklassen zwei Methoden mit
gleichem Namen und gleicher Signatur: eine aus der Superklasse geerbte und eine
aus der Subklasse. Welche wird ausgeführt, wenn wir die Methode aufrufen?

11.4 Dynamische Methodensuche


Wir erleben eine kleine Überraschung, wenn wir die Methode zeigen der Klasse
NewsFeed aufrufen. Wenn wir wie in Abschnitt 11.1 einige Objekte erzeugen und
eintragen, dann liefert die Methode zeigen in der neuen Version folgende Ausgabe:
Ich hatte eine großartige Idee heute Morgen.
Doch jetzt habe ich vergessen, was es war. Es hatte irgendwas mit
Fliegen zu tun …

[[Link]]
Ich denke, ich werde dies Ding 'Telefon' nennen.
Wir können an dieser Ausgabe erkennen, dass die anzeigen-Methoden der Klassen
NachrichtenEinsendung und FotoEinsendung aufgerufen werden, aber nicht die der
Klasse Einsendung.
Das mag auf den ersten Blick seltsam erscheinen. Unsere Untersuchung in Abschnitt
11.2 hat ergeben, dass der Compiler auf eine Methode anzeigen in der Klasse Ein-
sendung besteht – die Methoden in den Subklassen haben nicht ausgereicht. Der
letzte Versuch hat nun gezeigt, dass diese Methode aus der Klasse Einsendung über-
haupt nicht ausgeführt wird, stattdessen aber die der Subklassen. Zusammengefasst:
 Die Typüberprüfung berücksichtigt den statischen Typ, aber zur Laufzeit wer-
den die Methoden des dynamischen Typs ausgeführt.
Das ist eine sehr wichtige Aussage. Um sie besser zu verstehen, sehen wir uns
genauer an, wie Methoden aufgerufen werden. Dieser Mechanismus wird Metho-
densuche (method lookup), Methodenbindung (method binding) oder Metho-
denauswahl (method dispatch) genannt. Wir werden im Weiteren den Begriff
„Methodensuche“ verwenden.
Wir beginnen mit einem einfachen Szenario für eine Methodensuche. Nehmen
Sie an, wir haben ein Objekt der Klasse FotoEinsendung, das in einer Variablen v1
vom statischen Typ FotoEinsendung gespeichert ist (Abbildung 11.5). Die Klasse
FotoEinsendung hat eine Methode anzeigen und keine explizit deklarierte Super-
klasse. Das ist eine sehr einfache Situation – weder Vererbung noch Polymorphie
sind beteiligt. Wir führen nun folgende Anweisung aus:
[Link]();
Wenn diese Anweisung ausgeführt wird, erfolgt der Aufruf der Methode anzeigen
in folgenden Schritten:
1 Auf die Variable v1 wird zugegriffen.

2 Das in der Variablen gespeicherte Objekt wird gefunden (indem der Referenz
gefolgt wird).

3 Die Klasse dieses Objekts wird gefunden (indem der Referenz „ist Instanz
von“ gefolgt wird).

410
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.4 Dynamische Methodensuche

4 Die Implementierung der Methode anzeigen wird in der Klasse gefunden und
ausgeführt.
[Link](); FotoEinsendung Abbildung 11.5
Methodensuche mit
...
einem einfachen
anzeigen Objekt.

Instanz von
FotoEinsendung v1;

: FotoEinsendung

Bisher ist alles sehr gradlinig und nicht überraschend.


Als Nächstes betrachten wir eine Methodensuche mit Vererbung. Das Szenario
ist ähnlich, aber diesmal hat die Klasse FotoEinsendung eine Superklasse Einsendung
und die Methode anzeigen ist ausschließlich in der Superklasse definiert (Abbil-
dung 11.6).
[Link](); Einsendung Abbildung 11.6
Methodensuche mit
anzeigen
Vererbung.

FotoEinsendung

Instanz von
FotoEinsendung v1;

: FotoEinsendung

Wir führen dieselbe Anweisung aus. Der Aufruf der Methode beginnt ähnlich wie
oben, die ersten drei Schritte sind unverändert. Aber dann geht es anders weiter:

4 In der Klasse FotoEinsendung wird keine Methode anzeigen gefunden.


5 Weil keine passende Methode gefunden wird, wird die Superklasse nach ei-
ner passenden Methode durchsucht. Wenn auch dort die Methode nicht
gefunden wird, wird die nächste Superklasse (wenn es noch eine gibt)
durchsucht. Dies setzt sich durch die Vererbungshierarchie nach oben zur
Klasse Object hin fort, bis eine Methode gefunden wird. Beachten Sie, dass
zur Laufzeit garantiert eine passende Methode gefunden wird, sonst hätte
die Klasse nicht übersetzt werden können.
6 In unserem Beispiel wird die Methode anzeigen in der Klasse Einsendung ge-
funden und ausgeführt.

411
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Dieses Szenario beschreibt, wie Objekte Methoden erben. Jede Methode, die in
einer Superklasse gefunden werden kann, kann auf einem Objekt einer Subklasse
aufgerufen werden und wird korrekt gefunden und ausgeführt.
Als Nächstes kommen wir zum spannendsten Szenario: der Methodensuche mit
einer polymorphen Variablen und einer überschriebenen Methode (Abbildung 11.7).
Die Situation ist wieder der vorherigen recht ähnlich, aber es gibt zwei Änderungen:
 Der deklarierte Typ der Variablen v1 ist nun Einsendung, nicht FotoEinsendung.
 Die Methode anzeigen ist definiert in der Klasse Einsendung und redefiniert
(überschrieben) in der Klasse FotoEinsendung.
Abbildung 11.7 [Link](); Einsendung
Methodensuche mit
Polymorphie und anzeigen
Überschreiben.

FotoEinsendung

anzeigen

Instanz von
Einsendung v1;

: FotoEinsendung

Dieses Szenario ist das wichtigste, um das Verhalten in unserer kleinen Netzwerk-
Anwendung zu verstehen und eine Lösung für unser Problem der anzeigen-Metho-
den zu finden.
Die Schritte, in denen die Methode ausgeführt wird, sind exakt die gleichen
Schritte 1 bis 4 wie im ersten Szenario. Lesen Sie diese noch einmal.
Einige Beobachtungen sind hervorzuheben:
 Es gibt keine besonderen Suchregeln für die Methodensuche, wenn der dyna-
mische Typ nicht gleich dem statischen Typ ist. Das beobachtete Verhalten ent-
spricht genau den allgemeinen Regeln.
 Welche Methode zuerst gefunden und ausgeführt wird, hängt vom dynami-
schen Typ ab, nicht vom statischen Typ. Anders gesagt: Die Tatsache, dass der
deklarierte Typ der Variablen v1 hier Einsendung ist, spielt keine Rolle. Die Ins-
tanz, mit der wir hier umgehen, ist von der Klasse FotoEinsendung – das ist das
Einzige, worauf es ankommt.
 Überschreibende Methoden in Subklassen haben Vorrang vor Methoden aus
Superklassen. Da die Methodensuche in der dynamischen Klasse der Instanz
beginnt (am unteren Ende der Vererbungshierarchie), wird die letzte Redefini-
tion einer Methode zuerst gefunden und auch entsprechend ausgeführt.
 Wenn eine Methode überschrieben ist, dann wird nur die letzte Version (die
am weitesten unten stehende in der Vererbungshierarchie) ausgeführt. Versio-
nen der gleichen Methode in den Superklassen werden nicht automatisch
ebenfalls ausgeführt.

412
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.5 super-Aufrufe in Methoden

Dies erklärt das Verhalten, das wir in unserem Netzwerk-Projekt beobachten. Bei
der Ausgabe der Einsendungen werden nur die anzeigen-Methoden aus den Sub-
klassen (NachrichtenEinsendung und FotoEinsendung) ausgeführt, und es kommt zu
unvollständigen Ausgaben. Im nächsten Abschnitt besprechen wir, wie wir dies
beheben können.

11.5 super-Aufrufe in Methoden


Nachdem wir nun detailliert verstehen, wie überschriebene Methoden ausgeführt
werden, können wir die Lösung des Problems nachvollziehen. Man erkennt leicht,
dass es für die gegebene Situation das Eleganteste wäre, wenn für jeden Aufruf der
Methode anzeigen für beispielsweise ein FotoEinsendung-Objekt sowohl die anzeigen-
Methode aus Einsendung als auch die anzeigen-Methode aus FotoEinsendung ausge-
führt würde. Dann würden alle Details ausgegeben werden. (Eine alternative Lösung
werden wir später noch besprechen.)
Das ist tatsächlich recht einfach zu erreichen. Wir können das super-Konstrukt benut-
zen, dem wir bereits im Zusammenhang mit Konstruktoren in Kapitel 10 begegnet
sind. Listing 11.2 zeigt die Idee anhand der anzeigen-Methode in der Klasse Nach-
richtenEinsendung.

Listing 11.2
Redefinierende
Methode mit
super-Aufruf.

Wenn nun anzeigen an einem FotoEinsendung-Objekt aufgerufen wird, wird zuerst


die Methode anzeigen aus der Klasse FotoEinsendung ausgeführt. Als erste Anwei-
sung ruft diese Methode die Methode anzeigen der Superklasse auf, die wiederum
die allgemeinen Informationen über eine Sendung ausgibt. Wenn der Kontrollfluss
aus der Superklasse zurückkehrt, sorgen die restlichen Anweisungen in der Methode
der Subklasse für die Ausgabe der spezifischen Daten der FotoEinsendung-Klasse.
Drei Details sind hier zu beachten:
 Im Gegensatz zu super-Aufrufen in Konstruktoren wird der Name der Methode
der Superklasse explizit genannt. Ein super-Aufruf in einer Methode hat immer
folgende Form:
[Link] ( Parameterliste )
Die Parameterliste kann dabei natürlich leer sein.
 Ebenfalls im Gegensatz zur Regel für super-Aufrufe in Konstruktoren kann der
super-Aufruf in einer Methode an jeder beliebigen Stelle in der Methode erfol-
gen. Er muss nicht die erste Anweisung sein.
 Im Gegensatz zu super-Aufrufen in Konstruktoren muss kein super-Aufruf
erfolgen und es wird auch nicht automatisch ein Aufruf generiert; er ist optio-
nal. Auf diese Weise ergibt sich das Standardverhalten, dass eine überschrei-
bende Methode die entsprechende Version aus der Superklasse komplett ver-
deckt (überschreibt).

413
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Übung 11.3 Fügen Sie in Ihrer neuesten Version des Netzwerk-Projekts den
super-Aufruf in die anzeigen-Methoden ein. Testen Sie. Verhält sich die Anwen-
dung wie erwartet? Sehen Sie Probleme mit dieser Lösung?

In diesem Zusammenhang sei noch einmal auf die Ausführungen von Abschnitt
10.6 verwiesen: Grundsätzlich kann eine Subklasse auf alle nicht privaten Elemente
ihrer Superklasse ohne spezielle Syntax direkt zugreifen. Ein Zugriff über super ist
nur dann erforderlich, wenn die Superklassen-Version einer überschriebenen
Methode aufgerufen werden soll.
Wenn Sie Übung 11.3 abgeschlossen haben, werden Sie feststellen, dass diese
Lösung zwar funktioniert, aber noch nicht perfekt ist. Sie gibt alle Details aus, aber
nicht in der gewünschten Reihenfolge. Dieses werden wir weiter hinten im Kapitel
beheben.

11.6 Methoden-Polymorphie
Konzept In den vorigen Abschnitten haben wir eine weitere Form der Polymorphie ken-
nengelernt. Diese neue Form ist unter dem Begriff polymorphe Methodensuche
Methoden-
(oder kurz Methoden-Polymorphie) bekannt.
Polymorphie:
Methodenaufrufe Erinnern Sie sich, die definierende Eigenschaft einer polymorphen Variablen besteht
in Java sind poly- darin, dass sie auf Objekte unterschiedlicher Typen verweisen kann (jede Objektvari-
morph. Derselbe
able ist in Java potenziell polymorph). Auf ähnliche Weise sind Methodenaufrufe in
Methodenaufruf
kann zu unter- Java polymorph, denn ihr Aufruf kann zur Ausführung unterschiedlicher Methoden
schiedlichen Zeit- zu unterschiedlichen Zeitpunkten führen. Beispielsweise kann der Aufruf
punkten verschie- [Link]();
dene Methoden
aufrufen, abhängig zu einem Zeitpunkt die anzeigen-Methode einer NachrichtenEinsendung aufrufen
vom dynamischen und zu einem anderen die anzeigen-Methode einer FotoEinsendung, je nach refe-
Typ der Variablen, renziertem Objekt, also je nach dynamischem Typ der Variablen sendung.
mit der der Aufruf
durchgeführt wird.

11.7 Methoden aus Object: toString


In Kapitel 10 haben wir erwähnt, dass die universelle Superklasse Object einige
Methoden implementiert, die somit alle Objekte anbieten. Die spannendste dieser
Methoden ist toString, die wir jetzt vorstellen wollen. Wenn Sie weitere Details wis-
sen möchten, können Sie sich die Schnittstelle von Object in der Dokumentation der
Standardbibliothek ansehen.

Übung 11.4 Suchen Sie die Methode toString in der Java-Dokumentation.


Welche Parameter hat sie? Welchen Ergebnistyp liefert sie?

414
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.7 Methoden aus Object: toString

Die Methode toString dient dazu, eine String-Repräsentation eines Objekts zu Konzept
liefern. Das ist sehr nützlich für solche Objekte, die irgendwann einmal als Text in
Jedes Objekt in
einer Benutzungsoberfläche sichtbar werden, hilft aber auch ganz allgemein bei
Java bietet eine
allen Objekten – beispielsweise können sie während einer Fehlersuche gut ausge- Methode toString
geben werden. an, die eine String-
Repräsentation des
Die Standardimplementierung von toString in der Klasse Object kann nicht sehr
Objekts liefert. Um
viele nützliche Details anzeigen. Wenn wir beispielsweise toString an einem Foto- diese Methode
Einsendung-Objekt aufrufen, dann bekommen wir einen String in folgender Form: nützlich zu machen,
FotoEinsendung@65c221c0 kann sie in Sub-
klassen überschrie-
Das Ergebnis zeigt den Namen der Klasse des Objekts und eine interne Zahl.2 ben werden.

Übung 11.5 Erzeugen Sie ein Objekt der Klasse FotoEinsendung in Ihrem Pro-
jekt und rufen Sie dann im Kontextmenü des Objekts die Methode toString
aus dem Untermenü GEERBT VON OBJECT auf.

Typischerweise überschreiben wir diese Methode, um sie etwas nützlicher zu


machen. Wir könnten beispielsweise die Methode anzeigen auf Basis der Methode
toString definieren. Die Methode toString würde den Text nicht ausgeben, son-
dern ihn lediglich als Ergebnis liefern. Listing 11.3 zeigt, wie das aussehen könnte.

Listing 11.3
toString in den
Klassen Einsendung
und Nachrichten-
Einsendung.

2 Diese Nummer ist die Speicheradresse, an der dieses Objekt gespeichert ist. Sie ist nicht sehr
nützlich und kann nur zur Identifikation dienen. Wenn diese Nummer bei zwei Aufrufen gleich
ist, dann betrachten wir dasselbe Objekt. Wenn sie verschieden sind, haben wir auch unter-
schiedliche Objekte.

415
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Letztendlich würden wir versuchen, die anzeigen-Methoden komplett aus den Klas-
sen zu verbannen. Der große Vorteil einer toString-Methode ist, dass wir nicht vor-
schreiben, was mit dem Text tatsächlich passiert. In der Originalversion unserer
Anwendung wurde der Text immer auf der Konsole ausgegeben. Jetzt steht jedem
Klienten (beispielsweise der Klasse NewsFeed) frei, wie er mit dem Text umgeht. Er
kann ihn im Textbereich einer grafischen Benutzungsschnittstelle ausgeben, ihn in
eine Datei speichern, ihn über ein Datennetz verschicken, ihn in einem Webbrow-
ser anzeigen oder wie bisher, auf der Konsole ausgeben.
Die Anweisung zur Ausgabe einer Sendung könnte in einem Klienten dann fol-
gendermaßen aussehen:
[Link]([Link]());
Es geht aber noch eleganter, denn für die Methoden [Link] und
[Link] gilt eine Besonderheit: Wenn der Parameter einer dieser Metho-
den nicht vom Typ String ist, dann wird in der Methode automatisch an dem über-
gebenen Objekt die Methode toString aufgerufen. Wir brauchen diesen Aufruf
deshalb gar nicht selbst auszuführen, sondern können stattdessen schreiben:
[Link](einsendung);
Betrachten Sie nun die modifizierte Version der Methode zeigen aus der Klasse
NewsFeed in Listing 11.4. In dieser Version haben wir den Aufruf von toString ent-
fernt. Lässt sich dieser Quelltext übersetzen und fehlerfrei ausführen?

Listing 11.4
Eine neue Version der
Methode zeigen in
der Klasse NewsFeed.

416
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.8 Objektgleichheit: equals und hashCode

Tatsächlich funktioniert diese Methode wie erwartet. Wenn Sie dieses Beispiel im
Detail erklären können, dann haben Sie die meisten Konzepte, die wir in diesem
und dem vorigen Kapitel vorgestellt haben, anscheinend schon recht gut verstan-
den! Hier kommt eine ausführliche Erklärung der Ausgabeanweisung println
innerhalb der Schleife:
 Die for-each-Schleife iteriert über alle Einsendungen und weist sie der Variab-
len mit dem statischen Typ Einsendung zu. Der dynamische Typ ist entweder
NachrichtenEinsendung oder FotoEinsendung.
 Da dieses Objekt mit [Link] ausgegeben wird und es kein String ist, wird
automatisch seine toString-Methode aufgerufen.
 Der Aufruf dieser Methode ist nur deshalb zulässig, weil die Klasse Einsendung
(der statische Typ!) eine toString-Methode hat. (Erinnern Sie sich: Die Typüber-
prüfung erfolgt mit dem statischen Typ. Dieser Aufruf wäre nicht zulässig,
wenn die Klasse Einsendung nicht die Methode toString anbieten würde. Die
Methode toString in der Klasse Object garantiert jedoch, dass jede Klasse diese
Methode anbietet.)
 Die Ausgabe zeigt wie gewünscht alle Details an, weil alle möglichen dynami-
schen Typen (NachrichtenEinsendung und FotoEinsendung) die toString-Methode
überschreiben und die dynamische Methodensuche gewährleistet, dass die
jeweils redefinierende Methode aufgerufen wird.
Die Methode toString ist allgemein nützlich bei der Fehlersuche. Es ist oft ange-
nehm, wenn Objekte sehr leicht vernünftig formatiert ausgegeben werden können.
Die meisten Klassen aus der Java-Bibliothek überschreiben toString (alle Sammlun-
gen beispielsweise können auf diese Weise ausgegeben werden) und für unsere
selbst geschriebenen Klassen ist das meist auch sehr sinnvoll.

11.8 Objektgleichheit: equals und hashCode


In der Praxis stellt sich häufig die Frage, ob zwei Objekte „gleich“ sind. Hierzu defi-
niert die Klasse Object die beiden Methoden equals und hashCode, die sehr eng mit
der Frage nach Gleichheit verknüpft sind. Allerdings sollten wir mit dem Begriff
„Gleichheit“ sehr sorgfältig umgehen, da er im Zusammenhang mit Objekten zwei
ganz verschiedene Dinge bezeichnen kann. Manchmal möchten wir wissen, ob
zwei verschiedene Variablen auf dasselbe Objekt verweisen. Ein solcher Fall liegt
beispielsweise vor, wenn eine Objektvariable einer Methode als Parameter überge-
ben wird: Es gibt nur ein Objekt, aber sowohl die ursprüngliche Variable als auch
die Parametervariable verweisen darauf. Gleiches gilt, wenn eine beliebige Objekt-
variable einer anderen zugewiesen wird. Situationen wie diese führen zu einer
sogenannten Referenzgleichheit. Ob Referenzgleichheit vorliegt, wird mit dem ==-
Operator überprüft. Somit liefert der folgende Test true, wenn var1 und var2 auf
dasselbe Objekt verweisen (oder beide null sind), und false, wenn jede Variable
auf etwas anderes verweist:
var1 == var2
Bei der Referenzgleichheit bleiben die Inhalte der Objekte, auf die Bezug genom-
men wird, völlig unberücksichtigt. Das einzige, was zählt, ist, ob zwei verschie-
dene Variablen auf ein Objekt oder auf zwei verschiedene Objekte verweisen.

417
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Aus diesem Grunde definieren wir neben der Referenzgleichheit die sogenannte
Inhaltsgleichheit. Wenn der Inhalt auf Gleichheit geprüft wird, wird untersucht,
ob zwei Objekte intern gleich sind, das heißt, ob die inneren Zustände von zwei
Objekten gleich sind. Das war der Grund, warum wir für den Vergleich von Zei-
chenketten in Kapitel 6 nicht die Referenzgleichheit verwendet haben.
Was unter Inhaltsgleichheit zwischen zwei Objekten zu verstehen ist, wird von
der Klasse der Objekte definiert. Genau dort machen wir Gebrauch von der
equals-Methode, die jede Klasse von der Superklasse Object erbt. Wenn wir defi-
nieren müssen, was es für zwei Objekte bedeutet, hinsichtlich ihrer internen
Zustände gleich zu sein, müssen wir die equals-Methode überschreiben, die es
gestattet, zwei Objekte wie folgt zu vergleichen:
[Link](var2)
Wir müssen sie überschreiben, weil die von der Klasse Object geerbte equals-
Methode nur auf Referenzgleichheit prüft. Sie sieht in etwa wie folgt aus:
public boolean equals(Object obj)
{
return this == obj;
}
Da die Klasse Object weder über eigene Datenfelder verfügt noch vorausahnen
kann, welche Datenfelder in den Subklassen vorhanden sind, gibt es keinen Zustand,
den sie vergleichen könnte.
Um festzustellen, ob zwei Objekte inhaltsgleich sind, müssen Sie prüfen, ob die
Werte ihrer beiden Datenfeldsätze gleich sind. Beachten Sie jedoch, dass der Para-
meter der equals-Methode vom Typ Object ist, sodass eine Prüfung der Datenfel-
der nur dann sinnvoll ist, wenn Sie Datenfelder des gleichen Typs vergleichen.
Das bedeutet, dass wir erst einmal ermitteln müssen, ob der Typ des als Parame-
ter übergebenen Objekts mit dem Typ des Objekts übereinstimmt, mit dem es
verglichen wird. So könnten wir beispielsweise die Methode in der Klasse Student
aus unserem Projekt Laborkurs aus Kapitel 1 wie folgt schreiben:
public boolean equals(Object obj)
{
if(this == obj) {
return true; // Referenzgleichheit
}
if(!(obj instanceof Student)) {
return false; // nicht derselbe Typ
}
// Zugriff auf die Datenfelder des anderen Studenten
Student anderer = (Student) obj;
return [Link]([Link]) &&
[Link]([Link]) &&
scheine == [Link];
}
Die erste Prüfung dient lediglich der Effizienzsteigerung. Wenn dem Objekt für
den Vergleich eine Referenz auf sich selbst übergeben wurde, dann wissen wir,
dass Inhaltsgleichheit vorliegen muss. Die zweite Prüfung stellt sicher, dass wir
zwei Studenten vergleichen. Wenn nicht, betrachten wir die beiden Objekte

418
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.8 Objektgleichheit: equals und hashCode

automatisch als verschieden. Nachdem wir festgestellt haben, dass das andere
Objekt vom Typ Student ist, bedienen wir uns des Cast-Operators und einer wei-
teren Variable des richtigen Typs, auf deren Details wir nachfolgend problemlos
zugreifen können. Und schließlich nutzen wir die Tatsache, dass die Instanz einer
Klasse direkt auf die privaten Elemente eines anderen Objekts dieser Klasse
zugreifen kann. Dies ist in diesem Falle sehr wichtig, da nicht immer für jedes pri-
vate Datenfeld in einer Klasse sondierende Methoden definiert sind. Beachten
Sie, dass wir die Instanzfelder matrikelnummer und name durchweg auf inhaltliche
Gleichheit geprüft haben und nicht auf Referenzgleichheit.
Es ist nicht immer notwendig, alle Datenfelder der beiden Objekte zu vergleichen,
um festzustellen, ob diese gleich sind. Wenn wir beispielsweise mit Sicherheit wissen,
dass jedem Student eine eindeutige matrikelnummer zugewiesen wurde, müssen wir
die Datenfelder name und scheine nicht überprüfen. Dann wäre es möglich, die
letzte Anweisung wie folgt zu verkürzen:
return [Link]([Link]);
Immer, wenn die equals-Methode überschrieben wird, muss die hashCode-Methode
ebenfalls überschrieben werden. Die hashCode-Methode wird von Datenstruktu-
ren wie HashMap und HashSet verwendet, um ein effizientes Einfügen und Suchen
von Objekten in diesen Sammlungen zu gewährleisten. Im Wesentlichen liefert
die hashCode-Methode einen ganzzahligen Wert zurück, der ein Objekt repräsen-
tiert. Die Standardimplementierung in der Klasse Object versucht für ungleiche
Objekte auch unterschiedliche hashCode-Werte zurückzuliefern.
Die beiden Methoden equals und hashCode sind insofern eng miteinander verbun-
den, als zwei Objekte, die gemäß equals gleich sind, auch identische hashCode-
Werte zurückliefern müssen. Diese Forderung bzw. Übereinkunft ist in der Beschrei-
bung zu hashCode in der API-Dokumentation der Klasse Object zu finden.3 Es
würde den Rahmen dieses Buches sprengen, wenn wir detailliert beschreiben
würden, wie man am besten Hashcodes berechnet. Doch dem interessierten
Leser möchten wir das Werk von Joshua Bloch Effective Java ans Herz legen, des-
sen Technik wir hier einsetzen.4 Letzten Endes geht es darum, auf der Basis der
Werte der Datenfelder, die von der überschriebenen equals-Methode verglichen
werden, einen ganzzahligen Wert zu berechnen. Sehen Sie im Folgenden eine
hypothetische hashCode-Methode, die die Werte eines int-Feldes namens zaehler
sowie ein String-Datenfeld namens name nutzt, um den Code zu berechnen:
public int hashCode()
{
int ergebnis = 17; // ein beliebiger Anfangswert
// Sorge dafür, dass der berechnete Wert von der Reihenfolge
// abhängt, in der die Datenfelder verarbeitet werden
ergebnis = 37 * ergebnis + zaehler;
ergebnis = 37 * ergebnis + [Link]();
return ergebnis;
}

3 Beachten Sie, dass es auf der anderen Seite nicht notwendig ist, dass ungleiche Objekte auch
immer ungleiche Hashcodes zurückliefern.
4 Joshua Bloch, Effective Java, 2. Auflage, Addison-Wesley, 2008.

419
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

11.9 Der Zugriff über protected


Konzept In Kapitel 10 haben wir angemerkt, dass die Regeln über öffentliche und private
Sichtbarkeit von Klasseneigenschaften sowohl zwischen einer Subklasse und
Datenfelder und
ihrer Superklasse als auch zwischen Klassen aus verschiedenen Vererbungshierar-
Methoden, die als
protected dekla- chien gelten. Das kann manchmal etwas restriktiv sein, denn das Verhältnis zwi-
riert sind, sind für schen einer Superklasse und ihren Subklassen ist deutlich enger als das zwischen
(direkte und indi- anderen Klassen. Aus diesem Grund definieren objektorientierte Sprachen oft
rekte) Subklassen eine Zugriffsstufe, die zwischen komplett privatem Zugriff und freier öffentlicher
zugreifbar. Verfügbarkeit liegt. In Java wird diese Stufe protected genannt und entsprechend
durch das Schlüsselwort protected definiert (als Alternative zu private und
public). Listing 11.5 zeigt ein Beispiel einer sondierenden Methode für die Klasse
Einsendung, die als protected deklariert ist.

Listing 11.5
Ein Beispiel für eine als
protected deklarierte
Methode.
Datenfelder und Methoden mit der Zugriffsstufe protected können innerhalb der
Klasse selbst und von allen Subklassen benutzt werden, aber nicht von anderen Klas-
sen.5 Die Methode gibZeitstempel aus Listing 11.5 kann in der Klasse Einsendung
oder in jeder ihrer Subklassen aufgerufen werden, aber nicht in anderen Klassen.
Abbildung 11.8 illustriert dies: Die ovalen Bereiche in dem Diagramm kennzeichnen
jeweils die Gruppe der Klassen, die auf Teile der Klasse EineKlasse zugreifen können.

Abbildung 11.8
Zugriffsstufen:
private, protected
und public.

Obwohl alle Teile einer Klasse als protected deklariert werden können, sollte dies
üblicherweise den Konstruktoren und Methoden vorbehalten bleiben. Datenfelder

5 In Java ist diese Regel nicht so eindeutig, wie hier beschrieben, da Java eine weitere Sichtbar-
keitsstufe kennt, die Paketstufe genannt wird, aber nicht mit einem bestimmten Schlüsselwort
verbunden ist. Wir werden hierauf nicht näher eingehen und Sie sollten im Allgemeinen davon
ausgehen, dass die Zugriffsstufe protected der besonderen Beziehung zwischen Superklasse
und Subklasse vorbehalten ist.

420
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.9 Der Zugriff über protected

werden üblicherweise nicht als protected deklariert, da dies die Kapselung schwä-
chen würde. Soweit möglich, sollten veränderbare Datenfelder in Superklassen pri-
vat bleiben. Es kann allerdings vereinzelte Ausnahmen geben, in denen der direkte
Zugriff auf Datenfelder aus Subklassen sinnvoll ist. Durch Vererbung wird eine sehr
viel engere Kopplung zwischen Klassen definiert als durch Klientenbeziehungen.
Vererbung bindet Klassen stärker aneinander und eine Veränderung an einer Super-
klasse kann leichter zu Problemen in einer Subklasse führen. Dies sollte beim Ent-
wurf von Klassen und ihren Beziehungen berücksichtigt werden.

Übung 11.6 Die Version der Methode anzeigen aus Listing 11.2 erzeugt die
in Abbildung 11.9 gezeigte Ausgabe. Ordnen Sie in Ihrer Version des Netz-
werk-Projekts die Anweisungen in der Methode so um, dass die Details wie
in Abbildung 11.10 ausgegeben werden.

Abbildung 11.9
Mögliche Ausgabe von
Leonardo da Vinci anzeigen: Aufruf der
vor 40 Sekunden. 2 Person(en) gefällt dies. Superklasse zu Anfang
Keine Kommentare von anzeigen (mar-
kierte Bereiche werden
Hatte heute Morgen eine grossartige Idee.
von der Superklasse
Doch jetzt habe ich vergessen, was es war. Es hatte irgendwas mit
ausgegeben).
Fliegen zu tun …

Abbildung 11.10
Alternative Ausgabe
Hatte heute Morgen eine grossartige Idee.
von anzeigen (mar-
Doch jetzt habe ich vergessen, was es war. Es hatte irgendwas mit
kierte Bereiche werden
Fliegen zu tun …
von der Superklasse
Leonardo da Vinci ausgegeben).
vor 40 Sekunden. 2 Person(en) gefällt dies.
Keine Kommentare

Übung 11.7 Die Notwendigkeit, in der Methode anzeigen den super-Aufruf


durchführen zu müssen, schränkt uns etwas bei der Formatierung der Aus-
gabe ein, denn wir sind abhängig davon, wie die Superklasse ihre Daten for-
matiert. Nehmen Sie die notwendigen Änderungen an der Klasse Einsendung
und an der Methode anzeigen in NachrichtenEinsendung vor, sodass die Aus-
gabe aus Abbildung 11.11 produziert wird. Alle Änderungen an der Klasse
Einsendung sollten nur für ihre Subklassen sichtbar sein. Hinweis: Um dies zu
erreichen, könnten Sie der Klasse sondierende protected-Methoden hinzufü-
gen.

421
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Abbildung 11.11
Ausgabe von
anzeigen, in der Leonardo da Vinci
Details der Super- Hatte heute Morgen eine grossartige Idee.
und Subklasse Doch jetzt habe ich vergessen, was es war. Es hatte irgendwas mit
gemischt sind (mar- Fliegen zu tun …
kierte Bereiche kenn-
zeichnen Daten der vor 40 Sekunden. 2 Person(en) gefällt dies.
Superklasse). Keine Kommentare

11.10 Der Operator instanceof


Die Einführung der Vererbung in das Netzwerk-Projekt hatte unter anderem zur
Folge, dass die NewsFeed-Klasse nur Einsendung-Objekte kennt und nicht zwischen
Nachrichten- und Fotoeinsendungen unterscheiden kann. Dies bot die Möglich-
keit, alle Arten von Einsendungen in einer einzelnen Liste zu speichern.
Doch was wäre, wenn wir nur die Nachrichteneinsendungen oder nur die Fotoein-
sendungen aus der Liste herausziehen möchten? Wie würden wir dann vorgehen?
Oder nehmen wir an, wir suchten nach einer Nachricht von einem bestimmten
Autor. Würde die Klasse Einsendung eine Methode gibAutor definieren, wäre dies
an sich kein großes Problem, doch es bliebe dann immer noch die Frage, wie wir
reagieren sollen, wenn einige der Nachrichteneinsendungen und Fotoeinsendungen
denselben Autor haben. Macht es dann einen Unterschied, welcher Typ zurückge-
liefert wird?
Es gibt Fälle, in denen wir den speziellen dynamischen Typ eines Objekts ermit-
teln müssen und uns der gemeinsame Supertyp nicht ausreicht. Hierfür stellt uns
Java den instanceof-Operator zur Verfügung. Er prüft, ob ein gegebenes Objekt
direkt oder indirekt eine Instanz einer gegebenen Klasse ist. Der Test
obj instanceof MeineKlasse
liefert true zurück, wenn der dynamische Typ von obj gleich MeineKlasse oder einer
Subklasse von MeineKlasse ist. Der linke Operand ist immer eine Objektreferenz
und der rechte Operand immer der Name einer Klasse. So liefert zum Beispiel
einsendung instanceof NachrichtenEinsendung
nur dann true zurück, wenn einsendung eine NachrichtenEinsendung und keine
FotoEinsendung ist.
Nach dem Einsatz des instanceof-Operators folgt oft direkt eine Typumwandlung
der Objektreferenz in den identifizierten Typ – wie in dem folgenden Code, der
alle Nachrichteneinsendungen aus einer Liste der Einsendungen herausfiltert und
in einer eigenen Liste speichert:
ArrayList<NachrichtenEinsendung> nachrichten =
new ArrayList<>();
for(Einsendung einsendung : einsendungen) {
if(einsendung instanceof NachrichtenEinsendung) {
[Link](einsendung);
}
}

422
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.11 Ein weiteres Beispiel für Vererbung mit Überschreiben

Es sollte jedoch klar sein, dass die Typumwandlung das Einsendungsobjekt in kei-
ner Weise ändert, da wir gerade festgestellt haben, dass es bereits ein Nachrich-
tenEinsendung-Objekt ist.

11.11 Ein weiteres Beispiel für Vererbung


mit Überschreiben
Um ein weiteres Beispiel mit einer ähnlichen Verwendung von Vererbung zu disku-
tieren, gehen wir zurück zum Zuul-Projekt aus Kapitel 8. In dem Spiel Die Welt von
Zuul haben wir eine Reihe von Raum-Objekten benutzt, um eine Umgebung für ein
einfaches Spiel zu erzeugen. Übung 8.45 schlug vor, dass Sie einen Teleporter-Raum
implementieren (einen Raum, der den Spieler in einen zufälligen anderen Raum
„beamt“, wenn er ihn betritt oder verlässt). Wir betrachten diese Übung hier noch
einmal, weil ihre Lösung sehr stark von Vererbung profitieren kann. Wenn Sie sich
nicht mehr an das Projekt erinnern, schlagen Sie noch einmal kurz in Kapitel 8 nach
oder sehen Sie sich Ihr eigenes Zuul-Projekt an.
Es gibt nicht nur eine Lösung für diese Aufgabe. Viele verschiedene Ansätze sind
denkbar und können implementiert werden. Einige von ihnen können aber
durchaus besser sein als andere – eleganter, einfacher zu lesen, einfacher zu war-
ten oder einfacher zu erweitern.
Nehmen Sie an, dass wir diese Aufgabe so implementieren wollen, dass der Spieler
automatisch in einen zufällig gewählten Raum gebeamt wird, wenn er den Telepor-
ter-Raum verlassen will. Die offensichtlichste Lösung, die den meisten unmittelbar in
den Sinn kommt, implementiert dieses Verhalten in der Klasse Spiel, da dort die
Befehle implementiert sind. Einer der Befehle ist go, der in der Methode wechsleRaum
implementiert ist. In dieser Methode ist die folgende Anweisung der zentrale Quell-
textabschnitt:
naechsterRaum = [Link](richtung);
Diese Anweisung fragt vom aktuellen Raum den Nachbarraum in der angegebe-
nen Richtung ab. Um unsere Teleportation einzubauen, könnten wir diese Anwei-
sung folgendermaßen modifizieren:
if ([Link]().equals("Teleporter-Raum")) {
naechsterRaum = gibZufaelligenRaum();
}
else {
naechsterRaum = [Link](richtung);
}
Die Idee ist einfach: Wir prüfen, ob wir im Teleporter-Raum sind. Wenn ja, dann
lassen wir den nächsten Raum zufällig auswählen (wir müssen die Methode gib-
ZufaelligenRaum natürlich noch implementieren), ansonsten machen wir das Glei-
che wie vorher.
Obwohl diese Lösung funktioniert, hat sie einige Nachteile. Der erste ist, dass der
Raum über seinen Namen identifiziert wird. Nehmen Sie an, jemand wollte dieses
Spiel in eine andere Sprache übersetzen, beispielsweise ins Englische. Dann würde

423
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

der Raum nicht mehr „Teleporter-Raum“ genannt, sondern vielleicht „teleporting


room“ – und das Spiel würde nicht mehr funktionieren! Das ist ganz eindeutig ein
Wartungsproblem.
Die zweite, etwas bessere Lösung würde deshalb eine Instanzvariable anstelle des
Raumnamens verwenden, um den Teleporter-Raum zu identifizieren. Ungefähr so:
if(aktuellerRaum == teleporterRaum) {
naechsterRaum = gibZufaelligenRaum();
}
else {
naechsterRaum = [Link](richtung);
}
Hier nehmen wir an, dass es eine Instanzvariable teleporterRaum vom Typ Raum
gibt, in der wir die Referenz auf unseren Teleporter-Raum speichern.6 Die Über-
prüfung ist nun unabhängig vom Namen des Raumes. Das ist etwas besser.
Es gibt aber immer noch Raum für Verbesserungen. Wir erkennen die Nachteile
dieser Lösung, wenn wir an eine weitere Wartungsänderung denken. Nehmen
Sie an, wir möchten zwei weitere Teleporter-Räume hinzufügen, sodass das Spiel
über drei verschiedene Teleporter-Räume verfügt.
Ein sehr schöner Aspekt unseres bestehenden Entwurfs war, dass wir den Raum-
plan an einer einzigen Stelle festlegen konnten und der Rest des Spiels von dieser
Planung komplett unabhängig war. Wir konnten das Layout der Räume sehr
leicht ändern und trotzdem hat alles noch funktioniert – ein Muster an Wartbar-
keit! Mit unserer aktuellen Erweiterung geht dieser Vorteil jedoch verloren. Wenn
wir zwei weitere Teleporter-Räume hinzufügen, dann müssen wir zwei weitere
Instanzvariablen oder ein Array (für die Referenzen auf die Teleporter-Räume)
definieren und wir müssen die Methode wechsleRaum ändern, damit diese auch
auf die beiden neuen Räume abprüft. Aus Sicht von einfacher Änderbarkeit ist
das ein Schritt rückwärts.
Die entscheidende Frage ist also: Wie können wir eine Lösung finden, bei der
nicht bei jedem neuen Teleporter-Raum die Implementierung eines Befehls geän-
dert werden muss? Hier kommt die nächste Idee.
Wir können in der Klasse Raum eine Methode istTeleporterRaum hinzufügen. Auf
diese Weise muss das Spiel-Objekt nicht alle Teleporter-Räume kennen – die Räume
selbst können diese Information halten. Wenn ein Raum erzeugt wird, dann kann
über einen booleschen Wert entschieden werden, ob es sich um einen Teleporter-
Raum handelt oder nicht. Die Methode wechsleRaum könnte dann den folgenden
Code verwenden:
if([Link]()) {
naechsterRaum = gibZufaelligenRaum();
}
else {
naechsterRaum = [Link](richtung);
}

6 Überlegen Sie sich, warum es hier am sinnvollsten ist, auf Referenzgleichheit zu testen.

424
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.11 Ein weiteres Beispiel für Vererbung mit Überschreiben

Nun können wir so viele Teleporter-Räume hinzufügen, wie wir wollen – die Klasse
Spiel muss nicht jedes Mal angepasst werden. Allerdings hat die Klasse Raum
hierzu ein neues Datenfeld bekommen, das eigentlich nur für ein oder zwei Ins-
tanzen benötigt wird. Solcher Spezialcode ist immer ein Hinweis darauf, dass wir
es mit einem schlechten Entwurf zu tun haben. Dieser Ansatz ist auch nicht gut
erweiterbar für den Fall, dass wir weitere Spezialräume einführen wollen, von
denen jeder seine eigenen Datenfelder und sondierenden Methoden benötigt.7
Mithilfe von Vererbung können wir das besser machen und eine Lösung entwer-
fen, die sogar noch flexibler als die letzte ist. Wir können eine Klasse Teleporter-
Raum als Subklasse der Klasse Raum implementieren. In dieser neuen Klasse können
wir die Methode gibAusgang überschreiben und ihre Implementierung so ändern,
dass sie einen zufällig gewählten Raum zurückliefert:
public class TeleporterRaum extends Raum
{
/**
* Liefere einen zufällig gewählten Raum, unabhängig vom Parameter richtung.
* @param richtung wird ignoriert.
* @return einen zufälligen Ausgang aus diesem Raum.
*/
public Raum gibAusgang(String richtung)
{
return findeZufaelligenRaum();
}

/*
* Wähle zufällig einen Raum aus.
*/
private Raum findeZufaelligenRaum()
{
... // Implementierung hier ausgelassen
}
Diese Lösung ist ausgesprochen elegant, da weder die ursprüngliche Klasse Spiel
noch die Klasse Raum geändert werden muss! Wir können diese Klasse einfach
dem Projekt hinzufügen und die Methode wechsleRaum funktioniert genau wie
vorher. Das Einfügen des Teleporter-Raums bei der Erzeugung der Räume und
ihrer Verbindungen ist (fast) alles, was zu tun ist. Beachten Sie auch, dass die
neue Klasse keine Markierung benötigt, die ihren Sonderstatus signalisiert – ihr
Typ und ihr spezifisches Verhalten sorgen automatisch für diese Unterscheidung.
Weil TeleporterRaum eine Subklasse von Raum ist, können Objekte dieser Klasse an
allen Stellen verwendet werden, an denen Raum-Objekte erwartet werden. Sie
können als Nachbarräume für andere Räume dienen und in der Klasse Spiel als
aktueller Raum gehalten werden.
Wir haben hier die Implementierung der Methode findeZufaelligenRaum ausgelas-
sen. In der Praxis wäre es vermutlich besser, diese Funktionalität in einer eigenen
Klasse (etwa ZufallsraumWaehler) anzubieten als in der Klasse TeleporterRaum selbst.
Wir überlassen diese Entscheidung dem Leser zur Übung.

7 Wir könnten an dieser Stelle auch instanceof verwenden, aber keine dieser Lösungen wäre
wirklich gut.

425
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Übung 11.8 Implementieren Sie einen Teleporter-Raum mithilfe von Ver-


erbung in Ihrer Version des Zuul-Projekts.
Übung 11.9 Diskutieren Sie, wie man mithilfe von Vererbung im Zuul-Pro-
jekt eine Spieler- und eine Monster-Klasse implementieren könnte.
Übung 11.10 Könnte (oder sollte) Vererbung benutzt werden, um eine Verer-
bungsbeziehung (Super-, Sub- oder Geschwisterklasse) zwischen einer Figur des
Spiels und einem Gegenstand zu definieren?

Zusammenfassung
Wenn wir mit Subklassen und polymorphen Variablen zu tun haben, dann
müssen wir zwischen dem statischen und dem dynamischen Typ einer Variab-
len unterscheiden. Der statische Typ ist der deklarierte Typ, während der
dynamische Typ der Typ des Objekts ist, auf das die Variable jeweils verweist.
Typüberprüfungen finden auf Basis des statischen Typs statt, während die
Methodensuche zur Laufzeit auf dem dynamischen Typ basiert. Dies ermöglicht
uns, sehr flexible Strukturen durch Überschreiben von Methoden zu erzeugen.
Auch wenn wir über eine Variable eines Supertyps einen Aufruf durchführen,
garantiert uns das Überschreiben, dass die spezialisierten Methoden des jeweili-
gen Subtyps aufgerufen werden. Auf diese Weise können Objekte verschiede-
ner Klassen unterschiedlich auf denselben Methodenaufruf reagieren.
Wenn wir überschreibende Methoden implementieren, können wir mit dem
Schlüsselwort super die überschriebene Version der Superklasse aufrufen.
Wenn Datenfelder und Methoden mit dem Zugriffsmodifikator protected
deklariert werden, sind sie für Subklassen zugreifbar, für andere Klassen jedoch
nicht.

NEUE BEGRIFFE IN DIESEM KAPITEL


statischer Typ, dynamischer Typ, Überschreiben, Redefinition, Methoden-
suche, Methodenauswahl, Methoden-Polymorphie, protected

426
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.11 Ein weiteres Beispiel für Vererbung mit Überschreiben

Zusammenfassung der Konzepte


 Statischer Typ Der statische Typ einer Variablen v ist der Typ, mit dem die
Variable im Quelltext der Klasse deklariert wurde.
 Dynamischer Typ Der dynamische Typ einer Variablen v ist der Typ des
Objekts, das zurzeit in der Variablen v gehalten wird.
 Überschreiben einer Methode Eine Subklasse kann die Implementie-
rung einer Methode überschreiben. Dazu deklariert die Subklasse eine
Methode mit der gleichen Signatur wie in der Superklasse, implementiert
diese jedoch mit einem anderen Rumpf. Die überschreibende Methode
wird dann bei Aufrufen an Objekte der Unterklasse vorgezogen.
 Methoden-Polymorphie Methodenaufrufe in Java sind polymorph. Der-
selbe Methodenaufruf kann zu unterschiedlichen Zeitpunkten verschie-
dene Methoden aufrufen, abhängig vom dynamischen Typ der Variablen,
mit der der Aufruf durchgeführt wird.
 toString Jedes Objekt in Java bietet eine Methode toString an, die eine
String-Repräsentation des Objekts liefert. Um diese Methode nützlich zu
machen, kann sie in Subklassen überschrieben werden.
 protected Datenfelder und Methoden, die als protected deklariert sind,
sind für (direkte und indirekte) Subklassen zugreifbar.

427
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung

Übung 11.11 Nehmen Sie an, Sie sehen folgende Quelltextzeilen:


Geraet geraet = new Drucker();
[Link]();
Drucker ist eine Subklasse von Geraet. Welche dieser Klassen muss eine
Methode gibName definieren, damit sich diese Zeilen übersetzen lassen?
Übung 11.12 In der gleichen Situation wie in der vorigen Übung: Wenn
beide Klassen eine Implementierung von gibName enthalten, welche wird
ausgeführt?
Übung 11.13 Nehmen Sie an, Sie schreiben eine Klasse Student, die keine
explizit definierte Superklasse hat. Sie schreiben keine toString-Methode.
Betrachten Sie die folgenden Quelltextzeilen:
Student st = new Student();
String s = [Link]();
Lassen sich diese Zeilen übersetzen? Wenn ja, was passiert, wenn Sie sie aus-
führen?
Übung 11.14 In der gleichen Situation wie in der vorigen Übung (Klasse
Student, keine Methode toString): Lassen sich die folgenden Zeilen überset-
zen? Warum?
Student st = new Student();
[Link](st);
Übung 11.15 Nehmen Sie an, Ihre Klasse Student überschreibt toString so,
dass die Methode den Namen des Studenten liefert. Sie haben weiterhin eine
Liste von Studenten. Lassen sich die folgenden Zeilen übersetzen? Wenn nicht,
warum? Wenn ja, was wird ausgegeben? Erklären Sie im Detail, was passiert.
for (Object st : meineListe) {
[Link](st);
}
Übung 11.16 Schreiben Sie einige Zeilen Quelltext, die dazu führen, dass
eine Variable x den statischen Typ T und den dynamischen Typ D hat.

428
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

12 Weitere Techniken zur


Abstraktion

Lernziele
Zentrale Konzepte in diesem Kapitel: abstrakte Klassen, Interfaces, mul-
tiple Vererbung
Java-Konstrukte in diesem Kapitel: abstract, implements, interface

In diesem Kapitel untersuchen wir weitere Techniken, die mithilfe von Vererbungs-
konzepten Entwürfe verbessern und sie wartbarer und erweiterbarer machen. Diese
Techniken führen Verbesserungen ein, mit denen Abstraktionen besser repräsentiert
werden können.
In den beiden vorherigen Kapiteln wurden die wichtigsten Aspekte von Vererbung
für den Entwurf von Anwendungen vorgestellt, aber einige fortgeschrittene Benut-
zungen und Probleme wurden bisher nicht betrachtet. Wir werden das Bild in die-
sem Kapitel mit einem etwas anspruchsvolleren Beispiel abrunden.
Das Projekt, das wir in diesem Kapitel benutzen werden, ist eine Simulation. Wir
setzen es zur Diskussion von Vererbung ein und demonstrieren, dass einige für uns
neue Probleme auftreten. Daraufhin werden abstrakte Klassen und Interfaces ein-
geführt, mit denen diese Probleme gelöst werden können.

12.1 Simulationen
Sehr häufig werden Computer eingesetzt, um Simulationen von realen Systemen
durchzuführen. Dies schließt Verkehrssimulationen, Wettervorhersagen, Simula-
tionen von Atomreaktionen, Börsensimulationen, Umweltsimulationen und vieles
mehr ein. Tatsächlich werden weltweit die meisten Supercomputer für Simulatio-
nen eingesetzt.
Wenn wir eine Computersimulation entwerfen, versuchen wir das Verhalten eines
Ausschnitts aus der realen Welt in einem Softwaremodell zu erfassen. Jede Simula-
tion nimmt dabei zwangsläufig Vereinfachungen vor. Die Entscheidung, welche
Details vernachlässigt werden können und welche berücksichtigt werden sollten,
ist oft sehr anspruchsvoll. Je detaillierter eine Simulation ist, desto genauer kann sie
das Verhalten des realen Systems vorhersagen. Aber mehr Details erhöhen nicht

429
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

nur die Komplexität des Models, sondern fordern auch mehr Rechenleistung und
mehr Entwicklungszeit. Ein bekanntes Beispiel ist die Wettervorhersage: In den
letzten Jahrzehnten wurden die Klimamodelle in den Wettersimulationen immer
detaillierter. Dadurch haben sich Wettervorhersagen in ihrer Genauigkeit erheblich
verbessert (auch wenn sie noch lange nicht perfekt sind, wie wir alle sicher schon
feststellen mussten). Viele dieser Verbesserungen sind auf Fortschritte in der Rech-
nertechnologie zurückzuführen.
Ein Vorteil von Simulationen ist, dass wir Experimente durchführen können, die
wir in der Realität nicht durchführen könnten, entweder weil wir die Realität nicht
kontrollieren können (etwa das Wetter), oder weil es in der Realität zu teuer, zu
gefährlich oder unumkehrbar im Falle eines Scheiterns wäre. Wir können Simula-
tionen einsetzen, um das Verhalten eines Systems unter bestimmten Bedingungen
zu untersuchen oder um „Was wäre, wenn“-Fragen zu beantworten.
Ein Beispiel für den Einsatz von Umweltsimulationen könnte der Versuch sein, die
Auswirkungen von menschlichen Aktivitäten auf die Umwelt vorherzusagen.
Betrachten wir als Beispiel ein Naturschutzgebiet mit bedrohten Tierarten, durch
das eine neue Autobahn gebaut werden soll, die das Gebiet in zwei komplett
getrennte Teile zerschneidet. Die Befürworter des Autobahnprojekts behaupten,
dass die Autobahn nur wenig Landverlust bedeutet und keinen Einfluss auf die
Tiere im Gebiet haben wird, während Umweltaktivisten das Gegenteil behaup-
ten. Wie können wir wissen, welchen Effekt die Autobahn haben wird, wenn wir
sie nicht bauen? Eine Option ist die Simulation. Wo immer eine Simulation he-
rangezogen wird, stellt sich natürlich die Frage nach der Aussagekraft der Simu-
lation. Mit einer zu stark vereinfachenden oder schlecht entworfenen Simulation
kann praktisch alles „bewiesen“ werden. Deshalb ist es immer auch notwendig,
durch kontrollierte Experimente Vertrauen in eine Simulation zu gewinnen.
Im Falle unseres Autobahnprojekts läuft letztlich alles auf die Frage heraus, ob für
das Überleben von Arten ein großer, zusammenhängender Lebensraum notwendig
ist, oder ob zwei getrennte Gebiete (mit der gleichen Gesamtgröße) genauso gut
sind. Anstatt die Autobahn erst zu bauen und dann zu sehen, was passiert, könn-
ten wir die Effekte simulieren, um eine fundierte Entscheidung treffen zu können.1
Da es uns vordringlich um die Einführung neuer Konzepte für objektorientierte
Entwürfe und Implementierungen geht, wird die Simulation, an der wir uns nach-
folgend versuchen, notgedrungen einfacher sein als das oben beschriebene Sze-
nario. Sie wird also nicht das Potenzial haben, viele Aspekte der Natur akkurat
abzubilden, aber es werden sich einige interessante Dinge beobachten lassen. Ins-
besondere werden wir die typische Struktur von Simulationen kennenlernen.
Außerdem wird Sie die Genauigkeit überraschen. Es wäre ein Fehler, davon auszu-
gehen, größere Komplexität mit größerer Genauigkeit gleichzusetzen. Es kommt
oft vor, dass ein vereinfachtes Modell größere Erkenntnisse liefert als ein komple-
xeres, weil es bei Letzterem oft schwer ist, die zugrunde liegenden Mechanismen
zu isolieren – oder einfach nur sicher zu sein, dass das Modell gültig ist.

1 In diesem Beispiel ist der Einfluss, nebenbei bemerkt, signifikant: Die Größe eines Naturschutz-
gebiets hat sehr großen Einfluss auf die Eignung als Lebensraum für Tiere.

430
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.2 Die Fuechse-und-Hasen-Simulation

12.2 Die Fuechse-und-Hasen-Simulation


Die Simulation, die wir für dieses Kapitel gewählt haben, basiert auf dem Auto-
bahnbeispiel und beobachtet die Population von Füchsen und Hasen in einem
abgeschlossenen Feld. Sie ist ein Beispiel für sogenannte Jäger-Beute-Simulatio-
nen. Solche Simulationen werden häufig benutzt, um die Schwankungen in der
Population zu modellieren, die sich aus dem Zusammenleben von Raubtieren mit
ihren Beutetieren ergeben. Meist besteht eine empfindliche Balance zwischen
diesen beiden Arten. Eine hohe Population der Beutetiere liefert potenziell Nah-
rung im Überfluss für die Raubtiere. Zu viele Raubtiere andererseits könnten alle
Beutetiere töten und deshalb letztlich ohne Nahrung zurückbleiben. Die Popula-
tion kann auch durch die Größe oder die Art der Umgebung beeinflusst sein. So
kann beispielsweise ein zu kleiner Lebensraum zur Überpopulation führen und
damit den Raubtieren ihre Beutesuche erleichtern, oder Umweltverschmutzun-
gen könnten die Beutetiere so beeinträchtigen, dass selbst für eine geringe
Anzahl an Raubtieren nicht genug zum Überleben bleibt. Da Raubtiere häufig
selbst wieder Beutetiere für andere Raubtiere sind (zum Beispiel Katzen, Vögel
und Würmer), kann ein Ausfall eines Glieds in der Nahrungskette fatale Folgen
für das Überleben aller anderen Teile haben.
Wie auch in den vorherigen Kapiteln werden wir mit einer Version der Anwen-
dung beginnen, die aus Benutzungssicht einwandfrei funktioniert, deren interne
Struktur jedoch in Hinsicht auf einen guten objektorientierten Entwurf nicht so
gelungen ist. Wir werden diese Basisversion benutzen, um schrittweise mithilfe
von neuen Abstraktionsmechanismen verbesserte Versionen zu erstellen.
Insbesondere wollen wir uns der Tatsache annehmen, dass die Basisversion keinen
guten Gebrauch der Vererbungstechniken macht, die wir in Kapitel 10 kennenge-
lernt haben. Zuerst wollen wir jedoch die Funktionsweise der Simulation verstehen,
ohne die Implementierung zu stark zu kritisieren. Sobald wir ihre Arbeitsweise ver-
standen haben, können wir qualifiziert Verbesserungen vornehmen.

Jäger-Beute-Modellierung
Die mathematische Modellierung von Jäger-Beute-Beziehungen hatte bereits
vor der Erfindung des Computers eine lange Geschichte, da sie sowohl für die
Wirtschaft als auch die Umwelt von Bedeutung sind. So wurden beispiels-
weise am Anfang des zwanzigsten Jahrhunderts mathematische Modelle ent-
wickelt, um die Schwankungen der Fischbestände im Adriatischen Meer als
Nebeneffekt des Ersten Weltkriegs zu klären. Wenn Sie dieses Thema interes-
siert und Sie mehr darüber wissen möchten, um zum Beispiel die Populations-
dynamik besser zu verstehen, sollten Sie im Web nach dem Begriff Lotka-
Volterra-Modell suchen.

431
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

12.2.1 Das Projekt Fuechse-und-Hasen


Öffnen Sie das Projekt Fuechse-und-Hasen-V1. Das zugehörige Klassendiagramm
zeigt Abbildung 12.1.

Abbildung 12.1 Simulationsansicht


Das Klassendiagramm
des Projekts Fuechse-
und-Hasen.
Simulator FeldStatistik

Zaehler

Feld

Position

Fuchs

Hase

Zufallssteuerung

Die zentralen Klassen für unsere Diskussion sind Simulator, Fuchs und Hase. Die Klas-
sen Fuchs und Hase bilden einfache Modelle für eine Raubtier- und für eine Beute-
rasse. Wir haben hier nicht versucht, eine exakte biologische Modellierung von Füch-
sen und Hasen vorzunehmen; vielmehr soll die Anwendung die Prinzipien von Jäger-
Beute-Simulationen verdeutlichen. Unser Hauptaugenmerk liegt auf den Aspekten,
die die Populationsgröße am stärksten beeinflussen: Geburt, Tod und Nahrungs-
angebot.
Die Klasse Simulator stellt den Anfangszustand der Simulation her und kontrol-
liert ihren Ablauf. Der Simulator hält Sammlungen von Füchsen und Hasen und
gibt diesen Tieren wiederholt die Möglichkeit, einen Schritt2 ihres Lebenszyklus
zu durchleben. In jedem Schritt darf jeder Fuchs und jeder Hase die Aktionen
ausführen, die charakteristisch für sein Verhalten sind. Nach jedem Schritt (nach-
dem alle Tiere die Gelegenheit hatten, zu agieren) wird der aktuelle Zustand des
Feldes auf dem Bildschirm angezeigt.

2 Beachten Sie, dass wir nicht definieren, wie lang ein „Schritt“ wirklich dauert. In der Praxis hängt
die Dauer eines Simulationsschritts von einer Reihe von Faktoren ab – wie zum Beispiel, was wir
zu entdecken versuchen, welche Ereignisse wir simulieren und wie viel Echtzeit verfügbar ist, um
die Simulation laufen zu lassen.

432
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.2 Die Fuechse-und-Hasen-Simulation

Wir können die Funktion der übrigen Klassen folgendermaßen zusammenfassen:


 Feld repräsentiert ein zweidimensionales begrenztes Feld. Das Feld besteht aus
einer festgelegten Anzahl von Positionen, die in Zeilen und Spalten angelegt
sind. Eine Position im Feld kann von höchstens einem Tier eingenommen wer-
den. Jede Position im Feld hält eine Referenz auf ein Tier oder ist leer.
 Position repräsentiert eine zweidimensionale Position innerhalb des Felds. Eine
Position wird durch einen Zeilen- und einen Spaltenwert definiert.
 Diese fünf Klassen (Simulator, Fuchs, Hase, Feld und Position) bilden gemeinsam
das Modell der Simulation; sie legen ihr komplettes Verhalten fest.
 Die Zufallssteuerung gibt uns eine gewisse Kontrolle über diejenigen Aspekte
der Simulation, die auf Zufallszahlen basieren (z.B. die Geburt neuer Tiere).
 Die Klassen Simulationsansicht, FeldStatistik und Zaehler kümmern sich um
die grafische Darstellung der Simulation. Die Ansicht der Simulation zeigt den
Zustand des Felds und Zähler für die beteiligten Arten (die Anzahl der Füchse
und Hasen).
 Simulationsansicht visualisiert den Zustand des Felds grafisch. Ein Beispiel ist in
Abbildung 12.2 zu sehen.
 FeldStatistik liefert die Anzahl der Füchse und Hasen im Feld für die Visuali-
sierung.
 Ein Zaehler speichert die aktuelle Anzahl der Exemplare einer Tierart.
Führen Sie die folgenden Übungen durch, um ein Verständnis von der Arbeits-
weise der Simulation zu bekommen, bevor Sie die Implementierung betrachten.

Abbildung 12.2
Die grafische Darstel-
lung der Simulation
von Füchsen und
Hasen.

433
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Übung 12.1 Erzeugen Sie ein Simulator-Objekt mit dem parameterlosen


Konstruktor, sodass der Anfangszustand der Simulation wie in Abbildung
12.2 angezeigt wird. Die zahlreicheren Rechtecke repräsentieren die Hasen.
Ändert sich die Anzahl der Füchse, wenn Sie die Methode simuliereEinen-
Schritt einmal aufrufen?
Übung 12.2 Ändert sich die Anzahl der Füchse mit jedem Schritt? Welche
natürlichen Prozesse werden Ihrer Meinung nach modelliert, die die Anzahl
der Füchse erhöhen oder reduzieren?
Übung 12.3 Rufen Sie die Methode simuliere so auf, dass ein größerer Zeit-
raum von 50 bis 100 Schritten simuliert wird. Verändern sich die Zahlen der
Füchse und Hasen in ähnlicher Weise?
Übung 12.4 Welche Änderungen beobachten Sie, wenn Sie die Simulation
für einen sehr großen Zeitraum laufen lassen, etwa für 4000 Schritte? Sie
können dazu die Methode starteLangeSimulation benutzen.
Übung 12.5 Benutzen Sie die Methode zuruecksetzen, um einen neuen
Anfangszustand für die Simulation zu erzeugen, und starten Sie die Simula-
tion dann erneut. Ist der zweite Lauf identisch mit dem ersten? Wenn nicht:
Erkennen Sie dennoch gemeinsame Muster?
Übung 12.6 Wenn Sie die Simulation lange genug laufen lassen: Sterben
die Füchse oder die Hasen komplett aus? Wenn ja: Können Sie Gründe fest-
machen, warum das so ist?
Übung 12.7 Finden Sie im Quelltext der Klasse Simulator die Methode simu-
liere. In deren Rumpf sehen Sie einen Aufruf für eine Methode verzoegern,
die im Moment noch auskommentiert ist. Aktivieren Sie den Aufruf und las-
sen Sie die Simulation laufen. Experimentieren Sie mit unterschiedlichen Ver-
zögerungen, sodass Sie das Verhalten der Simulation deutlicher beobachten
können. Belassen Sie es am Schluss in einem Zustand, in dem die Simulation
auf Ihrem Rechner brauchbar aussieht.
Übung 12.8 Notieren Sie sich die Anzahl der Füchse und Hasen nach jedem
der ersten Schritte und am Ende eines langen Laufs. Später, wenn wir dazu
übergehen, Änderungen vorzunehmen und Regressionstests durchzufüh-
ren, werden sich diese Notizen als recht nützlich erweisen.
Übung 12.9 Lassen Sie die Simulation eine Weile durchlaufen. Anschließend
rufen Sie die statische Methode zuruecksetzen der Klasse Zufallssteuerung auf
und dann die Methode zuruecksetzen des Simulator-Objekts. Führen Sie jetzt
die ersten Schritte erneut aus. Die ursprüngliche Simulation wird wieder-
holt. Versuchen Sie dem Code der Klasse Zufallssteuerung zu entnehmen,
woran das liegt. Ziehen Sie gegebenenfalls die API-Dokumentation der
Klasse [Link] zurate.
Übung 12.10 Überprüfen Sie, ob Sie den identischen Lauf der Simulation
aus Übung 12.9 dadurch aufheben können, indem Sie das Datenfeld nutze-
Gemeinsam in Zufallssteuerung auf false setzen. Hinterher sollten Sie das Daten-
feld jedoch wieder auf true setzen, da die Wiederholbarkeit für das spätere Tes-
ten von großer Bedeutung ist.

434
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.2 Die Fuechse-und-Hasen-Simulation

Nachdem wir einen allgemeinen Eindruck davon haben, was dieses Projekt macht,
betrachten wir in den folgenden Abschnitten die Implementierungen der Klassen
Hase, Fuchs und Simulator.

12.2.2 Die Klasse Hase


Der Quelltext der Klasse Hase ist in Listing 12.1 gezeigt.

Listing 12.1
Die Klasse Hase.

435
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Die Klasse Hase enthält eine Reihe von Klassenvariablen, die Einstellungen für alle
Hasen definieren. Dies schließt das Höchstalter ein, das ein Hase erreichen kann
(als Anzahl von Schritten), und die maximale Größe eines Wurfes, den ein Hase
in einem Schritt gebären kann. Die zentrale Steuerung der Zufallsaspekte der
Simulation wird durch ein einzelnes gemeinsam genutztes Random-Objekt der
Klasse Zufallssteuerung gewährleistet. Das ermöglicht die Wiederholbarkeit, wie
wir sie in Übung 12.9 kennengelernt haben. Jeder Hase hat außerdem Instanz-
variablen, die seinen Zustand beschreiben: sein Alter in Schritten, ob er noch leben-
dig ist und seine Position im Feld.

436
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.2 Die Fuechse-und-Hasen-Simulation

Übung 12.11 Haben Sie den Eindruck, dass das Auslassen des Geschlechts
als Attribut in der Klasse Hase zu einer nicht akkuraten Simulation führt?
Übung 12.12 Sehen Sie weitere Vereinfachungen in unserer Implementie-
rung der Klasse Hase, verglichen mit echten Hasen? Haben Sie den Eindruck,
dass diese die Simulation in ihrer Genauigkeit stark beeinflussen?
Übung 12.13 Experimentieren Sie mit Änderungen an den Werten der
Klassenvariablen in der Klasse Hase. Welchen Einfluss auf die Population von
Füchsen und Hasen hat es beispielsweise, wenn die Gebärwahrscheinlich-
keit für Hasen sehr viel niedriger oder sehr viel höher ist?

Das Verhalten eines Hasen ist in der Methode laufe definiert, die wiederum die
Methoden gebaereNachwuchs und alterErhoehen benutzt. In jedem Simulations-
schritt wird die Methode laufe aufgerufen, sodass ein Hase sein Alter erhöht,
sich bewegt und, wenn er alt genug ist, Nachwuchs gebärt. Sowohl die Bewe-
gung als auch das Gebärverhalten sind durch Zufall beeinflusst: Die Bewegungs-
richtung wird zufällig ausgewählt und auch das Gebären findet zufällig statt,
beeinflusst durch die Klassenvariable GEBAER_WAHRSCHEINLICHKEIT.
Sie können hier bereits erkennen, dass in diesem Modell eines Hasen etliche Ver-
einfachungen vorgenommen wurden: Männliche und weibliche Tiere werden
beispielsweise nicht unterschieden, und ein Hase kann potenziell in jedem Schritt
Nachwuchs gebären, sofern er alt genug ist.

12.2.3 Die Klasse Fuchs


Es gibt etliche Ähnlichkeiten zwischen den Klassen Fuchs und Hase. Wir zeigen
deshalb nur die abweichenden Abschnitte von Fuchs in Listing 12.2.

Listing 12.2
Die Klasse Fuchs.

437
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

438
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.2 Die Fuechse-und-Hasen-Simulation

Für jeden Fuchs wird in jedem Schritt die Methode jage aufgerufen, die sein Ver-
halten definiert. Zusätzlich zum Altern und Nachwuchsgebären sucht ein Fuchs
auch nach Nahrung (mit der Methode findeNahrung). Wenn er einen Hasen in
einer Nachbarposition finden kann, dann wird der Hase gefressen und der Futter-
Level des Fuchses wird erhöht. Wie bei den Hasen wird ein Fuchs, der sich nicht
mehr bewegen kann, als tot, d.h. als Opfer der Überbevölkerung betrachtet.

Übung 12.14 Wie schon bei den Hasen sollen Sie untersuchen, inwieweit wir
das Modell eines Fuchses vereinfacht haben. Beurteilen Sie, ob diese Vereinfa-
chungen zu einer unrealistischen Simulation führen.
Übung 12.15 Führt ein höheres Maximalalter für Füchse zu einer deutlich
höheren Anzahl an Füchsen in der Simulation oder wird die Population der
Hasen dadurch komplett ausgelöscht?
Übung 12.16 Experimentieren Sie mit verschiedenen Kombinationen von
Einstellungen (gebärfähiges Alter, Maximalalter, Gebärwahrscheinlichkeit,
Wurfgröße etc.) für Füchse und Hasen. Verschwindet eine Spezies immer kom-
plett in einigen Einstellungen? Gibt es Konfigurationen, die stabile Populatio-
nen erzeugen – d.h., über eine längere Zeit ein Gleichgewicht der Anzahl
herstellen?
Übung 12.17 Experimentieren Sie mit unterschiedlichen Feldgrößen. (Sie
können dazu den zweiten Konstruktor von Simulator verwenden.) Beein-
flusst die Größe des Felds die Wahrscheinlichkeit zum Überleben einer Spe-
zies?
Übung 12.18 Vergleichen Sie die Ergebnisse nach der Ausführung einer
Simulation mit einem einzelnen großen Feld und von zwei Simulationen mit
Feldern, die jeweils nur halb so groß sind wie das einzelne Feld. Dies model-
liert in etwa die Aufteilung eines Gebietes in zwei Hälften durch eine Auto-
bahn. Stellen Sie irgendwelche Unterschiede in der Populationsdynamik
zwischen den beiden Szenarien fest?
Übung 12.19 Wiederholen Sie die Untersuchungen der vorherigen Übung,
aber variieren Sie die Proportionen der zwei kleineren Felder (z.B. drei Viertel
und ein Viertel oder zwei Drittel und ein Drittel). Spielt es überhaupt eine
Rolle, wie das einzelne Feld aufgeteilt wird?
Übung 12.20 Momentan frisst ein Fuchs maximal einen Hasen pro Schritt.
Modifizieren Sie die Methode findeNahrung so, dass die Hasen aller Nachbar-
positionen in einem Schritt gefressen werden. Überprüfen Sie die Auswir-
kungen dieser Änderung auf die Ergebnisse der Simulation. Beachten Sie,
dass die Methode findeNahrung zurzeit nur die Position des einen Hasen
zurückliefert, der bisher gefressen wurde. Das heißt, Sie müssen Ihre Version
so umschreiben, dass sie die Position eines der gefressenen Hasen zurücklie-
fert. Vergessen Sie auch nicht, null zurückzugeben, wenn es keine Hasen
zum Fressen gibt.

439
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Übung 12.21 Bauen wir die vorherige Übung ein wenig aus: Wenn ein
Fuchs eine große Anzahl an Hasen in einem Schritt frisst, können wir auf
unterschiedliche Weise seinen Futter-Level modellieren. Wenn wir den Nähr-
wert aller Hasen zum Erhöhen des Futter-Levels benutzen, dann hat ein
Fuchs einen sehr hohen Futter-Level und wird sehr lange nicht vor Hunger
sterben. Alternativ könnte ein Höchstwert für den Futter-Level von Füchsen
eingeführt werden. Dies würde ein Raubtier modellieren, das auch tötet,
wenn es nicht hungrig ist. Überprüfen Sie die Auswirkungen einer Imple-
mentierung dieser Möglichkeit auf die resultierende Simulation.
Übung 12.22 Zusatzaufgabe. Begründen Sie auf Basis der verwendeten
Zufallselemente, warum die Populationszahlen in einer anscheinend stabilen
Simulation schlussendlich zusammenbrechen könnten.

12.2.4 Die Klasse Simulator: die Initialisierung


Die Klasse Simulator ist das Herzstück der Simulation, das alle anderen Teile koor-
diniert. Listing 12.3 zeigt die wichtigsten Abschnitte aus ihrem Quelltext.

Listing 12.3
Teile der Klasse
Simulator.

440
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.2 Die Fuechse-und-Hasen-Simulation

Der Simulator besteht aus drei wichtigen Teilen: seinem Konstruktor, der Methode
bevoelkere und der Methode simuliereEinenSchritt. (Der Rumpf der Methode
simuliereEinenSchritt wird in Listing 12.4 gezeigt.)

Wenn ein Simulator-Objekt erzeugt wird, werden auch alle anderen Teile einer
Simulation erzeugt (das Feld, die Listen für die Tiere und die grafische Darstel-

441
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

lung). Nachdem diese Teile initialisiert wurden, wird die Methode bevoelkere des
Simulators aufgerufen (indirekt über die Methode zuruecksetzen), um die Anfangs-
population der Füchse und Hasen einzusetzen. Verschiedene Wahrscheinlichkei-
ten werden benutzt, um für eine Position zu entscheiden, ob sie ein Tier enthält
oder nicht. Beachten Sie, dass die Tiere, die zu Beginn der Simulation erzeugt
werden, ein zufälliges Alter bekommen. Dies dient zwei Zwecken:
1 Es repräsentiert genauer eine Population mit verschieden alten Tieren, die
der Normalzustand der Simulation sein sollte.

2 Wenn alle Tiere mit dem Alter null beginnen würden, würden keine neuen
Tiere geboren, bis die Anfangspopulation das jeweilige gebärfähige Alter er-
reicht hat. Mit Füchsen, die Hasen unabhängig von ihrem eigenen Alter
fressen, werden entweder die Hasen ausgerottet, bevor sie sich reproduzie-
ren können, oder die Füchse würden aufgrund von Hunger aussterben.

Übung 12.23 Modifizieren Sie die Methode bevoelkere in der Klasse Simulator,
um festzustellen, ob es zu einer Katastrophe führt, wenn das Anfangsalter
für Füchse und Hasen auf 0 gesetzt wird. Stellen Sie sicher, dass Sie die
Methode oft genug ausführen – jeweils mit unterschiedlichen Anfangs-
zuständen natürlich!
Übung 12.24 Wenn ein zufälliges Anfangsalter für Hasen, aber nicht für
Füchse vergeben wird, dann neigt die Population der Hasen zum schnellen
Wachsen, während die Population der Füchse relativ gering bleibt. Wenn
die Füchse dann alt genug sind, um Nachwuchs zu gebären: Verhält sich die
Simulation dann genauso wie die ursprüngliche Version? Was sagt dies über
das Verhältnis der beiden Anfangspopulationen und ihre Auswirkung auf
das Ergebnis der Simulation?

12.2.5 Die Klasse Simulator: ein Simulationsschritt


Der zentrale Teil der Klasse Simulator ist die Methode simuliereEinenSchritt, die
in Listing 12.4 gezeigt ist. Diese benutzt getrennte Schleifen, um jeder Tierart
Gelegenheit zum Bewegen zu geben (oder zum Gebären oder was sonst zum
jeweiligen Verhalten gehört). Da jedes Tier im aktuellen Schritt Nachkommen
gebären kann, werden den Methoden laufe und jage der Klassen Fuchs und Hase
passende Listen als Parameter übergeben, in denen die neugeborenen Tiere am
Ende eines Simulationsschrittes gespeichert werden können. Längere Simulatio-
nen durchzuführen ist trivial: Die Methode simuliereEinenSchritt wird in einer
einfachen Schleife mehrfach aufgerufen.

Um jedes Tier agieren zu lassen, hält der Simulator getrennte Listen für die ver-
schiedenen Tierarten. Wir machen hier keinen Gebrauch von Vererbung, und die
Situation ist sehr ähnlich der im ersten Netzwerk-Projekt, das wir aus Kapitel 10
kennen.

442
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.2 Die Fuechse-und-Hasen-Simulation

Listing 12.4
Innerhalb der Klasse
Simulator: ein Simu-
lationsschritt.

Übung 12.25 Alle Tiere werden gleichzeitig in zwei verschiedenen Daten-


strukturen verwaltet: im Feld und in den Listen hasen und fuechse von Simu-
lator. Es besteht daher die Gefahr, dass diese Datenstrukturen nicht über-
einstimmen. Prüfen Sie, ob Sie auch genau verstanden haben, wie das Feld
und die Tierlisten in der Methode simuliereEinenSchritt der Klasse Simulator,
der Methode jage der Klasse Fuchs und der Methode laufe der Klasse Hase
konsistent gehalten werden.
Übung 12.26 Meinen Sie, es wäre besser, in der Klasse Simulator keine
separaten Listen für Füchse und Hasen zu führen, sondern diese Listen aus
dem Inhalt des Feldes am Anfang eines jeden Simulationsschrittes neu zu
erzeugen? Diskutieren Sie die Vor- und Nachteile.
Übung 12.27 Schreiben Sie einen Test, um sicherzustellen, dass am Ende
eines Simulationsschrittes kein Tier (tot oder lebendig) mehr im Feld ist, das
nicht in einer der Listen steht, und umgekehrt. Gibt es zu diesem Zeitpunkt
überhaupt irgendwelche toten Tiere in den Listen?

12.2.6 Verbesserungen der Simulation


Nachdem wir nun wissen, wie die Simulation abläuft, können wir als Nächstes Ver-
besserungen am internen Entwurf und an der Implementierung vornehmen. Im
Fokus der nächsten Abschnitte steht die schrittweise Verbesserung durch Einfüh-
rung neuer Programmiertechniken. Wir könnten an verschiedenen Punkten anset-
zen, aber eine der offensichtlichsten Schwächen ist, dass in den Klassen Fuchs und

443
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Hase, die vieles gemein haben, nicht die Vorteile von Vererbung genutzt wurden.
Um dies zu verbessern, stellen wir nun das Konzept einer abstrakten Klasse vor.

Übung 12.28 Finden Sie Gemeinsamkeiten und Unterschiede zwischen der


Klasse Fuchs und der Klasse Hase. Erstellen Sie separate Listen der Datenfel-
der, Methoden und Konstruktoren und unterscheiden Sie auch Klassenvariab-
len (statische Variablen) von Instanzvariablen.
Übung 12.29 Offensichtliche Kandidaten für Methoden, die in eine Super-
klasse verlagert werden sollten, sind diejenigen, die in allen Subklassen identisch
sind. Für welche Methoden in den Klassen Fuchs und Hase trifft dies zu? Bei der
Untersuchung könnte Ihnen helfen, wenn Sie die Werte von Klassenvariablen in
den Rümpfen, in denen sie benutzt werden, direkt einsetzen würden.
Übung 12.30 In der aktuellen Version der Simulation sind die Werte aller
gleich benannten Klassenvariablen unterschiedlich. Wenn die beiden Werte
einer bestimmten Klassenvariablen gleich wären (beispielsweise GEBAER_ALTER),
würde dies Ihre Einschätzung, welche Methoden gleich sind, beeinflussen?

12.3 Abstrakte Klassen


Kapitel 10 hat Konzepte wie Vererbung und Polymorphie eingeführt, die wir in
unserer Simulation einsetzen sollten. Beispielsweise haben die Klassen Fuchs und
Hase viele Ähnlichkeiten, die es nahelegen, dass diese Klassen Subklassen einer
gemeinsamen Superklasse sein sollten, beispielsweise Tier. In diesem Abschnitt
beginnen wir mit solchen Änderungen, damit Entwurf und Implementierung der
Anwendung insgesamt verbessert werden. Wie schon bei dem Projekt aus
Kapitel 10 sollte eine gemeinsame Superklasse Code-Duplizierung in den Subklas-
sen vermeiden und den Code in Klientenklassen (hier: Simulator) vereinfachen. Es ist
wichtig, hier anzumerken, dass wir nun einige Refactorings vornehmen wollen;
diese dürfen nicht die zentralen Eigenschaften der Simulation aus der Sicht eines
Benutzers beeinflussen.

12.3.1 Die Superklasse Tier


Für unseren ersten Satz an Änderungen werden wir identische Teile aus den Klas-
sen Fuchs und Hase in eine Superklasse Tier verlagern. Das Projekt Fuechse-und-
Hasen-V1 enthält die Basisversion, von der aus wir die folgenden Änderungen
nachvollziehen können.
 Sowohl Fuchs als auch Hase definieren die Datenfelder alter, lebendig, feld und
position. An dieser Stelle werden wir jedoch nur lebendig, position und feld in
die Superklasse Tier verschieben und auf das Datenfeld alter später eingehen.
Wie gewohnt werden wir die Datenfelder in der Superklasse als private dekla-
rieren. Die Anfangswerte werden im Konstruktor von Tier gesetzt, wobei
lebendig auf true gesetzt wird und feld und position ihre Werte über super-
Aufrufe von den Konstruktoren der Klassen Fuchs und Hase erhalten.

444
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.3 Abstrakte Klassen

 Da diese Felder verändernde und sondierende Methoden benötigen, verschieben


wir die bestehenden Methoden gibPosition, setzePosition, istLebendig und
sterben von Fuchs und Hase nach Tier. Außerdem müssen wir Tier eine Methode
gibFeld hinzufügen, um in den Subklassen-Methoden laufe, jage, gebaereNach-
wuchs und findeNahrung den direkten Zugriff auf feld adäquat ersetzen zu kön-
nen.
 Beim Verschieben dieser Methoden müssen wir uns Gedanken über ihre Sicht-
barkeit machen. So ist zum Beispiel setzePosition sowohl in Fuchs als auch in
Hase privat, kann diese Sichtbarkeit jedoch in Tier nicht beibehalten, da sonst
Fuchs und Hase die Methode nicht aufrufen könnten. Deshalb sollten wir den
Zugriffsmodus in protected ändern, um anzuzeigen, dass die Methode von
Subklassen aufgerufen werden kann.
 Ein ähnlicher Fall liegt bei der Methode sterben vor, die in Hase public, aber in
Fuchs private ist. Sollte in Tier deshalb die Sichtbarkeit public sein? Die
Methode wurde in Hase als public definiert, weil ein Fuchs die Möglichkeit
haben muss, die sterben- Methode eines Hasen aufzurufen, wenn er seine
Beute frisst. Jetzt, wo beide Geschwisterklassen einer gemeinsamen Super-
klasse sind, wäre protected die geeignetere Sichtbarkeit, wodurch wiederum
angezeigt würde, dass diese Methode nicht Teil der allgemeinen Schnittstelle
eines Tiers ist – zumindest nicht auf dieser Stufe der Projektentwicklung.
Diese Änderungen sind ein erster Schritt, Code-Duplizierung durch den Einsatz
von Vererbung zu vermeiden. Die Vorgehensweise entspricht der in Kapitel 10.

Übung 12.31 Welche Strategie für Regressionstests könnten Sie sich vor-
stellen, bevor wir mit dem Refactoring der Simulation beginnen? Könnte
diese Strategie leicht automatisiert werden?
Übung 12.32 Mit der Klasse Zufallsteuerung können wir steuern, ob die
„zufälligen“ Elemente der Simulation wiederholbar sind oder nicht. Wenn
ihr Datenfeld nutzeGemeinsam auf true gesetzt ist, dann wird ein einzelnes
Random-Objekt von allen Simulationsobjekten gemeinsam genutzt. Außer-
dem wird mit der Zufallsteuerung-Methode zuruecksetzen der Anfangspunkt
für das gemeinsam genutzte Random-Objekt zurückgesetzt. Nutzen Sie diese
Möglichkeiten bei der folgenden Übung, um sicherzustellen, dass Sie nicht
irgendetwas Wichtiges an der Gesamtsimulation ändern, wenn Sie die neue
Klasse Tier einführen.
Erzeugen Sie eine Superklasse Tier in Ihrer Version des Projekts. Führen Sie die
oben diskutierten Änderungen durch. Vergewissern Sie sich, dass die Simula-
tion noch genau wie vorher funktioniert. Sie können dies prüfen, indem Sie
zum Beispiel die alte und neue Version des Projekts nebeneinander geöffnet
haben und in beiden identische Aufrufe der Simulator-Objekte abschicken –
die dann erwartungsgemäß zu identischen Ergebnissen führen sollten.
Übung 12.33 Wie sehr hat die Benutzung von Vererbung das Projekt bisher
verbessert? Diskutieren Sie.

445
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

12.3.2 Abstrakte Methoden


Bisher hat die Einführung der Superklasse Tier uns bei der Vermeidung von umfang-
reicher Code-Duplizierung in den Klassen Fuchs und Hase geholfen; und sie macht es
leichter, zukünftig weitere Tierklassen einzuführen. Wie wir jedoch in Kapitel 10
gesehen haben, sollte der intelligente Einsatz von Vererbung aber auch die Klienten
vereinfachen – hier die Klasse Simulator. Das werden wir nun untersuchen.
In der Klasse Simulator haben wir getrennte Listen von Füchsen und Hasen benutzt
und getrennt über beide Listen iteriert, um einen Simulationsschritt zu implementie-
ren. Der entsprechende Quelltext dazu ist in Listing 12.4 zu sehen. Mit der neuen
Klasse Tier können wir diese Situation nun verbessern. Da alle Objekte in unseren
Tier-Sammlungen Subtypen von Tier sind, können wir die Listen in einer Sammlung
zusammenfassen und müssen nur noch einmal mit dem Tier-Typ iterieren. Ein Prob-
lem einer Lösung mit nur einer Liste wird jedoch in Listing 12.5 deutlich. Obwohl wir
wissen, dass jedes Element in der Liste ein Tier ist, müssen wir dennoch herausfin-
den, welche Art von Tier es ist, um die richtige Methode zum Handeln für den
jeweiligen Typ (in diesem Fall laufe oder jage) aufrufen zu können. Wir ermitteln
diesen Typ mit dem instanceof-Operator.

Listing 12.5
Ein unbefriedigender
Ansatz, die Tiere einer
einzelnen Liste agie-
ren zu lassen.

Die Tatsache, dass in Listing 12.5 für jeden Tiertyp einzeln getestet und der Cast-
Operator angewendet werden muss und dass spezieller Code für jede Tierklasse
existiert, ist ein deutlicher Hinweis darauf, dass wir noch nicht alle Vorteile der Ver-
erbung ausgeschöpft haben. Wenn wir stattdessen sicherstellen, dass die Super-
klasse (Tier) eine Methode hat, die Tiere agieren lässt, und dass diese Methode in
jeder Subklasse redefiniert wird, dann können wir einen polymorphen Methoden-
aufruf benutzen, um jedes Tier agieren zu lassen, ohne auf einen speziellen Tiertyp
testen zu müssen. Dies ist eine Standard-Refactoring-Technik, die in Situationen
eingesetzt wird, in denen subtypspezifisches Verhalten aus einem Kontext aufge-
rufen wird, der nur mit dem Supertyp arbeitet.
Nehmen wir an, dass wir eine solche Methode – nennen wir sie agiere – definieren
und untersuchen wir den resultierenden Quelltext. Listing 12.6 zeigt den Quelltext
mit dieser Lösung.

446
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.3 Abstrakte Klassen

Listing 12.6
Ein vollständig über-
arbeiteter Ansatz,
Tiere agieren zu
lassen.

An dieser Stelle sind einige wichtige Dinge zu beobachten:


 Die Variable, die wir für jedes Sammlungselement benutzen (tier), ist vom Typ
Tier. Das ist zulässig, weil alle Objekte in der Sammlung Füchse oder Hasen
sind und somit Subtypen von Tier.
 Wir gehen davon aus, dass die spezifischen Methoden zum Agieren (laufe für
Hase, jage für Fuchs) in agiere umbenannt wurden. Statt jedem Tier mitzuteilen,
was es tun soll, soll es nun einfach agieren, und es bleibt dem Tier selbst über-
lassen, wie es agieren möchte. Dies verringert die Kopplung zwischen der Klasse
Simulator und den individuellen Tier-Subklassen.
 Weil der dynamische Typ einer Variablen darüber entscheidet, welche Methode tat-
sächlich ausgeführt wird (wie in Kapitel 11 diskutiert), wird die Methode zum Agie-
ren aus der Klasse Fuchs für Füchse und die der Klasse Hase für Hasen ausgeführt.
 Da die Typüberprüfung nur den statischen Typ berücksichtigt, kann dieser
Quelltextabschnitt nur übersetzt werden, wenn die Klasse Tier eine Methode
agiere mit dem richtigen Kopf definiert.

Der letzte dieser Punkte stellt das einzige verbleibende Problem dar. Da wir eine
Anweisung
[Link](neueTiere);
benutzen und die Variable tier vom Typ Tier ist, wissen wir aus Kapitel 11, dass
dies nur übersetzt werden kann, wenn Tier eine Methode agiere definiert. Aller-
dings haben wir hier eine Situation, die sich von der unterscheidet, die wir mit
der Methode anzeigen in der Klasse Einsendung vorgefunden haben. Dort konnte
die Methode anzeigen in der Superklasse eine sinnvolle Aufgabe erfüllen – sie hat
die Datenfelder der Superklasse ausgegeben. Hier können wir, obwohl jedes Tier
eine bestimmte Menge an Aktionen ausführen muss, nicht im Detail beschrei-
ben, was ein Tier im Allgemeinen tun soll. Die jeweilige Aktion hängt von der
konkreten Unterklasse ab.
Unser Problem besteht darin, wie die Methode agiere in der Klasse Tier definiert
werden soll.
Dieses Problem spiegelt die Tatsache wider, dass es niemals eine Instanz der Klasse Konzept
Tier geben wird. Es gibt kein Objekt in unserer Simulation (oder in der Natur), das
Die Definition einer
ausschließlich ein Tier ist und nicht auch eine Instanz einer spezifischen Subklasse.
abstrakten
Solche Klassen, die niemals selbst für die Erzeugung von Objekten verwendet wer- Methode
den sollen und lediglich als Superklassen dienen, werden abstrakte Klassen genannt. besteht aus einem
Für Tiere beispielsweise können wir sagen, dass jedes Tier in irgendeiner Weise Methodenkopf
agiert, aber wir können nicht genau sagen, wie es agiert, ohne uns auf eine konkrete ohne einen Rumpf.
Subklasse zu beziehen. Dies ist typisch für abstrakte Klassen und kann entsprechend Sie wird mit dem
mit Java-Konstrukten modelliert werden. Schlüsselwort
abstract markiert.

447
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Für die Klasse Tier möchten wir definieren, dass es eine Methode agiere geben
soll, aber ohne eine vernünftige Implementierung angeben zu können. Die Lösung
in Java ist, diese Methode als abstrakt zu deklarieren. Hier ist ein Beispiel für eine
abstrakte Methode agiere:
abstract public void agiere(List<Tier> neueTiere);
Eine abstrakte Methode hat zwei definierende Merkmale:
1 Sie wird mit dem Schlüsselwort abstract definiert.
2 Sie hat keinen Rumpf. Stattdessen wird der Kopf der Methode durch ein Se-
mikolon abgeschlossen.
Da die Methode keinen Rumpf hat, kann sie niemals ausgeführt werden. Aber
wir haben bereits festgestellt, dass wir keine Methode agiere von einem Tier aus-
führen wollen, also ist das kein Problem.
Bevor wir uns genauer die Effekte bei der Benutzung von abstrakten Methoden
ansehen, sollten wir zuerst das Konzept einer abstrakten Klasse etwas formaler
vorstellen.

12.3.3 Abstrakte Klassen


Konzept Nicht nur Methoden können als abstrakt deklariert werden; auch Klassen können
als abstrakt deklariert werden. Listing 12.7 zeigt ein Beispiel einer Klasse Tier als
Eine abstrakte
abstrakte Klasse. Klassen werden als abstrakt deklariert, indem das Schlüsselwort
Klasse ist eine
Klasse, von der keine abstract in der Kopfzeile der Klasse angegeben wird.
Instanzen erzeugt
werden sollen. Sie
dient ausschließlich
als Superklasse für
andere Klassen.
Abstrakte Klassen
dürfen abstrakte
Methoden anbieten.

Listing 12.7
Tier als eine
abstrakte Klasse.

Klassen, die nicht abstrakt sind (wie alle bisher betrachteten Klassen), werden kon-
krete Klassen genannt.
Eine Klasse als abstrakt zu definieren dient mehreren Zwecken:
 Von abstrakten Klassen können keine Instanzen erzeugt werden. Der Versuch,
das Schlüsselwort new mit einer abstrakten Klasse zu benutzen, führt zu einem
Fehler und wird vom Compiler nicht erlaubt. Dies spiegelt sich auch in BlueJ
wider: Ein Rechtsklick auf eine abstrakte Klasse im Klassendiagramm listet keine
Konstruktoren im Kontextmenü auf. Dies unterstützt unsere oben formulierte
Absicht: Wir haben gesagt, dass wir keine direkten Instanzen der Klasse Tier
ermöglichen wollen – die Klasse soll ausschließlich als Superklasse dienen. Die
Klasse als abstrakt zu definieren unterstützt diese Beschränkung.

448
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.3 Abstrakte Klassen

 Nur abstrakte Klassen dürfen abstrakte Methoden definieren. Dies stellt sicher,
dass alle Methoden von konkreten Klassen immer ausgeführt werden können.
Wenn wir abstrakte Methoden in konkreten Klassen zulassen würden, dann
könnten wir Instanzen einer Klasse erzeugen, der eine Implementierung für
eine Methode fehlt.
 Abstrakte Klassen mit abstrakten Methoden erzwingen, dass Subklassen die
abstrakt deklarierten Methoden überschreiben und implementieren. Wenn
eine Subklasse keine Implementierung für eine geerbte abstrakte Methode
anbietet, dann ist sie selbst abstrakt und es können keine Instanzen von ihr
erzeugt werden. Damit eine Subklasse konkret sein kann, muss sie Implemen-
tierungen für alle abstrakten Methoden anbieten.
Jetzt erkennen wir langsam den Zweck von abstrakten Methoden: Obwohl sie keine Konzept
Implementierung anbieten, garantieren sie dennoch, dass alle konkreten Subklassen
Abstrakte Sub-
eine Implementierung dieser Methode anbieten müssen. Mit anderen Worten:
klassen: Damit
Obwohl die Klasse Tier keine Implementierung der Methode agiere anbietet, eine Subklasse
garantiert sie, dass alle existierenden Tiere eine Implementierung der Methode einer abstrakten
agiere haben. Dies wird garantiert, weil Klasse eine kon-
 keine direkte Instanz der Klasse Tier erzeugt werden kann, krete Klasse wer-
den kann, muss sie
 alle konkreten Subklassen die Methode agiere implementieren müssen. Implementierun-
Obwohl wir keine Instanzen einer abstrakten Klasse erzeugen können, können wir gen für alle geerb-
ten abstrakten
eine abstrakte Klasse als Typ auf die übliche Weise benutzen. Beispielsweise erlau-
Methoden anbie-
ben uns die Regeln für Polymorphie, Füchse und Hasen als Instanzen des Typs Tier ten. Sonst ist die
anzusehen. Deshalb können die Teile der Simulation, die nicht mit spezifischen Subklasse selbst
Subklassen umgehen müssen, stattdessen den Typ der Superklasse benutzen. ebenfalls abstrakt.

Übung 12.34 Obwohl im Rumpf der Schleife aus Listing 12.6 nicht mehr
die Typen Fuchs und Hase verwendet werden, wird weiterhin der Typ Tier
verwendet. Warum ist es in der Schleife nicht möglich, alle Objekte in der
Sammlung einfach über den Typ Object zu bearbeiten?
Übung 12.35 Ist es notwendig, dass eine Klasse mit einer oder zwei abs-
trakten Methoden als abstrakt definiert wird? Wenn Sie sich nicht sicher
sind, dann experimentieren Sie mit dem Quelltext der Klasse Tier aus dem
Projekt Fuechse-und-Hasen-V2.
Übung 12.36 Kann eine Klasse ohne abstrakte Methoden als abstrakt dekla-
riert werden? Wenn Sie sich nicht sicher sind, dann ändern Sie die Methode
agiere in der Klasse Tier in eine konkrete Methode, indem Sie ihr einen leeren
Rumpf ohne Anweisungen hinzufügen.
Übung 12.37 Kann es sinnvoll sein, eine Klasse als abstrakt zu deklarieren,
auch wenn sie keine abstrakten Methoden enthält? Diskutieren Sie dies.
Übung 12.38 Welche Klassen im Paket [Link] sind abstrakt? Einige von
ihnen haben abstract im Klassennamen, aber kann man es auch auf andere
Weise aus der Dokumentation ersehen? Welche konkreten Klassen erweitern
diese?

449
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Übung 12.39 Können Sie aus der API-Dokumentation für eine abstrakte
Klasse ersehen, welche ihrer Methoden (falls überhaupt) abstrakt sind? Müs-
sen Sie überhaupt wissen, welche Methoden abstrakt sind?
Übung 12.40 Betrachten Sie noch einmal die Regeln für das Überschreiben
von Methoden und Datenfeldern aus Kapitel 11. Warum sind sie besonders
wichtig bei unseren Versuchen, Vererbung in diese Anwendung einzuführen?
Übung 12.41 Die Änderungen, die wir in diesem Abschnitt vorgenommen
haben, haben Abhängigkeiten (Kopplungen) der Methode simuliereEinen-
Schritt von den Klassen Fuchs und Hase aufgelöst. Die Klasse Simulator ist
allerdings noch immer eng mit Fuchs und Hase gekoppelt, weil diese Klassen in
der Methode bevoelkere referenziert werden. Daran führt kein Weg vorbei:
Wenn wir Tier-Instanzen erzeugen wollen, dann müssen wir exakt angeben,
von welcher Klasse Instanzen zu erzeugen sind.
Dies könnte verbessert werden, indem die Klasse Simulator in zwei Klassen
aufgespaltet wird: Eine Klasse Simulator, die die Simulation ausführt und
komplett von den Tierklassen entkoppelt ist, und eine Klasse Population-
Erzeuger (erzeugt und aufgerufen vom Simulator), die die Population
erzeugt. Nur diese Klasse wäre an die konkreten Tierklassen gekoppelt und
würde es einem Wartungsprogrammierer damit leichter machen, die für
eine Änderung notwendigen Stellen in der Anwendung zu finden. Versuchen
Sie, dieses Refactoring zu implementieren. Die Klasse PopulationErzeuger
sollte auch die Farben für die verschiedenen Tierarten festlegen.

Das Projekt Fuechse-und-Hasen-V2 liefert eine Implementierung unserer Simula-


tion mit den bis hierher diskutierten Verbesserungen. Beachten Sie, dass die
Änderung an der Klasse Simulator, die Tiere in einer Liste statt in zwei getrennten
Listen zu verarbeiten, dazu führt, dass die Simulationsergebnisse in Version 2
nicht mit den Ergebnissen in Version 1 identisch sind.
In den Buchprojekten finden Sie zudem eine dritte Version dieses Projekts: Fuechse-
und-Hasen-Graph. Dieses Projekt ist hinsichtlich seines Modells (d.h. der Tier-, Fuchs-,
Hase-, Simulator-Implementierungen) identisch zu Fuechse-und-Hasen-V2, aber
es fügt eine zweite Ansicht zu dem Projekt hinzu: ein Graph, der die Popula-
tionszahlen über die Zeit anzeigt. Wir werden einige Aspekte dieser Implementie-
rung weiter hinten in diesem Kapitel diskutieren; experimentieren Sie im Moment
einfach mit diesem Projekt herum.

Übung 12.42 Öffnen Sie das Projekt Fuechse-und-Hasen-Graph und füh-


ren Sie es aus. Achten Sie auf die Diagrammansicht-Ausgabe. Erläutern Sie
schriftlich die Bedeutung des angezeigten Graphen und versuchen Sie zu
erklären, warum er so verläuft. Gibt es eine Beziehung zwischen den bei-
den Kurven?

450
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.4 Weitere abstrakte Methoden

Übung 12.43 Wiederholen Sie einige Ihrer Experimente mit verschiedenen


Feldgrößen (besonders mit kleineren Feldern). Erhalten Sie durch die Dia-
grammansicht irgendwelche neuen Erkenntnisse oder hilft sie Ihnen, das
Gesehene zu verstehen oder zu erläutern?

Wenn Sie bis hierher alle Übungen in diesem Kapitel nachvollzogen haben, ent-
spricht Ihre Projektversion dem Projekt Fuechse-und-Hasen-V2 und ist, abge-
sehen von der Anzeige des Graphen, identisch zu Fuechse-und-Hasen-Graph. Sie
können die Übungen ab jetzt mit jeder der beiden Projektversionen fortsetzen.

12.4 Weitere abstrakte Methoden


Als wir die Superklasse Tier in Abschnitt 12.3 angelegt haben, haben wir dies auf
Basis der Gemeinsamkeiten der Subklassen getan. Allerdings haben wir uns dafür
entschieden, das Datenfeld alter samt den damit verbundenen Methoden nicht zu
verschieben. Das könnte etwas zu konservativ sein. Dabei wäre es ganz einfach
gewesen, das Datenfeld in die Superklasse Tier zu verschieben und dort für den
Zugriff eine sondierende und eine verändernde Methode zu implementieren, die
dann von Subklassen-Methoden wie alterErhoehen aufgerufen werden kann. Und
warum nicht auch gleich die Methoden alterErhoehen und kannGebaeren in die
Klasse Tier verschieben? Der Grund, warum dies für diese und andere Methoden
der Klassen Fuchs und Hase mit identischen Rümpfen nicht ohne Weiteres geht, ist,
dass diese Methoden auf Klassenvariablen zugreifen, die für jede Klasse spezifische
Werte haben. Im Falle der Methode kannGebaeren liegt das Problem zum Beispiel
bei der Klassenvariable GEBAER_ALTER während traechtig von den Variablen
GEBAER_WAHRSCHEINLICHKEIT und MAX_WURFGROESSE abhängt. Wenn die Methode kann-
Gebaeren in die Klasse Tier verschoben wird, dann fordert der Compiler in der Klasse
Tier einen Zugriff auf den Wert des gebärfähigen Alters. Es ist verlockend, eine sol-
che Variable dann ebenfalls in der Klasse Tier zu definieren und zu hoffen, dass ihr
Wert durch gleichnamige Datenfelder in Subklassen überschrieben wird. Datenfel-
der werden in Java jedoch anders behandelt als Methoden: Sie können nicht durch
Versionen in Subklassen überschrieben werden.3 Das bedeutet, dass eine Methode
kannGebaeren in Tier einen sinnlosen Wert der in dieser Klasse definiert wurde, ver-
wenden würde anstatt einen, der spezifisch für eine bestimmte Subklasse ist.
Die Tatsache, dass der Wert des Datenfeldes sinnlos ist, gibt uns allerdings einen
Hinweis darauf, wie sich dieses Problem umgehen lässt und wie wir am Ende doch
mehr von diesen gleichlautenden Methoden von den Subklassen in die Superklasse
verschieben können.
Erinnern wir uns, dass wir agieren in Tier als abstract definiert haben, da hier ein
Rumpf für die Methode sinnlos gewesen wäre. Wenn wir auf das gebärfähige
Alter über eine Methode statt über ein Feld zugreifen, umgehen wir die Prob-
leme im Zusammenhang mit den altersabhängigen Eigenschaften. Dieser Ansatz
wird in Listing 12.8 verfolgt.

3 Diese Regel gilt unabhängig davon, ob ein Datenfeld statisch ist oder nicht.

451
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Listing 12.8
Die Methode
kannGebaeren in
der Klasse Tier.

Die Methode kannGebaeren wurde in Tier verschoben und so umgeschrieben, dass


sie anstelle des Werts einer Klassenvariablen den Ergebniswert eines Methoden-
aufrufs benutzt. Damit dies funktioniert, muss eine Methode gibGebaerAlter in
der Klasse Tier definiert sein. Da wir kein gebärfähiges Alter für Tiere allgemein
definieren können, benutzen wir auch hier eine abstrakte Methode in der Klasse
Tier und konkrete Redefinitionen in den Subklassen. In Fuchs und Hase wird
jeweils eine eigene Version von gibGebaerAlter definiert, die den jeweiligen Wert
von GEBAER_ALTER zurückliefert:
/**
* @return das Alter, ab dem ein Hase gebären kann.
*/
public int gibGebaerAlter()
{
return GEBAER_ALTER;
}

Konzept Obwohl also der Aufruf von gibGebaerAlter im Code der Superklasse verankert ist,
wird letztlich die Methode aus der Subklasse ausgeführt. Das mag auf den ersten
Superklassen-
Blick etwas mysteriös erscheinen, basiert aber auf den gleichen Prinzipien, die
Methodenauf-
rufe: Aufrufe von schon in Kapitel 11 beschrieben wurden; das heißt, der dynamische Typ eines
nicht privaten In- Objekts wird herangezogen, um festzulegen, welche Version einer Methode zur
stanzmethoden aus Laufzeit aufgerufen werden soll. Die hier gezeigte Technik ermöglicht es den In-
einer Superklasse stanzen, den jeweils für ihren Subklassentyp entsprechenden Wert zu verwenden.
heraus werden Analog diesem Ansatz können wir jetzt auch die übrigen Methoden alterErhoehen
immer im erweiter- und traechtig in die Superklasse verschieben.
ten Kontext des
dynamischen Typs
des Objekts ausge-
wertet. Übung 12.44 Arbeiten Sie mit der letzten Version Ihres Projekts (oder mit
dem Projekt Fuechse-und-Hasen-V2, falls Sie die Übungen nicht durchge-
führt haben) und notieren Sie sich die Anzahl der Füchse und Hasen bei den
ersten Schritten, um sich für die Regressionstests nach den folgenden Ände-
rungen vorzubereiten.
Übung 12.45 Verschieben Sie das Datenfeld alter aus den Klassen Fuchs und
Hase in die Klasse Tier. Initialisieren Sie es im Konstruktor mit 0. Implementie-
ren Sie sondierende und verändernde Methoden für das Datenfeld und ver-
wenden Sie diese in Fuchs und Hase anstatt direkt auf das Feld zuzugreifen.
Stellen Sie sicher, dass sich das Programm übersetzen und ausführen lässt.

452
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.5 Multiple Vererbung

Übung 12.46 Verschieben Sie die Methode kannGebaeren aus den Klassen
Fuchs und Hase in die Klasse Tier und schreiben Sie die Methode wie in Lis-
ting 12.8 gezeigt um. Stellen Sie entsprechende Versionen von gibGebaer-
Alter in den Klassen Fuchs und Hase bereit, die die verschiedenen Werte für
das Gebäralter zurückgeben.
Übung 12.47 Verschieben Sie die Methode alterErhoehen aus Fuchs und
Hase nach Tier, indem Sie eine abstrakte Methode gibHoechstalter in Tier
und konkrete Versionen in Fuchs und Hase definieren.
Übung 12.48 Kann die Methode traechtig in die Klasse Tier verschoben
werden? Wenn ja, dann führen Sie diese Änderung durch.
Übung 12.49 In Anbetracht der bisher an diesen drei Klassen durchgeführten
Änderungen sollten Sie die Sichtbarkeit aller Methoden noch einmal überprü-
fen und weitere Änderungen vornehmen, die Ihnen sinnvoll erscheinen.
Übung 12.50 War es möglich, all diese Änderungen ohne Auswirkungen auf
die anderen Klassen des Projekts durchzuführen? Wenn ja: Was sagt dies über
den Grad der Kapselung und Kopplung in der Originalversion aus?
Übung 12.51 Zusatzaufgabe. Definieren Sie eine vollständig neue Tierart als
Subklasse von Tier. Sie werden entscheiden müssen, welche Auswirkungen
die neue Spezies auf die bestehenden Tiere haben soll. Die neue Art könnte
beispielsweise in Konkurrenz zu den Füchsen auf Hasenjagd gehen oder sie
könnte Füchse statt Hasen jagen. Sie werden vermutlich feststellen, dass Sie
sehr viel mit den Einstellungen für die neue Art experimentieren müssen. Sie
werden auch die Methode bevoelkere modifizieren müssen, damit einige
Exemplare der neuen Tierart beim Start der Simulation existieren.
Sie sollten auch eine neue Farbe für die neue Tierklasse definieren. Sie finden
eine Liste der vordefinierten Farbnamen in der Dokumentation der Klasse Color
im Paket [Link].
Übung 12.52 Zusatzaufgabe. Der Code der Methoden gebaereNachwuchs ist
in den Klassen Fuchs und Hase sehr ähnlich. Der einzige Unterschied ist, dass
in der einen Methode neue Fuchs-Objekte und in der anderen neue Hase-
Objekte erzeugt werden. Ist es möglich, die oben im Zusammenhang mit
kannGebaeren beschriebene Technik zu verwenden und den gemeinsamen
Code in eine gemeinsam genutzte gebaereNachwuchs-Methode in Tier zu ver-
schieben? Wenn Sie das für möglich halten, versuchen Sie es. Hinweis: Die
Regeln der polymorphen Ersetzbarkeit gelten nicht nur für Werte, die von
Methoden zurückgeliefert werden, sondern auch in Zuweisungen oder bei
der Parameterübergabe.

12.5 Multiple Vererbung


12.5.1 Eine Klasse Akteur
In diesem Abschnitt diskutieren wir einige mögliche Erweiterungen und die Pro-
grammierkonstrukte, die dies ermöglichen.

453
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Die erste offensichtliche Erweiterung ist die Einführung von neuen Tieren. Wenn
Sie sich an Übung 12.51 versucht haben, dann haben Sie schon etwas Erfahrung
damit. Wir sollten diese Erweiterung allerdings etwas verallgemeinern: Möglicher-
weise sind nicht alle Teilnehmer an einer Simulation Tiere. Unsere aktuelle Struktur
geht davon aus, dass alle Teilnehmer Tiere sind und von der Klasse Tier erben.
Eine Erweiterung, die wir vielleicht vornehmen wollen, ist die Einführung von
menschlichen Teilnehmern, entweder als Jäger oder als Fallensteller. Diese passen
nicht in das bisherige Bild von Akteuren, die Tiere sind. Wir könnten die Simula-
tion auch um Futterpflanzen für die Hasen oder sogar Wettereinflüsse erweitern
wollen. Die Pflanzen als Futter könnten die Population der Hasen beeinflussen
(genau genommen werden die Hasen zu „Jägern“ der Pflanzen) und das Wachs-
tum der Pflanzen wiederum könnte durch das Wetter beeinflusst sein. All diese
Komponenten würden an der Simulation teilnehmen, aber es wären längst nicht
alles Tiere. Deshalb wäre es nicht richtig, sie als Subklassen von Tier einzurichten.
Da wir gerade die Möglichkeiten für weitere Akteure in der Simulation untersu-
chen, lässt sich günstig die Entwurfsentscheidung erläutern, Informationen über
die Tiere sowohl in einem Feld-Objekt als auch in den Tier-Listen zu halten. Jedes
Tier in der Liste durchzugehen, ist das, was wir unter einem einzelnen Simula-
tionsschritt verstehen. Alle Teilnehmer in einer Liste unterzubringen, hält den
grundlegenden Simulationsschritt so einfach wie möglich. Allerdings werden
dadurch Informationen dupliziert und dies kann leicht zu Inkonsistenzen führen.
Ein Grund für diese Entwurfsentscheidung ist, dass damit auch Teilnehmer an der
Simulation berücksichtigt werden können, die sich nicht im Feld befinden – eine
Repräsentation für das Wetter könnte ein Beispiel dafür sein.
Um allgemeinere Akteure zu modellieren, scheint die Einführung einer Klasse
Akteur sinnvoll. Diese Klasse würde als Superklasse für alle Arten von Simulations-
teilnehmern dienen, unabhängig von den konkreten Ausprägungen. Abbildung
12.3 zeigt ein Klassendiagramm für diesen Teil der Simulation. Die Klassen Akteur
und Tier sind abstrakt, während Hase, Fuchs und Jaeger konkrete Klassen sind.

Abbildung 12.3
Eine Simulations-
struktur mit Akteur.

454
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.5 Multiple Vererbung

Die Klasse Akteur würde die Teile definieren, die alle Akteure gemeinsam haben.
Die wichtigste Gemeinsamkeit ist, dass Akteure in irgendeiner Weise agieren.
Außerdem müssen wir wissen, ob ein Akteur noch aktiv ist oder nicht. Die einzigen
Definitionen in der Klasse Akteur wären somit die abstrakten Methoden agiere und
istAktiv.
// alle Kommentare ausgelassen
public abstract class Akteur
{
abstract public void agiere(List<Akteur> neueAkteure);
abstract public boolean istAktiv();
}
Dies reicht schon aus, damit in der Simulationsschleife (Listing 12.6) die Klasse
Akteur anstelle von Tier benutzt werden kann. (Entweder würde man die Methode
istLebendig in IstAktiv umbenennen oder man würde in Tier eine eigene istAk-
tiv-Methode definieren, die einfach die bestehende Methode istLebendig aufruft.)

Übung 12.53 Führen Sie in Ihrer Simulation die Klasse Akteur ein. Schreiben
Sie die Methode simuliereEinenSchritt in der Klasse Simulator so um, dass sie
statt Tier Akteur benutzt. Dies können Sie sogar tun, ohne neue teilnehmende
Arten einzuführen. Lässt sich die Klasse Simulator übersetzen? Oder wird noch
etwas in der Klasse Akteur benötigt?

Diese neue Struktur ist flexibler, denn sie gestattet auch die Einführung von Akteu-
ren, die keine Tiere sind. Tatsächlich könnten wir sogar die Klasse für die Erfassung
der Statistiken, FeldStatistik, als einen Akteur definieren – denn auch sie handelt
bei jedem Schritt. Ihre Aktion wäre das Aktualisieren der aktuellen Zählerstände
der Tiere.

12.5.2 Flexibilität durch Abstraktion


Durch die Verschiebung hin zu einer Simulationsform, die agierende Objekte verwal-
tet, haben wir uns mithilfe von Abstraktion recht weit entfernt von unserem
ursprünglichen Szenario der Füchse und Hasen in einem rechteckigen Feld. Dieser
Prozess der Abstraktion hat zu einer größeren Flexibilität geführt, aufgrund derer wir
unseren Blick für zusätzliche Möglichkeiten weiten konnten, die noch in einer Simula-
tion berücksichtigt werden könnten. Wenn wir an die Anforderungen anderer Simu-
lationsszenarien denken, kommen uns vielleicht weitere Ideen für Erweiterungen.
Es könnte beispielsweise nützlich sein, andere Jäger-Beute-Szenarien zu simulieren,
etwa eine Meeressimulation mit Fischen und Haien oder mit Fischen und Fischerei-
flotten. Wenn diese Meeressimulation auch die Nahrungsversorgung der Fische
modellieren sollte, dann wollen wir möglicherweise das Plankton nicht visualisieren –
entweder weil es zu viel davon gibt oder weil es einfach zu klein ist. Umweltsimula-
tionen könnten das Wetter mit einbeziehen, das zwar ein Akteur wäre, aber viel-
leicht keine Visualisierung in den Feldzellen erfordert.

455
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Im nächsten Abschnitt werden wir versuchen, als eine mögliche Erweiterung unse-
res Simulationsrahmens die Visualisierung vom Agieren zu trennen.

12.5.3 Selektive Darstellung


Eine Möglichkeit, das Agieren vom Visualisieren zu trennen, besteht in einer Ände-
rung bei der Ausführung in der Simulation. Anstatt jedes Mal über das gesamte Feld
zu iterieren und den jeweiligen Akteur neu zu zeichnen, könnten wir über eine
zusätzliche Sammlung von sichtbaren Akteuren iterieren. Der Code in der Klasse
Simulator könnte etwa so aussehen:
// lasse alle Akteure agieren
for(Akteur akteur : akteure ) {
[Link](...);
}

// zeichne alle zeichenbaren Akteure


for(Zeichenbar zeichenbar : zeichenbare ) {
[Link](...);
}
Alle Akteure wären in der Sammlung akteure, während die Akteure, die auch auf
dem Bildschirm dargestellt werden können, sich außerdem noch in der Sammlung
zeichenbare befinden. Damit dies funktioniert, benötigen wir eine weitere Super-
klasse Zeichenbar, die eine abstrakte Methode zeichnen definiert. Zeichenbare
Akteure müssen dann sowohl von Akteur als auch von Zeichenbar erben (Abbildung
12.4 zeigt ein Beispiel, in dem wir Ameisen annehmen, die zwar agieren, aber nicht
visualisiert werden sollen).

Abbildung 12.4
Eine Akteur-Hierarchie
mit einer Klasse
Zeichenbar.

Konzept 12.5.4 Zeichenbare Akteure: multiple Vererbung


Multiple Verer- Das dargestellte Szenario benutzt eine Struktur, die multiple Vererbung genannt
bung: Eine Situa- wird. Multiple Vererbung tritt auf, wenn eine Klasse mehr als eine Superklasse
tion, in der eine hat.4 Die Subklasse verfügt dann über alle Eigenschaften beider Superklassen
Klasse von mehr als und über die in der Klasse selbst definierten.
einer Superklasse
erbt, wird multiple
Vererbung genannt.

456
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.6 Interfaces

Multiple Vererbung ist im Prinzip leicht zu verstehen, kann aber zu großen Schwierig-
keiten in der Umsetzung in einer Programmiersprache führen. Verschiedene objekt-
orientierte Programmiersprachen gehen sehr unterschiedlich mit multipler Vererbung
um: Einige erlauben multiple Superklassen, andere nicht. Die Sprache Java liegt
ungefähr in der Mitte: Sie lässt nur eine Superklasse zu, bietet aber ein anderes Kon-
strukt, das eine eingeschränkte Form vom multipler Vererbung ermöglicht: Inter-
faces. Wir stellen diese im folgenden Abschnitt vor.

12.6 Interfaces
Bis zu diesem Punkt haben wir im vorliegenden Buch den Begriff „Schnittstelle“ in Konzept
einem informellen Sinne benutzt, um den Teil einer Klasse zu beschreiben, über
Ein Interface in
den sie mit anderen Klassen interagiert. Java fasst dieses Konzept etwas formaler,
Java ist eine Spezi-
indem Interface-Typen definiert werden können. fikation eines Typs
Auf den ersten Blick sind Interfaces Klassen sehr ähnlich. In ihrer allgemeinsten Form (in Form eines Typ-
sind sie abstrakten Klassen, in denen alle Methoden abstrakt sind, am ähnlichsten. namens und einer
Menge von Metho-
Wir können die wichtigsten Eigenschaften von Interfaces wie folgt zusammenfas-
den). Häufig wird
sen: keine Implementie-
 Im Kopf wird statt des Schlüsselworts class das Schlüsselwort interface ver- rung für die meis-
wendet. ten seiner Metho-
den zur Verfügung
 Interfaces enthalten keine Konstruktoren. gestellt.
 Interfaces enthalten keine Instanzfelder.
 In einem Interface sind nur konstante Datenfelder (static und final) mit
öffentlicher Sichtbarkeit erlaubt. Die Schlüsselwörter public, static und final
können deshalb weggelassen werden; sie werden automatisch übernommen.
 Abstrakte Methoden müssen nicht das Schlüsselwort abstract in ihrem Kopf
erhalten.
Vor Java 8 mussten alle Methoden in einem Interface abstrakt sein, aber die fol-
genden nicht abstrakten Methodentypen sind nun ebenfalls verfügbar:
 Methoden mit dem Schlüsselwort default haben einen Methodenrumpf.
 Methoden mit dem Schlüsselwort static haben einen Methodenrumpf.
Alle Methoden eines Interface – ob abstrakt, konkret oder statisch – sind öffent-
lich sichtbar, das Schlüsselwort public kann somit in ihren Definitionen weggelas-
sen werden.

12.6.1 Ein Interface Akteur


Listing 12.9 zeigt Akteur definiert als ein Interface-Typ.

4 Verwechseln Sie diesen Fall nicht mit der normalen Situation, in der eine einzelne Klasse mehrere
Superklassen in ihrer Vererbungshierarchie haben kann, so wie Fuchs, Tier, Akteur und Object.
Das ist mit multipler Vererbung nicht gemeint.

457
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Listing 12.9
Das Interface Akteur.

Eine Klasse kann auf ähnliche Weise von einem Interface erben wie von einer
Klasse. In Java wird jedoch ein anderes Schlüsselwort für das Beerben von Inter-
faces benutzt: implements.
Eine Klasse implementiert ein Interface, wenn sie eine implements-Klausel in ihrem
Klassenkopf definiert. Beispielsweise:
public class Fuchs extends Tier implements Zeichenbar
{
// Klassenrumpf hier ausgelassen
}
In einem solchen Fall wie diesem, in dem eine Klasse sowohl eine Klasse erweitert als
auch ein Interface implementiert, muss die extends-Klausel zuerst aufgeführt sein.
Zwei der abstrakten Klassen aus unserem obigen Beispiel, Akteur und Zeichenbar,
sind gute Kandidaten für Interfaces. Beide enthalten jeweils nur die Definition von
Methoden ohne Methodenimplementierungen. Damit erfüllen sie bereits perfekt
die abstrakteste Anforderung an ein Interface: Sie definieren keine Datenfelder,
keine Konstruktoren und keine Methodenrümpfe.
Bei der Klasse Tier sieht dies anders aus. Sie ist eine echte abstrakte Klasse, die
eine partielle Implementierung mit Instanzfeldern einem Konstruktor und vielen
Methoden mit Methodenrümpfen anbietet. Sie besitzt nur eine einzige abstrakte
Methode in ihrer ursprünglichen Version. Aufgrund all dieser Eigenschaften muss
sie eine Klasse bleiben und eignet sich nicht als Interface.

Übung 12.54 Redefinieren Sie in Ihrem Projekt die Klasse Akteur als ein Interface.
Lässt sich die Simulation noch übersetzen? Lässt sie sich ausführen? Nehmen Sie
alle notwendigen Änderungen vor, damit sich die Klasse wieder ausführen lässt.
Übung 12.55 Sind die Datenfelder im folgenden Interface statische Daten-
felder oder Instanzfelder?
public interface Quiz
{
int KORREKT = 1;
int INKORREKT = 0;
...
}
Welche Sichtbarkeit haben sie?

458
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.6 Interfaces

Übung 12.56 Welche Fehler enthält das folgende Interface?


public interface Monitor
{
private static final int OBERGRENZE = 50;
private int wert;
public Monitor (int initial);
void aktualisiere(int ablesung);
public int gibObergrenze()
{
return OBERGRENZE;
}
...
}

12.6.2 Default-Methoden in Interfaces


Eine als default gekennzeichnete Methode innerhalb eines Interface besitzt
einen Methodenrumpf, der von allen implementierenden Klassen geerbt wird.
Durch das Hinzukommen der Default-Methoden zu Interfaces in Java 8 ist die
Unterscheidung zwischen abstrakten Klassen und Interfaces noch komplizerter
geworden: Jetzt gilt nicht mehr, dass Interfaces keine Methodenrümpfe haben.
Es ist jedoch wichtig, Vorsicht walten zu lassen, wenn man darüber nachdenkt,
eine Default-Methode innerhalb eines Interface zu definieren. Vergessen Sie
nicht, dass Default-Methoden in erster Linie deshalb in Java eingeführt wurden,
um das Ergänzen von neuen Methoden zu Schnittstellen der API, die vor Java 8
existierte, zu unterstützen. Default-Methoden ermöglichen es, bestehende
Schnittstellen zu verändern, ohne die zahlreichen Klassen zu ruinieren, die bereits
die älteren Versionen dieser Schnittstellen implementiert haben.
Aufgrund der anderen Beschränkungen von Interfaces – keine Konstruktoren
und keine Instanzfelder – sollte klar sein, dass die Funktionalität, die innerhalb
einer Default-Methode möglich ist, streng begrenzt ist, da es keinen Zustand
gibt, der direkt untersucht oder manipuliert werden kann. Im Allgemeinen wer-
den wir uns daher, wenn wir unsere eigenen Interfaces schreiben, auf rein abs-
trakte Methoden beshränken. Außerdem werden wir häufig, wenn wir in diesem
Kapitel die Eigenschaften von Interfaces besprechen, den Fall der nicht abstrak-
ten Methoden aus Gründen der Einfachheit ignorieren.

12.6.3 Multiple Vererbung für Interfaces


Wie bereits oben erwähnt, erlaubt Java nur die Angabe maximal einer Klasse als
Superklasse. Aber eine Klasse darf beliebig viele Interfaces implementieren (zusätz-
lich zur möglichen Erweiterung von genau einer Klasse). Somit können wir, wenn
wir Akteur und Zeichenbar als Interfaces statt als abstrakte Klassen definieren, eine
Klasse Jaeger (Abbildung 12.4) definieren, die beide implementiert:

459
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

public class Jaeger implements Akteur, Zeichenbar


{
// Klassenrumpf hier ausgelassen
}
Die Klasse Jaeger erbt die Methoden aller Interfaces (in diesem Fall agiere und
zeichnen) als abstrakte Methoden. Sie muss dann Implementierungen für beide
Methoden anbieten oder selbst abstrakt deklariert werden.
Die Klasse Tier ist ein Beispiel für eine Klasse, die eine geerbte Interface-Methode
nicht implementiert. Tier erbt, in der neuen Struktur aus Abbildung 12.4, die
abstrakte Methode agiere von Akteur. Sie bietet keinen Methodenrumpf für diese
Methode an und muss deswegen selbst abstrakt sein (sie muss das Schlüsselwort
abstract im Kopf der Klasse angeben). Die Subklassen von Tier implementieren
dann die Methode agiere und werden damit zu konkreten Klassen.
Die Tatsache, dass Default-Methoden in Interfaces vorkommen, kann Komplika-
tionen bei der Implementierung von mehreren Interfaces durch eine Klasse mit
sich bringen. Angenommen, eine Klasse implementiert mehrere Interfaces und
zwei oder mehr dieser Interfaces haben eine Default-Methode mit derselben Sig-
natur. Dann muss die implementierende Klasse diese Methode überschreiben –
selbst wenn die alternativen Versionen der Methoden identisch sind. Der Grund
dafür ist, dass es im allgemeinen Fall eindeutig sein muss, welche der alternati-
ven Implementierungen von der Klasse geerbt werden soll. Das Überschreiben
kann die Klasse entweder dadurch vornehmen, dass sie die bevorzugte Version in
der überschreibenden Methode aufruft, oder indem eine völlig andere Imple-
mentierung definiert wird. Der Kopf der überschreibenden Methode enthält
nicht das Schlüsselwort default, da dieses nur in Interfaces benutzt wird.
Es gibt eine neue Syntax unter Verwendung des Schlüsselworts super, um eine
Default-Methode aus einem der Interfaces in einer überschreibenden Methode
aufzurufen. Nehmen wir zum Beispiel an, dass die Interfaces Akteur und Zeichen-
bar beide jeweils eine Default-Methode zuruecksetzen definieren, die void als
Rückgabetyp hat und keine Parameter verlangt. Wenn eine Klasse, die beide
Interfaces implementiert, diese Methode überschreibt, indem beide Default-Ver-
sionen aufgerufen werden, dann könnte die überschreibende Methode wie folgt
definiert werden:
public void zuruecksetzen()
{
[Link]();
[Link]();
}
Mit anderen Worten, eine implementierende Klasse, die eine geerbte Default-
Methode überschreibt, kann diese Default-Methode mittels der Syntax
[Link](...)
aufrufen.

460
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.6 Interfaces

Übung 12.57 Zusatzaufgabe. Fügen Sie der Simulation einen Akteur hinzu,
der kein Tier ist. Sie können beispielsweise eine Klasse Jaeger mit den folgen-
den Eigenschaften definieren: Jäger haben kein Höchstalter und müssen
weder Nahrung zu sich nehmen noch gebären sie Nachwuchs. In jedem
Schritt der Simulation bewegt sich ein Jäger auf eine zufällig gewählte Nach-
barposition und feuert eine feste Anzahl von Schüssen auf zufällig gewählte
Nachbarpositionen. Jedes Tier, das sich in einer dieser Positionen befindet,
wird getötet.
Setzen Sie zu Beginn der Simulation nur eine geringe Anzahl an Jägern in
das Feld. Bleiben die Jäger während der gesamten Simulation im Feld oder
verschwinden sie irgendwann? Wenn sie verschwinden: Warum passiert das
und ist das ein realistisches Verhalten?
Welche weiteren Klassen müssen aufgrund der Einführung der Jäger ange-
passt werden? Besteht die Notwendigkeit, die bestehenden Klassen weiter
zu entkoppeln?

12.6.4 Interfaces als Typen


Wenn eine Klasse ein Interface implementiert, dann erbt sie häufig keinerlei Imple-
mentierung von ihr. Es stellt sich die Frage: Welchen Nutzen hat es, Interfaces zu
implementieren?
Als wir in Kapitel 10 Vererbung eingeführt haben, haben wir zwei wichtige Vorteile
davon hervorgehoben:
1 Die Subklasse erbt den Code (Methodenimplementierungen und Datenfel-
der) der Superklasse. Dies ermöglicht die Wiederverwendung von existieren-
dem Code und vermeidet Code-Duplizierung.

2 Die Subklasse wird zu einem Subtyp der Superklasse. Dies erlaubt polymor-
phe Variablen und Methodenaufrufe. Mit anderen Worten: Speziellere Ob-
jekte (Objekte von Subklassen) können einheitlich behandelt werden (als In-
stanzen des Supertyps).
Interfaces werden nicht vorrangig für den erstgenannten Vorteil benutzt, sondern
für den zweiten. Ein Interface definiert genau wie eine Klasse einen Typ. Somit kön-
nen Variablen von einem Interface-Typ deklariert werden, obwohl keine Objekte
dieses Typs existieren können (nur Objekte der Subtypen).
In unserem Beispiel können wir deshalb, obwohl Akteur nun ein Interface ist, wei-
terhin eine Variable vom Typ Akteur in der Klasse Simulator deklarieren. Die Simu-
lationsschleife arbeitet weiterhin unverändert.
Von Interfaces gibt es keine direkten Instanzen, aber sie dienen als Supertypen
für andere Klassen.

461
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

12.6.5 Interfaces als Spezifikationen


In diesem Kapitel haben wir Interfaces als eine Möglichkeit eingeführt, multiple
Vererbung in Java zu definieren. Das ist ein wichtiges Einsatzgebiet von Interfaces,
aber es ist nicht das einzige.
Die wichtigste Eigenschaft von Interfaces ist, dass sie die Definition der Funktionali-
tät (die Schnittstelle einer Klasse) fast vollständig von ihrer Implementierung tren-
nen. Ein gutes Praxisbeispiel dafür findet sich in der Hierarchie der Java-Sammlun-
gen.
Die Hierarchie der Java-Sammlungen definiert (neben anderen Typen) das Interface
List und die Klassen ArrayList und LinkedList (Abbildung 12.5). Das Interface List
definiert die volle Funktionalität einer Liste (ihre Schnittstelle), ohne deren
zugrunde liegende strukturelle Implementierung zu begrenzen. Die Subklassen
(LinkedList und ArrayList) bieten zwei verschiedene Implementierungen dieser
Schnittstelle an. Das ist interessant, denn die beiden Implementierungen unter-
scheiden sich erheblich in der Effizienz einiger Funktionen. Ein wahlfreier Zugriff
auf Elemente in der Mitte einer Liste beispielsweise ist sehr viel schneller bei einer
ArrayList. Das Einfügen und Entfernen von Elementen kann andererseits mit einer
LinkedList sehr viel schneller sein.
Welche Implementierung für eine gegebene Anwendung am besten passt, ist im
Voraus oft schwer festzulegen. Das hängt sehr stark von der relativen Häufigkeit
der Methodenaufrufe sowie von einigen anderen Faktoren ab. In der Praxis lässt
es sich meist am besten herausfinden, indem eine Anwendung mit beiden Alter-
nativen implementiert wird und die Effizienzunterschiede gemessen werden.

Abbildung 12.5 «interface»


Das Interface List und List
seine Subklassen.

implementiert implementiert

ArrayList LinkedList

Durch das Interface List ist das sehr einfach. Wenn Sie statt ArrayList und Linked-
List immer List für die Typnamen von Parametern und Variablen verwenden, wird
die Anwendung unabhängig von der aktuell gewählten Implementierung funktio-
nieren. Lediglich bei der Erzeugung einer Liste müssen wir „Farbe bekennen“ und
tatsächlich eine Implementierung benennen. Beispielsweise würden wir schreiben:
List<Typ> meineListe = new ArrayList<>();
Beachten Sie, dass der Typ der Variablen lediglich List von Typ ist. Auf diese Weise
können wir in der gesamten Anwendung eine LinkedList verwenden, indem wir
einfach an dieser einen Stelle ArrayList durch LinkedList ersetzen, wenn eine Liste
erzeugt wird.

462
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.6 Interfaces

12.6.6 Bibliotheksunterstützung durch abstrakte


Klassen und Interfaces
In Kapitel 6 haben wir darauf hingewiesen, wie wichtig es ist, auf die Namen der
Sammlungsklassen zu achten: ArrayList, LinkedList, HashSet, TreeSet usw. Nach-
dem wir inzwischen abstrakte Klassen und Interfaces kennengelernt haben, ver-
stehen wir auch, warum diese besonderen Namen gewählt wurden. Das Paket
[Link] definiert mehrere wichtige Sammlungsabstraktionen in Form von Inter-
faces wie List, Map und Set. Die Wahl der konkreten Klassennamen soll darüber
informieren, welcher Art von Interface sie entsprechen und wie sie grundsätzlich
implementiert sind. Diese Informationen sind sehr nützlich, wenn es nötig ist,
kompetente Entscheidungen darüber zu treffen, welches die richtige konkrete
Klasse in einer bestimmten Situation ist. Indem wir, wo immer möglich, die
höchste Ebene des abstrakten Typs (sei es abstrakte Klasse oder Interface) für
unsere Variablen wählen, bereiten wir unseren Quelltext besser auf potenzielle
Bibliotheksänderungen vor – wie zum Beispiel das Hinzufügen einer neuen Map-
oder Set-Implementierung.
In Kapitel 13, das sich der Einführung der GUI-Bibliotheken in Java widmet, wer-
den wir ausgiebig Gebrauch von abstrakten Klassen und Interfaces machen, um zu
zeigen, wie sie mit nur sehr wenig zusätzlichem Quelltext anspruchsvolle Funktio-
nalität bieten können.

Übung 12.58 Welche Methoden haben die Klassen ArrayList und Linked-
List, die nicht im Interface List definiert sind? Was denken Sie, warum
diese Methoden nicht in List enthalten sind?
Übung 12.59 Schreiben Sie eine Klasse, die die Effizienz der korrespondie-
renden List-Interface-Methoden in den Klassen ArrayList und LinkedList
(wie add, get und remove) vergleicht. Verwenden Sie die oben beschriebene
Technik der polymorphen Variablen, um die Klasse zu schreiben, sodass sie
nur weiß, dass sie ihre Tests auf Objekten vom Interface-Typ List und nicht
auf den konkreten Typen ArrayList und LinkedList ausführt. Testen Sie mit
großen Listen von Objekten, damit die Ergebnisse aussagekräftig sind. Sie
können die Anfangs- und Endzeit Ihrer Testmethoden mithilfe der Methode
currentTimeMilis der Klasse System ermitteln.
Übung 12.60 Lesen Sie die API-Beschreibung zu den sort-Methoden in der
Klasse Collections aus dem Paket [Link]. Welche Interfaces sind in der
Beschreibung erwähnt? Welche Methoden im Interface [Link] haben
default-Implementierungen?
Übung 12.61 Zusatzaufgabe. Untersuchen Sie das Interface Comparable. Es
ist ein parametrisiertes Interface. Definieren Sie eine einfache Klasse, die
Comparable implementiert. Erzeugen Sie eine Sammlung mit Objekten dieser
Klasse und sortieren Sie die Sammlung. Hinweis: Die Klasse Logeintrag im Pro-
jekt Weblog-Auswertung aus Kapitel 7 implementiert dieses Interface.

463
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

12.6.7 Funktionale Interfaces und Lambda-


Ausdrücke (fortgeschritten)
Java 8 führte eine spezielle Klassifikation für Interfaces ein, die nur eine einzige
abstrakte Methode besitzen (unabhängig von der Anzahl der sonst noch vorkom-
menden Default- und/oder statischen Methoden). Solch ein Interface wird funktio-
nales Interface genannt. Die Notation @FunctionalInterface kann in die Deklara-
tion aufgenommen werden, in diesem Fall kann der Compiler überprüfen, ob
das Interface den Regeln der funktionalen Interfaces entspricht.
Zwischen funktionalen Interfaces und Lambda-Ausdrücken besteht eine besondere
Beziehung. Überall, wo ein Objekt eines funktionalen Interface benötigt wird, kann
stattdessen ein Lambda-Ausdruck verwendet werden. Im nächsten Kapitel, wenn
wir grafische Benutzungsschnittstellen implementieren, werden wir diese Möglich-
keit ausgiebig nutzen.
Die Verbindung zwischen Lambda-Ausdrücken und funktionalen Interfaces ist
wichtig. Sie gibt uns insbesondere ein praktisches Mittel an die Hand, um einen
Lambda-Ausdruck einem Typ zuzuordnen, zum Beispiel um eine Variable zu
deklarieren, die einen Lambda-Ausdruck enthält. Das Paket [Link]
definiert eine große Anzahl Interfaces, die eine Benennung für die am häufigsten
auftretenden Typen von Lambda-Ausdrücken anbieten. Allgemein bezeichnen
die Namen der Interfaces den Rückgabetyp und die Parametertypen ihrer einzi-
gen Methode, und somit einen Lambda-Ausdruck dieses Typs. Zum Beispiel:
 Consumer-Interfaces hängen mit Lamda-Ausdrücken zusammen, die void als
Rückgabetyp haben. Beispielsweise hat DoubleConsumer einen double-Parame-
ter und gibt keinen Ergebniswert zurück.
 BinaryOperator-Interfaces haben zwei Parameter und liefern einen Ergebnis-
wert desselben Typs. IntBinaryOperator hat zum Beispiel zwei int-Parameter
und gibt einen int-Wert zurück.
 Supplier-Interfaces liefern einen Ergebniswert des angegebenen Typs, zum
Beispiel LongSupplier.
 Predicate-Interfaces geben einen boolean-Wert zurück, beispielsweise IntPre-
dicate.

All diese Interfaces erweitern das Function-Interface, dessen einzige abstrakte


Methode apply heißt. Ein potenziell nützlicher funktionaler Interface-Typ aus dem
Paket [Link] ist Runnable. Runnable schließt eine Lücke in der Liste der Consumer-
Interfaces, da er keine Parameter und void als Rückgabetyp hat. Er besitzt eine ein-
zige abstrakte Methode namens run.
Funktionale Interface-Typen ermöglichen es, Lambda-Ausdrücke an Variablen zuzu-
weisen oder als aktuelle Parameter weiterzureichen. Nehmen wir beispielsweise an,
dass wir Zeichenkettenpaare aus Name und Alias haben, die wir auf bestimmte Art
und Weise formatieren wollen, zum Beispiel „Michelangelo Merisi (AKA Caravag-
gio)“. Ein Lambda-Ausdruck, der zwei String-Parameter entgegennimmt und ein
String-Ergebnis liefert, ist mit dem BinaryOperator-Interface kompatibel und wir kön-
nen wie folgt einem Lambda-Ausdruck einen Typ und einen Namen geben:
BinaryOperator<String> aka =
(name, alias) –> return name + " (AKA " + alias + ")";

464
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.7 Ein weiteres Beispiel für ein Interface

Dieser Lambda-Ausdruck kann durch Aufruf seiner apply-Methode mit den pas-
senden Parametern verwendet werden, zum Beispiel:
[Link]([Link]( "Michelangelo Merisi",
"Caravaggio"));

12.7 Ein weiteres Beispiel für ein Interface


Im vorigen Abschnitt haben wir diskutiert, wie Interfaces benutzt werden können,
um die Spezifikation einer Komponente von ihrer Implementierung zu trennen,
sodass verschiedene Implementierungen „hineingesteckt“ werden können und auf
diese Weise der Austausch von Komponenten in einem System erleichtert wird.
Diese Technik wird häufig eingesetzt, um Systemteile, die nur lose gekoppelt sind,
besser voneinander zu trennen.
Ein Beispiel für diese Technik haben wir bereits am Ende von Abschnitt 12.3 gese-
hen (ohne es jedoch ausführlich zu diskutieren). Dort sprachen wir über das Projekt
Fuechse-und-Hasen-Graph, das eine zusätzliche Sicht auf die Populationen bietet:
in Form eines Liniendiagramms. Ein Blick in das Klassendiagramm dieses Projekts
verrät uns, dass dabei auch ein Java-Interface geschrieben wurde (Abbildung 12.6).

«interface» Abbildung 12.6


Simulationsansicht Das Interfache
Simulationsansicht
und die implementie-
renden Klassen.
implementiert implementiert

DiagrammAnsicht GitterAnsicht

Die vorherigen Versionen des Fuechse-und-Hasen-Projekts enthielten einfach nur


eine Klasse Simulationsansicht. Es handelte sich dabei um eine konkrete Klasse
und sie lieferte die Implementierung der gitterbasierten Ansicht des Feldes. Wie
wir sehen konnten, war die Visualisierung weitgehend getrennt von der Simula-
tionslogik (dem Feld und den Akteuren), sodass verschiedene Visualisierungs-
ansichten möglich sind.
Für das Projekt Fuechse-und-Hasen-Graph wurde Simulationsansicht von einer
Klasse in ein Interface umgewandelt und die Implementierung der gitterbasierten
Ansicht wurde in eine Klasse GitterAnsicht verschoben.
GitterAnsicht ist identisch zur vorherigen Klasse Simulationsansicht. Das neue Simu-
lationsansicht-Interface wurde eingerichtet, indem die Klasse Simulator nach allen
Methoden durchsucht wurde, die tatsächlich von außen aufgerufen wurden, und
dann ein Interface definiert wurde, das genau diese Methoden spezifiziert. Sie lauten:
[Link](classObjekt, farbe);
[Link](feld);
[Link](schritt, feld);
[Link]();

465
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Wir können somit die benötigte Schnittstelle von Simulationsansicht als ein Inter-
face definieren:
import [Link];

public interface Simulationsansicht


{
void setzeFarbe(Class<?> tierklasse, Color farbe);
void istAktiv(Feld feld);
void zeigeStatus(int schritt, Feld feld);
void zuruecksetzen();
}
Eine kleine Schwierigkeit in der obigen Definition ist die Verwendung des Typs
Class<?> als erstem Parameter an die Methode setzeFarbe. Das werden wir im
nächsten Abschnitt näher erläutern.
Die vorherige Klasse Simulationsansicht, jetzt GitterAnsicht genannt, wird spezifi-
ziert, um das neue Interface Simulationsansicht zu implementieren:
public class GitterAnsicht extends JFrame implements Simulationsansicht
{
...
}
Es wird kein weiterer Quelltext benötigt, da er bereits die Methoden des Interface
implementiert. Nach diesen Änderungen ist es jedenfalls nicht mehr sonderlich
schwierig, weitere Ansichten für die Simulation vorzusehen: einfach indem wei-
tere Implementierungen des Interface Simulationsansicht bereitgestellt werden.
Die neue Klasse DiagrammAnsicht, die das Liniendiagramm erzeugt, ist ein Beispiel
hierfür.
Sobald wir mehr als eine Ansichtsimplementierung haben, können wir problem-
los die aktuelle Ansicht durch eine andere ersetzen oder, wie in unserem Beispiel,
sogar zwei Ansichten gleichzeitig anzeigen. In der Klasse Simulator werden die
konkreten Subklassen GitterAnsicht und GraphAnsicht nur einmal erwähnt, wenn
jede Ansicht erstellt wird. Danach werden sie in einer Sammlung gespeichert, die
Elemente des Supertyps Simulationsansicht hält, und nur der Interface-Typ wird
verwendet, um mit ihnen zu kommunizieren.
Die Implementierungen der Klassen GitterAnsicht und DiagrammAnsicht sind rela-
tiv komplex und wir erwarten nicht, dass Sie sie zu diesem Zeitpunkt bereits voll-
ständig verstehen. Wichtig ist an diesem Punkt nur, dass Sie das Muster, zwei
Implementierungen für ein einzelnes Interface zu liefern, verstehen.

Übung 12.62 Betrachten Sie den Quelltext der Klasse Simulator erneut und
suchen Sie nach allen Vorkommen der Ansichtsklassen und Interfaces und
verfolgen Sie alle Variablen, die unter Verwendung eines dieser Typen dekla-
riert wurden. Erläutern Sie genau, wie die Ansichten in der Klasse Simulator
verwendet werden.

466
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.8 Die Klasse Class

Übung 12.63 Implementieren Sie eine neue Klasse Textansicht, die die Simu-
lationsansicht implementiert. Textansicht soll eine textbasierte Ansicht der
Simulation erzeugen. Nach jedem Simulationsschritt soll sie eine Zeile der fol-
gende Form ausgeben:
Füchse: 121 Hasen: 266
Setzen Sie Textansicht zum Testen anstelle von GitterAnsicht ein. (Löschen Sie
die Klasse GitterAnsicht nicht. Wir wollen die Möglichkeit haben, zwischen
den Ansichten zu wechseln.)
Übung 12.64 Können Sie dafür sorgen, dass beide Ansichten gleichzeitig
aktiv sind?

12.8 Die Klasse Class


In Kapitel 10 haben wir die Klasse Object kennengelernt, deren Name paradox
anmutet. Es sollte Sie deshalb nicht überraschen, dass es auch eine Klasse Class gibt!
Dies macht die Diskussion von Klassen und Objekten manchmal sehr verwirrend.
Im vorherigen Abschnitt haben wir den Typ Class beim Definieren der Schnitt-
stelle Simulationsansicht verwendet. Leiten Sie daraus aber bitte nicht ab, dass
die Klasse Class in irgendeiner besonderen Weise mit den Interfaces verknüpft
wäre. Es handelt sich vielmehr um ein allgemeines Merkmal von Java und es ist
nur Zufall, dass es uns hier das erste Mal begegnet ist. Der Punkt ist, dass mit
jedem Typ ein Class-Objekt verbunden ist.
Die Klasse Object definiert die Methode getClass, die das Class-Objekt eines Objekts
zurückliefert. Eine andere Möglichkeit, das Class-Objekt für einen Typ zu erhalten,
besteht darin, .class nach dem Typnamen zu schreiben: zum Beispiel [Link] oder
[Link] – beachten Sie, dass sogar mit den primitiven Typen Class-Objekte verbun-
den sind.
Die Class-Klasse ist eine generische Klasse – sie hat einen Typparameter, der den kon-
kreten Subtyp der Klasse spezifiziert, den wir referenzieren. Der Typ von [Link]
ist zum Beispiel Class<String>. Wir können ein Fragezeichen anstelle des Typparame-
ters verwenden – Class<?> –, wenn wir eine Variable deklarieren wollen, die alle Klas-
senobjekte aller Typen halten kann.
Class-Objekte sind besonders nützlich, wenn wir wissen wollen, ob der Typ von zwei
Objekten der gleiche ist. Wir nutzen dies in der originalen Simulationsansicht-Klasse,
um jeden Tier-Typ mit einer Farbe im Feld zu assoziieren. Simulationsansicht ver-
wendet das folgende Feld, um die Zuordnung zwischen beiden zu definieren.
private Map<Class<?>, Color> farben;
Nachdem die Ansicht eingerichtet ist, ruft der Konstruktor von Simulator die
Methode setzeFarbe der Ansicht wie folgt auf:
[Link]([Link], [Link]);
[Link]([Link], [Link]);

467
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Wir werden hier nicht näher auf Class<?> eingehen. Aber diese Beschreibung
sollte ausreichen, um den Quelltext im vorherigen Abschnitt zu verstehen.

12.9 Abstrakte Klasse oder Interface?


In einigen Situationen muss die Wahl zwischen einer abstrakten Klasse und einem
Interface getroffen werden, während in anderen Fällen sowohl eine abstrakte
Klasse als auch ein Interface die Aufgabe erfüllen kann. Vor der Einführung von
Default-Methoden in Java 8 war diese Entscheidung etwas klarer: Wenn ein Typ
Elemente einer konkreten Implementierung benötigte – wie Instanzfelder, Kon-
struktoren oder Methodenrümpfe –, dann wurde eine abstrakte Klasse eingesetzt.
Die aktuelle Verfügbarkeit von Default-Methoden in Interfaces sollte die Antwort
in den meisten Fällen jedoch eigentlich nicht ändern. Im Allgemeinen sollte es ver-
mieden werden, Default-Methoden in Interfaces zu definieren, außer wenn es
darum geht, alten Code zu adaptieren.
Wenn wir die Wahl haben, sollten wir uns meist für ein Interface entscheiden.
Interfaces sind relativ leichtgewichtige Typen, die Beschränkungen bei der Klassen-
implementierung minimieren. Wenn wir zudem einen Typ als abstrakte Klasse defi-
nieren, dann können Subklassen keine weiteren Klassen mehr beerben. Da Inter-
faces multiple Vererbung zulassen, besteht bei der Benutzung eines Interface diese
Einschränkung nicht. Interfaces trennen die Typspezifikation sauber von der Imple-
mentierung, was zu loser Kopplung führt. Der Einsatz von Interfaces führt deshalb
zu flexibleren und besser erweiterbaren Strukturen.

12.10 Ereignisgesteuerte Simulationen


Charakteristisch für den Stil der Simulation, den wir in diesem Kapitel verwendet
haben, ist, dass die Zeit in diskreten, gleich langen Schritten vergeht. Bei jedem
Schritt wird jeder Akteur in der Simulation aufgefordert zu agieren, d.h. die Aktionen
auszuführen, die seinem gegenwärtigen Zustand entsprechen. Dieser Stil der Simula-
tion wird manchmal zeitbasierte oder synchrone Simulation genannt. In dieser Art
von Simulation haben die meisten Akteure bei jedem Schritt etwas zu tun: bewegen,
gebären und essen. In vielen Simulationsszenarien verbringen die Akteure viele Zeit-
schritte mit Nichtstun – normalerweise warten sie darauf, dass irgendetwas passiert,
sodass sie ihrerseits in Aktion treten können. Betrachten Sie dazu beispielsweise in
unserer Simulation den Fall eines neugeborenen Hasen. Er wird wiederholt gefragt,
ob er gebärt, auch wenn es mehrere Zeitschritte dauert, bis dies möglich ist. Gibt es
eine Möglichkeit, diese Frage zu vermeiden, bis der Hase tatsächlich so weit ist?
Auch stellt sich die Frage, wie groß der optimale Zeitschritt ist. Wir haben uns
absichtlich vage gehalten bezüglich der tatsächlichen Größe des Zeitschritts, und die
verschiedenen Aktionen werden in der Realität unterschiedlich häufig ausgeführt
(essen und bewegen sollten zum Beispiel viel häufiger erfolgen als gebären). Gibt es
eine Möglichkeit, sich auf eine Schrittgröße zu einigen, die nicht so klein ist, dass die
meiste Zeit nichts passiert, oder zu groß, sodass verschiedene Arten von Aktionen
nicht deutlich genug zwischen den Zeitschritten unterschieden werden.

468
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.10 Ereignisgesteuerte Simulationen

Ein alternativer Ansatz ist ein ereignisbasierter oder asynchroner Simulationsstil.


Hierbei wird die Simulation dadurch gesteuert, dass ein Zeitplan für zukünftige
Ereignisse erstellt wird. Der offensichtlichste Unterschied zwischen diesen beiden
Stilen ist, dass in einer ereignisbasierten Simulation die Zeitspannen der Ereig-
nisse sehr unregelmäßig sind. Ein Ereignis könnte beispielsweise zur Zeit t erfol-
gen und die nächsten beiden Ereignisse zu den Zeiten t+2 und t+8, während die
folgenden drei Ereignisse alle zur Zeit t+9 erfolgen.
Für die Fuechse-und-Hasen-Simulation wären dies die Ereignisse: Gebären, Bewe-
gen, Jagen und Einen-natürlichen-Tod-sterben. Was normalerweise passiert ist,
dass bei jedem Ereignis ein „frisches“ Ereignis für einen Punkt in der Zukunft vor-
gesehen wird. Wenn zum Beispiel ein Gebären-Ereignis eintritt, dann ist das Ereig-
nis für das Sterben des Tiers aus Altersgründen vorprogrammiert. Alle zukünftigen
Ereignisse sind in einer geordneten Warteschlange gespeichert, in der das nächste
stattfindende Ereignis am Kopf der Warteschlange steht. Das heißt aber nicht, dass
neu anberaumte Ereignisse immer an das Ende der aktuellen Warteschlange
gestellt werden. Oft müssen sie irgendwo vor dem Ende eingefügt werden, um die
zeitlich korrekte Reihenfolge in der Warteschlange zu gewährleisten. Außerdem
kann es vorkommen, dass einige zukünftige Ereignisse überflüssig werden, nach-
dem andere Ereignisse zuvor stattgefunden haben – ein offensichtliches Beispiel ist
das Ereignis eines natürlichen Todes für einen Hasen, das nicht eintritt, wenn der
Hase zuvor gefressen wird!
Ereignisgesteuerte Simulationen eignen sich besonders gut für die Techniken, die
wir in diesem Kapitel beschrieben haben. Das Konzept eines Ereignisses wird
dabei wahrscheinlich als eine abstrakte Klasse Event implementiert, die sowohl
konkrete Details (darüber, wann das Ereignis eintritt) als auch abstrakte Details
(was ist mit dem Ereignis verbunden) enthält. Konkrete Subklassen von Event
können dann die spezifischen Details für die verschiedenen Ereignistypen liefern.
Normalerweise muss sich die Hauptschleife der Simulation nicht mit den konkre-
ten Ereignistypen befassen, sondern kann polymorphe Methodenaufrufe ver-
wenden, wenn ein Ereignis eintritt.
Ereignisbasierte Simulationen sind oft effizienter und besser geeignet, wenn große
Systeme und sehr viele Daten vorliegen, während synchrone Simulationen besser
sind, um zeitbasierte Visualisierungen (wie Animationen der Akteure) zu erzeugen,
da die Zeit gleichmäßiger abläuft.

Übung 12.65 Finden Sie weitere Unterschiede zwischen den ereignisbasier-


ten und den zeitbasierten Simulationen.
Übung 12.66 Werfen Sie einen Blick in das Paket [Link], um festzustel-
len, ob es irgendwelche Klassen gibt, die sich gut dazu eignen, um eine
Ereigniswarteschlange in einer ereignisbasierten Simulation zu speichern.
Übung 12.67 Zusatzaufgabe. Schreiben Sie die Fuechse-und-Hasen-Simula-
tion zu einer ereignisbasierten Simulation um.

469
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

12.11 Zusammenfassung der Vererbung


In den Kapiteln 10 bis 12 haben wir viele verschiedene Aspekte von Vererbungs-
techniken betrachtet. Dazu zählen vor allem die Wiederverwendung von Code und
die Benutzung von Subtypen, aber auch das Beerben von Interfaces, von abstrak-
ten und von konkreten Klassen.
Grundsätzlich können zwei Kernkonzepte bei der Benutzung von Vererbung iden-
tifiziert werden: Wir können sie einsetzen, um Code zu erben (Code-Vererbung),
und wir können sie einsetzen, um Subtypen zu definieren. Ersteres dient der Code-
Wiederverwendung, Letzteres der Polymorphie und der Spezialisierung.
Wenn wir von konkreten Klassen erben (mit extends), tun wir beides: Wir erben die
Implementierung und den Typ. Wenn wir von einem Interface erben (mit imple-
ments), dann trennen wir diese Aspekte: Wir erben den Typ, aber (in der Regel)
keine Implementierung. In Fällen, in denen beides teilweise nützlich ist, können
wir von einer abstrakten Klasse erben: Dann erben wir einen Typ und eine parti-
elle Implementierung.
Wenn wir eine vollständige Implementierung erben, können wir Methoden hin-
zufügen und geerbte Methoden überschreiben. Wenn keine oder nur eine par-
tielle Implementierung von einem Typ geerbt wird, dann muss die Subklasse die
Implementierung liefern, um instanziierbar zu sein.
Einige andere objektorientierte Programmiersprachen bieten auch Mechanismen
an, mit denen Code geerbt werden kann, ohne auch einen Typ zu erben. Java
bietet ein solches Konstrukt nicht an.

470
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.11 Zusammenfassung der Vererbung

Zusammenfassung
In diesem Kapitel haben wir die grundlegenden Prinzipien von Computer-
simulationen diskutiert. Wir haben eine solche Simulation benutzt, um ab-
strakte Klassen und Interfaces als Konstrukte einzuführen, mit denen bessere
Abstraktionen und flexiblere Anwendungen konstruiert werden können.
Abstrakte Klassen sind Klassen, von denen keine direkten Instanzen erzeugt
werden sollen. Sie dienen als Superklassen für andere Klassen. Abstrakte
Klassen können sowohl abstrakte Methoden – Methoden, die einen Kopf
haben, aber keine Implementierung – als auch Methodenimplementierun-
gen enthalten. Konkrete Subklassen von abstrakten Klassen müssen die abs-
trakten Methoden überschreiben, um für alle Methoden eine Implementie-
rung anzubieten.
Eine weitere Möglichkeit zur Definition von Typen in Java sind Interfaces.
Interfaces sind vollständig abstrakten Klassen sehr ähnlich – sie definieren
Methodenköpfe, aber im Allgemeinen keine Implementierungen. Interfaces
definieren Typen, die für Variablendeklarationen benutzt werden können.
Interfaces können benutzt werden, um eine Spezifikation für eine Klasse
(oder einen Teil einer Anwendung) zu definieren, ohne jegliche Implementie-
rungsentscheidungen vorwegzunehmen.
Java erlaubt multiple Vererbung für Interfaces (die implements-Beziehung),
aber nur einfache Vererbung für Klassen (die extends-Beziehung). Multiple
Vererbung wird durch miteinander in Konflikt stehende Default-Methoden
komplizierter.

NEUE BEGRIFFE IN DIESEM KAPITEL


abstrakte Methode, abstrakte Klasse, konkrete Klasse, abstrakte Sub-
klasse, multiple Vererbung, Interface, implements-Beziehung

471
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion

Zusammenfassung der Konzepte


 Abstrakte Methode Die Definition einer abstrakten Methode besteht
aus einer Methodensignatur ohne einen Rumpf. Sie wird mit dem Schlüs-
selwort abstract markiert.
 Abstrakte Klasse Eine abstrakte Klasse ist eine Klasse, von der keine Ins-
tanzen erzeugt werden sollen. Sie dient ausschließlich als Superklasse für
andere Klassen. Abstrakte Klassen dürfen abstrakte Methoden anbieten.
 Abstrakte Subklasse Damit eine Subklasse einer abstrakten Klasse eine
konkrete Klasse werden kann, muss sie Implementierungen für alle geerb-
ten abstrakten Methoden anbieten. Sonst ist sie selbst ebenfalls abstrakt.
 Superklassen-Methodenaufrufe Aufrufe von nicht privaten Instanz-
methoden aus einer Superklasse heraus werden immer im erweiterten
Kontext des dynamischen Typs des Objekts ausgewertet.
 Multiple Vererbung Eine Situation, in der eine Klasse von mehr als einer
Superklasse erbt, wird multiple Vererbung genannt.
 Interface Ein Interface in Java ist eine Spezifikation eines Typs (in Form
eines Typnamens und einer Menge von Methoden). Sie bietet häufig
keine Implementierungen für die meisten ihrer Methoden.

Übung 12.68 Kann eine abstrakte Klasse konkrete (nicht abstrakte) Metho-
den haben? Kann eine konkrete Klasse abstrakte Methoden haben? Kön-
nen Sie eine abstrakte Klasse ohne abstrakte Methoden definieren? Begrün-
den Sie Ihre Antworten.
Übung 12.69 Sehen Sie sich den folgenden Quelltext an. Sie haben fünf
Typen (Klassen oder Interfaces), U, G, B, Z und X, und jeweils eine Variable von
jedem Typ.
U u;
G g;
B b;
Z z;
X x;
Die folgenden Zuweisungen sind alle legal (nehmen Sie an, dass sie sich
übersetzen lassen).
u = z;
x = b;
g = u;
x = u;

472
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.11 Zusammenfassung der Vererbung

Die folgenden Zuweisungen sind alle illegal (sie führen zu Übersetzungsfeh-


lern).
u = b;
x = g;
b = u;
z = u;
g = x;
Was können Sie über diese Typen und ihre Beziehungen zueinander aussa-
gen?
Übung 12.70 Nehmen Sie an, Sie wollen die Personen an einer Universität
modellieren, um ein Kursverwaltungssystem zu schreiben. Es sind unter-
schiedliche Personen beteiligt: Uni-Angestellte, Studenten, Lehrpersonal,
technisches Personal, Tutoren, studentische Hilfskräfte. Tutoren und studen-
tische Hilfskräfte sind dabei interessant: Tutoren seien Studenten, die für
Lehraufgaben angestellt wurden, und studentische Hilfskräfte seien Studen-
ten, die für technische Unterstützung angestellt wurden.
Zeichnen Sie eine Hierarchie (Klassen und Interfaces), die diese Situation wider-
spiegelt. Markieren Sie, welche Typen konkrete Klassen, abstrakte Klassen oder
Interfaces sind.
Übung 12.71 Zusatzaufgabe. Es gibt in der Java-Standardbibliothek einige
Paare Klasse/Interface, die exakt die gleichen Methoden definieren. Häufig
endet der Interface-Name mit Listener und der Klassenname mit Adapter. Ein
Beispiel ist PrintJobListener und PrintJobAdapter. Das Interface definiert
einige Methodenköpfe und die Adapter-Klasse definiert die gleichen Metho-
den, jede mit einem leeren Rumpf. Aus welchem Grund mag es solche Paare
geben?
Übung 12.72 Die Sammlungsbibliothek verfügt über eine Klasse TreeSet, die
eine sortierte Menge modelliert. Die Elemente in dieser Menge werden in
einer Ordnung gehalten. Lesen Sie die Beschreibung dieser Klasse sehr gründ-
lich und schreiben Sie dann eine Klasse Person, deren Instanzen in ein TreeSet
eingefügt werden können, das diese Person-Objekte nach ihrem Alter sortiert.
Übung 12.73 Verwenden Sie die API-Dokumentation für die Klasse Abstract-
List, um eine konkrete Klasse zu schreiben, die eine nicht modifizierbare Liste
hält.

473
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

13 Grafische
Benutzungsoberflächen

Lernziele
Zentrale Konzepte in diesem Kapitel: Grafische Oberflächen konstruie-
ren, Oberflächen-Layout, Oberflächen-Komponenten, Ereignisverarbeitung
Java-Konstrukte in diesem Kapitel: JFrame, JLabel, JButton, JMenuBar, JMenu,
JMenuItem, ActionEvent, Color, FlowLayout, BorderLayout, GridLayout, BoxLayout,
Box, JOptionPane, EtchedBorder, EmptyBorder, anonyme innere Klassen

13.1 Einführung
Bisher haben wir uns in diesem Buch darauf konzentriert, Anwendungen mit
textbasierten Schnittstellen zu entwerfen. Der Grund dafür war nicht etwa, dass
textbasierte Schnittstellen prinzipiell besser sind; sie sind lediglich einfacher zu
konstruieren.
Wir wollten zu Beginn der Beschäftigung mit objektorientierter Programmierung
nicht zu sehr die Aufmerksamkeit von den wichtigen Softwareentwicklungsprin-
zipien ablenken, zu denen wir den Aufbau von Objekten, ihre Interaktion, den
Klassenentwurf und die Qualität von Quelltexten zählen.
Grafische Benutzungsoberflächen (im Folgenden meist kurz GUIs genannt, vom
Englischen Graphical User Interface) werden auch mit interagierenden Objekten
realisiert, haben aber eine sehr spezielle Struktur; wir haben ihre Einführung bis-
her vermieden, um zuerst die allgemeingültigen Prinzipien von Objektstrukturen
zu besprechen. Nun sind wir jedoch ausreichend vorbereitet, um auch einen Blick
auf die Konstruktion von grafischen Oberflächen zu werfen.
Wir werden in diesem Kapitel an einigen Stellen das Konzept der Lambda-Aus-
drücke ausgiebig einsetzen, die mit Java 8 eingeführt wurden. Dieses Konzept
haben wir ausführlich in Kapitel 5 besprochen – ein Kapitel, das wir an dieser
Stelle im Buch als „fortgeschritten“ ausgewiesen hatten. Wenn Sie dieses Kapitel
noch nicht gelesen haben, empfehlen wir Ihnen, dies jetzt nachzuholen, um sich
mit der Syntax und dem Gebrauch von Lambda-Ausdrücken vertraut zu machen.
Außerdem lohnt es sich, die als „fortgeschritten“ bezeichneten Themen in Kapi-
tel 12 zu wiederholen, da in den GUI-Bibliotheken von Java in wesentlichem
Umfang auf abstrakte Klassen und Interface-Typen zurückgegriffen wird.

475
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Grafische Oberflächen geben unseren Anwendungen eine Schnittstelle, die aus


Fenstern, Menüs, Knöpfen und anderen Komponenten besteht – und geben
ihnen damit viel stärker das Aussehen von typischen Anwendungen, mit denen
die meisten Computeranwender umgehen.
Beachten Sie, dass wir hier wieder über die Doppelbedeutung des Begriffs
„Schnittstelle“ stolpern. Die Schnittstellen, über die wir hier reden, sind nicht die
Schnittstellen von Klassen. Wir reden hier über Benutzungsschnittstellen – den
Teil einer Anwendung, der auf dem Bildschirm sichtbar zur Interaktion mit dem
Benutzer bereitsteht.
Sobald wir wissen, wie man GUIs mit Java erstellt, können wir sehr viel ansehnli-
chere Programme schreiben.

13.2 Komponenten, Layout und


Ereignisbehandlung
Die Zahl der Detailmöglichkeiten bei der Konstruktion von GUIs ist riesig. Wir wer-
den in diesem Buch nicht in der Lage sein, alle diese Details darzustellen, aber wir
werden die grundlegenden Prinzipien und reichlich Beispiele diskutieren.
Sämtliche GUI-Programmierung in Java erfolgt durch die Benutzung von speziell
dafür ausgewiesenen Standardbibliotheken. Sobald wir die Prinzipien verstanden
haben, können wir alle notwendigen Details in der Dokumentation dieser Biblio-
theken nachlesen.
Die Kernfragen, die wir klären müssen, ordnen sich thematisch in drei Bereiche:
 Welche Elemente können wir auf dem Bildschirm darstellen?
 Wie können wir diese Elemente auf dem Bildschirm arrangieren?
 Wie können wir auf Benutzereingaben reagieren?
Konzept
Wir widmen uns diesen Fragen unter den Schlagworten Komponenten, Layout und
Eine GUI wird Ereignisverarbeitung.
erzeugt, indem
Komponenten Komponenten sind die individuellen Bestandteile, aus denen sich eine grafische
auf dem Bildschirm Oberfläche zusammensetzt. Dazu gehören Dinge wie Knöpfe, Menüs, Menüein-
arrangiert werden. träge, Combo-Boxen, Schieberegler, Textfelder und so weiter. Die Java-Bibliothek
Komponenten wer-
enthält eine große Anzahl vorgefertigter Komponenten, und wir können auch
den durch Objekte
repräsentiert. eigene erstellen. Wir werden die wichtigsten Komponenten kennenlernen und
erfahren, wie man sie erzeugt und so gestaltet, wie man sie gern hätte.

Konzept Das Layout regelt, wie die Komponenten auf dem Bildschirm arrangiert werden.
Ältere, primitivere GUI-Systeme haben dies mit zweidimensionalen Koordinaten defi-
Das Layout der niert: Ein Programmierer musste die x- und y-Koordinaten (in Bildschirmpunkten) für
Komponenten, also
die Position und Größe jeder einzelnen Komponente festlegen. In moderneren GUI-
ihre Anordnung auf
dem Bildschirm, wird Systemen wäre das zu kurz gedacht. Heutzutage müssen verschiedene Bildschirm-
durch Layout-Mana- auflösungen und Zeichensätze berücksichtigt werden, außerdem Benutzer, die die
ger kontrolliert. Fenster in ihrer Größe verändern, sowie weitere Aspekte, die dazu führen, dass Lay-
out-Fragen sehr viel komplizierter werden. Die Lösung ist deshalb ein Schema, in
dem das Layout von Komponenten auf einer abstrakteren Ebene beschrieben wird.

476
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.4 Das Beispiel: ein Bildbetrachter

Wir können darin beispielsweise festlegen, dass „diese Komponente unter jener
erscheinen sollte“ oder dass „diese Komponente sich ausdehnen soll, wenn das
Fenster vergrößert wird, aber jene immer ihre konstante Größe behalten sollte“. Wir
werden sehen, dass dies mithilfe von sogenannten Layout-Managern möglich ist.
Ereignisverarbeitung wird die Technik genannt, mit der Benutzereingaben behan- Konzept
delt werden. Sobald wir die Komponenten unserer Oberfläche erzeugt und auf
Der Begriff Ereig-
dem Bildschirm positioniert haben, sollten wir auch sicherstellen, dass etwas pas-
nisverarbei-
siert, wenn ein Benutzer auf einen Knopf klickt. Das Modell, das in der Java-Bib- tung bezeichnet
liothek verwendet wird, basiert auf Ereignissen: Wenn ein Benutzer eine Kompo- die Aufgabe, auf
nente aktiviert (beispielsweise einen Knopf anklickt oder einen Menüeintrag Benutzereingaben
selektiert), löst das System ein Ereignis aus. Unsere Anwendung kann dann von wie Mausklicks
diesem Ereignis benachrichtigt werden (indem eine ihrer Methoden aufgerufen oder Tastendrücke
wird), und wir können die angemessene Reaktion generieren. zu reagieren.

Wir werden diese Themenbereiche sehr viel detaillierter im Laufe dieses Kapitels
untersuchen. Vorher brauchen wir jedoch noch ein wenig mehr Hintergrund-
information und etwas Terminologie.

13.3 AWT und Swing


Java verfügt über drei GUI-Bibliotheken. Die älteste heißt AWT (Abstract Window
Toolkit) und wurde mit dem allerersten Java-System ausgeliefert. Später wurde
eine stark verbesserte GUI-Bibliothek namens Swing hinzugefügt. Zuletzt kam
die Bibliothek JavaFX hinzu. In diesem Kapitel werden wir uns auf den Einsatz
von Swing konzentrieren, doch viele der hier behandelten Prinzipien können all-
gemein auf GUI-Bibliotheken angewandt werden.
Swing benutzt einige der Klassen aus dem AWT, ersetzt einige AWT-Klassen mit
einer eigenen Version und fügt sehr viele neue Klassen hinzu (Abbildung 13.1).
Das bedeutet, dass wir einige AWT-Klassen verwenden, die noch immer in Swing-
Programmen benutzt werden, aber dass wir die Swing-Versionen all der Klassen
verwenden, die in beiden Bibliotheken angeboten werden.

Abbildung 13.1
AWT und Swing.

Immer dann, wenn äquivalente Klassen in AWT und Swing existieren, sind die
Swing-Versionen durch ein „J“ am Anfang des Klassennamens gekennzeichnet.
Sie werden beispielsweise die Klassen Button und JButton finden, Frame und JFrame,
Menu und JMenu und so weiter. Die Klassen mit dem „J“ am Anfang sind die Swing-
Versionen – und diese werden wir benutzen. In einer Anwendung sollten die bei-
den Versionen nicht gemeinsam benutzt werden.
Das ist vorläufig genug Hintergrundinformation. Sehen wir uns einen Programm-
text genauer an.

477
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

13.4 Das Beispiel: ein Bildbetrachter


Konzept Wie immer werden wir uns die neuen Konzepte anhand eines Beispiels verdeut-
lichen. Die Anwendung, die wir in diesem Kapitel erstellen werden, ist ein Bild-
Bildformat: Digi-
betrachter (Abbildung 13.2). Mit diesem Programm können Bilddateien im JPEG-
tale Bilder können
in unterschiedli- und im PNG-Format betrachtet werden, die Bilder können aber auch verändert
chen Formaten und wieder auf der Platte gespeichert werden.
gespeichert werden.
Insgesamt werden wir eine eigene Bildklasse zur Repräsentation von Bilddaten im
Die Unterschiede
wirken sich primär Speicher benutzen, wir werden verschiedene Filter implementieren, mit denen
auf die Größe und wir ein Bild verändern können, und wir werden Swing-Komponenten benutzen,
den Informations- um eine grafische Oberfläche zu erstellen. Während wir all das tun, wollen wir
gehalt aus. uns primär mit der Diskussion der GUI-Aspekte beschäftigen.
Wenn Sie neugierig auf das sind, was wir bauen werden, dann können Sie das
Projekt Bildbetrachter1-0 öffnen und ausprobieren; erzeugen Sie einfach ein
Objekt der Klasse Bildbetrachter – es ist die Version, die in Abbildung 13.2
gezeigt wird. Es gibt einige Beispielbilder im Ordner bilder, der sich im Ordner
Kapitel13 befindet (eine Ebene über dem Projektordner). Sie können natürlich
auch Ihre eigenen Bilder öffnen. Wir werden hier jetzt etwas langsamer starten,
mit einer viel simpleren Version, und uns den Weg zur vollständigen Anwendung
Schritt für Schritt erarbeiten.

Abbildung 13.2
Eine einfache
Anwendung zum
Bildbetrachten.

13.4.1 Erste Schritte: ein Fenster erzeugen


Fast alles, was Sie in einer grafischen Oberfläche zu sehen bekommen, befindet sich
in einem speziellen Fenster, einem Top-Level-Fenster. Ein solches Fenster heißt Top-
Level, weil es sich unmittelbar unter der Kontrolle der Fensterverwaltung des jeweili-
gen Betriebssystems befindet und typischerweise unabhängig von anderen Fenstern
bewegt, in der Größe verändert, minimiert und maximiert werden kann.

478
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.4 Das Beispiel: ein Bildbetrachter

In Java heißen diese Top-Level-Fenster Frames. In Swing werden sie durch die Klasse
JFrame repräsentiert.

Listing 13.1
Eine erste
Version der Klasse
Bildbetrachter.

Um eine grafische Oberfläche auf den Bildschirm zu bekommen, müssen wir als
Erstes einen solchen Frame erzeugen und anzeigen. In Listing 13.1 wird die kom-
plette Klassendefinition einer Klasse gezeigt (in weiser Vorausschau Bildbetrachter
genannt), die einen Frame auf dem Bildschirm anzeigt. Diese Klasse ist im Projekt
Bildbetrachter0-1 verfügbar (die Nummer steht für Version 0.1).

Übung 13.1 Öffnen Sie das Projekt Bildbetrachter0-1 (dies wird die Basis
für Ihren eigenen Bildbetrachter). Erzeugen Sie eine Instanz der Klasse Bild-
betrachter. Vergrößern Sie das angezeigte Fenster. Was beobachten Sie bei
der Platzierung des Textes innerhalb des Fensters?

Wir werden uns nun die Klasse Bildbetrachter aus Listing 13.1 im Detail ansehen.

479
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Die ersten drei Zeilen der Klassendefinition sind import-Anweisungen für alle Klassen
aus den Paketen [Link], [Link] und javax.swing1. Wir brauchen etliche
der Klassen in diesen Paketen für alle Swing-Anwendungen, deswegen sollten wir
diese drei Pakete immer komplett in all unsere GUI-Anwendungen importieren.
Ein Blick auf den Rest der Klassendefinition zeigt schnell, dass die interessanten
Dinge in der Methode fensterErzeugen passieren. Diese Methode kümmert sich
um den Aufbau der grafischen Oberfläche. Im Konstruktor der Klasse steht ledig-
lich ein Aufruf dieser Methode. Wir haben dies getan, weil auf diese Weise sämt-
licher Quelltext zur Erzeugung der Oberfläche an einer wohldefinierten Stelle
steht und später leichter wiederzufinden ist (Kohäsion!). Wir werden dies in all
unseren GUI-Beispielen ebenfalls tun.
Die Klasse hat eine Instanzvariable vom Typ JFrame. Diese Variable hält eine Refe-
renz auf das Fenster, das der Bildbetrachter auf dem Bildschirm anzeigen möchte.
Sehen wir uns die Methode fensterErzeugen genauer an.
Die erste Zeile lautet:
fenster = new JFrame("Bildbetrachter");
Diese Anweisung erzeugt einen neuen Frame und speichert ihn in einer Instanz-
variablen für die weitere Verwendung.
Als prinzipielle Richtlinie sollten Sie sich, parallel zum Studieren der Beispiele im
Buch, auch die Klassendokumentationen der Klassen ansehen, die wir neu kennen-
lernen. Dies gilt für alle Klassen, die wir benutzen – wir werden von jetzt an nicht
mehr darauf hinweisen, sondern davon ausgehen, dass Sie es selbstständig tun.

Übung 13.2 Finden Sie die Dokumentation der Klasse JFrame. Welchem Zweck
dient der Parameter Bildbetrachter, den wir im obigen Konstruktoraufruf ver-
wenden?

Konzept Ein Frame besteht aus drei Teilen: der Titelzeile, einer optionalen Menüzeile (menu
bar) und der Inhaltsfläche (content pane), siehe Listing 13.3. Das exakte Erschei-
Komponenten wer-
nungsbild der Titelzeile hängt vom jeweiligen Betriebssystem ab. Sie enthält norma-
den in einem Fens-
ter platziert, indem lerweise den Fenstertitel und einige Knöpfe zur Fensterkontrolle.
sie entweder in die Die Menüzeile und die Inhaltsfläche befinden sich unter der Kontrolle der Anwen-
Menüzeile
dung. Beiden können GUI-Komponenten hinzugefügt werden, um eine Oberflä-
(menu bar ) oder
die Inhalts- che zu konstruieren. Wir sehen uns zuerst die Inhaltsfläche an.
fläche (content
pane ) eingefügt
werden.

1 Das swing-Paket befindet sich tatsächlich in einem Paket, das javax (mit einem „x“ am Ende)
und nicht java heißt. Dies hat überwiegend historische Gründe – es scheint keine logische Er-
klärung dafür zu geben.

480
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.4 Das Beispiel: ein Bildbetrachter

Abbildung 13.3
Titelleiste
Die verschiedenen
Menüleiste
Teile eines Frames.

Inhaltsbereich

Rahmen

13.4.2 Einfache Komponenten einfügen


Unmittelbar nach Erzeugung des JFrame ist das Fenster unsichtbar und die Inhalts-
fläche ist leer. Wir fahren fort, indem wir einen Text in die Inhaltsfläche einfügen.
Container contentPane = [Link]();

JLabel label = new JLabel("Ich bin ein Label");


[Link](label);
In der ersten Zeile lassen wir uns die Inhaltsfläche des Fensters geben. Dies müs-
sen wir immer tun – GUI-Komponenten werden in einen Frame eingefügt, indem
sie in die Inhaltsfläche eingefügt werden.2
Die Inhaltsfläche selbst ist vom Typ Container. Ein solcher Container ist eine Swing-
Komponente, die eine beliebige Gruppe weiterer GUI-Komponenten enthalten
kann – so wie eine ArrayList eine beliebige Sammlung von Objekten enthalten
kann. Wir werden uns Container später noch genauer ansehen.
Wir erzeugen dann eine Komponente, die mit einem Text versehen werden kann
(quasi ein „Etikett“, deshalb vom Typ JLabel) und fügen sie in die Inhaltsfläche ein.
Ein solches Label kann Text und/oder ein Bild anzeigen.
Schließlich haben wir die beiden Zeilen
[Link]();
[Link]();
Die erste Zeile sorgt dafür, dass das Fenster die eingefügten Komponenten ordent-
lich arrangiert und sich selbst entsprechend in der Größe anpasst. Wir müssen die
Methode pack jedes Mal an einem JFrame aufrufen, wenn wir Komponenten hinzu-
fügen oder in der Größe verändern.

2 Die Java-Klasse JFrame definiert ebenfalls eine add-Methode, mit der eine Komponente direkt
in die Inhaltsfläche eingefügt werden kann (ohne den Umweg über getContentPane()). Da wir
allerdings den expliziten Zugriff auf die Inhaltsfläche aus bestimmten Gründen später sowieso
noch benötigen, können wir besagte Technik ebenso gut auch hier schon zum Einfügen der
Komponenten in die Inhaltsfläche verwenden.

481
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Die letzte Zeile schließlich macht das Fenster auf dem Bildschirm sichtbar. Wir
beginnen immer mit einem unsichtbaren Fenster, damit das Arrangieren der Kom-
ponenten innerhalb des Fensters während des Konstruktionsprozesses nicht sicht-
bar wird. Wenn das Fenster vollständig konstruiert ist, können wir es im komplet-
ten Zustand anzeigen.

Übung 13.3 Eine weitere häufig verwendete Swing-Komponente ist ein Knopf
(button, Typ JButton), der gedrückt werden kann. Ersetzen Sie im obigen Bei-
spiel das Label mit einem Knopf.
Übung 13.4 Was passiert, wenn Sie zwei Label (oder zwei Knöpfe) in die
Inhaltsfläche einfügen? Können Sie erklären, was Sie beobachten? Experi-
mentieren Sie mit Größenänderungen am Fenster.

13.4.3 Eine alternative Struktur


Wir haben uns dafür entschieden, unsere Anwendung zu entwickeln, indem wir ein
JFrame-Objekt als Attribut der Klasse Bildbetrachter erzeugen und es mit weiteren
GUI-Komponenten bestücken, die außerhalb des Frame-Objekts erzeugt werden.
Eine alternative Struktur für diese Top-Level-Lösung wäre es, Bildbetrachter als eine
Subklasse von JFrame zu definieren und intern zu bestücken. Dieser Stil ist ebenfalls
häufig anzutreffen. Listing 13.2 ist die alternative Entsprechung zu Listing 13.1.
Diese Version finden Sie als Projekt unter der Bezeichnung Bildbetrachter0-1a. Sie
sollten sich mit beiden Stilen vertraut machen, denn keine ist der anderen vorzuzie-
hen. Wir werden im Rest des Kapitels mit unserer ursprünglichen Version fortfahren.

Listing 13.2
Eine alternative Struk-
tur für die Klasse
Bildbetrachter.

482
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.4 Das Beispiel: ein Bildbetrachter

13.4.4 Menüs hinzufügen


Unser nächster Schritt bei der Konstruktion einer GUI ist das Definieren von
Menüs und Menüeinträgen. Konzeptuell ist das einfach, mit einem schwierigen
Detail: Wie definieren wir die Reaktion auf Benutzereingaben, beispielsweise die
Auswahl eines Menüeintrags? Dazu kommen wir gleich.
Zuerst erstellen wir die Menüs. Drei Klassen sind daran beteiligt:
 JMenuBar – Ein Objekt dieser Klasse repräsentiert eine Menüzeile, die unterhalb
der Titelzeile am oberen Rand eines Fensters angezeigt werden kann (siehe
Abbildung 13.3). Jedes Fenster hat höchstens einen JMenuBar.3
 JMenu – Objekte dieser Klasse repräsentieren ein einzelnes Menü (wie etwa die
bekannten Menüs „Datei“, „Bearbeiten“ oder „Hilfe“). Menüs werden oft in
einer Menüzeile platziert. Sie können auch als Kontextmenüs auftreten, aber
diese werden wir vorläufig nicht behandeln.
 JMenuItem – Objekte dieser Klasse repräsentieren einen einzelnen Menüeintrag
innerhalb eines Menüs (wie etwa „Öffnen“ oder „Speichern“).
Für unseren Bildbetrachter werden wir eine Menüzeile mit mehreren Menüs und
Menüeinträgen erzeugen.
Die Klasse JFrame bietet eine Methode setJMenuBar. Wir können eine Menüzeile
erzeugen und sie mit dieser Methode in das Hauptfenster eintragen:
JMenuBar menuezeile = new JMenuBar();
[Link](menuezeile);
Nun können wir ein Menü erzeugen und es in die Menüzeile einfügen:
JMenu dateimenue = new JMenu("Datei");
[Link](dateimenue);
Diese beiden Zeilen erzeugen ein Menü mit der Bezeichnung DATEI und fügen es in
die Menüzeile ein. Schließlich können wir Menüeinträge in das Menü einfügen.
Die folgenden Zeilen fügen zwei Einträge, bezeichnet mit ÖFFNEN und BEENDEN, in
das Dateimenü ein:

3 Im MacOS ist die Standarddarstellung anders: Die Menüzeile befindet sich am oberen Rand des
Bildschirms, nicht am oberen Rand des jeweiligen Fensters. In Java-Anwendungen sorgt die Vorein-
stellung dafür, dass die Menüzeile Teil des Fensters ist. Sie kann über eine für das MacOS spezifische
Einstellungsmöglichkeit auch bei Java-Anwendungen am oberen Bildschirmrand platziert werden.

483
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

JMenuItem oeffnenEintrag = new JMenuItem("Öffnen");


[Link](oeffnenEintrag);
JMenuItem beendenEintrag = new JMenuItem("Beenden");
[Link](beendenEintrag);

Übung 13.5 Fügen Sie das hier diskutierte Menü und die Menüeinträge in
Ihr Bildbetrachter-Projekt ein. Was passiert, wenn Sie einen Menüeintrag
selektieren?
Übung 13.6 Fügen Sie ein weiteres Menü HILFE hinzu, das einen Eintrag INFO
enthält. (Hinweis: Um Lesbarkeit und Kohäsion zu erhöhen, könnte es sinn-
voll sein, die Erzeugung der Menüs in eine eigene Methode mit einem Namen
wie menuezeileErzeugen zu verschieben, die aus der Methode fensterErzeugen
aufgerufen wird.)

Bis zu diesem Punkt haben wir bereits die halbe Miete: Wir können Menüs erzeu-
gen und anzeigen. Aber die zweite Hälfe fehlt noch: Bisher passiert nichts, wenn
ein Benutzer einen Menüeintrag auswählt. Wir müssen nun Code hinzufügen,
mit dem auf die Selektion von Menüeinträgen reagiert werden kann. Dies ist
Thema des nächsten Abschnitts.

13.4.5 Ereignisverarbeitung
Swing benutzt ein sehr flexibles Modell für den Umgang mit Oberflächeneingaben:
Die Ereignisverarbeitung mit sogenannten Event-Listenern, die quasi auf Ereig-
nisse „lauschen“ (vom Englischen to listen, zuhören).

Konzept Das Swing-Rahmenwerk selbst und einige seiner Komponenten lösen Ereignisse
(events) aus, wenn etwas passiert, an dem andere Objekte interessiert sein könnten.
Ein Objekt kann auf
Es gibt verschiedene Arten von Ereignissen, die durch unterschiedliche Arten von
Ereignisse „lau-
schen“, indem es Aktionen ausgelöst werden. Wenn ein Knopf geklickt oder ein Menüeintrag selek-
ein Event-Liste- tiert wird, löst eine Komponente ein ActionEvent aus. Wenn eine Maus geklickt oder
ner-Interface bewegt wird, wird ein MouseEvent ausgelöst. Wenn ein Fenster geschlossen oder
implementiert. minimiert wird, wird ein WindowEvent generiert. Darüber hinaus gibt es viele weitere
Ereignisarten.
Jedes unserer Objekte kann zu einem Event-Listener für jedes dieser Ereignisse
gemacht werden. Wenn es ein solcher Listener ist, wird es über alle Ereignisse
informiert, auf die es „lauscht“. Ein Objekt wird zu einem Event-Listener, indem
es eines der zahlreich existierenden Listener-Interfaces implementiert. Wenn es
das richtige Interface implementiert, kann es sich bei der Komponente registrie-
ren, die es „belauschen“ will.
Sehen wir uns ein Beispiel an. Menüeinträge (Klasse JMenuItem) lösen ActionEvents
aus, wenn sie vom Benutzer selektiert werden. Objekte, die auf diese Ereignisse

484
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.4 Das Beispiel: ein Bildbetrachter

reagieren wollen, müssen das Interface ActionListener aus dem Paket [Link].
event implementieren.

Es gibt zwei Möglichkeiten für die Implementierung von Event-Listenern: Entwe-


der lauscht ein einzelnes Objekt auf Ereignisse von vielen verschiedenen Ereignis-
quellen oder jede Ereignisquelle bekommt einen eigenen Listener. Wir werden
beide Möglichkeiten in den nächsten beiden Abschnitten diskutieren.

13.4.6 Die zentralisierte Variante für


Ereignisverarbeitung
Wir müssen drei Dinge tun, um unseren Bildbetrachter zum einzigen Listener für
alle Ereignisse aus dem Menü zu machen:
1 Wir müssen im Kopf der Klasse deklarieren, dass sie das Interface Action-
Listener implementiert.

2 Wir müssen eine Methode mit der Signatur


public void actionPerformed(ActionEvent e)
implementieren. Dies ist die einzige Methode, die im Interface ActionListener
definiert ist.
3 Wir müssen die Methode addActionListener des jeweiligen Menüeintrags auf-
rufen, um das Bildbetrachter-Objekt als einen Listener zu registrieren.
Die Punkte 1 und 2 – Implementieren des Interface und Definieren der Methode –
stellen sicher, dass unser Objekt ein Subtyp von ActionListener ist. Punkt 3 regist-
riert unser eigenes Objekt dann als Listener bei den Menüeinträgen. Listing 13.3
zeigt den Quelltext für dieses Vorgehen im Zusammenhang.

Listing 13.3
Einfügen eines Action-
Listener bei einem
Menüeintrag.

485
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Beachten Sie insbesondere die Zeilen


JMenuItem oeffnenEintrag = new JMenuItem("Öffnen");
[Link](this);
in diesem Programmbeispiel. Hier wird ein Menüeintrag erzeugt und anschließend
das aktuelle Objekt (das Bildbetrachter-Objekt selbst) als Action-Listener registriert,
indem this als Parameter an die Methode addActionListener übergeben wird.
Das Registrieren unseres Objekts als Listener des Menüeintrags bewirkt, dass unsere
eigene Methode actionPerformed durch den Menüeintrag jedes Mal dann aufgeru-
fen wird, wenn der Eintrag selektiert wird. Wenn unsere Methode aufgerufen wird,
übergibt der Menüeintrag einen Parameter vom Typ ActionEvent, der zusätzliche
Informationen über das eingetretene Ereignis liefert. Zu diesen Informationen gehö-
ren der exakte Zeitpunkt des Ereignisses, der Zustand der Zusatztasten (Umschalt-,
Steuerung- und Alt/Meta-Tasten), ein „Kommando-String“ und einiges mehr.
Der Kommando-String ist eine Zeichenkette, die auf geeignete Weise die Kom-
ponente identifiziert, die das Ereignis ausgelöst hat. Bei Menüeinträgen ist dies
per Voreinstellung der Text des Eintrags.
In unserem Beispiel in Listing 13.3 registrieren wir bei beiden Menüeinträgen das-
selbe Listener-Objekt. Dies bedeutet, dass beide Menüeinträge dieselbe action-
Performed-Methode aufrufen, wenn sie aktiviert werden.

In der actionPerformed-Methode geben wir einfach den Kommando-String des


Eintrags aus, um zu zeigen, dass dieses Schema funktioniert. An dieser Stelle könn-
ten wir nun den Code einfügen, der die Selektion eines Menüeintrags korrekt ver-
arbeitet.
Der bisher besprochene Quelltext steht als Projekt Bildbetrachter0-2 im Zusatz-
material zur Verfügung.

486
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.4 Das Beispiel: ein Bildbetrachter

Übung 13.7 Implementieren Sie den oben diskutierten Code zur Verarbeitung
von Menü-Ereignissen in Ihrem eigenen Bildbetrachter-Projekt. Alternativ kön-
nen Sie auch das Projekt Bildbetrachter0-2 öffnen und sich sorgfältig den
Quelltext ansehen. Beschreiben Sie schriftlich und ausführlich die Sequenz der
Ereignisse, die sich aus der Aktivierung des Menüeintrags BEENDEN ergibt.
Übung 13.8 Fügen Sie einen weiteren Menüeintrag SPEICHERN hinzu.
Übung 13.9 Fügen Sie Ihrer Klasse drei private Methoden dateiOeffnen, datei-
Speichern und beenden hinzu. Ändern Sie die Methode actionPerformed so ab,
dass sie die entsprechende Methode aufruft, wenn ein Menüeintrag selektiert
wird.
Übung 13.10 Wenn Sie Übung 13.6 durchgeführt haben (Einfügen eines
HILFE-Menüs), stellen Sie sicher, dass auch dieser Menüeintrag geeignet behan-
delt wird.

Wie wir sehen, funktioniert dieser Ansatz. Wir können nun Methoden für die
einzelnen Menüeinträge implementieren, die die jeweiligen Programmaufgaben
ausführen. Es gibt jedoch einen Aspekt, den wir näher untersuchen sollten: Die
aktuelle Lösung ist hinsichtlich Wartbarkeit und Erweiterbarkeit nicht besonders
gelungen.
Untersuchen Sie den Rumpf der Methode actionPerformed, die Sie für Übung 13.9
schreiben mussten. Er birgt mehrere Probleme:
 Sie haben vermutlich eine if-Anweisung mit der Methode getActionCommand
benutzt, um herauszufinden, welcher Eintrag aktiviert wurde. Beispielsweise
könnten Sie schreiben:
if([Link]().equals("Öffnen")) …
Es ist keine gute Idee, abhängig von der Bezeichnung eines Menüeintrags eine
Funktion auszuführen. Was passiert, wenn Sie Ihre Oberfläche in eine andere
Sprache übersetzen? Wenn Sie den Text des Menüeintrags ändern, würde das
Programm nicht mehr funktionieren. (Oder Sie müssten alle Stellen im Quell-
text finden, an denen diese Zeichenkette verwendet wird, und sie entspre-
chend abändern – eine mühsame und fehlerträchtige Arbeit.)
 Eine zentrale „Verteiler“-Methode zur Auswahl des aufzurufenden Anwen-
dungscodes (wie unsere Methode actionPerformed) ist keine gelungene Struktur.
Letztendlich rufen alle Menüeinträge eine einzige Methode auf, die wiederum
nichts anderes tut, als für jeden Eintrag eine zugeordnete Methode aufzurufen.
Dies ist ärgerlich aus Wartungssicht (für jeden zusätzlichen Menüeintrag müssen
wir eine neue if-Anweisung in actionPerformed einfügen) und wirkt auch wie
überflüssiger Aufwand. Es wäre deutlich besser, wenn wir jeden Menüeintrag
dazu bringen könnten, die zugeordnete Methode selbst direkt aufzurufen.
Im nächsten Abschnitt zeigen wir, wie Lambda-Ausdrücke von Java 8 den Code
zur Ereignisverarbeitung deutlich vereinfachen können.

487
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

13.4.7 Lambda-Ausdrücke zur Ereignisverarbeitung


In diesem Abschnitt werden wir das Konzept der Lambda-Ausdrücke ausgiebig
einsetzen, die mit Java 8 eingeführt wurden und die wir ausführlich in Kapitel 5
dieses Buchs behandelt haben.
Das Interface ActionListener enthält eine einzige abstrakte Methode, was bedeu-
tet, dass es laut Definition ein funktionales Interface ist, wie in Kapitel 12 einge-
führt. Dort hatten wir gesagt, dass ein Lambda-Ausdruck immer dann verwendet
werden kann, wenn ein Objekt eines funktionalen Interface-Typs benötigt wird.
Wird also die addActionListener-Methode eines JMenuItem aufgerufen wird, so
können wir einen Lambda-Ausdruck benutzen, um die Aktionen festzulegen, die
ausgeführt werden, wenn das entsprechende ActionEvent eintritt. Der folgende
Code zeigt die vollständige Syntax für einen Lambda-Ausdruck, der zur Ereignis-
verarbeitung für den Menüeintrag ÖFFNEN eingesetzt wird:
[Link](
(ActionEvent e) –> { dateiOeffnen(); }
);
Wird ein Menüeintrag ausgewählt, dann erhält der zugehörige Listener-Lambda-
Ausdruck ein ActionEvent-Objekt – mit dem er nichts macht – und der Rumpf des Lis-
teners ruft die Methode dateiOeffnen auf, die in der umschließenden Bildbetrach-
ter-Klasse definiert ist. Der Rumpf eines Lambda-Ausdrucks kann auf alle Anteile der
umschließenden Klasse zugreifen – einschließlich der als privat deklarierten.

Übung 13.11 Implementieren Sie die hier beschriebene Behandlung von


Menüeinträgen mit inneren Klassen in Ihrer eigenen Version des Bildbetrach-
ters.

Wir können die vereinfachte Syntax für Lambda-Ausdrücke benutzen, die es uns
erlaubt, den Parametertyp bei Lambda-Ausdrücken mit nur einem Parameter
auszulassen sowie die geschweiften Klammern bei einer einzigen Anweisung.
Der Code, um den Listener hinzuzufügen, sieht dann folgendermaßen aus:
[Link](e –> dateiOeffnen());
Version 0-3 des Bildbetrachter-Projekts implementiert seine Listener auf diese Weise.

Übung 13.12 Öffnen Sie das Projekt Bildbetrachter0-3 und untersuchen Sie
es. Damit ist gemeint: Testen Sie es und sehen Sie sich den Quelltext an.
Wundern Sie sich nicht, wenn Sie einiges nicht verstehen, denn einige neue
Konstrukte werden erst in diesem Kapitel thematisiert.
Übung 13.13 Sie haben möglicherweise bemerkt, dass das Auswählen des
Menüeintrags BEENDEN nun das Programm beendet. Sehen Sie sich an, wie
dies erfolgt. Lesen Sie in der Dokumentation der beteiligten Klassen und
Methoden nach.

488
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.4 Das Beispiel: ein Bildbetrachter

Viele der Listener-Interfaces, die im Zusammenhang mit GUIs stehen, sind funk-
tionale Interfaces und es ist ratsam, Listener mithilfe von Lambda-Ausdrücken zu
implementieren. Wir werden dieser Konvention bei zukünftigen Versionen des
Bildbetrachter-Projekts folgen. Für nicht funktionale Interfaces – d.h. solche, die
mehr als eine abstrakte Methode haben – ist eine andere Syntax erforderlich, die
wir in Abschnitt 13.8 behandeln werden: innere Klassen.

Beachten Sie auch, dass nun nicht mehr die Klasse Bildbetrachter den Action-
Listener implementiert (wir haben ihre actionPerformed-Methode entfernt), son-
dern der Lambda-Ausdruck. (Lambda-Ausdrücke implementieren Interfaces allein
aufgrund der Tatsache, dass sie eine Implementierung liefern, die zur Signatur
der Methode im funktionalen Interface passt.) Dies erlaubt es uns, Lambda-Aus-
drücke als Action-Listener für die Menüeinträge zu verwenden.

Zusammengefasst haben wir erreicht, dass nicht länger das Bildbetrachter-Objekt


auf alle Ereignisse lauscht, sondern haben stattdessen für jedes mögliche Ereignis
separate Listener-Objekte erzeugt – definiert durch einen Lambda-Ausdruck –,
wobei jeder Listener nur mit einem Ereignistyp umgehen muss. Da jeder Listener
seine eigene Implementierung der actionPerformed-Methode hat, können wir nun
eine spezifische Behandlung in diesen Methoden angeben. Und da die Lambda-
Ausdrücke im Sichtbarkeitsbereich der umschließenden Klasse liegen, können sie
in der der Implementierung ihres Rumpfs vollen Gebrauch von der umschließenden
Klasse machen, weil sie auf die privaten Datenfelder und Methoden dieser Klasse
zugreifen können.

Diese Struktur ist kohärent und erweiterbar. Wenn wir einen zusätzlichen Menü-
eintrag benötigen, fügen wir einfach Code zum Erzeugen des Eintrags sowie den
Lambda-Ausdruck zur Behandlung der zugehörigen Funktion hinzu. Es ist keine
Auflistung in einer zentralen Methode notwendig.

Der Einsatz von Lambda-Ausdrücken kann allerdings den Quelltext schwerer les-
bar machen, wenn die Ausdrücke lang sind. Es wird dringend empfohlen, sie nur
für sehr kurze Handler-Funktionalität und für etablierte Programmiermuster zu
verwenden. Für komplexeren Code ist es besser, den Lambda-Ausdruck in einer
separaten Methode aufzurufen, die in der umschließenden Klasse definiert ist –
wie wir es hier getan haben – oder den Einsatz von inneren Klassen zu erwägen.
Innere Klassen werden in Abschnitt 13.8 besprochen.

13.4.8 Zusammenfassung der zentralen


GUI-Konzepte
Zu Anfang dieses Kapitels haben wir die drei Stützpfeiler der Entwicklung von
GUI-Anwendungen aufgelistet: Komponenten, Layout und Ereignisverarbeitung.
In den folgenden Abschnitten haben wir uns dann auf zwei dieser Themenge-
biete konzentriert: die Komponenten, von denen wir diverse Vertreter kennenge-
lernt haben (Label, Knöpfe, Menüs, Menüeinträge) und die Ereignisverarbeitung,
die wir in einiger Ausführlichkeit besprochen haben.

489
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Um zum aktuellen Zustand zu gelangen – Anzeigen eines Fensters mit einem Label
und einigen Menüs – mussten wir hart arbeiten, denn es gab eine Menge Hinter-
grundkonzepte zu diskutieren. Von jetzt an wird es einfacher – wirklich! Das Ver-
stehen der Ereignisverarbeitung für Menüeinträge ist vermutlich das Schwierigste,
das wir für unser Beispiel zu meistern hatten.
Das Hinzufügen weiterer Menüs und Komponenten zu dem Frame sollte uns
jetzt keine Schwierigkeiten mehr bereiten – schließlich geht es nur darum, das
Gelernte erneut anzuwenden. Der einzige neue Punkt, den wir noch ansprechen
müssen, ist das Layout und die Frage: Wie werden Komponenten im Frame
angeordnet?

13.5 Bildbetrachter 1.0: die erste komplette


Version
Wir werden nun an der ersten vollständigen Version basteln – einer Version, die
wirklich ihre Hauptaufgabe erfüllt: Bilder anzeigen.

13.5.1 Klassen zur Bildverarbeitung


Auf dem Weg zur Lösung betrachten wir noch eine weitere Zwischenversion:
Bildbetrachter0-4. Ihre Klassenstruktur ist in Abbildung 13.4 zu sehen.

Abbildung 13.4 Bildbetrachter


Die Klassenstruktur
der Bildbetrachter-
Anwendung.
Bildflaeche BilddateiManager

Farbbild

Wie Sie sehen, haben wir drei neue Klassen hinzugefügt: Farbbild, Bildflaeche und
BilddateiManager. Farbbild ist eine Klasse, die Bilder repräsentiert, die wir betrachten
und bearbeiten wollen. BilddateiManager ist eine Hilfsklasse, die uns statische
Methoden liefert, mit denen Bilddateien (im JPEG- und PNG-Format) von der Fest-
platte gelesen und im Farbbild-Format geliefert werden und mit denen ein Farbbild
auf der Festplatte gespeichert werden kann. Bildflaeche ist eine selbst geschriebene
Swing-Komponente zum Anzeigen des Bildes in der GUI.
Wir werden die wichtigsten Aspekte jeder dieser Klassen etwas ausführlicher dar-
stellen. Wir werden die Klassen jedoch nicht komplett erläutern – sie sollen dem
neugierigen Leser auch noch zur eigenen Erkundung dienen können.

490
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version

Die Klasse Farbbild ist unser selbst definiertes Format, in dem wir Bilder im Spei-
cher halten. Sie können sich ein Farbbild als ein zweidimensionales Array von
Bildpunkten vorstellen. Jeder Bildpunkt kann eine Farbe haben. Wir benutzen die
Standardklasse Color (aus dem Paket [Link]), um die Farbe jedes Bildpunktes zu
repräsentieren. (Sehen Sie sich auch die Dokumentation der Klasse Color an – wir
werden sie später noch brauchen.)
Farbbild ist implementiert als eine Subklasse der Java-Standardklasse Buffered-
Image (aus dem Paket [Link]). Die Klasse BufferedImage liefert uns einen
Großteil der Funktionalität, die wir benötigen (sie repräsentiert ein Bild ebenfalls
als ein zweidimensionales Array), aber sie bietet keine Methoden, mit denen wir
die Farbe eines Bildpunktes über ein Color-Objekt setzen oder auslesen können
(sie benutzt intern ein anderes Format, das wir nicht benutzen wollen). Deshalb
haben wir eine eigene Subklasse definiert, die diese beiden Methoden hinzu-
fügt.
Im Rahmen dieses Projektes können wir die Klasse Farbbild als eine Biblio-
theksklasse ansehen – Sie werden diese Klasse nicht bearbeiten müssen.
Die wichtigsten Methoden der Klasse Farbbild sind für uns:
 gibPunktfarbe und setzePunktfarbe zum Lesen und Verändern einzelner Bild-
punkte, sowie
 getHeight und getWidth (geerbt von BufferedImage) zum Auslesen der Höhe und
der Breite eines Bildes.
Die Klasse BilddateiManager hat drei Methoden: eine zum Lesen einer Bilddatei von
der Festplatte, die ein Farbbild liefert, eine zum Schreiben eines Farbbildes auf die
Festplatte und eine zum Öffnen eines Dateiauswahldialoges, damit der Benutzer
interaktiv eine Bilddatei zum Laden auswählen kann. Die Methoden können
Dateien in den Standardformaten JPEG und PNG lesen, die Methode zum Speichern
schreibt im JPEG-Format. Dabei werden die Standard-Java-Ein-Ausgabe-Methoden
für Bilder der Klasse ImageIO verwendet (Paket [Link]).
Die Klasse Bildflaeche realisiert eine selbst definierte Swing-Komponente zum
Anzeigen unseres Bildes. Selbst definierte Swing-Komponenten können einfach
erzeugt werden, indem eine Subklasse einer existierenden Komponente definiert
wird. Auf diese Weise können sie in einen Swing-Container eingefügt werden und
in unserer GUI wie jede andere Swing-Komponente angezeigt werden. Bildflaeche
ist eine Subklasse von JComponent. Wichtig für uns ist vor allem, dass Bildflaeche
eine Methode setzeBild anbietet, die als Parameter ein Farbbild zum Anzeigen
bekommt.

13.5.2 Einfügen des Bildes


Nachdem wir die Klassen für den Umgang mit Bildern vorbereitet haben, ist das
Einfügen des Bildes in die Benutzeroberfläche einfach. Listing 13.4 zeigt die wich-
tigsten Unterschiede zu den vorherigen Versionen.

491
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Listing 13.4
Die Klasse
Bildbetrachter mit
Bildflaeche.

Wenn wir diesen Quelltext mit den vorherigen Versionen vergleichen, stellen wir
nur zwei kleine Unterschiede fest:
 In der Methode fensterErzeugen erzeugen wir nun eine Bildflaeche statt einem
JLabel und fügen sie mit add als Komponente ein. Das ist nicht komplizierter
als mit einem Label. Das Bildflaeche-Objekt wird in einem Datenfeld gespei-
chert, damit wir später wieder darauf zugreifen können.
 Unsere Methode dateiOeffnen wurde so geändert, dass sie nun tatsächlich eine
Bilddatei öffnet und anzeigt. Mithilfe unserer Bildverarbeitungsklassen ist dies
nun auch einfach. Die Klasse BilddateiManager bietet eine Methode zum Selek-
tieren und Öffnen eines Bildes und die Klasse Bildflaeche bietet eine Methode,
um dieses Bild anzuzeigen. Beachtenswert ist, dass wir [Link]() am Ende
der Methode dateiOeffnen aufrufen müssen, da sich die Größe unserer Bild-
Komponente geändert hat. Die Methode pack wird das Layout des Fensters neu
berechnen und anzeigen, sodass die Größenänderung korrekt behandelt wird.

Übung 13.14 Öffnen und testen Sie das Projekt Bildbetrachter0-4. Der Ord-
ner für dieses Kapitel enthält auch einen Ordner namens bilder. Darin kön-
nen Sie einige Testbilder finden, die Sie benutzen können. Natürlich können
Sie auch eigene Bilder verwenden.

492
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version

Übung 13.15 Was passiert, wenn Sie ein Bild öffnen und dann das Fenster
in der Größe verändern? Was passiert, wenn Sie die Fenstergröße zuerst
ändern und dann ein Bild öffnen?

Mit dieser Version haben wir die zentrale Aufgabe gelöst: Wir können nun eine
Bilddatei aus dem Dateisystem laden und auf dem Bildschirm anzeigen. Bevor wir
unser Projekt wirklich „Version 1.0“ nennen können und es damit erstmalig für
vollständig erklären, wollen wir aber noch einige Verbesserungen vornehmen
(siehe Abbildung 13.2):
 Wir wollen zwei Labels hinzufügen: eines zum Anzeigen des Dateinamens im
oberen Bereich und eines für Statusmeldungen im unteren Bereich.
 Wir wollen ein FILTER-Menü mit einigen Filtern hinzufügen, mit denen das Aus-
sehen des Bildes verändert werden kann.
 Wir wollen ein HILFE-Menü hinzufügen, das einen Eintrag INFO… enthält. Die
Auswahl dieses Eintrags soll einen Dialog mit Informationen wie dem Namen
der Anwendung und ihrer Versionsnummer anzeigen.

13.5.3 Layout
Zuerst werden wir die beiden Text-Label in unsere Oberfläche einfügen: eines oben,
auf dem der Dateiname der aktuell angezeigten Bilddatei steht, und eines unten,
das für verschiedene Statusmeldungen benutzt wird.
Das Erzeugen dieser Labels ist einfach – sie sind beide simple Instanzen der Klasse
JLabel. Wir halten sie in Datenfeldern, damit wir später zum Ändern der angezeig-
ten Texte auf sie zugreifen können. Die einzige Frage ist, wie wir sie auf dem Bild-
schirm arrangieren wollen.
Ein erster – naiver und inkorrekter – Ansatz könnte so aussehen:
Container contentPane = [Link]();

dateinameLabel = new JLabel();


[Link](dateinameLabel);

bildflaeche = new Bildflaeche();


[Link](bildflaeche);

statusLabel = new JLabel("Version 1.0");


[Link](statusLabel);
Die Idee hinter diesem Ansatz ist einfach: Wir lassen uns vom Frame die Inhalts-
fläche geben und fügen, eine nach der anderen, alle Komponenten ein, die wir
angezeigt bekommen wollen. Das Problem dabei ist: Wir haben nicht genau
angegeben, wie die drei Komponenten arrangiert werden sollen. Wir könnten sie
nebeneinander haben wollen oder übereinander oder in einem beliebigen ande-
ren Layout. Da wir kein Layout angegeben haben, benutzt der Container (die
Inhaltsfläche) ein voreingestelltes Layout. Und das, so stellt sich heraus, ist nicht
das, was wir wollen.

493
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Swing benutzt Layout-Manager zur Gestaltung des Layouts der Komponenten in


einer GUI. Jeder Container mit enthaltenen Komponenten, die Inhaltsfläche, ver-
fügt über einen Layout-Manager, der sich um die Ausrichtung der Komponenten
im Container kümmert.

Übung 13.16 Benutzen Sie, ausgehend von Ihrer letzten Version des Projekts,
das obige Quelltextfragment zum Einfügen der beiden Label. Was beobach-
ten Sie?

Swing bietet mehrere verschiedene Layout-Manager, um verschiedene Layout-Ein-


stellungen zu unterstützen. Die wichtigsten sind: FlowLayout, BorderLayout, Grid-
Layout und BoxLayout. Jedes dieser Layouts wird durch eine Java-Klasse der Swing-
Bibliothek repräsentiert und arrangiert die Komponenten unter seiner Kontrolle auf
andere Weise.
Im Folgenden ist jedes Layout kurz beschrieben. Die Hauptunterschiede bestehen
in der Art, wie die Komponenten positioniert werden und der verfügbare Bereich
zwischen den Komponenten verteilt wird. Sie finden die hier besprochenen Bei-
spiele in dem Projekt Layouts.
Ein FlowLayout (Abbildung 13.5) arrangiert alle Komponenten sequenziell von links
nach rechts. Es lässt allen Komponenten ihre bevorzugte Größe und zentriert sie an
der vertikalen Achse. Wenn der Platz in der Horizontalen nicht groß genug für alle
Komponenten ist, werden die Komponenten auf eine weitere Zeile umgebrochen.
Bei einem FlowLayout kann die Ausrichtung der Komponenten auch links- oder
rechtsbündig gesetzt werden. Da die Komponenten nach einer Vergrößerung des
Fensters den verfügbaren Raum nicht durch Anpassung ihrer Größen voll ausnut-
zen, gibt es leeren Raum um die Komponenten.

Abbildung 13.5
FlowLayout.

Ein BorderLayout (Abbildung 13.6) platziert bis zu fünf Komponenten in einem


festgelegten Muster: eine in der Mitte und jeweils eine oben, unten, rechts und
links. Jede dieser Positionen darf auch leer sein, sodass auch weniger als fünf
Komponenten zulässig sind. Die fünf Positionen sind mit den Konstanten CENTER,
NORTH, SOUTH, EAST und WEST der Klasse BorderLayout benannt. Beim BorderLayout
wird auch nach einer Vergrößerung des Fensters der ganze Bereich von den
Komponenten (ungleichmäßig verteilt) eingenommen.

494
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version

Abbildung 13.6
BorderLayout.

Dieses Layout mag auf den ersten Blick sehr speziell erscheinen – man fragt sich,
wie häufig es verwendet wird. In der Praxis ist dies jedoch ein überraschend nütz-
liches Layout, das in vielen Anwendungen eingesetzt wird. In BlueJ wird bei-
spielsweise sowohl für das Hauptfenster als auch für den Editor ein BorderLayout
als primärer Layout-Manager verwendet.
Wenn ein BorderLayout in der Größe verändert wird, wird die mittlere Kompo-
nente in beide Dimensionen gedehnt. Die Ost- und West-Komponenten ändern
ihre Höhe, bleiben jedoch in der Breite gleich. Die Nord- und Süd-Komponenten
behalten ihre Höhe und nur ihre Breite ändert sich.
Ein GridLayout (Abbildung 13.7) ist nützlich, um Komponenten in einem gleich-
mäßigen Gitter (grid) zu arrangieren. Die Anzahl der Zeilen und Spalten kann
angegeben werden, der GridLayout-Manager ordnet alle Komponenten in dersel-
ben Größe an. Dies kann nützlich sein, um beispielsweise eine Reihe von Knöp-
fen in die gleiche Größe zu zwingen. Die Breite einer JButton-Instanz ist norma-
lerweise festgelegt durch den Text des Knopfes: Jeder Knopf wird so breit
gemacht, dass der Text Platz findet. Das Einfügen von Knöpfen in ein GridLayout
führt dazu, dass alle Knöpfe die Größe des breitesten Knopfes erhalten. Wenn
eine ungerade Anzahl an gleichgroßen Komponenten ein 2-D-Gitter nicht füllen
kann, kommt es in einigen Konfigurationen zu einem leeren Bereich.

Abbildung 13.7
GridLayout.

495
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Ein BoxLayout arrangiert mehrere Komponenten entweder vertikal oder horizontal.


Bei einer Größenänderung wird nicht umgebrochen (Abbildung 13.8). Verschachtelt
man mehrere BoxLayouts ineinander, so können anspruchsvolle zweidimensionale
Layouts erstellt werden.

Abbildung 13.8
BoxLayout.

Übung 13.17 Nutzen Sie das Projekt Layouts und experimentieren Sie mit
den hier beschriebenen Beispielen. Fügen Sie Komponenten hinzu und ent-
fernen Sie Komponenten aus den bestehenden Klassen, um ein Gefühl für die
Hauptmerkmale der verschiedenen Layout-Stile zu bekommen. Was passiert,
wenn es beispielsweise keine CENTER-Komponente beim BorderLayout gibt?

13.5.4 Geschachtelte Container


Alle oben beschriebenen Layout-Strategien sind relativ einfach. Der Schlüssel zum
Bau gut aussehender und gut benutzbarer Oberflächen liegt in einem letzten
Detail: Layouts können ineinander verschachtelt werden. Viele der Swing-Kompo-
nenten sind Container. Container erscheinen nach außen wie einzelne Komponen-
ten, können aber viele andere Komponenten enthalten. Jeder Container verfügt
über einen eigenen Layout-Manager.
Der am häufigsten verwendete Container ist die Klasse JPanel. Ein JPanel kann
als Komponente in die Inhaltsfläche eines Frames eingefügt werden und in dem
JPanel können dann mehrere Komponenten ausgerichtet werden. Abbildung
13.9 beispielsweise zeigt eine Oberflächenanordnung, die ähnlich zu der des Haupt-
fensters von BlueJ ist. Die Inhaltsfläche des Frames benutzt ein BorderLayout, bei
dem die EAST-Position unbenutzt ist. Der NORTH-Bereich des BorderLayout enthält
einen JPanel mit einem horizontalen FlowLayout, das seine Komponenten (mögli-
cherweise Knöpfe einer Werkzeugleiste) in einer Reihe arrangiert. Die SOUTH-
Komponente ist ähnlich: ein weiterer JPanel mit einem FlowLayout.

496
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version

JPanel mit GridLayout JPanel mit FlowLayout Abbildung 13.9


Erstellen einer GUI
mit geschachtelten
Containern.

JPanel mit vertikalem FlowLayout


JPanel mit FlowLayout Inhaltsfläche mit BorderLayout
(EAST-Bereich leer)

Die Gruppe von Knöpfen im WEST-Bereich wurde zuerst in einen JPanel mit einem
einspaltigen GridLayout eingefügt, um für alle Knöpfe die gleiche Breite zu errei-
chen. Dieser JPanel wurde dann in einen weiteren JPanel mit einem FlowLayout ein-
gefügt, damit das Gitter sich nicht über die volle Höhe des WEST-Bereichs erstreckt.
Dieser äußere JPanel schließlich wurde in den WEST-Bereich des Frames eingefügt.
Beachten Sie, wie der Container und der Layout-Manager beim Layout der Kompo-
nenten kooperieren. Der Container hält die Komponenten, aber der Layout-Mana-
ger entscheidet über ihre Anordnung auf dem Bildschirm. Jeder Container hat einen
Layout-Manager. Er verwendet einen voreingestellten Layout-Manager, wenn wir
nicht explizit einen setzen. Die Voreinstellung ist für verschiedene Container unter-
schiedlich: Die Inhaltsfläche eines JFrame beispielsweise hat per Voreinstellung ein
BorderLayout, ein JPanel hingegen hat ein FlowLayout voreingestellt.

Übung 13.18 Sehen Sie sich die grafische Benutzungsoberfläche des Rech-
ner-Projekts aus Kapitel 9 an (Abbildung 9.6). Welche Art von Containern-/
Layout-Managern wurde bei ihrer Erzeugung verwendet? Nachdem Sie diese
Frage schriftlich beantwortet haben, öffnen Sie das Projekt Rechner-GUI und
überprüfen Sie Ihre Antwort durch Lesen des Quelltextes.
Übung 13.19 Welche Layout-Manager könnten beim Layout des Editor-
fensters von BlueJ zum Einsatz gekommen sein?
Übung 13.20 Rufen Sie in BlueJ die Funktion KLASSE AUS BIBLIOTHEK
VERWENDEN… aus dem WERKZEUGE-Menü auf. Sehen Sie sich den Dialog auf
dem Bildschirm an. Welche Container-/Layout-Manager wurden hier ver-
mutlich eingesetzt? Verändern Sie den Dialog in seiner Größe und beobach-
ten Sie das Verhalten bei der Größenänderung, um zusätzliche Informatio-
nen zu bekommen.

497
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Nun sollten wir wieder den Quelltext unserer Bildbetrachter-Anwendung unter-


suchen. Unser Ziel ist sehr simpel. Wir wollen drei Komponenten übereinander
sehen: ganz oben ein Label, das Bild in der Mitte und unten ein weiteres Label. Dies
können mehrere Layout-Manager. Welchen wir wählen sollten, wird klarer, wenn
wir über das Verhalten bei Größenänderungen nachdenken. Wenn wir das Fens-
ter vergrößern, dann sollen die Labels ihre Höhe beibehalten und das Bild soll den
gesamten Zusatzplatz zugeteilt bekommen. Dies legt ein BorderLayout nahe: Die
Labels können im NORTH- und SOUTH-Bereich liegen und das Bild im CENTER. Listing
13.5 zeigt den Quelltext, in dem dies umgesetzt ist.

Listing 13.5
Die Verwendung von
BorderLayout zum
Arrangieren von
Komponenten.

Zwei Details sind hier noch erwähnenswert. Erstens: Die Methode setLayout wird an
der Inhaltsfläche aufgerufen, um den gewünschten Layout-Manager zu setzen.4
Der Layout-Manager selbst ist ein Objekt, also erzeugen wir eine Instanz der Klasse
BorderLayout und übergeben sie an die Methode setLayout.

Zweitens: Wenn wir eine Komponente in einen Container mit BorderLayout einfü-
gen, dann verwenden wir eine andere add-Methode, die einen zweiten Parame-
ter hat. Der Wert für den zweiten Parameter ist eine der öffentlichen Konstanten
NORTH, SOUTH, EAST, WEST oder CENTER, die in der Klasse BorderLayout definiert sind.

Übung 13.21 Implementieren und testen Sie den oben gezeigten Quelltext
in Ihrer Version des Projektes.
Übung 13.22 Experimentieren Sie mit anderen Layout-Managern. Probieren
Sie alle oben genannten Layout-Manager in Ihrem Projekt aus und testen
Sie, ob diese sich wie erwartet verhalten.

4 Um genau zu sein: Der Aufruf von setLayout ist an dieser Stelle nicht notwendig, da der Lay-
out-Manager der Inhaltsfläche bereits ein BorderLayout ist. Wir haben den Aufruf hier zur Klar-
heit und Lesbarkeit eingefügt.

498
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version

13.5.5 Bildfilter
Zwei Dinge bleiben noch zu tun, bis wir unsere erste Version des Bildbetrachters
fertig gestellt haben: das Hinzunehmen einiger Bildfilter und das Einfügen eines
HILFE-Menüs. Als Nächstes werden wir uns um die Filter kümmern.
Die Bildfilter sind der erste Schritt zur Bildmanipulation. Letztendlich wollen wir nicht
nur Bilder laden und betrachten können, sondern sie auch manipulieren und geän-
dert abspeichern können.

Wir beginnen hier mit drei einfachen Filtern. Ein Filter ist eine Funktion, die auf das
gesamte Bild angewendet wird. (Er könnte auch so geändert werden, dass er nur auf
Teile eines Bildes angewendet wird, aber das wollen wir vorläufig noch nicht tun.)

Die drei Filter nennen wir Dunkler, Heller und Schwellwert. Dunkler macht das
ganze Bild etwas dunkler, Heller hellt es etwas auf. Der Schwellwert-Filter wan-
delt das Bild in ein Graustufenbild mit nur wenigen voreingestellten Graustufen
um. Wir haben einen dreistufigen Schwellwert gewählt. Dies bedeutet, dass wir
drei Farben verwenden: Schwarz, Weiß und ein mittleres Grau. Alle Bildpunkte,
die in ihrer Helligkeit im oberen Drittel liegen, werden in weiße Punkte umge-
wandelt, alle im unteren Drittel in schwarze und die im mittleren Drittel in
graue.

Um dies zu erreichen, müssen wir zwei Dinge tun:


 Wir müssen einen Menüeintrag samt Menü-Listener für jeden Filter erzeugen
und
 wir müssen die eigentlichen Filteroperationen implementieren.
Zuerst die Menüs. Das ist nichts wirklich Neues. Es ist lediglich mehr Code in der
Art, wie wir ihn schon für die existierenden Menüs geschrieben haben. Wir müs-
sen Folgendes erledigen:
 Wir erzeugen ein neues Menü (Klasse JMenu) namens FILTER und fügen es in die
Menüzeile ein.
 Wir erzeugen drei Menüeinträge (Klasse JMenuItem) namens HELLER, DUNKLER
und SCHWELLWERT und fügen sie in unser FILTER-Menü ein.
 Zu jedem Menüeintrag fügen wir mithilfe von Lambda-Ausdrücken, wie wir
sie für die anderen Menüeinträge verwendet haben, einen Action-Listener
hinzu. Die Action-Listener sollten die Methoden dunkler, heller und schwell-
wert entsprechend aufrufen.

Nachdem wir das Menü eingefügt und die (anfangs leeren) Methoden imple-
mentiert haben, müssen wir nun die Filter implementieren.
Die einfachste Art von Filter besteht aus einer Iteration über alle Bildpunkte eines
Bildes, bei der an jedem Bildpunkt eine Änderung der Farbe vorgenommen wird.
Ein Muster für diesen Prozess zeigt Listing 13.6. Kompliziertere Filter könnten auch
die Werte benachbarter Bildpunkte mit einbeziehen, um den neuen Farbwert eines
Bildpunktes zu berechnen.

499
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Übung 13.23 Fügen Sie das neue Menü und die Menüeinträge in Ihre Ver-
sion des Projekts ein wie hier beschrieben. Um die Action-Listener eintragen
zu können, müssen Sie die drei Methoden dunkler, heller und schwellwert
als private Methoden in Ihre Bildbetrachter-Klasse einfügen. Sie alle haben
den Rückgabewert void und übernehmen keine Parameter. Diese Methoden
können anfangs leere Rümpfe haben oder sie könnten einfach ausgeben,
dass sie aufgerufen worden sind.

Listing 13.6
Muster für einen
einfachen Filter-
prozess.

Die Filterfunktion operiert auf dem Bild und sollte deshalb laut Entwurf nach Zustän-
digkeiten in der Klasse Farbbild implementiert sein. Andererseits schließt die
Behandlung eines Menüaufrufs auch GUI-bezogenen Code ein (beispielsweise
müssen wir bei einem Filteraufruf prüfen, ob überhaupt ein Bild offen ist), der
eher in die Klasse Bildbetrachter gehört.
Aufgrund dieser Überlegungen erzeugen wir zwei Methoden, eine in Bildbe-
trachter und eine in Farbbild, um die Arbeit aufzuteilen (Listing 13.7 und Listing
13.8). Wir können sehen, dass die Methode dunkler in Bildbetrachter den Teil der
Aufgabe enthält, der sich auf die GUI bezieht (Prüfen, ob ein Bild geladen ist,
Anzeigen einer Statusmeldung, Neuanzeigen des Frames), während die Methode
abdunkeln in Farbbild die eigentliche Arbeit übernimmt, alle Bildpunkte des Bildes
etwas abzudunkeln.

Listing 13.7
Die Filtermethode
in der Klasse
Bildbetrachter.

500
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version

Listing 13.8
Implementierung eines
Filters in der Klasse
Farbbild.

Übung 13.24 Was bewirkt der Methodenaufruf [Link](), den Sie


in der Methode dunkler sehen?
Übung 13.25 Wir können den Aufruf einer Methode statusAnzeigen sehen,
der offensichtlich ein interner Methodenaufruf ist. Aufgrund des Namens
können wir erraten, dass diese Methode eine Statusmeldung mithilfe des
bereits besprochenen Status-Labels ausgeben soll. Implementieren Sie diese
Methode in Ihrer Version des Projekts Bildbetrachter0-4. (Hinweis: Sehen Sie
sich die Methode setText in der Klasse JLabel an.)
Übung 13.26 Was passiert, wenn der Menüeintrag DUNKLER selektiert wird,
aber kein Bild geladen ist?
Übung 13.27 Erklären Sie detailliert, wie die Methode abdunkeln in der Klasse
Farbbild funktioniert. (Hinweis: Sie enthält einen Methodenaufruf einer Me-
thode darker. Zu welcher Klasse gehört diese zweite Methode? Schlagen Sie
es nach.)
Übung 13.28 Implementieren Sie den Filter zum Aufhellen in der Klasse
Farbbild.
Übung 13.29 Implementieren Sie den Schwellwert-Filter. Um die Helligkeit
eines Bildpunktes zu bekommen, können Sie seinen Rot-, Grün- und Blau-
wert abfragen und diese aufaddieren. Die Klasse Color definiert statische
Referenzen zu passenden Schwarz-, Weiß- und Grau-Objekten.

Sie finden eine lauffähige Implementierung aller bisher beschriebenen Aspekte


im Projekt Bildbetrachter1-0. Sie sollten jedoch die Übungen selbst durchführen,
bevor Sie sich die Lösung ansehen.

501
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

13.5.6 Dialoge
Unsere letzte Aufgabe für diese Version ist das Hinzufügen eines HILFE-Menüs mit
einem Menüeintrag INFO… . Wenn dieser Eintrag selektiert wird, dann soll ein Dia-
logfenster erscheinen, das eine kurze Information anzeigt.
Nun müssen wir die Methode zeigeInfo implementieren, sodass diese den Infor-
mationsdialog anzeigt.

Übung 13.30 Fügen Sie ein weiteres Menü HILFE hinzu. Fügen Sie in dieses
einen Menüeintrag mit der Beschriftung INFO… ein.
Übung 13.31 Fügen Sie eine leere Methode (eine Methode mit einem lee-
ren Rumpf) namens zeigeInfo ein und tragen Sie bei dem neuen Menüein-
trag eine Ereignisverarbeitung ein, die zeigeInfo aufruft.

Eine der wichtigsten Eigenschaften eines Dialogs ist, ob er modal ist oder nicht.
Ein modaler Dialog blockiert alle Interaktionen mit der Anwendung, bis der Dia-
log wieder geschlossen wird. Er zwingt den Benutzer, auf den Dialog zu reagie-
ren. Nichtmodale Dialoge hingegen erlauben Interaktionen in anderen Frames
während der Dialog sichtbar ist.
Dialoge können auf ähnliche Weise implementiert werden wie der JFrame des
Hauptfensters. Sie benutzen häufig die Klasse JDialog zum Anzeigen ihres Fensters.
Für modale Dialoge mit einer Standardstruktur gibt es allerdings einige bequeme
Methoden in der Klasse JOptionPane, die das Anzeigen solcher Dialoge stark ver-
einfachen. JOptionPane verfügt, neben anderen Dingen, über statische Methoden,
um drei Typen von Standarddialogen anzuzeigen. Diese sind:
 Meldungsdialog (message dialog): Dies ist ein Dialog, der eine Meldung
anzeigt und einen OK-Knopf zum Schließen des Dialogs hat.
 Auswahldialog (confirm dialog): Dieser Dialog zeigt üblicherweise eine Frage an
und bietet dem Benutzer einige Knöpfe zur Auswahl einer Antwort, beispiels-
weise JA, NEIN und ABBRECHEN.
 Eingabedialog (input dialog): Dieser Dialog enthält eine Eingabeaufforderung
und ein Textfeld, in das der Benutzer Text eingeben kann.
Unsere Info-Box ist ein einfacher Meldungsdialog. Wenn wir uns die Dokumenta-
tion der Klasse JOptionPane ansehen, finden wir statische Methoden mit dem
Namen showMessageDialog für diesen Zweck.

Übung 13.32 Finden Sie die Dokumentation zu showMessageDialog. Wie viele


Methoden mit diesem Namen gibt es? Worin unterscheiden sich diese? Wel-
che sollten wir für unsere Info-Box benutzen? Warum?
Übung 13.33 Implementieren Sie die Methode zeigeInfo in Ihrer Bild-
betrachter-Klasse, indem Sie den Aufruf einer showMessageDialog-Methode ver-
wenden.

502
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version

Übung 13.34 Die showInputDialog-Methoden der Klasse JOptionPane erlau-


ben es, den Benutzer bei Bedarf zu einer Eingabe aufzufordern. Die Kompo-
nente JTextField hingegen ermöglicht die ständige Anzeige eines Textein-
gabefelds in einer GUI. Finden Sie die Dokumentation dieser Klasse. Welche
Eingabe bewirkt, dass ein mit einem JTextField verknüpfter Action-Listener
benachrichtigt wird? Kann ein Benutzer am Bearbeiten des Textes im Text-
feld gehindert werden? Ist es möglich, dass ein Listener über beliebige
Änderungen am Text informiert wird? (Hinweis: In welcher Weise verwendet
ein JTextField ein Document-Objekt?)
Ein JTextField wird beispielsweise im Projekt Rechner in Kapitel 9 verwendet.

Nachdem wir uns die Dokumentation angesehen haben, können wir nun unsere
Info-Box implementieren, indem wir einen Aufruf der Methode showMessageDialog
einfügen. Der Quelltext ist in Listing 13.9 zu sehen. Beachten Sie, dass wir eine
String-Konstante VERSION eingeführt haben, die die aktuelle Versionsnummer hält.

Listing 13.9
Anzeigen eines
modalen Dialogs.

Dies war die letzte Aufgabe zur Vervollständigung von „Version 1.0“ unserer Bild-
betrachter-Anwendung. Wenn Sie alle Übungen durchgeführt haben, dann haben
Sie jetzt eine Version des Projektes, mit der Sie einiges tun können: Bilder öffnen und
anzeigen, Filter anwenden, Statusmeldungen ausgeben und einen Dialog anzeigen.
Das Projekt Bildbetrachter1-0, das Sie im Zusatzmaterial zum Buch finden, enthält
eine Implementierung der bisher behandelten Funktionalität. Sie sollten sich diese
Lösung sorgfältig ansehen und sie mit Ihrer eigenen vergleichen.
In dieser Version haben wir auch die Methode dateiOeffnen überarbeitet, um eine
bessere Benachrichtigung im Fehlerfall zu erreichen. Wenn der Benutzer eine
Datei auswählt, die kein gültiges Bildformat hat, können wir nun eine vernünf-
tige Fehlermeldung anzeigen. Nachdem wir jetzt über Meldungsdialoge Bescheid
wissen, ist das eine einfache Sache.

13.5.7 Zusammenfassung der Layout-Verwaltung


In diesem Abschnitt haben wir einige spezielle Klassen für den Umgang mit Bil-
dern geschrieben. Wichtiger noch für unsere GUI war aber, dass wir uns einge-
hender mit dem Layout der Komponenten beschäftigt haben und dabei sehen
konnten, wie Container und Layout-Manager zusammenarbeiten, um auf dem
Bildschirm exakt die Komponentenanordnung anzuzeigen, die wir wünschen.
Wie man Layout-Manager einsetzt, lernt man vor allem, indem man Erfahrungen
sammelt und hin und wieder Layouts nach dem Versuch-und-Irrtum-Prinzip ein-

503
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

fach ausprobiert. Mit der Zeit werden Ihnen die Layout-Manager aber immer ver-
trauter werden.
Damit hätten wird die Grundlagen aller wichtigen Bereiche der GUI-Programmie-
rung behandelt. Im Rest des Kapitels können wir uns daher darauf konzentrieren,
unsere Anwendung noch weiter zu verbessern und ihr den letzten Schliff zu geben.

13.6 Bildbetrachter 2.0: die


Programmstruktur verbessern
Version 1.0 unserer Anwendung hat eine benutzbare GUI und kann Bilder anzei-
gen. Sie bietet außerdem drei Filter an.
Der nächste konsequente Schritt zur Verbesserung unserer Anwendung ist das
Hinzufügen weiterer interessanter Filter. Bevor wir uns darauf stürzen, sollten wir
uns jedoch gut überlegen, was das für uns bedeutet.
Mit der aktuell realisierten Struktur für Filter müssen wir für jeden Filter drei Dinge tun:
1 einen Menüeintrag hinzufügen,

2 einen Methode zum Verarbeiten der Aktivierung des Menüeintrags hinzufü-


gen und

3 eine Implementierung des Filters in Farbbild hinzufügen.


Die Punkte 1 und 3 sind unvermeidlich – wir brauchen einen Menüeintrag und
eine Filterimplementierung. Aber Punkt 2 sieht verdächtig aus. Wenn wir uns diese
Methoden in der Klasse Bildbetrachter ansehen (Listing 13.10 zeigt zwei von
ihnen als Beispiel), dann sieht das nach reichlicher Code-Duplizierung aus. Diese
Methoden machen alle im Prinzip das Gleiche (abgesehen von einigen Details)
und – was noch schlimmer ist – wir müssen für jeden neuen Filter eine weitere die-
ser fast gleichen Methoden hinzufügen.
Wir wissen, dass Code-Duplizierung ein Zeichen für einen schlechten Entwurf ist
und vermieden werden sollte. Wir reagieren darauf, indem wir unseren Quelltext
restrukturieren. Wir suchen nach einem Entwurf, durch den wir nicht für jeden
neuen Filter eine Methode zum Weiterleiten implementieren müssen.
Listing 13.10
Zwei der Methoden
zur Filter-Behandlung
in Bildbetrachter.

504
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.6 Bildbetrachter 2.0: die Programmstruktur verbessern

Um dies zu erreichen, sollten wir vermeiden, dass alle Filter in der Klasse Bild-
betrachter „hart verdrahtet“ sind. Stattdessen verwenden wir eine Sammlung
von Filtern und schreiben eine einzelne Aktivierungsmethode, die den richtigen
Filter findet und aufruft. Dies entspricht der Einführung einer agieren-Methode
beim Entkoppeln des Simulators von den einzelnen Akteur-Typen im Projekt
Fuechse-und-Hasen (Kapitel 12).
Damit dies möglich wird, müssen zuerst die Filter selbst zu Objekten werden und nicht
nur einfache Methodennamen. Wenn wir sie in einer gemeinsamen Sammlung able-
gen und aus dieser Sammlung aufrufen wollen, dann benötigen alle Filter eine
gemeinsame Superklasse, die wir Filter nennen wollen und die die Methode anwenden
enthalten soll (Abbildung 13.10 zeigt die Struktur und Listing 13.11 den Quelltext).
Jeder Filter hat einen Namen und eine Methode anwenden, die den Filter auf ein Bild
anwendet. Beachten Sie, dass diese Klasse abstrakt ist, da die Methode anwenden
auf dieser Ebene abstrakt sein muss, aber die Methode gibName kann vollständig
implementiert werden, sodass es kein Interface ist.

Bildbetrachter Abbildung 13.10


Klassenstruktur für
Filter-Objekte.

<<abstract>>
BilddateiManager Filter

Bildflaeche

Abdunkelfilter Aufhellfilter Schwellwertfilter

Farbbild

Listing 13.11
Die abstrakte Klasse
Filter: Superklasse
für alle Filter.

505
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Nachdem wir die Superklasse geschrieben haben, lassen sich spezifische Filter
leicht als Subklassen implementieren. Wir müssen lediglich eine Implementierung
der Methode anwenden erstellen, die das Bild (das als Parameter übergeben wird)
über die Methoden gibPunktfarbe und setzePunktfarbe manipuliert. Listing 13.12
zeigt ein Beispiel.

Listing 13.12
Die Implementierung
einer spezifischen
Filterklasse.

Als Nebeneffekt kann die Klasse Farbbild nun stark vereinfacht werden, da alle
Filtermethoden aus ihr entfernt werden können. Sie definiert nun nur noch die
Methoden setzePunktfarbe und gibPunktfarbe.
Wenn alle Filter auf diese Weise definiert wurden, können wir Filter-Objekte
erzeugen und in einer Sammlung halten (Listing 13.13).

Listing 13.13
Eine Sammlung von
Filtern anlegen.

506
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.6 Bildbetrachter 2.0: die Programmstruktur verbessern

Sobald wir diese Struktur umgesetzt haben, können wir die beiden letzten not-
wendigen Änderungen vornehmen:
 Wir ändern den Code, der die Filtermenüeinträge erzeugt, so ab, dass dieser über
die Filtersammlung iteriert. Für jeden enthaltenen Filter erzeugt er einen Menüein-
trag und benutzt die Filtermethode gibName, um den Namen des zugehörigen
Labels festzulegen.
 Danach können wir eine generische Methode filterAnwenden schreiben, die
einen Filter als Parameter bekommt und diesen auf das aktuelle Bild anwendet.
Das Projekt Bildbetrachter2-0 enthält eine vollständige Implementierung dieser
Änderungen.

Übung 13.35 Öffnen Sie das Projekt Bildbetrachter2-0. Sehen Sie sich die
Implementierung der neuen Methode zum Erzeugen und Anwenden der Filter
in der Klasse Bildbetrachter genau an. Achten Sie besonders auf die Metho-
den menuezeileErzeugen und filterAnwenden. Erläutern Sie ausführlich, wie
die Menüeinträge für das Filtermenü erzeugt werden und was bei ihrer Akti-
vierung geschieht. Zeichnen Sie ein Objektdiagramm.
Übung 13.36 Was muss getan werden, um einen neuen Filter dem Bildbe-
trachter hinzuzufügen?
Übung 13.37 Zusatzaufgabe. Vielleicht ist Ihnen aufgefallen, dass die anwen-
den-Methoden von allen Filter-Subklassen ganz ähnlich strukturiert sind:
Iteriere über das ganze Bild und ändere den Wert von jedem Bildpunkt
unabhängig von den umgebenden Bildpunkten. Es sollte möglich sein, diese
Duplizierung genau so zu isolieren wie bei der Erzeugung der Klasse Filter.
Erzeugen Sie eine Methode in der Klasse Filter, die über das Bild iteriert und
eine filterspezifische Transformation auf jeden einzelnen Bildpunkt anwendet.
Ersetzen Sie die Rümpfe der Methode anwenden in den drei Filter-Subklassen
durch einen Aufruf dieser Methode, wobei Sie das Bild und etwas, das die
entsprechende Transformation ausführen kann, als Parameter übergeben.

507
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

In diesem Abschnitt haben wir ausschließlich Quelltext restrukturiert. Wir haben


die Funktionalität der Anwendung gar nicht verändert, sondern haben ausschließ-
lich die Struktur der Implementierung verbessert, um zukünftige Änderungen zu
erleichtern.
Nachdem wir dieses Refactoring abgeschlossen haben, sollten wir testen, ob noch
alles wie gewünscht funktioniert. In allen Entwicklungsprojekten benötigen wir
Phasen wie diese. Wir treffen nicht immer von Anfang an perfekte Entwurfsent-
scheidungen, Anwendungen wachsen und die Anforderungen ändern sich.
Obwohl unser Hauptaugenmerk in diesem Kapitel der GUI-Konstruktion gilt, muss-
ten wir einen Schritt zurücktreten und unseren Quelltext restrukturieren, bevor wir
weiterarbeiten. Dies wird sich auf lange Sicht auszahlen, weil spätere Änderungen
leichter fallen werden.
Manchmal ist die Versuchung groß, Strukturen unverändert zu lassen, auch wenn
wir erkennen, dass sie nicht gut sind. Sich mit ein wenig Code-Duplizierung zu
arrangieren mag kurzfristig einfacher sein als ein sorgfältiges Refactoring. Man
kommt eine Weile damit durch, aber in lang laufenden Projekten führt es zwangs-
läufig irgendwann zu Problemen. Als generelle Regel gilt deshalb: Nehmen Sie sich
die Zeit, Ihren Code sauber zu halten.
Nachdem wir mit dem Aufräumen fertig sind, können wir nun weitere Filter hin-
zufügen.

Übung 13.38 Fügen Sie dem Projekt einen Graustufenfilter hinzu. Der Filter
wandelt das Bild in ein Schwarzweißbild mit Graustufen um. Sie können
einem Bildpunkt eine beliebige Graustufe zuweisen, indem Sie allen Farb-
komponenten (Rot, Grün, Blau) denselben Wert geben. Die Helligkeit jedes
Bildpunktes sollte unverändert bleiben.
Übung 13.39 Fügen Sie einen Spiegelfilter hinzu, der das Bild an der vertika-
len Achse spiegelt. Der Bildpunkt in der linken oberen Ecke wandert dabei in
die rechte obere Ecke und umgekehrt, sodass ein Effekt wie beim Betrachten
des Bilds in einem Spiegel entsteht.
Übung 13.40 Fügen Sie einen Negativfilter hinzu, der jede Farbe invertiert.
„Invertieren“ bei einer Bildpunkt bedeutet, seinen Farbwert x mit dem Wert
225 – x zu ersetzen.
Übung 13.41 Fügen Sie einen Weichzeichnerfilter hinzu. Ein Weichzeich-
nerfilter ersetzt den Farbwert jedes Bildpunkts mit dem Durchschnitt seiner
benachbarten Bildpunkte und ihm selbst (insgesamt also neun Bildpunkte).
Sie müssen an den Kanten des Bilds vorsichtig sein, weil dort einige Nach-
barpunkte nicht existieren. Sie müssen auch auf einer Zwischenkopie des
Bilds arbeiten, weil das Ergebnis sonst nicht korrekt ist. (Warum?) Sie kön-
nen sich leicht eine Kopie des Bilds erzeugen, indem Sie dem Konstruktor
von Farbbild das Original als Parameter übergeben.

508
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.7 Bildbetrachter 3.0: weitere GUI-Komponenten

Übung 13.42 Implementieren Sie einen Solarisationsfilter. Solarisation tritt


auf, wenn ein Foto-Negativ mit einem bereits entwickelten Negativ belichtet
wird. Wir können dies simulieren, indem wir die Farbkomponente jedes
Bildpunkts mit einem Wert w niedriger als 128 mit 255 – w ersetzen. Die
helleren Komponenten (mit einem Wert von 128 oder höher) lassen wir
unverändert. (Dies ist ein sehr einfacher Algorithmus zur Solarisation – Sie
können sehr viel bessere in der Literatur finden.)
Übung 13.43 Implementieren Sie einen Kantenerkennungsfilter. Dazu ana-
lysieren Sie die neun Bildpunkte in dem 3×3-Quadrat um einen Bildpunkt
(ähnlich wie beim Weichzeichnerfilter) und setzen den Wert des mittleren
Bildpunkts auf die Differenz zwischen dem höchsten und dem niedrigsten
Wert in der Gruppe. Tun Sie dies für jede Farbkomponente (Rot, Grün, Blau).
Dies sieht insbesondere dann gut aus, wenn Sie das Bild auch invertieren.
Übung 13.44 Experimentieren Sie mit Ihren Filtern auf verschiedenen Bil-
dern. Wenden Sie auch mehrere Filter nacheinander an.

Sobald Sie einige eigene Filter implementiert haben, sollten Sie die Versionsnum-
mer Ihres Projekts auf „Version 2.1“ erhöhen.

13.7 Bildbetrachter 3.0: weitere


GUI-Komponenten
Bevor wir das Bildbetrachter-Projekt abschließen, wollen wir noch einige Verbes-
serungen vornehmen und dabei einen Blick auf zwei weitere GUI-Komponenten
werfen: Knöpfe und Rahmen.

13.7.1 Knöpfe
Wir wollen den Bildbetrachter nun um die Möglichkeit erweitern, die Größe eines
Bildes zu ändern. Dazu bieten wir zwei Funktionen an: größer, die die Größe des
Bilds verdoppelt, und kleiner, die die Größe halbiert. (Um genau zu sein: Wir ver-
doppeln oder halbieren die Seitenlängen, nicht die Fläche.)
Eine Möglichkeit wäre, diese Funktionalität über Filter zu implementieren. Aber
wir entscheiden uns dagegen. Bisher haben Filter nicht die Größe eines Bilds ver-
ändert, und das wollen wir beibehalten. Stattdessen führen wir eine Werk-
zeugleiste am linken Rand unseres Fensters ein, die zwei Knöpfe enthält, die mit
GRÖßER und KLEINER beschriftet sind (Abbildung 13.11). Dies gibt uns auch die
Möglichkeit, noch ein wenig mit Knöpfen, Containern und Layout-Managern zu
experimentieren.

509
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Abbildung 13.11
Bildbetrachter mit
Knöpfen in einer
Werkzeugleiste.

Bisher benutzt unser Fenster ein BorderLayout, bei dem der WEST-Bereich leer ist.
Wir können diesen Bereich benutzen, um unsere Werkzeugzeugleiste einzufügen.
Es gibt nur ein kleines Problem. Der WEST-Bereich bei einem BorderLayout kann nur
eine Komponente halten, wir haben aber zwei Knöpfe.
Die Lösung ist einfach. Wir fügen einen JPanel in den WEST-Bereich des Frame (wir
wissen, ein JPanel ist ein Container) ein und platzieren die beiden Knöpfe (vom
Typ JButton) im JPanel. Listing 13.14 zeigt den Quelltext dazu.

Listing 13.14
Ein JPanel als Werk-
zeugleiste mit zwei
Knöpfen.

Übung 13.45 Fügen Sie zwei Knöpfe, beschriftet mit GRÖßER und KLEINER, in
die neueste Version Ihres Projekts ein, ähnlich wie in Listing 13.14 beschrie-
ben. Testen Sie. Was beobachten Sie?

Wenn wir dies ausprobieren, erkennen wir, dass es teilweise funktioniert, aber nicht
ganz wie erwartet. Das liegt daran, dass JPanel per Voreinstellung ein FlowLayout
verwendet, das seine Komponenten horizontal arrangiert. Wir möchten sie jedoch
vertikal anordnen.
Wir können dies erreichen, indem wir einen anderen Layout-Manager verwenden.
Ein GridLayout leistet das, was wir wollen. Bei der Erzeugung eines GridLayout legen
Konstruktorparameter fest, wie viele Reihen und Spalten wir haben wollen. Ein Wert
von null hat dabei eine spezielle Bedeutung, er steht für „so viele wie notwendig“.
Somit können wir ein einspaltiges GridLayout erzeugen, indem wir 0 als die Anzahl
der Reihen angeben und 1 als die Anzahl der Spalten. Wir können dieses GridLay-

510
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.7 Bildbetrachter 3.0: weitere GUI-Komponenten

out dann für unser JPanel verwenden, indem wir die Methode setLayout unmittel-
bar nach seiner Erzeugung aufrufen.
JPanel werkzeugleiste = new JPanel();
[Link](new GridLayout(0,1));
Alternativ kann der Layout-Manager auch bei der Erzeugung des Containers als
Konstruktorparameter angegeben werden:
JPanel werkzeugleiste = new JPanel(new GridLayout(0,1));

Übung 13.46 Ändern Sie Ihre Implementierung so ab, dass die Werk-
zeugleiste wie oben beschrieben ein GridLayout verwendet. Testen Sie. Was
beobachten Sie?

Wenn wir dies ausprobieren, sehen wir schon eine Verbesserung, aber zufrieden
sind wir immer noch nicht. Unsere Knöpfe sind viel größer als gewünscht. Dies liegt
daran, dass ein Container in einem BorderLayout (der JPanel als Werkzeugleiste in
diesem Fall) immer seinen Bereich voll ausfüllt (den WEST-Bereich in unserem Frame).
Und ein GridLayout vergrößert seine Komponenten immer so weit, dass sie den
kompletten Container ausfüllen.
Ein FlowLayout tut dies nicht – es ist ganz zufrieden, wenn es etwas Platz um seine
Komponenten lassen kann. Unsere Lösung sollte deshalb beide verwenden: Ein
GridLayout arrangiert die Knöpfe in einer Spalte und darum herum erlaubt ein
FlowLayout leeren Platz. Wir enden also mit einem GridLayout innerhalb eines
FlowLayout innerhalb eines BorderLayout. Listing 13.15 zeigt diese Lösung. Konstruk-
tionen dieser Art sind sehr verbreitet. Sehr häufig werden Sie verschiedene Con-
tainer ineinanderschachteln, um das richtige Erscheinungsbild zu bekommen.

Listing 13.15
Ein Container
mit GridLayout,
geschachtelt in
einen Container
mit FlowLayout.

Die Knöpfe sehen unserer Zielvorstellung jetzt schon sehr ähnlich. Bevor wir ihnen
den letzten Schliff geben, sollten wir uns zuerst darauf konzentrieren, dass die
Knöpfe auch funktionieren.
Wir brauchen zwei Methoden, beispielsweise bildVerkleinern und bildVergroessern,
die die eigentliche Arbeit übernehmen, und müssen zwei Action-Listener bei den
Knöpfen anmelden, die diese Methoden aufrufen.

511
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Übung 13.47 Fügen Sie Ihrem Projekt zwei Methoden namens bildVerkleinern
und bildVergroessern hinzu. Fügen Sie anfangs nur eine println-Anweisung
in die Methodenrümpfe ein, um ihren Aufruf überprüfen zu können. Die
Methoden können privat deklariert werden.
Übung 13.48 Fügen Sie den Knöpfen Lambda-Ausdrücke zur Ereignisverar-
beitung hinzu, die die beiden neuen Methoden aufrufen. Das Einfügen von
Ereignisverarbeitung bei Knöpfen ist identisch zum Einfügen bei Menüein-
trägen. Sie können einfach das Programmiermuster von dort kopieren. Tes-
ten Sie. Stellen Sie sicher, dass die Methoden bildVerkleinern und bildVer-
groessern aufgerufen werden, wenn die Knöpfe geklickt werden.
Übung 13.49 Implementieren Sie die Methoden bildVerkleinern und bild-
Vergroessern. Dazu müssen Sie ein neues Farbbild mit veränderter Größe
erzeugen, die Bildpunkte aus dem aktuellen Bild in das neue kopieren (und
dabei entsprechend skalieren) und dann das neue Bild als aktuelles Bild set-
zen. Am Ende Ihrer Methoden sollten Sie die Methode pack am JFrame auf-
rufen, damit die Komponenten an die geänderte Größe angepasst werden.
Übung 13.50 Alle Swing-Komponenten haben eine Methode setEnabled
(boolean), mit der eine Komponente aktiviert und deaktiviert werden kann.
Deaktivierte Komponenten erscheinen normalerweise in einem hellen Grau
und reagieren nicht auf Eingaben. Ändern Sie Ihren Bildbetrachter so ab, dass
die beiden Knöpfe zu Anfang deaktiviert sind. Wenn ein Bild geladen ist, sol-
len sie aktiviert sein, wenn es geschlossen ist, sollen sie wieder deaktiviert
sein.

13.7.2 Rahmen
Zuletzt wollen wir unsere Benutzungsschnittstelle noch ein wenig mit internen Rah-
men (border) aufpolieren. Rahmen können verwendet werden, um Komponenten
zu gruppieren oder um lediglich für etwas Abstand zwischen ihnen zu sorgen. Jede
Swing-Komponente kann einen Rahmen haben.
Einige Layout-Manager akzeptieren auch Konstruktorparameter zum Festlegen
von Abständen, der Layout-Manager sorgt dann für den angeforderten Abstand
zwischen seinen Komponenten.
Die am häufigsten verwendeten Rahmen sind BevelBorder, CompoundBorder, Empty-
Border, EtchedBorder und TitledBorder. Mit diesen sollten Sie sich vertraut machen.

Wir werden drei Dinge tun, um das Aussehen unserer GUI zu verbessern:
 etwas Abstand zu den Außenseiten eines Frame definieren,
 Abstand zwischen den Komponenten innerhalb eines Frame definieren und
 eine Linie um das Bild ziehen.
Der zugehörige Quelltext ist in Listing 13.16 zu sehen. Der Aufruf von setBorder
an der Inhaltsfläche mit einem EmptyBorder als Parameter sorgt für etwas Abstand
zu den Außenseiten des JFrame. Beachten Sie, dass wir nun an contentPane den

512
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.8 Innere Klassen

Cast-Operator mit JPanel anwenden, da der Supertyp Container keine Methode


setBorder anbietet.5

Listing 13.16
Abstände definieren.

Das Erzeugen eines BorderLayout mit zwei int-Parametern sorgt dafür, dass im Lay-
out Abstände zwischen den Komponenten entstehen. Und schließlich führt das Set-
zen eines EtchedBorder an der bildflaeche zu einer Linie um das Bild, die dieses
etwas eingelassen aussehen lässt. (Rahmen sind definiert im Paket javax. [Link]-
der – wir müssen entsprechend eine import-Anweisung für dieses Paket einfügen.)
Alle besprochenen Verbesserungen sind in der nächsten Version dieser Anwen-
dung im Zusatzmaterial enthalten: Bildbetrachter3-0. In dieser Version haben wir
auch eine Funktion SPEICHERN UNTER… in das Dateimenü aufgenommen, damit
Bilder auch wieder auf der Platte gespeichert werden können.
Wir haben außerdem einen weiteren Filter, den Fischaugenfilter, eingefügt, um
Ihnen einen noch besseren Eindruck von den Möglichkeiten der Bildbearbeitung
zu geben. Probieren Sie ihn aus. Er wirkt besonders gut bei Porträts.

13.8 Innere Klassen


Bis zu diesem Zeitpunkt haben wir Listeners zur Ereignisverarbeitung mithilfe von
Lambda-Ausdrücken implementiert. Das war möglich, weil die Listener-Interfaces
funktionale Interfaces waren – d.h., sie besaßen nur eine einzige abstrakte
Methode. Dies wird jedoch nicht bei allen Listener-Interfaces der Fall sein. Zum Bei-
spiel bestehen die Interfaces KeyListener, MouseListener und MouseMotionListener
aus dem Paket [Link] aus mehr als einer abstrakten Methode. Deshalb
können wir für deren Implementierungen keine Lambda-Ausdrücke verwenden.

13.8.1 Benannte innere Klassen


Der offensichtliche Ansatz für solche Klassen besteht darin, eine separate Klasse
zu definieren, die das benötigte Interface implementiert, und dann eine Instanz
zu erzeugen, die als Listener-Objekt fungiert. Es gibt jedoch einige Nachteile bei
diesem Ansatz, wenn wir einfach das Interface als eine normale Klasse innerhalb
eines Projekts auf der gleichen Ebene wie alle anderen Klassen implementieren:
 Es gibt in der Regel eine sehr enge Kopplung zwischen einem Listener und
dem Objekt, das die vielen Komponenenten der gesamten GUI verwaltet. Der
Listener wird häufig auf private Elemente auf den Zustand des GUI-Objekts

5 Die Anwendung des Cast-Operators funktioniert hier nur, weil der dynamische Typ der Content-
Pane bereits JPanel ist. Es ist nicht so, dass erst der Cast das ContentPane-Objekt in ein JPanel
verwandeln würde!

513
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

zugreifen und diese verändern müssen. Dieser Kopplungsgrad ist nicht zu ver-
meiden, doch es wäre angenehmer, wenn wir einen Weg finden könnten, ihn
unter den vielen Klassen eines Projekts klarer auszumachen.
 Wir haben gesehen, dass ein Lambda-Ausdruck vollständigen Zugang sogar
zu den privaten Elementen der umschließenden GUI-Klasse hat – auf diese Art
entsteht die enge Kopplung. Für eine externe Klasse wäre dies ohne zusätzli-
che sondierende und verändernde Methoden in der GUI-Klasse nicht möglich,
die vermutlich ausschließlich zugunsten des Listeners zur Verfügung gestellt
werden. Daüber hinaus könnte das Vorhandensein der Methoden eine unbe-
absichtigte Kopplung zu anderen Klassen mit sich bringen.
 Wir erzeugen häufig nur eine einzige Instanz einer Listener-Klasse und eine voll-
ständige externe Klasse erscheint für diese Art von Nutzung oft übertrieben.
Glücklicherweise stellt Java ein Konstrukt zur Verfügung, das all diese Probleme
umgeht. Dieses Konstrukt haben wir bisher noch nicht behandelt: innere Klassen.
Innere Klassen sind Klassen, die innerhalb einer anderen Klasse deklariert werden:
class UmschliessendeKlasse
{
...
class InnereKlasse
{
...
}
}
Instanzen der inneren Klasse sind gebunden an Instanzen der umschließenden Klasse
– sie können nur zusammen mit einer umschließenden Instanz existieren. Konzeptu-
ell existieren sie innerhalb der umschließenden Instanz. Aus diesem Konstrukt sieht
man direkt, dass die enge Kopplung zwischen innerer Listener-Klasse und der
umschließenden GUI-Klasse eindeutig ist. Eine weitere Eigenschaft ist, dass Anwei-
sungen in Methoden der inneren Klasse die privaten Datenfelder und Methoden der
umschließenden Klasse sehen und auf diese zugreifen können – so wie es ein
Lambda-Ausdruck kann. Die innere Klasse wird als ein Bestandteil der umschließen-
den Klasse angesehen, so wie die Methoden der umschließenden Klasse auch. Damit
wird erfolgreich das zweite Problem angegangen, das wir oben beschrieben haben.
Wir können dieses Konstrukt nun benutzen, um ein MouseListener zu implemen-
tieren, beispielsweise innerhalb der Klasse Bildbetrachter. Die Struktur sieht fol-
gendermaßen aus:
public class Bildbetrachter
{
...
private class MouseHandler implements MouseListener
{
public void MouseClicked(MouseEvent event)
{
// die Klick-Aktion ausführen
}
public void mouseEntered (MouseEvent event)
{

514
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.8 Innere Klassen

// die Eintreten-Aktion ausführen


}
... weitere MouseListener-Methoden hier ausgelassen ...
}
}
(Per Konvention schreiben wir innere Klassen an das Ende der umschließenden
Klasse – nach den Methoden.) Beachten Sie, dass wir die inneren Klassen mit pri-
vater Sichtbarkeit ausgestattet haben, um zu bekräftigen, dass sie eine Aufgabe
durchführen, die sehr spezifisch für die Bildbetrachter-Klasse ist und nicht als
unabhängig von dieser angesehen werden sollte.
Sobald wir eine innere Klasse definiert haben, können wir Instanzen von dieser
auf die gleiche Art erzeugen, wie wir es mit Klassen bisher auch getan haben:
// Entdeckt Mausklicks auf dem Bild, wenn der Nutzer
// dieses bearbeiten möchte.
[Link](new MouseHandler());
Beachten Sie einige typische Nutzungseigenschaften dieser inneren Listener-Klassen:
 Wir machen uns nicht die Mühe, die Instanzen in Variablen zu speichern – sie
sind also eigentlich anonyme Objekte. Nur die Komponente, mit der sie verbun-
den sind, besitzt eine Referenz auf deren spezifisches Listener-Objekt, sodass sie
die Listener-Methoden zur Ereignisverarbeitung aufrufen können.
 Wir erzeugen häufig nur ein einzelnes Objekt von jeder der inneren Klassen,
da jede spezialisiert ist auf eine bestimmte Komponente innerhalb einer spezi-
fischen GUI.
Diese Merkmale sind der Grund, dass wir im nächsten Abschnitt eine weitere Eigen-
schaft von Java untersuchen.
Innere Klassen können generell benutzt werden, um die Kohäsion in größeren Pro-
jekten zu verbessern. Das Projekt Fuechse-und-Hasen aus Kapitel 12 beispielsweise
enthält eine Klasse Simulationsansicht, die eine innere Klasse Feldansicht hat. Sie
können sich dieses Beispiel näher ansehen, um Ihr Verständnis zu vertiefen.

13.8.2 Anonyme innere Klassen


Die Lösung zur Implementierung von Interfaces mit mehreren Methoden auf Basis
von inneren Klassen ist bereits ziemlich gut, aber wir wollen noch einen Schritt wei-
tergehen: Wir möchten anonyme innere Klassen verwenden. Um dieses Konzept
zu illustrieren, werden wir eine weitere Version des Bildbetrachters entwickeln:
eine, die uns ein einfaches Bearbeiten der einzelnen Bildschirmpunkte erlaubt. Die
Idee ist, dass der Benutzer irgendwo auf das Bild klicken kann, um die Farbe eines
Bildschirmpunkts zu ändern. Das heißt, es müssen zwei Dinge gemacht werden:
 die Farbe auswählen, die im Bild platziert werden soll
 den Bildschirmpunkt auswählen, der ersetzt werden soll
Die Farbe wird über einen JColorChooser-Dialog ausgewählt, der erscheint, wenn
der Benutzer eine Maustaste gleichzeitig mit der (ª)-Taste drückt. Der zu erset-
zende Bildschirmpunkt wird ausgewählt, indem die Maus ohne (ª)-Taste geklickt

515
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

wird. Sobald eine Farbe ausgewählt wurde, werden die Bildschirmpunkte durch
diese Farbe ersetzt, bis eine neue Farbe gewählt wird. Die Implementierung dazu
finden Sie als Bildbetrachter4-0.
Natürlich ist ein Mausklick das einzige Ereignis, an dem wir interessiert sind, aber
die Implementierung von MouseListener verlangt, dass wir fünf separate Metho-
den implementieren. Dies ist ärgerlich, da vier der fünf Methodenrümpfe leer
sein werden. Wir werden oft ähnlichen Problemen begegnen, wenn wir andere
Listener-Interfaces mit mehreren Methoden implementieren. Zum Glück haben
die Entwickler der Java-API dieses Problem erkannt und sogenannte no-op-
Implementierungen6 dieser Interfaces in Form von abstrakten Adapter-Klassen
zur Verfügung gestellt, zum Beispiel MouseAdapter und MouseMotionAdapter. Dies
bedeutet, dass wir häufig eine Subklasse einer Adapter-Klasse erzeugen können,
anstatt das gesamte Interface zu implementieren, und nur die ein oder zwei
Methoden überschreiben, die wir für eine bestimmte Aufgabe benötigen. In
unserem Beispiel werden wir also eine innere Klasse anlegen, die eine Subklasse
von MouseAdapter ist, und nur die mousePressed-Methode überschreiben.
Jetzt sind wir in der Lage, anonyme innere Klassen zu schreiben. Der relevante
Code sieht folgendermaßen aus:
Bildflaeche = new Bildflaeche();
[Link](new MouseAdapter() {
public void mousePressed(MouseEvent e)
{
... aktiv werden, wenn die Maus gedrückt wird ...
}
});
Dieser Code sieht ausgesprochen mysteriös aus, wenn man ihn das erste Mal sieht,
und es fällt Ihnen möglicherweise schwer, ihn zu interpretieren, selbst wenn Sie
bisher alles in diesem Buch verstanden haben. Aber keine Angst – wir werden ihn
ganz langsam auseinandernehmen.

Konzept Die Idee für dieses Konstrukt rührt von den Überlegungen her, dass wir jede innere
Klasse nur genau einmal benutzen, um eine einzelne, unbenannte Instanz zu
Anonyme
erzeugen. Für genau diese Situation bieten anonyme innere Klassen eine syntakti-
innere Klassen
sind ein nützliches sche Abkürzung: Sie erlauben uns, eine Klasse und auch gleich eine Instanz dieser
Konstrukt zur Imple- Klasse in einem einzigen Schritt zu erzeugen. Der Effekt ist der gleiche wie bei der
mentierung von Version mit inneren Klassen, allerdings mit dem Unterschied, dass wir keine extra
Event-Listenern, die benannten Klassen definieren müssen und dass die Definition der Listener-Metho-
keine funktionalen den noch dichter an der Registrierung des Listeners bei der GUI-Komponente steht.
Interfaces sind.
Die farbliche Hervorhebung von zusammenhängenden Bereichen im BlueJ-Editor
gibt uns einige Hinweise, die uns helfen können, diese Struktur besser zu verste-
hen (Abbildung 13.12) Die grüne Schattierung markiert eine Klasse, gelblich unter-
legt sind die Methodendefinitionen und der weiße Hintergrund identifiziert einen
Methodenrumpf. Wir stellen fest, dass der Rumpf der Methode fensterErzeugen
eine sehr komprimierte (seltsam aussehende) Klassendefinition enthält, die eine
Methodendefinition mit einem kurzen Rumpf aufweist.

6 Von englisch no operation (Nulloperation).

516
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.8 Innere Klassen

Abbildung 13.12
Farbliche Hervor-
hebung eines Bereichs
mit einer anonymen
inneren Klasse.

Wenn wir eine anonyme innere Klasse benutzen, dann erzeugen wir eine innere
Klasse, ohne sie zu benennen, und erzeugen sofort ein einziges Exemplar dieser
Klasse. Im obigen Quelltext geschieht dies durch das Code-Fragment
new MouseAdapter() {
public void mousePressed(MouseEvent e) ... }
}
Eine anonyme innere Klasse wird erzeugt, indem ein Supertyp benannt wird (häufig
eine abstrakte Klasse oder ein Interface, in diesem Fall MouseAdapter), gefolgt von
einem Block mit Implementierungen der abstrakten Methoden des Supertyps oder
Methoden, die wir überschreiben möchten. Dies sieht etwas ungewöhnlich aus,
auch wenn es einige Gemeinsamkeiten zur Syntax der Lambda-Ausdrücke gibt.
In diesem Beispiel erzeugen wir eine neue Subklasse von MouseAdapter, die die
Methode mousePressed überschreibt. Diese neue Subklasse bekommt keinen eige-
nen Namen. Stattdessen stellen wir das Schlüsselwort new voran und erzeugen so
eine einzelne Instanz dieser Klasse. Diese einzelne Instanz ist ein MouseListener-
Objekt (indirekt ein Subtyp von MouseListener) und kann so an die Methode
addMouseListener einer GUI-Komponente übergeben werden. Jeder Subtyp eines
so erzeugten MouseListener- oder MouseAdapter-Objekts repräsentiert eine eindeu-
tige anonyme Klasse.
Genauso wie Lambda-Ausdrücke und benannte innere Klassen können anonyme
innere Klassen auf die Datenfelder und Methoden der umschließenden Klasse
zugreifen. Zusätzlich können sie, weil sie innerhalb einer Methode definiert wurden,
auf die lokalen Variablen und die Parameter dieser Methode zugreifen.
Einige Beobachtungen zu anonymen inneren Klassen, die auch für Lambda-Aus-
drücke gelten, müssen hervorgehoben werden. Diese Struktur ist angenehm kohä-
rent und erweiterbar. Wenn wir einen zusätzlichen Menüeintrag benötigen, fügen
wir einfach Code zum Erzeugen des Eintrags mitsamt Listener hinzu sowie eine
Methode zur Behandlung der zugehörigen Funktion.

517
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Es gilt aber, dass anonyme innere Klassen den Quelltext schwerer lesbar machen.
Es wird dringend empfohlen, sie nur für sehr kurze Klassen und für etablierte Pro-
grammiermuster zu verwenden. Beispielsweise ist es fraglich, ob eine vollständige
Implementierung des MouseListener-Interface als anonyme innere Klasse – ange-
sichts der benötigten Codemenge – angemessen wäre. Eine benannte innere
Klasse wäre wahrscheinlich in Bezug auf Kohäsion vorzuziehen. Für uns gilt, dass
wir das Konstrukt in diesem Buch nur für die Ereignisverarbeitung benutzen.7

Übung 13.51 Sehen Sie sich die Implementierung des Bildpunkte-Editors in


Bildbetrachter4-0 noch einmal an. Überlegen Sie, ob die aktuellen Maus-
Operationen, mit denen eine Farbe oder ein Bildpunkt ausgewählt wird, für
einen Benutzer am geeignetsten sind. Ändern Sie sie, wenn Sie das Gefühl
haben, dass sie es nicht sind.
Übung 13.52 Fügen Sie dem Projekt ein Menü hinzu, mit dem man im Bild-
punkte-Editor eine „Pinselgröße“ einstellen kann. Verwenden Sie die Pinsel-
größe, um mehrere benachbarte Bildschirmpunkte mit jedem Mausklick zu
bearbeiten.
Übung 13.53 Überschreiben Sie weitere Methoden der Klasse MouseAdapter,
um Freihandzeichnen auf der Bildfläche zu erlauben. Überlegen Sie gründ-
lich, ob Sie mit dem Listener als anonyme innere Klasse weiterarbeiten wol-
len oder ob Sie daraus eine benannte innere Klasse machen möchten.

13.9 Zusätzliche Erweiterungen


Das Programmieren von GUIs mit Swing ist ein riesiges Themengebiet. Swing bie-
tet viele verschiedene Komponenten und viele verschiedene Container und Lay-
out-Manager, jeweils mit zahlreichen Datenfeldern und Methoden.
Man braucht deshalb Zeit, um sich mit der gesamten Swing-Bibliothek vertraut
zu werden, sicher mehr als ein paar Wochen. Normalerweise lesen wir uns wei-
tere Details an, die wir noch nicht kannten, während wir an GUIs arbeiten, und
werden auf diese Weise langsam zu Experten.
Das in diesem Kapitel beschriebene Beispiel ist, obwohl es eine Menge Details
enthält, nur eine kurze Einführung in die GUI-Programmierung. Wir haben die
meisten wichtigen Konzepte beschrieben, aber es gibt noch eine ganze Menge
Funktionalität zu entdecken, die weit über den Rahmen dieses Buches hinaus-
geht. Es gibt verschiedene Quellen für weitergehende Informationen. Sie werden
noch häufig die API-Dokumentation der Swing-Klassen nachschlagen müssen.
Ohne diese geht es gar nicht. Es gibt außerdem viele Swing-/GUI-Tutorials,
sowohl gedruckt als auch im Web.

7 Wenn Sie mehr über innere Klassen herausfinden wollen, dann sehen Sie sich die folgenden
Abschnitte des Java-Tutorials an, das online verfügbar ist: [Link]
tutorial/java/javaOO/[Link] und http:// [Link]/javase/tutorial/java/javaOO/
[Link].

518
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.9 Zusätzliche Erweiterungen

Ein guter Startpunkt ist, wie bei vielem, das öffentlich verfügbare Java-Tutorial
auf der Oracle-Website. Es enthält einen Abschnitt mit dem Titel Creating a GUI
with JFC/Swing ([Link]
In diesem Abschnitt gibt es einige spannende Unterabschnitte. Einer der nützlichsten
ist vermutlich der Abschnitt Using Swing Components und darin der Unterabschnitt
How to … . Er enthält Einträge wie How To Use Buttons, Check Boxes, and Radio
Boxes; How to Use Labels; How to Make Dialogs; How to Use Panels und so weiter.
In ähnlicher Weise enthält der Abschnitt Laying Out Components Within a Con-
tainer einen Unterabschnitt How to …, der alles über die verfügbaren Layout-Mana-
ger darstellt.

Übung 13.54 Finden Sie im Online-Java-Tutorial den Abschnitt Creating a GUI


with JFC/Swing (die Abschnitte werden auf den Webseiten trails genannt).
Erstellen Sie sich ein Lesezeichen in Ihrem Browser für diese Seite.
Übung 13.55 Was haben CardLayout und GroupLayout zu bieten, das sich von
den bisher in diesem Kapitel diskutierten Layout-Managern unterscheidet?
Übung 13.56 Was ist ein Slider? Finden Sie eine Beschreibung und fassen
Sie diese zusammen. Erstellen Sie ein kurzes Beispiel in Java, das einen Slider
erzeugt und benutzt.
Übung 13.57 Was ist ein Tabbed Pane? Finden Sie eine Beschreibung und
fassen Sie diese zusammen. Geben Sie Beispiele, bei denen ein Tabbed Pane
nützlich sein kann.
Übung 13.58 Was ist ein Spinner? Finden Sie eine Beschreibung und fassen
Sie diese zusammen.
Übung 13.59 Finden Sie die Demo-Anwendung ProgressBarDemo. Führen
Sie sie auf Ihrem Computer aus. Beschreiben Sie, was Sie sehen.

Dies ist der Punkt, an dem wir die Diskussion des Bildbetrachter-Beispiels beenden. Es
kann aber durch interessierte Leser in viele Richtungen erweitert werden. Mithilfe der
Informationen aus dem Online-Tutorial können Sie zahlreiche Schnittstellen-Kompo-
nenten hinzufügen.
Die folgenden Übungen geben einige Anregungen; viele weitere sind denkbar.

Übung 13.60 Implementieren Sie eine Funktion Rückgängig in Ihrem Bild-


betrachter. Diese Funktion nimmt die letzte Operation zurück.
Übung 13.61 Deaktivieren Sie alle Menüeinträge, die nicht benutzt werden
können, wenn kein Bild geladen ist.
Übung 13.62 Implementieren Sie eine Funktion Erneut laden, die alle Ände-
rungen am aktuellen Bild verwirft und es erneut von der Platte lädt.

519
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Übung 13.63 Die Klasse JMenu ist tatsächlich eine Subklasse von JMenuItem.
Dies bedeutet, dass geschachtelte Menüs erzeugt werden können, indem ein
JMenu in ein anderes eingetragen wird. Fügen Sie ein Menü AUSRICHTEN in die
Menüzeile ein. Fügen Sie in dieses ein Menü ROTIEREN ein, mit dem das Bild
entweder um 90 oder 180 Grad gedreht werden kann, mit oder gegen den
Uhrzeigersinn. Implementieren Sie diese Funktionalität. Das Menü AUSRICHTEN
könnte auch Menüeinträge enthalten, die beispielsweise die bestehende
Funktionalität zum Vergrößern und Verkleinern aufrufen.
Übung 13.64 Die Anwendung passt die Größe des Fensters immer wieder
an, damit sichergestellt ist, dass das komplette Bild immer sichtbar ist. Ein
sehr großes Fenster ist aber nicht immer wünschenswert. Lesen Sie die
Dokumentation der Klasse JScrollPane. Statt die Bildflaeche direkt in die
Inhaltsfläche einzufügen, fügen Sie sie in einen JScrollPane ein und dann
den JScrollPane in die Inhaltsfläche. Laden Sie ein großes Bild und experi-
mentieren Sie mit Größenänderungen am Fenster. Welcher Unterschied
ergibt sich aus der Verwendung eines JScrollPane? Ermöglicht sie Ihnen die
Anzeige von Bildern, die sonst zu groß für den Bildschirm wären?
Übung 13.65 Ändern Sie Ihre Anwendung so ab, dass sie mehrere Bilder
gleichzeitig laden kann (aber immer nur ein Bild angezeigt wird). Fügen Sie
dann ein Pop-up-Menü hinzu (Klasse JComboBox), mit dem das anzuzeigende
Bild selektiert wird.
Übung 13.66 Verwenden Sie als Alternative zur JComboBox wie in Übung 13.65
ein Registersteuerelement (Klasse JTabbedPane), um mehrere geöffnete Bilder
zu verwalten.
Übung 13.67 Implementieren Sie eine Diashow-Funktion, bei der Sie ein
Verzeichnis auswählen können und dann jedes Bild in diesem Verzeichnis
für eine bestimmte Dauer angezeigt wird (beispielsweise fünf Sekunden).
Übung 13.68 Wenn Sie die Diashow implementiert haben, fügen Sie einen
Schieberegler (Klasse JSlider) hinzu, mit dem Sie ein Bild der Show mit dem
Regler auswählen können. Während die Diashow läuft, sollte der Schiebe-
regler sich bewegen, um den Fortschritt anzuzeigen.

13.10 Ein weiteres Beispiel: der Musikplayer


In diesem Kapitel haben wir bisher ein Beispiel einer GUI-Anwendung ausführlich
diskutiert. Wir wollen nun eine zweite Anwendung vorstellen, um aus einem wei-
teren Beispiel lernen zu können. Diese Anwendung führt uns einige weitere GUI-
Komponenten vor.
Das zweite Beispiel ist ein Programm zum Abspielen von Musikdateien, also ein
Musikplayer. Wir werden es hier nicht in voller Ausführlichkeit diskutieren. Es dient
in erster Linie als Studienobjekt für Sie und als Quelle für Code-Fragmente, die Sie
kopieren und verändern können. In diesem Kapitel werden wir nur auf einige
Aspekte dieser Anwendung eingehen, die uns erwähnenswert erscheinen.

520
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.10 Ein weiteres Beispiel: der Musikplayer

Übung 13.69 Öffnen Sie das Projekt Musikplayer. Erzeugen Sie eine Instanz
der Klasse MusikPlayerGUI und experimentieren Sie mit der Anwendung.

Das Projekt Musikplayer bietet eine GUI für Klassen, die auf den Musiksammlung-
Projekten aus Kapitel 4 basieren. Wie dort findet und spielt das Programm mp3-
Dateien im Ordner audio unterhalb des Projektordners. Wenn Sie selbst über
eigene Musikstücke im richtigen Format verfügen, sollten Sie sie abspielen können,
wenn Sie sie im Ordner audio ablegen.
Der Musikplayer wird durch drei Klassen implementiert: MusicPlayerGUI, Music-
Player und MusicFilePlayer. Wir wollen uns hier nur die erste ansehen. Die Klasse
MusicFilePlayer kann im Prinzip als eine Bibliotheksklasse angesehen werde; In-
stanzen werden zusammen mit dem Namen der abzuspielenden mp3-Datei
erzeugt. Machen Sie sich mit ihrer Schnittstelle vertraut, aber Sie brauchen ihre
Implementierung nicht zu verstehen oder zu bearbeiten. (Natürlich können Sie sich
bei Interesse diese Klasse genauer ansehen, aber sie benutzt einige Konzepte, die
wir in diesem Buch nicht diskutieren werden.)
Es folgen einige erwähnenswerte Beobachtungen zu diesem Projekt.

Trennung von Modell und Ansicht


Diese Anwendung nimmt eine bessere Trennung zwischen Modell und Ansicht vor
als das vorige Beispiel. Dies bedeutet, dass die Funktionalität der Anwendung (das
Modell) sauber getrennt ist von ihrer Benutzungsschnittstelle (die GUI). Beide Teile,
die Funktionalität und die Ansicht, können jeweils aus mehreren Klassen bestehen,
aber jede Klasse sollte eindeutig einer der beiden Teile zugeordnet werden können,
um eine klare Trennung zu erreichen. In unserem Beispiel besteht die Ansicht nur
aus einer einzigen GUI-Klasse.
Die Trennung der Funktionalität einer Anwendung von ihrer Benutzungsschnitt-
stelle bewirkt eine gute Kohäsion: Sie macht das Programm verständlicher, einfa-
cher zu warten und einfacher an veränderte Anforderungen anpassbar (insbeson-
dere an andere Benutzungsschnittstellen). So ist es beispielsweise vergleichsweise
leicht, eine textbasierte Schnittstelle für den Musikplayer zu entwickeln, die letzt-
lich MusicPlayerGUI ersetzt, aber MusicPlayer unverändert lässt.

Erben von JFrame


In diesem Beispiel demonstrieren wir eine andere häufig angewendete Vorgehens-
weise für das Konstruieren von Frames, die wir bereits am Anfang des Kapitels
angesprochen haben. Unsere GUI-Klasse erzeugt kein Objekt der Klasse JFrame; sie
erbt von der Klasse JFrame. Diese bewirkt, dass alle Methoden von JFrame, die wir
aufrufen müssen (wie etwa getContentPane, setJMenuBar, pack, setVisible usw.),
nun als interne (geerbte) Methoden aufrufbar sind.
Es gibt keinen besonderen Grund, die eine Variante (Benutzen einer Instanz von
JFrame) gegenüber der anderen (Erben von JFrame) zu bevorzugen. Die Entschei-
dung kann nach persönlichen Vorlieben getroffen werden. In der Praxis sind
beide Techniken weitverbreitet.

521
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Unbewegte Bilder anzeigen


Sehr häufig wollen wir ein Bild in unserer GUI anzeigen. Die einfachste Art, dies
zu tun, ist über einen JLabel in der Oberfläche, der eine Grafik anzeigt (ein JLabel
kann entweder Text oder Grafik oder beides anzeigen). Der Musikplayer enthält
ein Beispiel dafür. Die betreffende Quelltextzeile ist
JLabel bild = new JLabel(new ImageIcon("[Link]"));
Diese Anweisung lädt eine Bilddatei namens [Link] aus dem Projektverzeichnis,
erzeugt ein Icon mit dem Bild und erzeugt dann einen JLabel, der das Icon anzeigt.
(Der Begriff Icon scheint hier darauf hinzuweisen, dass wir nur mit kleinen Bildern
umgehen, aber tatsächlich können die Bilder beliebige Größe haben.) Dies funktio-
niert für Bilder im JPEG-, GIF- und PNG-Format.

Combo-Boxen
Der Musikplayer enthält ein Beispiel für die Verwendung einer JComboBox. Eine
Combo-Box ist eine Menge von Werten, von denen jeweils immer nur einer aus-
gewählt ist. Der gewählte Wert wird angezeigt und die Selektion kann über ein
Popup-Menü vorgenommen werden. Im Musikplayer wird die Combo-Box ver-
wendet, um eine bestimmte Reihenfolge auswählen zu können, z.B. nach Inter-
pret, Titel usw.
Eine JComboBox kann auch editierbar sein. Die Werte sind dann nicht alle vordefi-
niert, sondern können durch den Benutzer eingegeben werden. In unserem Fall
ist dies jedoch nicht so.

Listen
Das Programm enthält auch ein Beispiel für eine Liste (Klasse JList), für die Liste
der Musikstücke. Eine Liste kann eine beliebige Anzahl von Werten halten und
einer oder mehrere können selektiert sein. Die Werte in der Liste sind in diesem
Fall Strings, aber andere Typen sind ebenso möglich. Eine Liste hat nicht automa-
tisch einen Rollbalken.

Rollbalken
Ein weiterer Aspekt, der in diesem Beispiel demonstriert wird, ist die Verwendung
von Rollbalken.
Rollbalken können über einen speziellen Container erzeugt werden – eine Instanz
der Klasse JScrollPane. Beliebige GUI-Komponenten können in einen JScrollPane
eingefügt werden, der für den Fall, dass die anzuzeigende Komponente zu groß
für den zur Verfügung stehenden Platz ist, die notwendigen Rollbalken anzeigt.
In unserem Beispiel haben wir unsere Liste von Musikdateien in einen JScrollPane
eingefügt. Der JScrollPane selbst wiederum wird in ihren Eltern-Container einge-
fügt. Die Rollbalken werden nur im Bedarfsfall angezeigt. Sie können dies aus-
probieren, indem Sie entweder so viele Musikdateien hinzufügen, bis diese nicht
mehr zusammen angezeigt werden können, oder indem Sie das Fenster so stark
verkleinern, dass es kleiner ist als die aktuelle Liste.

522
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.10 Ein weiteres Beispiel: der Musikplayer

Andere Elemente, die in diesem Beispiel demonstriert werden, sind ein Schiebe-
regler (der nicht viel macht) und die Verwendung von Farben (aus der Liste) zur
Veränderung des Aussehens einer Anwendung. Jedes GUI-Element bietet diverse
Methoden an, mit deren Hilfe das Aussehen oder das Verhalten der Komponente
geändert werden kann. Sie sollten sich die Dokumentation von Komponenten,
die Sie interessieren, genauer ansehen und mit den Eigenschaften dieser Kompo-
nenten experimentieren.

Übung 13.70 Ändern Sie den Musikplayer so ab, dass er ein anderes Bild in
der Mitte anzeigt. Suchen Sie sich ein Bild aus dem WWW oder erstellen Sie
ein eigenes.
Übung 13.71 Ändern Sie die Farben der anderen Komponenten (Vorder-
und Hintergrundfarbe) so ab, dass sie zu Ihrem Bild passen.
Übung 13.72 Fügen Sie eine neue Komponente hinzu, die nähere Angaben
zu dem aktuell abgespielten Musikstück anzeigt.
Übung 13.73 Fügen Sie dem Musikplayer eine reload-Eigenschaft hinzu,
die erneut den Ordner audio einliest. Dann können Sie eine neue Musik-
datei in den Ordner einfügen und laden, ohne die Anwendung neu starten
zu müssen.
Übung 13.74 Fügen Sie eine ÖFFNEN-Funktion in das DATEI-Menü ein. Wenn
der Eintrag selektiert wird, öffnet sich ein Dateiauswahldialog, mit dem der
Benutzer eine Musikdatei zum Laden auswählen kann. Wenn der Benutzer
ein Verzeichnis auswählt, dann soll der Player alle Musikdateien aus diesem
Verzeichnis laden (wie er es jetzt schon mit dem audio-Ordner tut).
Übung 13.75 Verändern Sie den Schieberegler so, dass Start und Ende (und
möglicherweise weitere Zwischenstufen) mit Zahlen beschriftet sind. Der
Start sollte Null sein, das Ende sollte die Länge des Musikstücks in Sekunden
sein. Die Klasse MusicPlayer verfügt über eine Methode gibLaenge. Beachten
Sie, dass der Schieberegler noch nicht funktioniert.
Übung 13.76 Verändern Sie den Musikplayer so, dass ein Doppelklick auf
ein Element in der Liste der Musikstücke das Stück abspielt.
Übung 13.77 Verbessern Sie das Aussehen der Knöpfe. Alle Knöpfe, die
zeitweilig keine Funktion haben, sollten während dieser Phasen ausgegraut
sein. Sie sollten nur aktivierbar sein, wenn ihre Verwendung sinnvoll ist.
Übung 13.78 Die Anzeige der Musikstücke besteht zurzeit einfach aus einer
JList von String-Objekten. Prüfen Sie, ob es irgendwelche Swing-Kompo-
nenten gibt, die besser geeignet sind. Gibt es beispielsweise eine Möglichkeit,
eine Kopfzeile anzubieten und Interpret, Titel und andere Informationen der
Musikstücke auszurichten? Implementieren Sie dies in Ihrer Version.
Übung 13.79 Zusatzaufgabe. Fügen Sie Quelltext ein, sodass sich der Schiebe-
regler parallel zum Abspielen des Musikstücks bewegt.

523
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen

Zusammenfassung
In diesem Kapitel haben wir eine Einführung in die GUI-Programmierung mit
AWT und Swing gegeben. Wir haben die drei wichtigsten Konzepte erläutert:
die Erzeugung von GUI-Komponenten, Layout und Ereignisverarbeitung.
Wir haben gesehen, dass der Aufbau einer GUI normalerweise mit der Erzeu-
gung eines Fensters auf oberster Ebene beginnt, wie beispielsweise einem
JFrame. Der Frame wird dann mit verschiedenen Komponenten befüllt, die dem
Benutzer Informationen und Funktionalität anbieten. Unter anderem haben wir
Menüs, Menüeinträge, Knöpfe, Labels und Rahmen kennengelernt.
Komponenten werden auf dem Bildschirm mithilfe von Containern und Lay-
out-Managern arrangiert. Container halten Sammlungen von Komponen-
ten und jeder Container hat einen Layout-Manager, der die Anordnung der
Komponenten innerhalb des Bildschirmbereichs des Containers bestimmt.
Das Verschachteln von Containern durch Kombinieren von verschiedenen
Layout-Managern ist ein probates Mittel, um die gewünschte Kombination
von Komponentengröße und Positionierung zu erreichen.
Interaktive Komponenten (solche, die auf Benutzereingaben reagieren kön-
nen) erzeugen Ereignisse, wenn sie von Benutzern aktiviert werden. Andere
Objekte können sich über das Auftreten solcher Ereignisse informieren lassen,
indem sie Standard-Interfaces implementieren. Sie werden dadurch zu Liste-
ner-Objekten. Wenn ein Listener-Objekt benachrichtigt wird, kann es die
angemessenen Maßnahmen ergreifen, um mit dem Ereignis umzugehen.
Event-Listener werden häufig mithilfe von Lambda-Ausdrücken implemen-
tiert.
Wir haben anonyme innere Klassen als eine alternative modulare, erweiter-
bare Technik für das Schreiben von Ereignis-Listenern vorgestellt. Diese Klas-
sen sind besonders nützlich, wenn ein Listener-Interface kein funktionales
Interface ist (es hat mehr als eine abstrakte Methode) und nicht mithilfe von
Lambda-Ausdrücken implementiert werden kann.
Und schließlich haben wir einen Hinweis auf das Online-Tutorial gegeben, mit
dessen Hilfe Details nachgeschlagen werden können, die in diesem Kapitel
nicht behandelt wurden.

NEUE BEGRIFFE IN DIESEM KAPITEL


GUI, AWT, Swing, Komponente, Layout, Ereignis (event), Ereignisverar-
beitung, Event-Listener, Frame, Menüzeile, Menü, Menüeintrag, Inhalts-
fläche (content pane), modaler Dialog, anonyme innere Klasse

524
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.10 Ein weiteres Beispiel: der Musikplayer

Zusammenfassung der Konzepte


 Komponenten Eine GUI wird erzeugt, indem Komponenten auf dem Bild-
schirm arrangiert werden. Komponenten werden durch Objekte repräsen-
tiert.
 Layout Das Layout der Komponenten, also ihre Anordnung auf dem Bild-
schirm, wird durch Layout-Manager kontrolliert.
 Ereignisverarbeitung Der Begriff Ereignisverarbeitung bezeichnet die Auf-
gabe, auf Benutzereingaben wie Mausklicks oder Tastendrücke zu reagieren.
 Bildformate Digitale Bilder können in unterschiedlichen Formaten gespei-
chert werden. Die Unterschiede wirken sich primär auf die Größe und den
Informationsgehalt aus.
 Menüzeile, Inhaltsfläche Komponenten werden in einem Fenster plat-
ziert, indem sie entweder in die Menüzeile (menu bar) oder die Inhaltsflä-
che (content pane) eingefügt werden.
 Event-Listener Ein Objekt kann auf Ereignisse „lauschen“, indem es ein
Event-Listener-Interface implementiert.
 Anonyme innere Klassen Anonyme innere Klassen sind ein nützliches
Konstrukt zur Implementierung von Event-Listenern, die keine funktiona-
len Interfaces sind.

Übung 13.80 Versehen Sie das Projekt Zuul aus Kapitel 8 mit einer GUI. Für
jeden Raum sollte es ein passendes Bild geben, das angezeigt wird, wenn
ein Spieler den Raum betritt. Es sollte einen nicht editierbaren Textbereich
geben, in dem Text ausgegeben werden kann. Für die Eingabe von Kom-
mandos gibt es verschiedene Möglichkeiten: Sie können sie textbasiert las-
sen und ein Textfeld (Klasse JTextField) zur Eingabe vorsehen, oder Sie kön-
nen Kommandos durch Knöpfe auslösen lassen.
Übung 13.81 Fügen Sie dem Zuul-Projekt Töne hinzu. Sie können individu-
elle Geräusche mit Räumen, Gegenständen oder Figuren verbinden.
Übung 13.82 Entwerfen und bauen Sie eine GUI für einen Texteditor.
Benutzern sollte ermöglicht werden, Text einzugeben, zu ändern, sich im
Text zu bewegen etc. Überlegen Sie sich Funktionen zum Formatieren (Zei-
chensatz, Zeichengröße und -varianten wie kursiv und fett) und eine Funk-
tion zum Zählen der Zeichen und Wörter. Die Funktion zum Laden und Spei-
chern von Texten brauchen Sie noch nicht zu implementieren – damit
können Sie warten, bis Sie das nächste Kapitel gelesen haben.

525
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

14 Fehlerbehandlung

Lernziele
Zentrale Konzepte in diesem Kapitel: defensive Programmierung, Fehler
melden, Exceptions auslösen und behandeln, einfache Dateiverarbeitung
Java-Konstrukte in diesem Kapitel: TreeMap, TreeSet, SortedMap, assert,
Exception, throw, throws, try, catch, File, FileReader, FileWriter, Path, Scanner,
Stream

In Kapitel 9 haben wir erwähnt, dass logische Programmfehler schwieriger zu finden


sind als syntaktische Fehler, weil ein Compiler bei logischen Fehlern nicht helfen kann.
Logische Fehler entstehen aus unterschiedlichen Gründen, die sich oft überschneiden:
 Die Lösung für ein Problem wurde nicht korrekt implementiert. Beispielsweise
könnte bei einer Statistikaufgabe für eine Menge an Werten statt des Medians (des
„mittleren“ Werts) fälschlicherweise der Durchschnittswert berechnet werden.
 Ein Objekt könnte um eine Dienstleistung gebeten werden, die es nicht erbrin-
gen kann. Beispielsweise könnte die get-Methode eines Sammlungsobjekts mit
einem Index aufgerufen werden, der außerhalb des gültigen Bereichs liegt.
 Ein Objekt könnte auf eine Weise benutzt werden, die der Entwerfer der Klasse
so nicht vorhergesehen hat und die das Objekt in einen inkonsistenten oder
unangemessenen Zustand versetzt. Das kann leicht passieren, wenn eine Klasse
in einem anderen als ihrem ursprünglich geplanten Zusammenhang wiederver-
wendet wird, beispielsweise durch Vererbung.
Obwohl wir mit den in Kapitel 9 diskutierten Teststrategien viele logische Fehler fin-
den und beheben können, bevor ein Programm zum Einsatz kommt, zeigt die Erfah-
rung doch auch, dass es weiterhin zu Programmfehlern kommen wird. Selbst das
bestgetestete Programm kann scheitern, wenn Umstände zum Tragen kommen, die
außerhalb des Einflussbereichs des Entwicklers liegen. Nehmen Sie den Fall eines
Webbrowsers, der eine nicht existierende Seite anzeigen soll, oder eines Programms,
das Daten in ein Dateisystem schreiben soll, dessen Speicher ausgeschöpft ist. Solche
Probleme sind nicht die Folge logischer Programmierfehler, können aber ein Pro-
gramm leicht scheitern lassen, wenn dieser Problemfall nicht vorhergesehen wurde.
In diesem Kapitel werden wir betrachten, wie Fehlersituationen vorausgesehen wer-
den können und wie im laufenden Programm auf solche Fehlersituationen reagiert
werden kann. Außerdem werden wir aufzeigen, wie auftretende Fehler gemeldet

527
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

werden können. Wir geben auch eine kurze Einführung in den Umgang mit Text-
dateien, da bei der Benutzung von Dateien sehr leicht Fehler auftreten können.

14.1 Das Adressbuch-Projekt


Wir werden eine Reihe von Adressbuch-Projekten benutzen, um die Prinzipien der
Meldung von Fehlern und der Fehlerbehandlung zu demonstrieren, wie sie in vielen
Anwendungen zum Einsatz kommen. Die Projekte modellieren eine Anwendung zur
Verwaltung persönlicher Kontakte – Name, Adresse und Telefonnummer – für eine
beliebige Anzahl an Personen. Die Kontakte werden im Adressbuch sowohl nach den
Namen als auch nach den Telefonnummern indiziert. Die zentralen Klassen, die wir dis-
kutieren werden, sind Adressbuch (Listing 14.1) und Kontakt. Zusätzlich ist eine Klasse
AdressbuchDemo vorgegeben, die ein Adressbuch mit einigen Testdaten initialisiert.

Listing 14.1
Die Klasse
Adressbuch.

528
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.1 Das Adressbuch-Projekt

529
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Neue Kontakte können mit der Methode neuerKontakt in das Adressbuch eingetra-
gen werden. Dabei wird angenommen, dass der Kontakt wirklich neu ist und nicht
einen bereits bestehenden Eintrag ändern soll. Für eine Änderung steht die
Methode aendereKontakt zur Verfügung, die einen alten Eintrag entfernt und durch
einen neuen ersetzt. Das Adressbuch bietet zwei Möglichkeiten zum Abfragen von
Kontakten: Die Methode gibKontakt nimmt einen Namen oder eine Telefonnum-
mer als Suchschlüssel und liefert den passenden Kontakt dazu; die Methode suche
liefert ein Array aller Kontakte, deren Name oder Telefonnummer mit einem gege-
benen Präfix beginnt. Beispielsweise liefert der Suchbegriff "04" alle Einträge, deren
Vorwahl mit diesen Ziffern beginnt.
Es sind zwei einführende Versionen des Adressbuch-Projekts zum Erkunden vor-
gegeben. Beide ermöglichen den Zugriff auf die Version von Adressbuch, die in
Listing 14.1 angegeben ist. Das Projekt Adressbuch-V1T bietet eine textbasierte
Schnittstelle, ähnlich zur Schnittstelle des Zuul-Projekts aus Kapitel 8. In dieser
Version stehen Befehle zum Auflisten des Adressbuchinhalts, zum Durchsuchen
und zum Eintragen eines neuen Kontakts zur Verfügung. Von der Schnittstelle
vermutlich interessanter ist die Version Adressbuch-V1G, die eine einfache grafi-
sche Benutzungsschnittstelle bietet. Experimentieren Sie mit beiden Versionen,
um die Möglichkeiten der Anwendung zu erkunden.

Übung 14.1 Öffnen Sie das Projekt Adressbuch-V1G in BlueJ und erzeugen Sie
ein Objekt der Klasse AdressbuchDemo. Rufen Sie seine Methode zeigeSchnitt-
stelle auf und interagieren Sie über die grafische Schnittstelle mit dem Test-
Adressbuch.
Übung 14.2 Wiederholen Sie Ihre Experimente mit der textbasierten Schnitt-
stelle des Projekts Adressbuch-V1T.
Übung 14.3 Untersuchen Sie die Implementierung der Klasse Adressbuch und
beurteilen Sie, ob sie gut entworfen ist oder nicht. Sehen Sie Kritikpunkte an
der vorliegenden Implementierung?

530
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.2 Defensive Programmierung

Übung 14.4 Die Klasse Adressbuch benutzt etliche Klassen aus dem Paket
[Link]; wenn Sie mit diesen noch nicht vertraut sind, dann sehen Sie sich
die API-Dokumentation an, um Ihre Lücken zu schließen. Halten Sie die Ver-
wendung so vieler unterschiedlicher Klassen für gerechtfertigt? Hätte eine
HashMap anstelle der TreeMap verwendet werden können?

Wenn Sie sich nicht sicher sind, versuchen Sie, TreeMap in ein HashMap zu ändern,
und schauen Sie, ob HashMap Ihnen ebenfalls alle benötigte Funktionalität bietet.
Übung 14.5 Verändern Sie die Klassen Befehlswoerter und AdressbuchText-
eingabe im Projekt Adressbuch-V1T so, dass auch ein interaktiver Zugriff auf die
Methoden gibKontakt und entferneKontakt der Klasse Adressbuch möglich ist.
Übung 14.6 Die Klasse Adressbuch definiert ein Datenfeld, das die Anzahl
der Einträge im Adressbuch hält. Hielten Sie es für angemessener, wenn die-
ser Wert aus der Anzahl der eindeutigen Einträge in der TreeMap berechnet
würde? Können Sie sich beispielsweise Umstände vorstellen, unter denen
die folgende Berechnung nicht den gleichen Wert liefern würde?
return [Link]() / 2;
Übung 14.7 Wie leicht ist es Ihrer Meinung nach, ein String-Datenfeld für
eine E-Mail-Adresse zu der Klasse Kontakt hinzuzufügen und es dann als drit-
ten Schlüssel in Adressbuch zu verwenden?

14.2 Defensive Programmierung

14.2.1 Interaktion zwischen Klient und Dienstleister


Ein Adressbuch ist ein typisches Dienstleistungsobjekt, das von sich aus keine Aktio-
nen durchführt; all seine Aktivitäten werden durch Anfragen von Klienten angesto-
ßen. Die Programmierer einer Dienstleisterklasse können mindestens zwei grundle-
gende Sichtweisen einnehmen:
 Sie können annehmen, dass alle Klienten wissen, was sie tun, und nur sinnvolle
und wohlstrukturierte Anfragen stellen.
 Sie können von einer möglicherweise fehlerhaften Umgebung ausgehen, in der
alles unternommen werden sollte, damit Klienten den Dienstleister nicht auf fal-
sche Weise benutzen können.
Das sind offensichtlich zwei extreme Positionen. In der Praxis liegt die Wahrheit
üblicherweise irgendwo in der Mitte: Die meisten Anfragen von Klienten sind sinn-
voll, aber es gibt vereinzelte fehlerhafte Anfragen – entweder aufgrund logischer
Programmierfehler oder wegen falscher Annahmen des Entwicklers, der den Klien-
ten programmiert hat. Eine dritte Möglichkeit ist natürlich ein absichtlich feindlich
agierender Klient, der versucht, dem Server zu schaden und seine Schwächen aus-
zunutzen.

531
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Ausgehend von diesen beiden unterschiedlichen Sichtweisen lassen sich Fragen


wie die folgenden diskutieren:
 Wie weit sollte ein Dienstleister die Anfragen von Klienten überprüfen?
 Wie sollte ein Dienstleister Fehler an seine Klienten melden?
 Wie kann ein Klient mögliche Scheiterungsgründe für eine Anfrage vorhersehen?
 Wie sollte ein Klient mit dem Scheitern einer Anfrage umgehen?
Wenn wir die Klasse Adressbuch mit diesen Fragen im Hinterkopf betrachten, dann
erkennen wir, dass sie vollständig im Vertrauen auf eine sinnvolle Benutzung durch
Klienten implementiert wurde. Übung 14.8 zeigt eine Benutzungssituation, in der
dieses Vertrauen zu einem Fehler führt.

Übung 14.8 Erzeugen Sie im Projekt Adressbuch-V1G ein neues Adressbuch-


Objekt auf der Objektleiste. Dieses enthält dann noch keine Einträge. Rufen
Sie nun die Methode entferneKontakt mit einem beliebigen Schlüsselwert
auf. Was passiert? Können Sie erklären, warum das passiert?
Übung 14.9 Die einfachste Antwort eines Programmierers auf eine Fehler-
situation ist der Abbruch des Programms (also ein „Absturz“). Können Sie sich
Situationen vorstellen, in denen ein schlichter Programmabbruch gefährlich sein
kann?
Übung 14.10 Viele kommerziell vertriebene Programme enthalten Fehler,
die nicht korrekt behandelt werden und die Software deshalb zum Abstür-
zen bringen. Ist das unvermeidlich? Ist es akzeptabel? Diskutieren Sie.

Das Problem in der Methode entferneKontakt liegt darin, dass dort angenommen
wird, dass der gegebene Schlüssel ein gültiger Schlüssel innerhalb des Adress-
buches ist. In der Methode wird der gegebene Schlüssel benutzt, um den zuge-
hörigen Kontakt auszulesen:
Kontakt kontakt = [Link](schluessel);
Wenn für den Schlüssel aber kein zugeordnetes Objekt existiert, dann enthält die
Variable kontakt nun den Wert null. Das ist an sich noch kein Fehler; der Fehler
entsteht durch folgende Anweisung, bei der wir davon ausgehen, dass kontakt
sich auf ein gültiges Objekt bezieht.
[Link]([Link]());
Es ist ein Fehler, eine Methode an einer Variablen aufzurufen, die null enthält, und
das Ergebnis ist immer ein Laufzeitfehler. BlueJ meldet dies als eine NullPointer-
Exception und hebt im Quelltext die Zeile hervor, in der der Fehler aufgetreten ist. Wir
werden später in diesem Kapitel Exceptions noch ausführlich besprechen. An dieser
Stelle können wir einfach anmerken, dass ein solcher Fehler in einer laufenden
Anwendung zu einem Programmabbruch führt (d.h., diese unkontrolliert beendet),
bevor das Programm seine Aufgabe erfüllt hat.

532
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.2 Defensive Programmierung

Hier besteht also ein Problem; aber wessen Problem ist es? Ist es ein Fehler des
Klienten-Objekts, weil es die Methode mit einem fehlerhaften Parameter aufge-
rufen hat, oder ist es ein Fehler des Dienstleisters, der mit dieser Situation nicht
angemessen umgegangen ist? Der Programmierer der Klienten-Klasse könnte
sagen, dass die Dokumentation der Methode nichts darüber aussagt, dass der
Schlüssel gültig sein muss. Andererseits könnte der Programmierer des Dienstleis-
ters sagen, dass es offensichtlich falsch ist, einen Kontakt mit einem ungültigen
Schlüssel entfernen zu wollen. Unsere Absicht in diesem Kapitel ist nicht, einen
solchen Streit aufzulösen, sondern ihn gar nicht erst auftreten zu lassen. Wir
beginnen mit der Fehlerbehandlung aus Sicht des Dienstleisters.

Übung 14.11 Speichern Sie eine Kopie eines der Adressbuch-V1-Projekte


unter einem anderen Namen, um auf dieser Kopie weiterarbeiten zu können.
Verändern Sie die Methode entferneKontakt so, dass es bei einem Aufruf mit
einem ungültigen Schlüssel nicht mehr zu einer NullPointerException kommt.
Wenn der Schlüssel ungültig ist, sollte die Methode einfach nichts tun.
Übung 14.12 Ist es notwendig, einen Aufruf von entferneKontakt mit einem
falschen Schlüssel zu melden? Wenn ja: Wie würden Sie dies melden?
Übung 14.13 Gibt es andere Methoden in der Klasse Adressbuch, die gegen-
über ähnlichen Fehlern anfällig sind? Wenn ja, dann korrigieren Sie auch
diese in Ihrer Version des Projekts. Ist es in allen Fällen angemessen, dass die
Methode bei einem falschen Parameter einfach nichts tut? Sollten die Fehler
auf irgendeine Weise gemeldet werden? Wenn ja: Wie würden Sie dies tun
und würde die Meldung für alle Fehler gleich aussehen?

14.2.2 Parameter prüfen


Ein Dienstleister ist am anfälligsten, wenn seine Konstruktoren oder Methoden
Parameterwerte übergeben bekommen. Die an einen Konstruktor übergebenen
Werte initialisieren üblicherweise den Objektzustand; die an eine Methode über-
gebenen Parameter beeinflussen die Auswirkungen des Methodenaufrufs und
möglicherweise seinen Ergebniswert. Deshalb sollte ein Dienstleister sehr genau
wissen, ob er den übergebenen Parametern blind vertraut oder ob er sie selbst
auf ihre Gültigkeit überprüft. In der momentanen Version wird sowohl in der
Klasse Kontakt als auch in der Klasse Adressbuch keinerlei Überprüfung der Para-
meter durchgeführt. Wie wir am Beispiel der Methode entferneKontakt gesehen
haben, kann dies zu einem schweren Laufzeitfehler führen.
Es ist relativ einfach, eine NullPointerException in entferneKontakt zu verhindern;
Listing 14.2 zeigt, wie das getan werden kann. Beachten Sie, dass wir neben der
Implementierung auch den Kommentar der Methode geändert haben, damit Kli-
enten wissen, dass unbekannte Schlüssel ignoriert werden.

533
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Listing 14.2
Ignorieren unbe-
kannter Schlüssel in
entferneKontakt.

Wenn wir die anderen Methoden der Klasse Adressbuch untersuchen, dann fallen
uns weitere Stellen für ähnliche Verbesserungen auf:
 Die Methode neuerKontakt sollte prüfen, dass ihr Parameter nicht den Wert
null hat.
 Die Methode aendereKontakt sollte prüfen, dass der alte Schlüssel bekannt ist und
der Parameter nicht den Wert null hat.
 Die Methode suche sollte prüfen, dass der Schlüssel nicht null ist.
Diese Änderungen wurden alle in der jeweils zweiten Version der bisher betrach-
teten Projekte umgesetzt: Adressbuch-V2T und Adressbuch-V2G.

Übung 14.14 Was denken Sie, warum wir ähnliche Änderungen nicht auch
an den Methoden gibKontakt und schluesselBekannt vornehmen wollten?
Übung 14.15 Bei der Behandlung von Parameterfehlern haben wir keine
Fehlermeldungen ausgegeben. Denken Sie, dass ein Adressbuch eine Fehler-
meldung ausgeben sollte, wenn es einen fehlerhaften Parameter bekommt?
Gibt es Situationen, in denen eine Fehlermeldung nicht angemessen wäre?
Sind beispielsweise auf der Konsole ausgegebene Fehlermeldungen unan-
gemessen bei der GUI-Version des Projekts?
Übung 14.16 Gibt es weitere Überprüfungen, die wir Ihrer Meinung nach
an den Parametern der anderen Methoden vornehmen sollten, damit ein
Adressbuch nicht in einen inkonsistenten Zustand kommen kann?

14.3 Fehlermeldungen durch den


Dienstleister
Nachdem wir einen Dienstleister vor illegalen Operationen durch falsche Parame-
ter bewahrt haben, könnten wir jetzt die Meinung vertreten, dass der Program-
mierer eines Dienstleisters nichts weiter zu tun braucht. Tatsächlich würden wir
aber gerne solche Fehlersituationen von vornherein ausschließen. Außerdem sind
falsche Parameter meist auf Programmierfehler im Klienten zurückzuführen. Des-
halb sollten wir, statt nur uns selbst zu schützen, auch den Klienten auf einen

534
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.3 Fehlermeldungen durch den Dienstleister

aufgetretenen Fehler hinweisen – entweder das benutzende Objekt selbst oder


den Benutzer oder den Programmierer. Auf diese Weise gibt es eine reale
Chance, dass der fehlerhafte Code korrigiert wird. Beachten Sie aber, dass wir es
hier mit drei ganz unterschiedlichen Typen von „Adressaten“ für die Benachrich-
tigung zu tun haben.
Welches ist die beste Art für einen Dienstleister, ein aufgetretenes Problem zu
melden? Es gibt nicht die eine richtige Antwort auf diese Frage, und die ange-
messene Antwort hängt häufig vom Zusammenhang ab, in dem der Dienstleister
verwendet wird. In den folgenden Abschnitten werden wir einige Optionen für
die Meldung von Fehlern untersuchen.

Übung 14.17 Auf wie viele Arten kann eine Methode inkorrekte Parameter-
werte oder andere Umstände melden, die sie davon abhalten, ihre Aufgabe
korrekt auszuführen? Betrachten Sie möglichst viele unterschiedliche Arten
von Anwendungen. Beispielsweise solche mit einer grafischen Benutzungs-
schnittstelle; solche mit einer textbasierten Schnittstelle und einem mensch-
lichen Benutzer; aber auch solche, in denen keine Interaktion mit einem Benut-
zer stattfindet, etwa beim elektronischen Motorenmanagement eines Autos;
oder Software in eingebetteten Systemen, wie z.B. ein Bankautomat.

14.3.1 Den Benutzer informieren


Die offensichtliche Möglichkeit, mit der ein Objekt auf einen fehlerhaften Umstand
reagieren kann, ist, den Benutzer der Anwendung zu benachrichtigen. Diese
Option bedeutet entweder die Ausgabe einer Fehlermeldung über [Link] oder
[Link] oder das Öffnen eines Fensters mit einem Fehlertext.

Die Hauptprobleme mit beiden Ansätzen sind:


 Sie gehen davon aus, dass die Anwendung von einem Benutzer bedient wird,
der die Fehlermeldung zu sehen bekommt. Es gibt aber viele Anwendungen,
die völlig unabhängig von einem Benutzer ausgeführt werden. Eine Fehlermel-
dung oder ein Fehlerfenster bleibt dann völlig unbeachtet. Tatsächlich könnte
der Computer, auf dem die Anwendung läuft, gar keinen Bildschirm zur Aus-
gabe zur Verfügung haben.
 Selbst wenn ein Benutzer die Fehlermeldung sehen kann, wird er selten in der
Lage sein, etwas gegen das Problem zu unternehmen. Stellen Sie sich einen
Bankautomaten vor, der Ihnen eine NullPointerException meldet! Nur in den Fäl-
len, in denen eine unmittelbare Aktion des Benutzers zu dem Fehler geführt hat –
etwa eine falsche Eingabe –, kann der Benutzer möglicherweise das nächste
Mal korrigierend eingreifen.
Programme, die unangemessene Fehlermeldungen ausgeben, werden ihre Benut-
zer eher verärgern, als ihnen mit den Meldungen zu nützen. Deshalb ist, abgesehen
von sehr wenigen Situationen, das Informieren des Benutzers keine allgemeingül-
tige Lösung zur Frage der Fehlerbehandlung.

535
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Übung 14.18 Die Java-API enthält im Paket [Link] eine durch-


dachte Sammlung von Klassen zur Fehlerprotokollierung. Die statische
Methode getLogger der Logger-Klasse liefert ein Logger-Objekt. Untersuchen
Sie die Eigenschaften der Klasse für die textbasierte Protokollierung zur Fehler-
diagnose. Welche Bedeutung haben die unterschiedlichen Protokollebenen?
Wie unterscheidet sich die Methode info von der Methode warning? Kann die
Protokollierung ab- und wieder angeschaltet werden?

14.3.2 Den Klienten informieren


Ein komplett anderer Ansatz als die bisher diskutierten ist, wenn der Dienstleister
einen aufgetretenen Fehler an seinen Klienten meldet. Es gibt zwei Wege, wie dies
erfolgen kann:
 Ein Dienstleister kann den Ergebniswert einer Methode ungleich void benutzen,
um entweder den Erfolg oder den Misserfolg des Methodenaufrufs zu signali-
sieren.
 Ein Dienstleister kann in der Methode eine Exception werfen, wenn etwas schief-
geht. Das ist ein für uns neues Java-Konzept, das es auch in anderen Program-
miersprachen gibt. Wir werden es ausführlich in Abschnitt 14.4 erläutern.
Beide Techniken haben den Vorteil, dass sie den Programmierer des Klienten dazu
auffordern, sich auch über das mögliche Scheitern eines Aufrufs Gedanken zu
machen. Allerdings hält nur das Werfen einer Exception einen Klienten aktiv davon
ab, einen fehlerhaften Aufruf zu ignorieren.
Der erste Ansatz ist leicht umzusetzen, wenn die betrachtete Methode einen Ergeb-
nistyp void hat, wie etwa entferneKontakt. Wenn das void durch den Typ boolean
ersetzt wird, dann kann die Methode true liefern, sofern das Entfernen erfolgreich
war, und false würde signalisieren, dass etwa schiefgelaufen ist (Listing 14.3).
Listing 14.3
Ein boolescher Ergeb-
nistyp signalisiert
Erfolg oder Misserfolg.

Dies ermöglicht einem Klienten, mit einer if-Anweisung weitere Anweisungen zu


schützen, die vom erfolgreichen Entfernen eines Eintrags abhängen:

536
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.3 Fehlermeldungen durch den Dienstleister

if([Link]("...")) {
// Eintrag erfolgreich entfernt; normal weiterarbeiten.
...
}
else {
// Entfernen schlug fehl; sinnvolle Fehlerbehandlung, falls möglich.
...
}
Wenn eine Methode des Dienstleisters bereits einen Ergebnistyp ungleich void hat
– und damit einen booleschen Wert zur Fehlermeldung verhindert – kann immer
noch auf andere Weise über den Ergebnistyp signalisiert werden, dass etwas
schiefgegangen ist. Das ist dann der Fall, wenn der Wertebereich des Ergebnistyps
einen Wert für fehlerhafte Situationen anbietet. Beispielsweise liefert die Methode
gibKontakt ein Kontakt-Objekt für den gegebenen Schlüssel zurück; der folgende
Quelltextabschnitt beispielsweise geht davon aus, dass ein spezifischer Schlüssel
einen gültigen Kontakt zurückliefert:
// Sende eine Nachricht an Andreas
Kontakt kontakt = [Link]("Andreas");
String telefon = [Link]();
...
Eine Möglichkeit für die Methode gibKontakt, einen ungültigen Schlüssel zu sig-
nalisieren, besteht darin, statt eines Kontakt-Objekts den Wert null zurückzulie-
fern (Listing 14.4).

Listing 14.4
Signalisieren eines
Fehlers durch ein
null-Ergebnis.

Dies würde es einem Klienten ermöglichen, in Abhängigkeit vom Aufrufergebnis


entweder den normalen Kontrollfluss fortzusetzen oder in geeigneter Weise auf
den Fehler zu reagieren:
Kontakt kontakt = [Link]("Andreas");
if(kontakt != null) {
// Sende eine Nachricht an Andreas
String telefon = [Link]();
...
}
else {
// Eintrag nicht gefunden; sinnvolle Fehlerbehandlung, falls möglich.
...
}

537
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Es ist durchaus üblich, dass Methoden, die eine Objektreferenz zurückliefern, den
Wert null zum Signalisieren einer Fehlersituation verwenden. Bei Methoden, die
Werte der primitiven Typen zurückliefern, gibt es oft einen Wert, der eine Grenz-
überschreitung repräsentiert: Beispielsweise liefert die Methode indexOf der Klasse
String einen negativen Wert als ein Signal, dass das gesuchte Zeichen nicht gefun-
den wurde.

Übung 14.19 Denken Sie, dass die unterschiedlichen Oberflächentypen der


V2T- und V2G-Projekte auch zu unterschiedlichen Arten von Fehlermeldun-
gen führen sollten?
Übung 14.20 Ändern Sie die Klasse Adressbuch in einer Kopie des Projekts
Adressbuch-V2T so ab, dass einem Klienten an geeigneter Stelle mitgeteilt
wird, dass eine Methode ungültige Parameterwerte bekommen hat oder
aus einem anderen Grund ihren Dienst nicht erfüllen kann.
Übung 14.21 Denken Sie, dass ein Aufruf der Methode suche, der keine
passenden Kontakte findet, zu einer Fehlermeldung führen sollte? Rechtfer-
tigen Sie Ihre Antwort.
Übung 14.22 Gibt es Kombinationen von Parameterwerten, die Ihrer Mei-
nung nach ungeeignet sind, um bei einem Konstruktoraufruf der Klasse
Kontakt benutzt zu werden?

Übung 14.23 Verfügt ein Konstruktor über Möglichkeiten, einem Klienten


zu signalisieren, dass er mit den gegebenen Parameterwerten den Objektzu-
stand nicht sinnvoll initialisieren kann? Wie sollte ein Konstruktor reagieren,
wenn er ungültige Parameterwerte übergeben bekommt?

Offensichtlich kann dieser Ansatz (Anzeige eines Fehlers durch einen Wert, der
außerhalb des gültigen Wertebereichs liegt) nicht verwendet werden, wenn alle
Werte des Ergebnistyps bereits eine gültige Bedeutung für den Klienten haben.
In solchen Fällen bleibt keine andere Wahl als das Auslösen einer Exception
(siehe Abschnitt 14.4), das auch einige weitere Vorteile bietet. Damit diese Vor-
teile deutlich werden, heben wir hier zwei Aspekte der Technik mit dem Ergeb-
niswert hervor:
 Es gibt keine Möglichkeit, einen Klienten explizit zum Überprüfen des Ergebnis-
werts aufzufordern. Deshalb kann ein Klient weiterarbeiten, als wäre nichts
geschehen; dies könnte in einer NullPointerException enden oder – noch schlim-
mer – der Fehlerwert könnte als ein regulärer Ergebniswert verwendet werden
und zu schwer zu entdeckenden logischen Fehlern führen!
 In einigen Fällen könnte der Fehlerwert für zwei sehr unterschiedliche Zwecke
verwendet werden. Das gilt beispielsweise für die überarbeiteten Methoden
gibKontakt (Listing 14.3) und entferneKontakt (Listing 14.4). Einerseits soll dem
Klienten signalisiert werden, ob die Anfrage erfolgreich war oder nicht. Ande-
rerseits soll signalisiert werden, dass die Anfrage fehlerhaft war, etwa auf-
grund ungültiger Parameterwerte.

538
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.4 Prinzipien der Exception-Behandlung

In den meisten Fällen stellt eine nicht erfolgreiche Anfrage keinen logischen Pro-
grammierfehler dar, eine fehlerhafte Anfrage hingegen meist schon. Wir sollten
in diesen beiden Fällen von einem Klienten zwei sehr unterschiedliche Reaktio-
nen erwarten. Es gibt keine generelle Möglichkeit, dies befriedigend über Ergeb-
niswerte zu behandeln.

14.4 Prinzipien der Exception-Behandlung


Das Werfen einer Exception ist die effektivste Möglichkeit für einen Dienstleister,
einem Klienten zu signalisieren, dass seine Anfrage nicht bearbeitet werden konnte.
Einer der wichtigsten Vorteile gegenüber speziellen Ergebniswerten ist, dass es für
einen Klienten (fast) unmöglich ist, eine aufgetretene Exception zu ignorieren und
unberührt weiterzuarbeiten. Wenn der Klient die Exception nicht behandelt, dann
wird die laufende Anwendung automatisch und unmittelbar beendet.1 Außerdem
kann der Exception-Mechanismus unabhängig von bereits bestehenden Ergebnis-
typen verwendet werden.
Ein wichtiger Punkt, den Sie während der folgenden Diskussion berücksichtigen
sollten, ist, dass im Zusammenhang mit Exceptions der Ort, an dem ein Fehler fest-
gestellt wird, sich von dem Ort unterscheidet, an dem versucht wird, den Fehler zu
beheben (sofern dies vorgesehen ist). Die Feststellung eines Fehlers erfolgt in einer
Methode des Dienstleisters und die Fehlerbehebung obliegt dem Klienten. Wenn
es möglich wäre, den Fehler gleich dort zu beheben, wo er festgestellt wurde,
gäbe es keinen Grund, eine Exception auszulösen.

14.4.1 Das Auslösen einer Exception


Listing 14.5 zeigt, wie eine Exception mit einer throw-Anweisung geworfen wird.
In der Methode gibKontakt wird im Falle eines null-Parameters eine Exception
geworfen, um zu signalisieren, dass die Übergabe von null für den Schlüssel eine
ungültige Anfrage ist.

Listing 14.5
Eine Exception
wird geworfen.

1 Dies ist genau das, was Ihnen immer dann passiert ist, wenn Ihr Programm aufgrund einer
NullPointerException oder IndexOutOfBoundsException ungewollt abgestürzt ist.

539
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Konzept Das Auslösen einer Exception besteht aus zwei Schritten: Zuerst wird mit dem
Schlüsselwort new ein Exception-Objekt erzeugt (in diesem Fall ein Objekt der Klasse
Eine Exception
IllegalArgumentException), und dann wird dieses Exception-Objekt mit dem Schlüs-
ist ein Objekt, das
Informationen über selwort throw geworfen. Diese beiden Schritte werden fast immer in einer einzigen
einen Programmfeh- Anweisung vorgenommen:
ler hält. Eine Excep- throw new ExceptionTyp("optionaler Fehlertext für die Diagnose");
tion wird ausgelöst,
um zu signalisieren, Bei der Erzeugung eines Exception-Objekts kann ein Fehlertext an den Konstruktor
dass ein Fehler übergeben werden. Dieser String kann später vom Empfänger der Exception wie-
aufgetreten ist. der über die Methoden getMessage oder toString abgefragt werden. Der String
wird dem Benutzer auch dann angezeigt, wenn die Exception nicht behandelt wird
und zur Beendigung des Programms führt. Der hier von uns verwendete Excep-
tion-Typ IllegalArgumentException ist in dem Paket [Link] definiert und wird oft
dazu verwendet, um anzuzeigen, dass einer Methode oder einem Konstruktor ein
nicht korrekter Parameterwert übergeben wurde.
Listing 14.5 zeigt außerdem, dass in der Dokumentation einer Methode zusätzlich
über das javadoc-Schlüsselwort @throws angegeben werden kann, welche Excep-
tions diese Methode im Falle eines Fehlers wirft.

14.4.2 Geprüfte und ungeprüfte Exceptions


Eine Exception ist immer eine Instanz einer Klasse aus einer speziellen Vererbungshie-
rarchie. Wir können neue Exception-Typen definieren, indem wir Subklassen dieser
Hierarchie erzeugen (Abbildung 14.1). Genau genommen ist jede Exception-Klasse
eine Subklasse der Klasse Throwable, die im Paket [Link] definiert ist.2 Üblicher-
weise folgen wir der Konvention, dass neue Exception-Klassen als Subklassen der
Klasse Exception deklariert werden, die ebenfalls in [Link] definiert ist. Das Paket
[Link] definiert eine Reihe von häufig auftretenden Exception-Klassen, die Ihnen
während der Programmierung möglicherweise schon begegnet sind, etwa Null-
PointerException, IndexOutOfBoundsException und ClassCastException.
Java unterteilt die Exception-Klasse in zwei Kategorien: geprüfte Exceptions (checked
exception) und ungeprüfte Exceptions (unchecked exception). Alle Subklassen der
Klasse RuntimeException definieren ungeprüfte Exceptions; alle anderen Subklas-
sen von Exception definieren geprüfte Exceptions.
Der Unterschied, etwas vereinfacht dargestellt, ist der folgende: Geprüfte Excep-
tions sind für solche Fälle gedacht, in denen ein Klient damit rechnen sollte, dass
eine Operation fehlschlagen könnte (wenn wir beispielsweise auf eine Festplatte
schreiben, dann wissen wir, dass die Platte voll sein könnte). In diesen Fällen sollte
ein Klient dazu gezwungen werden, den Erfolg der Operation zu überprüfen. Unge-
prüfte Exceptions sind gedacht für Fälle, die im normalen Betrieb eigentlich nicht
auftreten sollten – sie werden meist durch Programmfehler ausgelöst. So würde bei-
spielsweise ein Programmierer niemals mit Absicht versuchen, auf ein Element an
einer nicht vorhandenen Position in der Liste zuzugreifen. Passiert es dennoch, löst
dies folglich eine ungeprüfte Exception aus.

2 Exception ist eine der beiden direkten Subklassen von Throwable; die andere ist Error. Subklas-
sen von Error sind üblicherweise für Fehler des Laufzeitsystems vorgesehen und nicht für Feh-
ler, auf die ein Programmierer Einfluss hat.

540
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.4 Prinzipien der Exception-Behandlung

Abbildung 14.1
Die Hierarchie der
Exception-Klassen.

Leider gibt es keine klar definierten Regeln, aus welcher Kategorie im jeweiligen
Fall eine Exception ausgelöst werden soll, aber wir können einige allgemeine Hin-
weise geben:
 Eine Faustregel besagt, dass ungeprüfte Exceptions in Situationen verwendet wer-
den sollten, die zu einem Programmabbruch führen könnten – typischerweise,
weil angenommen werden muss, dass ein logischer Fehler im Programm vorliegt,
der eine weitere Ausführung unmöglich macht. Daraus folgt, dass geprüfte
Exceptions immer dann verwendet werden sollten, wenn ein Problem aufgetreten
ist, das ein Klient sinnvoll behandeln kann. Die Schwierigkeit mit dieser Richtlinie
ist, dass sie davon ausgeht, dass ein Dienstleister genug über den Kontext weiß, in
dem er benutzt wird, und aufgrund dieses Wissens einschätzen kann, ob ein Wie-
deraufsetzen nach einer Exception möglich ist oder nicht.
 Eine zweite Faustregel besagt, dass ungeprüfte Exceptions in Situationen benutzt
werden sollten, die vermeidbar gewesen wären. Beispielsweise ist der Aufruf einer
Methode an einer Variablen, die null enthält, ein logischer Programmierfehler, der
vollständig vermeidbar ist; dass für diesen Fehlerfall die ungeprüfte Exception
NullPointerException verwendet wird, passt dann zu dieser Regel. Daraus folgt,
dass geprüfte Exceptions für Fälle verwendet werden sollten, die außerhalb des
Einflussbereichs des Programmierers liegen, etwa eine volle Platte bei einem
Schreibzugriff auf das Dateisystem oder ein fehlgeschlagener Netzwerkzugriff
aufgrund einer Verbindungsunterbrechung.
Die formalen Regeln im Umgang mit Exceptions unterscheiden sich sehr stark für
geprüfte und ungeprüfte Exceptions, und wir werden diese Unterschiede ausführ-
lich in den Abschnitten 14.4.4 und 14.5.1 herausarbeiten. Hier nur kurz zusam-
mengefasst: Der Klient einer Methode, die eine geprüfte Exception werfen kann,
muss zwei Dinge tun: Er muss sowohl Anweisungen programmieren, die eine sol-
che Exception erwarten, als auch Anweisungen, die mit der Exception umgehen,
wenn sie tatsächlich auftritt.3

3 Leider ist es nach wie vor noch viel zu einfach, dass der Programmierer eines Klienten sich formal
an diese Regeln hält, ohne jedoch wirklich eine angemessene Fehlerbehandlung durchzuführen.

541
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Übung 14.24 Nennen Sie drei Exception-Typen aus dem Paket [Link].
Übung 14.25 Ist SecurityException aus dem Paket [Link] eine geprüfte
oder eine ungeprüfte Exception? Was ist mit NoSuchMethodException? Begrün-
den Sie Ihre Antwort.

14.4.3 Die Auswirkungen einer Exception


Was passiert, wenn eine Exception ausgelöst wird? Zwei Effekte müssen hier
betrachtet werden: die Auswirkungen auf die Methode, in der das Problem fest-
gestellt wurde, und die Auswirkungen auf den Aufrufer der problematischen
Methode.
Wenn eine Exception ausgelöst wird, wird die Ausführung der auslösenden Methode
sofort beendet – sie wird nicht bis zum Ende des Methodenrumpfes ausgeführt. Eine
Konsequenz daraus ist, dass eine Methode mit einem anderen Ergebnistyp als void
kein Ergebnis zurückliefern muss, wenn eine Exception ausgelöst wurde. Das ist
durchaus sinnvoll, denn das Auslösen einer Exception ist ein Signal dafür, dass eine
Methode ihre Aufgabe nicht korrekt ausführen konnte, somit also auch kein Ergeb-
nis liefern kann. Wir können dieses Prinzip verdeutlichen, indem wir eine geänderte
Version des Methodenrumpfes aus Listing 14.5 betrachten:
if(schluessel == null) {
throw new IllegalArgumentException("Parameter in gibKontakt ist null.");
}
else {
return [Link](schluessel);
}
Das Fehlen einer return-Anweisung in dem Zweig, in dem die Exception geworfen
wird, ist korrekt. Tatsächlich würde der Compiler einen Fehler melden, falls weitere
Anweisungen nach der throw-Anweisung folgten, denn diese Anweisungen könn-
ten niemals erreicht werden.
Die Auswirkungen auf die Stelle im Programm, an der die Methode aufgerufen
wurde, sind etwas komplexer. Insbesondere hängen sie davon ab, ob an dieser
Stelle Anweisungen geschrieben wurden, die die Exception fangen. Betrachten
Sie folgenden konstruierten Aufruf von gibKontakt:
Kontakt kontakt = [Link](null);
// Die folgenden Anweisungen können nicht erreicht werden.
String telefon = [Link]();
Wir können sagen, dass auf jeden Fall die Ausführung dieser Anweisungen nicht
vollständig erfolgt – die Exception, die von gibKontakt geworfen wird, unterbricht
die Ausführung der ersten Anweisung und es erfolgt keine Zuweisung an die
Variable kontakt. Als Konsequenz wird auch die zweite Anweisung nicht ausge-
führt.
Dieses Beispiel zeigt die Mächtigkeit von Exceptions: Sie halten einen Klienten
davon ab, unbeirrt fortzufahren, wenn ein Fehler aufgetreten ist. Was als Nächs-

542
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.4 Prinzipien der Exception-Behandlung

tes passiert, hängt davon ab, ob die Exception gefangen wird oder nicht. Wenn
sie nicht gefangen wird, dann wird das Programm schlicht beendet, mit dem Hin-
weis, dass eine NullPointerException aufgetreten ist. Das Fangen einer Exception
werden wir in Abschnitt 14.5.2 diskutieren.

14.4.4 Ungeprüfte Exceptions


Ungeprüfte Exceptions sind aus Sicht des Programmierers am einfachsten zu hand- Konzept
haben, weil ein Compiler nur wenige Überprüfungen für sie vornimmt. So soll das
Ungeprüfte
Adjektiv „ungeprüft“ auch verstanden werden – der Compiler führt statisch (zur
Exceptions sind
Übersetzungszeit) keine Überprüfungen durch, weder in der Methode, in der die eine Art von Excep-
ungeprüfte Exception geworfen wird, noch an der Stelle, an der die Methode auf- tion, bei deren
gerufen wird. Eine Exception-Klasse definiert ungeprüfte Exceptions, wenn sie eine Verwendung der
Subklasse der Klasse RuntimeException aus dem Paket [Link] ist. Alle Beispiele, Compiler keine
mit denen wir bisher das Auslösen einer Exception demonstriert haben, haben zusätzlichen
ungeprüfte Exceptions verwendet. Deshalb gibt es an dieser Stelle wenig über das Überprüfungen
vornimmt.
Auslösen einer ungeprüften Exception zu sagen: Verwenden Sie einfach eine throw-
Anweisung.

Wenn wir auch der Konvention folgen, dass ungeprüfte Exceptions immer dann
verwendet werden sollten, wenn das Ergebnis voraussichtlich ein Programm-
abbruch ist – die Exception also voraussichtlich nicht gefangen wird –, dann gibt
es auch nicht viel darüber zu sagen, was der Aufrufer tun sollte; denn er wird
nichts tun und das Programm wird abgebrochen. Wenn allerdings der Bedarf
besteht, dass auch eine ungeprüfte Exception gefangen werden soll, dann kann
eine Exception-Behandlung, genau wie für eine geprüfte Exception, implemen-
tiert werden. Wie dies geschieht, werden wir in Abschnitt 14.5.2 sehen.

Die ungeprüfte IllegalArgumentException haben wir bereits kennengelernt. Diese


wird geworfen, wenn ein Konstruktor oder eine Methode signalisieren will, dass die
übergebenen Parameterwerte ungültig sind. Beispielsweise könnte die Methode
gibKontakt diese Exception werfen, wenn der übergebene Schlüssel eine leere Zei-
chenkette ist (Listing 14.6).

Listing 14.6
Eine Prüfung auf
einen ungültigen
Parameterwert.

543
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Es ist durchaus angemessen, dass eine Methode eine Reihe von Prüfungen an ihren
Parameterwerten vornimmt, bevor sie ihre eigentliche Aufgabe ausführt. Dies
macht es unwahrscheinlicher, dass die Methode erst dann eine Exception aufgrund
ungültiger Parameter auslösen muss, wenn sie ihre Aufgabe bereits teilweise aus-
geführt hat. Solchen Situationen soll insbesondere deshalb vorgebeugt werden,
weil eine unvollständige Änderung des Objektzustands das Objekt möglicherweise
in einem inkonsistenten Zustand hinterlässt. Wenn eine Operation aus irgendei-
nem Grund fehlschlägt, dann sollte das Objekt möglichst in dem Zustand belassen
werden, in dem es sich vor dem Aufruf befunden hat.
Die statische requireNonNull-Methode der Klasse [Link] bietet eine
praktische Kurzform, um einen Ausdruck zu testen und eine Ecxeption zu werfen,
falls der Ausdruck null ist. Der folgende Ausdruck wird eine NullPointerException
zusammen mit der entsprechenden Zeichenkette ausgeben, falls der Wert von
schluessel null ist; andernfalls passiert nichts:
[Link](schluessel, "null-Schluessel an gibDetails uebergeben");

Übung 14.26 Überprüfen Sie alle Methoden der Klasse Adressbuch darauf,
ob einige von ihnen eine IllegalArgumentException auslösen sollten. Falls ja,
dann fügen Sie die notwendigen Prüfungen und throw-Anweisungen ein.
Übung 14.27 Wenn Sie dies noch nicht getan haben, dann fügen Sie auch
die entsprechenden javadoc-Kommentare ein, die die möglichen Exceptions
erläutern.
Übung 14.28 UnsupportedOperationException ist eine ungeprüfte Exception,
die in dem Paket [Link] definiert ist. Wie kann sie in einer Implementie-
rung des Interface [Link] verwendet werden, um zu verhindern,
dass Elemente aus einer Sammlung entfernt werden, über die gerade iteriert
wird? Versuchen Sie dies in der Klasse LogdateiLeser des Projekts Weblog-
Auswertung in Kapitel 7.

14.4.5 Objekterzeugung verhindern


Eine wichtige Verwendung von Exceptions dient dazu, die Erzeugung von Objek-
ten zu verhindern, die nicht in einen gültigen Anfangszustand versetzt werden
können. Üblicherweise ist das der Fall, wenn einem Konstruktor ungültige Parame-
terwerte übergeben werden. Wir können dies an der Klasse Kontakt verdeutlichen.
Der Konstruktor ist momentan recht nachsichtig mit seinen Parameterwerten: Er
weist null-Werte nicht zurück, sondern ersetzt sie durch leere Zeichenketten. Aller-
dings benötigt das Adressbuch mindestens einen gültigen Namen oder eine gül-
tige Telefonnummer als eindeutigen Schlüssel, da ein Eintrag mit leerem Namen
und leerer Telefonnummer nicht indiziert werden kann. Wir können diese Anforde-
rung verdeutlichen, indem wir in einem solchen Fall die Objekterzeugung verwei-
gern. Das Auslösen einer Exception aus einem Konstruktor erfolgt genau wie aus
einer Methode heraus. Listing 14.7 zeigt den überarbeiteten Konstruktor der
Klasse Kontakt, der verhindert, dass ein Kontakt einen leeren Namen und eine leere
Telefonnummer haben kann.

544
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.5 Die Behandlung von Exceptions

Listing 14.7
Der Konstruktor der
Klasse Kontakt.

Eine Exception, die von einem Konstruktor geworfen wird, hat die gleiche Aus-
wirkung wie eine Exception aus einer Methode. Deshalb wird der folgende Ver-
such, ein ungültiges Kontakt-Objekt zu erzeugen, fehlschlagen – er wird nicht
dazu führen, dass in der Variablen ein null-Wert gespeichert wird:
Kontakt falscherKontakt = new Kontakt("", "", "");

14.5 Die Behandlung von Exceptions


Während für das Werfen einer Exception in Java für geprüfte und ungeprüfte Excep-
tions die gleichen Regeln gelten, fordern die Sprachregeln lediglich für geprüfte
Exceptions, dass sie behandelt werden müssen. Eine Klasse für geprüfte Exceptions
ist eine Subklasse der Klasse Exception, aber nicht Subklasse von RuntimeException.
Bei der Benutzung von geprüften Exceptions gibt es etliche Regeln zu beachten, da
der Compiler Überprüfungen sowohl in der auslösenden Methode als auch beim
Aufrufer durchführt.

14.5.1 Geprüfte Exceptions: die throws-Klausel


Die erste Forderung des Compilers ist, dass eine Methode, die eine geprüfte Excep-
tion werfen kann, diesen Umstand durch eine throws-Klausel im Kopf der Methode
deklarieren muss. Beispielsweise würde eine Methode, die die Exception IOException
aus dem Paket [Link] auslösen kann, folgenden Kopf besitzen:4
public void speichereInDatei(String dateiname)
throws IOException

4 Beachten Sie, dass hier das Schlüsselwort throws statt throw verwendet wird.

545
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Konzept Es ist zulässig, eine throws-Klausel auch für eine ungeprüfte Exception anzugeben,
aber der Compiler fordert dies nicht. Wir empfehlen, in einer throws-Klausel nur die
Geprüfte
geprüften Exceptions aufzuführen, die durch die Methode ausgelöst werden kön-
Exceptions sind
Exception-Typen, nen.
bei deren Verwen- Es sollte unbedingt zwischen der throws-Klausel im Kopf der Methode und dem
dung der Compiler javadoc-Kommentar unmittelbar vor der Methode unterschieden werden; der
zusätzliche Über-
Letztere ist für beide Exception-Arten völlig optional. Wir empfehlen dennoch,
prüfungen durch-
führt und einfor-
dass der entsprechende javadoc-Kommentar sowohl für geprüfte als auch für
dert. Insbesondere ungeprüfte Exceptions angegeben wird. Auf diese Weise werden einem Benutzer
erfordern geprüfte dieser Methode so viele Informationen wie möglich zur Verfügung gestellt.
Exceptions in Java,
dass throws-
Klauseln und try- 14.5.2 Exceptions fangen: der try-Block
Blöcke angegeben
werden müssen. Die zweite Anforderung ist, dass der Aufrufer einer Methode, die eine geprüfte
Exception werfen kann, Maßnahmen für den Umgang mit dieser Exception ergreifen
muss. Üblicherweise geschieht dies durch einen sogenannten Exception-Handler in
Form eines try-Blocks. Die meisten try-Blöcke haben in der Praxis die allgemeine
Form aus Listing 14.8. Hier werden zwei neue Schlüsselwörter eingeführt – try und
catch –, die eine try-Klausel bzw. eine catch-Klausel einleiten. Beide Klauseln beste-
hen aus einem Block von Anweisungen.

Listing 14.8
Die try- und die
catch-Klausel eines
Exception-Handlers.

Konzept Angenommen, wir haben eine Methode, die den Inhalt eines Adressbuches in
einer Datei speichert. Der Benutzer wird in irgendeiner Form nach dem Namen
Ein Programm-
einer Datei gefragt (vielleicht über ein GUI-Dialogfenster) und die Adressbuch-
abschnitt, der
Anweisungen Methode speichereInDatei wird dann aufgerufen, um die Liste in die Datei zu
schützt, in denen schreiben. Wenn wir keine Exceptions zu berücksichtigen hätten, sähe der Quell-
eine Exception text wie folgt aus:
ausgelöst werden
dateiname = Dateiname vom Benutzer erfragen
kann, wird Excep-
[Link](dateiname);
tion-Handler
genannt. Er defi- Weil jedoch der Schreibprozess mit einer geprüften Exception fehlschlagen kann,
niert Anweisungen muss der Aufruf von speichereInDatei durch einen try-Block umklammert werden,
zur Meldung und/
um anzuzeigen, dass dieser Fall berücksichtigt wurde. Listing 14.9 veranschaulicht,
oder zum Wieder-
aufsetzen nach wie wir dies schreiben würden, um möglichen Programmfehlern vorzubeugen.
einer aufgetrete-
nen Exception.

Listing 14.9
Ein Exception-Handler.

546
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.5 Die Behandlung von Exceptions

Beachten Sie, dass ein try-Block eine beliebige Anzahl an Anweisungen enthal-
ten kann, sodass wir dort meistens nicht nur eine Anweisung unterbringen, die
fehlschlagen kann, sondern alle Anweisungen, die in irgendeiner Weise damit
verbunden sind. Der Grundgedanke ist, dass ein try-Block eine Folge von Aktio-
nen repräsentiert, die wir als logische Einheit behandelt möchten, die aber zu
irgendeinem Punkt fehlschlagen können.5 Der catch-Block wird dann versuchen,
die Situation zu behandeln oder das Problem zu melden, wenn eine Exception
von irgendeiner der Anweisungen im dazugehörigen try-Block ausgelöst wird.
Beachten Sie, dass die Variablen dateiname und erfolgreich in diesem Beispiel aus
Gründen der Sichtbarkeit außerhalb der try-Anweisung deklariert werden müs-
sen, da try- und catch-Blöcke darauf zugreifen.
Um die Arbeitsweise eines Exception-Handlers zu verstehen, muss man sich klar-
machen, dass eine Exception den normalen Kontrollfluss beim Aufrufer unter-
bricht. Eine Exception bricht beim Aufrufer die Anweisung ab, die den Aufruf
ausführt, und folglich werden alle folgenden Anweisungen nicht mehr ausge-
führt. Daraus ergibt sich die Frage: „Wo wird die Ausführung denn fortgesetzt?“
Ein try-Block liefert die Antwort: Wenn eine Exception aufgrund eines Aufrufs in
einem try-Block ausgelöst wird, dann wird die Ausführung in der zugehörigen
catch-Klausel fortgesetzt. Wenn wir also das Beispiel aus Listing 14.9 betrachten,
dann führt das Werfen einer IOException durch den Aufruf von speichereInDatei
dazu, dass der Kontrollfluss aus dem try-Block in den catch-Block übergeht, wie
in Listing 14.10 dargestellt.

1. Exception wird hier ausgelöst 2. Ausführung wird hier fortgesetzt Listing 14.10
Wechsel des
Kontrollflusses in
einem try-Block.

Die Anweisungen in einer try-Klausel werden auch geschützte Anweisungen


genannt. Wenn keine Exception während der Ausführung der geschützten Anwei-
sungen auftritt, dann wird die catch-Klausel am Ende des try-Blocks übergangen.
Die Ausführung wird durch den Teil fortgesetzt, der auf den kompletten try-catch-
Abschnitt folgt.
Eine catch-Klausel benennt den Typ der Exception, die durch die Klausel behandelt
werden soll, innerhalb von geschweiften Klammern unmittelbar nach dem Schlüs-
selwort catch. Neben dem Exception-Typ wird auch ein Variablenname angegeben
(oft einfach nur e oder ex), über den im catch-Block auf die geworfene Exception
zugegriffen werden kann. Durch den Zugriff auf die Exception können nützliche
Informationen abgefragt werden, die ein Wiederaufsetzen oder die Meldung des
Problems ermöglichen – zum Beispiel der Zugriff auf irgendwelche Informationen
über den Fehler, die von der werfenden Methode dort platziert wurden. Nachdem

5 In Übung 14.31 finden Sie ein Beispiel für das, was passieren kann, wenn eine Anweisung, die zu
einer Exception führen könnte, getrennt von den umliegenden Anweisungen behandelt wird.

547
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

die catch-Klausel abgearbeitet wurde, kehrt der Kontrollfluss nicht wieder an die
Stelle zurück, an der die Exception ausgelöst wurde.

Übung 14.29 Im Projekt Adressbuch-V3T werden an einigen Stellen unge-


prüfte Exceptions geworfen, wenn der Wert eines Parameters null ist. Das
Projekt enthält auch eine geprüfte Exception-Klasse KeinPassenderKontakt-
Exception, die nicht benutzt wird. Verändern Sie die Methode entferneKontakt
der Klasse Adressbuch so, dass sie diese Exception auslöst, wenn der über-
gebene Schlüssel nicht bekannt ist. Fügen Sie einen Exception-Handler in
die Methode entferneEintrag der Klasse AdressbuchTexteingabe ein, der diese
Exception fängt und ihr Auftreten meldet.
Übung 14.30 Benutzen Sie die KeinPassenderKontaktException in der Methode
aendereKontakt von Adressbuch. Erweitern Sie die Textschnittstelle so, dass die
Daten eines bestehenden Eintrags geändert werden können. Lassen Sie in
AdressbuchTexteingabe die Exceptions fangen und melden, die durch einen
unbekannten Schlüssel ausgelöst werden.
Übung 14.31 Warum ist Folgendes keine sinnvolle Benutzung eines Excep-
tion-Handlers? Lässt sich dieser Quelltext übersetzen und ausführen?
Person p;
try {
p = [Link](personendetails);
}
catch (Exception e) {
}
[Link]("Die Personendetails gehören zu: " + p);

Beachten Sie, dass in allen Beispielen zu den try-Anweisungen, die Sie bisher ken-
nengelernt haben, die Exceptions nicht direkt von den Anweisungen in der try-
Klausel geworfen werden. Vielmehr werden die Exceptions indirekt ausgelöst – als
Reaktion einer Methode, die von den Anweisungen in der try-Klausel aufgerufen
wurde. Dies entspricht dem normalen Muster und es ist fast immer ein Program-
mierfehler, eine throw-Anweisung direkt in einen try-Block zu stellen.

14.5.3 Mehrere Exceptions werfen und fangen


Manchmal wirft eine Methode mehr als einen Exception-Typ, um verschiedene
Fehlersituationen zu signalisieren. Wenn es sich dabei um geprüfte Exceptions
handelt, dann müssen diese vollständig in der throws-Klausel aufgeführt werden,
voneinander durch ein Komma getrennt. Beispielsweise:
public void verarbeite()
throws EOFException, FileNotFoundException
Ein Exception-Handler muss alle geprüften Exceptions berücksichtigen, die durch
die geschützten Anweisungen ausgelöst werden können. Ein try-Block kann des-

548
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.5 Die Behandlung von Exceptions

halb mehrere catch-Klauseln enthalten, wie in Listing 14.11 gezeigt. Beachten


Sie, dass derselbe Variablenname für die verschiedenen Fälle verwendet werden
kann.

Listing 14.11
Mehrere catch-
Klauseln in einem
try-Block.

Wenn eine Exception durch eine der geschützten Anweisungen eines try-Blocks
ausgelöst wird, werden die catch-Klauseln in der Reihenfolge, in der sie im Quell-
text stehen, nach einer zu der Exception passenden Klausel durchsucht. So würde
in diesem Fall bei einer EOFException die erste Klausel ausgewählt werden und bei
der FileNotFoundException würde der Kontrollfluss bei der zweiten fortgesetzt.
Sobald das Ende einer einzelnen catch-Klausel erreicht wurde, wird die Ausführung
nach der letzten catch-Klausel fortgesetzt.
Mit Polymorphie kann bei Bedarf vermieden werden, dass mehrere catch-Klauseln
angegeben werden müssen. Dies geschieht dann aber möglicherweise auf Kosten
der Möglichkeit, typspezifisch reagieren zu können. In Listing 14.12 wird eine ein-
zelne catch-Klausel definiert, die alle Exceptions der geschützten Anweisungen
fängt. Das funktioniert deshalb, weil bei der Suche nach einer passenden catch-Klau-
sel lediglich geprüft wird, ob das Exception-Objekt eine Instanz des in der Klausel
benannten Typs ist. Da alle Exception-Klassen Subtypen der Klasse Exception sind,
wird die einzelne Klausel alle denkbaren Exceptions fangen – egal, ob geprüft oder
ungeprüft. Aufgrund der beschriebenen Suchstrategie folgt, dass die Reihenfolge
der catch-Klauseln relevant ist und dass die catch-Klausel eines bestimmten Typs
nicht auf eine catch-Klausel eines seiner Supertypen folgen kann – denn die vorher
genannte Supertyp-Klausel wird immer gewählt werden, bevor die Subtyp-Klausel
geprüft wird. Der Compiler wird dies als Fehler melden.

Listing 14.12
Fangen aller Excep-
tions in einer einzigen
catch-Klausel.

Eine bessere Möglichkeit, mehrere Exceptions mit der gleichen Aktion zum Wie-
deraufsetzen oder Melden zu verarbeiten, ist, die Exception-Typen zusammen
aufzulisten. Die verschiedenen Exception-Typen stehen vor dem Variablennamen
der Exception und werden durch das Symbol | getrennt (Listing 14.13).

549
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Listing 14.13
Fangen mehrerer
Exceptions.

Übung 14.32 Verändern Sie die try-Blöcke Ihrer Lösungen zu den Übungen
14.29 und 14.30 so, dass geprüfte und ungeprüfte Exceptions in unterschiedli-
chen catch-Klauseln verarbeitet werden.
Übung 14.33 Was ist falsch am folgenden try-Block?
Person p = null;
try {
p = [Link](personendetails);
[Link]("Die Personendetails gehören zu: " + p);
}
catch(Exception e) {
// Behandle jede geprüfte Exception ...
...
}
catch(RuntimeException e) {
// Behandle jede ungeprüfte Exception ...
...
}

14.5.4 Eine Exception propagieren


Bisher haben wir gesagt, dass eine Exception so früh wie möglich gefangen und
behandelt werden sollte. Also sollte eine Exception, die durch die Methode
verarbeite ausgelöst wurde, in der Methode gefangen und behandelt werden, die
verarbeite aufgerufen hat. Tatsächlich ist das jedoch keine strikte Sprachregel,
denn Java erlaubt, dass eine Methode, die eine Exception auslöst, diese an ihre
eigenen Aufrufer propagieren kann. Eine Methode propagiert eine Exception ein-
fach durch den Umstand, dass sie keinen Exception-Handler für die Exception
implementiert. Für geprüfte Exceptions fordert der Compiler dann jedoch, dass
diese Methode in ihrem eigenen Kopf eine throws-Klausel angibt, obwohl sie die
Exception nicht selbst erzeugt und auslöst. Das bedeutet, dass Sie manchmal auf
eine Methode stoßen, die eine throws-Klausel aufweist, aber keine throw-Anwei-
sung im Methodenrumpf enthält. Ein Propagieren ist üblich, wenn die aufrufende
Methode die Exception entweder nicht behandeln kann oder nicht behandeln
muss, dies aber weiter oben in der Aufrufkette möglich oder notwendig ist. Es ist
auch üblich in Konstruktoren, bei denen das Einrichten eines neuen Objekts fehl-
schlägt und der Konstruktor anschließend nicht wiederaufsetzen kann.
Wenn es sich um eine ungeprüfte Exception handelt, dann ist die throws-Klausel
wieder optional; wir lassen sie dann üblicherweise aus.

550
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.5 Die Behandlung von Exceptions

14.5.5 Die finally-Klausel


Ein try-Block kann einen dritten Abschnitt enthalten, der optional ist. Es ist die
finally-Klausel (Listing 14.14), die häufig ausgelassen wird. Mit einer finally-Klau-
sel können Anweisungen angegeben werden, die auf jeden Fall ausgeführt wer-
den sollen, unabhängig davon, ob eine Exception aufgetreten ist oder nicht. Wenn
der Kontrollfluss das Ende einer try-Klausel erreicht, dann werden die catch-Klau-
seln ignoriert, die finally-Klausel wird jedoch ausgeführt. Tritt andererseits eine
Exception auf, dann wird die passende catch-Klausel ausgewählt und anschließend
die finally-Klausel ausgeführt.

Listing 14.14
Ein try-Block mit einer
finally-Klausel.

Auf den ersten Blick erscheint eine finally-Klausel redundant. Führt das fol-
gende Beispiel nicht zur gleichen Ausführungsreihenfolge wie die Anweisungen
in Listing 14.14?
try {
eine oder mehrere geschützte Anweisungen
}
catch(Exception e) {
die Exception melden und eventuell wieder aufsetzen
}
gemeinsame Aktionen, die ausgeführt werden müssen,
unabhängig davon, ob eine Exception aufgetreten ist oder nicht
Tatsächlich gibt es sogar zwei Situationen, in denen diese beiden Quelltextabschnitte
sich unterschiedlich verhalten:
 Eine finally-Klausel wird auch dann ausgeführt, wenn eine return-Anweisung
in der try- oder der catch-Klausel ausgeführt wird.
 Wenn in der try-Klausel eine Exception ausgelöst wird, die nicht gefangen wird,
dann wird dennoch die finally-Klausel ausgeführt.
Im zweiten Fall könnte die nicht aufgefangene Exception eine ungeprüfte Excep-
tion sein, für die keine catch-Klausel angegeben werden muss. Es könnte sich
aber auch um eine geprüfte Exception handeln, die nicht durch eine der catch-
Klauseln gefangen, aber von der Methode propagiert wird, um weiter oben in
der Aufrufkette behandelt zu werden. Auch in einem solchen Fall würde die
finally-Klausel noch ausgeführt.

Es kann also try-Blöcke ohne catch-Klauseln geben, die sowohl eine try-Klausel als
auch eine finally-Klausel haben, wenn die Methode alle Exceptions propagiert:
try {
eine oder mehrere zu schützende Anweisungen
}

551
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

finally {
Aktionen, die ausgeführt werden müssen,
unabhängig davon, ob eine Exception aufgetreten ist oder nicht
}

14.6 Neue Exception-Klassen definieren


Falls die vordefinierten Exception-Klassen die Ursache eines Problems nicht ausrei-
chend beschreiben, können mithilfe von Vererbung zusätzliche Exception-Klassen
definiert werden, die bessere Informationen liefern. Zusätzliche Klassen für geprüfte
Exceptions können als Subklassen jeder bereits bestehenden Klasse für geprüfte
Exceptions (etwa Exception) definiert werden, zusätzliche ungeprüfte Exceptions
wären Subklassen in der Hierarchie von RuntimeException.
Alle bestehenden Exception-Klassen bieten die Möglichkeit, dem Konstruktor eine
Fehlerbeschreibung zu übergeben. Einer der Hauptgründe für das Definieren neuer
Exception-Klassen ist jedoch, dem Exception-Objekt zusätzliche Informationen mit-
zugeben, die die Diagnose und das Wiederaufsetzen erleichtern. Beispielsweise
erwartet eine Methode wie aendereKontakt in der Adressbuch-Anwendung einen
Parameter schluessel, der auf einen existierenden Eintrag passen sollte. Wenn kein
passender Eintrag existiert, ist das ein Programmierfehler, da die Methode ihre Auf-
gabe nicht erfüllen kann. Beim Melden dieses Fehlers ist es nützlich, wenn der
Schlüssel, der den Fehler verursacht hat, mitgeliefert wird. Listing 14.15 zeigt eine
neue Klasse für geprüfte Exceptions, die im Projekt Adressbuch-V3T definiert wird.
Sie bekommt als Konstruktorparameter den Schlüssel und macht diesen sowohl
über den Diagnose-String als auch über eine speziell definierte sondierende
Methode zugänglich. Wenn diese Exception durch einen Exception-Handler gefan-
gen wird, steht der Schlüssel für die Anweisungen zur Verfügung, die ein Wieder-
aufsetzen versuchen sollen.

Listing 14.15
Eine Exception-Klasse
mit zusätzlichen Diag-
nose-Informationen.

552
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.7 Die Verwendung von Zusicherungen

Dieses Prinzip, Informationen zum leichteren Wiederaufsetzen mitzuliefern, sollte


insbesondere bei Klassen für geprüfte Exceptions beachtet werden. Durch das
Definieren formaler Parameter für den Exception-Konstruktor kann gewährleistet
werden, dass solche Diagnoseinformationen auch zur Verfügung stehen. Zusätz-
lich können durch Überschreiben der Methode toString nützliche Informationen
für die Fehlerdiagnose geliefert werden, auch wenn kein Wiederaufsetzen mög-
lich oder notwendig ist.

Übung 14.34 Definieren Sie im Projekt Adressbuch-V3T eine neue Klasse


für geprüfte Exceptions: DoppelterSchluesselException. Diese sollte durch die
Methode neuerKontakt ausgelöst werden, wenn eines der beiden nichtleeren
Schlüsselfelder des neuen Kontakts einen bereits bekannten Schlüssel ent-
hält. Die Exception-Klasse sollte die Werte der falschen Schlüssel speichern.
Nehmen Sie alle notwendigen Änderungen an der Klasse für die Benut-
zungsschnittstelle vor, um die Exception fangen und melden zu können.
Übung 14.35 Denken Sie, dass die Exception DoppelterSchluesselException
eine geprüfte oder eine ungeprüfte Exception sein sollte? Begründen Sie Ihre
Antwort.

14.7 Die Verwendung von Zusicherungen


14.7.1 Interne Konsistenzüberprüfungen
Wenn wir eine Klasse entwerfen oder implementieren, dann haben wir häufig
ein intuitives Gefühl für Aussagen über den Quelltext, die zu einem bestimmten
Zeitpunkt der Ausführung wahr sein sollten; aber wir formulieren diese selten
formal. Beispielsweise würden wir bei einem Objekt der Klasse Kontakt erwarten,
dass mindestens eines seiner Datenfelder nicht leer ist; oder dass nach einem
Aufruf der Methode entferneKontakt mit einem bestimmten Schlüssel dieser
Schlüssel anschließend nicht mehr in Gebrauch ist. Typischerweise wollen wir sol-
che Bedingungen festlegen, während die Klasse entwickelt wird, bevor sie aus-
geliefert wird. In gewisser Weise sind die Tests, die wir in Kapitel 9 diskutiert
haben, solche Versuche, ein sinnvolles Verhalten einer Klasse oder einer Methode
formal festzulegen. Charakteristisch für die dort diskutierten Tests ist, dass sie
außerhalb der zu testenden Klasse definiert sind. Wenn eine Klasse verändert
wurde, dann sollten wir uns Zeit für die Ausführung der Regressionstests neh-
men, um uns davon zu überzeugen, dass die Klasse weiterhin korrekt funktio-
niert; das wird leicht vergessen. Die Art der Parameterüberprüfung, die wir in

553
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

diesem Kapitel eingeführt haben, verschiebt die Betonung bereits etwas von rein
äußerlichen Überprüfungen hin zu einer Kombination von externer und interner
Überprüfung. Allerdings dient Parameterüberprüfung in erster Linie dem Schutz
eines Dienstleisters vor fehlerhafter Benutzung durch einen Klienten. Dies lässt
die Frage offen, ob wir auch interne Prüfungen einführen können, die das kor-
rekte Verhalten des Dienstleisters überprüfen.
Wir könnten solche internen Prüfungen während der Entwicklung mithilfe der
normalen Exception-Mechanismen implementieren. In der Praxis müssten wir
dafür ungeprüfte Exceptions verwenden, denn wir können nicht fordern, dass
Klienten Exception-Handler für Fehler formulieren müssen, die intern in einem
Dienstleister auftreten. Dann müssten wir uns mit der Frage beschäftigen, ob wir
diese internen Prüfungen entfernen, sobald der Entwicklungsprozess abgeschlos-
sen wurde, damit wir die möglicherweise hohen Kosten für die Überprüfungen
zur Laufzeit vermeiden können.

14.7.2 Die assert-Anweisung für Zusicherungen


Für effiziente interne Konsistenzprüfungen, die während der Entwicklung ange-
schaltet, aber für die Auslieferung eines Programms abgeschaltet werden kön-
nen, bietet Java einen Zusicherungsmechanismus in Form der assert-Anweisung
an. Eine ähnlich Idee ist uns bereits in Kapitel 9 im Zusammenhang mit den JUnit-
Tests begegnet, in denen wir die Ergebnisse zusicherten, die wir von den Metho-
denaufrufen erwarteten, und das JUnit-Rahmenwerk testete, ob diese Zusiche-
rungen bestätigt wurden.
Das Projekt Adressbuch-Assert ist eine Entwicklungsversion der Adressbuch-Pro-
jekte, in dem die Verwendung von solchen Zusicherungen demonstriert wird. Lis-
ting 14.16 zeigt die Methode entferneKontakt, die zwei Formen von assert-Anwei-
sungen enthält.

Listing 14.16
assert-Anweisungen
für interne Konsistenz-
prüfungen.

Das Schlüsselwort assert wird gefolgt von einem booleschen Ausdruck. Mit einer
solchen Anweisung soll ausgedrückt werden, dass an dieser Stelle in der Methode
eine bestimmte Bedingung zugesichert wird. Beispielsweise sichert die erste assert-

554
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.7 Die Verwendung von Zusicherungen

Anweisungen in Listing 14.16 zu, dass schluesselBekannt an dieser Stelle false lie-
fern sollte, entweder weil der Schlüssel von vornherein nicht bekannt war oder weil
er nach dem Entfernen des Kontakts nicht mehr in Benutzung ist. Diese anscheinend
offensichtliche Zusicherung ist wichtiger, als sie auf den ersten Blick erscheinen mag;
beachten Sie, dass beim Entfernen selbst der Schlüssel nicht direkt beteiligt ist.
Eine assert-Anweisung dient somit zwei Zwecken. Sie drückt etwas aus, das wir Konzept
an einem bestimmten Punkt der Ausführung als wahr annehmen, und erhöht
Eine Zusiche-
somit die Lesbarkeit sowohl für den aktuellen Entwickler als auch für zukünftige
rung ist eine
Wartungsprogrammierer; und sie führt darüber hinaus die Überprüfung der Zusi- Tatsachenaussage,
cherung durch, sodass wir benachrichtigt werden, wenn unserer Annahmen die bei normaler
nicht zutreffen. Dies kann sehr dabei helfen, Fehler früh und leicht zu finden. Programmausfüh-
rung zutreffen
Wenn der boolesche Ausdruck in einer assert-Anweisung als wahr ausgewertet
sollte. Wir können
wird, dann hat die assert-Anweisung keinen weiteren Effekt. Wenn das Ergebnis Zusicherungen ver-
jedoch false ist, dann wird ein AssertionError ausgelöst. Die zugehörige Klasse ist wenden, um unsere
eine Subklasse von Error (siehe Abbildung 14.1) und somit Teil der Hierarchie, die Annahmen explizit
die nicht behebbaren Fehler repräsentiert: Folglich sollte ein Klient keinen Excep- zu formulieren und
tion-Handler dafür vorsehen. um Programmier-
fehler leichter ent-
Die zweite assert-Anweisung Listing 14.16 illustriert die alternative Form einer decken zu können.
assert-Anweisung. Der String, der auf den Doppelpunkt folgt, wird als Parameter
an den Konstruktor von AssertionError übergeben, um einen Diagnosetext zu lie-
fern. Der zweite Ausdruck muss kein expliziter String sein; jeder Ausdruck mit
einem Ergebnis ist zulässig und wird in einen String umgewandelt, der an den
Konstruktor übergeben wird. Die erste assert-Anweisung zeigt, dass eine Zusi-
cherung häufig eine existierende Methode innerhalb der Klasse benutzt (schlues-
selBekannt). Das zweite Beispiel illustriert, dass es nützlich sein kann, spezifisch
für die Überprüfung einer Zusicherung eine eigene Methode anzubieten (konsis-
tenteGroesse in diesem Beispiel). Dies kann sinnvoll sein, wenn die Überprüfung
umfangreiche Berechnungen vornimmt. Listing 14.17 zeigt die Methode konsis-
tenteGroesse, die überprüfen soll, ob das Datenfeld anzahlEintraege korrekt die
Anzahl der eindeutigen Kontakte im Adressbuch wiedergibt.
Listing 14.17
Interne Konsistenz-
prüfung im Adress-
buch.

14.7.3 Richtlinien zur Benutzung von Zusicherungen


Zusicherungen dienen primär der Überprüfung von Konsistenzbedingungen wäh-
rend der Entwicklungs- und Testphase eines Projektes. Sie sind nicht für ausgeliefer-
ten Code gedacht. Aus diesem Grund fügt ein Java-Compiler die assert-Anweisun-
gen nur auf explizite Anweisung hin in den übersetzten Code ein. Daraus folgt, dass

555
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

assert-Anweisungen niemals dazu verwendet werden sollten, reguläre Funktionali-


tät zur Verfügung zu stellen. Beispielsweise wäre es falsch, Zusicherungen mit dem
Entfernen eines Kontaktes aus einem Adressbuch zu verknüpfen, wie in folgendem
Beispiel:
// Fehler: assert niemals für normale Ausführung verwenden!
assert [Link]([Link]()) != null;
assert [Link]([Link]()) != null;

Übung 14.36 Öffnen Sie das Projekt Adressbuch-Assert. Sehen Sie sich die
Klasse Adressbuch genau an und machen Sie sich bei allen assert-Anweisun-
gen klar, was und warum geprüft wird.
Übung 14.37 Die Klasse AdressbuchDemo enthält mehrere Testmethoden, die
Methoden von Adressbuch mit assert-Anweisungen darin aufrufen. Sehen Sie
sich den Quelltext der Klasse AdressbuchDemo an und machen Sie sich die Tests
klar und führen Sie anschließend jede Testmethode aus. Werden Zusiche-
rungsfehler generiert? Wenn ja, verstehen Sie, weshalb?
Übung 14.38 Die Methode aendereKontakt in Adressbuch enthält momentan
keine assert-Anweisung. Eine Zusicherung, die wir vornehmen könnten, ist,
dass das Adressbuch am Ende dieselbe Anzahl an Einträgen haben sollte
wie zu Anfang der Methode. Fügen Sie eine assert-Anweisung (und weitere
Anweisungen, die notwendig sind) ein, die dies überprüft. Führen Sie
anschließend die Methode testAendern aus AdressbuchDemo aus. Denken Sie,
dass diese Methode auch eine Überprüfung auf konsistente Größe enthal-
ten sollte?
Übung 14.39 Angenommen, wir entscheiden, dass ein Adressbuch nicht nur
über Name und Telefonnummer, sondern auch über die Adresse indizierbar
sein sollte. Wenn wir einfach die Anweisung
[Link]([Link](), kontakt);
in der Methode neuerKontakt einfügen, vermuten Sie, dass eine der Zusiche-
rungen fehlschlägt? Probieren Sie es aus! Nehmen Sie alle notwendigen Ände-
rungen an Adressbuch vor, die dazu führen, dass alle Zusicherungen zutreffen.
Übung 14.40 Objekte der Klasse Kontakt sind unveränderlich – sie haben
keine verändernden Methoden. Wie wichtig ist dieser Umstand für die
interne Konsistenz der Klasse Adressbuch? Nehmen Sie beispielsweise an, die
Klasse würde eine Methode setzeTelefon anbieten – können Sie Tests for-
mulieren, die die Probleme dieses Umstandes aufdecken könnten?

14.7.4 Zusicherungen und JUnit-Tests in BlueJ


In Kapitel 9 haben wir die Unterstützung vorgestellt, die BlueJ für das Testrahmen-
werk JUnit anbietet. Diese Unterstützung basiert auf dem Zusicherungsmechanis-
mus, den wir in diesem Abschnitt diskutiert haben. Methoden von JUnit wie assert-

556
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.8 Wiederaufsetzen und Fehlervermeidung

Equals sind um assert-Anweisungen herum formuliert, deren boolescher Ausdruck


aus den jeweiligen Parametern der Methoden zusammengesetzt ist. Wenn Testklas-
sen von JUnit zum Testen von Klassen benutzt werden, die eigene assert-Anwei-
sungen enthalten, dann werden Zusicherungsfehler aus diesen Anweisungen im
Fenster für Testergebnisse neben den Fehlern aus Zusicherungen in der Testklasse
angezeigt. Das Projekt Adressbuch-JUnit enthält eine Testklasse, die diese Kombina-
tion demonstriert. Die Methode testNeuerKontaktError aus AdressbuchTest wird
einen AssertionError auslösen, weil die Methode neuerKontakt nicht verwendet
werden sollte, um einen existierenden Eintrag zu ändern (siehe Übung 14.34).

14.8 Wiederaufsetzen und


Fehlervermeidung
Bisher haben wir uns in diesem Kapitel darauf konzentriert, dass der Dienstleister
Fehler erkennt und dem Klienten diese Fehler zurückmeldet. Zwei weitere wich-
tige Begriffe beim Umgang mit Fehlern sind das Wiederaufsetzen und die Fehler-
vermeidung.

14.8.1 Wiederaufsetzen
Die erste Voraussetzung für ein erfolgreiches Wiederaufsetzen nach einem Fehler
ist, dass der Klient den Fehler überhaupt bemerkt. Das mag offensichtlich klingen,
aber es kommt durchaus häufig vor, dass ein Klient sich auf die ordnungsgemäße
Ausführung einer gerufenen Methode verlässt und den Ergebniswert gar nicht
überprüft. Obwohl Exceptions sehr viel schwieriger zu ignorieren sind, haben wir
schon mehrfach das folgende Muster für Exception-Handler zu sehen bekommen:
Kontakt kontakt = null;
try {
kontakt = [Link](...);
}
catch(Exception e) {
[Link]("Fehler: " + e);
}
String telefon = [Link]();
Obwohl die Exception gefangen und gemeldet wird, wird nicht berücksichtigt,
dass es eventuell falsch sein kann, einfach weiterzuarbeiten.
Der try-Block von Java bietet die Möglichkeit, nach dem Auftreten einer Excep-
tion einen Mechanismus zum Wiederaufsetzen zu installieren. Ein Wiederauf-
setzen nach einem Fehler umfasst üblicherweise einige korrigierende Anweisun-
gen in dem catch-Block und dann einen erneuten Versuch. Wiederholte Versuche
können vorgenommen werden, indem der try-Block in einer Schleife ausgeführt
wird. Ein Beispiel dieses Ansatzes zeigt Listing 14.18, das eine Erweiterung der
Version aus Listing 14.9 ist. Die Versuche, alternative Dateinamen zu erstellen,
könnten beispielsweise eine Liste möglicher Ordner einbeziehen oder einen inter-
aktiven Benutzer nach einem anderen Namen fragen.

557
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Listing 14.18
Wiederaufsetzen
explizit programmiert.

Obwohl dieses Beispiel das Wiederaufsetzen in einer sehr spezifischen Situation


zeigt, lassen sich doch einige Aspekte verallgemeinern:
 Das Vorhersehen eines Fehlers und der Versuch, nach einem Fehler wieder auf-
zusetzen, erfordert üblicherweise komplexere Kontrollstrukturen als für Fälle, in
denen kein Fehler auftreten kann.
 Die Anweisungen in der catch-Klausel stellen die Voraussetzungen für ein Wieder-
aufsetzen her.
 Wiederaufsetzen bedeutet häufig Wiederholung.
 Erfolgreiches Wiederaufsetzen kann nicht garantiert werden.
 Es sollte verhindert werden, dass ein erfolgloses Wiederaufsetzen in einer end-
losen Schleife wiederholt wird.
Es steht nicht immer ein interaktiver Benutzer für alternative Eingaben zur Verfü-
gung, daher kann es in der Verantwortung des Klienten liegen, den Fehler zum
Beispiel mittels einer Logging-API zu protokollieren, sodass er bemerkt wird und
später untersucht werden kann.

14.8.2 Fehlervermeidung
Es sollte klar geworden sein, dass eine Situation, in der eine Exception aufgetreten
ist, schlimmstenfalls zum Programmabbruch führt und bestenfalls nur sehr umständ-
lich wieder bereinigt werden kann. Es kann sehr viel einfacher sein, den Fehler von
vornherein zu vermeiden; dies erfordert jedoch die Zusammenarbeit von Klient und
Dienstleister.
Viele der Fälle, in denen ein Adressbuch-Objekt gezwungen ist, eine Exception aus-
zulösen, beschäftigen sich mit dem Übergeben von null-Werten an seine Metho-
den. Diese Fälle stellen logische Programmierfehler dar, die offensichtlich vermieden
werden könnten, wenn der Klient die Parameter vor einem Aufruf des Dienstleisters
selbst überprüfen würde. Parameter mit null-Werten belegen üblicherweise falsche
Annahmen im Klienten. Betrachten Sie beispielsweise folgende Anweisungen:
String schluessel = [Link](postleitzahl);
Kontakt universitaet = [Link](schluessel);

558
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.8 Wiederaufsetzen und Fehlervermeidung

Wenn die Suche in der Datenbank fehlschlägt, dann kann der gelieferte Schlüssel
sehr leicht entweder leer oder null sein. Das direkte Weitergeben dieses Werts
an die Methode gibKontakt würde zu einer Exception führen. Mit einer einfachen
Prüfung des Suchergebnisses kann die Exception jedoch vermieden werden und
stattdessen das eigentliche Problem der erfolglosen Suche untersucht werden:
String schluessel = [Link](postleitzahl);
if(schluessel != null && ![Link]() {
Kontakt universitaet = [Link](schluessel);
...
}
else {
Umgang mit dem Fehler bei der Postleitzahl ...
}
In diesem Fall könnte der Klient selbst herausfinden, dass eine Methode nicht
aufgerufen werden sollte. Manchmal ist dies jedoch nicht möglich und manch-
mal muss der Klient dafür die Hilfe des Dienstleisters in Anspruch nehmen.
In Übung 14.34 wurde gefordert, dass die Methode neuerKontakt keinen Kontakt
akzeptieren sollte, wenn einer der beiden Schlüssel bereits im Adressbuch bekannt
ist. Um einen solchen ungeeigneten Aufruf zu vermeiden, könnte der Klient die
Methode schluesselBekannt des Adressbuches folgendermaßen benutzen:
// Einen eigentlich neuen Kontakt ins Adressbuch eintragen if([Link]
Bekannt([Link]()) {
[Link]([Link](), kontakt);
}
else if([Link]([Link]()) {
[Link]([Link](), kontakt);
}
else {
den Kontakt neu einfügen ...
}
Durch dieses Vorgehen kann offensichtlich vermieden werden, dass die Methode
neuerKontakt die DoppelterSchluesselException wirft, und die Exception kann des-
halb von einer geprüften zu einer ungeprüften umgewandelt werden.
Dieses Beispiel verdeutlicht einige allgemeine Prinzipien:
 Wenn die Methoden des Dienstleisters zur Gültigkeits- oder Zustandsüberprü-
fung auch Klienten zur Verfügung stehen, kann dieser häufig vermeiden, dass
Exceptions überhaupt auftreten.
 Wenn eine Exception auf diese Weise vermieden werden kann, dann weist ihr
Auslösen auf einen logischen Programmierfehler im Klienten hin. Deshalb
kann für solche Fälle eine ungeprüfte Exception verwendet werden.
 Durch Verwendung einer ungeprüften Exception muss der Klient keinen try-
Block angeben, wenn er für sich bereits sichergestellt hat, dass diese nicht auf-
treten kann. Das ist ein großer Gewinn, denn für einen Programmierer ist es
sehr ärgerlich, wenn er einen try-Block programmieren muss, obwohl die ent-
sprechende Exception aus seiner Sicht nicht auftreten kann; das kann dazu füh-
ren, dass die Fehlerbehandlung für andere Fehler nicht mehr angemessen ernst
genommen wird.

559
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Dieses Vorgehen hat aber nicht nur Vorteile. Hier sind einige Gründe, warum der
Ansatz nicht immer praktikabel ist:
 Wenn Methoden des Dienstleisters zur Gültigkeits- oder Zustandsüberprüfung
auch Klienten zur Verfügung stehen, kann dies ein Aufbrechen der Kapselung
bedeuten und zu einer engeren Kopplung zwischen Klient und Dienstleister
führen als erwünscht.
 Ein Dienstleister kann sich möglicherweise nicht darauf verlassen, dass der Klient
die Prüfungen zur Vermeidung der Exception wirklich durchführt. Dies führt
dann häufig zu einer doppelten Prüfung sowohl im Klienten als auch im Dienst-
leister. Wenn solche Prüfungen recht „teuer“ (rechenintensiv in ihrer Ausfüh-
rung) sind, dann sind solche Verdopplungen eventuell unerwünscht oder sogar
nicht vertretbar. Aus unserer Sicht gilt: Wenn die Möglichkeit zur Wahl besteht,
dann sollte eher ein Teil der Effizienz geopfert werden, wenn dadurch die resul-
tierenden Programme sicherer werden.

14.9 Dateibasierte Ein- und Ausgabe


Ein wichtiger Bereich der Programmierung, in dem die Behandlung von Exceptions
nicht ignoriert werden kann, ist die Ein- und Ausgabe. In diesem Bereich hat ein
Programmierer meist nur wenig Einfluss auf die Umgebung, in der die Anwendung
läuft. Beispielsweise könnte eine Datei, die von einer Anwendung benötigt wird,
vor der Ausführung aus Versehen gelöscht oder beschädigt worden sein. Oder der
Versuch, eine Datei anzulegen, schlägt aufgrund von fehlenden Zugriffsrechten
oder Quota-Regelungen für das Dateisystem fehl. Es gibt viele mögliche Gründe,
warum eine Eingabe- oder Ausgabe-Operation fehlschlagen kann. Außerdem ist
moderne Ein- und Ausgabe nicht mehr darauf beschränkt, dass ein Programm ein-
fach auf seinen lokalen Dateispeicher zugreift, sondern hat sich auf eine Netzwerk-
umgebung verlagert, in der die Konnektivität für den Ressourcenzugriff anfällig
und inkonsistent ist – wie zum Beispiel im Falle einer mobilen Umgebung.
Die API von Java hat über die Jahre viele Veränderungen erfahren, die die zuneh-
mende Diversität der Umgebungen, in denen Java-Programme inzwischen ausge-
führt werden, widerspiegeln. Das wichtigste Paket für ein- und ausgabespezifische
Klassen ist seit jeher [Link]. Dieses Paket enthält eine Reihe von Klassen zur Unter-
stützung von plattformunabhängigen Eingabe-/Ausgabe-Operationen In diesem
Paket wird auch die geprüfte IOException definiert, die als allgemeines Signal ver-
wendet wird, dass eine Ein-/Ausgabe-Operation fehlgeschlagen ist. Fast jede Ein-
oder Ausgabe-Operation muss davon ausgehen, dass eine der Exceptions geworfen
werden kann. Instanzen von geprüften Subklassen von IOException (wie etwa
EOFException oder FileNotFoundException) können gelegentlich geworfen werden,
um ausführlichere Diagnoseinformationen zu bieten. In neueren Java-Versionen gab
es hinsichtlich der Anzahl an Klassen eine Verschiebung vom Paket [Link] zur
[Link]-Hierarchie, ohne jedoch alles vollständig in [Link] zu ersetzen.

Eine komplette Darstellung der zahlreichen Klassen in den Paketen [Link] und
[Link] würde den Rahmen dieses Buches sprengen. Wir beschränken uns an die-
ser Stelle daher darauf, anhand von bereits bekannten Projekten einige grund-
legende Beispiele aufzuzeigen. Sie sollten Ihnen eine ausreichende Grundlage liefern,

560
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.9 Dateibasierte Ein- und Ausgabe

um in Ihren eigenen Projekten mit Ein- und Ausgabe experimentieren zu können. Im


Einzelnen werden wir folgende typische Dateioperationen demonstrieren:
 Informationen über eine Datei aus dem Dateisystem ziehen
 Text mithilfe der Klasse FileWriter in eine Datei schreiben
 Text mithilfe der Klassen FileReader und BufferedReader aus einer Datei einlesen
 das Vorhersehen von IOExceptions
 Eingaben mit der Scanner-Klasse parsen
Außerdem wollen wir uns damit beschäftigen, wie Objekte binär in Dateien geschrie-
ben oder binär aus Dateien eingelesen werden können, und uns unter diesem As-
pekt die Java-Unterstützung für Serialisierung einmal näher ansehen.
Zum weiteren Einlesen in die Eingabe-/Ausgabe-Unterstützung von Java empfeh-
len wir das Tutorial von Oracle, das zu finden ist unter:
[Link]

14.9.1 Reader, Writer und Streams


Viele der Klassen im Paket [Link] fallen in eine von zwei Hauptkategorien: Klassen
für den Umgang mit Textdateien und Klassen für den Umgang mit Binärdateien. Wir
können uns Textdateien so vorstellen, dass sie Daten im Format des Java-Typs char
enthalten – typischerweise einfache, zeilenbasierte, menschenlesbare, alphanumeri-
sche Informationen. Webseiten in HTML sind ein besonderes Beispiel. Binärdateien
sind etwas vielfältiger: Bilddateien sind ein typisches Beispiel oder ausführbare Pro-
gramme wie ein Textverarbeitungsprogramm oder ein Media-Player. Klassen für den
Umgang mit Textdateien werden in Java Reader und Writer genannt, während Klas-
sen für Binärdateien mit sogenannten Streams (im Sinne von Datenströmen) umge-
hen.6 Hier werden wir ausschließlich Reader und Writer betrachten.

14.9.2 Die Klasse File und das Interface Path


Eine Datei ist mehr als nur ein Name und irgendein Inhalt. Eine Datei wird zum Bei-
spiel in einem bestimmten Ordner oder Verzeichnis auf einem bestimmten Laufwerk
gespeichert; außerdem haben verschiedene Betriebssysteme unterschiedliche Kon-
ventionen hinsichtlich der Zeichen, die in den Dateipfadnamen erlaubt sind. Die
Klasse File von [Link] ermöglicht es einem Programm, sich über eine externe Datei
zu informieren, und zwar unabhängig von dem Dateisystem, auf dem das Programm
ausgeführt wird. Durch die Erzeugung eines File-Objekts in einem Programm wird
keine Datei im Dateisystem angelegt. Vielmehr werden Informationen über eine exis-
tierende Datei in dem File-Objekt abgespeichert, vorausgesetzt die Datei existiert.
Das modernere Interface Path sowie die Klasse Files (man beachte den Plural)
aus [Link] erfüllen eine ähnliche Rolle für die alte File-Klasse. Da Path
jedoch ein Interface ist und keine Klasse, wird eine konkrete Instanz einer imple-
mentierenden Klasse mithilfe einer statischen get-Methode der Klasse Paths
(auch hier Plural) erzeugt, die sich ebenfalls in dem Paket [Link] befindet.

6 Es ist wichtig zu beachten, dass die Stream-Klasse zur Ein-/Ausgabe im Paket [Link] sich von
den Stream-Klassen im Paket [Link] unterscheidet.

561
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Wer mit altem Code arbeitet, kann mit der toPath-Methode von File ein äquiva-
lentes Path-Objekt aus einem File-Objekt erzeugen.
Wir können manchmal Situationen vermeiden, die eine Exception-Behandlung
erfordern, indem wir mit der Files-Klasse und einem Path-Objekt prüfen, ob eine
Datei existiert. Die Files-Klasse verfügt über die Methoden exists und isReadable,
die – sofern zuerst verwendet – das Öffnen einer Datei seltener fehlschlagen las-
sen. Da jedoch das Öffnen einer existierenden Datei immer noch voraussetzen
kann, dass eine Exception zu behandeln ist, machen sich viele Programmierer
nicht die Mühe, vorher zu prüfen.
Die Klasse Files bietet eine große Zahl von statischen Methoden, um die Attri-
bute eines Path-Objekts abzufragen.

Übung 14.41 Lesen Sie die API-Dokumentation der Klasse Files im Paket
[Link]. Welche Art von Information ist verfügbar über Objekte der
Klasse Files/Path?
Übung 14.42 Mit welchen Methoden der Files-Klasse können Sie feststellen,
ob ein Path-Objekt eine normale Datei oder ein Verzeichnis (Ordner) angibt?
Übung 14.43 Ist es möglich, irgendwelche Informationen über den Inhalt
einer Datei mithilfe der Files-Klasse zu erhalten?

14.9.3 Textausgaben
Zum Speichern von Daten in eine Datei sind drei Schritte nötig:
 die Datei öffnen
 die Daten schreiben
 die Datei schließen
Es liegt in der Natur von Dateiausgaben, dass jeder dieser Schritte fehlschlagen
kann, und zwar aus den unterschiedlichsten Gründen, die meist komplett außer-
halb des Einflussbereichs des Anwendungsentwicklers liegen. Deshalb muss bei
jedem Schritt damit gerechnet werden, dass eine Exception ausgelöst wird.
Um in eine Textdatei zu schreiben, wird üblicherweise ein Objekt der Klasse File-
Writer erzeugt, das als Konstruktorparameter den Namen der Zieldatei bekommt.
Der Dateiname kann entweder in Form eines String- oder eines File-Objekts ange-
geben werden. Die Erzeugung eines FileWriters führt dazu, dass die externe Datei
geöffnet und auf den Empfang von Ausgabedaten vorbereitet wird. Wenn das Öff-
nen aus irgendeinem Grund fehlschlägt, löst der Konstruktor eine IOException aus.
Gründe für ein Fehlschlagen können mangelnde Rechte im Dateisystem oder ein
ungültiger Pfad der Datei sein.
Wenn eine Datei erfolgreich geöffnet wurde, dann kann die Methode write eines
FileWriters für das Speichern von Zeichen – meist in Form von Strings – in die
Datei benutzt werden. Jeder Schreibversuch kann fehlschlagen, auch wenn die
Datei erfolgreich geöffnet wurde. Solche Fehlversuche sind selten, aber möglich.
Nachdem alle Daten geschrieben wurden, ist es wichtig, die Datei zu schließen. Dies
garantiert, dass die Daten auch wirklich in das externe Dateisystem geschrieben

562
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.9 Dateibasierte Ein- und Ausgabe

wurden, und führt üblicherweise auch zur Freigabe von internen und externen Res-
sourcen. Auch das Schließen der Datei kann in sehr seltenen Fällen fehlschlagen.
Das Grundmuster für den beschriebenen Ablauf sieht also etwa so aus:
FileWriter writer = new FileWriter(Dateiname);
while(noch Text zum Ausgeben vorhanden) {
...
[Link](nächsten Textabschnitt);
...
}
[Link]();
Das Hauptproblem besteht darin, die während dieser drei Schritte möglicherweise
auftretenden Exceptions angemessen behandeln zu können. Eine Exception, die
durch das Öffnen der Datei ausgelöst wird, ist die Einzige, die eventuell sinnvoll
behandelt werden kann, und das auch nur, wenn ein alternativer Dateiname
gewählt werden kann. Da dies üblicherweise erfordert, dass der Benutzer der
Anwendung eingreift, hängen die Chancen für eine solche Behandlung sehr stark
von der Anwendung und vom jeweiligen Kontext ab. Wenn das Schreiben in die
Datei fehlschlägt, dann ist es unwahrscheinlich, dass ein erneuter Versuch erfolg-
reich sein wird; das Gleiche gilt für das Schließen der Datei. Die Folge ist in der
Regel eine unvollständige Datei.
Ein praktisches Beispiel hierfür finden Sie in Listing 14.19. Die Klasse Logdatei-
Erzeuger im Projekt Weblog-Auswertung umfasst eine Methode, um eine Reihe
von zufälligen Logeinträgen in eine Datei zu schreiben, deren Name als Parameter
der Methode erzeugeDatei übergeben wird. Die Methode verwendet zwei verschie-
dene write-Methoden: eine zum Schreiben eines Strings und eine zum Schreiben
eines Zeichens. Nachdem wir den Text des Eintrags als String ausgegeben haben,
schreiben wir das Zeichen für den Zeilenumbruch in die Datei, damit jeder Eintrag
in einer separaten Zeile steht.

Listing 14.19
In eine Textdatei
schreiben.

563
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

14.9.4 Die try-Anweisung mit Ressourcen


Es ist wichtig, eine Datei nach Beendigung wieder zu schließen, und es gibt eine
Version der try-Anweisung, die die Aufgabe vereinfacht, dies sicherzustellen. Diese
Art der Anweisung wird try-Anweisung mit Ressourcen (try-with-resources) oder
automatisches Ressourcen-Management (ARM, Automatic Resource Management )
genannt. Listing 14.20 zeigt, wie sie in der Klasse LogdateiErzeuger eingesetzt wird,
die Sie im Projekt Weblog-Auswertung finden.

Listing 14.20
In eine Textdatei
schreiben mithilfe
einer try-Anweisung
mit Ressourcen.

Das Ressourcen-Objekt wird in einer neuen Klammer direkt im Anschluss an das


Schlüsselwort try erzeugt. In diesem Fall wird die Ressource einer Datei zugeord-
net. Sobald die try-Anweisung beendet ist, wird auf die Ressource automatisch
die Methode close aufgerufen. Diese Version der try-Anweisung eignet sich nur
für Objekte von Klassen, die das Interface AutoClosable aus dem Paket [Link]
implementieren. Das werden vornehmlich die Klassen sein, die mit Ein- und Aus-
gabe zu tun haben.

Übung 14.44 Überarbeiten Sie das Zuul-Projekt so, dass es die Benutzerein-
gaben protokolliert und in eine Textdatei schreibt (als Aufzeichnungsskript des
Spiels). Zur Lösung dieser Aufgabe sind mehrere Lösungsansätze denkbar:
 Sie könnten die Parser-Klasse so ändern, dass jede Eingabezeile in einer
Liste gespeichert wird, die dann am Ende des Spiels ausgegeben wird. Diese
Lösung kommt dem oben angesprochenen einfachen Programmiermuster
am nächsten.
 Sie könnten die gesamte Dateibehandlung in die Parser-Klasse integrieren,
sodass die gelesenen Zeilen ohne weitere Bearbeitung direkt in die Datei
geschrieben werden. Das Öffnen, Schreiben und Schließen der Datei wer-
den zeitlich voneinander getrennt (oder wäre es sinnvoll, die Datei für jede
zu schreibende Zeile zu öffnen, zu schreiben und zu schließen?)

564
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.9 Dateibasierte Ein- und Ausgabe

 Sie könnten die Behandlung der Datei in der Spiel-Klasse unterbringen und
in der Befehl-Klasse eine toString-Methode implementieren, die den für
jeden Befehl zu schreibenden String zurückgibt.
Bewerten Sie jede dieser möglichen Lösungen im Hinblick auf den Entwurf
nach Zuständigkeiten und die Exception-Behandlung und überlegen Sie, wel-
che Auswirkungen es für das Spielen hätte, wenn das Skript nicht geschrieben
werden könnte. Wie können Sie sicherstellen, dass die Skriptdatei immer am
Ende des Spiels geschlossen wird? Das ist wichtig, damit die Ausgabe auch
physikalisch in die Datei geschrieben wird.

14.9.5 Einlesen von Text


Das Gegenstück zur Textausgabe in eine Datei mit einem FileWriter ist das Einle-
sen mit einem FileReader. Wie leicht zu erraten ist, sind auch hier drei Schritte
notwendig: die Datei öffnen, die Daten einlesen, die Datei wieder schließen.
Während die natürlichen Einheiten zur Textausgabe Zeichen und Strings sind,
sind es beim Einlesen Zeichen und Textzeilen. Aber während die Klasse FileReader
eine Methode zum Einlesen eines einzelnen Zeichens anbietet,7 bietet sie keine
Methode zum Lesen einer Zeile. Das Problem beim Einlesen einer Zeile besteht
darin, dass es keine Obergrenze für ihre Länge gibt. Somit müsste jede Methode,
die die nächste komplette Zeile aus einer Datei liefern soll, eine beliebige Anzahl
an Zeichen einlesen können. Aus diesem Grund arbeiten wir üblicherweise mit
einem BufferedReader (also einen Reader mit einem expliziten Puffer), der eine
Methode readLine anbietet. Allerdings kann BufferedReader keine Dateien öff-
nen! Deshalb müssen wir zuerst ein FileReader erzeugen und es direkt in einem
BufferedReader-Objekt kapseln.
Ein BufferedReader wird mit der Methode newBufferedReader der Klasse Files
erzeugt. Zusätzlich zu einem Path-Parameter, der die zu öffnenden Datei angibt,
kommt erschwerend hinzu, dass ein Charset-Parameter erforderlich ist. Es wird benö-
tigt, um den Zeichensatz für die Zeichen in der Datei zu beschreiben. Charset ist im
Paket [Link] zu finden. Es gibt mehrere Standardzeichensätze, wie US-
ASCII und ISO-8859-1, und weitere Informationen hierzu sind in der API-Dokumenta-
tion zu Charset zu finden. Das Zeichen für das Zeilenende ist im Ergebnis der
Methode readLine nicht enthalten und ein Ergebniswert null signalisiert das Ende
der Datei.
Dies legt folgendes Programmiermuster für das Einlesen einer Datei nahe:
Charset zeichensatz = [Link]("ISO-8859-1");
Path pfad = [Link](Dateiname);
BufferedReader reader =
[Link](
pfad, zeichensatz);

7 Tatsächlich liefert die Methode read jedes Zeichen als einen int-Wert und nicht als char, weil
-1 als ein spezieller Wert zur Grenzüberschreitung das Ende der Datei signalisiert. Dies ist genau
die Art von Technik, die wir in Kapitel 14.3.2 beschrieben haben.

565
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

String zeile = [Link]();


while(zeile != null) {
tue etwas mit der Zeile
zeile = [Link]();
}
[Link]();
Listing 14.21 demonstriert, wie BufferedReader in dem Projekt Technischer-Kunden-
dienst aus Kapitel 6 dazu genutzt werden kann, die Standardantworten des Systems
aus einer Datei auszulesen, statt sie im Quelltext der Beantworter-Klasse hart fest-
legen zu müssen (Projekt Technischer-Kundendienst-IO im Ordner dieses Kapitels).

Listing 14.21
Aus einer
Textdatei mittles
BufferedReader lesen.

Wie schon bei der Dateiausgabe stellt sich die Frage, wie mit auftretenden Excep-
tions umgegangen werden soll. In diesem Beispiel haben wir eine Fehlermeldung
ausgegeben und dafür gesorgt, dass bei einem kompletten Fehlschlagen des Les-
evorgangs zumindest eine Antwort bereitgestellt wird.
Es gibt sogar einen noch einfacheren Weg, eine vollständige Textdatei einzule-
sen, ohne BufferedReader benutzen zu müssen. Die Methode lines der Files-
Klasse bekommt einen Path-Parameter und gibt den Inhalt der Datei in der Form

566
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.9 Dateibasierte Ein- und Ausgabe

Stream<String> zurück. Dies kann dann als Stream verarbeitet oder in ein String-
Array umgewandelt oder in einer ArrayList gespeichert werden.

Übung 14.45 Die Datei [Link] im Projektordner des Pro-


jekts Technischer-Kundendienst-IO enthält die Standardantworten, die von
der Methode standardantwortlisteFuellen eingelesen werden. Ändern Sie
mit einem beliebigen Texteditor den Inhalt dieser Datei so, dass sie eine
leere Zeile zwischen zwei Antworten enthält. Ändern Sie anschließend Ihren
Quelltext, damit er immer noch die Antworten korrekt aus dieser Datei liest.
Übung 14.46 Ändern Sie Ihren Quelltext so, dass mehrere Textzeilen, die in
der Datei nicht durch eine Leerzeile getrennt sind, als eine Antwort gelesen
werden. Ändern Sie die Datei [Link] so, dass es mehrere
Antworten enthält, die sich über mehrere Zeilen erstrecken.
Übung 14.47 Modifizieren Sie die Klasse Beantworter des Projekts Techni-
scher-Kundendienst-IO so, dass sie die Kombinationen von Schlüsselwörtern
und Antworten aus einer Textdatei liest, anstatt antworteMap mit Strings zu initi-
alisieren, die im Quelltext der Methode antworteMapBefuellen fest vorgegeben
sind. Sie können die Methode standardantwortenlisteFuellen als Muster neh-
men, müssen allerdings einige Änderungen an der Logik vornehmen, da für
jeden Eintrag zwei Strings statt einem gelesen werden müssen: das Schlüssel-
wort und die Antwort. Versuchen Sie, die Schlüsselwörter und die Antworten
in abwechselnden Zeilen zu speichern, wobei Sie den Text zu jeder Antwort in
einer Zeile halten sollten. Sie können voraussetzen, dass immer eine gerade
Anzahl Zeilen vorhanden ist (d.h. es gibt keine fehlenden Antworten).

14.9.6 Scanner: Eingaben parsen


Bisher haben wir Eingaben größtenteils als unstrukturierte Textzeilen behandelt, für
die sich BufferedReader am besten eignet. Viele Anwendungen gehen jedoch noch
einen Schritt weiter und zerlegen die einzelnen Zeilen in mehrere Komponenten, die
Datenwerte verschiedener Typen repräsentieren. Das CSV-Format (comma-sepa-
rated values) wird häufig verwendet, um Textdateien zu speichern, deren Zeilen
aus mehreren Werten bestehen, die jeweils vom Nachbarwert durch ein Komma
getrennt werden.8 Dadurch haben solche Textzeilen eine implizite Struktur. Diese
zugrunde liegende Struktur zu identifizieren, wird als Parsen bezeichnet – im Gegen-
satz zum Scannen, bei dem die einzelnen Zeichen zu getrennten Datenwerten
zusammengestellt werden.
Die Klasse Scanner im Paket [Link] wurde speziell dazu entworfen, Text zu scan-
nen und zusammengesetzte Zeichenfolgen in Werte eines Typs zu konvertieren, zum
Beispiel ganze Zahlen (nextInt) und Fließkommazahlen (nextDouble). Während ein
Scanner dazu verwendet werden kann, um String-Objekte zu zerlegen, wird er auch
sehr oft dazu genutzt, um den Inhalt von Dateien direkt zu lesen und umzuwandeln,
anstatt ein BufferedReader einzusetzen. Die dazugehörigen Konstruktoren können

8 In der Praxis kann jedes beliebige Zeichen anstelle eines Kommas als Trennzeichen verwendet
werden, häufig wird beispielsweise auch ein Tabulatorzeichen verwendet.

567
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

String-, File- oder auch Path-Argumente übernehmen. Listing 14.22 veranschau-


licht, wie eine Textdatei, deren Inhalt als ganze Zahlen interpretiert werden soll, auf
diese Weise gelesen werden könnte.

Listing 14.22
Ganze Zahlen mit
Scanner lesen.

Beachten Sie, dass diese Methode nicht garantiert, dass die Datei vollständig gelesen
wird. Die Scanner-Methode hasNextInt, die die Schleife steuert, liefert false zurück,
wenn sie in der Datei auf Text trifft, der nicht Teil einer ganzen Zahl zu sein scheint.
Damit wird das Sammeln der Daten abgebrochen. Tatsächlich ist es möglich und gar
nicht unüblich, beim Parsen einer Datei Aufrufe von verschiedenen next-Dateien zu
mischen, wenn die darin enthaltenen Daten aus gemischten Typen bestehen.
Eine weitere häufige Verwendung von Scanner ist es, Eingaben von der „Konsole“
einzulesen, die mit einem Programm verbunden ist. Wir haben regelmäßig die
Methoden print und println von [Link] verwendet, um Text auf der BlueJ-
Konsole auszugeben. [Link] ist vom Typ [Link] und leitet zu dem
um, was als Ziel häufig Standardausgabe (standard output) genannt wird. In ähn-
licher Weise existiert eine korrespondierende Standardeingabe (standard input ) als
[Link], die vom Typ [Link] ist. Ein InputStream wird üblicherweise
nicht direkt verwendet, wenn Eingaben vom Benutzer gelesen werden sollen, weil
er die Eingaben nur zeichenweise liefert. Stattdessen wird [Link] üblicherweise
an den Konstruktor der Klasse Scanner übergeben. Die Klasse Eingabeleser im Pro-
jekt Technischer-Kundendienst-komplett aus Kapitel 6 verwendet diesen Ansatz,
um Fragen des Benutzers einzulesen:
Scanner leser = new Scanner([Link]);
... Code hier ausgelassen ...
String eingabeZeile = [Link]();

568
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.9 Dateibasierte Ein- und Ausgabe

Die Methode nextLine der Klasse Scanner liefert die komplette nächste Eingabezeile
von der Standardeingabe (ohne das abschließende Zeichen für den Zeilenumbruch).

Übung 14.48 Sehen Sie sich die Klasse Eingabeleser aus Technischer-Kun-
dendienst-komplett noch einmal genau an und überprüfen Sie, ob Sie die
dortige Verwendung der Klasse Scanner verstehen.
Übung 14.49 Lesen Sie die Dokumentation der Klasse Scanner im Paket
[Link]. Welche next-Methoden außer den bisher in diesem Abschnitt
erwähnten bietet sie an?
Übung 14.50 Sehen Sie sich in der Klasse Parser im Projekt Zuul-besser noch
einmal an, wie sie ebenfalls einen Scanner verwendet. Sie tut dies auf eine
leicht andere Weise.
Übung 14.51 Sehen Sie sich die Klasse LogdateiZeilenzerleger von Weblog-
Auswertung noch einmal an, wie sie einen Scanner verwendet, um ganze
Zahlen aus den Logdateizeilen zu extrahieren.

14.9.7 Objektserialisierung
Vereinfacht gesagt, erlaubt Serialisierung das Schreiben ganzer Objekte in eine Konzept
externe Datei mit einer einzelnen Schreiboperation und das Wiedereinlesen zu
Serialisierung
einem späteren Zeitpunkt mit nur einer einzigen Leseoperation.9 Dies ist sowohl
erlaubt, ganze
mit einfachen als auch mit zusammengesetzten Objekten wie etwa Sammlungen Objekte und
möglich. Durch diesen mächtigen Mechanismus ist es beispielsweise nicht not- Objekthierarchien
wendig, Objekte einzeln Datenfeld für Datenfeld in eine Datei zu schreiben. Der in einer einzigen
Mechanismus ist für Anwendungen mit persistenten Daten wie Adressbücher Operation zu lesen
oder Media-Datenbanken besonders nützlich, da alle Einträge, die während einer und zu schreiben.
Sitzung erstellt wurden, gespeichert und in der nächsten Sitzung wiederverwen- Jedes beteiligte
Objekt muss dazu
det werden können. Natürlich könnten wir die Inhalte der Objekte auch als Text-
Instanz einer Klasse
strings schreiben, aber das Lesen der Texte, das Umwandeln der eingelesenen sein, die das Inter-
Informationen in die richtigen Typen und das Wiederherstellen des exakten Zustan- face Serializable
des eines komplexen Objektsatzes ist oft schwierig, wenn nicht sogar unmöglich. implementiert.
Die Objektserialisierung ist im Vergleich dazu viel zuverlässiger.
Damit eine Klasse bei der Serialisierung berücksichtigt werden kann, muss sie das
Interface Serializable aus dem Paket [Link] implementieren. Dabei ist interes-
sant, dass dieses Interface keine Methoden definiert. Der Prozess der Serialisierung
wird automatisch durch das Laufzeitsystem kontrolliert und erfordert nur sehr
wenig selbst programmierten Code. In unserem Adressbuch-IO-Projekt implemen-
tieren sowohl Adressbuch als auch Kontakt dieses Interface, sodass ihre Instanzen
per Serialisierung in einer Datei gespeichert werden können. Um zu veranschau-
lichen, wie eine Serialisierung implementiert wird, definiert die Klasse Adressbuch-
Dateiverwalter die Methoden speichereInDatei und liesAusDatei. Listing 14.23

9 Dies ist deshalb eine Vereinfachung, weil Objekte beispielsweise auch über eine Netzverbindung
geschrieben und gelesen werden können, nicht nur innerhalb eines externen Dateisystems.

569
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

enthält den Quelltext von speichereInDatei und zeigt, mit wie wenig Code sich das
ganze Adressbuch in einer einzigen write-Anweisung speichern lässt. Beachten Sie
auch, dass statt eines Writer-Objekts ein Stream-Objekt verwendet wird, weil wir
die Objekte in binärer Form schreiben. Die Klasse AdressbuchDateiverwalter enthält
noch weitere Beispiele für grundlegende Lese- und Schreibtechniken im Zusam-
menhang mit Textdateien, z.B. die Methoden speichereSuchergebnisse und zeige-
Suchergebnisse.

Listing 14.23
Serialisierung eines
kompletten Adress-
buch-Objekts mit
allen Kontaktdaten.

Übung 14.52 Ändern Sie das Projekt Netzwerk aus Kapitel 11 so, dass die
Daten in einer Datei gespeichert werden können. Verwenden Sie Objektseria-
lisierung. Welche Klassen müssen Sie als serialisierbar deklarieren?
Übung 14.53 Was passiert, wenn Sie die Definition einer Klasse um, sagen wir,
ein zusätzliches Datenfeld ändern und dann versuchen, serialisierte Objekte
wieder einzulesen, die noch mit der alten Version der Klasse erzeugt wurden?

570
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.9 Dateibasierte Ein- und Ausgabe

Zusammenfassung
Wenn zwei Objekte miteinander interagieren, dann kann es dabei aus den
verschiedensten Gründen zu Problemen kommen. Beispielsweise:
 Der Programmierer eines Klienten hat ein falsches Verständnis vom Zustand
oder von der Leistungsfähigkeit des Dienstleisters.
 Der Dienstleister kann eine Anfrage eines Klienten aufgrund von äußeren
Umständen nicht erfüllen.
 Ein Klient wurde falsch programmiert und übergibt deshalb ungültige
Parameterwerte an eine Methode des Dienstleisters.
Wenn etwas schiefgeht, dann wird das Programm entweder vorzeitig abbre-
chen oder es produziert falsche und unerwünschte Ergebnisse. Wir können
viele dieser Probleme vermeiden, indem wir einen Exception-Mechanismus
benutzen. Ein solcher Mechanismus gibt einem Dienstleister ein klar definier-
tes Mittel an die Hand, um einen Klienten über ein Problem zu informieren.
Exceptions verhindern außerdem, dass ein Klient dieses Problem einfach igno-
riert, und fordern Programmierer explizit dazu auf, einen alternativen Ausfüh-
rungspfad für den Fehlerfall zu definieren.
Beim Entwickeln einer Klasse können assert-Anweisungen eingebaut wer-
den, um interne Konsistenzprüfungen vorzunehmen. Diese Anweisungen
werden normalerweise aus dem Produktionscode wieder entfernt.
Die Ein- und Ausgabe ist ein Bereich, in dem Exceptions häufiger auftreten.
Das liegt vornehmlich daran, dass die Programmierer nur geringen (oder
keinen) Einfluss auf die Umgebungen haben, in denen ihre Programme aus-
geführt werden. Außerdem spiegelt es die Komplexität und Diversität dieser
Umgebungen wider.
Die Java API unterstützt die Ein-/Ausgabe von Text- und Binärdaten mithilfe der
Klassen für Reader, Writer und Streams. Neuere Java-Versionen haben die
Pakete [Link] eingeführt, die einige der älteren [Link]-Klassen ersetzt haben.

NEUE BEGRIFFE IN DIESEM KAPITEL


Exception, ungeprüfte Exception, geprüfte Exception, Exception-Handler,
Zusicherung, Serialisierung

571
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung

Zusammenfassung der Konzepte


 Exception Eine Exception ist ein Objekt, das Informationen über einen
Programmfehler hält. Eine Exception wird ausgelöst, um zu signalisieren,
dass ein Fehler aufgetreten ist.
 Ungeprüfte Exception Ungeprüfte Exceptions sind Exception-Typen, bei
deren Verwendung der Compiler keine zusätzlichen Überprüfungen vor-
nimmt.
 Geprüfte Exception Geprüfte Exceptions sind Exception-Typen, bei
deren Verwendung der Compiler zusätzliche Überprüfungen durchführt
und einfordert. Insbesondere erfordern geprüfte Exceptions in Java, dass
throws-Klauseln und try-Blöcke angegeben werden müssen.
 Exception-Handler Ein Programmabschnitt, der Anweisungen schützt,
in denen eine Exception ausgelöst werden kann, wird Exception-Handler
genannt. Er definiert Anweisungen zur Meldung und/oder zum Wieder-
aufsetzen nach einer aufgetretenen Exception.
 Zusicherung Eine Tatsachenaussage, die bei normaler Programmausfüh-
rung zutreffen sollte. Zu verwenden, um Annahmen zu formulieren und Pro-
grammierfehler leichter zu entdecken.
 Serialisierung Serialisierung erlaubt, ganze Objekte und Objekthierar-
chien in einer einzigen Operation zu lesen und zu schreiben. Jedes betei-
ligte Objekt muss dazu Instanz einer Klasse sein, die das Interface Seria-
lizable implementiert.

Übung 14.54 Ergänzen Sie das Projekt Adressbuch um die Fähigkeit, meh-
rere E-Mail-Adressen in einem Kontakt-Objekt zu speichern. Alle diese E-Mail-
Adressen sollten gültige Schlüssel sein. Verwenden Sie auf jeder Stufe dieses
Prozesses Zusicherungen und JUnit-Tests, um maximales Vertrauen in die
endgültige Version zu schaffen.

572
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

15 Entwurf von Anwendungen

Lernziele
Zentrale Konzepte in diesem Kapitel: Identifizieren von Klassen, Identifi-
zieren von Klassen, CRC-Karten, Entwurfsmuster
Java-Konstrukte in diesem Kapitel: (In diesem Kapitel werden keine neuen
Java-Konstrukte vorgestellt.)

In den vorangegangenen Kapiteln haben wir beschrieben, wie man gute Klassen
schreibt. Wir haben diskutiert, wie man sie entwirft, wie man sie wartbar und robust
macht und wie man sie miteinander interagieren lässt. All das ist wichtig, aber etwas
sehr Wichtiges fehlt noch: das Finden der geeigneten Klassen.
In allen vorangegangenen Beispielen haben wir angenommen, dass wir mehr oder
weniger wissen, welches die richtigen Klassen für unsere Problemlösungen sind. In
einem realen Softwareprojekt kann das Finden der richtigen Klassen jedoch eines
der schwierigsten Probleme sein. Diesen Aspekt der Softwareentwicklung wollen
wir in diesem Kapitel näher betrachten.
Die frühen Phasen bei der Entwicklung eines Softwaresystems werden im Allge-
meinen Analyse und Entwurf genannt. Wir analysieren das Problem, dann entwer-
fen wir eine Lösung. Der erste Entwurf wird dabei meist auf einer allgemeineren
Ebene formuliert, als wir es in Kapitel 8 diskutiert haben. Wir werden zuerst überle-
gen, welche Klassen zur Problemlösung zu entwerfen sind und wie diese interagie-
ren sollen. Sobald wir diesen Schritt abgeschlossen haben, können wir uns dem
Entwurf der einzelnen Klassen und ihren Implementierungen zuwenden.

15.1 Analyse und Entwurf


Analyse und Entwurf von Softwaresystemen sind umfangreiche und komplexe
Themengebiete. Ihre detaillierte Diskussion würde den Rahmen dieses Buches
sprengen. Zahlreiche Methodiken sind in der Literatur beschrieben und werden
in der Praxis eingesetzt. In diesem Kapitel wollen wir lediglich versuchen, einen
Eindruck der mit diesen Tätigkeiten verbundenen Probleme zu geben.

573
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

Wir werden dazu eine sehr einfache Methode verwenden, die ihren Zweck für ver-
gleichsweise kleine Probleme gut erfüllt. Um die ersten Klassen zu finden, werden
wir die Verb/Substantiv-Methode verwenden. Anschließend werden wir CRC-Kar-
ten benutzen, um einen ersten Entwurf vorzunehmen.

15.1.1 Die Verb/Substantiv-Methode


Konzept Bei dieser Methode geht es ausschließlich darum, die Klassen und Objekte für einen
Problembereich sowie ihre Verknüpfungen und Interaktionen zu identifizieren. Die
Bei der Verb/
Substantive in der natürlichen Sprache beschreiben üblicherweise Dinge wie Men-
Substantiv-
Methode korres- schen, Gebäude und so weiter. Die Verben beschreiben Aktionen wie schreiben,
pondieren Klassen essen und so weiter. Aus diesen Konzepten der natürlichen Sprache können wir
in einem System in darauf schließen, dass bei einem Programmierproblem die Substantive oft mit den
etwa mit den Klassen und Objekten korrespondieren, während die Verben oft mit den Dingen
Substantiven in der korrespondieren, die diese Objekte tun: also ihren Methoden. Wir brauchen keine
Systembeschrei- sehr lange Beschreibung, um diese Technik zu illustrieren. Es reicht üblicherweise
bung. Die Metho-
eine Darstellung in ein paar Absätzen.
den korrespondie-
ren mit den Verben. Das Beispiel, an dem wir diesen Entwurfprozess verdeutlichen wollen, ist ein
Buchungssystem für ein Kino.

15.1.2 Das Beispiel: ein Kinobuchungssystem


Diesmal werden wir nicht mit einem vorgegebenen System beginnen. Wir nehmen
an, dass wir uns in einer Situation befinden, in der wir eine Anwendung von Grund
auf neu entwerfen müssen. Die Aufgabe ist, für ein Kinounternehmen ein System
zu entwickeln, mit dem die Plätze für Kinovorstellungen gebucht werden können.
Kinobesucher rufen häufig im Voraus an, um Plätze zu reservieren. Die Anwen-
dung sollte dann in der Lage sein, die freien Plätze zu finden und für den Besucher
zu reservieren.
Wir nehmen an, dass wir schon einige Treffen mit den Betreibern des Kinounterneh-
mens hatten, bei denen sie uns die Funktionalität beschrieben haben, die sie von
dem System erwarten. (Die erwartete Funktionalität zu verstehen, sie zu beschreiben
und diese Beschreibung mit dem Kunden abzustimmen, ist üblicherweise bereits ein
Problem für sich. Diese Thematik geht jedoch weit über den Anspruch dieses Buches
hinaus und kann in anderen Kursen und Büchern näher untersucht werden.)
Hier ist die Beschreibung, die wir für das Kinobuchungssystem formuliert haben:
Das Kinobuchungssystem sollte Platzreservierungen für mehrere Kinosäle ver-
walten. Jeder Kinosaal hat Plätze, die in Reihen angeordnet sind. Kinobesucher
können Plätze reservieren und bekommen eine Reihen- und eine Platznummer
zugewiesen. Sie können nach nebeneinanderliegenden Plätzen fragen.
Jede Platzreservierung gilt für eine bestimmte Vorstellung (also einen be-
stimmten Film zu einem bestimmten Zeitpunkt). Vorstellungen finden zu fest-
gelegten Zeitpunkten an festgelegten Tagen statt und sind einem Kinosaal zu-
geteilt, in dem sie gezeigt werden. Das System speichert die Telefonnummer
des Kinobesuchers.

574
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.1 Analyse und Entwurf

Anhand dieses mehr oder weniger klar formulierten Textes können wir einen ers-
ten Versuch vornehmen, auf Basis der Substantive und Verben Klassen und Metho-
den zu identifizieren.

15.1.3 Identifizieren von Klassen


Der erste Schritt beim Finden der Klassen besteht darin, dass wir durch die Beschrei-
bung gehen und alle Substantive und Verben im Text markieren. Wenn wir dies tun,
finden wir die folgenden Substantive und Verben (die Substantive sind in der Reihen-
folge ihres Auftretens aufgeführt, die Verben sind den Substantiven zugeordnet, auf
die sie sich beziehen):

Substantive: Verben:
Kinobuchungssystem verwaltet (Platzreservierungen)
speichert (Telefonnummer)

Platzreservierung hat (Plätze)


Kinosaal

Platz
Reihe
Kinobesucher reserviert (Plätze)
bekommt zugewiesen (Reihennummer, Platznummer)
fragt nach (nebeneinanderliegenden Plätzen)

Reihennummer
Platznummer
Vorstellung wird zugeteilt (einem Kinosaal)

Film
Zeitpunkt
Tag
Telefonnummer

Die Substantive, die wir gefunden haben, geben uns einen ersten Hinweis auf die
Klassen in unserer Anwendung. Als erste Näherung können wir eine Klasse für jedes
Substantiv formulieren. Das ist kein exaktes Vorgehen – wir können später feststel-
len, dass wir noch zusätzliche Klassen benötigen oder einige der Substantive nicht
benötigt werden. Das werden wir jedoch erst später herausfinden. Es ist auf jeden
Fall wichtig, keine Substantive von Anfang an auszusortieren – wir haben noch nicht
genügend Informationen, um eine fundierte Entscheidung treffen zu können.

575
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

Wenn wir diese Übung mit unseren Studenten machen, lassen einige Studenten
sofort einige der Substantive weg. Zum Beispiel verzichtet einer den Studenten auf
das Substantiv Reihe aus der obigen Beschreibung, weil es seiner Meinung nach
nur eine Zahl ist, für das ein int reicht und keine Klasse benötigt wird. Auf dieser
Stufe ist es wirklich wichtig, nicht den gleichen Fehler zu machen. Wir haben zu
diesem Zeitpunkt einfach noch nicht genug Informationen, um zu entscheiden, ob
Reihe ein int oder eine Klasse sein sollte. Diese Entscheidung können wir erst viel
später treffen. Zurzeit gehen wir einfach nur mechanisch die Absätze durch und
schreiben alle Substantive auf. Wir entscheiden noch nicht, welche Substantive
„gut“ sind und welche nicht.
Sie haben möglicherweise festgestellt, dass alle Substantive im Singular aufge-
führt sind. Es ist bei Klassen typisch, dass ihre Namen im Singular und nicht im
Plural formuliert sind. Beispielsweise würden wir eine Klasse immer eher Kino als
Kinos nennen, denn die Vervielfachung geschieht über das Erzeugen von Instan-
zen einer Klasse.

Übung 15.1 Denken Sie an die Projekte aus den vorherigen Kapiteln zurück.
Erinnern Sie sich an Fälle, in denen ein Klassenname im Plural definiert war?
Falls ja: Gab es in diesen Fällen gute Gründe für diese Entscheidung?

15.1.4 CRC-Karten
Der nächste Schritt in unserem Entwurfsprozess besteht darin, die Interaktionen
zwischen unseren Klassen herauszuarbeiten. Dazu verwenden wir CRC-Karten.1
CRC steht für Class/Responsibilities/Collaborators (Klasse/Zuständigkeiten/Partner-
klassen). Die Idee ist, für jede Klasse eine Pappkarte (normale Karteikarten sind
gut geeignet) zu verwenden. Wichtig ist, dass tatsächlich für jede Klasse eine
eigenständige, anfassbare Karte angelegt wird und nicht eine Computerdarstel-
lung oder ein einzelnes Blatt Papier für alle. Jede Karte wird in drei Bereiche
unterteilt: ein Bereich oben links, in den der Name der Klasse geschrieben wird;
ein Bereich darunter, in dem die Zuständigkeiten der Klasse eingetragen werden;
und ein Bereich rechts, in dem die Partnerklassen eingetragen werden (Klassen,
mit denen diese Klasse kooperiert). Abbildung 15.1 illustriert das Layout einer CRC-
Karte.

Übung 15.2 Erstellen Sie CRC-Karten für die Klassen im Kinobuchungssys-


tem. An dieser Stelle müssen Sie lediglich die Klassennamen eintragen.

1 CRC-Karten wurden erstmals in dem Artikel „A Laboratory For Teaching Object-Oriented Think-
ing“ von Kent Beck und Ward Cunningham erwähnt. Dieser Artikel ist als Ergänzung zu diesem
Kapitel lesenswert. Sie können ihn online unter [Link] finden
oder eine Websuche nach dem Titel vornehmen.

576
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.1 Analyse und Entwurf

Abbildung 15.1
Aufbau einer
CRC-Karte.

15.1.5 Szenarios
Wir haben nun eine erste Näherung für die Klassen in unserem System und eine Konzept
physische Repräsentation auf CRC-Karten. Um nun die notwendigen Interaktio-
Szenarios (auch
nen zwischen den Klassen in unserem System herauszufinden, spielen wir einige
als „Geschäfts-
Szenarios durch. Ein Szenario ist ein Beispiel für eine Aktivität, die das System fälle“ oder unter
ausführen oder unterstützen muss. Szenarios werden häufig auch als Geschäfts- dem englischen
fälle (use case) bezeichnet. Wir benutzen diesen Begriff hier nicht, da er häufig Begriff use cases
eine formalere Art zur Beschreibung von Szenarios bezeichnet. bekannt) können
benutzt werden,
Szenarios lassen sich am besten in einer Gruppe durchspielen. Jedes Gruppenmit- um ein Verständnis
glied bekommt eine Klasse (oder eine kleine Anzahl an Klassen) zugewiesen und von den Interaktio-
spielt die Rolle dieser Klasse, indem es laut ausspricht, was diese Klasse gerade tut. nen in einem Sys-
Während ein Szenario durchgespielt wird, halten die Teilnehmer auf den CRC-Kar- tem zu bekommen.
ten alles fest, was die gerade agierende Klasse betrifft: wofür sie zuständig sein soll
und mit welchen Klassen sie kooperieren muss.
Wir beginnen mit einem simplen Beispielszenario:
Ein Kunde ruft beim Kino an und möchte zwei Plätze für Die Verurteilten heute
Abend buchen. Der Kinoangestellte versucht nun mit dem Buchungssystem,
zwei Plätze zu finden und zu reservieren.
Da ein menschlicher Benutzer mit dem System interagiert (das durch die Klasse
Kinobuchungssystem repräsentiert wird), fangen wir hier mit dem Szenario an. Als
Nächstes könnte Folgendes passieren:
 Der Benutzer (der Kinoangestellte) möchte alle Vorstellungen von Die Verur-
teilten finden, die heute angesetzt sind. Also können wir auf der CRC-Karte
Kinobuchungssystem als Zuständigkeit eintragen: kann Vorstellungen nach Film-
titel und Tag finden. Wir können auch die Klasse Vorstellung als Partnerklasse
eintragen.
 Wir müssen uns fragen: Wie findet das System die Vorstellung? Wen fragt es
dazu? Eine Lösung könnte sein, dass das Kinobuchungssystem eine Sammlung von
Vorstellungen hält. Das führt uns zu einer weiteren Klasse: der Sammlung. (Diese
könnte später durch eine ArrayList, eine LinkedList, ein HashSet oder eine andere
Sammlung implementiert werden, mit zusätzlichen Methoden, die zu einer Vor-
stellung passen. Diese Entscheidung können wir später treffen – momentan notie-
ren wir nur eine VorstellungSammlung.) Dies ist ein Beispiel dafür, dass wir beim
Durchspielen von Szenarios weitere Klassen entdecken können. Es kann immer

577
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

wieder passieren, dass wir Klassen aus Implementierungsgründen hinzufügen


müssen, die wir bis dahin übersehen haben. Wir fügen also bei den Zuständig-
keiten auf der Karte für das Kinobuchungssystem ein: speichert eine Sammlung von
Vorstellungen. Und VorstellungSammlung wird als Partnerklasse eingetragen.

Übung 15.3 Erstellen Sie eine CRC-Karte für die neu erkannte Klasse Vor-
stellungSammlung und fügen Sie diese in Ihr System ein.

 Wir nehmen an, dass es heute drei Vorstellungen gibt: eine um 17:30 Uhr, eine
um 21:00 Uhr und eine um 23:30 Uhr. Der Angestellte teilt dem Kunden diese
Zeiten mit und der Kunde wählt die Vorstellung um 21:00 Uhr. Der Angestellte
möchte deshalb als Nächstes Informationen zu dieser Vorstellung abrufen (ob
sie ausverkauft ist, in welchem Kinosaal sie läuft etc.). Also muss unser System
in der Lage sein, die Details einer Vorstellung abzurufen und anzuzeigen. Spie-
len Sie dies durch. Die Person, die das Buchungssystem spielt, sollte die Person,
die die Vorstellungen repräsentiert, nach den benötigten Details fragen. Darauf-
hin notieren Sie auf der Karte für das Kinobuchungssystem: Ruft Details einer Vor-
stellung ab und zeigt sie an, und auf der Karte Vorstellung: Liefert Informatio-
nen über Kinosaal und Anzahl der freien Plätze.
 Nehmen wir an, dass es reichlich freie Plätze gibt. Der Kunde wählt die Plätze 13
und 14 in Reihe 12. Der Angestellte nimmt die Buchung vor. Wir notieren auf
der Karte Kinobuchungssystem: Akzeptiert Platzreservierungen vom Benutzer.
 Wir müssen nun durchspielen, wie eine Platzreservierung ablaufen soll. Eine
Platzreservierung ist sicherlich an eine Vorstellung gebunden. Das Kinobuchungs-
system sollte also die Vorstellung über die Reservierung informieren. Es delegiert
das tatsächliche Durchführen der Reservierung an das Objekt Vorstellung. Wir
können also für Vorstellung notieren: Kann Plätze reservieren. (Sie haben viel-
leicht schon festgestellt, dass die Grenze zwischen Klasse und Objekt sehr leicht
verschwimmt, wenn man mit CRC-Karten umgeht; letztlich repräsentiert die
Person, die eine Klasse repräsentiert, auch die Instanzen dieser Klasse. Das ist
absichtlich so und im Allgemeinen auch kein Problem.)
 Jetzt sind wir bei der Klasse Vorstellung. Sie hat die Anfrage bekommen, einen
Platz zu reservieren. Wie geht sie mit dieser Anfrage um? Um Platzreservierun-
gen vornehmen zu können, muss sie auf eine Repräsentation der Plätze in
einem Kinosaal zugreifen können. Also nehmen wir an, dass eine Vorstellung
eine Verbindung zu einem Kinosaal-Objekt hat. (Notieren Sie dies auf der
Karte: Speichert Kinosaal. Diese Klasse wird damit auch zur Partnerklasse.) Der
Kinosaal sollte vermutlich die exakte Anzahl und Anordnung seiner Plätze
kennen. (Wir können an dieser Stelle im Hinterkopf behalten oder auf einem
Blatt Papier notieren, dass jede Vorstellung eine eigene Kopie eines Kinosaals
haben sollte, damit eine Reservierung für eine Vorstellung nicht auch einen
Platz in einer anderen Vorstellung blockiert. Darauf sollten wir achten, wenn
eine Vorstellung erzeugt wird. Wir werden darüber später noch einmal nach-
denken, wenn wir ein anderes Szenario durchspielen: das Ansetzen neuer Vor-

578
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.1 Analyse und Entwurf

stellungen.) Vermutlich wird also eine Vorstellung die Reservierung wiederum


an einen Kinosaal delegieren.
 Der Kinosaal hat nun die Anfrage bekommen, einen Platz zu reservieren. (Notie-
ren Sie dies auf der Karte: Akzeptiert Reservierungsanfrage.) Wie geht er damit
um? Der Kinosaal könnte eine Sammlung seiner Plätze halten. Oder er könnte
eine Sammlung von Reihen halten (und jede Reihe ist ein eigenes Objekt), und
Reihen wiederum halten Plätze. Welche dieser Alternativen ist besser? Wenn
wir bereits einige andere Szenarios im Auge haben, dann entscheiden wir uns
vermutlich für den Ansatz mit den Reihen. Wenn ein Kunde beispielsweise vier
Plätze nebeneinander in derselben Reihe reservieren möchte, dann könnten
benachbarte Plätze einfacher zu finden sein, wenn sie alle in einer Reihe organi-
siert sind. Wir notieren also auf der Karte Kinosaal: Speichert Reihen. Und Reihe
wird damit zur Partnerklasse.
 Wir notieren auf der Karte Reihe: Speichert Sammlung von Plätzen. Und als
neue Partnerklasse: Platz.
 Zurück zur Klasse Kinosaal. Wir haben noch nicht entschieden, wie sie mit der
Reservierungsanfrage umgehen soll. Nehmen wir an, dass sie zwei Dinge tut:
Sie findet die gewünschte Reihe und stellt an das Reihe-Objekt die Anfrage,
die gewünschten Plätze zu reservieren.
 Als Nächstes notieren wir auf der Karte Reihe: Akzeptiert Reservierungsanfrage
für einen Platz. Es muss dazu das entsprechende Platz-Objekt finden (wir können
als Zuständigkeit notieren: Kann Plätze über ihre Nummer finden) und kann eine
Reservierung dieses Platzes vornehmen. Es wird dies tun, indem es dem Platz-
Objekt mitteilt, dass es nun reserviert ist.
 Wir können nun auf der Karte Platz notieren: Akzeptiert Reservierungen. Der
Platz selbst kann sich merken, dass er reserviert worden ist. Wir notieren auf
der Karte Platz: Speichert Reservierungszustand (frei/reserviert).

Übung 15.4 Spielen Sie dieses Szenario auf Ihren Karten durch (mit einer
Gruppe von Personen, falls möglich). Fügen Sie weitere Informationen hinzu,
die Ihrer Meinung nach bisher ausgelassen wurden.

Sollte ein Platz auch speichern, wer ihn reserviert hat? Er könnte den Namen des
Kunden oder seine Telefonnummer speichern. Oder vielleicht sollten wir ein Kunden-
Objekt erzeugen, sobald jemand eine Reservierung vornimmt, und eine Referenz auf
dieses Kunden-Objekt im reservierten Platz speichern? Das sind spannende Fragen
und wir werden versuchen, die beste Antwort über das Durchspielen von Szenarios
zu finden.
Dies war nur ein erstes, einfaches Szenario. Wir müssen noch viele weitere durch-
spielen, bevor wir ein angemessenes Verständnis vom gewünschten System bekom-
men.
Das Durchspielen von Szenarios funktioniert am besten, wenn eine Gruppe von
Personen um einen Tisch herumsitzt und die Karten nach Bedarf verschoben

579
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

werden. Karten, die eng zusammenarbeiten, können auch räumlich enger ange-
ordnet werden, um die Kopplung im System zu visualisieren.
Weitere durchzuspielende Szenarios würden unter anderem die folgenden sein:
 Ein Kunde möchte fünf Plätze nebeneinander reservieren. Spielen Sie möglichst
exakt durch, wie diese fünf Plätze gefunden werden.
 Ein Kunde ruft an und teilt mit, dass er die Platznummern, die er gestern reser-
vieren ließ, vergessen hat. Könnten Sie die Platznummern bitte noch einmal her-
ausfinden?
 Ein Kunde ruft an und möchte seine Reservierung stornieren. Er kann seinen
Namen und die Vorstellung angeben, aber er hat die Platznummern vergessen.
 Eine Kundin, die bereits reserviert hat, ruft an. Sie möchte wissen, ob sie einen
weiteren Platz direkt neben ihren bereits reservierten Plätzen bekommen kann.
 Eine Vorstellung wird abgesagt. Das Kino möchte alle Kunden anrufen, die für
diese Vorstellung reserviert haben.
Diese Szenarios sollten Ihnen ein gutes Verständnis von dem Teil des Systems ver-
mitteln, das mit Platzsuche und -reservierung zu tun hat. Wir brauchen noch eine
weitere Gruppe von Szenarios: solche, die mit dem Aufbau der Kinosäle und
dem Ansetzen von Vorstellungen zu tun haben. Einige mögliche wären:
 Das System soll für ein neues Kino eingerichtet werden. Das Kino hat zwei Kino-
säle verschiedener Größe. Saal A hat 26 Reihen mit jeweils 18 Plätzen. Saal B
hat 32 Reihen, wobei die ersten sechs Reihen 20 Plätze haben, die nächsten 10
Reihen haben 22 Plätze und die restlichen Reihen 26 Plätze.
 Ein neuer Film kommt in die Kinos. Er wird in den nächsten zwei Wochen drei-
mal täglich gezeigt (um 16:40 Uhr, um 18:30 Uhr und um 20:30 Uhr). Die Vor-
stellungen müssen neu in das System eingefügt werden. Alle Vorstellungen lau-
fen im Kinosaal A.

Übung 15.5 Spielen Sie diese Szenarios durch. Notieren Sie alle unbeant-
worteten Fragen auf einem getrennten Blatt Papier. Protokollieren Sie, wel-
che Szenarios Sie bereits durchgespielt haben.
Übung 15.6 Welche weiteren Szenarios können Sie sich vorstellen? Schrei-
ben Sie diese auf und spielen Sie sie durch.

Das Durchspielen von Szenarios erfordert einige Geduld und auch Übung. Sie sollten
sich dafür genügend Zeit nehmen. Die hier genannten Szenarios können mehrere
Stunden dauern.
Neulinge mit dieser Technik neigen häufig zu Abkürzungen, indem sie nicht jedes
Detail während des Durchspielens hinterfragen oder nicht ausreichend festhal-
ten. Das ist gefährlich! Wir werden sehr bald dazu übergehen, dieses System in
Java zu implementieren, und wenn Details offengeblieben sind, dann besteht die
Gefahr, dass bei der Implementierung Entscheidungen ad hoc getroffen werden,
die sich später als schlecht erweisen könnten.

580
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.2 Klassenentwurf

Neulinge neigen auch dazu, Szenarios zu vergessen. Wenn Teile nicht genügend
durchdacht sind, bevor mit dem Entwurf und der Implementierung von Klassen
begonnen wird, kann der Aufwand sehr groß werden, wenn bereits implemen-
tierte Teile wieder geändert werden müssen.

Die Szenarios gut durchzuspielen, sorgfältig alle notwendigen Schritte durchzu-


gehen und die Schritte ausführlich festzuhalten, bedarf einiger Übung und einer
großen Disziplin. Diese Aufgabe ist schwerer, als sie aussieht, und wichtiger, als
Ihnen vielleicht zurzeit klar ist.

Übung 15.7 Machen Sie einen Entwurf für die Simulation eines Flughafenkon-
trollsystems. Verwenden Sie CRC-Karten und Szenarios. Hier ist die Beschrei-
bung des Systems:
 Das Programm ist eine Simulation eines Flughafens. Für einen neu zu bau-
enden Flughafen müssen wir herausfinden, ob wir mit zwei Rollfeldern aus-
kommen oder ob wir drei benötigen. Der Flughafen funktioniert folgender-
maßen:
 Es gibt mehrere Rollfelder. Flugzeuge starten von und landen auf diesen
Rollfeldern. Fluglotsen kontrollieren den Verkehr und geben Flugzeugen die
Erlaubnis, zu landen oder zu starten. Die Lotsen geben teilweise die Erlaub-
nis unmittelbar, teilweise lassen sie Flugzeuge warten. Flugzeuge müssen
einen Mindestabstand zueinander einhalten. Der Zweck des Programms ist,
den Flughafen im Betrieb zu simulieren.

15.2 Klassenentwurf
Jetzt ist der nächste größere Schritt zu machen: von den CRC-Karten zu Java-Klas-
sen. Während der Übungen mit den CRC-Karten sollten Sie ein gutes Verständnis
von der Struktur Ihrer Anwendung bekommen haben und davon, wie die Klassen
zur Erfüllung der Aufgaben des Systems zusammenarbeiten. Möglicherweise hat-
ten Sie Situationen, in denen neue Klassen eingeführt werden mussten (das gilt
meist für Klassen, die interne Datenstrukturen repräsentieren), und möglicherweise
hatten Sie auch Karten für Klassen, die gar nicht benutzt wurden. Wenn das der
Fall war, können Sie diese Karten nun entfernen.

Das Erkennen der Klassen für die Implementierung ist nun trivial: Die Karten zei-
gen uns den kompletten Satz an Klassen, den wir benötigen. Das Festlegen der
Schnittstellen dieser Klassen (also ihrer öffentlichen Methoden) ist etwas schwie-
riger, aber auch dafür haben wir bereits einen wichtigen Schritt getan. Wenn das
Durchspielen der Szenarios gut gelaufen ist, dann sollten die Zuständigkeiten, die
für die Klassen notiert wurden, mehr oder weniger ihre öffentlichen Methoden
beschreiben (und möglicherweise auch einige ihrer Datenfelder). Die Zuständig-
keiten jeder Klasse sollten nach den Prinzipien aus Kapitel 8 ausgewertet werden:
Entwurf nach Zuständigkeiten, Kopplung und Kohäsion.

581
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

15.2.1 Entwurf von Klassenschnittstellen


Bevor wir unsere Anwendung in Java implementieren, können wir die Karten noch
ein weiteres Mal benutzen, um durch ein Übersetzen der informellen Beschreibun-
gen in Methoden mit Parametern dem endgültigen Entwurf näher zu kommen.
Um zu einer formaleren Beschreibung zu kommen, können wir die Szenarios
noch einmal durchspielen, diesmal aber unter Benutzung der Begriffe Methoden-
aufruf, Parameter und Ergebniswerte. Die Logik und der Ablauf der Anwendung
sollten sich nicht mehr ändern; wir versuchen nun, die Methodensignaturen und
Instanzfelder komplett zu definieren. Dies tun wir mit einem neuen Satz Karten.

Übung 15.8 Erstellen Sie einen neuen Satz CRC-Karten für die Klassen, die
Sie identifiziert haben. Spielen Sie die Szenarios erneut durch. Notieren Sie
diesmal exakte Methodennamen für alle Aufrufe anderer Klassen und geben
Sie alle Details (Name und Typ) aller Parameter, die übergeben werden, sowie
der Ergebniswerte an. Die Methodensignaturen werden anstelle der Zustän-
digkeiten auf die CRC-Karten geschrieben. Auf der Rückseite einer Karte kön-
nen Sie die Datenfelder notieren, die eine Klasse haben soll.

Sobald wir die oben beschriebene Übung durchgeführt haben, ist das Erstellen der
Klassenschnittstellen einfach. Wir können direkt von den Karten nach Java überset-
zen. Üblicherweise sollten alle Klassen angelegt werden und für alle öffentlichen
Methoden sollten sogenannte Stubs definiert werden. Ein Stub (englisch für Stumpf,
Stummel) ist ein Platzhalter für eine Methode, der die korrekte Signatur, aber einen
leeren Rumpf hat.2
Viele Studenten halten dieses Vorgehen für zu aufwendig. Am Ende des Projekts
werden Sie aber hoffentlich den Wert dieser Aktivitäten zu schätzen wissen. Viele
Entwicklungsteams haben nachträglich feststellen müssen, dass die Zeit, die sie
während der Entwurfsphase eingespart haben, mehrfach aufgewendet werden
musste, um Fehler oder Auslassungen zu beheben, die zu spät entdeckt wurden.
Unerfahrene Programmierer sehen das Schreiben von Quelltexten als das „eigent-
liche Programmieren“ an. Das Erstellen eines grundlegenden Entwurfs wird, wenn
nicht als überflüssig, so doch meist als ärgerlich angesehen, und die meisten Ent-
wickler können gar nicht früh genug mit der „echten“ Arbeit anfangen. Das ist
eine gefährliche Sichtweise.
Der grundlegende Entwurf ist einer der wichtigsten Bestandteile eines Projekts.
Sie sollten mindestens so viel Zeit für den Entwurf einplanen, wie Sie für die
Implementierung vorgesehen haben. Anwendungsentwurf ist nicht etwas, das
vor dem Programmieren durchgeführt wird – es ist (der wichtigste Teil vom) Pro-
grammieren.

2 Bei Bedarf können Sie bei den Methoden, die einen anderen Ergebnistyp als void haben, trivi-
ale return-Anweisungen angeben. Liefern Sie als Ergebnis einfach den Wert null für Objekt-
typen und null oder false für die primitiven Typen.

582
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.3 Dokumentation

Fehler in der Implementierung lassen sich später relativ leicht beheben. Fehler im
Entwurf können im besten Fall teuer in der Korrektur sein, im schlimmsten Fall sind
sie fatal für die Anwendung. In manchen Fällen sind sie praktisch gar nicht mehr zu
beheben (und erfordern einen fast kompletten Neubeginn).

15.2.2 Entwurf von Benutzungsschnittstellen


Ein Aspekt, den wir bisher in der Diskussion nicht berücksichtigt haben, ist der Ent-
wurf der Benutzungsschnittstelle.3 An einem bestimmten Punkt müssen wir detail-
liert festlegen, was ein Benutzer am Bildschirm zu sehen bekommt und wie er mit
dem System interagieren kann.
In einer gut entworfenen Anwendung ist dieser Aspekt weitgehend unabhängig
von der Funktionalität der Anwendung, sodass er unabhängig von den übrigen
Klassen entworfen werden kann. In den vorangehenden Kapiteln haben wir gese-
hen, dass wir durch BlueJ die Mittel haben, mit der Anwendung zu interagieren,
noch bevor eine endgültige Benutzungsschnittstelle zur Verfügung steht, und kön-
nen somit die interne Struktur zuerst bearbeiten.
Die Benutzungsschnittstelle kann grafisch sein, mit Knöpfen und Menüs, oder sie
kann textbasiert sein oder wir können ausschließlich mit dem Aufrufmechanismus
von BlueJ arbeiten. Vielleicht läuft das System in einem Netzwerk und die Benut-
zungsschnittstelle wird auf verschiedenen Rechnern im Webbrowser angezeigt.
Wir werden vorläufig den Entwurf der Benutzungsschnittstelle ignorieren und mit-
hilfe von BlueJ mit unserem Programm interagieren.

15.3 Dokumentation
Nachdem wir die Klassen und ihre Schnittstellen identifiziert haben und bevor wir
die Methoden einer Klasse implementieren, sollten wir ihre Schnittstelle dokumen-
tieren. Dies umfasst das Schreiben eines Klassenkommentars und der Methoden-
kommentare für alle Klassen im System. Diese sollten so ausführlich sein, dass der
Zweck jeder Klasse und jeder Methode deutlich wird.
Neben der Analyse und dem Entwurf ist die Dokumentation ein weiterer Bereich,
der von Neulingen leicht vernachlässigt wird. Es ist nicht leicht für einen unerfah-
renen Programmierer zu erkennen, warum die Dokumentation so wichtig ist. Das
liegt daran, dass unerfahrene Programmierer meist an Projekten arbeiten, die nur
aus einer Handvoll Klassen bestehen und die sich meist nur über wenige Wochen
oder Monate erstrecken. Ein Programmierer kann mit einer schlechten Doku-
mentation durchkommen, wenn er an solchen Miniprojekten arbeitet.

3 Beachten Sie die Doppelbedeutung von Entwurf von Schnittstellen an diesem Punkt! Vorher
haben wir über die Schnittstellen von einzelnen Klassen gesprochen (den Satz ihrer öffent-
lichen Methoden); hier reden wir nun über die Benutzungsschnittstelle – den Teil, den ein Be-
nutzer auf dem Bildschirm sieht und über den er mit der Anwendung interagieren kann. Beides
sind wichtige Dinge und unglücklicherweise wird der Begriff Schnittstelle für beide verwendet.

583
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

Andererseits wundern sich selbst erfahrene Programmierer häufig, wie man eine
Dokumentation noch vor der Implementierung schreiben kann. Eine gute Dokumen-
tation ist auf einer abstrakteren Ebene gehalten, die lediglich beschreibt, was eine
Klasse oder Methode tut, aber nicht die Details darstellt, wie dies geschieht. Dies ist
normalerweise symptomatisch, wenn die Implementierung als wichtiger erachtet
wird als der Entwurf.
Wenn ein Entwickler sich mit interessanteren Problemen beschäftigen will und pro-
fessionell in realen Projekten arbeitet, dann arbeitet er nicht selten mit Dutzenden
von Kollegen über mehrere Jahre zusammen. Die Ad-hoc-Lösung, die „Dokumenta-
tion im Kopf“ zu haben, funktioniert dann nicht mehr.

Übung 15.9 Erzeugen Sie ein BlueJ-Projekt für das Kinobuchungssystem.


Erzeugen Sie die notwendigen Klassen. Legen Sie Stubs für alle Methoden an.
Übung 15.10 Dokumentieren Sie alle Klassen und Methoden. Wenn Sie in
einer Gruppe gearbeitet haben, dann weisen Sie den Gruppenmitgliedern
die Zuständigkeit für einzelne Klassen zu. Verwenden Sie das javadoc-For-
mat für Ihre Kommentare mit den passenden javadoc-Schlüsselwörtern für
die Details.

15.4 Kooperation

Programmieren im Paar
Traditionell werden Klassen von Einzelpersonen implementiert. Die meisten
Programmierer entwickeln ihren Quelltext allein, andere Personen werden
meist erst einbezogen, wenn die Implementierung abgeschlossen ist und
bewertet oder getestet werden soll.
In den letzten Jahren wurde verstärkt das Programmieren im Paar (pair pro-
gramming) als eine Technik empfohlen, mit deren Hilfe besserer Quelltext
(besser strukturiert und mit weniger Fehlern) erstellt werden kann. Program-
mieren im Paar ist auch ein Element des Extreme Programming. Suchen Sie im
Web nach den Begriffen „pair programming“ und „extreme programming“,
um mehr zu erfahren.

Softwareentwicklung wird üblicherweise in Teams vorgenommen. Ein wohlstruk-


turierter objektorientierter Ansatz liefert eine gute Unterstützung für Teamarbeit,
denn er erlaubt die Zerlegung einer Aufgabe in lose gekoppelte Komponenten
(Klassen), die unabhängig voneinander implementiert werden können.
Obwohl die grundlegende Entwurfsarbeit im Team erfolgt ist, sollten Sie nun
getrennt arbeiten. Wenn die Definition der Klassenschnittstellen und der Dokumen-
tation gut gemacht wurde, sollten die Klassen unabhängig voneinander implemen-

584
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.5 Prototyping

tiert werden können. Klassen können also Programmierern zugewiesen werden, die
diese allein oder im Paar umsetzen.
Die Implementierungsphase für das Kinobuchungssystem werden wir hier nicht
im Detail besprechen. In dieser Phase kommen all die Techniken und Vorgehens-
weisen zum Einsatz, die wir in den vorherigen Kapiteln kennengelernt haben; wir
hoffen also, dass die Leser inzwischen selbst wissen, wie sie von diesem Punkt
aus weiter vorgehen müssen.

15.5 Prototyping
Anstatt eine Anwendung in einem einzigen, großen Schritt zu entwerfen und zu Konzept
entwickeln, können Prototypen verwendet werden, um Teile eines Systems zu
Prototyping ist
erkunden.
die Konstruktion
Ein Prototyp ist eine Version einer Anwendung, in der Teile dieser Anwendung simu- von unvollständi-
liert werden, um mit anderen Teilen experimentieren zu können. Sie können bei- gen Systemen,
in denen einige
spielsweise einen Prototyp entwickeln, um eine grafische Benutzungsschnittstelle zu
Anteile simuliert
testen. Die Funktionalität der Anwendung ist dann eventuell nicht vollständig imple- werden. Es dient
mentiert. Stattdessen implementieren Sie Methoden, die ihre Aufgaben nur simulie- dazu, möglichst
ren. Beispielsweise könnte beim Aufruf der Methode des Kinosystems, die einen früh ein Verständ-
freien Platz suchen soll, immer Platz 3, Reihe 15 geliefert werden, ohne dass wirk- nis von der Arbeits-
lich eine Suche implementiert ist. Prototyping erlaubt uns, ein ausführbares (aber weise eines
nicht voll funktionierendes) System schnell zu entwickeln, um Teile der Anwendung Systems zu
bekommen.
in der Praxis zu erproben.
Prototypen sind auch nützlich für einzelne Klassen, um die Entwicklung im Team
zu unterstützen. Wenn verschiedene Teammitglieder an unterschiedlichen Klas-
sen arbeiten, werden diese Klassen oft nicht gleichzeitig fertig. In einigen Fällen
kann eine fehlende Klasse die Entwicklung und den Test anderer Klassen aufhal-
ten. Dann kann es nützlich sein, für diese Klasse einen Prototyp zu schreiben. Der
Prototyp implementiert alle Methoden mit Stubs; diese enthalten keine vollstän-
dig abgeschlossenen Implementierungen, sondern simulieren ihre Funktionalität
nur. Dieser Prototyp sollte schnell erstellt werden, damit Entwickler von Klassen,
die diese Klasse benutzen müssen, bis zur Fertigstellung der vollständigen Imple-
mentierung weiterarbeiten können.
In Abschnitt 15.6 werden wir auch ansprechen, dass ein Prototyp zusätzlich dazu
dienen kann, Schwierigkeiten oder Probleme aufzudecken, die in einer frühen Phase
noch nicht bedacht wurden.

Übung 15.11 Umreißen Sie einen Prototyp für Ihr Kinosystem. Welche Klas-
sen sollten zuerst implementiert werden und welche sollten vorläufig nur als
Prototypen realisiert werden?
Übung 15.12 Implementieren Sie den Prototyp Ihres Kinosystems.

585
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

15.6 Softwarewachstum
Es gibt viele Modelle, nach denen Software entwickelt werden kann. Das bekann-
teste ist das sogenannte Wasserfallmodell (so benannt, weil Aktivitäten stufen-
weise nacheinander erfolgen wie die Zwischenstufen bei manchen Wasserfällen –
es gibt üblicherweise kein Zurück).

15.6.1 Das Wasserfallmodell


Im Wasserfallmodell werden verschiedene Phasen der Softwareentwicklung in einer
festen Reihenfolge vorgenommen:
 Analyse des Problems
 Entwurf der Software
 Implementierung der Softwarekomponenten
 Modultests
 Integrationstests
 Auslieferung des Systems an den Kunden
Wenn eine dieser Phasen fehlschlägt, müssen wir möglicherweise eine Stufe zurück-
gehen – wenn beispielsweise Tests fehlschlagen, müssen wir zurück zur Implemen-
tierung –, aber ein weiteres Zurückgehen ist nicht vorgesehen.
Dies ist vermutlich das etablierteste und konservativste Vorgehensmodell für Soft-
wareentwicklung, das seit vielen Jahren weitverbreitet ist. Über die Jahre wurden
jedoch auch etliche Nachteile dieses Modells festgestellt. Die beiden wichtigsten
Schwächen sind die Annahmen, dass die Entwickler die Funktionalität des zu erstel-
lenden Systems von Anfang an vollständig verstanden haben und ein System sich
nach seiner Auslieferung nicht mehr ändert.
In der Praxis treffen beide Annahmen meist nicht zu. Es kommt häufig vor, dass der
Entwurf der Systemfunktionalität nicht von Anfang an perfekt ist; meist liegt dies
daran, dass der Kunde, der sein Aufgabengebiet sehr gut kennt, nicht viel von Soft-
wareentwicklung versteht, und die Softwaretechniker, die gute Programme schrei-
ben können, nur ein begrenztes Verständnis des Aufgabengebiets haben.

15.6.2 Iterative Vorgehensmodelle


Eine Möglichkeit, den Schwächen des Wasserfallmodells beizukommen, ist der
frühe Einsatz von Prototypen und die Einbeziehung des Kunden in den Entwick-
lungsprozess. Es werden Prototypen entwickelt, die anfangs nur einen Eindruck
davon vermitteln, wie sich das System präsentiert und was es tun wird, und der
Kunde beurteilt regelmäßig den Entwurf und die Funktionalität. Dies führt zu
einem eher zyklischen Vorgehen als beim Wasserfallmodell. Hier iteriert die Soft-
wareentwicklung mehrfach durch einen Zyklus Analyse/Entwurf/Prototyp – Imple-
mentierung – Feedback des Kunden.
Ein anderer Ansatz besagt, dass Software nicht entworfen wird, sondern wächst.
Die Idee ist, dass zu Anfang ein kleines und sauber entworfenes System gebaut
wird, das der Endbenutzer wirklich einsetzen kann. Anschließend wird nach und
nach in kontrollierter Weise weitere Funktionalität hinzugefügt (die Software

586
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.6 Softwarewachstum

wächst), sodass mehrfach und in regelmäßigen Abständen komplett benutzbare


und auslieferbare Softwarestände erstellt werden.
In der Realität steht ein solches Wachsen natürlich nicht im Widerspruch zum
Entwurf von Software. Jeder Wachstumsschritt wird sorgfältig entworfen. Aber
es wird nicht versucht, das System von Anfang an komplett zu entwerfen. Denn:
Eigentlich existiert so etwas wie ein fertiges Softwaresystem gar nicht!
Das traditionelle Wasserfallmodell hat ein vollständiges Softwaresystem zum Ziel.
Das Modell vom Softwarewachstum hingegen geht davon aus, dass es vollstän-
dige Systeme, die auf lange Sicht unverändert benutzt werden, nicht gibt. Es gibt
nur zwei Dinge, die mit einer Software geschehen können: Entweder wird sie
ständig angepasst und verbessert oder sie wird bedeutungslos.
Diese Diskussion ist von großer Bedeutung für dieses Buch, denn sie beeinflusst
unsere Sicht in Bezug darauf, welche Aufgaben und Fähigkeiten wir für einen Pro-
grammierer oder Softwaretechniker für wichtig halten. Möglicherweise haben Sie
schon erraten, dass die Autoren dieses Buches das Modell der iterativen Entwicklung
dem Wasserfallmodell deutlich vorziehen.4
Deshalb bekommen bestimmte Aufgaben und Fähigkeiten einen sehr viel höhe-
ren Stellenwert, als sie im Wasserfallmodell haben würden: Softwarewartung,
Quelltexte lesen (und nicht nur schreiben), Entwurf für Erweiterbarkeit, Doku-
mentation, Erstellung von lesbaren Quelltexten und viele weitere Aspekte, die
wir in diesem Buch erwähnt haben, bekommen ihre Relevanz durch die Tatsache,
dass wir genau wissen, dass nach uns andere kommen werden, die unsere Quell-
texte anpassen und erweitern müssen.
Ein Stück Software als eine ständig wachsende, sich ändernde und sich anpas-
sende Einheit zu betrachten und nicht als ein statisches Stück Text, das geschrieben
und erhalten wird wie ein Roman, bestimmt unsere Vorstellung davon, wie gute
Software erstellt werden sollte. Alle Techniken, die wir in diesem Buch behandeln,
zielen darauf ab.

Übung 15.13 Auf welche Weise könnte das Kinobuchungssystem in Zukunft


angepasst oder erweitert werden? Welche Änderungen sind wahrscheinlicher
als andere? Erstellen Sie eine Liste der möglichen zukünftigen Änderungen.
Übung 15.14 Gibt es andere Organisationen, die ähnliche Buchungssysteme
wie das beschriebene verwenden könnten? Welche Unterschiede bestehen
zwischen diesen Systemen?
Übung 15.15 Können Sie sich den Entwurf eines „generischen“ Buchungs-
systems vorstellen, das jeweils für die Zwecke verschiedener Organisationen
spezifisch angepasst und zugeschnitten wird? Wenn Sie ein solches System
entwerfen sollten, an welchem Punkt im Entwicklungsprozess des Kinosys-
tems würden Sie Änderungen vornehmen? Oder würden Sie dieses System
ignorieren und von Grund auf neu beginnen?

4 Ein hervorragendes Buch über die Probleme bei der Softwareentwicklung und ihre möglichen
Lösungen ist The Mythical Man-Month von Frederick P. Brooks, erschienen bei Addison-Wesley.
Obwohl die erste Ausgabe inzwischen 40 Jahre alt ist, ist es nach wie vor eines der spannends-
ten und lehrreichsten Bücher zu diesem Thema.

587
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

15.7 Der Einsatz von Entwurfsmustern


In früheren Kapiteln haben wir ausführlich Techniken vorgestellt, mit denen wir Teile
unserer Arbeit wiederverwenden und durch die wir unsere Quelltexte für andere
besser verständlich machen können. Diese Diskussionen sind jedoch meist auf der
Ebene des Quelltextes einzelner Klassen geblieben.
Je erfahrener wir werden und je größer die Systeme werden, die wir bauen,
desto mehr treten die Schwierigkeiten bei der Implementierung einzelner Klassen
in den Hintergrund. Die Struktur des Gesamtsystems hingegen – die komplexen
Beziehungen zwischen den Klassen – wird zunehmend schwieriger zu entwerfen
und zu verstehen als der Quelltext einzelner Klassen.
Es ist konsequent, wenn wir versuchen, für Klassenstrukturen dieselben Ziele zu
erreichen wie für Quelltexte: Wir wollen möglichst große Teile wiederverwenden
und wir wollen, dass andere unsere Strukturen verstehen können.

Konzept Auf der Ebene von Klassenstrukturen können beide Ziele durch die Verwendung
von Entwurfsmustern unterstützt werden. Ein Entwurfsmuster beschreibt ein bei
Ein Entwurfs-
der Softwareentwicklung häufig auftretendes Problem und liefert dann eine allge-
muster ist eine
Beschreibung eines meine Lösung für dieses Problem, die in vielen Zusammenhängen eingesetzt wer-
Entwurfsproblems, den kann. Bei einem Softwareentwurfsmuster besteht diese Lösung meist aus der
verbunden mit der Beschreibung einer kleinen Menge von Klassen mitsamt ihren Interaktionen.
Beschreibung einer
kleinen Menge an Entwurfsmuster helfen uns auf zweierlei Weise. Erstens dokumentieren sie gute
Klassen und ihrer Lösungen zu Problemen, sodass diese Lösungen später für ähnliche Probleme
Interaktionsstruk- benutzt werden können. Die Wiederverwendung findet also nicht auf Ebene des
tur, mit deren Hilfe Quelltextes statt, sondern auf der Ebene der Klassenstruktur.
das Problem gelöst
werden kann. Zweitens haben Entwurfsmuster Namen und bilden auf diese Weise ein Vokabular,
mit dessen Hilfe Softwaretechniker über ihre Entwürfe reden können. Wenn erfah-
rene Entwickler die Struktur einer Anwendung diskutieren, dann könnte einer
sagen: „An dieser Stelle sollten wir ein Singleton verwenden.“ Singleton ist der
Name eines bekannten Entwurfsmusters, und wenn beide Entwickler mit diesem
Muster vertraut sind, dann können sie sich auf der gleichen Ebene unterhalten,
ohne zu sehr ins Detail gehen zu müssen. Somit führt die Sprache, die durch
bekannte und weitverbreitete Entwurfsmuster gebildet wird, eine höhere Ebene der
Abstraktion ein, auf der wir mit der Komplexität anspruchsvoller Systeme besser
umgehen können.
Entwurfsmuster für Software wurden bekannt durch ein 1995 veröffentlichtes Buch,
das eine Reihe von Mustern, ihre Anwendbarkeit und ihre Vorteile beschreibt.5 Die-
ses Buch ist auch heute noch eines der wichtigsten Werke zum Thema Entwurfs-
muster. Wir werden hier nicht versuchen, einen vollständigen Überblick über die
darin beschriebenen Entwurfsmuster zu geben. Stattdessen werden wir im Folgen-
den nur eine kleine Auswahl dieser Muster diskutieren, um dem Leser einen Ein-
druck der Vorteile von Entwurfsmustern zu geben. Weitere Entwurfsmuster können
dann in der entsprechenden Literatur nachgelesen werden.

5 Design Patterns: Elements of Reusable Object-Oriented Software, von Erich Gamma, Richard
Helm, Ralph Johnson und John Vlissides, Addison-Wesley, 1995. Die exzellente deutsche Über-
setzung von Dirk Riehle ist 1996 erschienen, ebenfalls bei Addison-Wesley.

588
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.7 Der Einsatz von Entwurfsmustern

15.7.1 Struktur eines Musters


Musterbeschreibungen werden üblicherweise in einer einheitlichen Form ange-
geben, die ein bestimmtes Minimum an Informationen garantiert. Eine Muster-
beschreibung enthält nicht nur Informationen über die Struktur einiger Klassen,
sondern auch über die Probleme, die dieses Muster angeht, sowie konkurrierende
Einflüsse, die für oder gegen den Einsatz des Musters sprechen.

Die Beschreibung eines Musters enthält mindestens:


 einen Namen, der das Reden über das Muster erleichtert
 eine Beschreibung des Problems, das das Muster adressiert (häufig aufgeteilt
in Abschnitte wie Zweck, Motivation und Anwendbarkeit)
 eine Beschreibung der Lösung (meist unterteilt in Struktur, Teilnehmer und
Partner)
 eine Erläuterung der Konsequenzen, die sich aus dem Einsatz des Musters
ergeben, mitsamt Ergebnissen und Vor- und Nachteilen

In den folgenden Abschnitten werden wir einige weitverbreitete Muster kurz vor-
stellen.

15.7.2 Dekorierer
Das Entwurfsmuster Dekorierer behandelt das Hinzufügen von Funktionalität zu
einem bereits existierenden Objekt. Wir nehmen an, dass wir ein Objekt benöti-
gen, das auf die gleichen Methodenaufrufe reagiert (also die gleiche Schnittstelle
hat) wie unser Objekt, aber zusätzliches oder geändertes Verhalten zeigt. Mögli-
cherweise wollen wir die Schnittstelle auch erweitern.

Eine Möglichkeit der Umsetzung ist über Vererbung. Eine Unterklasse könnte Imple-
mentierungen von Methoden überschreiben oder weitere Methoden hinzufügen.
Aber die Verwendung von Vererbung ist eine statische Lösung: Einmal erzeugt,
kann ein Objekt sein Verhalten nicht mehr ändern.

Eine dynamischere Lösung ist die Verwendung eines Dekorierer-Objekts. Ein Deko-
rierer ist ein Objekt, das ein anderes Objekt umschließt und anstelle des umschlosse-
nen Objekts verwendet werden kann (es implementiert üblicherweise die gleiche
Schnittstelle). Klienten kommunizieren dann mit dem Dekorierer statt direkt mit dem
Original (ohne von dieser Ersetzung wissen zu müssen). Das Dekorierer-Objekt leitet
die Aufrufe an das umschlossene Objekt weiter, kann aber auch zusätzliche Aktio-
nen durchführen. Ein Beispiel liefert uns die Java-Bibliothek zur Ein-/Ausgabe. Dort
ist ein BufferedReader ein Dekorierer für einen Reader (Abbildung 15.2). Ein Buffered-
Reader implementiert die gleiche Schnittstelle und kann anstelle eines ungepufferten
Readers verwendet werden, fügt aber zusätzliches Verhalten hinzu. Im Unterschied
zur Vererbung kann ein Dekorierer auch für bereits existierende Objekte verwendet
werden.

589
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

Abbildung 15.2
Struktur des Entwurfs- :BufferedReader
musters Dekorierer.
:Reader

15.7.3 Singleton
In vielen Programmen gibt es Situationen, in denen es von einer bestimmten Klasse
nur eine einzige Instanz geben sollte. In unserem Zuul-Spiel beispielsweise benöti-
gen wir nur einen einzigen Parser. Wenn wir eine Softwareentwicklungsumgebung
programmieren, dann benötigen wir möglicherweise nur einen einzelnen Compiler
oder Debugger.
Das Entwurfsmuster Singleton garantiert, dass von einer Klasse nur eine einzige
Instanz erzeugt wird, und ermöglicht einen einheitlichen Zugriff auf diese Instanz.
In Java kann ein Singleton definiert werden, indem der Konstruktor privat dekla-
riert wird. Dies garantiert, dass er nicht von außerhalb der Klasse aufgerufen wer-
den kann und Klienten somit keine Instanzen erzeugen können. Wir erzeugen
dann in der Singleton-Klasse selbst eine Instanz und ermöglichen den Zugriff auf
sie (Listing 15.1 illustriert dies für die Klasse Parser).

Listing 15.1
Das Entwurfsmuster
Singleton.

In dieser Umsetzung des Musters


 ist der Konstruktor privat, sodass Instanzen nur innerhalb der Klasse selbst erzeugt
werden können. Dies muss im statischen Teil der Klasse passieren (bei der Initi-
alisierung der statischen Felder oder in statischen Methoden), da sonst keine
Instanzen existieren würden;
 wird ein privates, statisches Datenfeld definiert und initialisiert, das die (ein-
zige) Instanz des Parsers hält;
 wird eine statische Methode gibInstanz definiert, die den Zugriff auf die eine
Instanz ermöglicht.
Klienten des Singletons können diese statische Methode benutzen, um Zugriff
auf das Parser-Objekt zu bekommen:
Parser parser = [Link]();

590
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.7 Der Einsatz von Entwurfsmustern

In der Tat bietet Java einen einfacheren Weg an, um die grundlegenden Funktio-
nen eines Singletons zu erhalten – ein Aufzählungstyp mit einem einzigen Wert
(Listing 15.3).
Listing 15.2
Eine Singleton-Defini-
tion, die einen Java-
Aufzählungstyp ver-
wendet.

Auf die Singleton-Instanz würde dann über [Link] zugegriffen werden.


Da beliebige Funktionalität in der Definition eines Aufzählungstyps ebenso wie in
einer Klasse eingebaut werden kann, ist es dieser Ansatz wert, im Gegensatz zu
einem handgestrickten Ansatz, in Java beachtet zu werden.

15.7.4 Fabrikmethode
Das Entwurfsmuster Fabrikmethode liefert die Schnittstelle für die Erzeugung von
Objekten, überlässt jedoch Subklassen die Entscheidung darüber, von welchen
konkreten Klassen Instanzen erzeugt werden. Typischerweise erwartet ein Klient
eine Superklasse oder ein Interface vom dynamischen Typ des tatsächlichen Objekts,
während die Fabrikmethode Spezialisierungen liefert.
Die Iteratoren der Sammlungsklassen (Subtypen von Collection) sind ein Beispiel für
diese Technik. Wenn wir eine Variable vom Typ Collection haben, können wir sie
nach einem Iterator fragen (mit der Methode iterator) und dann mit diesem arbei-
ten (Listing 15.3). Die Methode iterator ist in diesem Beispiel die Fabrikmethode.
Listing 15.3
Die Benutzung einer
Fabrikmethode.

Aus der Sicht des Klienten (im Quelltext in Listing 15.3) gehen wir mit Objekten vom
Typ Collection und Iterator um. Tatsächlich ist der (dynamische) Typ der Samm-
lung jedoch möglicherweise ArrayList, wodurch der Aufruf von iterator ein Objekt
vom Typ ArrayListIterator zurückliefert. Oder die Sammlung ist ein HashSet und
iterator liefert einen HashSetIterator. Die Fabrikmethode wird in Subklassen spezi-
alisiert, damit von dort spezialisierte Instanzen des „offiziellen“ Ergebnistyps gelie-
fert werden.
Wir können dieses Muster sehr gut verwenden, um in unserer Fuechse-und-Hasen-
Simulation aus Kapitel 12 die Klasse Simulator von den spezifischen Tierklassen zu
entkoppeln. (Sie erinnern sich: In unserer Version der Simulation war der Simulator
mit den Klassen Fuchs und Hase eng gekoppelt, weil er die Startpopulation erzeugt.)
Stattdessen können wir nun ein Interface AkteurFabrik einführen sowie Klassen, die
dieses Interface für jeden Akteur implementieren (also etwa eine FuchsFabrik und
eine HasenFabrik). Der Simulator würde einfach eine Sammlung von AkteurFabrik-
Objekten halten und jede dieser Fabriken um die Erzeugung einer Anzahl von Akteu-
ren bitten. Jede Fabrik würde einen anderen Typ von Akteur erzeugen, aber der Simu-
lator würde mit den Fabriken nur über das Interface AkteurFabrik kommunizieren.

591
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

15.7.5 Beobachter
In der Diskussion vieler Projekte in diesem Buch haben wir versucht, das interne
Modell einer Anwendung von seiner Darstellung auf dem Bildschirm (seiner Ansicht)
zu trennen. Das Beobachter-Muster bietet eine Möglichkeit, diese Trennung zwi-
schen Modell und Ansicht zu erreichen.
Allgemeiner gesprochen: Das Beobachter-Muster definiert eine 1-zu-n-Beziehung,
sodass die Änderung des Zustands eines Objekts dazu führt, dass alle abhängigen
Objekte benachrichtigt und automatisch aktualisiert werden. Es erreicht dies mit
einer möglichst losen Kopplung zwischen den Beobachtern und dem beobachte-
ten Objekt (dem sogenannten Subjekt).
Mit diesem Muster können nicht nur Modell und Ansicht entkoppelt werden,
sondern auch mehrere Ansichten des gleichen Modells (entweder alternativ oder
simultan) ermöglicht werden. Als Beispiel betrachten wir wieder unsere Fuechse-
und-Hasen-Simulation.
In der Simulation wird die Population der Tiere auf dem Bildschirm in einem zwei-
dimensionalen Feld animiert. Es gibt aber noch andere Möglichkeiten: Wir könnten
auch die Darstellung als Graph der Populationszahlen auf einem Zeitstrahl vorziehen
oder als bewegte Balkengrafik (Abbildung 15.3). Wir könnten auch alle Ansichten
auf einmal sehen wollen.
Abbildung 15.3
Mehrere Ansichten für
dasselbe Subjekt.

Für das Beobachter-Muster benutzen wir zwei Typen: Observable (englisch für beob-
achtbar) und Observer6 (englisch für Beobachter). Die beobachtbare Einheit (das

6 In dem Java-Paket [Link] gibt es hierzu bereits zwei vordefinierte Typen: die Klasse
Observable und das Interface Observer (das nur die Methode update aufweist).

592
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.7 Der Einsatz von Entwurfsmustern

Feld in unserer Simulation) erweitert die Klasse Observable und der Beobachter
(Simulationsansicht) implementiert das Interface Observer (Abbildung 15.4).

Interface Abbildung 15.4


Struktur des Entwurfs-
B musters Beobachter.

Die Klasse Observer bietet Methoden an, mit denen sich Beobachter bei der beob-
achtbaren Einheit anmelden können. Sie garantiert, dass jedes Mal die Methode
update an jedem Beobachter aufgerufen wird, wenn die beobachtete Einheit (das
Feld) die geerbte Methode notify aufruft. Die tatsächlichen Beobachter (die Ansich-
ten) können sich dann über den neuen, aktualisierten Zustand des Felds informieren
und diesen erneut anzeigen.
Das Beobachter-Muster kann auch für andere Probleme als für die Trennung zwi-
schen Modell und Ansicht benutzt werden. Es kann immer dann angewendet wer-
den, wenn der Zustand eines oder mehrerer Objekte vom Zustand eines anderen
Objekts abhängt.

15.7.6 Muster zusammengefasst


Die Diskussion über Entwurfsmuster und ihre Anwendungsmöglichkeiten geht weit
über den Rahmen dieses Buches hinaus. Wir haben hier nur einen kurzen Eindruck
davon vermittelt, was Entwurfsmuster sind, und eine informelle Beschreibung eini-
ger weitverbreiteter Muster gegeben.
Wir hoffen jedoch, dass diese Diskussion die mögliche Richtung für weiteres Arbei-
ten aufzeigt. Sobald wir verstanden haben, wie wir gute Implementierungen ein-
zelner Klassen erstellen können, können wir uns darauf konzentrieren, welche
Arten von Klassen wir in unseren Anwendungen benötigen und wie diese koope-
rieren. Gute Lösungen sind oft nicht leicht zu finden, und Entwurfsmuster
beschreiben Strukturen, die sich als nützlich für die Lösung immer wieder auftre-
tender Probleme erwiesen haben. Sie unterstützen uns bei der Erstellung guter
Klassenstrukturen.
Je erfahrener Sie als Softwareentwickler werden, desto mehr denken Sie über höhere
Strukturen nach als über die Implementierung einzelner Methoden.

593
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

Übung 15.16 Drei zusätzliche häufig verwendete Entwurfsmuster sind


Zustand (state), Strategie (strategy ) und Besucher (visitor). Finden Sie Beschrei-
bungen dieser Muster und benennen Sie jeweils mindestens einen Anwen-
dungszusammenhang, in dem sie zum Einsatz kommen können.
Übung 15.17 In einer späten Phase eines Softwareprojekts stellen Sie fest,
dass zwei Teams, die an unterschiedlichen Teilen der Anwendung gearbeitet
haben, inkompatible Klassen programmiert haben. Die Schnittstelle einiger
Klassen des einen Teams weichen leicht von den Schnittstellen ab, die das
andere Team erwartet. Erklären Sie, wie das Adapter-Muster in dieser Situa-
tion helfen kann, ohne dass eine der bestehenden Klassen geändert werden
muss.

Zusammenfassung
In diesem Kapitel haben wir uns auf eine höhere Ebene der Abstraktion
bewegt, weg vom Entwurf einzelner Klassen (oder der Kooperation zwi-
schen zwei Klassen) hin zum Entwurf einer gesamten Anwendung. Im Kern
des Entwurfs eines objektorientierten Softwaresystems steht die Entschei-
dung, welche Klassen für seine Realisierung definiert werden und wie diese
Klassen kooperieren sollen.
Einige Klassen sind recht offensichtlich und leicht zu entdecken. Wir haben
die Verb/Substantiv-Methode bei einer textbasierten Problembeschreibung
verwendet, um einen Startpunkt zu finden. Nach dem Identifizieren der
Klassen können wir CRC-Karten und durchgespielte Szenarios benutzen,
um die Abhängigkeiten und Details der Kommunikation zwischen den Klas-
sen zu entwerfen und ihre Zuständigkeiten herauszuarbeiten. Weniger erfah-
renen Entwicklern hilft es, wenn die Szenarios in einer Gruppe durchgespielt
werden.
CRC-Karten können benutzt werden, um den Entwurf bis auf der Ebene der
Methodennamen und ihrer Parameter zu verfeinern. Nachdem dies erfolgt
ist, können Klassen mit Stubs in Java programmiert und ihre Schnittstellen
dokumentiert werden.
Das Einhalten einer so organisierten Vorgehensweise dient mehreren Zwe-
cken. Es stellt sicher, dass potenzielle Probleme mit frühen Entwurfsentschei-
dungen entdeckt werden können, bevor viel Aufwand in die Implementierung
geflossen ist. Es ermöglicht auch, dass Programmierer parallel an mehreren
Klassen arbeiten können, ohne dass bei der Implementierung der einen Klasse
auf die Implementierung einer anderen gewartet werden muss.

594
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.7 Der Einsatz von Entwurfsmustern

Flexible, erweiterbare Klassenstrukturen sind nicht immer leicht zu entwerfen.


Entwurfsmuster werden verwendet, um bewährte Strukturen zur Lösung ver-
schiedener Problemklassen zu dokumentieren. Über das Studium von Ent-
wurfsmustern kann ein Softwaretechniker sehr viel über gute Anwendungs-
strukturen lernen und seine eigene Entwurfstechnik verbessern.
Je größer eine Aufgabenstellung ist, desto wichtiger ist eine gute Anwen-
dungsstruktur. Je erfahrener ein Softwaretechniker wird, desto mehr Zeit ver-
bringt er mit dem Entwurf von Anwendungsstrukturen statt allein mit dem
Schreiben von Quelltext.

NEUE BEGRIFFE IN DIESEM KAPITEL


Analyse und Entwurf, Verb/Substantiv-Methode, CRC-Karte, Szenario,
Geschäftsfall, Stub, Entwurfsmuster

Zusammenfassung der Konzepte


 Verb/Substantiv Die Klassen in einem System korrespondieren in etwa
mit den Substantiven in der Systembeschreibung. Die Methoden korrespon-
dieren mit den Verben.
 Szenario Szenarios (auch als „Geschäftsfälle“ oder unter dem englischen
Begriff „use cases“ bekannt) können benutzt werden, um ein Verständnis
von den Interaktionen in einem System zu bekommen.
 Prototyping Prototyping ist die Konstruktion unvollständiger Systeme, in
denen einige Anteile simuliert werden. Es dient dazu, möglichst früh ein
Verständnis von der Arbeitsweise eines Systems zu bekommen.
 Entwurfsmuster Ein Entwurfsmuster ist eine Beschreibung eines Ent-
wurfsproblems, verbunden mit der Beschreibung einer kleinen Menge an
Klassen und ihrer Interaktionsstruktur, mit deren Hilfe das Problem gelöst
werden kann.

595
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen

Übung 15.18 Angenommen Sie haben ein Schulverwaltungssystem für Ihre


Schule, mit einer Klasse namens Datenbank (eine ziemlich wichtige Klasse),
die Objekte vom Typ Schueler verwaltet. Zu jedem Schüler gehört eine
Adresse, die in einem Adresse-Objekt gespeichert wird (d.h., jedes Schueler-
Objekt hält eine Referenz auf ein Adresse-Objekt).
Jetzt möchten Sie von der Datenbank-Klasse aus auf die Straße, Stadt und
Postleitzahl des Schülers zugreifen – Daten, für die die Klasse Adresse pas-
sende sondierende Methoden definiert. Für den Entwurf der Klasse Schueler
haben Sie jetzt zwei Möglichkeiten:
Entweder implementieren Sie die Methoden gibStrasse, gibStadt und gibPLZ
in der Klasse Schueler, die einfach den Methodenaufruf an das Adresse-
Objekt weiterleiten und das Ergebnis zurückgeben, oder Sie implementieren
in der Klasse Schueler eine Methode gibAdresse, die der Datenbank das voll-
ständige Adresse-Objekt zurückliefert und das Datenbank-Objekt die Metho-
den des Adresse-Objekts direkt aufrufen lässt.
Welche dieser Möglichkeiten ist die bessere und warum? Erstellen Sie ein
Klassendiagramm für jede Alternative und nennen Sie jeweils gute Gründe
für die Lösung.

596
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL

16 Eine Fallstudie

Lernziele
Zentrale Konzepte in diesem Kapitel: Entwicklung einer kompletten
Anwendung
Java-Konstrukte in diesem Kapitel: (In diesem Kapitel werden keine neuen
Java-Konstrukte vorgestellt.)

In diesem Kapitel führen wir viele der objektorientierten Prinzipien zusammen, die
wir in diesem Buch vorgestellt haben, indem wir eine ausführliche Fallstudie
betrachten. Wir werden bei dieser Fallstudie von einer Problembeschreibung aus-
gehen, um dann über das Identifizieren der Klassen und den Entwurf zu einem
iterativen Prozess von Implementierung und Test zu gelangen. Im Gegensatz zu
den bisherigen Kapiteln wollen wir hier keine neuen Themen diskutieren, sondern
den zweiten Teil des Buches wiederholen und vertiefen, insbesondere die Themen
Vererbung, Abstraktionstechniken, Fehlerbehandlung und Anwendungsentwurf.

16.1 Die Fallstudie


In unserer Fallstudie werden wir ein Modell eines Taxiunternehmens entwickeln.
Dieses Unternehmen untersucht gerade, ob es sein Einsatzgebiet in einen bisher
nicht abgedeckten Teil der Stadt ausdehnen soll. Das Unternehmen betreibt ein-
fache Taxis und Großraumtaxis. Einfache Taxis setzen ihre Fahrgäste erst am Ziel-
ort ab, bevor sie neue Fahrgäste aufnehmen. Großraumtaxis sammeln mehrere
Fahrgäste an mehreren Abholpunkten auf und fahren diese während einer Fahrt
zu verschiedenen Zielen (etwa indem sie Gäste in mehreren Hotels aufsammeln
und diese zu verschiedenen Terminals am Flughafen fahren). Basierend auf den
geschätzten Zahlen möglicher Fahrgäste im betrachteten Stadtteil möchte das
Unternehmen herausfinden, ob eine Expansion profitabel sein kann und wie viele
Taxis am neuen Standort für einen effektiven Betrieb notwendig wären.

597
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

16.1.1 Die Problembeschreibung


Der folgende Absatz liefert eine informelle Beschreibung der Abläufe im Taxi-
unternehmen, wie sie sich nach mehreren Besprechungen herauskristallisiert hat:
Das Unternehmen betreibt sowohl Taxis als auch Großraumtaxis. Die Taxis
werden eingesetzt, um einzelne Personen (oder Kleingruppen) von einem
Ort zum anderen zu transportieren. Großraumtaxis werden eingesetzt, um
Einzelpersonen an verschiedenen Positionen aufzunehmen und sie an ver-
schiedene Ziele zu transportieren. Wenn das Unternehmen einen Taxiruf von
einer Einzelperson, einem Hotel, einem Veranstaltungsort oder einer Touris-
tenorganisation erhält, versucht es, diesen Fahrgästen ein freies Taxi zuzuwei-
sen. Wenn kein Taxi frei ist, gibt es keinerlei Wartesystem. Wenn ein Fahrzeug
an einem Abholpunkt ankommt, benachrichtigt der Fahrer das Unternehmen.
Ebenso informiert der Fahrer das Unternehmen, wenn ein Fahrgast an seinem
Ziel abgesetzt wurde.
In Kapitel 12 haben wir vorgeschlagen, dass ein wichtiger Effekt beim Modellie-
ren sein sollte, etwas über die Situation zu lernen, die modelliert werden soll. Es
ist nützlich, möglichst früh das Lernziel zu identifizieren, denn solche Ziele könn-
ten den Entwurf beeinflussen. Wenn wir beispielsweise herausfinden wollen, wie
profitabel das Betreiben von Taxis im untersuchten Stadtteil ist, dann müssen wir
sicherstellen, dass unser Modell uns Informationen liefert, die wir entsprechend
auswerten können. Zwei Aspekte sollten wir deshalb berücksichtigen: Wie häu-
fig müssen Fahrgäste abgewiesen werden, weil kein Taxi zur Verfügung steht;
und im anderen Extrem, wie viele Taxis bleiben aus Mangel an Fahrgästen unge-
nutzt. Diese Einflüsse finden sich in der Basisbeschreibung der normalen Arbeits-
weise des Unternehmens nicht wieder, gehören aber zu Szenarios, die wir für
den Entwurf durchspielen sollten.
Wir sollten also folgenden Absatz zur Beschreibung hinzunehmen:
Das System registriert Kundenanfragen, die nicht erfüllt werden konnten. Es
liefert außerdem Informationen darüber, wie lange sich Fahrzeuge jeweils in
einem der folgenden Zustände befinden: mit einem Fahrgast besetzt, auf dem
Weg zu einem Abholpunkt oder ungenutzt.
Wir werden im Folgenden jedoch den Fokus auf die ursprüngliche Beschreibung
der Arbeitsweise des Unternehmens legen und die Zusatzbemerkungen für die
Übungen lassen.

Übung 16.1 Fallen Ihnen weitere Informationen ein, die durch das System
geliefert werden sollten? Wenn ja, dann fügen Sie die entsprechenden Anfor-
derungen dem gegebenen Text hinzu und verwenden Sie diese in Ihren eige-
nen Erweiterungen des Systems.

598
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.2 Analyse und Entwurf

16.2 Analyse und Entwurf


Wir beginnen, wie in Kapitel 15 vorgeschlagen, indem wir mithilfe der Verb/Sub-
stantiv-Methode die Klassen und Interaktionen des Systems identifizieren.

16.2.1 Identifizieren der Klassen


Die folgenden (Singularformen der) Substantive liegen in der Beschreibung vor:
Unternehmen, Taxi, Großraumtaxi, Einzelperson, Position, Ziel, Hotel, Veranstaltungs-
orte, Touristenorganisation, Fahrzeug, Passagier, Abholpunkt, Fahrer und Fahrgast.
Die erste wichtige Anmerkung ist, dass es falsch wäre, aus dieser Liste Substan-
tive direkt eine Menge an Klassen abzuleiten. Informelle Texte sind selten so for-
muliert, dass sie sich für eine solche direkte Abbildung eignen.
Sehr häufig ist es notwendig, in der Liste die Synonyme zu erkennen, also verschie-
dene Bezeichnungen für denselben Gegenstand. Beispielsweise sind „Einzelperson“
und „Passagier“ Synonyme für „Fahrgast“.
Eine weitere Vereinfachung ergibt sich, wenn Gegenstände, die nicht modelliert zu
werden brauchen, aus der Modellierung herausgenommen werden. Beispielsweise
liefert die Beschreibung verschiedene Möglichkeiten, wie ein Taxiunternehmen
kontaktiert werden kann: durch Einzelpersonen, Hotels, Veranstaltungsorte oder
Touristenorganisationen. Ist es wirklich notwendig, diese Unterschiede zu modellie-
ren? Die Antwort hängt davon ab, welche Informationen wir vom Modell geliefert
bekommen wollen. Wir könnten beispielsweise Rabatte für Hotels einräumen wol-
len, die viele Fahrgäste liefern, oder Prospekte an Vergnügungslokale schicken, die
bisher nur wenige Fahrgäste geliefert haben. Wenn solche Unterscheidungen
keine Rolle spielen, dann können wir das Modell so vereinfachen, dass Fahrgäste
nach einem vernünftigen statistischen Verfahren in das System „eingespeist“ wer-
den.

Übung 16.2 Überlegen Sie sich mögliche Vereinfachungen bei den Substanti-
ven für Fahrzeuge. Sind „Fahrzeug“ und „Taxi“ in diesem Kontext synonym?
Müssen wir zwischen „Großraumtaxi“ und „Taxi“ unterscheiden? Was ist mit
„Fahrer“? Begründen Sie Ihre Antworten.
Übung 16.3 Ist es im gegebenen Kontext möglich, von den folgenden
Begriffen einen oder mehrere als Synonyme zu entfernen: „Position“, „Ziel“
und „Abholpunkt“?
Übung 16.4 Identifizieren Sie die Substantive in den Erweiterungen, die Sie
der Beschreibung hinzugefügt haben, und vereinfachen Sie diese so weit wie
möglich.

599
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

16.2.2 CRC-Karten benutzen


Abbildung 16.1 enthält eine Zusammenfassung der Substantive und Verben, die
nach einigen Vereinfachungen der Originalbeschreibung übrig bleiben. Jedes der
Substantive sollte nun einer CRC-Karte zugewiesen werden, um Zuständigkeiten
und Partnerklassen identifizieren zu können.

Abbildung 16.1
Zusammenhänge Substantive Verben
zwischen Substanti- Unternehmen betreibt Taxis und Großraumtaxis
ven und Verben im erhält Taxirufe
Taxiunternehmen. teilt Fahrzeuge zu
Taxi transportiert einen Fahrgast
Großraumtaxi transportiert einen oder mehrere Fahrgäste
Fahrgast
Position
Fahrgastquelle ruft das Unternehmen an
Fahrzeug sammelt Einzelpersonen auf
kommt an Abholpunkt an
informiert Unternehmen über Ankunft
informiert Unternehmen über Ablieferung

Aus dieser Zusammenfassung wird deutlich, dass Taxis und Großraumtaxis unter-
schiedliche Spezialisierungen einer allgemeineren Klasse Fahrzeug sind. Der Haupt-
unterschied zwischen einem Taxi und einem Großraumtaxi besteht darin, dass ein
Taxi immer nur einen Fahrgast abholt und transportiert, während ein Großraumtaxi
mit mehreren unabhängigen Fahrgästen gleichzeitig umgeht. Diese Beziehung
zwischen den drei Begriffen legt eine Vererbungsbeziehung nahe, in der Taxi und
Großraumtaxi Subtypen von Fahrzeug sind.

Übung 16.5 Legen Sie echte CRC-Karten für die in diesem Abschnitt identi-
fizierten Substantive/Klassen an, um mit diesen die Szenarios durchspielen
zu können, die sich aus der Projektbeschreibung ergeben.
Übung 16.6 Tun Sie dies ebenfalls für alle Erweiterungen, die Sie im nächs-
ten Schritt berücksichtigen möchten.

16.2.3 Szenarios
Das Taxiunternehmen repräsentiert eigentlich keine sehr komplexe Anwendung.
Wir werden feststellen, dass ein Großteil der Interaktion im System durch das ele-
mentare Szenario abgedeckt ist, in dem die Anfrage eines Fahrgastes erfüllt wird,
von einer Position zu einer anderen gefahren zu werden. In der Praxis wird sich
dieses Szenario in mehrere Schritte aufteilen, die der Reihe nach abgearbeitet
werden, vom Entgegennehmen des Taxirufs bis zum Absetzen am Ziel.

600
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.2 Analyse und Entwurf

 Wir haben uns dafür entschieden, dass eine Fahrgastquelle alle neuen Fahr-
gäste für das System erzeugen soll. Somit ist die Zuständigkeit von Fahrgastquelle
das Erzeugen eines Fahrgastes und ein Partner ist Fahrgast.
 Die Fahrgastquelle ruft das Taxiunternehmen an und bittet um Abholung eines
Fahrgastes. Wir notieren Taxiunternehmen als einen Partner von Fahrgastquelle
und fügen ein Taxi rufen als Zuständigkeit ein. Analog fügen wir bei Taxi-
unternehmen die Zuständigkeit einen Taxiruf entgegennehmen ein. Verknüpft mit
einem Taxiruf werden der Fahrgast und der Abholpunkt sein. Also hat Taxiunter-
nehmen Fahrgast und Position als Partner. Wenn die Fahrgastquelle das Unter-
nehmen aufruft, könnte sie den Fahrgast und den Abholpunkt als getrennte
Objekte übergeben. Wir ziehen es jedoch vor, den Abholpunkt direkt mit dem
Fahrgast zu verknüpfen. Also wird Position zu einem Partner von Fahrgast und
eine Zuständigkeit von Fahrgast wird einen Abholpunkt angeben.
 Woher stammt der Abholpunkt eines Fahrgastes? Der Abholpunkt und das
Ziel könnten festgelegt werden, wenn ein Fahrgast erzeugt wird. Also fügen wir
Fahrgastquelle die Zuständigkeit Abholpunkt und Ziel für einen Fahrgast gene-
rieren hinzu, mit Position als Partner; und fügen Fahrgast die Zuständigkeiten
Abholpunkt und Ziel merken und liefere Ziel hinzu.
 Als Antwort auf einen Taxiruf ist Taxiunternehmen dafür zuständig, ein Fahrzeug
zuzuweisen. Dies legt eine weitere Zuständigkeit Speichern einer Sammlung von
Fahrzeugen nah, mit Sammlung und Fahrzeug als Partner. Da eine Anfrage fehlschla-
gen kann – weil kein freies Fahrzeug verfügbar ist –, sollte der Fahrgastquelle
geeignet signalisiert werden, ob der Taxiruf erfolgreich war oder nicht.
 Es gibt keinen Hinweis darauf, dass das Unternehmen beim Zuteilen von Fahr-
zeugen zwischen Taxis und Großraumtaxis unterscheidet, also betrachten wir
diesen Aspekt hier nicht näher. Auf jeden Fall kann ein Fahrzeug aber nur ein-
geteilt werden, wenn es frei ist. Deshalb ist ein Fahrzeug dafür zuständig, anzu-
zeigen, ob es frei ist.
 Wenn ein freies Fahrzeug ermittelt wurde, muss es zum Abholpunkt geschickt
werden. Taxiunternehmen hat also die Zuständigkeit Schicken eines Fahrzeugs
zum Abholpunkt mit der passenden Zuständigkeit bei Fahrzeug: Abholpunkt
erhalten. Position wird als Partner von Fahrzeug notiert.
 Nach Erhalt eines Abholpunkts kann sich das Verhalten von Taxis und Groß-
raumtaxis deutlich unterscheiden. Ein Taxi wird nur dann frei sein, wenn es nicht
gerade auf dem Weg zu einem Abholpunkt oder einem Ziel ist. Also ist eine
Zuständigkeit von Taxi zum Abholpunkt fahren. Im Gegensatz dazu muss ein
Großraumtaxi mit mehreren Fahrgästen umgehen. Wenn es einen Abholpunkt
erhält, könnte es zwischen mehreren Positionen als nächstem Zwischenziel
wählen. Also fügen wir bei Großraumtaxi die Zuständigkeit nächstes Fahrziel
wählen ein, mit einem Partner Sammlung, die die Menge der möglichen Fahrziele
hält. Die Tatsache, dass sich ein Fahrzeug zwischen verschiedenen Positionen
bewegt, legt nahe, dass es zuständig ist für das Halten seiner aktuellen Position.
 Bei Ankunft an einem Abholpunkt muss ein Fahrzeug das Unternehmen über
die Ankunft am Abholpunkt informieren, mit Taxiunternehmen als Partner; und
Taxiunternehmen hat die Zuständigkeit Ankunft am Abholpunkt zur Kenntnis
nehmen. In der Realität trifft ein Taxi seinen Fahrgast zum ersten Mal, wenn es
am Abholpunkt eintrifft. Somit wäre dies der natürliche Zeitpunkt, an dem ein
Fahrzeug seinen nächsten Fahrgast erhält. Im Modell erhält es diesen vom Taxi-

601
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

unternehmen, das es ursprünglich von der Fahrgastquelle erhalten hat. Zustän-


digkeit von Taxiunternehmen: Fahrgast an Fahrzeug übergeben; Zuständigkeit
von Fahrzeug: Fahrgast erhalten, mit Fahrgast als Partner von Fahrzeug.
 Das Fahrzeug erfragt nun vom Fahrgast sein Fahrziel. Zuständigkeit von Fahr-
zeug: Ziel erfragen; Zuständigkeit von Fahrgast: Fahrziel liefern. Auch hier wird
sich das Verhalten von Taxis und Großraumtaxis unterscheiden. Ein Taxi wird
einfach zum Ziel des Fahrgastes fahren. Ein Großraumtaxi wird das Ziel zur Samm-
lung der Ziele hinzufügen und das nächste Ziel auswählen.
 Bei Ankunft am Ziel des Fahrgastes muss ein Fahrzeug den Fahrgast absetzen
und das Unternehmen über die Zielankunft informieren. Das Taxiunternehmen
muss die Ankunft am Ziel zur Kenntnis nehmen.
Die hier umrissenen Schritte bilden die fundamentalen Aktivitäten eines Taxiunter-
nehmens, die immer wiederholt werden, solange Fahrgäste den Service in Anspruch
nehmen. Ein wichtiger Punkt, der noch angemerkt werden muss, ist dabei, dass
unser Computermodell in der Lage sein muss, diese Schritte durchzuführen, sobald
eine Anfrage eintrifft – auch wenn eine frühere Anfrage noch nicht zu Ende abgear-
beitet wurde. Mit anderen Worten: Innerhalb eines Schrittes könnte ein Fahrzeug
noch auf dem Weg zum Abholpunkt sein, während ein anderes gerade am Ziel
eines Fahrgastes ankommt und ein weiterer Fahrgast gerade ein Taxi ruft.

Übung 16.7 Betrachten Sie noch einmal die Problembeschreibung und das
Szenario, das wir durchgespielt haben. Gibt es noch weitere Szenarios, die wir
berücksichtigen müssen, bevor wir uns an den Klassenentwurf machen kön-
nen? Haben wir beispielsweise das Verhalten für den Fall, in dem kein freies
Fahrzeug eingeteilt werden kann, angemessen berücksichtigt? Vervollständi-
gen Sie die Beschreibung des Szenarios, wenn Ihnen Details fehlen.
Übung 16.8 Denken Sie, dass wir das Szenario ausreichend detailliert beschrie-
ben haben? Haben wir beispielsweise zu wenig oder zu viel über den Unter-
schied zwischen Taxis und Großraumtaxis gesprochen?
Übung 16.9 Denken Sie, dass in dieser Phase bereits angesprochen werden
sollte, wie die Fahrzeuge sich von einer Position zur nächsten bewegen?
Übung 16.10 Denken Sie, dass sich während der Entwicklung ein Bedarf
nach weiteren Klassen herausstellen wird – Klassen, die in der Beschreibung
nicht unmittelbar genannt werden? Wenn ja: Warum ist das so?

16.3 Klassenentwurf
In diesem Abschnitt machen wir den Schritt von einem abstrakten Entwurf auf
Papier zu einem konkreten Rahmenentwurf innerhalb eines BlueJ-Projekts.

16.3.1 Entwurf der Klassenschnittstellen


In Kapitel 15 haben wir für diesen Schritt empfohlen, einen neuen Satz CRC-Kar-
ten anzulegen, auf denen die Zuständigkeiten für jede Klasse in konkrete Metho-

602
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.3 Klassenentwurf

densignaturen umgewandelt werden. Ohne die Wichtigkeit dieses Schritts schmä-


lern zu wollen, werden wir diesen Zwischenschritt Ihnen selbst überlassen und
direkt zu einem Rahmen für ein BlueJ-Projekt übergehen, der Stubs für die Klassen
und Methoden definiert. Dies sollte einen guten Eindruck von der Komplexität des
Projekts vermitteln und aufzeigen, ob wir etwas Entscheidendes bei unseren bishe-
rigen Schritten übersehen haben.
Es sollte an dieser Stelle noch einmal betont werden, dass wir bei jedem Schritt des
Entwicklungszyklus damit rechnen müssen, auf Fehler oder offene Enden aus den
vorherigen Schritten zu stoßen. Dies ist nicht unbedingt ein Zeichen für Schwächen
der verwendeten Techniken oder mangelnde Sorgfalt. Es spiegelt vielmehr den
Umstand wider, dass die Entwicklung eines Projekts ein Erkenntnisprozess ist: Nur
über Ausprobieren und Erkunden kommen wir zu einem vollständigen Verständnis
unseres Projektziels. Das Aufdecken von Lücken im Entwurf bestätigt also eher
unsere Vorgehensweise!

16.3.2 Partnerklassen
Nachdem wir das Zusammenspiel zwischen Klassen identifiziert haben, stellt sich
häufig die Frage, wie ein bestimmtes Objekt Zugriff auf die Objekte seiner Part-
nerklassen bekommt. Es gibt üblicherweise drei unterschiedliche Möglichkeiten
dazu, die drei verschiedene Muster der Objektinteraktion repräsentieren:
 Ein Partnerobjekt wird als Parameter an einen Konstruktor übergeben. Ein solches
Partnerobjekt wird üblicherweise in einem der Datenfelder des neuen Objekts
gespeichert, sodass es dem Objekt über seine gesamte Lebensdauer zur Verfü-
gung steht. Das Partnerobjekt kann auf diese Weise von mehreren Objekten
gemeinsam benutzt werden. Beispiel: Ein Fahrgastquelle-Objekt bekommt über
seinen Konstruktor ein Taxiunternehmen-Objekt übergeben.
 Ein Partnerobjekt wird als Parameter an eine Methode übergeben. Die Interak-
tion mit einem solchen Partnerobjekt ist üblicherweise befristet – auf die Dauer
der Ausführung der Methode –, obwohl das gerufene Objekt die Referenz
auch in einem Datenfeld für langfristige Interaktion speichern könnte. Beispiel:
Taxiunternehmen bekommt ein Partnerobjekt Fahrgast über die Methode über-
geben, die einen Taxiruf modelliert.
 Das Objekt erzeugt sich sein eigenes Partnerobjekt. Das Partnerobjekt steht
exklusiv nur dem erzeugenden Objekt zur Verfügung, es sei denn, es wird auf
einem der ersten beiden Wege an andere Objekte weitergegeben. Wenn es in
einer Methode erzeugt wird, ist die Zusammenarbeit meist befristet auf die
Ausführungsdauer des Blocks mit dem Konstruktoraufruf. Wenn das Partner-
objekt hingegen in einem Datenfeld abgelegt wird, hält die Zusammenarbeit
meist über die gesamte Lebensdauer des erzeugenden Objekts. Beispiel: Das
Taxiunternehmen erzeugt sich eine Sammlung, um die Fahrzeuge zu halten.

Übung 16.11 Achten Sie bei der Vorstellung des Projekts Taxi-Rahmen im
nächsten Abschnitt speziell darauf, wann Objekte erzeugt werden und wie
kooperierende Objekte sich kennenlernen. Versuchen Sie, mindestens ein
Beispiel für jedes der beschriebenen Muster zu finden.

603
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

16.3.3 Die Rahmenimplementierung


Das Projekt Taxi-Rahmen enthält eine Rahmenimplementierung der Klassen, Zustän-
digkeiten und Kooperationen, die wir im Entwurfsprozess beschrieben haben. Sehen
Sie sich den Quelltext ausführlich an und ordnen Sie die konkreten Klassen den zuge-
hörigen Beschreibungen aus Abschnitt 16.2.3 zu. Listing 16.1 zeigt den Rahmen der
Klasse Fahrzeug aus diesem Projekt.
Listing 16.1
Eine Rahmenimple-
mentierung der Klasse
Fahrzeug.

604
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.3 Klassenentwurf

Bei der Implementierung dieses Rahmenprojekts sind etliche Fragen und Besonder-
heiten aufgetreten. Einige sollen hier erwähnt werden:

605
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

 Es sollten Ihnen einige Unterschiede zwischen dem Entwurf und der Imple-
mentierung auffallen, die sich aus dem Unterschied zwischen Entwurfs- und
Implementierungssprache ergeben. Beispielsweise wurde bei der Diskussion
der Szenarios vorgeschlagen, dass Fahrgastquelle die Zuständigkeit Abhol-
punkt und Ziel für einen Fahrgast generieren und Fahrgast die Zuständigkeit
Abholpunkt und Ziel merken haben sollte. Anstatt diese Zuständigkeiten auf
individuelle Methodenaufrufe abzubilden, ist die natürlichere Schreibweise in
Java etwas wie
new Fahrgast(new Position(...), new Position(...))
 Wir haben sichergestellt, dass unser Projektrahmen vollständig genug ist, um
übersetzt werden zu können. Das ist zu einem Zeitpunkt wie diesem nicht
unbedingt notwendig, aber es erleichtert die inkrementelle Entwicklung in den
nächsten Schritten. Andererseits birgt es aber auch die Gefahr, dass fehlender
Code nicht so leicht entdeckt wird, da der Compiler die offenen Enden nicht
aufzeigen kann.
 Die gemeinsamen und unterschiedlichen Elemente der Klassen Fahrzeug, Taxi
und Großraumtaxi nehmen erst im Lauf ihrer Weiterentwicklung Gestalt an.
Beispielsweise findet sich die unterschiedliche Behandlung eines Taxirufs durch
die beiden Fahrzeugtypen darin wieder, dass wir die Methode setzeAbholpunkt
als abstrakte Methode in Fahrzeug definiert haben, die in den beiden Unter-
klassen unterschiedlich implementiert werden kann. Auf der anderen Seite
können sie, auch wenn sie unterschiedlich über ihr nächstes Zwischenziel ent-
scheiden, das Konzept eines Ziels teilen. Deshalb ist das Datenfeld ziel in der
Superklasse implementiert.
 An zwei Punkten im Szenario soll ein Fahrzeug das Unternehmen über seine
Ankunft benachrichtigen, am Abholpunkt und am Ziel. Es gibt mindestens
zwei Möglichkeiten, dies in der Implementierung zu realisieren. Der direkte
Weg wäre, wenn jedes Fahrzeug eine Referenz auf sein Unternehmen hätte.
Dies würde zu einer expliziten Verbindung zwischen beiden Klassen im Klas-
sendiagramm führen.
Eine Alternative wäre der Einsatz des Beobachter-Musters gewesen, das in Kapi-
tel 15 vorgestellt wurde. Das Fahrzeug könnte dabei die Klasse Subjekt erweitern
und das Taxiunternehmen würde das Interface Beobachter implementieren. Die di-
rekte Kopplung zwischen Fahrzeug und Taxiunternehmen wäre reduziert, aber es
gäbe noch immer eine implizite Kopplung und der Benachrichtigungsprozess
wäre etwas aufwendiger zu implementieren.
 Bisher gab es keine Diskussion darüber, wie viele Fahrgäste ein Großraumtaxi auf-
nehmen kann. Gibt es möglicherweise Großraumtaxis unterschiedlicher Kapazi-
tät? Diesen Aspekt schieben wir für eine spätere Auflösung auf.
Es gibt keine feste Regel, wie weit man mit einer Rahmenimplementierung für
eine spezifische Anwendung gehen sollte. Der Zweck eines solchen Rahmens ist
nicht, ein vollständig funktionierendes Projekt zu liefern, sondern er soll die Rah-
menstruktur der Anwendung festhalten (die vorher durch den Entwurf mit CRC-
Karten ausgearbeitet wurde). Wenn Sie sich die Klassen im Projekt Taxi-Rahmen

606
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.3 Klassenentwurf

ansehen, geht Ihnen diese Umsetzung möglicherweise schon etwas zu weit, oder
sie geht Ihnen nicht weit genug. Positiv an einem übersetzbaren Rahmen ist, dass
wir schon gezwungen waren, über die Vererbungshierarchie für Fahrzeug nachzu-
denken: insbesondere darüber, welche Methoden wir vollständig in der Super-
klasse implementieren können und welche wir besser abstrakt lassen. Als negati-
ver Aspekt besteht die Gefahr, dass einige Implementierungsentscheidungen zu
früh getroffen werden, beispielsweise durch das Festlegen von Datenstrukturen,
die erst später festgelegt werden sollten oder durch die bereits getroffene Ent-
scheidung, dem Beobachter-Muster eine direktere Lösung vorzuziehen.

Übung 16.12 Sehen Sie sich für jede Klasse im Projekt die Schnittstelle an
und schreiben Sie eine Liste von JUnit-Tests, mit denen die Funktionalität
jeder Klasse getestet werden kann.
Übung 16.13 Das Projekt Taxi-Rahmen definiert eine Klasse Demo, die ein
Paar, bestehend aus einem Fahrgastquelle-Objekt und einem Taxiunternehmen-
Objekt, erzeugt. Erzeugen Sie ein Objekt der Klasse Demo und rufen Sie seine
Methode testeAbholung auf. Warum kann das Taxiunternehmen zu diesem Zeit-
punkt noch keinen Taxiruf bestätigen?
Übung 16.14 Denken Sie, dass wir den Quelltext schon hätten weiterentwi-
ckeln können, damit auf dieser Stufe zumindest schon ein Taxiruf erfolgreich
ist? Wenn ja: Wie viel weiter hätten Sie die Implementierung vorgenommen?

16.3.4 Testen
Nachdem wir mit der Implementierung begonnen haben, sollten wir nicht zu
schnell voranschreiten, bevor wir nicht über das Testen der Anwendung nachge-
dacht haben. Wir wollen nicht den Fehler begehen, die Implementierung erst
dann zu testen, wenn sie abgeschlossen ist. Wir können bereits einige Tests for-
mulieren, die sich dann mit der Implementierung weiterentwickeln können. Füh-
ren Sie die folgenden Übungen durch, um ein Gefühl dafür zu bekommen, wel-
che Teile in diesem frühen Stadium bereits getestet werden können.

Übung 16.15 Das Projekt Taxi-Rahmen-Tests enthält drei einfache JUnit-Test-


klassen, die einige grundlegende Tests definieren. Führen Sie diese aus. Fügen
Sie weitere Tests hinzu, die Ihrer Meinung nach für diese Stufe bereits ange-
messen sind und die Grundlage für zukünftige Tests bilden können. Spielt es
eine Rolle, dass so definierte Tests zu diesem Zeitpunkt fehlschlagen?
Übung 16.16 Die Klasse Position definiert momentan keine Datenfelder
und Methoden. Inwieweit würde eine Weiterentwicklung dieser Klasse die
Testklassen beeinflussen?

607
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

16.3.5 Einige weitere offene Fragen


Ein wichtiger Punkt, den wir bisher noch nicht berücksichtigt haben, ist die Orga-
nisation des Ablaufs der verschiedenen Aktivitäten: Fahrgastanfragen, Bewegen
der Fahrzeuge etc. Ein weiterer Punkt ist, dass wir den Positionen noch keine
detaillierte Form gegeben haben und Bewegungen somit noch gar keinen Effekt
haben. Während wir die Anwendung weiterentwickeln, werden sich die Lösun-
gen dieser und anderer Punkte ergeben.

16.4 Iterative Entwicklung


Wir haben offensichtlich noch einen weiten Weg zu gehen von der Rahmenimple-
mentierung in Taxi-Rahmen bis zu einer endgültigen Version. Aber statt überwäl-
tigt zu sein von der Komplexität der Gesamtaufgabe, können wir uns die Dinge
etwas vereinfachen, indem wir einige kleinere Zwischenschritte zum Gesamtziel
identifizieren und diese iterativ bearbeiten.

16.4.1 Entwicklungsstufen
Die Planung von Entwicklungsstufen hilft uns bei den Überlegungen, wie wir eine
große Aufgabe in mehrere kleine zerlegen können. Diese Teilaufgaben sind dann
meist leichter zu bewältigen als die eine große Aufgabe, lösen aber in ihrem Zusam-
menspiel letztlich die Gesamtaufgabe. Während wir die Teilaufgaben bearbeiten,
stellen wir möglicherweise fest, dass wir auch diese weiterunterteilen müssen.
Außerdem stellen wir vielleicht fest, dass sich einige unserer Annahmen als falsch
erwiesen oder wir einen ungeeigneten Entwurf gewählt haben. Dieser Erkennt-
nisprozess, kombiniert mit einem iterativen Entwicklungsansatz, führt dazu, dass
wir wertvolles Feedback zu unserem Entwurf und unseren Entscheidungen zu
einem sehr frühen Zeitpunkt bekommen, sodass wir es wieder einarbeiten können
und insgesamt zu einem flexiblen und sich selbst verbessernden Prozess kommen.
Überlegungen, in welche Schritte die Gesamtaufgabe zerlegt werden kann, haben
außerdem den Vorteil, dass sie zum Erkennen der Zusammenhänge zwischen Tei-
len der Anwendung führen. In großen Projekten hilft uns das, die Schnittstellen
zwischen Komponenten zu identifizieren. Das Identifizieren von Stufen hilft auch
bei der zeitlichen Planung des Entwicklungsprozesses.
Es ist wichtig, dass jede Stufe bei einer iterativen Entwicklung einen klar erkenn-
baren Schritt in Richtung der Gesamtanforderung darstellt. Insbesondere muss
klar sein, wann eine Stufe abgeschlossen wurde. Mit dem Abschluss sollten alle
Tests erfolgreich durchlaufen und es sollte über das Erreichte reflektiert werden,
damit die neuen Erkenntnisse in die nachfolgenden Schritte einfließen können.
Folgende Entwicklungsstufen sind für die Anwendung unseres Taxiunternehmens
denkbar:
 Ein einzelner Fahrgast wird abgeholt und von einem einzelnen Taxi an sein Ziel
gebracht.
 Mehrere Taxis stehen zur Verfügung, um mehrere Fahrgäste simultan abzu-
holen und zu ihren Zielen zu fahren.

608
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.4 Iterative Entwicklung

 Ein einzelner Fahrgast wird von einem Großraumtaxi abgeholt und zu seinem
Ziel gebracht.
 Informationen über Fahrgäste, die kein freies Taxi bekommen, werden proto-
kolliert.
 Ein Großraumtaxi sammelt mehrere Fahrgäste auf und fährt sie zu ihren ver-
schiedenen Zielen.
 Eine grafische Oberfläche zeigt die Aktivitäten aller Fahrzeuge und Fahrgäste
innerhalb der Simulation an.
 Taxis und Großraumtaxis können parallel betrieben werden.
 Die restliche Funktionalität steht zur Verfügung, komplett mit allen statistischen
Daten.
Wir werden die Implementierung all dieser Stufen nicht im Detail diskutieren, aber
wir werden die Anwendung bis zu einem Punkt vervollständigen, ab dem Sie die
übrige Funktionalität selbst hinzufügen können.

Übung 16.17 Werfen Sie einen kritischen Blick auf die Stufen, die wir hier
formuliert haben, mit den folgenden Fragen im Hinterkopf: Denken Sie,
dass die Reihenfolge angemessen ist? Ist die Komplexität der einzelnen Stu-
fen zu hoch, zu niedrig oder gerade richtig? Fehlen Stufen? Überarbeiten
Sie die Liste in Ihrem Sinne, damit Ihre Sicht des Projekts berücksichtigt ist.
Übung 16.18 Sind die Abschlusskriterien (Tests bei Abschluss) für alle Stu-
fen ausreichend offensichtlich? Wenn ja, dann dokumentieren Sie einige
Tests für jede Stufe.

16.4.2 Eine erste Stufe


Auf der ersten Stufe wollen wir ermöglichen, dass ein einzelner Fahrgast erzeugt
wird, von einem einzelnen Taxi abgeholt und an seinem Ziel abgeliefert wird. Dazu
müssen wir an einigen Klassen arbeiten: mit Sicherheit an Position, Taxi und Taxi-
unternehmen, möglicherweise auch an weiteren. Zusätzlich müssen wir die simu-
lierte Zeit erfassen, die ein Taxi für seine Fahrt durch die Stadt benötigt. Dies legt
nahe, einige der Ideen wiederzuverwenden, die wir in Kapitel 12 im Zusammen-
hang mit Akteuren kennengelernt haben.
Das Projekt Taxi-Stufe-Eins enthält eine Implementierung der Anforderungen der
ersten Stufe. Die Klassen wurden so weit entwickelt, dass ein Taxi einen Fahrgast
aufsammelt und am Ziel abliefert. Die Methode starten der Klasse Demo spielt dieses
Szenario durch. Wichtiger in dieser Stufe sind jedoch die Testklassen FahrgastTest,
FahrgastquelleTest, PositionTest und TaxiTest, die wir in Abschnitt 16.4.3 disku-
tieren werden.
Anstatt das Projekt im Detail zu besprechen, werden wir hier nur auf einige Punkte
hinweisen, die sich bei der Entwicklung von der vorherigen Rahmenversion als
interessant erwiesen haben. Sie sollten der folgenden Diskussion folgen, indem sie
parallel gründlich den Quelltext des Projekts lesen.

609
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

Das Ziel der ersten Stufe war absichtlich nicht sehr ambitioniert, deckt aber den-
noch die fundamentalen Aktivitäten der Anwendung ab – das Abholen und
Abliefern von Fahrgästen. Für diese Wahl gibt es einen guten Grund. Durch ein
einfaches Ziel kann diese Stufe in relativ kurzer Zeit abgeschlossen werden.
Durch die Relevanz des Ziels wird uns diese Aufgabe auf jeden Fall dem Ziel des
Gesamtprojekts näher bringen. Solche Faktoren sorgen für eine gute Motivation.
Wir haben das Konzept der Akteure aus dem Projekt Fuechse-und-Hasen (Kapitel
12) übernommen. Auf dieser Stufe müssen nur Taxis Akteure sein, und zwar über
ihre Superklasse Fahrzeug. In jedem Schritt der Simulation bewegt ein Taxi sich ent-
weder auf sein Ziel zu oder es bleibt ungenutzt (Listing 16.2). Obwohl wir auf die-
ser Stufe noch keine statistischen Daten ermitteln sollen, war es einfach, für Fahr-
zeuge die Anzahl der Simulationsschritte zu erfassen, die sie ungenutzt bleibt. Dies
nimmt bereits Arbeit einer späteren Stufe vorweg.
Listing 16.2
Die Klasse Taxi
als Akteur.

610
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.4 Iterative Entwicklung

Um das Bewegen von Fahrzeugen modellieren zu können, mussten wir die Klasse
Position weiter implementieren als in der Rahmenimplementierung. Auf den ersten
Blick sollte sie ein relativ einfacher Behälter für eine zweidimensionale Position sein. In
der Praxis muss sie allerdings auch einen Test auf die Gleichheit von Positionen anbie-
ten (equals) sowie die Möglichkeit, für ein Fahrzeug die nächste Position zu ermitteln,
abhängig von seiner aktuellen Position und seinem Ziel (naechstePosition). Auf dieser
Stufe gibt es keine Einschränkungen für die Positionen (außer der Beschränkung auf
positive Koordinaten), aber in späteren Stufen wird es eine Möglichkeit geben müs-
sen, den Einsatzbereich des Taxiunternehmens in seinen Grenzen zu definieren.
Eine der schwierigeren Aufgaben war es, die Verknüpfung zwischen einem Fahrgast
und einem Fahrzeug, zwischen der Anfrage um Abholung und der Ankunft zu
modellieren. Obwohl wir nur einen einzelnen Fahrgast und ein einzelnes Taxi behan-
deln müssen, versuchen wir bereits zu berücksichtigen, dass es später mehrere anste-
hende Anfragen geben kann. In Abschnitt 16.2.3 haben wir entschieden, dass ein
Fahrzeug seinen Fahrgast erhalten soll, wenn es das Unternehmen über seine
Ankunft am Abholpunkt benachrichtigt. Wenn eine solche Benachrichtigung ein-
trifft, muss das Unternehmen also feststellen können, welcher Fahrgast dem Fahr-
zeug zugewiesen worden war. Die von uns gewählte Lösung verwendet eine Abbil-
dung von Fahrzeugen auf Fahrgäste, die das Unternehmen in einer Map hält. Wenn
ein Fahrzeug dem Unternehmen seine Ankunft am Abholpunkt mitteilt, übergibt das
Unternehmen dem Fahrzeug seinen Fahrgast. Es gibt jedoch einige Hinweise darauf,
dass diese Lösung nicht perfekt ist, und wir werden dies in einigen der folgenden
Übungen näher untersuchen.

611
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

Die Fehlersituation, dass am Abholpunkt kein Fahrgast vorhanden ist, haben wir
explizit berücksichtigt. Da dies einen Programmierfehler impliziert, haben wir für
diesen Fehler die ungeprüfte Exception FehlenderFahrgastException definiert.
Da in dieser Stufe nur ein einzelner Fahrgast benötigt wird, haben wir die Entwick-
lung der Klasse Fahrgastquelle auf eine spätere Stufe verschoben. Stattdessen wer-
den Fahrgäste direkt in der Klasse Demo und den Testklassen erzeugt.

Übung 16.19 Falls Sie es noch nicht getan haben: Sehen Sie sich die Imple-
mentierung im Projekt Taxi-Stufe-Eins gründlich an. Machen Sie sich insbe-
sondere klar, wie ein Taxi über seine Methode agiere bewegt wird.
Übung 16.20 Denken Sie, dass das Taxiunternehmen-Objekt getrennte Listen
für freie und besetzte Fahrzeuge halten sollte, um die Effizienz der Zuwei-
sung freier Fahrzeuge zu erhöhen? Zu welchem Zeitpunkt würden die Fahr-
zeuge die Liste wechseln?
Übung 16.21 Die nächste geplante Stufe bietet mehrere Taxis, die parallel
mehrere Fahrgäste transportieren. Untersuchen Sie die Klasse Taxiunternehmen
in Hinsicht auf diese Zielsetzung noch einmal. Wird die benötigte Funktionalität
bereits angeboten? Wenn nicht, welche Änderungen sind noch notwendig?
Übung 16.22 Betrachten Sie noch einmal, wie die Abbildung von Fahrzeu-
gen auf Fahrgäste in der Map zuordnungen in Taxiunternehmen erfolgt. Sehen
Sie Schwächen in diesem Ansatz? Ermöglicht er, dass mehr als ein Fahrgast
an derselben Position aufgesammelt wird? Kann es vorkommen, dass für
ein Fahrzeug mehr als eine Zuordnung notwendig wird?
Übung 16.23 Wenn Sie Probleme mit der momentanen Realisierung der
Zuordnung Fahrzeug:Fahrgast sehen, würde es dann helfen, wenn Sie für jede
Zuordnung eine eindeutige Nummer vergeben würden – eine „Buchungsnum-
mer“ etwa? Wenn ja, müsste dann die Signatur von Methoden in der Fahrzeug-
Hierarchie geändert werden? Implementieren Sie eine verbesserte Version, die
die Anforderungen aller Szenarios erfüllt.

16.4.3 Testen der ersten Stufe


Als Teil der Implementierung der ersten Stufe haben wir zwei Testklassen entwickelt:
PositionTest und TaxiTest. Die erste überprüft die Basisfunktionalität der Klasse
Position, die für das Bewegen von Fahrzeugen notwendig ist. Die zweite überprüft,
ob ein Fahrgast mit der korrekten Abfolge der Schritte abgeholt und am Ziel abge-
setzt wird und ob das Taxi anschließend auch wieder frei ist. Um die zweite Testreihe
realisieren zu können, wurde die Klasse Position um eine Methode schritteZu
erweitert, die die Anzahl der Schritte zurückliefert, die zwischen zwei Positionen
zurückgelegt werden muss.1

1 Wir sehen bereits voraus, dass dies im weiteren Verlauf der Implementierung erweitert werden
muss, da das Unternehmen in der Lage sein sollte, Taxis aufgrund ihrer Nähe zu einem Abhol-
punkt zuzuweisen.

612
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.4 Iterative Entwicklung

Im normalen Betrieb läuft die Anwendung still vor sich hin, ohne eine grafische
Oberfläche gibt es keine Visualisierung der Bewegungen eines Taxis. Ein Ansatz wäre
deshalb, Ausgabeanweisungen in die Kernmethoden der Klassen Taxi und Taxi-
unternehmen einzufügen. BlueJ bietet jedoch auch die Möglichkeit, beispielsweise in
der Methode agiere der Klasse Taxi einen Haltepunkt zu setzen. Auf diese Weise
könnte die Bewegung eines Taxis schrittweise verfolgt werden.
Nachdem wir uns ausführlich vergewissert haben, dass dem aktuellen Stand der
Implementierung getraut werden kann, haben wir lediglich eine Ausgabeanweisung
in den Benachrichtigungsmethoden der Klasse Taxiunternehmen belassen, um ein
minimales Feedback zu erhalten.
Als Nachweis für den Wert begleitender Tests während der Implementierung kön-
nen wir erwähnen, dass die existierenden Testklassen zwei schwere Fehler in unse-
rem Quelltext aufgedeckt haben, die wir daraufhin korrigieren konnten.

Übung 16.24 Untersuchen Sie die Tests, die in den Testklassen von Taxi-Stufe-
Eins implementiert sind. Können diese als Regressionstests auf den weiteren
Stufen verwendet oder müssten sie substanziell geändert werden?
Übung 16.25 Implementieren Sie weitere Tests und Testklassen, die Ihrer
Meinung nach das Vertrauen in die Implementierung erhöhen könnten.
Beheben Sie alle dadurch aufgedeckten Fehler.

16.4.4 Eine spätere Stufe der Entwicklung


Wir wollen hier nicht die vollständige Realisierung der Taxi-Anwendung diskutie-
ren, da dies für Sie nicht von großem Nutzen wäre. Stattdessen präsentieren wir
die Anwendung in einer späteren Ausbaustufe und ermutigen Sie dazu, den Rest
selbst zu implementieren.
Abbildung 16.2
Eine Visualisierung
der Stadt.

613
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

Diese fortgeschrittene Stufe der Implementierung finden Sie im Projekt Taxi-spaetere-


Stufe. Diese geht mit mehreren Taxis und Fahrgästen um und präsentiert eine Ani-
mation der Bewegungen beider mit einer grafischen Oberfläche (Abbildung 16.2).
Hier ist eine Übersicht über die wichtigen Entwicklungsschritte, die von der vorigen
Version zu dieser gemacht wurden.
 Eine Klasse Simulation verwaltet nun die Akteure, ähnlich wie im Projekt Fuechse-
und-Hasen. Akteure sind die Fahrzeuge, die Fahrgastquelle und die grafische
Oberfläche, die in der Klasse StadtGUI realisiert ist. Nach jedem Simulationsschritt
wird kurz pausiert, damit die grafische Darstellung sich nicht zu schnell verändert.
 Der Bedarf nach etwas wie der Klasse Stadt hat sich während der Entwicklung
für Stufe eins herausgestellt. Die Klasse Stadt definiert die Dimensionen der
Stadtfläche und hält eine Sammlung aller Gegenstände, die in der Stadt von
Interesse sind: die Fahrzeuge und die Fahrgäste.
 Gegenstände in der Stadt können optional das Interface GrafischerGegenstand
implementieren, das der GUI ermöglicht, diese darzustellen. Grafiken für Fahr-
zeuge und Personen werden zu diesem Zweck im Verzeichnis grafiken im Pro-
jektverzeichnis mitgeliefert.
 Die Klasse Taxi implementiert das Interface GrafischerGegenstand. Es liefert der
GUI unterschiedliche Bilder, je nachdem, ob das Taxi belegt oder frei ist. Die
Bilder sind in einem Unterordner grafiken abgelegt, da sie auch für Großraum-
taxis benötigt werden.
 Die Klasse Fahrgastquelle wurde im Vergleich zur vorigen Version komplett über-
arbeitet, damit sie ihre Rolle als Akteur spielen kann. Außerdem hält sie einen
Zähler der nicht erfüllten Taxirufe für statistische Zwecke.
 Die Klasse Taxiunternehmen ist verantwortlich für das Erzeugen der Taxis, die in
der Simulation verwendet werden.
Beim Studieren des Quelltextes im Projekt Taxi-spaetere-Stufe werden Sie viele Kon-
zepte wiedererkennen, die wir im zweiten Teil dieses Buches behandelt haben: Ver-
erbung, Polymorphie, abstrakte Klassen, Interfaces und Fehlerbehandlung.

Übung 16.26 Fügen Sie Exception werfende Überprüfungen in alle Klassen


ein, um diese gegen fehlerhafte Benutzung zu schützen. Beispielsweise
können Sie sicherstellen, dass kein Fahrgast erzeugt werden kann, bei dem
Abholpunkt und Ziel identisch sind; stellen Sie sicher, dass ein Taxi niemals
zu einem Abholpunkt geschickt wird, wenn es gerade auf dem Weg zu einem
Ziel ist etc.
Übung 16.27 Werten Sie die statistischen Daten aus, die von den Taxis und der
Fahrgastquelle über ungenutzte Zeiten und über vergebliche Taxirufe ermittelt
werden. Experimentieren Sie mit unterschiedlichen Mengen von Taxis, um die
Balance zwischen diesen beiden Kennzahlen einschätzen zu können.
Übung 16.28 Passen Sie die Fahrzeugklassen so an, dass die Zeit bis zum
Abholpunkt und die Zeit bis zum Ziel protokolliert wird. Sehen Sie hier einen
möglichen Konflikt für Großraumtaxis?

614
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.4 Iterative Entwicklung

16.4.5 Weitere Ideen für die Weiterentwicklung


Die Version im Projekt Taxi-spaetere-Stufe stellt einen signifikanten Zwischenschritt
auf dem Weg zur vollständigen Anwendung dar. Es kann aber noch einiges hinzu-
gefügt werden. Beispielsweise haben wir die Klasse Großraumtaxi praktisch noch
gar nicht realisiert, sodass ihre Fertigstellung noch einige Herausforderungen bie-
tet. Der wichtigste Unterschied zwischen einem Taxi und einem Großraumtaxi
besteht darin, dass ein Großraumtaxi mit mehreren Fahrgästen umgehen kann,
während ein Taxi nur einen Fahrgast versorgt. Wenn ein Großraumtaxi bereits
einen Fahrgast hat, sollte dies nicht dazu führen, dass es nicht zu einem weiteren
Abholpunkt geschickt werden kann. Ebenso sollte es auch dann noch eine Anfrage
annehmen können, wenn es bereits auf dem Weg zu einem Abholpunkt ist. Diese
Punkte führen zu Fragen darüber, wie ein Großraumtaxi seine Prioritäten setzt.
Kann es beispielsweise passieren, dass ein Fahrgast hin und her gefahren wird,
während das Großraumtaxi ständig weitere Anfragen annimmt, sodass der Fahrgast
nie abgesetzt wird? Was heißt es für ein Großraumtaxi, nicht frei zu sein? Heißt das,
dass es voller Fahrgäste ist oder dass es genügend Anfragen erhalten hat, um die
Plätze zu füllen? Nehmen Sie an, dass einer der Fahrgäste sein Ziel erreicht, bevor
der letzte Abholpunkt erreicht wird: Kann ein Großraumtaxi dann mehr Anfragen
annehmen, als es Plätze hat?!
Ein anderer Bereich für weitere Entwicklungen ist die Zuteilung von Fahrzeugen.
Das Taxiunternehmen operiert in der vorliegenden Version nicht gerade intelligent.
Welches Fahrzeug sollte gewählt werden, wenn mehrere zur Verfügung stehen?
Momentan wird nicht versucht, Fahrzeuge auf Basis ihrer Entfernung zum Abhol-
punkt zuzuteilen. Das Unternehmen könnte die Methode schritteZu aus der Klasse
Position verwenden, um das Fahrzeug zuzuteilen, das am nächsten am Abholpunkt
ist. Würde dies einen Einfluss auf die durchschnittliche Wartezeit der Fahrgäste
haben? Wie könnten Informationen darüber gesammelt werden, wie lange ein
Fahrgast warten muss? Wie wäre es, wenn ungenutzte Taxis zu einer zentralen Posi-
tion fahren, um für zukünftige Anfragen die Wartezeiten zu verkürzen? Hat die
Größe der Stadt einen Einfluss auf die Effektivität dieses Ansatzes? Ist es bei-
spielsweise in einer großen Stadt besser, wenn sich die Taxis gleichmäßig vertei-
len statt sich im Zentrum zu sammeln?
Könnte die Simulation auch benutzt werden, um mehrere konkurrierende Taxiunter-
nehmen in derselben Stadt zu simulieren? Es könnten mehrere Objekte der Klasse
Taxiunternehmen erzeugt werden und die Fahrgastquelle könnte die Fahrgäste
danach zuteilen, wie schnell die Unternehmen in der Lage sind, auf Anfragen zu
reagieren. Ist dies eine zu große Änderung, um mit der vorliegenden Anwendung
realisiert zu werden?

16.4.6 Wiederverwendung
Bisher war unser Ziel, das Betreiben von Fahrzeugen zu simulieren, um den kom-
merziellen Nutzen für ein Unternehmen zu untersuchen. Sie haben möglicherweise
schon erkannt, dass Teile der Anwendung auch nützlich sein können, wenn das
Unternehmen bereits produktiv ist.

615
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie

Wenn wir beispielsweise einen intelligenten Zuteilungsalgorithmus implementieren,


der ein Fahrzeug für einen Taxiruf auswählt, oder wenn wir ein gutes Schema für die
Verteilung der wartenden Fahrzeuge entwickeln, dann könnten wir entscheiden,
dass diese Algorithmen auch im produktiven Betrieb eingesetzt werden können. Die
visuelle Repräsentation der Positionen der Fahrzeuge könnte ebenfalls helfen.
Mit anderen Worten: Es besteht Potenzial dafür, dass aus der Simulation ein Ver-
waltungssystem für den Betrieb eines echten Taxiunternehmens wird. Die Struk-
tur der Anwendung würde sich dabei natürlich ändern: Das Programm würde die
Taxis nicht mehr kontrollieren und bewegen, sondern lediglich ihre Position verfol-
gen, die durch Empfänger für GPS (das Global Positioning System) in allen Fahrzeu-
gen geliefert wird. Viele der Klassen aus der Simulation könnten jedoch ohne
große Änderungen wiederverwendet werden. Dies demonstriert die Stärke einer
guten Klassenstruktur und eines guten Entwurfs.

16.5 Ein weiteres Beispiel


Es gibt viele weitere Projekte, die Sie ganz ähnlich wie das Taxi-Projekt durchfüh-
ren können. Ein beliebtes Beispiel ist die Verwaltung von Fahrstühlen in einem
großen Gebäude. Hier wird die Koordination zwischen den einzelnen Fahrstühlen
besonders wichtig. In einem geschlossenen Gebäude kann es außerdem möglich
sein, die Anzahl der Personen pro Stockwerk zu schätzen und damit den Bedarf
vorherzusehen. Außerdem kann zeitabhängiges Verhalten berücksichtigt werden –
morgendliches Eintreffen, abendliches Verlassen des Gebäudes und lokale Spitzen
um die Mittagszeit herum.
Benutzen Sie den Ansatz, den wir in diesem Kapitel dargestellt haben, um die
Simulation eines Gebäudes mit mehreren Fahrstühlen zu implementieren.

16.6 Ein Blick nach vorn


Wir können Sie nur bis zu einem bestimmten Punkt bringen, indem wir unsere eige-
nen Ideen für Projekte präsentieren und zeigen, wie wir sie entwickeln würden. Sie
werden feststellen, dass Sie sehr viel weiter gehen können, wenn Sie Ihre eigenen
Ideen für Projekte entwickeln und diese auf Ihre eigene Weise implementieren. Wäh-
len Sie ein Thema, das Sie interessiert, und durchlaufen Sie die Stufen, die wir umris-
sen haben: Analysieren Sie das Problem, arbeiten Sie Szenarios aus, machen Sie
einen ersten Entwurf, planen Sie einige Implementierungsstufen und fangen Sie an.
Der Entwurf und die Implementierung von Programmen ist eine spannende und
kreative Tätigkeit. Wie bei jeder lohnenswerten Tätigkeit braucht es einige Zeit,
ehe man sie beherrscht. Lassen Sie sich also nicht entmutigen, wenn Ihre ersten
Versuche sehr lange brauchen oder voller Fehler sind. Das ist völlig normal; Sie
werden sich mit wachsender Erfahrung verbessern. Seien Sie zu Anfang nicht zu
ehrgeizig und rechnen Sie damit, dass Sie Ihre Vorstellungen unterwegs anpas-
sen müssen – all das gehört zum natürlichen Lernprozess.
Vor allem aber gilt: Haben Sie Spaß an der Sache!

616
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

A Arbeiten mit BlueJ-Projekten

A.1 BlueJ installieren


Um mit BlueJ arbeiten zu können, müssen Sie das BlueJ-System herunterladen und
installieren. Das System können Sie kostenlos von [Link] herunter-
laden. Der Download für Windows und Mac OS enthält eine Kopie des JDK (das
Java-System) – es muss nicht separat installiert werden. Unter Linux werden die
Paketabhängigkeiten automatisch die richtige JDK-Version installieren, falls nötig.

A.2 Ein Projekt öffnen


Sie können alle Beispielprojekte von der Companion Website dieses Buch herunter-
laden:
[Link]
Die englische Version der Quelltexte finden Sie unter:
[Link]
Laden Sie die zip-Datei herunter und entpacken Sie diese im Ordner Projekte.
Nachdem Sie BlueJ installiert und mit einem Doppelklick auf das sein Symbol
gestartet haben, wählen Sie den Eintrag PROJEKT ÖFFNEN… im Menü PROJEKT. Navi-
gieren Sie in den Ordner Projekte und wählen Sie eines der Projekte. (Sie können
mehrere Projekte parallel geöffnet haben.) Jeder Projektordner enthält eine Datei
[Link], die mit BlueJ verknüpft ist und auf die Sie nur doppelklicken müs-
sen, um das Projekt direkt zu öffnen.
Weitere Informationen über den Umgang mit BlueJ finden Sie im Tutorial zu
BlueJ. Dieses Tutorial ist über den Eintrag BLUEJ-TUTORIAL im HILFE-Menü von BlueJ
erreichbar.

A.3 Der Debugger in BlueJ


Informationen über die Benutzung des Debuggers finden Sie in Anhang F und im
BlueJ-Tutorial. Das Tutorial ist über den Eintrag BLUEJ-TUTORIAL im HILFE-Menü von
BlueJ erreichbar.

617
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang A Arbeiten mit BlueJ-Projekten

A.4 BlueJ konfigurieren


Sie können viele Einstellungen von BlueJ ändern, um BlueJ besser auf Ihre indivi-
duelle Situation abzustimmen. Einiges können Sie über den Dialog EINSTELLUNGEN
in BlueJ verändern, aber viele weitere Konfigurationseinstellungen können geän-
dert werden, indem Sie die Datei mit den BlueJ-Definitionen editieren. Der Pfad
dieser Datei lautet <bluej_home>/lib/[Link], wobei <bluej_home> den Ord-
ner bezeichnet, in dem das BlueJ-System installiert ist.1
Details zur Konfiguration werden im „Tips archive“ auf den BlueJ-Webseiten erläu-
tert. Sie erreichen das Archiv über
[Link]
Im Folgenden sind die Punkte beschrieben, die am häufigsten angepasst werden.
Viele weitere finden Sie, indem Sie sich die Datei [Link] genauer ansehen.

A.5 Auf deutsche Schnittstelle umstellen


Sie können BlueJ so konfigurieren, dass die Benutzungsschnittstelle in Deutsch ange-
zeigt wird, indem Sie [Link] in einem beliebigen Editor editieren.
In der Datei finden sie unter anderem die folgenden Zeilen:
[Link]=english
#[Link]=afrikaans
#[Link]=catalan
#[Link]=chinese
#[Link]=czech
#[Link]=danish
#[Link]=dutch
#[Link]=french
#[Link]=german
#[Link]=greek

Ein Hash-Symbol (#) am Anfang der Zeile repräsentiert einen Kommentar. Die
Zeile, die nicht mit einem Hash-Symbol beginnt, ist folglich die ausgewählte Ein-
stellung (hier: [Link]=english).
Sie können Deutsch als Benutzersprache auswählen, indem Sie diese Zeilen wie
folgt ändern:
#[Link]=english
#[Link]=afrikaans
#[Link]=catalan
#[Link]=chinese
#[Link]=czech
#[Link]=danish
#[Link]=dutch

1 Unter Mac OS ist die Datei [Link] im Anwendungspaket enthalten. Unter „Tips archive“
finden Sie nähere Angaben, wo Sie die Datei finden können.

618
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
A.6 Einbinden einer lokalen API-Dokumentation

#[Link]=french
[Link]=german
#[Link]=greek
Speichern Sie die Datei und starten Sie BlueJ.

A.6 Einbinden einer lokalen


API-Dokumentation
Sie können eine lokale Kopie der Dokumentation der Java-Klassenbibliotheken
(API) benutzen. Auf diese Weise erfolgt der Zugriff auf die Dokumentation schnel-
ler und sie müssen zum Lesen nicht online sein. Dazu laden Sie die Datei mit der
Java-Dokumentation von [Link]
loads/ herunter und entpacken sie an einem Ort, an dem Sie die Dokumentation
ablegen wollen. Dies erzeugt einen Ordner docs.
Starten Sie dann einen Webbrowser und öffnen Sie mit diesem über die Funktion
DATEI ÖFFNEN (oder einer ähnlichen Funktion) die Datei docs/api/[Link], die
innerhalb des besagten Ordners liegt. Wenn die API-Ansicht korrekt im Browser
angezeigt wird, kopieren Sie die URL (die Web-Adresse) aus dem Adressfeld des
Browsers, öffnen BlueJ und darin den Dialog WERKZEUGE/EINSTELLUNGEN…, wählen
den Reiter DIVERSES und fügen dort die URL in das Feld mit der Bezeichnung URL
DER JDK-DOKUMENTATION ein. Über den Eintrag JAVA KLASSENBIBLIOTHEKEN im Menü
HILFE sollten Sie nun Ihre lokale Kopie erreichen.

A.7 Vorlagen für neue Klassen ändern


Wenn Sie eine neue Klasse anlegen, wird der Quelltext der Klasse mit einem vorein-
gestellten Text vorgegeben. Dieser Text wird einer Vorlage entnommen, die an Ihre
Erfordernisse angepasst werden kann.
Die Vorlagen (templates) liegen in den Ordnern
<bluej_home>/lib/<Sprache>/templates/ und
<bluej_home>/lib/<Sprache>/templates/newclass/,
wobei <bluej_home> das Installationsverzeichnis von BlueJ ist und <Sprache> die
aktuelle Spracheinstellung benennt (beispielsweise german).
Die Vorlagen sind reine Textdateien und können mit jedem Standardeditor bear-
beitet werden.

619
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

B Datentypen in Java

Java kennt zwei Arten von Typen: primitive Typen und Objekttypen.
Primitive Typen werden direkt in Variablen gespeichert und haben Wertsemantik
(Werte werden bei einer Zuweisung an eine andere Variable kopiert). Primitive
Typen haben nichts mit Klassen zu tun und verfügen nicht über Methoden.
Objekttypen werden als Referenzen auf Objekte gespeichert (nicht die Objekte
selbst). Bei einer Zuweisung an eine andere Variable wird nur die Referenz kopiert,
nicht das Objekt. (Sie erfahren mehr über die Referenzsemantik im Abschnitt B.3.)

B.1 Primitive Typen


Die folgende Tabelle führt alle primitiven Typen der Sprache Java auf:

Typname Beschreibung Beispielkonstanten


Ganze Zahlen:
byte ganze Zahl in 8 Bit 24 –2
short ganze Zahl in 16 Bit 137 –119
int ganze Zahl in 32 Bit 5409 –2003
long ganze Zahl in 64 Bit 423266353 55L
Gleitpunktzahlen:
float einfache Fließkommazahl 43.889F
(32 Bit)
double doppelte Fließkommazahl 42.63 2.4e5
(64 Bit)
Andere Typen:
char einzelnes Zeichen (16 Bit) 'm' '?' '\u00F6'
boolean ein boolescher Wert true false
(wahr oder falsch)

621
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang B Datentypen in Java

Hinweise:
 Eine Zahl ohne Dezimalpunkt wird üblicherweise als ein int aufgefasst, aber
automatisch konvertiert, wenn sie einer Variablen vom Typ byte, short oder
long zugewiesen wird (wenn der Wert passt). Sie können eine Konstante als
long deklarieren, indem Sie ein L an die Zahl anhängen (auch l – ein kleines L –
kann benutzt werden, sollte aber wegen der Verwechslungsgefahr mit der
Eins vermieden werden).
 Eine Zahl mit einem Dezimalpunkt ist vom Typ double. Sie können eine float-
Konstante definieren, indem Sie ein f oder ein F an die Zahl anhängen.
 Ein Zeichen kann als Unicode-Zeichen in einfachen Anführungsstrichen oder
als Unicode-Wert mit einem vorangestellten \u angegeben werden.
 Die beiden booleschen Konstanten sind true und false.
Da Variablen mit primitivem Typ nicht auf Objekte verweisen, stehen für die pri-
mitiven Typen keine Methoden zur Verfügung. Wenn jedoch in bestimmten
Zusammenhängen ein Objekttyp benötigt wird, kann über Autoboxing ein primi-
tiver Wert automatisch in ein korrespondierendes Objekt umgewandelt werden.
In Anhang B.4 finden Sie mehr Informationen dazu.
Die folgende Tabelle gibt die verfügbaren Minimal- und Maximalwerte der nume-
rischen Typen an.

Typ Minimum Maximum


byte -128 127
short -32768 32767
int -2147483648 2147483647
long -9223372036854775808 9223372036854775807

Positives Minimum Positives Maximum


float 1.4e-45 3.4028235e38
double 4.9e-324 1.7976931348623157e308

B.2 Cast-Operator für primitive Typen


Manchmal ist es erforderlich, den Wert eines primitiven Typs in einen Wert eines
anderen primitiven Typs umzuwandeln. Meist soll dabei ein Wert mit einem
bestimmten Wertebereich in einen Wert mit einem kleineren Wertebereich umge-
wandelt werden. Dies wird auch als Casting bezeichnet. Casting geht fast immer
einher mit einem Verlust an Informationen, wie es zum Beispiel der Fall ist, wenn
wir einen Fließkommatyp in einen ganzzahligen Typ umwandeln. Java erlaubt die
Typumwandlung zwischen allen numerischen Datentypen. Allerdings ist es nicht
möglich, einen booleschen Wert durch Casting in einen anderen Datentyp umzu-
wandeln oder umgekehrt.

622
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
B.4 Wrapper-Klassen

Der Cast-Operator besteht aus dem in runden Klammern gesetzten Namen eines pri-
mitiven Datentyps und steht vor einer Variablen oder einem Ausdruck. Zum Beispiel
int wert = (int) durchschnitt;
Wenn durchschnitt eine Variable vom Datentyp double ist und den Wert 3,9 ent-
hält, würde die obige Anweisung in der Variablen wert den ganzzahligen Wert 3
speichern (Umwandlung durch Abrundung).

B.3 Objekttypen
Alle Typen, die nicht im Abschnitt B.1 aufgeführt sind, sind Objekttypen. Dies
schließt Klassen- und Interface-Typen aus der Standardbibliothek von Java (wie
String) ebenso ein wie benutzerdefinierte Typen.

Eine Variable eines Objekttyps hält eine Referenz (oder einen „Zeiger“) auf ein
Objekt. Zuweisung und Parameterübergabe haben Referenzsemantik (es wird die
Referenz kopiert, nicht das Objekt). Nach Zuweisung einer Variablen an eine
andere verweisen beide auf dasselbe Objekt. Man sagt, die beiden Variablen sind
Aliase für dasselbe Objekt. Diese Regel gilt nicht nur für eine einfache Zuweisung
zwischen Variablen, sondern auch, wenn ein Objekt als aktueller Parameter an
eine Methode übergeben wird. Folglich bleiben Zustandsänderungen an einem
Objekt über einen formalen Parameter nach Beenden der Methode in dem aktu-
ellen Parameter bestehen.
Klassen sind Schablonen für Objekte, die die Datenfelder und Methoden definie-
ren, die jede Instanz besitzt.
Arrays verhalten sich wie Objekttypen – auch sie haben Referenzsemantik. Es
gibt keine Klassendefinition.

B.4 Wrapper-Klassen
Zu jedem primitiven Typ in Java gibt es eine korrespondierende Wrapper-Klasse,
die den jeweiligen primitiven Typ als Objekttyp repräsentiert. Dies ermöglicht den
Einsatz primitiver Typen auch an Stellen, an denen Objekttypen erwartet werden,
indem das sogenannte Autoboxing zur Anwendung kommt. Die folgende Tabelle
benennt die primitiven Typen und die zugeordneten Wrapper-Klassen aus dem
Paket [Link]. Abgesehen von Integer und Character entsprechen die Namen
der Wrapper-Klassen den Namen der primitiven Typen mit einem großen Anfangs-
buchstaben.
Immer wenn ein primitiver Typ in einem Kontext verwendet wird, der einen Objekt-
typ erfordert, wendet der Compiler Autoboxing an, indem er automatisch den pri-
mitiven Wert in ein entsprechendes Wrapper-Objekt einbettet. Dies bewirkt bei-
spielsweise, dass ein primitiver Wert direkt in eine Sammlung eingefügt werden
kann. Die inverse Operation – Unboxing – wird ebenfalls automatisch angewen-
det, wenn ein Wrapper-Objekt in einem Kontext verwendet wird, der den Wert des
entsprechenden primitiven Typs erwartet.

623
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang B Datentypen in Java

Primitiver Typ Wrapper-Typ


byte Byte
short Short
int Integer
long Long
float Float
double Double
char Character
boolean Boolean

B.5 Cast-Operator für Objekttypen


Da viele Objekte Teil einer Vererbungshierarchie von Objekttypen sind, ist es
manchmal notwendig, die Objektreferenz eines Datentyps in eine Referenz eines
Subtyps tiefer in der Vererbungshierarchie umzuwandeln. Dieser Prozess wird auch
als Casting (oder Downcasting) bezeichnet. Der zugehörige Cast-Operator besteht
aus dem in runden Klammern gesetzten Namen des Zieltyps (eine Klasse oder ein
Interface) und steht vor einer Variablen oder einem Ausdruck. Zum Beispiel
Auto a = (Auto) kfz;
Wenn der deklarierte (d.h. statische) Typ der Variablen kfz vom Typ Kraftfahrzeug
ist und Auto eine Subklasse von Kraftfahrzeug ist, wird sich diese Anweisung kom-
pilieren lassen. Zur Laufzeit wird zusätzlich geprüft, ob das Objekt, auf das sich
kfz bezieht, wirklich ein Auto ist und nicht eine Instanz eines anderen Subtyps.

Beachten Sie unbedingt, dass das Casting zwischen Objekttypen etwas völlig
anderes ist als das Casting zwischen primitiven Typen (Anhang B.2). Insbesondere
bewirkt das Casting zwischen Objekttypen keine Änderung des betreffenden
Objekts. Es geht allein darum, Zugriff auf Typinformationen zu erhalten, die
bereits Teil des Objekts sind, weil sie zu seinem dynamischen Typ gehören.

624
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

C Operatoren

C.1 Arithmetische Ausdrücke


Java bietet zahlreiche Operatoren für arithmetische und logische Ausdrücke.
Tabelle C.1 zeigt alle Konstrukte, die als Operatoren eingestuft werden, einschließ-
lich dem Cast-Operator und der Parameterübergabe. Bei den meisten Operatoren
handelt es sich entweder um binäre Operatoren (deren Operanden links und rechts
stehen) oder um unäre Operatoren (die nur einen Operanden haben). Die wichtigs-
ten binären arithmetischen Operatoren lauten:
+ Addition

– Subtraktion

* Multiplikation

/Division

% Modulo oder ganzzahliger Rest bei Division

Die Ergebnisse der Operationen Division und Modulo hängen davon ab, ob ihre
Operanden ganze Zahlen oder Fließkommazahlen sind. Bei einer Division zwischen
zwei int-Werten wird ein int-Ergebnis ohne Rest geliefert, aber zwischen Fließ-
kommawerten wird ein Fließkommawert geliefert:
5 / 3 liefert als Ergebnis 1

5.0 / 3 liefert als Ergebnis 1.66666666666666667


(Beachten Sie, dass nur einer der Operanden eine Fließkommazahl sein muss, um
zu einem Fließkommaergebnis zu führen.)
Wenn in einem arithmetischen Ausdruck mehr als ein Operator enthalten ist, wer-
den für die Auswertungsreihenfolge Prioritätsregeln für Operatoren angewendet.
In Tabelle C.1 erscheinen Operatoren mit höherer Priorität weiter oben; wir sehen
also beispielsweise, dass Multiplikation, Division und Modulo eine höhere Priorität
haben als die Addition und die Subtraktion. Die folgenden Ausdrücke liefern des-
halb beide das Ergebnis 100:
51 * 3 - 53
154 - 2 * 27
Binäre Operatoren mit derselben Priorität werden von links nach rechts ausge-
wertet, während unäre Operatoren mit derselben Priorität von rechts nach links
ausgewertet werden.

625
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang C Operatoren

Wenn es notwendig wird, die Auswertungsreihenfolge zu ändern, können Klam-


mern verwendet werden. So lautet das Ergebnis der beiden folgenden Beispiele
100:
(205 - 5) / 2
2 * (47 + 3)
Die wichtigsten unären Operatoren sind -, |, ++, --, [] und new. Sicher ist Ihnen
aufgefallen, dass ++ und -- in den beiden obersten Reihen der Tabelle C.1 aufge-
führt sind. Die Operatoren in der obersten Reihe haben ihren Operanden zur Lin-
ken und die in der zweiten Reihe zur Rechten.

Tabelle C.1 [] . ++ -- (Parameterliste)


Java-Operatoren,
höchste Priorität oben. ++ -- + - ! ~
new (Cast)
* / %
+ -
<< >> >>>
< > >= <= instanceof
== !=
&
^
|
&&
||
?:
= += -= *= /= %= >>= <<= >>>= &= |= ^=

C.2 Boolesche Ausdrücke


In booleschen Ausdrücken werden Operatoren auf Operanden angewendet, um
entweder den Wert false oder den Wert true zu liefern. Solche Ausdrücke wer-
den oft in bedingten Anweisungen für die Bedingung und in Schleifen für die
Schleifenbedingung verwendet.
Die Vergleichsoperatoren kombinieren üblicherweise ein Paar arithmetische Ope-
randen. Vergleichsoperatoren in Java sind:

== gleich-zu > größer-als


!= nicht-gleich-zu <= kleiner-gleich
< kleiner-als >= größer-gleich

626
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
C.3 Abkürzungsoperatoren

Die binären logischen Operatoren kombinieren zwei boolesche Ausdrücke, um


ebenfalls einen booleschen Wert zu liefern. Die Operatoren sind:
&& und

|| oder

^ exklusiv-oder

Zusätzlich kann mit dem Operator


! nicht

ein boolescher Ausdruck von true in false umgewandelt werden oder umgekehrt.

C.3 Abkürzungsoperatoren
Sowohl && als auch || werden auf ungewöhnliche Weise ausgewertet. Wenn der
linke Operand von && den Wert false hat, dann ist der Wert des rechten Operan-
den irrelevant und wird nicht ausgewertet. In ähnlicher Weise wird der rechte Ope-
rand von || nicht ausgewertet, wenn der linke Operand den Wert true hat. Sie
sind deshalb auch als Abkürzungsoperatoren (short circuit operators) bekannt.

627
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

D Kontrollstrukturen in Java

D.1 Kontrollstrukturen
Kontrollstrukturen regeln die Abfolge, in der Anweisungen ausgeführt werden.
Es gibt im Wesentlichen zwei Kategorien von Kontrollstrukturen: Auswahlanwei-
sungen und Schleifen.
Eine Auswahlanweisung ist wie eine Gabelung, an der eine Entscheidung getrof-
fen werden muss, welchem Weg durch den Rumpf der Methode (oder des Kons-
truktors) zu folgen ist. Eine if-else-Anweisung verlangt eine Entscheidung zwischen
zwei Anweisungsblöcken, während eine switch-Anweisung mehrere alternative
Anweisungsblöcke zur Auswahl anbietet.
Schleifen ermöglichen die wiederholte Ausführung eines Anweisungsblocks. Die
Anzahl der Wiederholungen kann vorgegeben oder undefiniert sein. Für die erste
Variante werden typischerweise for-each- oder for-Schleifen eingesetzt, die zweite
Variante wird meist mit while- oder do-Schleifen realisiert.
Diese Charakterisierungen sind allerdings nicht verbindlich, Abweichungen sind
in der Praxis häufig zu finden. Beispielsweise kann man die if-else-Anweisung
auch zur Auswahl zwischen mehreren Alternativen nutzen, wenn der else-Teil
eine untergeordnete (verschachtelte) if-else-Anweisung enthält, und eine for-
Schleife kann auch für eine nicht festgelegte Anzahl von Wiederholungen einge-
setzt werden.

D.2 Auswahlanweisungen

D.2.1 if-else
Die if-else-Anweisung gibt es in zwei Varianten, die beide durch einen booleschen
Ausdruck gesteuert werden.
if (Ausdruck { if (Ausdruck) {
Anweisungen Anweisungen
} }
else {
Anweisungen
}

629
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang D Kontrollstrukturen in Java

In der ersten Variante entscheidet der boolesche Ausdruck darüber, ob die Anwei-
sungen ausgeführt werden oder nicht. In der zweiten Variante entscheidet der Aus-
druck, welcher der beiden alternativen Anweisungsblöcke ausgeführt werden soll.
Beispiele:
if ([Link]() == 0) {
[Link]("Das Feld ist leer.");
}
if (zahl < 0) {
meldeFehler();
}
else {
verarbeiteZahl(zahl);
}
Eine in der Praxis recht häufig zu findende Konstruktion ist die Verknüpfung von if-
else-Anweisungen, wobei eine zweite if-else-Anweisung in den else-Teil der ers-
ten Anweisung eingefügt wird. Dieses Muster kann beliebig oft wiederholt werden,
Sie sollten aber darauf achten, die Konstruktion mit einem else-Teil abzuschließen.
if (zahl < 0) {
verarbeiteNegative(zahl);
}
else if(zahl == 0) {
verarbeiteNull();
}
else {
verarbeitePositive(zahl);
}

D.2.2 switch
Die switch-Anweisung verzweigt auf Basis eines einzelnen Werts in eine beliebige
Anzahl von Fällen (cases). Zwei mögliche Programmiermuster sind:
switch (Ausdruck) { switch (Ausdruck) {
case Wert1: Anweisungen1; case Wert1:
break; case Wert2:
case Wert2: Anweisungen2; case Wert3:
break; Anweisungen1;
weitere Fälle hier ausgelassen break;
default: Anweisungen3; case Wert4:
break; case Wert5;
} Anweisungen2;
break;
weitere Fälle hier ausgelassen
default:
Anweisungen3;
break;
}

630
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
D.2 Auswahlanweisungen

Hinweise:
 Eine switch-Anweisung kann beliebig viele case-Klauseln enthalten.
 Die break-Anweisung nach einer case-Klausel sorgt dafür, dass die Ausführung
nicht in die folgende case-Klausel „durchläuft“. Die zweite Form oben macht
Gebrauch von diesem „Durchlaufen“. In diesem Fall führen die ersten drei Werte
zur Ausführung von Anweisungen1, während Wert4 und Wert5 zur Ausfüh-
rung von Anweisungen2 führen.
 Die default-Klausel, die „alle anderen Fälle“ abdeckt, ist optional. Wenn keine
default-Klausel angegeben ist, wird möglicherweise kein Fall ausgeführt.
 Die break-Anweisung nach der default-Klausel (oder nach der letzten case-Klau-
sel, wenn keine default-Klausel angegeben ist) ist nicht notwendig, wird jedoch
als guter Stil angesehen.
 Der Ausdruck, zu dem ausgewertet wird, sowie die case-Klauseln können vom
Typ String sein.
 Der Ausdruck und die case-Klauseln können ein Aufzählungstyp sein. Der Typ-
name wird bei den Klauseln weggelassen.
Beispiele:
switch(tag) {
case 1: tagString = "Montag";
break;
case 2: tagString = "Dienstag";
break;
case 3: tagString = "Mittwoch";
break;
case 4: tagString = "Donnerstag";
break;
case 5: tagString = "Freitag";
break;
case 6: tagString = "Samstag";
break;
case 7: tagString = "Sonntag";
break;
default: tagString = "ungültiger Tag";
break;
}
switch([Link]) {
case 1 "Mon";
case 2 "Die";
case 3 "Mit";
case 4 "Don";
case 5 "Fre";
zurArbeitGehen();
break;
case 6 "Sam";
case 7 "Son";
imBettBleiben();
break;
}

631
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang D Kontrollstrukturen in Java

D.3 Schleifen
Java bietet drei Schleifen: while, do-while und for. Die for-Schleife gibt es in zwei
Formen. Die while- und die do-while-Scheife eignen sich besonders gut für unbe-
grenzte Wiederholungen. Die for-each-Schleife wird bei einer bestimmten Anzahl
von Iterationen über eine Sammlung eingesetzt und die for-Schleife liegt zwi-
schen den beiden. Außer bei der for-each-Schleife werden die Wiederholungen in
allen Schleifen durch einen booleschen Ausdruck gesteuert.

D.3.1 while
Die while-Schleife führt einen Anweisungsblock wiederholt aus, solange die Aus-
wertung der Schleifenbedingung true liefert. Der Ausdruck für die Schleifenbedin-
gung wird vor der Ausführung des Schleifenrumpfes ausgewertet, sodass der Schlei-
fenrumpf eventuell gar nicht ausgeführt wird. Dies ist eine wichtige Eigenschaft der
while-Schleife.
while (Ausdruck) {
Anweisungen
}
Beispiele:
[Link]("Geben Sie einen Dateinamen ein: ");
eingabe = liesEingabe();
while(eingabe == null) {
[Link]("Bitte noch einmal versuchen: ");
eingabe = liesEingabe();
}

int index = 0;
boolean gefunden = false;
while(!gefunden && index < [Link]()) {
if([Link](index).equals(eintrag)) {
gefunden = true;
}
else {
index++;
}
}

D.3.2 do-while
Die do-while-Schleife führt einen Anweisungsblock wiederholt aus, solange die
Auswertung der Schleifenbedingung true liefert. Der Ausdruck für die Schleifen-
bedingung wird nach der Ausführung des Schleifenrumpfes ausgewertet, sodass
der Schleifenrumpf mindestens einmal ausgeführt wird. Dies ist ein wichtiger
Unterschied zur while-Schleife.

632
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
D.3 Schleifen

do {
Anweisungen
} while (Ausdruck)
Beispiel:
do {
[Link]("Geben Sie einen Dateinamen ein: ");
eingabe = liesEingabe();
} while (eingabe == null);
Beispiel:
do {
[Link]("Geben Sie einen Dateinamen ein: ");
eingabe = liesEingabe();
} while (eingabe == null);

D.3.3 for
Die for-Schleife hat zwei Formen. Die erste ist auch bekannt als for-each-Schleife
und wird ausschließlich verwendet, um über die Elemente einer Sammlung zu
iterieren. Der Schleifenvariablen wird in jedem Schleifendurchlauf der Wert eines
Elements der Sammlung zugewiesen.
for (Variablendeklaration : Sammlung) {
Anweisungen
}
Beispiel:
for(String notiz : liste) {
[Link](notiz);
}
Für die Elemente der Sammlung steht kein Indexwert zur Verfügung. Die for-each-
Schleife kann nicht verwendet werden, wenn die Sammlung während des Schlei-
fendurchlaufs verändert werden soll.
Die zweite Form der for-Schleife wird ausgeführt, solange die Auswertung der
Schleifenbedingung true liefert. Bevor die Schleife ausgeführt wird, wird eine Anwei-
sung zur Initialisierung genau einmal ausgeführt. Die Schleifenbedingung wird vor
Ausführung des Schleifenrumpfes ausgewertet (sodass die Schleife eventuell gar
nicht durchlaufen wird). Eine Inkrement-Anweisung wird nach jeder Ausführung
des Schleifenrumpfes ausgeführt.
for (Initialisierung; Schleifenbedingung; Inkrement) {
Anweisungen
}
Beispiel:
for(int i = 0; i < [Link](); i++) {
[Link]([Link](i));
}

633
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang D Kontrollstrukturen in Java

Die for-Schleife (in beiden Formen) wird üblicherweise immer dann eingesetzt,
wenn der Schleifenrumpf für eine bestimmte Anzahl von Wiederholungen aus-
geführt werden soll – beispielsweise einmal für jedes Element in einer Sammlung –
obwohl eine for-Schleife eigentlich näher an einer while-Schleife ist als an einer
for-each-Schleife

D.4 Exceptions
Das Werfen und Fangen von Exceptions stellt eine weitere Möglichkeit dar, den
Kontrollfluss zu ändern. Exceptions dienen aber vor allem dem Antizipieren und
der Behandlung von Ausnahmesituationen – und nicht der normalen Kontroll-
flusssteuerung.
try {
Anweisungen
}
catch (Exception-Typ Name) {
Anweisungen
}
finally {
Anweisungen
}
Beispiel:
try {
FileWriter writer = new FileWriter("[Link]");
[Link](text);
[Link]();
}
catch(IOException e) {
[Link]("Schreiben der Textdatei schlug fehl.");
[Link]("Die Exception lautet: " + e);
}
Eine Exception-Anweisung kann eine beliebige Anzahl von catch-Klauseln haben.
Sie werden in ihrer Reihenfolge im Quelltext ausgewertet und es wird nur die
erste passende Klausel ausgeführt. (Eine Klausel passt, wenn der dynamische Typ
des Exception-Objekts, das geworfen wurde, zuweisungskompatibel zum dekla-
rierten Exception-Typ in der catch-Klausel ist.) Die finally-Klausel ist optional.
Java 7 führte zwei wichtige Ergänzungen zu der try-Anweisung ein: mehrere
Exceptions mit einem catch fangen und try-Anweisung mit Ressourcen oder auto-
matisches Ressourcen-Management (ARM).
Mehrere Exceptions können in derselben catch-Klausel behandelt werden, indem
sie in einer Liste von Exception-Typen aufgeführt werden, die durch das |-Symbol
getrennt sind.

634
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
D.5 Zusicherungen

Beispiel:
try {
...
[Link]();
...
}
catch(EOFException | FileNotFoundException e) {
...
}
Automatisches Ressourcen-Management – auch als „try-Anweisung mit Ressour-
cen“ bezeichnet – trägt dem Umstand Rechnung, dass try-Anweisungen oft ver-
wendet werden, um Anweisungen zu schützen, die auf Ressourcen angewiesen
sind, die geschlossen werden sollten, wenn ihre Benutzung – erfolgreich oder nicht –
beendet ist. Der Kopf der try-Anweisung wird erweitert um das Öffnen der Res-
source (oft eine Datei) und diese Ressource wird am Ende der try-Anweisung auto-
matisch geschlossen.
Beispiel:
try (FileWriter writer = new FileWriter(dateiname)) {
...
hier wird der writer verwendet
...
}
catch(IOException e) {
...
}

D.5 Zusicherungen
Zusicherungen dienen hauptsächlich zur Überprüfung des Quelltextes während
der Entwicklungsphase. Für Zusicherungen gibt es zwei Formen der assert-Anwei-
sung:
assert boolescher-Ausdruck ;
assert boolescher-Ausdruck : Ausdruck ;
Beispiele:
assert gibDetails(schluessel) != null;
assert erwartet == aktuell :
"Der aktuelle Wert: " + aktuell +
" passt nicht zum erwarteten Wert: " + erwartet;
Wenn der boolesche Ausdruck einer Zusicherung false liefert, wird ein Assertion-
Error geworfen.

Über einen Compiler-Schalter können Sie dafür sorgen, dass im ausgelieferten über-
setzten Programm keine assert-Anweisungen vorhanden sind, ohne dass Sie diese
Anweisungen aus dem Quelltext entfernen müssen.

635
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

E Java ohne BlueJ

Dieses ganze Buch hindurch haben wir BlueJ benutzt, um unsere Java-Anwen-
dungen zu entwickeln und auszuführen. Dafür gibt es einen guten Grund: BlueJ
bietet einige Werkzeuge, die typische Aufgaben bei der Entwicklung erleichtern.
Insbesondere ermöglicht BlueJ die direkte Ausführung einzelner Methoden von
Klassen und Objekten – das ist nützlich, um schnell ein neues Stück Quelltext zu
testen.
Wir unterscheiden bei der Darstellung der Arbeit ohne BlueJ zwei Kategorien: das
Ausführen einer Anwendung ohne BlueJ und das Entwickeln ohne BlueJ.

E.1 Java ohne BlueJ ausführen


Anwendungen, die an Benutzer ausgeliefert werden, werden ohne BlueJ gestar-
tet. Sie haben üblicherweise genau einen Einstiegspunkt, an dem die Ausführung
beginnt, wenn ein Benutzer die Anwendung startet.
Das konkrete Vorgehen hängt vom jeweiligen Betriebssystem ab. Üblicherweise wird
mit einem Doppelklick auf ein Icon gestartet oder indem der Name der Anwendung
in eine Kommandozeile eingegeben wird. Das Betriebssystem muss dann wissen,
welche Methode welcher Klasse aufgerufen werden soll, um die gesamte Anwen-
dung zu starten.
In Java ist dieses Problem über eine Konvention gelöst: Wenn ein Java-Programm
gestartet wird, wird der Name der Klasse als Parameter an das Startkommando über-
geben, während der Name der Methode festgelegt ist: main. Dieser Name wurde
willkürlich von den Java-Entwicklern gewählt, steht jedoch fest, d.h., die Methode
muss main heißen. (Der Name dieser Methode geht zurück auf die Startmethode der
Programmiersprache C, von der Java ein Großteil der Syntax geerbt hat.)
Betrachten Sie beispielsweise das folgende Kommando, das in eine Kommando-
zeile, etwa in einem DOS-Fenster unter Windows oder in einer Unix-Shell, einge-
geben wird:
java Spiel
Das Kommando java startet die virtuelle Maschine von Java. Sie ist Teil des Java
Development Kit (JDK), das auf Ihrem System installiert sein muss. Spiel ist der Name
der Klasse, die wir starten wollen.

637
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang E Java ohne BlueJ

Das Java-System sucht dann in der Klasse Spiel nach einer Methode mit der fol-
genden Signatur:
public static void main(String[] args)
Die Methode muss öffentlich sein, damit sie von außen aufgerufen werden kann.
Sie muss statisch sein, weil zu Anfang noch kein aufrufbares Objekt existiert. Zu
Anfang existieren nur Klassen, also können wir nur statische Methoden aufrufen.
Diese statische Methode erzeugt dann üblicherweise das erste Objekt. Der
Ergebnistyp ist void, da die Methode kein Ergebnis zurückliefert.
Der Parameter ist ein Zeichenketten-Array. Dies ermöglicht dem Benutzer, zusätz-
liche Parameter zu übergeben. In unserem obigen Beispielaufruf ist der Wert des
Parameters args ein Array mit der Länge null sein. In der Kommandozeile können
aber auch Parameter angegeben werden, etwa folgendermaßen:
java Spiel 2 Fred
Jedes Wort in der Kommandozeile nach dem Klassennamen wird als eine sepa-
rate Zeichenkette aufgefasst und der Methode main als ein String-Element im
String-Array übergeben. In diesem Fall würde das Array args zwei Elemente ent-
halten, die beiden Strings "2" und "Fred". Kommandozeilenparameter werden
mit Java nur sehr selten verwendet.
Der Rumpf der Methode main kann theoretisch beliebige Anweisungen enthal-
ten. Es ist jedoch guter Stil, wenn diese Methode möglichst klein gehalten wird.
Insbesondere sollte hier keine Anwendungslogik implementiert sein.
Typischerweise sollte die Methode main genau das Gleiche tun, das Sie auch zum
interaktiven Starten in BlueJ tun würden. Wenn Sie beispielsweise ein Objekt der
Klasse Spiel erzeugen würden, um daran die Methode starten aufzurufen, dann
sollten Sie folgende main-Methode in Ihrer Klasse Spiel einfügen:
public static void main(String[] args)
{
Spiel spiel = new Spiel();
[Link]();
}
Auf diese Weise ahmt die Methode main ein interaktives Starten nach.
Jedes Java-Projekt liegt üblicherweise in einem eigenen Verzeichnis. Alle Klassen
des Projekts sind in diesem Verzeichnis abgelegt. Wenn Sie das Kommando zum
Starten von Java aufrufen und Ihre Anwendung ausführen wollen, dann sollten
Sie sicherstellen, dass das Projektverzeichnis das aktuelle Verzeichnis in Ihrer
Kommandoumgebung ist. Die Klassen können dann gefunden werden.
Wenn die angegebene Klasse nicht gefunden werden kann, gibt die virtuelle
Maschine von Java eine Fehlermeldung der folgenden Art aus:
Could not find or load main class Spiel
Wenn Sie eine Meldung wie diese bekommen, dann stellen Sie sicher, dass Sie
den Klassennamen korrekt eingegeben haben und das aktuelle Verzeichnis diese
Klasse auch wirklich enthält. Die Klasse wird in einer Datei mit der Endung .class

638
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
E.2 Konsolenanwendungen und die Problematik der Umlaute

gespeichert. Der Code für die Klasse Spiel beispielsweise ist in einer Datei mit
dem Namen [Link] gespeichert.
Wenn diese Klasse gefunden wird, aber keine main-Methode enthält (oder eine
main-Methode mit der falschen Signatur), werden Sie eine Meldung erhalten, die
ungefähr so aussieht:
Main method not found in class Spiel, please define the main method as:
public static void main(String[] args)
In diesem Fall sollten Sie sicherstellen, dass Ihre Klasse eine Methode main mit der
richtigen Signatur definiert.

E.2 Konsolenanwendungen und die


Problematik der Umlaute
Von den GUI-Anwendungen aus Kapitel 13 einmal abgesehen, haben wir uns in
den Beispielprogrammen dieses Buches zumeist der Methode [Link]()
bedient, um Ergebnisse und Meldungen auf die Konsole auszugeben. Solange Sie
in BlueJ arbeiten und die Programme oder Methoden von BlueJ aus starten, schickt
[Link]() sämtliche Ausgaben an die BlueJ-Konsole. Sobald Sie Ihre
Konsolenanwendungen aber ohne BlueJ starten, gehen alle Ihre [Link]-Aus-
gaben an die Konsole, von der aus das Programm – wie im vorangehenden
Abschnitt beschrieben – gestartet wurde. Für deutschsprachige Konsolenanwen-
dungen unter Windows hat dies in der Regel einen unangenehmen Nebeneffekt:
Die deutschen Umlaute werden nicht mehr korrekt angezeigt.
Wenn Sie Zeichenketten via [Link] auf die Windows-Konsole ausgeben, wer-
den die 16-Bit-Codes der einzelnen Zeichen auf je 8 Bit zurechtgestutzt, ohne
dass dabei allerdings eine korrekte Umcodierung in den 8-Bit-OEM-Zeichensatz
der Konsole stattfinden würde. Das traurige Ergebnis: Die deutschen Umlaute
sowie verschiedene weitere Umlaute und Sonderzeichen, die die Konsole prinzi-
piell anzeigen könnte, gehen verloren.
Um die Umlaute dennoch korrekt auszugeben, müssen Sie
 entweder die Zeichenkodierung des [Link]-Objekts anpassen
 oder auf das in Java 6 neu eingeführte Console-Objekt zurückgreifen.

E.2.1 Zeichenkodierung für [Link] anpassen


Wie Sie aus Kapitel 2 bereits wissen, verbirgt sich hinter [Link] ein PrintStream-
Objekt. Dieses Objekt ist mit der Konsole als Ausgabegerät verbunden und verwen-
det die Standard-Zeichenkodierung, welche von der Konfiguration des zugrunde
liegenden Betriebssystems abhängt. Wenn Sie eigene PrintStream-Objekte erzeu-
gen, können Sie die Zeichenkodierung frei wählen (soweit verfügbar).

639
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang E Java ohne BlueJ

public static void main(String[] args)


{
[Link] out;

try {
[Link](new [Link]([Link], true, "Cp850"));
}
catch ([Link] e) {
}

Spiel spiel new Spiel();


[Link]();
}
Dem PrintStream-Konstruktor werden hier drei Argumente übergeben:
 ein OutputStream-Objekt, das das Ziel der Ausgabe vorgibt (hier die Konsole)
 true, damit die Ausgaben sofort ausgeführt werden (andernfalls werden die
Ausgaben gepuffert, und Sie müssen die Methode flush() aufrufen)
 den Namen der gewünschten Zeichenkodierung (hier "Cp850" für die DOS-
Codepage 850)
Danach wird das neu erzeugte PrintStream-Objekt mittels der setOut()-Methode
als neues Standardausgabegerät registriert.

E.2.2 Das Console-Objekt


Die Klasse Console instanziieren Sie nicht selbst. Wenn Ihr Java-Code im Kontext
einer Java-Virtual-Machine-Instanz ausgeführt wird, die mit einem Konsolenfenster
verbunden ist, erzeugt die JVM automatisch intern ein Console-Objekt, welches das
Konsolenfenster repräsentiert. Über die statische Console-Methode console() kön-
nen Sie sich eine Referenz auf dieses Objekt zurückliefern lassen.
[Link] cons = [Link]();
Wenn die Java Virtual Machine kein beigeordnetes Konsolenfenster ermitteln kann,
liefert [Link]() eine null-Referenz zurück. Dies ist in der Regel dann der
Fall, wenn die Anwendung nicht aus einem Konsolenfenster heraus gestartet wurde
(also beispielsweise auch bei der Ausführung innerhalb von BlueJ). Zur Sicherheit
sollten Sie den Rückgabewert von [Link]() daher immer überprüfen.

Text auf die Konsole ausgeben


Alle Ausgaben, die Sie über das von [Link]() zurückgelieferte Console-
Objekt vornehmen, werden automatisch an die Zeichenkodierung der Konsole
angepasst.
[Link] cons = [Link]();

if (cons != null) {
[Link]("\n");
[Link](" Ausgabe der Umlaute mit Console \n");
[Link](" ä, ö, ü, ß \n");
}

640
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
E.3 Ausführbare jar -Dateien erzeugen

Beachten Sie, dass die Ausgabemethode der Klasse Console nicht println(), sondern
printf() heißt. Die Methode kann analog zur println()-Methode von [Link]
verwendet werden, gestattet aber auch die Ausgabe mit Formatierungsstrings und
Platzhaltern, die Umsteigern von C oder C++ vermutlich bekannt sein wird. So
erzeugt der Code
[Link] cons = [Link]();
String dvd = "Solaris";
double preis = 12,95;

if (cons != null) {
[Link]("Die %s-DVD kostet %4.2f", dvd, preis);
}
die Ausgabe
Die Solaris-DVD kostet 12,95

Von der Tastatur lesen


Über das vordefinierte Console-Objekt können Sie auch Tastatureingaben einle-
sen. Am einfachsten geht dies mit der Methode readLine(), die eine einzelne Ein-
gabezeile einliest und als String-Objekt zurückliefert. Numerische Eingaben kön-
nen Sie zeilenweise einlesen und mit den Parse-Methoden der verschiedenen
Wrapper-Klassen in die entsprechenden Werte umwandeln.
[Link] cons = [Link]();
String name = "";
int alter = 0;

if (cons != null) {
[Link](" Geben Sie Ihren Namen ein: ");
name = [Link]();
[Link](" Geben Sie Ihr Alter ein: ");
alter = [Link]( [Link]() );
[Link] (" %s (Alter: %d) \n", name, alter);
}
Wenn Sie Passwörter oder andere sensible Daten einlesen möchten, die für Umste-
hende nicht lesbar sein sollen, rufen Sie statt readLine() die Methode readPass-
word() auf.

E.3 Ausführbare jar-Dateien erzeugen


Java-Projekte werden üblicherweise als eine Sammlung von Dateien in einem Ver-
zeichnis abgelegt. Wir werden unten kurz die beteiligten Dateien beschreiben.
Um eine Anwendung an andere weitergeben zu können, ist es oft einfacher,
wenn alle Teile in einer einzelnen Datei zusammengefasst sind. Der Mechanismus
von Java für diesen Zweck ist das Java Archive Format (jar). Alle Dateien einer
Anwendung können dabei in einer einzelnen Datei zusammengefasst und den-
noch ausführbar sein. (Wenn Sie bereits das Komprimierungsformat zip kennen:
Es ist das gleiche Format. jar-Dateien können mit Zip-Programmen geöffnet wer-
den und umgekehrt.)

641
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang E Java ohne BlueJ

Um eine jar-Datei ausführbar zu machen, muss festgelegt werden, welche die


Startklasse ist. (Sie erinnern sich: Die Methode zum Starten heißt immer main,
aber wir müssen angeben, in welcher Startklasse diese Methode liegt.) Dazu wird
in einer jar-Datei eine Textdatei abgelegt (die so genannte Manifest-Datei), die
diese Information enthält. BlueJ bietet hierbei Unterstützung an.
Um eine ausführbare jar-Datei in BlueJ zu erstellen, können Sie im Menü PROJEKT
den Eintrag ALS JAR-ARCHIV SPEICHERN aufrufen und im folgenden Dialog die Klasse
auswählen, die die main-Methode enthält. (Sie müssen die main-Methode wie
oben beschrieben implementieren.)
Für weitere Fragen zu dieser Operation können Sie das BlueJ-Tutorial lesen, das
Sie im Menü HILFE über den Eintrag BLUEJ-TUTORIAL oder auf den BlueJ-Webseiten
finden. Sobald die ausführbare jar-Datei erzeugt wurde, kann sie mit einem Dop-
pelklick gestartet werden. Dazu muss auf dem Rechner, der die jar-Datei ausfüh-
ren soll, ein JDK (ein Java Development Kit) oder ein JRE (Java Runtime Environ-
ment) installiert und mit jar-Dateien assoziiert sein.

E.4 Entwickeln ohne BlueJ


Wenn Sie Ihre Programme nicht nur ohne BlueJ ausführen, sondern auch ohne
BlueJ entwickeln wollen, dann müssen Sie Ihre Klassen editieren und übersetzen.
Der Quelltext einer Klasse wird in einer Datei mit der Endung .java gespeichert.
Beispielsweise ist die Klasse Spiel in einer Datei mit dem Namen [Link]
gespeichert. Quelltextdateien können mit jedem Texteditor bearbeitet werden. Es
gibt sehr viele frei verfügbare oder kostengünstige Texteditoren. Einige, wie
Notepad oder Wordpad, werden mit Windows ausgeliefert; aber wenn Sie den
Editor für mehr als nur für einen schnellen Test benutzen wollen, dann werden
Sie sich schnell etwas Besseres besorgen wollen. Seien Sie jedoch vorsichtig mit
Textverarbeitungsprogrammen; diese speichern üblicherweise nicht im einfachen
Textformat, und das Java-System kann die Dateien dann nicht lesen.
Die Quelltextdateien können dann in der Kommandozeile übersetzt werden, indem
Sie den Compiler aufrufen, der mit dem JDK ausgeliefert wird. Er heißt javac. Um
eine Quelltextdatei [Link] zu übersetzen, benutzen Sie das Kommando
javac [Link]
Dieses Kommando wird die Klasse Spiel sowie alle Klassen, die von dieser Klasse
abhängen, übersetzen und dann eine Datei [Link] anlegen. Diese Datei ent-
hält den Code, der von der virtuellen Maschine für Java ausgeführt werden kann.
Um sie auszuführen, benutzen Sie das Kommando
java Spiel
Beachten Sie, dass bei diesem Aufruf die Endung .class nicht mit angegeben ist.

642
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

F Benutzung des Debuggers

Der Debugger in BlueJ bietet eine Reihe von absichtlich vereinfachten, aber sehr
nützlichen Funktionen zum Debuggen von Programmen und zum leichteren Ver-
stehen des Laufzeitverhaltens von Programmen.
Das Debugger-Fenster wird hervorgeholt, wenn der Eintrag DEBUGGER ANZEIGEN
aus dem Menü ANSICHT ausgewählt wird oder wenn über dem Aktivitätsbalken
mit der rechten Maus geklickt wird und der Eintrag ZEIGE DEBUGGER aus dem Kon-
textmenü ausgewählt wird. Abbildung F.1 zeigt das Debugger-Fenster.

Abbildung F.1
Das Debugger-Fenster
von BlueJ.

Das Debugger-Fenster enthält fünf Anzeigebereiche und fünf Kontrollknöpfe. Die


Bereiche und die Knöpfe werden nur aktiviert, wenn ein Programm einen Halte-
punkt erreicht oder aus einem anderen Grund anhält. Die folgenden Abschnitte
beschreiben, wie Haltepunkte gesetzt werden, wie die Programmausführung kon-
trolliert wird und welchen Zweck die verschiedenen Anzeigebereiche haben.

643
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang F Benutzung des Debuggers

F.1 Haltepunkte
Ein Haltepunkt ist ein Merker, der einer Quelltextzeile angeheftet wird (Abbildung
F.2). Wenn ein Haltepunkt während der Programmausführung erreicht wird, wer-
den die Debugger-Anzeigen und -Knöpfe aktiv, sodass Sie den Zustand des Pro-
gramms untersuchen und die weitere Ausführung kontrollieren können.

Abbildung F.2
Ein Haltepunkt, der
einer Quelltextzeile
angeheftet ist.

Haltepunkte werden im Editorfenster gesetzt. Entweder klicken Sie mit der linken
Maustaste in den Haltepunktbereich links vom Quelltext oder Sie platzieren den
Cursor in der Zeile, in der der Haltepunkt gesetzt werden soll, und selektieren
dann im Menü WERKZEUGE den Eintrag HALTEPUNKT SETZEN/ENTFERNEN. Auf die glei-
che Weise können Haltepunkte auch wieder entfernt werden. Haltepunkte kön-
nen nur in übersetzten Klassen gesetzt werden.

F.2 Die Kontrollknöpfe


Abbildung F.3 zeigt die Kontrollknöpfe, die an einem Haltepunkt aktiv sind.

Abbildung F.3
Aktive Kontrollknöpfe
an einem Haltepunkt.

F.2.1 Anhalten
Der ANHALTEN-Knopf ist nur während einer Programmausführung aktiv und erlaubt
es, bei Bedarf die Ausführung zu unterbrechen. Wenn die Ausführung hält, zeigt
der Debugger den Zustand des Programms so an, also wäre dieses an einem Halte-
punkt gestoppt worden.

F.2.2 Schritt über


Der SCHRITT ÜBER-Knopf setzt die Ausführung bei der aktuellen Anweisung fort. Nach
Ausführung der Anweisung wird erneut gehalten. Wenn die Anweisung einen
Methodenaufruf einschließt, dann wird dieser Aufruf komplett abgeschlossen, bevor
die Ausführung angehalten wird (es sei denn, der Aufruf führt zu einem weiteren
expliziten Haltepunkt).

644
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
F.3 Anzeige der Variablen

F.2.3 Schritt hinein


Der SCHRITT HINEIN-Knopf setzt die Ausführung bei der aktuellen Anweisung fort.
Wenn die Anweisung ein Methodenaufruf ist, dann führt die Ausführung in die
Methode hinein und hält bei der ersten Anweisung innerhalb dieser Methode.

F.2.4 Fortsetzen
Der FORTSETZEN-Knopf setzt die Ausführung fort, bis entweder der nächste Halte-
punkt erreicht wird oder die Ausführung mit dem ANHALTEN-Knopf gestoppt wird
oder die Ausführung regulär endet.

F.2.5 Beenden
Der BEENDEN-Knopf bricht das aktuelle Programm sofort ab, sodass mit der Aus-
führung nicht mehr fortgefahren werden kann. Wenn Sie die Ausführung nur
unterbrechen wollen, um den aktuellen Programmstatus zu untersuchen, sollten
Sie den ANHALTEN-Knopf betätigen.

F.3 Anzeige der Variablen


Abbildung F.4 zeigt alle drei an einem Haltepunkt aktiven Anzeigebereiche für Vari-
ablen in einem Beispiel aus der Jäger-Beute-Simulation, die in Kapitel 12 diskutiert
wird. Statische Variablen werden im oberen Bereich angezeigt, Instanzvariablen im
mittleren und lokale Variablen im unteren Bereich.

Abbildung F.4
Aktive Anzeigeberei-
che für Variablen.

Wenn ein Haltepunkt erreicht wird, wird die Ausführung an einer Anweisung eines
beliebigen Objektes innerhalb des Programms angehalten. Der Bereich Statische
Variablen zeigt die Werte der statischen Variablen, die in der Klasse dieses Objektes
definiert sind. Der Bereich Instanzvariablen zeigt die Werte der Instanzvariablen
des betreffenden Objektes. Beide Bereiche zeigen auch alle Variablen an, die von
Superklassen geerbt wurden.

645
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang F Benutzung des Debuggers

Der Bereich Lokale Variablen zeigt die Werte der lokalen Variablen und Parameter
der gerade ausgeführten Methode bzw. des gerade ausgeführten Konstruktors.
Lokale Variablen erscheinen in diesem Bereich erst dann, wenn sie initialisiert
wurden, da sie erst zu diesem Zeitpunkt innerhalb der virtuellen Maschine sicht-
bar werden.
Jede Variable in diesen Bereichen, die auf ein Objekt verweist, kann mit einem Dop-
pelklick weiter untersucht werden.

F.4 Die Anzeige der Aufruffolge


Abbildung F.5 zeigt den Fensterausschnitt für die Anzeige der Aufruffolge, hier
mit einer Sequenztiefe von vier Methoden. Die Methoden erscheinen im Format
[Link] in der Sequenz, unabhängig davon, ob es sich um eine Klassen-
oder Instanzmethode handelt. Konstruktoren erscheinen als Klasse.<init> in der
Sequenz.

Abbildung F.5
Eine Aufruffolge.

Die Aufruffolge arbeitet nach dem Stapelprinzip, bei dem die Methode ganz oben
diejenige bezeichnet, in der gerade der Kontrollfluss hält. Die Variablen-Anzeigen
zeigen jeweils Details zu der Methode oder dem Konstruktor, der aktuell in der
Aufruffolge hervorgehoben ist. Das Selektieren einer anderen Zeile aus der Folge
bewirkt, dass die Inhalte der anderen Anzeige-Bereiche aktualisiert werden.

F.5 Die Thread-Anzeige


Die Thread-Anzeige geht über den Rahmen dieses Buches hinaus und wird hier
nicht weiter behandelt.

646
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

G Testwerkzeuge für
Modultests mit JUnit

In diesem Anhang geben wir einen kurzen Überblick über die wichtigsten Eigen-
schaften der BlueJ-Unterstützung für Modultests im JUnit-Stil. Weitere Einzelhei-
ten können im Test-Tutorial nachgelesen werden, das auf der BlueJ-Website ver-
fügbar ist.

G.1 Aktivieren der Test-Funktionalität


Um die Test-Funktionalität von BlueJ zu aktivieren, müssen Sie sicherstellen, dass
unter WERKZEUGE/EINSTELLUNGEN/DIVERSES die Box Testwerkzeuge anzeigen selek-
tiert ist. Das Hauptfenster von BlueJ enthält dann einige weitere Knöpfe, die bei
einem geöffneten Projekt aktiviert sind.

G.2 Eine Testklasse erzeugen


Sie erzeugen eine Testklasse, indem Sie einen Rechtsklick auf einer Klasse im Klas-
sendiagramm ausführen und im Kontextmenü TESTKLASSE ERZEUGEN auswählen. Der
Name der Testklasse wird automatisch festgelegt, indem Test als Suffix für den
Namen der zugeordneten Klasse verwendet wird. Alternativ können Sie eine
Testklasse auch über den Knopf neue Klasse… erzeugen, indem Sie als Klassen-
typ Unit-Test angeben. Dann können Sie den Namen der Klasse frei festlegen.
Testklassen werden mit <<unit test>> im Klassendiagramm annotiert und haben
eine andere Farbe als gewöhnliche Klassen.

G.3 Eine Testmethode erzeugen


Testmethoden können interaktiv erzeugt werden. Eine Sequenz von Benutzerinterak-
tionen mit dem Klassendiagramm und der Objektleiste werden aufgezeichnet und
als eine Folge von Java-Anweisungen und Deklarationen in einer Methode der Test-
klasse festgehalten. Sie starten eine Aufzeichnung, indem Sie ERZEUGE TESTMETHODE
aus dem Kontextmenü einer Testklasse selektieren. Sie werden dann nach dem
Namen der neuen Testmethode gefragt. Frühere JUnit-Versionen (bis Version 3) setz-
ten voraus, dass dieser Name mit test begann. Dies ist heute nicht mehr der Fall. Das
Aufzeichnung-Symbol links vom Klassendiagramm erscheint nun rot und die Knöpfe
Beenden und Abbrechen werden verfügbar.

647
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang G Testwerkzeuge für Modultests mit JUnit

Sobald eine Aufzeichnung gestartet wurde, werden alle Objekterzeugungen oder


Methodenaufrufe Teil des Codes der neu erzeugten Methode. Selektieren Sie Been-
den, um die Aufzeichnung zu speichern, oder Abbrechen, um die Aufzeichnung
abzubrechen und die Testklasse unverändert zu lassen.
Testmethoden werden im Quelltext der Testklassen mit der Annotation @Test ver-
sehen.

G.4 Zusicherungen bei Tests


Während der Aufzeichnung einer Testmethode öffnet jeder Methodenaufruf mit
einem Ergebnis das Fenster Methodenergebnis. In diesem wird die Möglichkeit
gegeben, eine Zusicherung zu dem Ergebniswert zu formulieren, indem die Box
Sichere zu, dass selektiert wird. Das zugeordnete Menü enthält eine Menge von
möglichen Zusicherungen zu dem Testergebnis. Wenn eine Zusicherung gemacht
wurde, wird diese in der Testmethode durch entsprechenden Code umgesetzt,
der zu einem AssertionError führt, falls die Zusicherung nicht zutrifft.

G.5 Tests ausführen


Individuelle Testmethoden können ausgeführt werden, indem sie aus dem Kontext-
menü der zugehörigen Testklasse ausgewählt werden. Ein erfolgreicher Test wird
durch eine Meldung in der Statuszeile des Hauptfensters angezeigt. Ein nicht erfolg-
reicher Test lässt das Fenster Testergebnisse erscheinen. Selektieren von ALLES TESTEN
im Kontextmenü der Testklasse lässt alle Tests dieser Klasse ausführen. Das Fenster
Testergebnisse liefert dann Informationen über den Erfolg oder Misserfolg jeder
einzelnen Methode.

G.6 Testgerüste
Der Inhalt der Objektleiste kann als Testgerüst festgehalten werden, indem
OBJEKTZUSTAND SPEICHERN aus dem Kontextmenü einer Testklasse aufgerufen wird.
Das Erzeugen eines Testgerüstes bewirkt, dass in der Testklasse für jedes Objekt
eine Instanzvariable angelegt wird und Anweisungen in die Methode setUp ein-
gefügt werden, die den exakten Status der Objekte wie auf der Objektleiste her-
stellen. Die Objekte werden dadurch von der Objektleiste entfernt.
Die Methode setUp wird in der Testklasse mit @Before annotiert und automatisch
vor jeder Ausführung einer Testmethode ausgeführt, sodass alle Objekte eines
Testgerüsts für alle Tests zur Verfügung stehen.
Die Objekte eines Testgerüsts können wieder auf der Objektleiste erzeugt werden,
indem TESTGERÜST WIEDERHERSTELLEN aus dem Kontextmenü der Testklasse gewählt
wird.
Die Methode tearDown (mit der Annotation @After) wird nach jeder Testmethode
aufgerufen. Sie kann dazu verwendet werden, um eventuelle Aufräumarbeiten
nach dem Testen – sofern benötigt – durchzuführen.

648
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

H Werkzeuge für die Teamarbeit

In diesem Abschnitt werden wir kurz auf die Werkzeuge eingehen, die Ihnen für
die Teamarbeit zur Verfügung stehen.
Zum Umfang von BlueJ gehören auch verschiedene Werkzeuge für die Arbeit im
Team, die allesamt auf einem Quelltextarchiv-Modell basieren. Bei diesem Modell
wird ein Archiv-Server eingerichtet, der über das Internet für alle Teammitglieder
zugänglich ist.
Der Server sollte von einem Administrator eingerichtet werden. BlueJ unterstützt
sowohl Subversion- als auch CVS-Archive.

H.1 Server-Einrichtung
Die Einrichtung des Archiv-Servers sollte einem erfahrenen Administrator übertra-
gen werden. Ausführliche Anweisungen hierzu finden Sie im Dokument „BlueJ
Teamwork Repository Configuration“, auf das Sie über BLUEJ-TUTORIAL im HILFE-Menü
von BlueJ zugreifen können.

H.2 Teamarbeit-Funktionalität aktivieren


Die Teamarbeitswerkzeuge von BlueJ sind zu Beginn verborgen. Um die Werkzeuge
anzuzeigen, öffnen Sie das Dialogfeld WERKZEUGE/EINSTELLUNGEN und aktivieren Sie
auf der Registerseite DIVERSES die Option Teamarbeitswerkzeuge anzeigen. Damit
blenden Sie drei zusätzliche Schalter (Aktualisieren, Abgeben, Status) ein und legen
im Menü WERKZEUGE ein Untermenü namens TEAMARBEIT an.

H.3 Ein Projekt zur gemeinsamen Nutzung


einrichten
Um ein gemeinsames Projekt einzurichten, erzeugt eines der Teammitglieder das
Projekt als normales BlueJ-Projekt. Mit der Funktion Projekt gemeinsam nutzen im
Menü WERKZEUGE/TEAMARBEIT kann er das Projekt dann für alle zugänglich machen.
Mit dieser Funktion wird eine Kopie des Projekts in das zentrale Archiv abgelegt.
Der Servername und Zugriffsdetails müssen in einem Dialogfeld angegeben wer-

649
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang H Werkzeuge für die Teamarbeit

den; fragen Sie Ihren Administrator (denjenigen, der das zentrale Archiv angelegt
hat), was Sie hier genau eintragen müssen.

H.4 An einem Projekt gemeinsam arbeiten


Nachdem ein Benutzer ein gemeinsames Projekt im Archiv angelegt hat, können
andere Teammitglieder darauf zugreifen. Dazu müssen sie nur im Menü WERKZEUGE/
TEAMARBEIT den Menüpunkt ARBEITSKOPIE ERSTELLEN auswählen, der dafür sorgt, dass
eine Kopie des gemeinsamen Projekts vom zentralen Server auf ihrem Rechner
kopiert wird. Danach können sie auf der lokalen Kopie arbeiten.

H.5 Aktualisieren und Abgeben


Ab und zu müssen die verschiedenen Kopien des Projekts, an denen die Teammit-
glieder auf ihren eigenen Rechnern arbeiten, synchronisiert werden. Dies geschieht
über das zentrale Archiv. Mit der Funktion ÄNDERUNGEN ABGEBEN kopieren Sie ihre
Änderungen in das Archiv und mit der Funktion AKTUALSIERUNG ABHOLEN kopieren
Sie die Änderungen aus dem Archiv (die andere Teammitglieder vorgenommen
haben) in Ihre lokale Kopie. Es empfiehlt sich, das Projekt möglichst oft zu aktuali-
sieren und abzugeben, sodass die Änderungen nicht zu umfangreich werden.

H.6 Weitere Informationen


Weitere Informationen finden Sie in dem Teamarbeit-Tutorial auf das Sie über
BLUEJ-TUTORIAL im HILFE-Menü von BlueJ zugreifen können.

650
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

I Javadoc

Gutes Dokumentieren von Klassen und Interfaces ist eine wichtige Ergänzung zum
Schreiben hochwertiger Quelltexte. Mit der Dokumentation können Sie mensch-
lichen Lesern Ihre Vorstellungen auf einem hohen sprachlichen Niveau vermitteln,
anstatt sie zum Lesen des relativ primitiven Quelltextes zu zwingen. Dokumenta-
tion ist insbesondere für die öffentlichen Anteile von Klassen und Interfaces nütz-
lich, da ein Programmierer diese idealerweise benutzen kann, ohne die Details der
Implementierung kennen zu müssen.
In allen Beispielprojekten dieses Buches haben wir einen speziellen Kommentie-
rungsstil verwendet, der von dem Dokumentationswerkzeug Javadoc erkannt wird.
Javadoc wird zusammen mit dem JDK ausgeliefert. Dieses Werkzeug automatisiert
die konsistente Generierung von Klassendokumentationen in Form von HTML-Sei-
ten. Das API von Java wurde mit demselben Werkzeug generiert und man weiß dies
zu schätzen, wenn man die Bibliotheksklassen benutzt.
In diesem Anhang geben wir einen kurzen Überblick über die wichtigsten Doku-
mentationskommentare, die Sie in die Routine Ihrer eigenen Softwareentwicklung
integrieren sollten.

I.1 Dokumentationskommentare
Die zu dokumentierenden Teile einer Klasse sind die Klassendefinition als Ganzes
sowie ihre Datenfelder, Konstruktoren und Methoden. Für den Benutzer einer Klasse
ist die Dokumentation der Klasse sowie der öffentlichen Konstruktoren und Metho-
den am wichtigsten. Wir neigen dazu, Datenfelder nicht im Javadoc-Stil zu kommen-
tieren, weil wir sie als Implementierungsdetails ansehen, auf die sich ein Benutzer
nicht verlassen sollte.
Dokumentationskommentare werden immer mit der Zeichenfolge /** eingeleitet
und mit */ abgeschlossen. Zwischen diesen Symbolen kann ein Kommentar aus
einer Übersichtsbeschreibung, gefolgt von einem Markierungsabschnitt bestehen,
die beide optional sind.

651
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang I Javadoc

I.1.1 Die Übersichtsbeschreibung


Die Übersichtsbeschreibung einer Klasse sollte eine generelle Beschreibung des Klas-
senzwecks sein. Listing I.1 zeigt den Teil einer typischen Übersichtsbeschreibung aus
der Klasse Spiel des Zuul-Projektes. Beachten Sie, dass die Beschreibung Details dar-
über enthält, wie diese Klasse zum Starten des Spiels zu benutzen ist.

Listing I.1
Die Übersichts-
beschreibung eines
Klassenkommentars.

Die Übersichtsbeschreibung für eine Methode sollte recht allgemein gehalten sein,
ohne auf Details ihrer Implementierung einzugehen. Tatsächlich benötigt die Über-
sichtsbeschreibung einer Methode häufig nur einen einzelnen Satz, wie etwa
/**
* Erzeuge einen neuen Fahrgast mit verschiedener Abhol und
* Zielposition.
*/
Besonders sorgfältig sollte der erste Satz (bis zum ersten Punkt) der Übersichts-
beschreibung einer Klasse, eines Interface oder einer Methode formuliert sein, da
dieser in einem eigenen Übersichtsabschnitt zu Beginn der generierten Dokumenta-
tion eingetragen wird.
Javadoc unterstützt auch die Benutzung von HTML-Markierungen in Kommentaren.

I.1.2 Der Markierungsabschnitt


Im Anschluss an die Übersichtsbeschreibung folgt der Markierungsabschnitt. Java-
doc erkennt rund 20 Markierungen, von denen wir hier nur die wichtigsten
ansprechen wollen (Tabelle I.1). Markierungen können in zwei Formen auftreten:
als Blockmarkierung und als Zeilenmarkierung. Wir werden lediglich Blockmarkie-
rungen betrachten, weil diese am häufigsten benutzt werden. Weitere Details über
Zeilenmarkierungen und zusätzliche Blockmarkierungen können dem Abschnitt
über Javadoc der Dokumentation Tools and Utilities entnommen werden, die Teil
des JDK ist.

652
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
I.1 Dokumentationskommentare

Tabelle I.1
Markierung Verknüpfter Text Häufig genutzte Java-
@author Autorenname(n) doc-Markierungen.

@param Parametername und -beschreibung


@return Beschreibung des Ergebnisses
@see Querverweise
@throws Geworfener Exception-Typ mit Kontext
@version Versionsbeschreibung

Die Markierungen @author und @version sind üblicherweise in den Kommentaren


von Klassen und Interfaces zu finden, für Konstruktoren, Methoden und Daten-
felder sind sie nicht vorgesehen. Beide werden gefolgt von Freitext, an den keine
speziellen Anforderungen gestellt werden. Beispiele sind
@author Hacker T. Largebrain
@version 2004.12.31
Die Markierungen @param und @throws werden in Methoden und Konstruktoren
verwendet, während @return ausschließlich in Methoden zulässig ist. Beispiele sind
@param limit der maximal zulässige Wert.
@return eine Zufallszahl aus dem Bereich 1 bis limit (inklusiv).
@throws IllegalLimitException wenn limit kleiner als 1 ist.
Die Markierung @see kennt verschiedene Formate und kann in jedem Dokumenta-
tionskommentar verwendet werden. Es bietet die Möglichkeit, Querverweise von
einem Kommentar zu einer anderen Klasse oder Methode oder zu einer anderen
Form der Dokumentation zu formulieren. Ein solcher Querverweis führt dazu, dass
in der Dokumentation das dokumentierte Element um einen Abschnitt See Also
ergänzt wird. Hier sind einige typische Beispiele:
@see "Die Java Sprachspezifikation, von Joy et al."
@see <a href=[Link] BlueJ web site</a>
@see #istLebendig
@see [Link]#add
Das erste Beispiel bettet einfach nur einen Text ohne Hyperlink ein; das zweite bet-
tet einen Hyperlink zum genannten Dokument ein; das dritte verbindet mit der
Dokumentation der Methode istLebendig innerhalb derselben Klasse; das vierte
setzt einen Querverweis auf die Methode add in der Klasse [Link].

653
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang I Javadoc

I.2 Unterstützung für Javadoc in BlueJ


Wenn ein Projekt im Javadoc-Stil kommentiert wurde, dann bietet BlueJ Unterstüt-
zung bei der Generierung der kompletten HTML-Dokumentation. Selektieren Sie
im Hauptfenster den Menüeintrag WERKZEUGE/DOKUMENTATION ERZEUGEN und die
Dokumentation wird generiert und in einem Browserfenster angezeigt.
Im BlueJ-Editor kann von der Quelltextsicht auf die Dokumentationssicht umgeschal-
tet werden, indem die Option Implementierung im Fenster oben rechts auf Schnitt-
stelle umgestellt wird (Abbildung I.1). Dies ermöglicht eine schnelle Voransicht der
Dokumentation, die jedoch keine Referenzen auf Superklassen oder benutzte Klas-
sen enthält.

Abbildung I.1
Die Implementierung/
Schnittstelle-Option im
BlueJ-Editor.

Weitergehende Informationen sind verfügbar unter:


[Link]
[Link]

654
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

J Quelltextkonventionen

J.1 Benennung

J.1.1 Sprechende Namen verwenden


Benutzen Sie sprechende Namen für alle Bezeichner (Klassen-, Variablen- und
Methodennamen). Vermeiden Sie Mehrdeutigkeiten. Vermeiden Sie Abkürzungen.
Einfache verändernde Methoden sollten in der Form setzeIrgendetwas(…) benannt
werden. Einfache sondierende Methoden sollten in der Form gibIrgendetwas(…)
benannt werden. Sondierende Methoden mit einem booleschen Ergebnis sind oft
mit istIrgendetwas(…) benannt, beispielsweise istLeer().

J.1.2 Klassennamen beginnen mit einem


Großbuchstaben

J.1.3 Klassennamen sind Hauptwörter im Singular

J.1.4 Methoden und Variablennamen beginnen mit


einem Kleinbuchstaben
Benutzen Sie für alle drei – für Klassen-, Methoden- und Variablennamen – Groß-
buchstabe am Anfang eines neuen Wortes nach dem ersten Wort, um zusam-
mengesetzte Namen lesbarer zu machen, etwa anzahlEintraege.

J.1.5 Konstanten werden in GROSSBUCHSTABEN


geschrieben
Verwenden Sie Unterstriche, um zusammengesetzte Konstantennamen lesbarer
zu gestalten:
OBERE_GRENZE1

1 Anmerkung des Übersetzers: Im Deutschen ist dies aufgrund zusammengesetzter Hauptwörter


nicht so häufig notwendig wie im Englischen.

655
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang J Quelltextkonventionen

J.2 Layout

J.2.1 Eine Stufe der Einrückung besteht aus vier


(oder drei) Leerzeichen

J.2.2 Alle Anweisungen innerhalb eines Blocks sind


um eine Stufe eingerückt

J.2.3 Klammern für Klassen und Methoden stehen


allein auf einer Zeile
Die geschweiften Klammern für den Block einer Klasse oder einer Methode ste-
hen jeweils auf einer eigenen Zeile und befinden sich auf derselben Einrückungs-
stufe. Beispielsweise:
public int gibAlter()
{
Anweisungen
}

J.2.4 Für alle anderen Blöcke steht die öffnende


Klammer am Ende einer Zeile
Alle anderen Blöcke beginnen mit einer geschweiften Klammer am Ende der Zeile
mit dem einleitenden Schlüsselwort für den Block. Die schließende Klammer steht
auf einer eigenen Zeile und ist genauso weit eingerückt wie das einleitende Schlüs-
selwort für den Block. Beispielsweise:
while(Bedingung) {
Anweisungen
}
if(Bedingung) {
Anweisungen
}
else {
Anweisungen
}

J.2.5 In Kontrollstrukturen immer Klammern


verwenden
Verwenden Sie immer geschweifte Klammern in if-Anweisungen und Schleifen,
auch wenn der Rumpf nur aus einer einzelnen Anweisung besteht.

656
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
J.3 Dokumentation

J.2.6 Vor der öffnenden Klammer für den Block


einer Kontrollstruktur steht ein Leerzeichen

J.2.7 Operatoren links und rechts mit einem


Leerzeichen absetzen

J.2.8 Immer eine Leerzeile zwischen Methoden


(und Konstruktoren)
Benutzen Sie Leerzeilen, um logische Abschnitte im Quelltext voneinander abzu-
setzen; also mindestens zwischen Methoden immer eine Leerzeile, aber auch zwi-
schen logischen Abschnitten innerhalb einer Methode.

J.3 Dokumentation

J.3.1 Jede Klasse wird mit einem


Klassenkommentar eingeleitet
Der Klassenkommentar enthält mindestens
 eine allgemeine Beschreibung der Klasse
 Namen des/der Autoren
 eine Versionsnummer.
Jede Person, die zur Klasse beigetragen hat, sollte als Autor aufgeführt sein oder
in anderer Form erwähnt sein.
Eine Versionsnummer kann eine einfache Nummer oder ein Datum sein, sie kann
aber auch in einem anderen Format vorliegen. Wichtig ist, dass ein Leser an ihr
erkennen kann, dass zwei Versionen sich unterscheiden und welche von ihnen
die neuere ist.

J.3.2 Jede Methode hat einen


Methodenkommentar

J.3.3 Kommentare sind im Javadoc-Format


Klassen- und Methodenkommentare müssen von Javadoc erkannt werden kön-
nen. Anders gesagt: Sie sollten mit der Zeichenfolge /** beginnen.

657
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang J Quelltextkonventionen

J.3.4 Implementierungskommentare nur falls


notwendig
Kommentare in den Anweisungsteilen einer Klasse sollten eingefügt werden,
wenn der Quelltext nicht leicht zu verstehen ist (und es sollte eher versucht wer-
den, den Quelltext so leicht verständlich wie möglich zu machen). Kommentieren
Sie keine offensichtlichen Anweisungen – gehen Sie davon aus, dass Ihr Leser
Java versteht.

J.4 Restriktionen bei der Sprachbenutzung

J.4.1 Reihenfolge der Deklarationen: Datenfelder,


Konstruktoren, Methoden
Die Elemente einer Klassendefinition erscheinen (falls vorhanden) in folgender Rei-
henfolge: Paketzugehörigkeit; import-Anweisungen; Klassenkommentar; Klassen-
kopf; Definitionen der Datenfelder; Konstruktoren; Methoden; innere Klassen.

J.4.2 Datenfelder dürfen nicht als public deklariert


werden (Ausnahme: konstante Datenfelder)

J.4.3 Immer einen Zugriffsmodifikator benutzen


Deklarieren Sie alle Datenfelder und Methoden entweder als private, public oder
protected. Benutzen Sie nicht den Default ohne Schlüsselwort (Paket-privater
Zugriff).

J.4.4 Klassen einzeln importieren


Import-Anweisungen, die jede Klasse einzeln benennen, sind dem Import von gan-
zen Paketen vorzuziehen. Beispielsweise
import [Link];
import [Link];
ist besser als
import [Link].*;

658
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
J.5 Programmiermuster

J.4.5 Immer einen Konstruktor deklarieren


(auch wenn der Rumpf leer ist)

J.4.6 Immer einen expliziten super-Aufruf im


Konstruktor einfügen
Verlassen Sie sich im Konstruktor einer Subklasse nicht darauf, dass ein Aufruf des
Konstruktors der Superklasse automatisch eingefügt wird. Fügen Sie den Aufruf
super() explizit ein, auch wenn es ohne ginge.

J.4.7 Alle Datenfelder im Konstruktor initialisieren


Einschließlich Zuweisungen von Standardwerten. Dadurch wird es für einen Leser
deutlich, dass die korrekte Initialisierung aller Datenfelder beachtet wurde.

J.5 Programmiermuster

J.5.1 Iteratoren für Sammlungen benutzen


Benutzen Sie eine for-each-Schleife, um über eine Sammlung zu iterieren. Wenn
die Sammlung während der Iteration geändert werden muss, verwenden Sie einen
Iterator, keinen int-Index.

659
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG

K Wichtige Bibliotheksklassen

Die Java-Plattform beinhaltet eine große Anzahl von Bibliotheken, die ein weites
Spektrum an Programmieraufgaben unterstützen.
In diesem Anhang geben wir eine kurze Übersicht einiger Klassen und Interfaces
aus den wichtigsten Paketen des Java-API. Ein kompetenter Java-Programmierer
sollte mit den meisten von ihnen vertraut sein. Dieser Anhang gibt lediglich einen
Überblick und sollte in Verbindung mit der vollständigen API-Dokumentation
gelesen werden.

K.1 Das Paket [Link]


Die Klassen und Interfaces im Paket [Link] sind grundlegend für die Sprache
Java. Deshalb ist dieses Paket implizit und automatisch in jeder Klassendefinition
importiert.

Tabelle K.1
Klasse / Interface Beschreibung Paket [Link] –
Interface Comparable Die Implementierung dieses Interface ermöglicht den Ver- Kurzdarstellung der
gleich und die Sortierung von Objekten der implementie- wichtigsten Klassen.
renden Klasse. Statische Hilfsmethoden wie [Link]
und [Link] können für Objekte von Typen, die
Comparable implementieren, zum effizienten Sortieren
genutzt werden.
Interface Runnable Ein Interface, das keine Parameter hat und kein Ergebnis
zurückgibt, es wird als Typ für Lambda-Ausdrücke mit die-
sen Eigenschaften verwendet.
Klasse Math Die Klasse Math enthält ausschließlich statische Datenfelder
und Methoden. Die Werte der mathematischen Konstan-
ten e und sind hier definiert, zusammen mit den trigono-
metrischen und anderen Funktionen, wie beispielsweise
abs (Betrag), min, max und sqrt (Quadratwurzel).
Klasse Object Alle Klassen haben Object als die Wurzelklasse ihrer eige-
nen Klassenhierarchie. Von dieser Klasse erben alle Objekte
Standardimplementierungen von so wichtigen Methoden
wie equals und toString. Weitere wichtige Methoden in
dieser Klasse sind clone und hashCode.

661
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang K Wichtige Bibliotheksklassen

Klasse / Interface Beschreibung


Klasse String Strings spielen in vielen Anwendungen eine wichtige Rolle,
sie erhalten deshalb eine besondere Behandlung in Java.
Die zentralen Methoden der Klasse String sind charAt,
equals, indexOf, length, split und substring. Strings sind
unveränderliche Objekte, sodass Methoden wie trim, die
wie verändernde Methoden aussehen, tatsächlich ein
neues String-Objekt zurückliefern, das das Ergebnis der
Operation darstellt.
Klasse StringBuilder Die Klasse StringBuilder ist eine effiziente Alternative zu
String, wenn eine Zeichenkette aus mehreren Kompo-
nenten zusammengesetzt werden soll, etwa über Kon-
katenation. Die zentralen Methoden sind append, insert
und toString.

K.2 Das Paket [Link]


Das Paket [Link] ist eine relativ zusammenhangslose Sammlung von nützlichen
Klassen und Interfaces.

Tabelle K.2
Paket [Link] – Klasse / Interface Beschreibung
Kurzdarstellung der Interface Collection Dieses Interface definiert den Basissatz an Methoden der
wichtigsten Klassen meisten Sammlungsklassen im Paket [Link], wie bei-
und Interfaces. spielsweise ArrayList, HashSet und LinkedList. Es definiert
die Signaturen für die Methoden add, clear, iterator, remove
und size.
Interface Iterator Iterator definiert eine einfache und konsistente Schnittstelle
für das Iterieren über den Inhalt einer Sammlung. Die drei
wichtigsten Methoden dieses Interface sind hasNext, next,
und remove.
Interface List List erweitert das Interface Collection und definiert Metho-
den zur expliziten Manipulation der Ordnung in einer Samm-
lung. Viele Methoden akzeptieren deshalb einen Index als
Parameter, wie beispielsweise add, get, remove und set. Klas-
sen wie ArrayList und LinkedList implementieren List.
Interface Map Das Interface Map bietet eine Alternative zu den listenbasier-
ten Sammlungen, da es die Idee unterstützt, jedes Objekt in
einer Sammlung mit einem Schlüsselwert zu verbinden.
Objekte werden über ihre put und get-Methoden hinzuge-
fügt bzw. herausgeholt. Beachten Sie, dass eine Map keinen
Iterator zurückgibt, sondern dass ihre Methode keySet
einen Satz Schlüssel und ihre Methode values eine Samm-
lung von Objekten in der Map zurückliefert.
Interface Set Set erweitert das Interface Collection um den Anspruch,
dass es Sammlungen ohne Duplikate geben kann. Es muss
betont werden, dass Set keinerlei Garantie für diese Restrik-
tion liefern kann.

662
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
K.2 Das Paket [Link]

Klasse / Interface Beschreibung


Das bedeutet, dass Set lediglich als Marker-Interface angebo-
ten wird, mit dem die Implementierer von Sammlungsklas-
sen signalisieren können, dass ihre Klassen diese spezielle
Einschränkung erfüllen.
Klasse ArrayList ArrayList ist eine Implementierung des Interface List, die
mithilfe von Arrays über int-Indizes einen effizienten direk-
ten Zugriff auf die gespeicherten Elemente bietet. Wenn
Objekte an anderen Positionen als am Ende eingefügt
oder entfernt werden, dann müssen alle nachfolgenden
Elemente verschoben werden, um Platz zu schaffen oder
die entstehende Lücke zu schließen. Zentrale Methoden
sind add, get, iterator, remove und size.
Klasse Collections Collections ist eine Sammelstelle für statische Methoden,
mit denen Sammlungen manipuliert werden können. Zen-
trale Methoden sind binarySearch, fill und sort.
Klasse HashMap HashMap ist eine Implementierung des Interface Map. Zentrale
Methoden sind get, put, remove und size. Eine Iteration über
eine HashMap erfolgt üblicherweise in zwei Schritten: Frage
zuerst die Schlüsselmenge über die Methode keySet ab,
anschließend iteriere über die Schlüssel.
Klasse HashSet HashSet ist eine Hashtabellen-basierte Implementierung
des Interface Set. Zentrale Methoden sind add, remove und
size.
Klasse LinkedList LinkedList ist eine Implementierung des Interface List, die
intern eine verkettete Struktur zum Speichern der Elemente
verwendet. Direkter Zugriff auf die Enden der Liste ist effizi-
ent, aber der Zugriff auf beliebige Elemente über einen Index
ist weniger effizient als bei einer ArrayList. Andererseits
erfordert das Einfügen oder Entfernen innerhalb der Liste
nicht das Verschieben von Elementen. Zentrale Methoden
sind add, getFirst, getLast, iterator, removeFirst, removeIf,
removeLast, size und stream.
Klasse Random Die Klasse Random unterstützt die Erzeugung von Pseudozu-
fallswerten, typischerweise von Zufallszahlen. Die Sequenz
der erzeugten Zahlen wird durch einen Initialisierungswert
(seed) bestimmt, der an den Konstruktor übergeben oder
über die Methode setSeed gesetzt werden kann. Zwei
Random-Objekte, die vom gleichen Seed-Wert starten, liefern
dieselbe Sequenz von Werten bei identischen Aufrufen. Zen-
trale Methoden sind nextBoolean, nextDouble, nextInt und
setSeed.
Klasse Scanner Die Klasse Scanner bietet Unterstützung beim Einlesen und
Zerlegen von Eingaben. Sie wird häufig verwendet, um Ein-
gaben von der Tastatur einzulesen. Zentrale Methoden sind
next und hasNext.

663
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang K Wichtige Bibliotheksklassen

K.3 Die Pakete [Link] und [Link]


Die Pakete [Link] und [Link] enthalten Klassen und Interfaces zur Unter-
stützung von Eingabe und Ausgabe und von Zugriff auf das Dateisystem. Viele der
Ein-/Ausgabeklassen in [Link] sind danach klassifiziert, dass sie entweder auf
Datenströmen basieren also auf binären Daten arbeiten oder Reader- bzw. Writer-
Klassen sind, die auf (menschenlesbaren) Zeichen arbeiten. Im Paket [Link] fin-
den Sie verschiedene Klassen, die den bequemen Zugriff auf das Dateisystem
unterstützen.

Tabelle K.3
Paket [Link] – Klasse / Interface Beschreibung
Kurzdarstellung der Interface Serializable Das Interface Serializable ist ein leeres Interface, das
wichtigsten Klassen keinen Code in einer implementierenden Klasse erfor-
und Interfaces. dert. Klassen implementieren dieses Interface, um bei
einer Serialisierung berücksichtigt werden zu können.
Objekte dieser Klassen können als Ganzes aus Eingabe-
quellen und in Ausgabekanäle geschrieben werden. Dies
macht das Speichern und Wiederherstellen von persis-
tenten Daten zu einem relativ einfachen Prozess in Java.
Siehe auch die Klassen ObjectInputStream und Object-
OutputStream für weitere Informationen.
Klasse BufferedReader BufferedReader ist eine Klasse, die gepufferten, zeichen-
basierten Zugriff auf eine Eingabequelle bietet. Gepuf-
ferte Eingabe ist häufig effizienter als ungepufferte,
insbesondere, wenn die Eingabequelle im externen Datei-
system liegt. Weil die Eingabe gepuffert wird, kann diese
Klasse die Methode readLine zum Einlesen einer ganzen
Zeile anbieten, die in den meisten anderen Eingabeklas-
sen nicht vorhanden ist. Zentrale Methoden sind close,
read und readLine.
Klasse BufferedWriter BufferedWriter ist eine Klasse, die gepufferte, zeichen-
basierte Ausgabe anbietet. Gepufferte Ausgabe ist meist
effizienter als ungepufferte, insbesondere, wenn das Aus-
gabeziel im externen Dateisystem liegt. Zentrale Metho-
den sind close, flush und write.
Klasse File Die Klasse File bietet eine Objektabstraktion für Dateien
und Ordner (Verzeichnisse) in einem externen Dateisys-
tem. Sie bietet Methoden zum Prüfen, ob eine Datei les-
bar und/oder beschreibbar ist und ob es sich um eine
Datei oder ein Verzeichnis handelt. Ein File-Objekt kann
auch für eine nicht vorhandene Datei erzeugt werden,
häufig ist dies der erste Schritt für die Erzeugung einer
echten Datei in einem Dateisystem. Zentrale Methoden
sind canRead, canWrite, createNewFile, createTempFile,
getName, getParent, getPath, isDirectory, isFile und list-
Files.

664
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
K.4 Das Paket [Link]

Klasse / Interface Beschreibung


Klasse FileReader Die Klasse FileReader wird benutzt, um eine externe Datei
zum zeichenweisen Einlesen ihres Inhaltes vorzubereiten.
Ein FileReader-Objekt wird meist an den Konstruktor
einer anderen Klasse übergeben (etwa an BufferedReader)
und selten direkt benutzt. Zentrale Methoden sind close
und read.
Klasse FileWriter Die Klasse FileWriter wird benutzt, um eine externe
Datei zum Schreiben von zeichenbasierten Daten vorzu-
bereiten. Über Konstruktorparameter kann festgelegt
werden, ob die Daten an den existierenden Inhalt ange-
hängt werden oder ob der Inhalt überschrieben wird. Ein
FileWriter-Objekt wird meist an den Konstruktor einer
anderen Klasse übergeben (etwa an BufferedWriter) und
selten direkt benutzt. Zentrale Methoden sind close,
flush und write.
Klasse IOException IOException ist eine Klasse, die geprüfte Exceptions defi-
niert und die Wurzel für die Hierarchie der meisten Excep-
tionklassen für Ein-/Ausgabe darstellt.

Tabelle K.4
Klasse / Interface Beschreibung Paket [Link] –
Interface Path Das Interface Path bietet wichtige Methoden für den Kurzdarstellung der
Zugriff auf Informationen über eine Datei im Dateisys- wichtigsten Klassen
tem. Path ist genau genommen ein Ersatz für die ältere und Interfaces.
Klasse File im Paket [Link].
Klasse Paths Die Klasse Paths wird benutzt, um mit get-Methoden
konkrete Instanzen des Path-Interface zurückzuliefern.
Klasse Files Die Klasse Files enthält statische Methoden, um Attribute
von Dateien und Verzeichnissen (Ordnern) abzufragen und
das Dateisystem zu manipulieren, z.B. Verzeichnisse erstel-
len und Dateizugriffsrechte ändern. Hier finden Sie auch
Methoden zum Öffnen von Dateien, wie newBuffered-
Reader.

K.4 Das Paket [Link]


Das Paket [Link] enthält nur Interfaces. Es liefert die häufig vorkom-
menden Typen, die mit Lambda-Ausdrücken und Methodenreferenzen in Zusam-
menhang stehen. Darunter sind Consumer-, Supplier- und Predicate-Interfaces
sowie die Interfaces für binäre Operatoren. Alle Interfaces sind wichtig und wir
beschreiben hier nur eine kleine Auswahl.

665
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang K Wichtige Bibliotheksklassen

Tabelle K.5
Paket [Link]. Klasse / Interface Beschreibung
function – Kurzdar- Interface BiFunction Das Interface BiFunction hat zwei Parameter und liefert
stellung einiger typi-
einen Ergebniswert zurück. Die Typen von Paramter und
scher Interfaces.
Ergebnis können alle unterschiedlich sein, dies ist also
eine Generalisierung des BinaryOperator-Interface.
Interface Binary- Das Interface BinaryOperator hat zwei Parameter seines
Operator parametrisierten Typs und liefert ein Ergebnis vom sel-
ben Typ. Wird häufig in reduce-Operationen eingesetzt.
Interface Consumer Das Interface Consumer hat einen Parameter seines
parametrisierten Typs und void als Ergebnis.
Interface Predicate Das Interface Predicate hat einen Parameter seines
parametrisierten Typs und liefert ein Ergebnis vom Typ
boolean. Wird häufig in filter-Operationen eingesetzt.
Interface Supplier Das Interface Supplier hat keine Parameter und liefert
einen Ergebniswert seines parametrisierten Typs zurück.

K.5 Das Paket [Link]


Das Paket [Link] enthält Klassen und Interfaces zur Unterstützung der Netz-
werkprogrammierung. Die meisten von ihnen liegen außerhalb des Rahmens die-
ses Buches.

Tabelle K.6
Paket [Link] – Klasse / Interface Beschreibung
Kurzdarstellung der Klasse URL Die Klasse URL repräsentiert sogenannte Uniform Resource
wichtigsten Klassen. Locator, d.h., sie bietet die Möglichkeit, die Position einer
Ressource im Internet zu beschreiben. Tatsächlich kann sie
auch benutzt werden, um eine Ressource in einem lokalen
Dateisystem zu beschreiben. Wir haben diese Klasse hier
aufgenommen, weil Klassen aus den Paketen [Link] und
[Link] häufig URL-Objekte benutzen. Zentrale Metho-
den sind getContent, getFile, getHost, getPath und open-
Stream.

K.6 Weitere wichtige Pakete


Weitere wichtige Pakete sind
[Link]
[Link]
[Link]
[Link]
Diese werden ausführlich bei der Erstellung grafischer Benutzungsoberflächen
(GUIs) benutzt und enthalten viele nützliche Klassen, mit denen sich ein GUI-Pro-
grammierer vertraut machen sollte.

666
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register

Symbols catch 546, 634 B


!, Operator 106 finally 634 Baelle, Projekt 249
&&, Operator 106 geschützte 547 Bedingte Anweisung 76
%, Operator 109 if 76 Benutzungsoberfläche, grafische 475
+, Operator 108 import 136, 226 Besserer-Ticketautomat, Projekt 73,
für Zeichenketten 71 throw 539 129
=, Operator 64 throws 545 Bestimmte Iteration 152
>=, Operator 77 try 546, 634 Bibliothek
||, Operator 106 Zuweisung 64 AWT 477
Array importieren 136
A for-each-Schleife 272 Paket 135
Index 140 Swing 477
Abbildung
length 272 Bibliotheksklasse 661
mit dem Map-Konzept 228
mehrdimensionales 285 Bildbetrachter0-2, Projekt 486
Abkürzungsoperator 627
Objekt benutzen 268 Bildbetrachter0-4, Projekt 492
abstract
Objekt erzeugen 267 Bildbetrachter1-0, Projekt 478, 490
für Klassen 448
Variable deklarieren 266 Bildbetrachter2-0, Projekt 504
für Methoden 446
ArrayIndexOutOfBounds Bildbetrachter3-0, Projekt 509
Abstrakte Klasse 448
Exception 269 Bildformat 478
Beispiel 444
Array-Initialisierer 284 Block 66
vs. Interface 468
ArrayList 133 BlueJ
Abstrakte Methode 446
Beispiel für Benutzung 133, auf Deutsch umstellen 618
Abstrakte Subklasse 449
173, 223 installieren 617
Abstraktion 98
Elemente entfernen 142 konfigurieren 618
Flexibilität 455
generische Klasse 139 Boolescher Ausdruck 77
Adressbuch, Projekt 528
implementiert das Interface List 462 BorderLayout 494
Adressbuch-Assert, Projekt 554
Indexzugriff 165 BoxLayout 496
Adressbuch-IO, Projekt 569
iterator 163 Brain Projekt 285
Adressbuch-JUnit, Projekt 557
Iteratorzugriff 165 Buch-Aufgabe, Projekt 94
Adressbuch-V1T, Projekt 530
remove 166 Bug 119
Adressbuch-V2G, Projekt 534
Schlüsselmethoden 137
Adressbuch-V2T, Projekt 534
assert-Anweisung 554 C
Adressbuch-V3T, Projekt 548
Attribut
Akteur Cast-Operator 396
synonym zu Datenfeld 38
als abstrakte Klasse 454 für Objekttypen 624
Aufzählungstyp 326
als Interface 457 für primitive Typen 622
Auktionssystem, Projekt 169
Akzeptanztest 337 catch-Klausel 546, 634
Ausdruck 64
Analyse und Entwurf 573 Class (Klasse) 467
testen 89
Änderungen lokal halten 311 Closure 184
Ausgabeanweisung 360
Anonyme innere Klasse 515 Code-Duplizierung 298, 382
Auswahlanweisung 629
Anonymes Objekt 174 vermeiden für Klienten 396
Autoboxing 237
Anweisung Code-Vervollständigung 248
Automat Projekt 276
assert 554 Combo-Boxen 522
Automat, zellulärer 276
Ausgabeanweisung 360 Compiler 43
AWT (Java-Bibliothek) 477
bedingte 76

667
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register

Computermodell Verb/Substantiv-Methode 574 File (Klasse) 561


Modell eines Ausschnitts der von Benutzungsschnittstellen 583 filter (Funktion) 195
realen Welt 31 von Klassenschnittstellen 582 Filter-Map-Reduce 195
CRC-Karten 576 Wasserfallmodell 586 finally-Klausel 551, 634
Entwurfsmuster 588 FlowLayout 494
D Beobachter 592 forEach-Methode 190
Datenfeld 57 Dekorierer 589 for-each-Schleife 147, 151, 271
automatische Initialisierung 62 Fabrikmethode 591 Formaler Parameter 63
öffentlich 244 Singleton 590 vs. lokale Variable und
vs. lokale Variable und formaler Entwurfsregeln 330 Datenfeld 82
Parameter 82 Enumeration siehe Aufzählungstyp for-Schleife 270
Zustand halten 38 Ereignisverarbeitung 484 Frame (GUI) 480
Debugger 119, 363, 643 Ergebnistyp Fuechse-und-Hasen, Projekt 432
anhalten 644 nicht bei Konstruktoren 66 Funktion, anonyme 190
Aufruffolge 646 void 68
Einzelausführung 124 Ersetzbarkeit 394 G
Haltepunkt setzen 123, 644 Event-Listener 484 Geheimnisprinzip 243
Kontrollknöpfe 644 Exception 634 Generische Klasse 136, 139, 225
Schritt hinein 126 ArrayIndexOutOfBounds Generischer Typ 163
Defensive Programmierung 531 Exception 269 Geprüfte Exception 540, 545
Die Welt von Zuul, Projekt 296, 423 Auswirkungen 542 Geschäftsfall 577
Direkteingabe 40, 89 behandeln 545 Grafische Benutzungsoberfläche 475
Dokumentation fangen 546 GridLayout 495
generische Klassen 225 geprüfte 540, 545 GUI 475
Java-Klassenbibliothek 215 Handler 546 GUI-Klassen
Klasse 239 Hierarchie 541 JButton 510
Konstruktor 240 IndexOutOfBoundsException 141 JComboBox 520
Methode 240 mehrere Exceptions werfen und JFrame 479
Dynamische Methodensuche 410 fangen 548 JLabel 481
Dynamische Sicht 102 neue Klassen definieren 552 JList 522
Dynamischer Typ 405 Prinzipien der Behandlung 539 JMenu 483, 499
Dynamisches Binden 410 propagieren 550 JMenuBar 483
ungeprüfte 540, 543 JMenuItem 499
E werfen 539 JPanel 496
extends zwischen Klassen 383 JScrollPane 522
Ein-/Ausgabe
Externer Methodenaufruf 115
CSV-Format 567
dateibasierte 560 Extreme Programming 584 H
Eingabe parsen 567 Haltepunkt setzen 123, 644
Scanner 567 F HashMap, Beispiel für Benutzung 229,
Text ausgeben 562 Fallstudie 597 306
Text einlesen 565 Analyse und Entwurf 599 Haus, Projekt 41
Einzeiliger Kommentar 58 CRC-Karten 600 Hohe Kohäsion 312
Einzelausführung 124 iterative Entwicklung 608
Eliza 210 Klassen identifizieren 599 I
Enge Kopplung Partnerklassen 603
Implementierung
durch öffentliche Datenfelder 311 Szenarios 600
einer Klasse 216
Entkopplung 311 testen 607
vs. Schnittstelle 216
Entwurf Fehler implements (Schlüsselwort) 458
CRC-Karten 576 vermeiden 558
implements-Klausel 458
Dokumentation 583 vom Dienstleister gemeldet 534
import-Anweisung 136, 226
Klassen 293 Wiederaufsetzen 557 Index in einer Sammlung 140
Klassen identifizieren 575 Fehlerbeseitigung 336
Index-Grenzverletzung 141
Kooperation 584 Ausgabeanweisung 360
IndexOutOfBoundsException 141
nach Zuständigkeiten 160, 308 Debugger 363 Indexvariable 154
Prototyping 585 manuelle Ausführung 354
Indexzugriff 165
Softwarewachstum 586 mündliche Ausführung 360
Inhaltsfläche (GUI) 480
Szenarios 577 Figuren, Projekt 32 Initialisierung 60

668
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register

instanceof-Operator 422 Autoboxing 237 Kontrollstruktur 629


Instanz 33 beschreibt Objekte 31 Auswahlanweisung 629
einer bestimmten Klasse 33 Class 467 Schleifen 632
interaktiv erzeugen 32 definiert einen Typ 101 Konvention 655
synonym zu Objekt 32 entwerfen 293 Dokumentation 657
Instanzvariable, synonym zu Daten- File 561 Layout 656
feld 57 generische 136, 139, 225 Restriktion 658
Interaktion zwischen Klient und identifizieren 575 Kopf einer Methode 66
Dienstleister 531 importieren 136 Kopplung 243, 303
Interface Kohäsion 317 enge 311
als Spezifikation 462 neue Exception-Klasse 552 entkoppeln 311
als Typ 461 Object 397 implizite 311
Event-Listener 484 Paket 135 Kapselung 308
multiple Vererbung 459 parametrisierte 225 lose 303
Path 561 Random 221 Pfeile im Klassendiagramm 314
Serializable 569 Scanner 567 Kritzeln, Projekt 245
vs. abstrakte Klasse 468 Unboxing 237
Interner Methodenaufruf 114 verbessern 159 L
Ist-ein-Beziehung 383 Wrapper 236, 623 Laborkurse, Projekt 44, 86, 102, 401
Iteration Klassenbibliothek Lambda-Ausdruck 184, 188
bestimmte 152 importieren 136 Layout (GUI) 476
mit Indexvariable 154 Paket 135 Lebensdauer
unbestimmte 152 Klassendiagramm 101 einer lokalen Variable 81
Iterative Entwicklung 608 Klassendokumentation eines Datenfelds 63
Iterator 163 lesen 215 eines formalen Parameters 63
schreiben 239 LinkedList implementiert das Interface
J Klassenentwurf 293 List 462
Jäger-Beute-Modellierung 431 Klassenmethode 253 Liste 522
jar-Dateien 641 Einschränkung 254 Logischer Fehler 335
Java Archive Format 641 main 254 Logischer Operator 106
Java-Bibliothek 208 vs. Instanzmethode 253 Lokale Variable 80
Java-Datentypen 621 Klassenvariable 250 Lebensdauer 81
javadoc 651 Klausel Sichtbarkeit 81
Klasse 240 catch 546 vs. Datenfeld und formaler Para-
Konstruktorkommentar 240 finally 551 meter 82
Markierungsabschnitt 652 try 546 Lokalität von Änderungen 311
Methodenkommentar 240 Klient-Dienstleister-Interaktion 531 Lose Kopplung 303
Schlüsselwörter 241 Knopf (GUI) 509 Lotka-Volterra-Modell 431
JButton 510 Kohäsion
JComboBox 520 Beispiele 480, 484 M
JFrame 479 für bessere Lesbarkeit 318
Mail-System, Projekt 120
JLabel 481 für Wiederverwendung 319
main-Methode 254
JList 522 hohe 312 Manifest-Datei 642
JMenu 483, 499 von Klassen 317
Manuelle Ausführung 354
JMenuBar 483 von Methoden 316
map (Funktion) 195
JMenuItem 499 Kommentar 58
Map, Konzept 228
JPanel 496 Komponente (GUI) 476 Map-Klasse 227
JScrollPane 522 Konditionaloperator 279
Maschinencode 43
JUnit 344, 647 Konsistenzüberprüfung, interne 553
Mehrzeiliger Kommentar 58
JUnit-Test 556 Konstante 252 Menge, Konzept 232
Konstruktor 60
Menüzeile (GUI) 480
K der Superklasse aufrufen 388
Methode 35
Kapselung 304 Dokumentation 240 abstrakte 446
initialisiert ein Objekt 60
Kinobuchungssystem, Projekt 574 Aufrufe verketten 175
javadoc-Kommentar 240
Klasse aufrufen 42, 88
abstrakte 448 Überladung 114 Dokumentation 240
und Vererbung 387
anonyme innere 515 dynamische Methodensuche 410
ArrayList 134 Ergebnisse liefern 45

669
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register

extern aufrufen 115 Objektleiste in BlueJ 33 Adressbuch 528


implementiert Verhalten 56 Objektreferenz 102 Adressbuch-Assert 554
interaktiv aufrufen 33 Objekttyp 623 Adressbuch-IO 569
intern aufrufen 114 vs. primitiver Typ 103 Adressbuch-JUnit 557
javadoc-Kommentar 240 Öffentliches Datenfeld 244 Adressbuch-V1T 530
Kohäsion 316 Onlineshop, Projekt 337 Adressbuch-V2G 534
Kopf und Rumpf 66 OnlineShop-JUnit, Projekt 344 Adressbuch-V2T 534
main 254 Operation Adressbuch-V3T 548
private 244 intermediäre 198 Auktionssystem 169
protected 420 terminale 198 Automat 276
sondierende 67 Operator Automat-v1 277
super-Aufruf 413 + zur Verkettung 108 Automat-v2 281
überschreiben 408 Abkürzungsoperatoren 627 Automat-v3 282
verändernde 68 arithmetische Ausdrücke 625 Automat-v4 284
Methodenauswahl 410 boolesche Ausdrücke 626 Baelle 249
Methoden-Polymorphie 414 Cast- 396, 622, 624 Besserer-Ticketautomat 73
Methodensuche 410 instanceof 422 Bildbetrachter0-2 486
Mock-Up 353 Konditional- 279 Bildbetrachter0-4 492
Modell-Ansicht-Trennung 521 Modulo 109 Bildbetrachter1-0 478, 490
Modularisierung 99 new 113 Bildbetrachter2-0 504
Modulo-Operator % 109 ternärer 279 Bildbetrachter3-0 509
Modultest 337, 647 Brain 285
Moore-Nachbarschaft 286 P Buch-Aufgabe 94
Multiple Vererbung 453 Paar-Programmierung 584 Die Welt von Zuul 296, 423
für Interfaces 459 Pakete 135 Fallstudie 597
Musikplayer, Projekt 521 in der Standardbibliothek 226 Figuren 32
Musiksammlung, Projekt 133 [Link] 664 Fuechse-und-Hasen 432
[Link] 661 Haus 41
N [Link] 666 Kinobuchungssystem 574
Naiver Ticketautomat, Projekt 51 [Link] 664 Kritzeln 245
Negatives Testen 343 [Link] 136, 662 Laborkurse 44, 86, 102, 401
Netzwerk, Projekt 372 [Link] 665 Mail-System 120
Netzwerk-V1, Projekt 381 Parameter Musikplayer 521
Netzwerk-V2, Projekt 386 aktueller 63 Musiksammlung 133
Netzwerk-V3, Projekt 409 formaler 63 Naiver Ticketautomat 51
new, Operator 113 interaktiv übergeben 35 Netzwerk 372
null, Schlüsselwort 171 prüfen 533 Netzwerk-V1 381
Parameterübergabe 62 Netzwerk-V2 386
O Typregeln 65 Netzwerk-V3 409
Object (Klasse) 397 Parametrisierte Klasse 225 Onlineshop 337
Partnerklasse 603 OnlineShop-JUnit 344
equals 418
Pipeline 197 Recheneinheit 353, 364
hashCode 419
toString 398, 414 Polymorphe Variable 395 Recheneinheit-mit-Ausgaben 361
Positives Testen 343 Rechner-GUI 364
Objekt 31
Prädikat (Lambda) 199 Technischer-Kundendienst1 209
als Parameter 46
Primitiver Typ vs. Objekttyp 103 Technischer-Kundendienst2 227
anonymes 174
einer bestimmten Klasse 31 private (Zugriffsmodifikator) 242 Technischer-Kundendienst-IO 566
Programmausführung Technischer-Kundendienst-
erzeugen mit new 112
im Debugger 124 komplett 209, 568
erzeugt Objekte 112
Erzeugung verhindern 544 manuell 354 Tiermonitoring-v1 185
mündlich 360 Weblog-Auswertung 264
Interaktion 41
ohne BlueJ 637 Zeitanzeige 103
Iterator 163
Schnittstelle 118 Programmiermuster 659 Ziegelsteine 367
Programmierstil Zuul-besser 315
Serialisierung 569
funktionaler 184, 189 Zuul-schlecht 295
unveränderliches 218
Objektdiagramm 101 imperativer 183 protected (Zugriffsmodifikator) 420
Programmierung, defensive 531 Prototyping 585
Objekterzeugung 112
Projekte Pseudocode 76
Objektinspektor 38, 340

670
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register

public (Zugriffsmodifikator) 242 Schnittstelle 118 positives 343


Punkt-Notation 116 einer Klasse 216 Regressionstest 344
einer Methode 217 Testklasse erzeugen 647
Q öffentlicher Teil einer Klasse 242 Testmethode erzeugen 647
Quelltext 42 vs. Implementierung 216 von Grenzfälle 341
Quelltextkonventionen 655 Selektive Darstellung 456 Testgerüst 350, 648
Serializable, Interface 569 Tests aufzeichnen 347
R Sichtbarkeit 79 Testwerkzeuge 647
einer lokalen Variablen 83 this 121
Random (Klasse) 221
Reader 561 eines Datenfelds 63 throw-Anweisung 539
eines formalen Parameters 63, 83 throws-Anweisung 545
Recheneinheit, Projekt 353, 364
Simulation 429 Tiermonitoring, Projekt 185
Recheneinheit-mit-Ausgaben,
Projekt 361 ereignisgesteuert 468 toString in BlueJ 414
Softwareabstraktion 99 try-Klausel 546, 634
Rechner-GUI, Projekt 364
Softwarewachstum 586 Typ
Redefinieren, synonym zu Überschrei-
ben 409 Sondierende Methode 67 Aufzählung 326
static, Schlüsselwort 250 Datentypen in Java 621
reduce (Funktion) 196
Statischer Typ 405 dynamischer 405
Refactoring 320
Klassen extrahieren 324 Stream 184, 193, 561 eines Parameters 36
String (Klasse) 88 generischer 163
Methoden extrahieren 324
equals 220 Interface 461
und Testen 320
Regressionstest 344 length 88, 217 Iterator 163
startsWith 214 Objektypen 623
Responsibility-driven Design 308
substring 88 primitiver 103, 621
Rollbalken 522
Rückgabeanweisung 66 trim 218 statischer 405
Subklasse 383
Rumpf einer Methode 66
RuntimeException 552
definiert Subtyp 393 U
Subtyp 393 Überladen 114
Subtyp Überschreiben von Methoden 408
S Parameterübergabe 395 Unbestimmte Iteration 152
Sammlung
Subklasse 393 Unboxing 237
Abstraktion 132 Zuweisung 393
ArrayList 133 Und-Operator && 106
Subtyping 391 Ungeprüfte Exception 540, 543
durchsuchen 156
super in einer Methode 413 Unveränderliches Objekt 218
Elemente entfernen 142, 165 Superklasse 383
fester Größe 263 Use Case 577
Object 397
flexibler Größe 133
Swing (Java-Bibliothek) 477 V
Index 140 Syntaxfehler 335
Indexzugriff 165 Variable
[Link] 71
Iteratorzugriff 165 Index 154
Szenarios 577 lokale 81
Nummerierung 140
Objektstrukturen 138 polymorphe 395
selektiv verarbeiten 149
T und Subtypen 393
Teamarbeit 649 Verändernde Methode 68
verarbeiten 146
Scanner (Klasse) 567 Technischer-Kundendienst1, Verb/Substantiv-Methode 574
Projekt 209 Vererbung
Schleife
Technischer-Kundendienst2, für einfachere Wartung 391
do-while 632
Projekt 227 gegen Code-Duplizierung 382, 391
Elemente entfernen 165
for 633 Technischer-Kundendienst-IO, im Klassendiagramm 382
Projekt 566 Konstruktoren 387
for-each 147, 151, 271
Technischer-Kundendienst-komplett, multiple 453
for-Schleife 270
Kopf 147 Projekt 209, 568 Vorteile 391
Teile und herrsche 98 zur Spezialisierung 383
Schleifenvariable 148
Testen zur Wiederverwendung 389, 391
while 153, 632
Schleifenvariable 148 automatisieren 344 zwischen Klassen mit extends 385
bei Refactorings 320 Vererbungshierarchie 384
Schlüsselwort 56
JUnit 647 Verkettung von Zeichenketten 108
null 171
static 250 JUnit-Tests in BlueJ 556 void als Ergebnistyp 68
negatives 343

671
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register

W Z Zugriffsstufen 420
Walkthrough 354 Zeichenketten Zuordnungstabelle 283
Wasserfallmodell 586 auf Gleichheit prüfen 220 Zusicherung 348, 554, 635
Weblog-Auswertung, Projekt 264 verketten 108 in BlueJ 556
Weizenbaum, Joseph 210 zerlegen 233 Richtlinien 555
while-Schleife 153 Zeitanzeige, Projekt 103 Zustand
Wiederaufsetzen nach Fehlern 557 Ziegelsteine, Projekt 367 definiert über Datenfelder 38
Wiederholung 147 Zufälliges Verhalten in Computern 221 Zuul-besser, Projekt 315
Wiederverwendung Zufallszahl erzeugen 222 Zuul-schlecht, Projekt 295
durch Vererbung 391 Zugriffsmodifikator Zuweisung 64
Wolfram-Code 283 private 242, 244 linke und rechte Seite 64
Wrapper-Klasse 236, 623 protected 420 Typregeln 65
Writer 561 public 242 Zuweisungsoperator = 64

672
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Copyright
Daten, Texte, Design und Grafiken dieses eBooks, sowie die eventuell
angebotenen eBook-Zusatzdaten sind urheberrechtlich geschützt. Dieses eBook
stellen wir lediglich als persönliche Einzelplatz-Lizenz zur Verfügung!
Jede andere Verwendung dieses eBooks oder zugehöriger Materialien und
Informationen, einschließlich

 der Reproduktion,
 der Weitergabe,
 des Weitervertriebs,
 der Platzierung im Internet, in Intranets, in Extranets,
 der Veränderung,
 des Weiterverkaufs und
 der Veröffentlichung

bedarf der schriftlichen Genehmigung des Verlags. Insbesondere ist die


Entfernung oder Änderung des vom Verlag vergebenen Passwortschutzes
ausdrücklich untersagt!
Bei Fragen zu diesem Thema wenden Sie sich bitte an: info@[Link]

Zusatzdaten
Möglicherweise liegt dem gedruckten Buch eine CD-ROM mit Zusatzdaten bei.
Die Zurverfügungstellung dieser Daten auf unseren Websites ist eine freiwillige
Leistung des Verlags. Der Rechtsweg ist ausgeschlossen.
Hinweis
Dieses und viele weitere eBooks können Sie rund um die Uhr und legal auf
unserer Website herunterladen:

[Link]

Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.

Das könnte Ihnen auch gefallen