Java Lernen Mit Blue J
Java Lernen Mit Blue J
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.
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
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
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
6
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis
7
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis
8
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis
9
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis
10
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis
11
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Inhaltsverzeichnis
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
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 …
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
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!
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
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
Haus Kapitel 1
Ein Beispiel, in dem mit einigen Figuren eine Zeichnung erstellt wird; führt Quell-
text, Java-Syntax und Übersetzung ein.
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.
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.
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.
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
Lernziele
Zentrale Konzepte in diesem Kapitel: Objekte, Methoden, Klassen, Para-
meter
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.
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.
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.
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".
Ü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.
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.
Ü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.
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
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.
Ü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.
Ü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.
47
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 1 Objekte und Klassen
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.
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?
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
Ü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.
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.
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
Ü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
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.
62
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.5 Datenübergabe mit Parametern
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.
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;
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
Ü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?
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
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
Ü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?
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?
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
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
Ü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
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.
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.
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.
82
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.17 Datenfelder, Parameter und lokale Variablen
Ü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.
Ü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.
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.
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
Ü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.
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?
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.
92
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
2.22 Ausdrücke testen: die Direkteingabe
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;
}
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.
Abbildung 3.1
Die Anzeige einer
11:03 Digitaluhr.
97
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion
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.
99
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion
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.
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.
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
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
105
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion
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.
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.
109
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion
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
Abbildung 3.4
Ein Objektdiagramm
der Uhrenanzeige.
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
/**
* 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();
}
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.
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.
3.12 Methodenaufrufe
114
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.12 Methodenaufrufe
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.)
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.
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.
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.
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.
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.
Listing 3.5
Datenfelder und Kons-
truktor der Klasse
Nachricht.
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.
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.
Ü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
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
Ü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.
126
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
3.15 Mehr zu Methodenaufrufen
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.
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.
129
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 3 Objektinteraktion
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
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.
132
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.3 Ein Verwaltungssystem für Musikdateien
133
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen
Ü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?
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
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.
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.
: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>
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.
139
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen
Ü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.
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
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
:String :String
“MorningBlues.mp3” “MatchBoxBlues.mp3”
142
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.7 Nummerierung in Sammlungen
Ü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
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?
145
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen
Ü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
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.
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.
149
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen
Listing 4.4
Ausgewählte
Elemente der
Sammlung
ausgeben.
Ü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.
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.
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.
152
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.10 Unbestimmte Iteration
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) {
...
}
}
Listing 4.5
Benutzung einer
while-Schleife zur
Ausgabe der Tracks.
154
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.10 Unbestimmte Iteration
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.
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
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.
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.
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
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.
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.
165
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 4 Objektsammlungen
166
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.13 Zusammenfassung des Musiksammlung-Beispiels
Ü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?
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.
170
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem
Ü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.
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.
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
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.
174
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem
Ü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.
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?
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.
180
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
4.14 Ein weiteres Beispiel: ein Auktionssystem
Ü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
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.
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
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?
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.
189
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)
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)
Ü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.
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.
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.
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.
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.
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
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.
198
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams
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.
200
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
5.5 Streams
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.
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.
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.
205
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 5 Funktionale Verarbeitung von Sammlungen (fortgeschrittene Konzepte)
Ü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
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.
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.
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.
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
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?
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:
Returns:
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.
219
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen
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
Ü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.
Ü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.
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?
223
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen
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
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.
226
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.6 Benutzung von Map-Klassen für Abbildungen
Ü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.
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?
228
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.6 Benutzung von Map-Klassen für Abbildungen
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?
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.
231
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 6 Bibliotheksklassen nutzen
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.
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.
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
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.
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.
238
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.11 Die Klassendokumentation schreiben
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.
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
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).
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.
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.
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.
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.
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.
Ü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.
Ü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?
250
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.14 Klassenvariablen und Konstanten
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.
252
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.15 Klassenmethoden
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.
Ü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.
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.
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.
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.
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
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.
260
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
6.17 Weitere fortgeschrittene Konzepte
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
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.
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.)
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
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.
Ü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
Abbildung 7.1
Ein Array für 24
int-Werte.
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.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);
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).
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.
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.
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.
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.
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
Ü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?
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.
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
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.
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.
284
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
7.6 Arrays mit mehr als einer Dimension (fortgeschritten)
Ü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.
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
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)
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.
Ü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.
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
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.
297
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf
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
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.
301
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf
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.
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.
*/
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.).
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.
310
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.8 Änderungen lokal halten
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
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?
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
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.
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.
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.
318
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.11 Kohäsion
Ü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.
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
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.)
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.
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.
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.
328
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.14 Entwurfsregeln
Ü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.
331
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 8 Klassenentwurf
332
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
8.14 Entwurfsregeln
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
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.
336
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.3 Modultests in BlueJ
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
340
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.3 Modultests in BlueJ
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
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.
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.
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.
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.
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?
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
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)
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.
Ü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.
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.
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.
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.
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.
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
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
Listing 9.5
Zusätzliche Ausgabe-
anweisungen zur
Fehlersuche.
Listing 9.6
Eine Methode, die
den Objektzustand
ausgibt.
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
361
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 9 Fehler vermeiden
362
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
9.10 Debugger
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.
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 -> { })
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.
Ü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.
368
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
TEIL
II Anwendungsstrukturen
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
372
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“
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
ArrayList<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
374
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“
Listing 10.1
Der Quelltext der
Klasse Nachrichten-
Einsendung.
375
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung
376
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.1 Das Beispiel „Netzwerk“
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.
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.
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.
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.
Ü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?
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.
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
Abbildung 10.7
Einsendung
Einsendungen für
benutzername Netzwerk mit einer
zeitstempel
gefielWieOft
Klasse Ereignis-
kommentare * Methoden ausgelassen Einsendung.
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
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.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.
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.)
Ü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.
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.
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?
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
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.
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.
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.
399
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 10 Bessere Struktur durch Vererbung
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
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.
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.
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
Einsendung
...
anzeigen
NachrichtenEinsendung FotoEinsendung
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.
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.
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.
...
anzeigen
NachrichtenEinsendung FotoEinsendung
... ...
anzeigen anzeigen
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?
[[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
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:
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.
Listing 11.2
Redefinierende
Methode mit
super-Aufruf.
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.
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.
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.
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
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
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
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.
423
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung
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
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.
426
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
11.11 Ein weiteres Beispiel für Vererbung mit Überschreiben
427
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 11 Mehr über Vererbung
428
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
KAPITEL
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
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
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
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
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.
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.
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.
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?
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.
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.
444
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.3 Abstrakte Klassen
Ü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
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.
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.
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.
450
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
12.4 Weitere abstrakte Methoden
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.
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.
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.
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.
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.
Abbildung 12.4
Eine Akteur-Hierarchie
mit einer Klasse
Zeichenbar.
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.
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
459
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion
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?
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
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
Ü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
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"));
DiagrammAnsicht GitterAnsicht
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];
Ü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?
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.
468
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
10.10 Ereignisgesteuerte Simulationen
469
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion
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.
471
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 12 Weitere Techniken zur Abstraktion
Ü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
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
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.
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
Abbildung 13.2
Eine einfache
Anwendung zum
Bildbetrachten.
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
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.
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
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
Ü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.
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
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
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.
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.
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?
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.
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]();
493
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen
Ü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?
Abbildung 13.5
FlowLayout.
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
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?
496
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version
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
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.
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.
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.
502
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.5 Bildbetrachter 1.0: die erste komplette Version
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.
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.
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.
<<abstract>>
BilddateiManager Filter
Bildflaeche
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
Ü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
Sobald Sie einige eigene Filter implementiert haben, sollten Sie die Versionsnum-
mer Ihres Projekts auf „Version 2.1“ erhöhen.
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
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.
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
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.
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
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.
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.
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.
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.
521
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 13 Grafische Benutzungsoberflächen
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.
524
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
13.10 Ein weiteres Beispiel: der Musikplayer
Ü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
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.
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?
531
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung
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.
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?
534
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.3 Fehlermeldungen durch den Dienstleister
Ü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.
535
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung
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.
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.
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.
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.
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.
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.
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.
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.
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("", "", "");
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.
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.
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.
548
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.5 Die Behandlung von Exceptions
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 ...
...
}
550
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.5 Die Behandlung von Exceptions
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
}
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
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.
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.
555
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung
Ü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?
556
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
14.8 Wiederaufsetzen und Fehlervermeidung
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.
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.
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
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
Listing 14.20
In eine Textdatei
schreiben mithilfe
einer try-Anweisung
mit Ressourcen.
Ü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.
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
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.
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
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.
571
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 14 Fehlerbehandlung
Ü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
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.
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.
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.
Substantive: Verben:
Kinobuchungssystem verwaltet (Platzreservierungen)
speichert (Telefonnummer)
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.
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
Ü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
Ü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.
Ü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
Ü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.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.
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.
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).
586
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
15.6 Softwarewachstum
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
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
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.
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.
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).
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.
593
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen
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
595
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 15 Entwurf von Anwendungen
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.
597
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie
Ü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
Ü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
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
Ü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.
602
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.3 Klassenentwurf
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
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.
607
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie
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.
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.
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.
613
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Kapitel 16 Eine Fallstudie
614
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
16.4 Iterative Entwicklung
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
616
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG
617
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang A Arbeiten mit BlueJ-Projekten
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.
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.)
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.
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
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
– Subtraktion
* Multiplikation
/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
625
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang C Operatoren
626
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
C.3 Abkürzungsoperatoren
|| oder
^ exklusiv-oder
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
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.
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.
639
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang E Java ohne BlueJ
try {
[Link](new [Link]([Link], true, "Cp850"));
}
catch ([Link] e) {
}
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
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.
641
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang E Java ohne BlueJ
642
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG
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.
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.
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.
644
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
F.3 Anzeige der Variablen
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.
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.
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.
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.
647
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang G Testwerkzeuge für Modultests mit JUnit
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
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.
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.
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
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.
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.
653
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang I Javadoc
Abbildung I.1
Die Implementierung/
Schnittstelle-Option im
BlueJ-Editor.
654
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
ANHANG
J Quelltextkonventionen
J.1 Benennung
655
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang J Quelltextkonventionen
J.2 Layout
656
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
J.3 Dokumentation
J.3 Dokumentation
657
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang J Quelltextkonventionen
658
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
J.5 Programmiermuster
J.5 Programmiermuster
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.
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
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]
663
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Anhang K Wichtige Bibliotheksklassen
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]
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.
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.
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.
666
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register
667
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register
668
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register
669
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register
670
Persönliche Einzellizenz, Weitergabe an Dritte nicht gestattet.
Register
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
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]