Java in 14 Wochen: Ein Lehrbuch für Studierende der Wirtschaftsinformatik [1. Aufl.]
 9783658303129, 9783658303136

Table of contents :
Front Matter ....Pages I-XIV
Einführung und Motivation (Kaspar Riesen)....Pages 1-21
Daten und Ausdrücke (Kaspar Riesen)....Pages 23-52
Klassen des Java API Verwenden (Teil 1) (Kaspar Riesen)....Pages 53-74
Eigene Klassen Programmieren (Teil 1) (Kaspar Riesen)....Pages 75-102
Graphische Benutzeroberflächen (Teil 1) (Kaspar Riesen)....Pages 103-136
Klassen des Java API Verwenden (Teil 2) (Kaspar Riesen)....Pages 137-161
Bedingungen und Schleifen (Kaspar Riesen)....Pages 163-185
Arrays (Kaspar Riesen)....Pages 187-212
Eigene Klassen Programmieren (Teil 2) (Kaspar Riesen)....Pages 213-237
Methoden Planen und Programmieren (Kaspar Riesen)....Pages 239-262
Graphische Benutzeroberflächen (Teil 2) (Kaspar Riesen)....Pages 263-299
Schnittstellen und Vererbung (Kaspar Riesen)....Pages 301-330
Laufzeitfehler – Die Klasse Exception (Kaspar Riesen)....Pages 331-350
Und Jetzt? (Kaspar Riesen)....Pages 351-352
Back Matter ....Pages 353-385

Citation preview

Kaspar Riesen

Java in 14 Wochen Ein Lehrbuch für Studierende der Wirtschaftsinformatik

Java in 14 Wochen

Kaspar Riesen

Java in 14 Wochen Ein Lehrbuch für Studierende der Wirtschaftsinformatik

Kaspar Riesen Institut für Wirtschaftsinformatik FHNW, Hochschule für Wirtschaft Olten, Schweiz

ISBN 978-3-658-30312-9 ISBN 978-3-658-30313-6  (eBook) https://doi.org/10.1007/978-3-658-30313-6 Die Deutsche Nationalbibliothek verzeichnet diese Publikation in der Deutschen Nationalbibliografie; detaillierte bibliografische Daten sind im Internet über http://dnb.d-nb.de abrufbar. © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2021 Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung, die nicht ausdrücklich vom Urheberrechtsgesetz zugelassen ist, bedarf der vorherigen Zustimmung des Verlags. Das gilt insbesondere für Vervielfältigungen, Bearbeitungen, Übersetzungen, Mikroverfilmungen und die Einspeicherung und Verarbeitung in elektronischen Systemen. Die Wiedergabe von allgemein beschreibenden Bezeichnungen, Marken, Unternehmensnamen etc. in diesem Werk bedeutet nicht, dass diese frei durch jedermann benutzt werden dürfen. Die Berechtigung zur Benutzung unterliegt, auch ohne gesonderten Hinweis hierzu, den Regeln des Markenrechts. Die Rechte des jeweiligen Zeicheninhabers sind zu beachten. Der Verlag, die Autoren und die Herausgeber gehen davon aus, dass die Angaben und Informationen in diesem Werk zum Zeitpunkt der Veröffentlichung vollständig und korrekt sind. Weder der Verlag, noch die Autoren oder die Herausgeber übernehmen, ausdrücklich oder implizit, Gewähr für den Inhalt des Werkes, etwaige Fehler oder Äußerungen. Der Verlag bleibt im Hinblick auf geografische Zuordnungen und Gebietsbezeichnungen in veröffentlichten Karten und Institutionsadressen neutral. Planer: Sybille Thelen Springer Vieweg ist ein Imprint der eingetragenen Gesellschaft Springer Fachmedien Wiesbaden GmbH und ist ein Teil von Springer Nature. Die Anschrift der Gesellschaft ist: Abraham-Lincoln-Str. 46, 65189 Wiesbaden, Germany

Für Emilie und Maxime.

Vorwort

Das vorliegende Buch ist eine Zusammenstellung aus Materialien und Übungen, welche sich über die letzten Jahre in meinem Unterricht zum Modul Programmierung an der Fachhochschule Nordwestschweiz angesammelt haben (hauptsӓchlich auf Basis des Java Lehrbuches von John Lewis und William Loftus [2]). Das entstandene Buch ist insbesondere für Anfӓngerinnen und Anfänger der Programmierung geeignet. Das heisst, der Einstieg in dieses Buch sollte auch ohne jegliches Vorwissen bezüglich Programmierung möglich sein. Ich habe beim Verfassen versucht, den Schwierigkeiten und Problemen, mit denen Studierende ohne – oder mit nur wenig – Programmiererfahrungen konfrontiert sind, mit soliden und möglichst einfachen Erklӓrungen zu begegnen. Ich hoffe, dass mir dies gut gelungen ist. Im vorliegenden Buch wird die Theorie mit zahlreichen, illustrierenden Beispielen unterstützt. Es lohnt sich, diese Beispiele nicht nur zu studieren, sondern diese jeweils selber zu programmieren. Viele dieser Beispiele sind verlinkt mit Lernvideos, die zeigen, wie ich das jeweilige Programm erstelle. Zudem finden Sie am Ende jedes Kapitels mehrere Theorieaufgaben und Java Übungen. Wenn Sie vorhaben, das Programmieren tatsӓchlich zu erlernen, dann müssen Sie alle Aufgaben und alle Java Übungen sorgfӓltig und falls nötig mehrfach durcharbeiten. Programmieren lernen hat eine gewisse Ähnlichkeit mit dem Erlernen eines Musikinstrumentes. Wenn Sie mehrere Bücher zum Thema Klavier oder Saxophon gelesen haben und/oder wenn Sie stundenlang Menschen beobachtet haben, die ein Instrument spielen, können Sie dann dieses Instrument spielen? Wahrscheinlich nicht. Genauso verhӓlt es sich beim Programmieren – dieses Buch gründlich zu studieren und/ oder einem Kollegen oder einer Kollegin über die Schultern zu schauen, wӓhrend er oder sie programmiert, wird Ihnen bestenfalls ermöglichen, kleinere Aufgaben oder Übungen zu meistern. Aber richtig zu programmieren kann man nur lernen, indem man selber viel und vor allem regelmӓssig programmiert. Zu den Theorieaufgaben finden Sie Musterlösungen am Ende des Buches (wenn Sie dieses Buch im Kontext einer Unterrichtsreihe verwenden, wird Ihnen Ihre Lehrperson vielleicht auch noch alternative Lösungen im Unterricht präsentieren). Zu allen Java Übungen finden Sie zudem Lernvideos, die zeigen, wie ich die jeweilige Problemstellung VII

VIII

Vorwort

mit einem Java Programm leiste. Diese Videos zeigen jeweils nur eine mögliche Lösung – Programmieren ist auch ein kreativer Prozess und typischerweise gibt es viele verschiedene Lösungen für das gleiche Problem. Gerade für Programmieranfӓnger/innen können diese Lernvideos aber trotzdem hilfreich sein – wichtig ist in jedem Fall, dass Sie die Übungen jeweils so lange wiederholen, bis Sie das Beispiel so gut verstanden haben, dass Sie die Übung selbständig und ohne Hilfestellung programmieren können. Ich kann Ihnen aufgrund meiner mehrjӓhrigen Erfahrung mit den vorliegenden Inhalten versprechen, dass Ihnen dieses Buch den ersten Schritt in die spannende und kreative Welt der Programmierung ermöglichen kann. Nun wünsche ich Ihnen viel Spass beim Erlernen von Java und hoffe, dass Sie über genügend Neugierde und Frustrationstoleranz verfügen – die zwei wohl wichtigsten Eigenschaften, die Studierende beim Erlernen einer Programmiersprache mitbringen sollten. Bern (Schweiz) 2020

Kaspar Riesen

Danksagung

Ich bedanke mich bei der Hasler Stiftung Schweiz, welche mir die Arbeit an diesem Buch finanziert hat. Zudem bedanke ich mich bei der Fachhochschule Nordwestschweiz und insbesondere beim Institut für Wirtschaftsinformatik der Hochschule für Wirtschaft für die ausgezeichnete und inspirierende Arbeitsumgebung. Mein Dank hierfür gilt insbesondere Thomas Hanne und Rolf Dornberger. Ausserdem gilt mein Dank dem Verlag Springer Vieweg und insbesondere Sybille Thelen für ihre hilfreiche und stets freundliche Unterstützung bei diesem Projekt.

IX

Inhaltsverzeichnis

1 Einführung und Motivation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.1 Weshalb Java? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 1.2 Ein Erstes Java Programm. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 1.2.1 Die Methode main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.2.2 Kommentare. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 1.2.3 Bezeichner und Reservierte Wörter. . . . . . . . . . . . . . . . . . . . . . . . . 6 1.3 Programmentwicklung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.3.1 Kompilieren und Interpretieren. . . . . . . . . . . . . . . . . . . . . . . . . . . . 8 1.3.2 Entwicklungsumgebungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9 1.3.3 Programmierfehler. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11 1.4 Objektorientiertes Programmieren. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.4.1 Variablen und Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.4.2 Objekt vs. Klasse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.4.3 Kapselung und Vererbung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21 2 Daten und Ausdrücke. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.1 Zeichenketten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.1.1 Die Methoden print und println. . . . . . . . . . . . . . . . . . . . . . . 23 2.1.2 Konkatenation von Zeichenketten. . . . . . . . . . . . . . . . . . . . . . . . . . 25 2.1.3 Escape Sequenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27 2.2 Variablen und Konstanten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29 2.3 Primitive Datentypen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.3.1 Datenkonvertierung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.4 Arithmetische Ausdrücke. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36 2.5 Boolesche Ausdrücke und die if-Anweisung. . . . . . . . . . . . . . . . . . . . . . . 40 2.5.1 Boolesche Ausdrücke. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 2.5.2 Die if-else Anweisung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 2.5.3 Blockanweisungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44 2.5.4 Verschachtelte if-Anweisungen. . . . . . . . . . . . . . . . . . . . . . . . . . . 46 XI

XII

Inhaltsverzeichnis

2.6 Interaktive Programme. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52 3 Klassen des Java API Verwenden (Teil 1). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.1 Objekte aus Klassen Instanziieren. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53 3.1.1 Methoden von Instanziierten Objekten Verwenden. . . . . . . . . . . . . 56 3.2 Das Java API . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57 3.3 Die Klasse String. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 3.4 Die Klasse Scanner . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62 3.5 Die Klasse Random. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66 3.6 Kopieren von Objekten – Aliase . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74 4 Eigene Klassen Programmieren (Teil 1). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 4.1 Einführung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75 4.2 Aufbau einer Java Klasse. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76 4.3 Instanzvariablen und Sichtbarkeitsmodifikatoren. . . . . . . . . . . . . . . . . . . . 81 4.4 Methoden Aufrufen und dem Kontrollfluss Folgen. . . . . . . . . . . . . . . . . . . 84 4.5 Der Methodenkopf. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 4.6 Konstruktoren. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87 4.7 Parameter für Methoden. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88 4.8 Die return Anweisung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90 4.9 Getter und Setter . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 4.10 UML Klassendiagramme. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94 4.11 Ein Weiteres Beispiel einer Eigenen Klasse . . . . . . . . . . . . . . . . . . . . . . . . 96 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102 5 Graphische Benutzeroberflächen (Teil 1). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 5.1 Einführung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 5.2 Kontroll- und Behälterelemente im Szenengraphen . . . . . . . . . . . . . . . . . . 107 5.3 Kontrollelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108 5.4 Behälterelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 114 5.4.1 Die Klassen VBox, HBox, FlowPane und TilePane. . . . . . . . 116 5.4.2 Die Klasse GridPane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120 5.4.3 Die Klasse BorderPane. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124 5.4.4 Andere Behälterelemente. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126 6 Klassen des Java API Verwenden (Teil 2). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 6.1 Die Klasse Math. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 137 6.2 Die Klasse DecimalFormat. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 140 6.3 Wrapper Klassen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 143 6.4 Die while-Anweisung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 146 6.4.1 Endlosschleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 150 6.4.2 Verschachtelte Schleifen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 151

Inhaltsverzeichnis

XIII

6.5 Die Klasse ArrayList. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 154 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 161 7 Bedingungen und Schleifen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 163 7.1 Die switch-Anweisung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164 7.2 Der Conditional Operator. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 167 7.3 Die do-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 168 7.4 Die for-Anweisung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 169 7.4.1 Die for-each Schleife. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173 7.5 Vergleich der Schleifenkonzepte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 174 7.6 Daten Vergleichen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175 7.6.1 Vergleich von Gleitkommazahlen . . . . . . . . . . . . . . . . . . . . . . . . . . 175 7.6.2 Vergleich von Zeichen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 7.6.3 Vergleich von Zeichenketten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176 7.6.4 Vergleich von Objekten. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 185 8 Arrays. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 187 8.1 Arrays Deklarieren und Verwenden. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 188 8.1.1 Grenzen Finden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 191 8.2 Arrays für Objekte. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 193 8.3 Programmparameter. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 199 8.4 Variable Länge von Parameterlisten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 201 8.5 Zweidimensionale Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 205 8.6 Vergleich von Arrays und ArrayList . . . . . . . . . . . . . . . . . . . . . . . . . . . 208 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212 9 Eigene Klassen Programmieren (Teil 2). . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213 9.1 Klassen und Objekte Identifizieren. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 213 9.2 Enum-Typen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214 9.3 Klassen mit Statischen Variablen/Methoden. . . . . . . . . . . . . . . . . . . . . . . . 220 9.3.1 Statische Variablen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221 9.3.2 Statische Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 223 9.4 Beziehungen zwischen Klassen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228 9.4.1 Abhängigkeit. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 228 9.4.2 Aggregation. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 232 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237 10 Methoden Planen und Programmieren. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 10.1 Methoden mit Pseudocode Planen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239 10.2 Parameter an Methoden Übergeben. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243 10.3 Methoden Überladen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247 10.4 Ein Zusammenfassendes Beispiel. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249

XIV

Inhaltsverzeichnis

10.4.1 Organisieren der Klassen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249 10.4.2 Erweitern und Zusammenführen der Klassen. . . . . . . . . . . . . . . . . 251 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 262 11 Graphische Benutzeroberflächen (Teil 2). . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263 11.1 Auf Ereignisse Reagieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 263 11.2 Mehrere Ereignisse Behandeln . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 269 11.3 Ereignisse der Maus und der Tastatur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 11.4 Ein Erstes Beispiel mit einem Model, einer View und einem Controller. . . 275 11.5 Die Spielerkarten App. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 283 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 299 12 Schnittstellen und Vererbung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 12.1 Schnittstellen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 301 12.1.1 Klassen Tauschen – Schnittstellen Beibehalten. . . . . . . . . . . . . . . . 306 12.1.2 Die Schnittstelle Comparable . . . . . . . . . . . . . . . . . . . . . . . . . . . 309 12.2 Vererbung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 311 12.2.1 Der Konstruktor einer Subklasse. . . . . . . . . . . . . . . . . . . . . . . . . . . 316 12.2.2 Überschreiben von Geerbten Methoden . . . . . . . . . . . . . . . . . . . . . 318 12.2.3 Klassenhierarchien. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 319 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 330 13 Laufzeitfehler – Die Klasse Exception. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 331 13.1 Nicht Behandeln einer Exception. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 332 13.2 Die try-catch Anweisung. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333 13.3 Weitergeben einer Exception. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338 13.4 Eine Eigene Exception Programmieren und Werfen . . . . . . . . . . . . . . . 343 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350 14 Und Jetzt? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 351 Literatur. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352 Musterlösungen. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353 Stichwortverzeichnis. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381

1

Einführung und Motivation

1.1

Weshalb Java?

Programmieren hat viel mit Lösen von Problemen zu tun. Das allgemeine Ziel der Programmierung ist es, zu gegebenen Problemen ein oder mehrere Programme zu entwickeln, die auf Computern ausführbar sind und dabei die Probleme korrekt, vollständig und möglichst effizient lösen. Die Komplexität der Probleme, die mit Programmen gelöst werden sollen, kann dabei stark variieren: Von sehr einfachen Problemen wie z. B. der Addition zweier Zahlen, bis hin zu sehr komplexen Problemen, wie z. B. der Steuerung von Passagierflugzeugen. Möchte man ein Programm schreiben, das ein Computer ausführen kann, so muss dies in einer Sprache geschehen, welche eine Maschine verstehen kann – wir benötigen also eine Programmiersprache. Programmiersprachen geben bestimmte Wörter, Symbole und Regeln vor, die genau bestimmen, wie eine Programmiererin1 gültige Programmieranweisungen (engl. Programming Statements) definieren kann. Diese Anweisungen werden dann während der Ausführung des Programmes durch den Computer in einer von der Programmiererin festgelegten Reihenfolge abgearbeitet. Es existieren Dutzende von Programmiersprachen, die z. T. für unterschiedlichste Zwecke definiert worden sind. Diese unterschiedlichen Sprachen besitzen jeweils Vor- und Nachteile,

Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, .das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_1. 1 Um den Lesefluss nicht zu beeinträchtigen, nutzen wir in diesem Buch meist nur die weibliche Form zur Bezeichnung eines Menschen, der Computerprogramme schreibt: die Programmiererin. Die männliche Form – also der Programmierer – ist aber immer mitgemeint. Umgekehrt nutzen wir meist nur die männliche Form zur Bezeichnung eines Menschen, der mit einem Computerprogramm interagiert: der Benutzer – und auch hier gilt: Die weibliche Form ist immer mitgemeint.

© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_1

1

2

1 Einführung und Motivation

die von vielen Faktoren abhängig sind. In diesem Buch werden wir mit Hilfe der Programmiersprache Java das Programmieren erlernen. Obschon es sehr schwierig bzw. unmöglich ist zu sagen, welche Sprache die beste Programmiersprache ist, darf man behaupten, dass Java einige gewichtige Vorzüge aufweist: • Java ist extrem weit verbreitet und gilt als eine der am schnellsten wachsenden Programmiertechnologien aller Zeiten (90 % der Fortune 500 Firmen setzen Java ein, es gibt ca. 10 Mio. Java Programmierer und Programmiererinnen und Java läuft auf mehr als 3 Mrd. Geräten weltweit). Angesichts dieser weiten Verbreitung haben es andere Technologien schwer, Java zu verdrängen (gemäss dem Tiobe-Index ist Java aktuell die populärste Programmiersprache überhaupt [1]). • Java kann vielfältig eingesetzt werden. Java kann z. B. verwendet werden, um Anwendungen für das Android-Betriebssystem von Google zu entwickeln (wenn man Programme für mobile Geräte entwickeln möchte, ist Java also eine gute Wahl). Mit Java kann man aber auch Desktop-Anwendungen wie z. B. Mediaplayer oder Antivirenprogramme oder Web- und Unternehmensanwendungen (wie z. B. Bankensoftware) entwickeln. • Viele moderne Programmiersprachen sind objektorientiert und Java ist auch eine objektorientierte Programmiersprache. Zwar können sich verschiedene objektorientierte Sprachen stark unterscheiden, trotzdem sollte es einem nach dem Erlernen einer objektorientierten Sprache relativ leicht fallen, zu einer anderen objektorientierten Sprache zu wechseln. • Seit der „Erfindung“ von Java wurden zahlreiche Änderungen und Erweiterungen an Java vorgenommen (einige Teile der frühen Java Technologie sind heute überholt (engl. deprecated) – diese Konzepte sollten nicht mehr verwendet werden). Diese Entwicklung ist nicht zu Ende. Das heisst, Java ist eine lebendige Technologie, welche ständig weiterentwickelt wird. • Die Sprache Java wird von einer umfassenden und wachsenden Bibliothek (engl. Library) aus zusätzlicher Software begleitet, die wir zur Programmentwicklung verwenden können. Diese bereits erstellten Programme (geschrieben in Java) werden Java API oder Standard Class Library genannt (API steht für Application Programmer Interface). Unter anderem stellt uns das Java API Möglichkeiten zur Verfügung, um graphischen Oberflächen zu erzeugen oder um mit Datenbanken zu interagieren.

1.2

Ein Erstes Java Programm

In Abb. 1.1 ist der Quellcode (engl. Sourcecode) eines ersten, sehr einfachen Java Programmes abgebildet. Wird dieses Programm ausgeführt, werden die folgenden zwei Zeilen auf dem Bildschirm2 eines Computers ausgegeben:

2 Etwas genauer: Die Ausgabe der zwei Zeilen erfolgt auf der sogenannten Konsole des Computers.

1.2

Ein Erstes Java Programm

3

Abb. 1.1 Ein sehr einfaches Java Programm. → Video K-1.1

Steve Jobs: Es ist besser, ein Pirat zu sein, als der Marine beizutreten.

Dieses Programm ist sehr einfach und noch nicht sehr nützlich. Dennoch illustriert dieses Beispiel die Grundstruktur aller Java Programme schon recht deutlich: • Alle Java Programme werden mit Klassen erstellt. Der Klassenname ist im Klassenkopf ersichtlich (nach den zwei Worten public class). Diese Klasse heisst Quote. Wir können Klassen fast beliebige Namen – sogenannte Bezeichner (engl. Identifier) – geben. Diese Klasse könnte also z. B. auch Steve oder Zitat oder X heissen (es gibt allerdings ein paar Regeln für Bezeichner, die wir beachten müssen oder beachten sollten – Details folgen weiter unten). • Wir werden in diesem Buch jede Klasse in einer separaten Datei speichern – der Dateiname und der Klassenname müssen dabei zwingend übereinstimmen und die Dateiendung muss .java sein. Die Datei, welche die Klasse Quote enthält, heisst also in diesem Fall Quote.java. • Der Rumpf der Klasse Quote beginnt mit der öffnenden, geschweiften Klammer { nach dem Klassennamen auf Zeile 5 und endet mit der letzten, schliessenden, geschweiften Klammer } auf Zeile 15. • Im Klassenrumpf können beliebig viele Methoden definiert werden. Eine Methode ist eine Gruppierung von Programmieranweisungen, die unter einem bestimmten Bezeichner zusammengefasst werden (dieser Bezeichner steht im Methodenkopf ). In diesem Beispiel besitzt die Klasse eine einzige Methode, welche main heisst und zwei Programmieranweisungen zusammenfasst.

4

1 Einführung und Motivation

• Wie bei Klassen, werden auch die Methoden mit geschweiften Klammern geöffnet und wieder geschlossen. Innerhalb dieser geschweiften Klammern befindet sich der Methodenrumpf. • Der Rest des Quellcodes sind Kommentare (in grauer Farbe gezeigt). Kommentare beeinflussen das Programm nicht – diese sollen lediglich das Lesen und Verstehen des Quellcodes für Menschen erleichtern.

1.2.1

Die Methode main

Alle Java Programme besitzen eine Methode mit dem Bezeichner main. In dieser Hauptmethode startet das Programm. Wird ein Programm ausgeführt, so wird – im Prinzip – jede Programmieranweisungen in der main Methode von oben nach unten ausgeführt. Wird das Ende des Rumpfes der main Methode erreicht, endet das Programm (man sagt, das Programm terminiert). Eine main Methode in Java Programmen startet immer mit den Worten public static

void

Wir werden später auf diese Schlüsselwörter zurückkommen (der Nutzen von String, den eckigen Klammern [] und args wird ebenfalls später im Buch erläutert). Die zwei Zeilen Quellcode in der main Methode rufen eine andere Methode auf, die println (ausgesprochen print line) heisst. Wir rufen eine Methode auf, wenn wir wollen, dass diese ausgeführt wird. Die println Methode ist so programmiert, dass diese die in Klammern mitgegebenen Zeichen auf dem Bildschirm ausgibt (die Zeichen befinden sich hierbei innerhalb doppelter Anführungszeichen). Beachten Sie, dass in Java jede Programmieranweisung mit einem Semikolon ; abgeschlossen werden muss. Wenn das obige Programm ausgeführt wird, dann geschieht also folgendes: Die println Methode wird zweimal nacheinander mit unterschiedlichen Zeichen aufgerufen. Danach wird das Ende der main Methode erreicht und das Programm terminiert. Der Quellcode, der ausgeführt wird, wenn die println Methode aufgerufen wird, ist in unserem Programm nicht ersichtlich. Die println Methode ist Teil des Java APIs. Jemand anderes hat also die Funktionalität für Ausgaben von Zeichen auf einem Bildschirm in Java programmiert und die nötigen Anweisungen unter dem Bezeichner println zusammengefasst. Statt selber die Funktionalität in Java zu programmieren, rufen wir also einfach bestehenden Quellcode auf, der die Aufgabe erledigt. Beispiel 1.1 In Abb. 1.2 sehen Sie die Klasse Quotes, welche neben der main Methode noch zwei weitere Methoden beinhaltet: Die Methoden printQuoteOfBill und printQuoteOfSteve. Wird dieses Programm ausgeführt, so wird zuerst die Methode printQuoteOfSteve aufgerufen und diese wiederum ruft zweimal die println Methode auf (zur Erzeugung der Ausgabe des Zitates von Steve Jobs).

1.2

Ein Erstes Java Programm

5

Abb. 1.2 Ein Java Programm zur Ausgabe verschiedener Zitate. → Video K-1.2

Danach wird die Methode printQuoteOfBill aufgerufen, welche sich um die Ausgabe des Zitates von Bill Gates kümmert. Danach wird nochmals die Methode printQuoteOfSteve aufgerufen und schliesslich wird die letzte Zeile der main Methode erreicht und das Programm terminiert. Methoden können also in beliebiger Reihenfolge und beliebig oft aufgerufen werden.

1.2.2

Kommentare

Typischerweise enthalten Klassen an verschiedenen Stellen Kommentare, welche den Sinn und Zweck des Quellcodes in natürlicher Sprache erläutern. Kommentare sind für eine Programmiererin die einzige Möglichkeit, ihre Gedanken mitzuteilen. Kommentare sollen dabei helfen, die Absichten und Überlegungen der Programmiererin zu verstehen.

6

1 Einführung und Motivation

Gute Kommentare sind tatsächlich essentiell für die Qualität eines Programmes: Der Quellcode eines Programmes muss oftmals modifiziert oder erweitert werden. Kommentare helfen dabei, den Quellcode (auch Jahre nach dessen Entwicklung) schneller und besser zu verstehen (insbesondere – aber nicht nur – wenn der Quellcode von anderen Programmiererinnen geschrieben wurde). Wenn Sie nochmals den Quellcode in Abb. 1.1 betrachten, sollte Ihnen auffallen, dass wir an drei verschiedenen Stellen Kommentare angebracht haben: Ausserhalb des Klassenrumpfes (Zeilen 1 bis 4), innerhalb des Klassenrumpfes (Zeilen 7 bis 9) und innerhalb einer Methode (Zeile 11). Die drei gezeigten Kommentare entsprechen den drei möglichen Formen, in denen in Java Kommentare definiert werden können: • // Dieser Kommentar geht bis ans Ende der Zeile • /* Dieser Kommentar kann sich über mehrere Zeilen erstrecken und endet erst mit dem folgenden Endsymbol */ • /** * Das ist sogenannter javadoc Kommentar, welcher die * automatische Generierung von Dokumentationen erlaubt */

Kommentare sollten prägnant und in ganzen Sätzen formuliert werden, sollten nicht das offensichtliche kommentieren und müssen eindeutig sein.

1.2.3

Bezeichner und Reservierte Wörter

Die Wörter, die gebraucht werden, um ein Programm zu schreiben, heissen Bezeichner (engl Identifier). Die elf Bezeichner im Programm in Abb. 1.1 sind public, class, Quote, static, void, main, String, args, System, out, println (die Zeichen innerhalb doppelter Anführungszeichen, z. B. "Steve Jobs", sind keine Bezeichner sondern Zeichenketten – Thema des nächsten Kapitels). Alle Bezeichner von Java gehören zu einer der folgenden drei Kategorien: 1. Bezeichner, die für bestimmte Zwecke reserviert sind. In unserem Beispiel sind dies public, class, static und void. Alle reservierten Wörter erfüllen einen bestimmten Zweck bei der Programmierung und dürfen nicht für andere Zwecke verwendet werden (z. B. dürfen Sie kein reserviertes Wort als Klassen- oder Methodename wählen). Aktuell sind ca. 50 Wörter in Java reserviert. 2. Bezeichner, die sich die Programmiererin dieses Programms ausgedacht hat. In unserem Beispiel sind dies Quote und args (in Abb. 1.2 kommen noch die Bezeichner

1.2

Ein Erstes Java Programm

7

printQuoteOfBill und printQuoteOfSteve hinzu). Während die Programmiererin dieses Programm geschrieben hat, hat sie sich z. B. für Quote als Klassennamen entschieden. Der Bezeichner args (Kurzform für Arguments) wird zwar sehr oft so in der main Methode verwendet – die Programmiererin hätte aber auch hierfür einen beliebig anderen Bezeichner wählen können. 3. Bezeichner, die sich ein anderer Programmierer oder eine andere Programmiererin ausgedacht hat (String, System, out, println und main in unserem Beispiel). Diese Bezeichner sind nicht Teil der Programmiersprache Java – diese sind Teil des Java APIs mit Quellcode, den jemand in Java geschrieben hat. Die Autoren dieses Programmcodes haben die Bezeichner gewählt und wir benutzen diese nun. Ein Bezeichner, den wir selber wählen, kann eine beliebige Kombination aus Buchstaben, Ziffern, dem Unterstrich- und dem Dollarzeichen sein. Bezeichner dürfen dabei beliebig lang sein aber nicht mit einer Ziffer beginnen. Java unterscheidet zwischen Gross- und Kleinschreibung (engl. case sensitive). Das heisst, Minimum, minimum und MINIMUM sind in Java verschiedene Bezeichner. Es gibt zahlreiche Konventionen für die Gross- und Kleinschreibung von Bezeichnern (diese Konventionen sind aber nicht durch die Programmiersprache Java vorgeschrieben – Konventionen sind Abmachungen zwischen Programmiererinnen und Programmierern, die das Lesen von Quellcode vereinfachen sollen). Wir halten uns in diesem Buch an folgende Konventionen: • Bezeichner für Klassen sind typischerweise Substantive und beginnen mit einem Grossbuchstaben gefolgt von Kleinbuchstaben, wobei der erste Buchstabe jedes internen Wortes gross geschrieben wird (bspw. Student oder StudentActivity). • Bezeichner für Methoden sind typischerweise Verben, beginnen mit einem Kleinbuchstaben und interne Wörter werden gross geschrieben (bspw. main, sum oder findMin). • Bezeichner für Variablen sollten kurz (z. B. product statt theCurrentProductToBeSent), aussagekräftig (z. B. grade statt x) und eindeutig sein (steht der Bezeichner std bspw. für standard oder stockDistribution?). • Bezeichner für Konstanten bestehen ausschliesslich aus Grossbuchstaben und einzelne Wörter sind durch einen Unterstrich getrennt (bspw. MINIMUM oder MAX_CAPACITY). Beachten Sie, dass es beim Programmieren weitere Konventionen gibt. Z.B. die Konvention, dass eine Zeile Quellcode nicht mehr als 80 Zeichen umfassen sollte (diese Grenze von 80 Zeichen ist in den Programmen in den Abb. 1.1 und 1.2 mit einer grauen Linie eingezeichnet und wir sehen, dass in beiden Beispielen die Konvention nicht eingehalten wird – wir kommen später auf dieses „Problem“ zurück).

8

1 Einführung und Motivation

1.3

Programmentwicklung

1.3.1

Kompilieren und Interpretieren

Maschinensprachen sind Programmiersprachen, in denen die Instruktionen definiert sind, die vom Prozessor eines Computers direkt ausgeführt werden können. Jeder Prozessortyp besitzt seine eigene Maschinensprache. Einzelne Anweisungen einer Maschinensprache können nur sehr einfache Aktionen ausführen (z. B. Kopieren eines Wertes in einen Speicherregister oder Vergleichen eines bestimmten Wertes mit anderen Werten). Jeder Befehl der Maschinensprache ist durch einen oder mehrere binäre Zahlenwerte codiert. Eine sinnvolle Folge von solchen binären Zahlencodes bildet der Quellcode, der von einem Computer ausgeführt werden kann. Das Programmieren in Maschinensprache ist für Menschen zwar möglich, aber eher schwierig, fehleranfällig und vor allem langwierig. Um ein Computerprogramm zu programmieren, verwendet man typischerweise eine bestimmte Hochsprache (z. B. Java, Ruby oder Python). Das Erstellen und Verstehen von Quellcode, der in einer Hochsprache programmiert ist, ist relativ einfach (auf jeden Fall einfacher als eine sinnvolle Folge von Binärzahlen zu schreiben). Damit aber ein Programm auf einem Computer ausgeführt werden kann, muss dieses zwingend in der entsprechenden Maschinensprache ausgedrückt sein. Das bedeutet, dass Quellcode, welcher in einer anderen Sprache als Maschinensprache verfasst wurde, vor seiner Ausführung zuerst in Maschinensprache übersetzt werden muss. Kompilierer (engl. Compiler) sind Computerprogramme, die einen Quellcode, geschrieben in einer bestimmten Sprache A, in eine Zielsprache B übersetzen können (es braucht unterschiedliche Kompilierer für unterschiedliche Quell- und Zielsprachen). Oftmals ist die Zielsprache die Maschinensprache eines bestimmten Computers. Beachten Sie, dass eine einzige Anweisung in einer Hochsprache (z. B. die Anweisung println) vielen – vielleicht Hunderten – von Maschineninstruktionen entsprechen kann. Der Kompilierer erzeugt diese Maschineninstruktionen automatisch aus dem Quellcode. Java unterscheidet sich in der Übersetzung von Quellcode in Maschinensprache von anderen Sprachen. Es findet nämlich keine direkte Übersetzung des Quellcodes in Maschinensprache statt. Der Java Kompilierer übersetzt Java Quellcode in eine spezielle Repräsentation, die Bytecode genannt wird. Bytecode entspricht keiner traditionellen Maschinensprache. Mit einem anderen Programm, genannt Interpreter, wird der Bytecode schliesslich in Maschinensprache übersetzt. Der Interpreter führt den übersetzten Bytecode – also das Programm – zudem auch gleich auf der Maschine aus (der gesamte Prozess des Kompilierens und des Interpretierens ist in Abb. 1.3 visualisiert). Dieses etwas komplizierte Vorgehen hat den Vorteil, dass Java plattformunabhängig ist. Das heisst, ein kompiliertes Java Programm – also Bytecode – kann auf einer bestimmten Maschine erzeugt worden sein und auf anderen, beliebigen Maschinen ausgeführt werden. Die einzige Einschränkung ist, dass auf der ausführenden Maschine ein Java Interpreter

1.3

Programmentwicklung

Java Quellcode

9

Bytecode

COMPILER

INTERPRETER

.class

.java

Java Development Kit (JDK)

Java Runtime Environment (JRE)

Abb. 1.3 Java Quellcode wird zuerst mit einem Kompilierer in Bytecode übersetzt. Dieser Bytecode kann auf beliebigen Maschinen ausgeführt werden, solange die entsprechende Maschine einen Java Interpreter installiert hat. Der Interpreter übersetzt den Bytecode in die passende Maschinensprache und führt die erzeugten Instruktionen gleich aus

installiert sein muss, der Java Bytecode in die prozessorspezifische Maschinensprache übersetzen und ausführen kann. Tatsächlich haben die meisten Computer standardmässig einen Java Interpreter installiert – die sogenannte Java Laufzeitumgebung (engl. Java Runtime Environment (JRE)). Der Kompilierer für Java Quellcode hingegen muss typischerweise zuerst installiert werden. Dieser befindet sich im sogenannten Java Development Kit (JDK).

1.3.2

Entwicklungsumgebungen

Das Java Development Kit (JDK [3]) enthält die wichtigsten Werkzeuge, um Java Programme kompilieren und ausführen zu können. Java Programme können dabei mit jedem beliebigen Texteditor geschrieben werden, der reine Textdateien speichern kann (z. B. TextEdit, NotePad, Atom [4] oder ähnliche Programme). Das Kompilieren und Interpretieren von Java Programmen kann dann in der Eingabeaufforderung (Windows) oder dem Terminal (Mac und Linux) des Computers durchgeführt werden. Nehmen wir an, dass der Quellcode für die Klasse Quotes in der Datei Quotes.java im Ordner /Users/riesen/Projekte/ gespeichert ist. Wir öffnen das Terminal und wechseln in den Ordner, in dem sich die Datei mit dem Quellcode befindet. Mit dem Betriebssystem Mac OS geschieht dies mit dem Befehl cd: cd /Users/riesen/Projekte/ Damit sich Programme übersetzen und ausführen lassen, müssen wir nun nacheinander die Programme javac (zum Kompilieren) und java (zum Ausführen) aus dem JDK aufrufen. Mit dem Befehl javac können wir also die Klasse Quotes in der Datei Quotes.java kompilieren:

10

1 Einführung und Motivation

javac Quotes.java

Der Kompilierer legt – vorausgesetzt, der Quellcode ist fehlerfrei – nun die Datei Quotes.class an. Diese Datei enthält den Bytecode unseres Programmes. Beachten Sie, dass dieser Bytecode nun eben plattformunabhängig ist. Das heisst, das erzeugte Programm kann auf einem beliebigen Computer, der einen Interpreter für Java installiert hat, ausgeführt werden. Der Java Interpreter liest den Bytecode Anweisung für Anweisung aus, übersetzt diese in Maschinensprache und führt die erzeugten Befehle auf dem Prozessor aus. Der entsprechende Befehl, der im Terminal eingegeben werden muss, lautet: java Quotes

Als Argument erwartet der Interpreter den Namen der Klasse (ohne Endung .class). In Abb. 1.4 ist der gesamte Prozess, der im Terminal durchgeführt wird, illustriert. Eine Entwicklungsumgebung beschreibt die Menge von Werkzeugen, die man zum Erstellen, Testen, Modifizieren und Ausführen von Quellcode benötigt (es gibt sowohl frei erhältliche als auch kommerzielle Entwicklungsumgebungen). Eine Umgebung heisst Integrierte Entwicklungsumgebung (IDE für Integrated Development Environment), wenn diese mehrere Werkzeuge in einer Software vereint. IDEs bieten oftmals graphische Oberflächen und zusätzliche Fähigkeiten an, die das Programmieren stark vereinfachen können. Es existieren zahlreiche IDEs, die zur Java Programmentwicklung verwendet werden können, wie z. B. NetBeans [5] oder IntelliJ IDEA [6]. Ich empfehle Ihnen, die IDE Eclipse [7] zu verwenden. Eclipse ist weit verbreitet für die Entwicklung von Java Programmen und unterstützt auch andere Programmiersprachen wie zum Beispiel C, C++, PHP, oder Python. Eclipse ist zudem plattformunabhängig und läuft unter Windows, Linux und Mac OS.

Abb. 1.4 Java Programme in einem Terminal kompilieren und ausführen. → Video K-1.3

1.3

Programmentwicklung

1.3.3

11

Programmierfehler

Jede Programmiersprache besitzt ihre eigene Syntax. Die Syntax einer Programmiersprache gibt vor, wie man gültige Programmieranweisungen bilden kann. Wir haben bereits einige Elemente der Java Syntax diskutiert (z. B. die Regeln, dass ein Bezeichner nicht mit einer Ziffer beginnen darf, dass Programmieranweisungen mit einem Semikolon abgeschlossen werden müssen, oder dass geschweifte Klammern den Klassen- und Methodenrumpf einschliessen müssen). Die Semantik einer Programmieranweisung definiert, was passieren soll, wenn die Anweisung ausgeführt wird. Die Semantik beschreibt also die Bedeutung von Quellcode. Ein syntaktisch korrektes Programm muss nicht zwingend semantisch korrekt sein – ein Programm wird immer das tun, was wir tatsächlich programmiert haben und nicht das, was wir gemeint haben zu programmieren. Die Syntax definiert, wie ein Programm geschrieben werden darf, die Semantik hingegen definiert, was ein geschriebenes Programm tun wird. Dementsprechend kann man zwei Fehlerarten unterscheiden: • Kompilierfehler – Fehler in der Syntax • Logische Fehler – Fehler in der Semantik Wird ein Java Programm kompiliert, werden alle Regeln der Syntax vom Kompilierer kontrolliert. Jeder Fehler, der vom Kompilierer entdeckt wird, heisst Kompilier- oder auch Syntaxfehler. Falls ein Java Programm syntaktisch nicht korrekt ist (ein einziger Syntaxfehler reicht hierzu schon aus), erzeugt der Kompilierer eine Fehlermeldung und es wird kein Bytecode erzeugt. Das heisst, ein Programm mit fehlerhafter Syntax kann nicht ausgeführt werden. Umgekehrt erzeugt der Kompilierer bei erfolgreicher Kompilierung den Bytecode und speichert diesen automatisch in einer Datei mit der Endung .class ab. Das entsprechende Programm kann nun ausgeführt werden. Die zweite Fehlerart umfasst logische oder semantische Fehler. In diesem Fall kompiliert der Quellcode und das Programm kann ausgeführt werden, aber es produziert fehlerhafte Ausgaben oder das Programm terminiert „nicht ordnungsgemäss“. Z. B. berechnet das Programm den Mittelwert falsch oder nach einer bestimmten Eingabe des Benutzers stürzt das Programm einfach ab. Eine Programmiererin sollte ihre Programme immer ausführlich testen und die erwarteten Ergebnisse mit den tatsächlichen Ergebnissen vergleichen. Ausserdem liegt es an der Programmiererin, möglichst robuste Programme zu schreiben, welche verhindern, dass das Programm bei einer fehlerhaften oder unerwarteten Manipulation des Benutzers gleich abstürzt. Wenn logische Fehler beobachtet werden, so muss der Ursprung des Problems im Quellcode gefunden werden – dieser Prozess ist unter dem Namen Debugging bekannt und kann manchmal eine langwierige Aufgabe sein.

12

1.4

1 Einführung und Motivation

Objektorientiertes Programmieren

Es existieren unterschiedliche Programmierparadigmen wie zum Beispielfunktionale Programmierung oder prozedurale Programmierung. Java folgt dem Paradigma der Objektorientierung. In der objektorientierten Programmierung entwirft man in der Regel separate, mehr oder weniger unabhängige Programmteile, die jeweils für gewisse Aufgaben verantwortlich sind. Die Gesamtheit dieser Programmteile, die dann zur Laufzeit miteinander interagieren können, entspricht der entwickelten Software. In Java sind diese Programmteile sogenannte Objekte, die in Klassen kategorisiert und definiert werden. Objekte sind die fundamentalen Elemente in jedem Java Programm. Mit Objekten können Entitäten aus der echten Welt leicht und intuitiv mit einer Programmiersprache modelliert werden. Entitäten sind hierbei unterscheidbare, physisch oder gedanklich existierende Konzepte der zu modellierenden Welt (der sogenannten Problemdomäne). Die Möglichkeit, die Entitäten der Problemdomäne eins-zu-eins in einem Programm abzubilden, ist eine der wichtigsten Charakteristiken der objektorientierten Programmierung und hat massgeblich zum Erfolg der Objektorientierung in der Softwareentwicklung beigetragen. Beispiel 1.2 Beispiele für Objekte sind: Menschen, Studierende, Angestellte, Fahrzeuge, Autos, Motorräder, Möbel, Schreibtische, Geräte, Handys, Tablets, Bankkonten, Sparkonten, Rechnungen, Zahlungen, Schulden, Nachrichten, Tweets, Videos, Esswaren, Teigwaren, Eiscremes, etc.

1.4.1

Variablen und Methoden

Objekte besitzen zwei charakteristische Merkmale: • Ein Objekt besitzt Eigenschaften, die entweder konstant sind oder sich im Laufe der Zeit ändern können. • Ein Objekt besitzt ein Verhalten oder Funktionalitäten, d. h. es kann selber Dinge tun und/oder andere Objekte können etwas mit diesem Objekt tun. Die Eigenschaften und die Funktionalitäten definieren das Objekt letztendlich als eigenständiges Konzept. Die Eigenschaften der Objekte programmiert man in Java mit Variablen und die verschiedenen Verhaltensmöglichkeiten von Objekten programmiert man in Java mit Methoden. Die Variablen eines Objektes sind die Eigenschaften, die ein Objekt speichern und verwalten soll. Die in einem Objekt gespeicherten Variablen können auch wieder Objekte sein oder sogenannt primitive Daten (bestehend aus Zeichen und Ziffern). Ein Objekt Bankkonto kann z. B. den aktuellen Kontostand als eine Folge von Ziffern und den Kontoinhaber als

1.4

Objektorientiertes Programmieren

13

Objekt Kunde speichern. Den Variablen eines Objektes können Werte zugewiesen werden und im Allgemeinen können sich diese Werte im Laufe der Zeit ändern. Die aktuellen Werte der Variablen eines Objektes nennt man auch den Zustand des Objektes. Die Methoden eines Objektes bestimmen das mögliche Verhalten des Objektes. Eine Methode ist eine Gruppierung von Programmieranweisungen, die unter einem bestimmten Namen zusammengefasst werden. Wenn eine Methode eines Objektes aufgerufen wird, werden die einzelnen Anweisungen der Methode ausgeführt. Um z. B. das Verhalten „Einzahlung auf ein Bankkonto“ zu programmieren, müssen wir im Objekt Bankkonto eine Methode definieren, die mehrere Anweisungen enthält, welche die Variable Kontostand des Objektes Bankkonto korrekt anpasst.

1.4.2

Objekt vs. Klasse

Objekte sind immer durch eine Klasse definiert. Eine Klasse ist ein Modell oder eine Art „Bauplan“ für Objekte eines bestimmten Typs. Denken Sie an einen Bauplan eines Hauses erstellt durch einen Architekten. Ein Bauplan definiert die wichtigen Eigenschaften und Verhaltensmöglichkeiten des Hauses (Wände, Fenster, Türen, Raumanordnung, Anzahl Stockwerke, Heizung, Wasseranschlüsse, etc.). Ist der Bauplan einmal erstellt, so können theoretisch mehrere Häuser nach diesem Bauplan gebaut werden. Einerseits werden sich die so gebauten Häuser in verschiedenen Aspekten unterscheiden (z. B. in der Farbe der Fassade, der tatsächlichen Adresse, der Möblierung, der Bewohner, etc.), andererseits sind die Häuser, da sie ja auf dem gleichen Bauplan basieren, in vielerlei Hinsicht identisch. Um ein komplett anderes Haus bauen zu können (z. B. ein Mehrfamilien- statt einem Einfamilienhaus), bräuchten wir einen anderen Bauplan [2]. Die Unterscheidung zwischen Klasse und Objekt ist sehr wichtig: Eine Klasse ist kein Objekt (ein Bauplan ist auch noch kein Haus). Eine Klasse ist „nur“ der Bauplan für Objekte. Die Klasse bestimmt die Eigenschaften, die das Objekt beschreiben sollen (also die Variablen) sowie das Verhalten, das das Objekt ausführen können soll (also die Methoden). Ist eine Klasse einmal definiert, so können mehrere Objekte aus dieser Klasse erstellt oder instanziiert werden – man sagt auch: ein Objekt ist eine Instanz einer Klasse. Haben wir z. B. eine Klasse definiert, die das allgemeine Konzept eines Bankkontos repräsentiert, so können wir beliebig viele konkrete Objekte aus der Klasse Bankkonto instanziieren. Beispiel 1.3 Betrachten Sie das Java Programm in Abb. 1.5. Im Moment sind nur die Zeilen 17 bis 32 relevant – die anderen Zeilen Quellcode dürfen Sie ignorieren. Im Java API existiert eine Klasse Circle, welche das Konzept eines graphischen Kreises in Java modelliert. Ein Kreis besitzt als Eigenschaften einen Mittelpunkt (definiert durch (x, y)-Koordinaten), einen Radius (in Pixel) und eine bestimmte Farbe. Diese Eigenschaften sind in der Klasse Circle als Variablen definiert. Zudem besitzt ein Kreis auch ein

14

1 Einführung und Motivation

Abb. 1.5 Aus der Klasse Circle werden vier Objekte circle1, circle2, circle3 und circle4 erzeugt. → Video K-1.4

1.4

Objektorientiertes Programmieren

15

Abb. 1.6 Das erzeugte Fenster zum Programm CircleDemo aus Abb. 1.5

Verhalten – es kann z. B. seine Farbe ändern, sich in x und/oder y Richtung verschieben und seinen Radius vergrössern oder verkleinern. Auf den Zeilen 19 bis 22 werden aus der Klasse Circle vier Objekte instanziiert und unter den Bezeichnern circle1, circle2, circle3 und circle4 gespeichert (mit unterschiedlichen Zentren und Radien). Die Details, wie die Objekte aus der Klasse instanziiert werden, sind jetzt noch nicht wichtig. Wichtig ist aber, dass Sie sehen, dass es eine Klasse Circle gibt, aus der wir beliebig viele Objekte instanziieren können. Jedes dieser Objekte ist unabhängig von den anderen Objekten. Auf den Zeilen 25 bis 28 rufen wir für die vier Objekte jeweils die Methode setFill auf und definieren so für jedes der vier Kreisobjekte eine eigene Farbe. Auf den Zeilen 31 und 32 wird am Objekt circle2 noch das Verhalten des Verschiebens (in x-Richtung) und das Verändern des Radius demonstriert (durch zwei Methodenaufrufe). Beachten Sie, dass jetzt nur das Kreisobjekt circle2 verschoben und vergrössert wird. Das Programmfenster, das aus diesem Quellcode erzeugt wird, ist in Abb. 1.6 zu sehen. Obschon es nur eine Klasse Circle gibt, sind in diesem Fenster die vier instanziierten und unabhängigen Objekte circle1, circle2, circle3 und circle4 vom Typ Circle zu sehen.

1.4.3

Kapselung und Vererbung

Zwei der wichtigsten Eigenschaften der Objektorientierung sind die Kapselung (engl. encapsulation) und die Vererbung (engl. inheritance). Kapselung bedeutet, dass die Eigenschaften eines Objektes (bzw. die Werte der Variablen des Objektes) gegen Aussen verborgen bleiben. Das heisst, ein Objekt verbergt seinen Zustand gegenüber anderen Objekten des Programmes und verwaltet alle Zugriffe auf die eigenen Variablen selber. Das bedeutet, dass z. B. Änderungen der Variablen eines Objektes nur über die Methoden dieses Objektes vollzogen werden können. Objekte sollten also so entworfen werden, dass andere Objekte keinen direkten Zugriff auf die Variablen des Objektes haben, sondern immer via Methoden auf die Variablen zugreifen müssen. Dies erlaubt dem Objekt die alleinige Kontrolle über dessen Zustand. Diese alleinige Kontrolle des Objektes über seinen Zustand

16

1 Einführung und Motivation

hat den Vorteil, dass nur zulässige Veränderungen an den Variablen durchgeführt werden können. Beispiel 1.4 Gegeben sei ein Objekt rectangle, das aus einer Klasse Rectangle instanziiert wurde. Die Klasse Rectangle modelliere ein graphisches Rechteck. Kapselung bedeutet, dass der folgende direkte Zugriff, auf die im Objekt gespeicherte Variable width, verhindert werden sollte: rectangle.width = -5;

Vielmehr sollten wir sicherstellen, dass Änderungen der Variablen immer über eine entsprechende Methode erfolgen. Z.B.: rectangle.setWidth(-5);

Dies hat den Vorteil, dass wir als Programmiererin in der Methode setWidth durch „sorgfältiges“ programmieren sicherstellen können, dass der Variablen width nur gültige Werte zugewiesen werden. In obigem Beispiel könnte man die Methode setWidth z. B. so programmieren, dass die Variable width nur dann geändert wird, wenn die neue Breite eine positive Zahl ist. Klassen können auf der Basis bestehender Klassen definiert werden, indem Vererbung verwendet wird. Vererbung ist eine Möglichkeit, bereits programmierte Klassen wiederzuverwenden. Vererbung ist dann sinnvoll einsetzbar, wenn verschiedene Klassen, die wir erstellen wollen, starke Ähnlichkeiten aufweisen (also viele identische Variablen und Methoden besitzen). Da die Variablen und Methoden einer Klasse bei der Vererbung an die Kinder vererbt werden, können wir somit zuerst eine Basisklasse erstellen, von der dann die zu erstellenden, ähnlichen Klassen erben können. Aus einer Klasse können also durch Vererbung verschiedene Klassen abgeleitet werden. Aus den abgeleiteten Klassen können dann wiederum weitere Klassen durch Vererbung abgeleitet werden. Somit kann eine komplette Klassenhierarchie erstellt werden. Gemeinsame (häufige) Charakteristiken werden dann typischerweise in Klassen definiert, die sich hoch oben in der Hierarchie befinden, während spezifischere Variablen und Methoden nur in abgeleiteten Klassen (tiefer unten in der Hierarchie) definiert werden. Beispiel 1.5 In Abb. 1.7 ist eine Klassenhierarchie für verschiedene Tierarten gezeigt. Eigenschaften und Verhaltensweisen, welche alle Tiere teilen, würde man zuoberst in der Klasse Tier definieren (z. B. die Variable name oder die Methode makeSound). Die Eigenschaft der Flügelspannweite würde man dann vielleicht erst in der Klasse Vogel hinzufügen oder das Mähnenvolumen sogar erst auf der untersten Stufe der Hierarchie in der Klasse Löwe.

1.4

Objektorientiertes Programmieren

17

Tier

Säugetier

Hund

Dogge

Vogel

Adler

Katze

Pinscher

Hauskatze

Angora

Wildkatze

Tiger

Löwe

Abb. 1.7 Eine einfache Klassenhierarchie für verschiedene Tierarten

Aufgaben und Übungen zu Kap. 1 Theorieaufgaben 1. Begründen Sie, weshalb die folgenden zwei Kommentare nicht optimal sind: System.out.println("Hallo"); // gibt Hallo aus System.out.println("test"); // muss später geändert werden

2. Welche Ausgaben erzeugen die folgenden zwei Anweisungen? System.out.println("Hallo"); // gibt Hallo aus // gibt Hallo aus System.out.println("Hallo");

3. Welche Ausgabe erzeugt folgendes Code Fragment? System.out.println("Ha"); System.out.println(); System.out.println("Hi"); System.out.println("Ho");

4. Welche der folgenden Bezeichner sind syntaktisch nicht korrekt: grade, quiz Grade, 2ndGrade, MAX_GRADE, grade#, grades1&2, grade_17. 5. Finden Sie passende Bezeichner für. . .

18

1 Einführung und Motivation

– – – –

. . . eine Java Klasse, die eine Prüfung repräsentieren soll. . . . die erreichten Punkte in einer Prüfung. . . . eine Methode, welche den Durchschnittswert aller Prüfungen berechnet. . . . die maximale Punktzahl, die in einer Prüfung erreicht werden kann.

6. Illustrieren und erläutern Sie mit einer Skizze, wie man mit Java plattformunabhängige Programme erstellt. 7. Weisen Sie folgende Programmierfehler den zwei Kategorien Kompilierfehler oder Logischer Fehler zu: – Ein reserviertes Wort wird falsch geschrieben (z. B. viod statt void). – Zur Berechnung des Durchschnittswertes von Zahlen in einer Liste von Zahlen wird die Summe der Zahlen durch die Grösse der Liste dividiert. Aktuell beinhaltet die Liste 0 Elemente. – Anstelle des maximalen Punktestandes wird der durchschnittliche Punktestand angegeben. – Am Ende einer Programmieranweisung fehlt das Semikolon. – Ein Wort in der Ausgabe des Programmes ist falsch geschrieben. – Statt einer geschweiften Klammer { wird eine normale Klammer ( verwendet. – In einem Programm werden zwei Werte multipliziert statt addiert, wenn auf eine Schaltfläche beschriftet mit + geklickt wird. 8. In folgendem Quellcode finden sich fünf Syntaxfehler – finden Sie diese und korrigieren Sie sie.

1.4

Objektorientiertes Programmieren

19

9. Finden Sie für die Klassen Flight und Student je zwei Variablen (Eigenschaften) und zwei Methoden (Verhalten/Funktionen), die in den Klassen modelliert werden könnten. Java Übungen 1. Installieren Sie das JDK, die IDE Eclipse und JavaFX. Bei Problemen bei den Installationen sind Suchmaschinen hilfreich (Suchen Sie z. B. nach: install eclipse on windows o.ä.). a. Laden Sie das JAVA SE Development Kit 8 (JDK) unter http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads2133151.html runter und installieren Sie dieses. Wechseln Sie in die Eingabeaufforderung (oder das Terminal) Ihres Computers und geben Sie java -version

ein. Falls Sie als Antwort auf diesen Befehl folgende Ausgabe erhalten java version "1.8.x"’

sind Sie bereit, Java Programme auf Ihrem Computer zu kompilieren (mit dem Befehl javac) und ausführen zu lassen (mit dem Befehl java)3 b. Laden Sie die aktuelle Version von Eclipse unter https://www.eclipse.org/downloads/eclipse-packages/?show_instructions=TRUE runter und installieren Sie diese. c. Optional: Installieren Sie den JavaFX Wizard indem Sie den Anweisungen unter https://www.eclipse.org/efxclipse/install.html#for-the-ambitious Schritt für Schritt folgen.

3 java -version wird unter Umständen nur dann funktionieren, wenn Sie die Systemvariable

PATH anpassen (je nach Betriebssystem, das Sie benutzen). Suchen Sie in diesem Fall im WWW nach how to set path in java o.ä.

20

1 Einführung und Motivation

2. Schauen Sie sich folgendes Video an, bevor Sie die ersten Java Übungen durchführen:

→ Video V-1.2 3. Schreiben Sie das Programm Quote aus Kap. 1 in einem Texteditor ab, speichern Sie die Datei unter Quote.java ab und kompilieren Sie diese in der Eingabeaufforderung mit dem Befehl javac Quote.java. Danach fügen Sie der Reihe nach folgende Fehler in Ihrem Programm ein (machen Sie die Fehler jeweils wieder rückgängig, wenn Sie zum nächsten Fehler übergehen). Versuchen Sie jeweils das Programm zu kompilieren und schauen Sie sich die Fehlermeldung an. Falls das Programm fehlerfrei kompiliert, führen Sie dieses mit dem Befehl java Quote aus. – Bezeichnen Sie die Klasse mit quote statt Quote (ohne dabei den Dateinamen anzupassen). – Entfernen Sie das erste Anführungszeichen in der ersten Ausgabe. – Schreiben Sie Main statt main. – Schreiben Sie foo statt println. – Löschen Sie die letzte geschweifte Klammer des Programmes. – Löschen Sie das Semikolon am Ende einer println Anweisung.

→ Video V-1.3 4. Schreiben Sie ein Programm, welches den Satz Winter is coming ausgibt (erste Version: auf einer Zeile; zweite Version: jedes Wort auf einer separaten Zeile).

→ Video V-1.4 5. Schreiben Sie ein Programm, das folgende Angaben auf je einer Zeile ausgibt: Ihren Namen, Geburtstag, Hobbies, Lieblingsbuch und Lieblingsfilm. Fügen Sie jeder Ausgabe den entsprechenden Attributnamen hinzu (z. B. Hobbies: Segeln, Kochen, Lesen).

→ Video V-1.5

1.4

Objektorientiertes Programmieren

21

Literatur 1. https://www.tiobe.com/tiobe-index/. 2. John Lewis and William Loftus. Java Software Solutions – Foundations of Program Design. Pearson Global Edition, 8th edition edition, 2015. 3. www.oracle.com/technetwork/java/javase/downloads/index.html. 4. https://atom.io. 5. https://netbeans.org. 6. https://www.jetbrains.com/idea/. 7. https://www.eclipse.org/downloads/.

2

Daten und Ausdrücke

2.1

Zeichenketten

Eine Zeichenkette ist eine Folge von beliebigen Zeichen (Buchstaben, Ziffern, Interpunktionszeichen und andere Spezialzeichen). In Java kann man Zeichenketten definieren, indem Folgen von Zeichen innerhalb doppelter Anführungszeichen gesetzt werden. Wie wir im nächsten Kapitel sehen werden, sind Zeichenketten in Java eigentlich Objekte, welche durch die Klasse String definiert sind (für die Zwecke dieses Kapitels ist dies aber unwesentlich). Beispiel 2.1 Nachfolgend ein paar Beispiele für Zeichenketten: • "Wir sind Schweizermeister 2018 and 2019!" • "Meine eMail Adresse (für Beschwerden): [email protected]" • ""

Das letzte Beispiel entspricht der leeren Zeichenkette – eine Zeichenkette, die also kein Zeichen enthält.

2.1.1

Die Methoden print und println

In der Klasse Quote im vorigen Kapitel haben wir die Methode println unter anderem wie folgt aufgerufen: System.out.println("Steve Jobs:");

Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_2.

© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_2

23

24 Abb. 2.1 Methoden werden auf Objekten aufgerufen. Nach dem Methodenname folgt immer ein Klammerpaar, in dem sich ggf. Parameter befinden können

2 Daten und Ausdrücke

Objekt

Methode

Parameter

Anhand dieser Programmieranweisung kann man schön illustrieren, wie man Methoden und Objekte in Java verwenden kann. Das Objekt System.out repräsentiert ein Ausgabegerät oder eine Ausgabedatei1 . Standardmässig entspricht das Objekt System.out der sogenannten Konsole des Computers, welche Textausgaben ermöglicht (in Eclipse ist die Konsole in der graphischen Oberfläche integriert). Objekte modellieren i. d. R. ein Verhalten, das wir nutzen können – das Verhalten der Objekte ist jeweils in den Methoden definiert. Das Verhalten, das wir hier nutzen wollen, ist in der Methode println programmiert. Um die Methode println nutzen zu können, benötigen wir den Objektnamen (System.out) gefolgt vom Namen der Methode (println). Beachten Sie den Punkt zwischen dem Bezeichner des Objektes und dem Bezeichner der Methode. Nach dem Bezeichner der Methode folgt immer ein Klammerpaar (). Das, was wir einer Methode innerhalb dieser Klammern mitgeben, heisst Parameter. Die Methode println erwartet einen einzigen Parameter, nämlich die Zeichenkette, die ausgegeben werden soll. In Abb. 2.1 ist der Zusammenhang zwischen Objekt, Methode und Parameter dargestellt. Wir können also folgendes zusammenfassen: Um eine Methode mit dem Bezeichner methodenName eines Objektes mit Bezeichner objektName aufzurufen, folgt man in Java folgender Schreibweise: objektName.methodenName();

Das heisst, Methoden werden auf Objekten aufgerufen (es gibt Ausnahmen für diese Regel, auf die wir später zu sprechen kommen werden). Wir haben dieses Prinzip im vorigen Kapitel auch im Beispiel mit der Klasse Circle angewendet. Z. B. haben wir die Methode setRadius auf dem Objekt circle2 aufgerufen: circle2.setRadius(40);

1 Präziser müsste man eigentlich sagen: Das Objekt mit dem Bezeichner out, das in der Klasse System gespeichert ist, repräsentiert ein Ausgabegerät oder eine Ausgabedatei.

2.1

Zeichenketten

25

Diese Methode verlangt einen Parameter, der innerhalb von Klammern mitgegeben wird. Es gibt Methoden, die keine Parameter benötigen – die Klammern sind trotzdem immer nötig. Z. B. Kann man die Methode println tatsächlich auch ohne Parameter aufrufen: System.out.println();

Diese Anweisung gibt eine Zeile ohne Inhalt aus. Das Objekt System.out bietet mit der Methode print noch eine weitere Funktionalität an. Die Methode print ermöglicht Ausgaben ohne automatischen Zeilenumbruch. Das heisst, alles was nach einer print Anweisung ausgegeben wird, erscheint auf der gleichen Zeile. Ansonsten funktioniert die Methode print genau gleich wie println. Beispiel 2.2 Zur Unterscheidung von print und println beachten Sie die Klasse Start in Abb. 2.2. Die Ausgabe, die dieses Programm erzeugt, lautet: Achtung, Fertig, Los! Fehlstart! Nochmals....

2.1.2

Konkatenation von Zeichenketten

Zeichenketten dürfen beliebig lang sein. Dies kann dazu führen, dass wir die Konvention, dass eine Zeile Quellcode nicht mehr als 80 Zeichen umfassen sollte, nicht einhalten können (siehe Abb. 2.3a). Wird die Zeichenkette aber einfach mit einem Zeilenumbruch getrennt, führt dies zu einem Kompilierfehler. Mit anderen Worten: Zeichenketten dürfen das Spezialzeichen „Zeilenumbruch” nicht enthalten (siehe Abb. 2.3b).

Abb. 2.2 Demonstration zum Unterschied von print und println. → Video K-2.1

26

2 Daten und Ausdrücke

(a) Eine lange Zeichenkette in einer Programmieranweisung.

(b) Kompilierfehler, da eine Zeichenkette nicht durch einen Zeilenumbruch getrennt werden darf.

(c) Konkatenation von zwei Zeichenketten.

Abb. 2.3 Konkatenation von Zeichenketten

Wenn wir eine lange Zeichenkette definieren, die sich über mehrere Zeilen erstreckt, können wir die String Konkatenation verwenden, um eine Zeichenkette mit einer anderen zu verbinden. Der zugehörige Operator ist das Pluszeichen (+) und wird in Abb. 2.3c verwendet. Beachten Sie, dass die ausgegebene Zeichenkette keinen Zeilenumbruch enthält. Die ausgegebene Zeichenkette befindet sich also auf einer Zeile – wir kommen gleich darauf zu sprechen, wie Sie in einer Zeichenkette Zeilenumbrüche einfügen können. Der Konkatenationsoperator + kann auch dazu verwendet werden, um Zahlen mit Zeichenketten zu verbinden. Beispiel 2.3 In folgender Zeile wird eine Zeichenkette mit einer Zahl verbunden:

"Ich bin " + 38 + "Jahre alt." Beachten Sie, dass im obigen Beispiel die Zahl 38 nicht von Anführungszeichen umschlossen wird (das heisst, 38 wird als Zahl und nicht als Zeichenkette interpretiert). In diesem Fall wird die Zahl automatisch in eine Zeichenkette konvertiert und die beiden Zeichenketten werden anschliessend konkateniert. Hinweis: Wir hätten natürlich auch einfach "Ich bin 38 Jahre alt."

schreiben können, da Ziffern auch in Zeichenketten vorkommen können. Neben der Konkatenation wird das Pluszeichen (+) auch für arithmetische Additionen verwendet. Die tatsächliche Funktion des Symbols + hängt vom Typ der Daten ab, auf denen es angewendet wird. Der Operator + führt eine Konkatenation durch, wenn mindestens einer

2.1

Zeichenketten

27

der beiden Operanden eine Zeichenkette ist. Sind hingegen beide Operanden Zahlen, so werden diese addiert. Beachten Sie, dass in Java Operationen grundsätzlich von links nach rechts ausgewertet werden – man kann aber Klammern verwenden, um die Reihenfolge der Auswertung zu beeinflussen. Beispiel 2.4 Beachten Sie das Programm in Abb. 2.4. Im ersten Aufruf der Methode println führen beide + Operationen eine Konkatenation aus (da diese von links nach rechts ausgeführt werden). In der zweiten Anweisung wird zunächst eine Addition und danach eine Konkatenation ausgeführt. In der der dritten Anweisung erzwingen wir mit Hilfe von Klammern, dass zuerst zwei Zahlen addiert werden und danach erst wird die Zeichenkette mit dem Resultat der Addition konkateniert. Die Ausgabe des Programmes lautet demnach: Wir konkatenieren 7 und 3: 73 10: Auswertung erfolgt von links nach rechts Erzwingen einer anderen Reihenfolge: 10

2.1.3

Escape Sequenzen

Beachten Sie, dass Java doppelte Anführungszeichen dazu verwendet, um den Start und das Ende einer Zeichenkette festzulegen. Das heisst, dass die folgende Anweisung einen Kompilierfehler auslösen wird, da der Kompilierer die zweiten Anführungszeichen als Ende der Zeichenkette interpretiert – statt eines H erwartet der Kompilierer jetzt eigentlich eine schliessende Klammer und ein Semikolon );

Abb. 2.4 Konkatenation vs. Addition. → Video K-2.2

28

2 Daten und Ausdrücke

System.out.println("Er sagte: "Hallo.""); Um dieses (und ähnliche) Probleme zu bewältigen, bietet Java eine Reihe von Escape Sequenzen an, mit denen Spezialzeichen in Zeichenketten integriert werden können. Escape Sequenzen starten immer mit dem Zeichen \. Hier vier Beispiele von solchen Sequenzen: • • • •

\t: Tabulator \n: Zeilenumbruch \ ": Anführungszeichen \\: Backslash Zeichen

Beispiel 2.5 Die Ausgabe des Programmes Poem in Abb. 2.5 wird folgendermassen formatiert: Vier Maurer sassen einst auf einem Dach. Da sprach der erste: "Ach!" Der zweite: "Wie ist’s möglich dann?" Der dritte: "Dass das Dach halten kann!!!" Der vierte: "Ist doch kein Träger dran!!!!!!", Und mit einem Krach Brach das Dach. \

Kurt Schwitters \

Beispiel 2.6 Beachten Sie das Programm in Abb. 2.6, in dem in verschiedenen println Anweisungen der Umgang mit Zeichenketten zusammengefasst wird.

Abb. 2.5 Ein Beispiel mit verschiedenen Sonderzeichen in konkatenierten Zeichenketten

2.2 Variablen und Konstanten

29

Abb. 2.6 Demonstration zum Umgang mit Zeichenketten. → Video K-2.3

2.2

Variablen und Konstanten

In Java ist eine Variable ein Name für einen Speicherort, der einen bestimmten Wert enthalten kann. Die sogenannte Deklaration einer Variablen enthält immer zwei Dinge: Den Datentypen der Variablen und einen Bezeichner für die Variable. Folgende Codezeile deklariert eine Variable vom Datentyp int mit dem Bezeichner pages (der Datentyp int entspricht einer ganzen Zahl (engl. integer)): int pages;

Mit einer Deklaration instruieren wir den Kompilierer, dass ein Teil des Hauptspeichers reserviert werden muss, der gross genug ist, um einen Wert des angegebenen Datentyps speichern zu können. Solange einer deklarierten Variablen kein Wert zugewiesen ist, nennen wir die Variable undefiniert. Ist eine Variable deklariert, so können wir dieser einen Wert des deklarierten Typs zuweisen: pages = 256;

In diesem Fall sprechen wir von einer Zuweisung, da diese Operation einer Variablen einen Wert zuweist. Wird eine Zuweisung ausgeführt, so wird die rechte Seite des Zuwei-

30

2 Daten und Ausdrücke

sungsoperators (=) ausgewertet und das Resultat wird im Speicher an den Platz gelegt, auf den die Variable auf der linken Seite zeigt. Der Datentyp der Variablen muss bei einer Zuweisung nicht nochmals angegeben werden – dieser wird nur genau einmal angegeben, nämlich bei der Deklaration der Variablen. Eine Deklaration und eine Zuweisung können aber auch auf einer Zeile erfolgen: int figures = 46;

In einer Deklaration können zudem mehrere Variablen des gleichen Typs gleichzeitig deklariert werden. Jede Variable der Deklaration kann dabei mit oder ohne initiale Zuweisung deklariert werden. Folgende Zeile deklariert zwei Variablen vom Typ int. Eine Variable bleibt undefiniert und einer Variablen wird der Wert 46 zugewiesen: int figures = 46, tables;

Beispiel 2.7 Beachten Sie die Klasse BookStatistic in Abb. 2.7. Auf Zeile 8 deklarieren wir eine Variable mit dem Bezeichner pages, die eine ganze Zahl – also einen int – speichern kann. Auf Zeile 9 weisen wir der Variablen pages den Wert 256 zu. Auf den Zeilen 10 und 11 werden dann zwei weitere Variablen vom Typ int deklariert und auch diesen Variablen werden Werte zugewiesen. Eine Deklaration reserviert genügend Speicherplatz für den angegebenen Datentypen. Gleichzeitig definiert eine Deklaration den Bezeichner, über den der Wert/Inhalt dieser Variablen referenziert werden kann. Das heisst, dieser Bezeichner kann nach der Deklaration in unserem Quellcode verwendet werden. Im Programm BookStatistic werden beim ersten Aufruf der Methode println zwei Informationseinheiten verwendet. Die erste ist die Zeichenkette "Anzahl Seiten des Buches:" und die zweite die Variable pages. Wird eine Variable referenziert, so wird der Wert, der unter dieser Variablen gespeichert ist, verwendet. Daher wird dieses Programm folgende Ausgabe generieren: Anzahl Seiten des Buches: 256 Anzahl Abbildungen: 46; Anzahl Tabellen: 17

Wir betrachten als nächstes ein Programm, das den Wert einer Variablen zur Laufzeit des Programmes ändert (siehe Klasse Polygon in Abb. 2.8 – nach einer Idee aus [1]). In diesem Programm wird zunächst eine Variable vom Typ int mit dem Bezeichner sides deklariert und mit dem Wert 3 initialisiert. Danach wird der aktuelle Wert der Variablen sides ausgegeben. Die nächste Anweisung in der Methode main ändert den gespeicherten Wert der Variablen sides mit einer Zuweisung auf den Wert 12:

2.2 Variablen und Konstanten

Abb. 2.7 Deklaration von Variablen und Zuweisungen von Werten. → Video K-2.4

Abb. 2.8 Zuweisungen von Werten zu Variablen. → Video K-2.5

31

32

2 Daten und Ausdrücke

sides = 12; Danach erfolgt eine weitere Zuweisung mit dem Wert 19. Eine Variable kann immer nur einen Wert gleichzeitig speichern. Bei einer Zuweisung wird der alte Wert also mit dem neu zugewiesenen Wert überschrieben. Die ersten drei Ausgaben des Programmes Polygon lauten deshalb: Anzahl Seiten eines Trigons: 3 Anzahl Seiten eines Dodekagons: 12 Anzahl Seiten eines Nonadekagons: 19

Wird eine Variable aber nur verwendet (z. B. innerhalb einer arithmetischen Operation), ohne dass eine Zuweisung stattfindet, so bleibt der Wert der Variablen unverändert: Lesen von Daten verändert die Daten niemals – nur mit Zuweisungen können Sie die Werte von Variablen ändern! Betrachten Sie zum Beispiel die Anweisung auf Zeile 17: Hier wird die Variable sides nur gelesen und deshalb bleibt diese unverändert. Die zwei weiteren Ausgaben des Programmes Polygon lauten deshalb: Anzahl Seiten eines Ikosagons: 20 In der Variablen sides ist gespeichert: 19

Eine Konstante ist ein Bezeichner der ähnlich zu einer Variablen ist. Der Unterschied besteht darin, dass einer Konstante während ihrer gesamten Existenz nur einmal ein Wert zugewiesen werden kann (wie der Begriff andeutet, ist es eben eine Konstante und nicht eine Variable). In Java kann mit dem reservierten Wort final in der Deklaration festgelegt werden, dass eine Variable eine unveränderliche Konstante speichern soll. Folgende Zeile deklariert bspw. eine Konstante mit dem Bezeichner MAX_VISITORS vom Typ int (z. B. zur Festlegung der maximalen Anzahl Zuschauer in einem Kino) und weist der Konstanten den Wert 499 zu: final int MAX_VISITORS = 499;

Die Konvention, dass man Bezeichner für Konstanten mit Grossbuchstaben schreibt und einzelne Wörter mit einem Unterstrich trennt, hilft dem menschlichen Leser Konstanten von Variablen zu unterscheiden. Konstanten sind aus mindestens drei Gründen hilfreich: • Wenn wir in unserem Quellcode auf die maximal zulässige Zuschauerzahl referenzieren wollen, so könnten wir auch einfach die Zahl 499 verwenden. Die Verwendung des

2.3

Primitive Datentypen

33

Bezeichners MAX_VISITORS im Quellcode ist im Gegensatz zur Verwendung der Zahl 499 aber besser verständlich2 . • Konstanten vereinfachen die Wartung des Quellcodes. Auch wenn die Konstante MAX_VISITORS an mehreren Stellen im Quellcode verwendet wird, muss diese nur einmal mit dem Wert 499 definiert werden. So wird die Gefahr eliminiert, dass bei nachträglichen Anpassungen nicht alle Werte im Quellcode gefunden und angepasst werden. • Der Kompilierer wird eine Fehlermeldung erzeugen, wenn man versucht, den Wert einer Konstanten mit einer weiteren Zuweisung zu ändern. Konstanten stellen also sicher, dass einmal zugewiesene Werte nicht verändert werden können.

2.3

Primitive Datentypen

In Java existieren acht primitive Datentypen mit denen Variablen deklariert werden können. Vier dieser Datentypen repräsentieren ganze Zahlen (byte, short, int, long), zwei repräsentieren Gleitkommazahlen (float, double), ein Datentyp steht für einzelne Zeichen (char) und einer steht für Wahrheitswerte (boolean). Alle acht Bezeichner für diese Datentypen sind reservierte Wörter in Java. • Numerische Datentypen: Die Unterschiede der numerischen Datentypen bezieht sich auf deren Grösse und der Wertebereiche, die diese speichern können: Datentyp byte short int long float double

Min, Max −128, 127 −32 768, 32 767   −2 147 483 648, +2 147 483 647 ≈ ±9 × 1018 ≈ ±3.4 × 1038 ≈ ±1.7 × 10308

Wir werden in diesem Buch i. d. R. den Datentypen int für ganze Zahlen und den Datentypen double für Gleitkommazahlen verwenden. Beispiel 2.8 Nachfolgend ein paar Beispiele von Deklarationen (und Zuweisungen) von numerischen Variablen und Konstanten: int num = -17; int var1, var2, var3; 2 Werden im Quellcode Zahlen statt Konstanten/Variablen verwendet, so spricht man auch von Magic Numbers, weil i. d. R. nicht klar ist, was diese bedeuten, woher sie kommen und ob diese verändert werden dürfen.

34

2 Daten und Ausdrücke

final double PI = 3.14159265359;

• Zeichen: Eine Variable vom Typ char kann genau ein Zeichen speichern (Gross und Kleinbuchstaben, Ziffern, Interpunktionszeichen und weitere Sonderzeichen). Ein char wird in Java mit einem einfachen Anführungszeichen definiert (z. B. ’b’,’J’, oder ’;’). Beispiel 2.9 Nachfolgend ein paar Beispiele von Deklarationen (und Zuweisungen) von Variablen vom Typ char: char char char char

topValue = ’6’; character1 = ’A’, character2 = ’a’; eol = ’;’, separator = ’\t’; space = ’ ’;

Hinweis: Verwechseln Sie den Datentypen char nicht mit Zeichenketten. Zeichenketten werden mit doppelten Anführungszeichen begrenzt und dürfen null, ein oder mehr Zeichen beinhalten ("", "Q", "ABC", . . .). Eine Zeichenkette ist auch kein primitiver Datentyp, sondern ist definiert durch eine eigene Klasse – die Klasse String, welche im nächsten Kapitel genauer betrachtet wird. • Wahrheitswerte: Eine Boolesche Variable, d. h. eine Variable vom Typ boolean, kann nur die zwei Werte true oder false annehmen (true und false sind beides reservierte Wörter). Eine Variable vom Typ boolean wird üblicherweise dazu verwendet, um anzugeben, ob eine bestimmte Bedingung wahr oder falsch ist. Beispiel 2.10 Nachfolgend ein paar Beispiele von Deklarationen (und Zuweisungen) von Variablen vom Typ boolean: boolean on = false; boolean tooLarge, tooSmall; boolean done = true;

2.3.1

Datenkonvertierung

Jede Variable in Java ist von einem bestimmten Datentypen. Man sagt, Java ist eine stark typisierte Sprache. Das bedeutet, dass es Java im Prinzip nicht erlaubt, einer Variablen einen Wert zuzuweisen, der nicht dem deklarierten Typen entspricht. Manchmal ist es aber hilfreich oder nötig den Datentypen einer bestimmten Variablen anzupassen. Das heisst, wir benötigen die Möglichkeit Datentypen von Variablen zu konvertieren.

2.3

Primitive Datentypen

35

Konvertierungen von einem Datentypen mit einem kleinen Wertebereich zu einem anderen Datentypen mit einem grösseren Wertebereich (z. B. vom Datentyp int zum Datentyp double) können problemlos durchgeführt werden, da hierbei keine Informationen verloren gehen können. Beispiel 2.11 In folgendem Beispiel weisen wir der Variablen num vom Typ double einen int Wert zu. int count = 17; double num = count;

Für die zweite Zuweisung wird der Wert der Variablen count automatisch in einen double konvertiert. Das heisst, die Variable num enthält nun den Wert 17.0. Der Datentyp von count ist aber – und das ist wichtig – weiterhin int und der gespeicherte Wert in count ist nach wie vor die ganze Zahl 17. Immer wenn von einem „grösseren“ zu einem „kleineren“ Datentypen konvertiert wird, besteht die Gefahr, dass wir Informationen verlieren. Deshalb ist die umgekehrte Zuweisung nicht erlaubt. Würden wir also z. B. versuchen, der Variablen count einen Wert vom Typ double zuzuweisen, erhalten wir einen Kompilierfehler. Das heisst, folgende Zuweisung funktioniert nicht: double num = 12.34; int count = num;

Mit einer sogenannten Cast-Konvertierung können wir aber auch solche Zuweisungen erzwingen. Ein Cast ist ein Java Operator, der durch einen Typennamen in Klammern definiert ist. Der Cast Operator wird vor die Variable, deren Datentyp konvertiert werden soll, gestellt. Soll Beispielsweise num vom Typ double zu einer ganzen Zahl konvertiert werden, kann dies mit folgender Anweisung geschehen: int count = (int) num;

Diese Cast Operation entfernt den gebrochenen Teil vom Wert, der in num gespeichert ist. Ist z. B. der Wert 12.34 in num gespeichert, so enthält nun count den Wert 12. Der Wert in der Variablen num wird dabei aber nicht verändert!

36

2 Daten und Ausdrücke

2.4

Arithmetische Ausdrücke

Ein Ausdruck (engl. Expression) ist eine Kombination von einem oder mehreren Operatoren und Operanden. Die Operanden in einem Ausdruck können unter anderem Werte eines bestimmten Datentyps (z. B. 427, 3.14, false, "Haus", ’c’, etc.) oder zuvor deklarierte Variablen/Konstanten sein. Im Folgenden werden wir uns auf arithmetische Ausdrücke fokussieren, die numerische Operanden verwenden und numerische Resultate berechnen (im nächsten Abschnitt behandeln wir dann Boolesche Ausdrücke, die mit Variablen vom Typ boolean operieren). In Java sind die vier arithmetischen Grundoperationen (+, -, *, /) definiert. Beispiel 2.12 Zwei Beispiele von arithmetischen Ausdrücken in Java (eine Addition und eine Multiplikation): double num1 = 5.1, num2 = 3.2; double sum = num1 + num2; double product = num1 * num2;

Die Grundoperationen sind nicht nur für Gleitkommazahlen (double), sondern auch für ganze Zahlen (int) definiert. Bei der Division von ganzen Zahlen muss man allerdings vorsichtig sein. Wenn beide Operanden einer Division ganze Zahlen sind, so wird das Resultat der Operation auch eine ganze Zahl (der gebrochene Teil des Resultates wird dann einfach von Java weggelassen.) Das bedeutet z. B., dass in Java 14/3 zu 4 oder 8/12 zu 0 ausgewertet wird. Umgekehrt reicht es, wenn einer der beiden Operanden eine Gleitkommazahl ist, um eine Gleitkommadivision zu erzwingen (d. h. in Java werden 10.0/4, 10/4.0 oder 10.0/4.0 alle zu 2.5 ausgewertet). Java besitzt neben den vier Grundoperatoren noch weitere arithmetische Operatoren, z. B. den Modulo-Operator %. Dieser Operator gibt den Rest zurück, der entsteht, wenn man den ersten Operanden durch den zweiten Operanden dividiert. Zum Beispiel ergibt 18 % 4 = 2, da 18/4 = 4 mit Rest 2. Beispiel 2.13 Einige weitere Beispiele zur Modulooperation: • • • •

13 % 7 = 6 20 % 3 = 2 10 % 5 = 0 3 % 8 = 3

Der Modulo-Operator kann auch auf Gleitkommazahlen angewendet werden. Dies kann insbesondere hilfreich sein, wenn man den Nachkommabereich einer Zahl extrahieren möchte:

2.4

Arithmetische Ausdrücke

37

• 12.34 % 1 = 0.34 In Java können Operatoren auch kombiniert werden. Z. B. kann man in Java folgende Anweisung programmieren: double result = 20 + 10 / 2; In Java werden (wie aus der Mathematik bekannt) Multiplikationen und Divisionen vor Additionen und Subtraktionen durchgeführt (der Modulo-Operator besitzt die gleiche Priorität wie die Multiplikation und Division). Operatoren mit der gleichen Priorität werden von links nach rechts abgearbeitet – mit Hilfe von Klammern kann die Reihenfolge der Operationen aber beeinflusst werden (damit ein Ausdruck syntaktisch korrekt ist, muss die Anzahl linker Klammern mit der Anzahl rechter Klammern übereinstimmen und die Klammern müssen mathematisch korrekt platziert sein). Beispiel 2.14 Die folgende Anweisung double result = 20 + 10 / 2;

weist der Variablen result den Wert 25 zu, während double result = (20 + 10) / 2;

der Variablen result den Wert 15 zuweist. Beachten Sie, dass Klammern auch geschachtelt werden können. Die Evaluation eines Ausdrucks beginnt dann bei der innersten Klammer und arbeitet sich schrittweise nach Aussen vor. Die folgende Anweisung double result = 3 * ((15 - 3) / 2);

weist also der Variablen result den Wert 18 zu. Beispiel 2.15 Betrachten Sie die Klasse Grading in Abb. 2.9. Auf den Zeilen 12 und 13 wird die ganzzahlige Konstante MAX_POINTS und die ganzzahlige Variable points in einer Division miteinander verrechnet. In diesem Beispiel wäre das Resultat des arithmetischen Ausdrucks points / MAX_POINTS also 0. Wir konvertieren aber die Variable points für die Berechnungen via Cast Operator zu einem double, so dass eine korrekte Division durchgeführt wird. Auf Zeile 12 berechnen wir den Prozentwert der erreichten Punkte und auf Zeile 13 benutzen wir zur Berechnung einer Schulnote folgenden arithmetische Ausdruck: Schulnote =

Erreichte Punkte ×5+1 M¨ogliche Punkte

38

2 Daten und Ausdrücke

Abb. 2.9 Bei einer ganzzahligen Division kann man eine Cast Konvertierung vornehmen. → Video

K-2.6

Das Resultat dieser Berechnung ist eine (ungerundete) Note zwischen 1 und 63 . Die Ausgabe des Programmes lautet: Sie haben 17 von 20 Punkten erreicht Dies entspricht 85.0 % Schulnote: 5.25

Hinweis: Der Zuweisungsoperator (=) hat die kleinere Priorität als alle arithmetischen Operationen. Die gesamte rechte Seite einer Zuweisung wird also immer zuerst berechnet und danach erst der Variablen auf der linken Seite des Zuweisungsoperators zugewiesen (siehe Abb. 2.10). Aus diesem Grund darf die rechte und linke Seite einer Zuweisung die gleiche Variable beinhalten. Das heisst, dass zum Beispiel folgende Anweisung syntaktisch korrekt ist:

3 Wir verwenden das Schweizer Schulnotenmodell, bei dem 6 die Bestnote ist und Noten kleiner 4 einer ungenügenden Leistung entsprechen.

2.4

Arithmetische Ausdrücke

Abb. 2.10 Reihenfolge der Auswertung: Der Zuweisungsoperator = hat die niedrigste Priorität

39

4

1

3

2

count = count + 1;

Zuerst wird 1 zum ursprünglichen Wert von count addiert und danach wird der neue Wert in count gespeichert (das heisst, der alte Wert von count wird überschrieben). Beachten Sie insbesondere den Unterschied zur Anweisung count + 1;

welche keine Wirkung auf die Variable count hat. Zwei weitere sehr praktische arithmetische Operatoren sind das Inkrement (++) und das Dekrement (--). Beide Operatoren verwenden nur einen Operanden und addieren zu diesem 1 beim Inkrement, bzw. subtrahieren von diesem 1 im Fall eines Dekrements. Die folgende Anweisung führt dazu, dass der Wert von count um 1 erhöht wird: count++;

Das Resultat wird wiederum in der Variablen count gespeichert. Das heisst, das Inkrement ist funktional äquivalent zu count = count + 1;

Oftmals müssen auf Variablen Operationen ausgeführt werden und das Resultat dieser Operation soll danach wieder in der gleichen Variablen gespeichert werden. Zum Beispiel möchten wir zur Variablen num den Wert der Variablen count addieren. Beachten Sie aber, dass die Anweisung num + count;

die Variable num nicht verändert. Um den Wert der Variablen num entsprechend zu ändern, schreiben wir folgende Anweisung: num = num + count;

40

2 Daten und Ausdrücke

Der Einfachheit halber existieren in Java Zuweisungsoperatoren, welche diesen Vorgang erleichtern. Zum Beispiel ist die Anweisung num += count;

äquivalent zu der obigen Zuweisung. Es existieren unterschiedliche Zuweisungsoperatoren in Java, unter anderem die folgenden: • • • • •

x x x x x

+= -= *= /= %=

y entspricht x y entspricht x y entspricht x y entspricht x y entspricht x

= = = = =

x x x x x

+ * / %

y y y y y

Die rechte Seite einer Zuweisungsoperation kann ein komplexer Ausdruck sein. Dieser Ausdruck wird zuerst komplett ausgewertet und erst dann mit der linken Seite kombiniert (und in der Variablen gespeichert). Dies gilt für alle Zuweisungsoperatoren. Das bedeutet, dass z. B. total += (points - 10) / MAX_POINTS;

äquivalent ist zu total = total + ((points - 10) / MAX_POINTS);

2.5

Boolesche Ausdrücke und die if-Anweisung

Java Programme starten immer mit der ersten Anweisung der main Methode und führen danach – solange nichts anderes definiert ist – alle weiteren Anweisungen dieser Hauptmethode von oben nach unten aus, bis das Ende der Methode main erreicht wird. Innerhalb einer Methode können wir aber mit Hilfe von speziellen Programmieranweisungen die Reihenfolge der Ausführungen beeinflussen. In diesem Abschnitt schauen wir hierzu eine erste Möglichkeit an. Eine if -Anweisung besteht aus dem reservierten Wort if gefolgt von einem Booleschen Ausdruck in Klammern, gefolgt von einer beliebigen Programmieranweisung. Ein Boolescher Ausdruck ist entweder wahr oder falsch (besitzt also den Wert true oder false). Wenn er wahr ist, so wird die Programmieranweisung ausgeführt und wenn nicht, so wird die Programmieranweisung übersprungen und es wird mit der nächsten Anweisung unterhalb der if-Anweisung fortgefahren (siehe Abb. 2.11).

2.5

Boolesche Ausdrücke und die if-Anweisung

41

Abb. 2.11 Die Logik einer if-Anweisung

Bedingung

wahr falsch Anweisung

Beispiel 2.16 Betrachten Sie folgendes Beispiel einer if-Anweisung: if (count > 10) System.out.println("10 überschritten!");

Der Boolesche Ausdruck in dieser if-Anweisung ist count > 10. Dieser Ausdruck ist entweder wahr oder falsch – entweder ist der Wert, der in count gespeichert ist, grösser als oder aber er ist kleiner-gleich 10. Wenn count grösser ist als 10, so wird die println Anweisung ausgeführt. Umgekehrt wird die println Anweisung übersprungen und die nächste Anweisung unterhalb der if-Anweisung wird ausgeführt. Hinweis: Die Anweisung unter der if Klausel ist eingerückt. Dies impliziert, dass diese Programmieranweisung zur gesamten if-Anweisung gehört (und nur dann ausgeführt wird, wenn die Bedingung wahr ist). Für den menschlichen Leser von Quellcode sind solche Einrückungen wichtig, der Kompilierer hingegen ignoriert jegliche Formatierungen des Quellcodes. Beispiel 2.17 Das Beispielprogramm in Abb. 2.12 fällt eine Entscheidung aufgrund des Wertes der Variablen hours. Wenn der Wert dieser Variablen grösser ist, als die im Quellcode definierte Konstante MAX_HOURS, wird Arbeit stoppen! ausgegeben. Andernfalls wird diese println Anweisung übersprungen. In beiden Fällen wird danach Arbeitszeit überprüft. ausgegeben.

2.5.1

Boolesche Ausdrücke

Das obige Beispiel einer if-Anweisung basiert auf einem Booleschen Ausdruck, der zwei numerische Werte miteinander vergleicht (im Beispiel haben wir hierzu den Operator >

42

2 Daten und Ausdrücke

Abb. 2.12 Ein Programm zur Demonstration einer if-Anweisung. → Video K-2.8

verwendet). In Java existieren weitere sogenannte relationale Operatoren, die den Vergleich zweier numerischer Werte und die Definition eines Booleschen Ausdrucks ermöglichen: • • • • • •

==: Sind zwei Werte gleich? !=: Sind zwei Werte ungleich? : Ist der erste Wert grösser als der zweite Wert? =: Ist der erste Wert grösser-gleich dem zweiten Wert?

Die folgende if-Anweisung gibt den Satz Das Total entspricht den Kosten! aus, wenn die Variablen total und cost den gleichen Wert speichern: if (total == cost) System.out.println("Das Total entspricht den Kosten!");

Umgekehrt gibt die folgende if-Anweisung den Satz Differenz zwischen Total und Kosten. aus, wenn die beiden Variablen ungleiche Werte speichern. if (total != cost) System.out.println("Differenz zwischen Total und Kosten.");

Wenn die Variable total grösser oder gleich der Variablen cost ist, so gibt die folgende if-Anweisung den entsprechenden Satz aus.

2.5

Boolesche Ausdrücke und die if-Anweisung

43

if (total >= cost) System.out.println("Das Total ist grösser oder gleich den Kosten.");

Das logische AND, das logische OR und das logische NOT sind in Java ebenfalls vorhanden: • Der Operator ! wird verwendet, um in Java ein logisches NOT auszudrücken. Der Ausdruck !a ist wahr, wenn a falsch ist und umgekehrt. • Der Operator && wird verwendet, um in Java ein logisches AND auszudrücken. Der Ausdruck a && b ist wahr, wenn a und b beide wahr sind und sonst falsch. • Der Operator || wird verwendet, um in Java ein logisches OR auszudrücken. Der Ausdruck a || b ist wahr, wenn a oder b oder beide wahr sind und sonst falsch. Ein logischer Operator kann mit einer Wahrheitstabelle beschrieben werden, der alle möglichen true-false-Kombinationen für die involvierten Ausdrücke auflistet. Für das logische NOT gibt es nur zwei mögliche Situationen: a false true

!a true false

Beachten Sie, dass das logische NOT den gespeicherten Wert in a nicht verändert (wollen wir den Wert, der in a gespeichert ist, verändern, so braucht es eine Zuweisung: a = !a;). Für das logische AND (&&) und das logische OR (||) gibt es vier mögliche Situationen: a false false true true

b false true false true

a && b false false false true

a || b false true true true

Das logische NOT hat die höchste Priorität der drei logischen Operatoren, gefolgt vom logischen AND und vom logischen OR. Beispiel 2.18 Betrachten Sie die folgende if-Anweisung: if (!complete && (count > MAX)) System.out.println("Alarm!!!");

Die Variable complete sei hierbei vom Typ boolean – ist also entweder wahr oder falsch – genauso ist die Variable count entweder grösser als MAX oder nicht. Folgende Wahrheitstabelle listet alle möglichen Kombinationen auf:

44

2 Daten und Ausdrücke

complete

count > MAX

!complete

false false true true

false true false true

true true false false

!complete && (count > MAX) false true false false

Offensichtlich wird diese if-Anweisung nur dann Alarm!!! ausgeben, wenn complete falsch und count grösser als MAX ist.

2.5.2

Die if-else Anweisung

Manchmal möchten wir in einem Programm etwas ausführen, wenn eine Bedingung wahr ist und etwas anderes, wenn die Bedingung falsch ist. Wir können für solche Fälle ein else zu der if-Anweisung hinzufügen. Betrachten Sie folgendes Beispiel: if (count MAX) wahr ist, werden in obiger if-Anweisung nun zwei Anweisungen durchgeführt: Eine Ausgabe von Fehler und ein Inkrement der Variablen errorCount. Beachten Sie, dass in Java der if-Teil, der else-Teil oder beide Teile aus BlockAnweisungen bestehen können. In der folgenden if-else Anweisung bestehen beispielsweise beide Teile aus Blockanweisungen: if (count > MAX) { System.out.println("Fehler"); errorCount++; } else { System.out.println("Zählerstand: " + count); count += 2; }

46

2.5.4

2 Daten und Ausdrücke

Verschachtelte if-Anweisungen

Die Anweisungen innerhalb einer if- oder else-Anweisung können wiederum ifAnweisungen sein. Solche Konstrukte nennt man verschachtelte if-Anweisungen. Verschachtelte if-Anweisungen erlauben uns, komplexe Entscheidungen zu treffen, die auf mehreren – verschachtelten – Entscheidungen basieren. Beispiel 2.20 Die Klasse MinFinder in Abb. 2.14 verwendet verschachtelte if-Anweisungen, um die kleinste Zahl aus drei gegebenen Zahlen zu finden (nach einer Idee aus [1]). Betrachten Sie die folgende verschachtelte if-Anweisung: if (!full) if (pressure MAX) && !ok 17. Es sei • int num1 = 1, num2 = 10; • int num1 = 0, num2 = 10; • int num1 = 10, num2 = 1; Was ist die Ausgabe des folgenden Code Fragments für diese drei Fälle? if (num1 < num2) System.out.println("rot"); if ((num1 + 9) < num2) System.out.println("blau"); else System.out.println("weiss"); System.out.println("gelb");

18. Es sei • • • • •

int int int int int

num1 num1 num1 num1 num1

= = = = =

5, num2 = 4; 8, num2 = 4; 10, num2 = 11; 10, num2 = 12; 10, num2 = 13;

Was ist die Ausgabe des folgenden Code Fragments für diese fünf Fälle?

2.6

Interaktive Programme

51

if (num1 >= num2) { System.out.print("1 "); System.out.print("2 "); num1 = num2 - 1; } System.out.print("3 "); if ((num1 + 1) >= num2) System.out.print("4 "); else if ((num1 + 2) >= num2) { System.out.print("5 "); System.out.print("6 "); } else System.out.print("7 "); System.out.print("8 ");

Java Übungen 1. Erstellen Sie ein Programm, das zwei Variablen vom Typ int je einen Wert zuweist und erzeugen Sie danach eine Ausgabe, welche das Resultat der Addition dieser beiden Zahlen ausgibt. Also soll die Ausgabe zum Beispiel lauten: 17 + 5 = 22. → Video V-2.1 2. Erweitern Sie das obige Programm, so dass nicht nur die Summe sondern auch die Differenz, das Produkt und der Quotient der beiden Zahlen ausgegeben wird (Hinweis: Bei der Division benötigen Sie einen Cast Operator, der Ihnen die int Variable(n) in einen double konvertiert). → Video V-2.2 3. Schreiben Sie ein Programm, das für eine bestimmte ganze Zahl überprüft, ob diese gerade oder ungerade ist (Tipp: Verwenden Sie den Modulo-Operator %) und erzeugen Sie eine entsprechende Nachricht als Ausgabe: gerade Zahl bzw. ungerade Zahl. → Video V-2.3 4. Schreiben Sie ein Programm, das für eine bestimmte ganze Zahl überprüft, ob diese grösser, kleiner oder gleich einer von Ihnen im Quellcode definierten Konstante THRESHOLD ist und erzeugen Sie eine entsprechende Ausgabe. → Video V-2.4

52

2 Daten und Ausdrücke

5. Schreiben Sie die Klasse MinFinder aus Kap. 2 so um, dass keine verschachtelten ifAnweisungen nötig sind. Tipp: Verwenden Sie das logische AND (&&) zur Verknüpfung zweier Boolescher Ausdrücke. → Video V-2.5 6. Schreiben Sie ein Programm, das für eine bestimmte Temperatur überprüft, ob diese kleiner als 15 Grad ist (in diesem Fall geben Sie die Meldung kalt! aus), ob die Temperatur grösser-gleich 15 aber kleiner-gleich 24 Grad ist (in diesem Fall geben Sie die Meldung angenehm! aus) oder ob die Temperatur grösser als 24 Grad ist (in diesem Fall geben Sie die Meldung heiss! aus. Hinweis: Verwenden Sie Konstanten für die beiden Temperaturgrenzen. → Video V-2.6

Literatur 1. John Lewis and William Loftus. Java Software Solutions - Foundations of Program Design. Pearson Global Edition, 8th edition edition, 2015.

3

Klassen des Java API Verwenden (Teil 1)

Die Programmiersprache Java wird durch zahlreiche Klassenbibliotheken (engl. Class Libraries) unterstützt, die wir bei der Programmierung nutzen können. Eine Klassenbibliothek besteht aus bereits erstelltem Quellcode, der eine enorme Menge an Funktionalität bieten kann und so die eigene Programmierung unterstützt. Der Kompilierer von Java (etwas präziser: das JDK) beinhaltet solche Klassenbibliotheken – die sogenannte Standard Class Library auch Java API genannt (API steht für Application Programming Interface) – es gibt aber auch Klassenbibliotheken, die von Drittfirmen separat erworben werden können. In diesem Kapitel diskutieren wir den Gebrauch der Standardbibliothek von Java. Insbesondere geht es dabei um den Prozess, Objekte aus bestehenden Klassen des Java APIs zu instanziieren und dann die Funktionalitäten der erzeugten Objekte zu verwenden.

3.1

Objekte aus Klassen Instanziieren

In Java repräsentiert eine Variable entweder einen primitiven Datentypen (z. B. int, double, char, boolean, etc.) oder ein Objekt1 . Genau wie bei Variablen für primitive Datentypen müssen auch Variablen, die ein Objekt speichern sollen, zunächst deklariert werden. Die Deklarationen von Objektvariablen haben die gleiche Struktur, wie Deklarationen von primitiven Variablen: Datentyp und Bezeichner. Die Klasse, die als Bauplan für das Objekt dient, kann dabei als Datentyp des Objektes interpretiert werden.

Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_3. 1 Die Sprache Java ist also nicht bis zur letzten Konsequenz objektorientiert. Primitive Datentypen

wie zum Beispiel ganze Zahlen oder Wahrheitswerte werden in Java nicht als Objekte verwaltet. © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_3

53

54

3 Klassen des Java API Verwenden (Teil 1)

Beispiel 3.1 Betrachten Sie die folgenden Deklarationen zweier Variablen mit den Bezeichnern number und code: int number; String code;

Die erste Deklaration definiert eine Variable, die eine ganze Zahl speichern soll (der deklarierte Datentyp ist int). Die zweite Deklaration definiert eine Variable, die ein Objekt vom Datentyp String speichern soll. Dieser Datentyp ist in der Klasse String definiert. Im Gegensatz zu Variablen für primitive Datentypen, die den Wert der Variablen „direkt“ speichern, enthält eine Objektvariable niemals das Objekt selber sondern eine „Adresse“ (auch Referenz genannt), die auf das Objekt zeigt. Wir sagen: Die Variable referenziert das Objekt. Objektvariablen werden deshalb auch Referenzvariablen genannt (im vorliegenden Buch werden beide Begriffe synonym verwendet). Deklarierte Variablen besitzen zu Beginn keinen Wert. Wir sagen, die Variablen sind nicht initialisiert oder undefiniert (siehe Abb. 3.1a). Es ist wichtig, dass Variablen immer initialisiert werden, bevor diese verwendet werden. Für Objektvariablen bedeutet dies, dass diese ein Objekt referenzieren müssen, bevor sie verwendet werden. In den meisten Situationen wird der Kompilierer eine Fehlermeldung auslösen, wenn versucht wird, auf eine deklarierte Variable zuzugreifen, die noch undefiniert ist. Obschon wir also mit String code; eine Objektvariable deklariert haben, existiert zu diesem Zeitpunkt noch kein Objekt vom Typ String, das von der Variable mit dem Bezeichner code referenziert wird. Daher kann die Funktionalität des Objektes code auch noch nicht verwendet werden. In der Klasse sind die Eigenschaften und die Funktionalitäten der Objekte programmiert – und zwar mit Variablen und Methoden. Der Vorgang, ein neues Objekt aus einer Klasse zu generieren, heisst Instanziierung (man sagt denn auch, ein Objekt ist eine Instanz einer bestimmten Klasse).

a

b

number

-

number

code

-

code

17 Objekt Referenz/Adresse

Abb. 3.1 a Zwei deklarierte aber noch nicht initialisierte Variablen. b Den Variablen wurden Werte zugewiesen

3.1

Objekte aus Klassen Instanziieren

55

Um ein Objekt zu instanziieren, wird der sogenannte new-Operator verwendet, der ein Objekt aus dem entsprechenden Bauplan – also der Klasse – generiert. Der new-Operator instanziiert Objekte, indem der sogenannte Konstruktor der entsprechenden Klasse aufgerufen wird. Ein Konstruktor ist eine spezielle Methode, die immer den gleichen Bezeichner besitzt, wie die Klasse selbst. Zum Beispiel können wir mit new String("…");

den Konstruktor String aufrufen, der uns dann ein neues Objekt aus der Klasse String instanziiert. In diesem Beispiel verlangt der Konstruktor String einen Parameter, nämlich eine Zeichenkette, die die Zeichen spezifiziert, welche das neu instanziierte Objekt speichern soll. Der Konstruktor erzeugt nicht nur das Objekt, er gibt auch die Adresse auf das instanziierte Objekt zurück, die dann einer Variablen – zum Beispiel der Variablen code – zugewiesen werden kann. Beispiel 3.2 Die folgenden zwei Programmieranweisungen weisen den zuvor deklarierten Variablen number und code Werte zu: number = 17; code = new String("aBc-123");

Da eine Objektvariable nur die Adresse des Objektes enthält, kann eine solche Variable auch als Zeiger (engl. Pointer) aufgefasst werden, der auf den Speicherort des entsprechenden Objektes im Hauptspeicher zeigt (siehe Abb. 3.1b). Die Deklaration einer Objektvariablen und deren Instanziierung kann in einer Anweisung vollzogen werden (analog zur Deklaration und Initialisierung von primitiven Daten wie zum Beispiel int i = 0;): String title = new String("Programmieren mit Java");

Bis auf ein paar Ausnahmen folgt das Deklarieren und Instanziieren von Objekten aus Klassen diesem Muster (siehe auch Abb. 3.2). Beispiel 3.3 In den folgenden vier Anweisungen deklarieren und instanziieren wir vier Objektvariablen hello, random, reader und point. Diese Variablen sind vom Typ String, vom Typ Random, vom Typ Scanner und vom Typ Point (dies sind alles Klassen des Java APIs). Beachten Sie, dass bei einigen Konstruktoren Parameter nötig sind und

56

3 Klassen des Java API Verwenden (Teil 1)

Klassenname (Datentyp)

Objektvariable

Konstruktor

Optionale Parameter

new Operator

Abb. 3.2 Deklaration und Instanziierung eines Objektes

bei anderen nicht. String hello = new String("Hello"); Random random = new Random(); Scanner reader = new Scanner(System.in); Point point = new Point(2, 5);

Objektvariablen können in Java immer auch mit null initialisiert werden. Das Wort null ist ein reserviertes Wort in Java und definiert das „leere“ Objekt. Dies kann hilfreich sein, wenn der Kompilierer verlangt, dass ein Objekt initialisiert sein muss, Sie aber noch nicht wissen, mit welchen konkreten Inhalten das Objekt instanziiert werden soll (z. B. weil die entsprechenden Angaben erst zur Laufzeit des Programmes eingegeben werden). Beispiel 3.4 Beispielsweise können wir ein Objekt vom Typ String oder ein Objekt vom Typ Point (oder ein Objekt jedes anderen Typs) mit null initialisieren:

String name = null; Point point = null; Die Variablen name und point vom Typ String und Point sind nun deklariert und auch initialisiert (je mit dem leeren Objekt null).

3.1.1

Methoden von Instanziierten Objekten Verwenden

Sobald ein Objekt instanziiert worden ist, können wir den Punkt-Operator verwenden, um auf die Funktionalitäten des Objektes zuzugreifen. Die verfügbaren Funktionalitäten eines Objektes sind in der entsprechenden Klasse des Objektes als Methoden programmiert. Der Punkt-Operator wird direkt hinter die Objektvariable angehängt und danach folgt der Name der Methode, die von diesem Objekt ausgeführt werden soll.

3.2

Das Java API

57

Beispiel 3.5 Die Klasse String besitzt zum Beispiel eine Methode length. Um die Methode length aufzurufen, können wir den Punkt-Operator auf der Variablen code (vom Typ String) ausführen: code.length();

Die Methode length erwartet keine Parameter – die Klammern sind aber dennoch nötig, um dem Kompilierer anzugeben, dass es sich bei dem Bezeichner length um eine Methode handelt. Einige Methoden produzieren eine Rückgabe (engl. return value), sobald alle Anweisungen der Methode ausgeführt worden sind. Dies ist nicht zwingend – es existieren Methoden (bzw. man kann Methoden programmieren), die keine Rückgabe erzeugen. Die Methode length der Klasse String evaluiert die Länge der gespeicherten Zeichenkette (zählt also die Anzahl gespeicherter Zeichen) und gibt diesen Wert zurück. Referenziert die Variable code aktuell die Zeichenkette "aBc-123", gibt die Anweisung code.length() den Wert 7 zurück. Oft wollen wir die Rückgabe einer Methode im weiteren Verlauf unseres Programmes verwenden. Zu diesem Zweck können wir die Rückgabe speichern, indem wir diese einer Variablen zuweisen: int codeLength = code.length();

In diesem Beispiel wird der zurückgegebene Wert der Methode length der Variablen codeLength vom Typ int zugewiesen.

3.2

Das Java API

Die Klasse String, die wir in den bisherigen Beispielen verwendet haben, ist kein Bestandteil der Programmiersprache Java. Sie ist Teil der Java Standard Class Library, die in jeder Java Entwicklungsumgebung gefunden werden kann. Die verfügbaren Klassen dieser Bibliothek wurden durch Mitarbeiterinnen und Mitarbeiter von Sun Microsystems bzw. Oracle programmiert (also von den selben Menschen, die Java entwickelt haben bzw. die Sprache Java weiterentwickeln). Die Klassen der Standardbibliothek decken verschiedene Aufgaben ab, die beim Programmieren häufig gelöst werden müssen und sind daher sehr nützlich. Insbesondere kann durch die Verwendung dieser Klassen Zeit gespart werden, da nicht für jedes Teilproblem eigener Quellcode erstellt werden muss. Tatsächlich wird man als Programmiererin oft „abhängig“ von dieser Bibliothek und nimmt diese in Java geschriebene Programme fälschlicherweise als Teil der Programmiersprache wahr.

58

3 Klassen des Java API Verwenden (Teil 1)

Die Java Standardbibliothek besteht aus verschiedenen Gruppen von Klassen, die miteinander verwandt sind – den sogenannten Packages. Zum Beispiel befinden sich im Package java.sql verschiedene Klassen, welche den Zugriff auf Daten, die in einer Datenbank gespeichert sind, ermöglichen. Die Packages nennt man häufig auch Java APIs (Application Programming Interfaces) und die gesamte Java Standard Class Library wird deshalb oftmals einfach Java API genannt. In der folgenden Tabelle sind einige Packages kurz beschrieben, die zum Java API gehören. Package java.beans

java.io java.lang

java.math java.net java.rmi java.security java.sql java.text java.util java.xml

Ermöglicht... . . . die Definition von Komponenten (den sogenannten Beans), die einfach kombiniert werden können . . . vielfältige Input-Output Funktionen (z. B. über das Dateisystem des Computers) . . . generelle und fundamentale Unterstützung bei der Programmierung (dieses Package wird automatisch in alle Java Programme importiert) . . . das Durchführen mathematischer Operationen mit hoher Präzision . . . die Kommunikation über Netzwerke . . . das Erstellen von Programmen, die auf verteilten Systemen laufen sollen . . . das Erzwingen von Sicherheitsrestriktionen . . . die Interaktion mit Datenbanken . . . vielfältige Textformatierungen . . . die Anwendung allgemeiner Hilfsmittel . . . die Verarbeitung von XML Dokumenten

Die online Java API Dokumentation ist von grossem Wert für jede Java Programmiererin: http://docs.oracle.com/javase/8/docs/api/ Diese Dokumentation beinhaltet ausführliche Beschreibungen für jede Klasse des Java APIs. Zum Beispiel sind alle Methoden der jeweiligen Klassen aufgelistet und umfassend beschrieben (oftmals auch mit Beispielen). In Abb. 3.3 ist ein Beispiel gezeigt (für die Klasse Scanner, die wir gleich genauer betrachten werden). Die Klassen des Packages java.lang sind automatisch verfügbar, sobald man ein eigenes Java Programm schreibt. Um die Klassen der anderen Packages verwenden zu kön-

3.2

Das Java API

59

Abb. 3.3 Beispielseite der Java API Dokumentation

nen, müssen wir die entsprechende Klasse oder das gesamte zugehörige Package in unseren Quellcode importieren. Nach dem reservierten Wort import geben wir hierzu das entsprechende Package und dann die Klasse, die in unserem Programm nutzbar sein soll, an. Um z. B. die Klasse Scanner aus dem Package java.util verwenden zu können, muss folgende import Anweisung zu unserem Quellcode hinzugefügt werden: import java.util.Scanner;

Importanweisungen haben in Java immer vor dem Klassenkopf zu erfolgen. Werden mehr als zwei Klassen aus einem bestimmten Package benötigt, so verwendet man oft einen Asterisk (*) um anzugeben, dass alle Klassen des entsprechenden Packages importiert werden sollen: import java.util.*;

Beachten Sie, dass das Java API Hunderte von Klassen für unterschiedlichste Zwecke bereitstellt. Wir verwenden in den restlichen Abschnitten dieses Kapitels nur ein paar wenige Klassen des Java APIs, um uns exemplarisch mit wichtigen Konzepten der Programmierung und der Objektorientierung in Java vertraut zu machen.

60

3.3

3 Klassen des Java API Verwenden (Teil 1)

Die Klasse String

Wir haben die Klasse String bereits beispielhaft verwendet. Nun wollen wir die Klasse noch etwas genauer betrachten. Die Klasse String gehört zum Package java.lang – muss also nicht importiert werden. In folgender Liste sind ein paar nützliche Methoden der Klasse String zusammengefasst2 . Die Listen mit den Methoden von Klassen sind in diesem Buch immer folgendermassen aufgebaut: • Datentyp der Rückgabe Bezeichner der Methode(Datentyp des Parameters Bezeichner des Parameters): Beschreibung der Methode Die einzige Ausnahme bildet immer der Konstruktor, der keine Rückgabe erzeugt und für den deshalb auch kein entsprechender Datentyp angegeben werden kann. • String(String p): Der Konstruktor der Klasse instanziiert mit dem new-Operator ein neues Zeichenketten Objekt mit den Zeichen, die in p übergeben werden. Beispiel: String s = new String("Meine Zeichenkette!"); • char charAt(int index): Gibt das Zeichen an Position index einer Zeichenkette zurück. Beispiel: s.charAt(3); • String concat(String p): Gibt eine neues String Objekt zurück, das aus dieser Zeichenkette konkateniert mit p besteht. Beispiel: s.concat(p); • boolean equals(String p): Gibt true zurück, wenn diese Zeichenkette die gleichen Zeichen in der gleichen Reihenfolge wie p speichert (andernfalls wird false zurückgegeben). Beispiel: s.equals(p); • boolean equalsIgnoreCase(String p): Funktioniert gleich wie equals, aber ohne die Gross- und Kleinschreibung zu beachten. • int length(): Gibt die Anzahl Zeichen dieser Zeichenkette zurück. Beispiel: s.length(); • String replace(char oldChar, char newChar): Gibt eine Kopie dieser Zeichenkette zurück, in der alle Zeichen oldChar mit newChar ersetzt werden. Beispiel: s.replace(’A’, ’E’); • String substring(int offset, int endIndex): Gibt eine Kopie dieser Zeichenkette zurück, in der alle Zeichen vor offset und nach endIndex abgeschnit-

2 In diesem Buch werden wir immer nur einige der verfügbaren Methoden betrachten – wenn Sie

weitere Informationen benötigen, sollten Sie die entsprechende Beschreibung in der online API Dokumentation studieren.

3.3

Die Klasse String

61

ten werden. Beispiel: s.substring(1, 3); • String toLowerCase(): Gibt eine Kopie dieser Zeichenkette zurück, in der alle Grossbuchstaben mit dem zugehörigen Kleinbuchstaben ersetzt werden. Beispiel: s.toLowerCase(); • String toUpperCase(): Gibt eine Kopie dieser Zeichenkette zurück, in der alle Kleinbuchstaben mit dem zugehörigen Grossbuchstaben ersetzt werden. Beispiel: s.toUpperCase(); Einige der Methoden der Klasse String verwenden einen Index, um ein bestimmtes Zeichen zu referenzieren. Ein Zeichen in einem String Objekt kann über dessen Position, oder eben Index, spezifiziert werden. Der Index des ersten Zeichens in einer Zeichenkette ist 0, das nächste Zeichen hat Index 1, usw. Im String Objekt "Pferd" hat das Zeichen P also den Index 0 und der Index des letzten Buchstabens d ist 4. Gibt man in Java mit zwei Indizes Grenzen an, so gilt die Faustregel, dass die untere Grenze inklusive ist und die obere Grenze nicht mehr zur Auswahl gehört. Referenziert also zum Beispiel die Variable s die Zeichenkette "Hallo", so erzeugt s.substring(1, 3); die Zeichenkette "al". Zeichenketten sind zwar keine primitiven Datentypen, aber man kann diese trotzdem instanziieren, wie es bei primitiven Datentypen üblich ist – nämlich mit direkter Wertangabe (also ohne new-Operator und ohne Konstruktor): String city = "Bern";

Diese Initialisierung der Variablen city ist eine Abkürzung zur Instanziierung von String Objekten. Aber auch diese Initialisierung erzeugt ein String Objekt, indem der Kompilierer für uns mit dem new-Operator den Konstruktor aufruft: new String("Bern");. Wurde ein String Objekt instanziiert, so kann weder dessen Grösse noch können einzelne Zeichen verändert werden. Objekte, deren Zustand sich nicht verändern lässt, heissen unveränderlich (engl. immutable). Da String Objekte unveränderlich sind, können Veränderungsmethoden (z. B. die Methoden replace oder toUpperCase) nur ein neues String Objekt zurückgeben, welches das Resultat von Veränderungen an der ursprünglichen Zeichenkette sind. Beispiel 3.6 In Abb. 3.4 werden einige Methoden der Klasse String demonstriert (nach einer Idee aus [1]). Die Ausgabe dieses Programmes lautet: Original: Impossible is temporary Veränderung 1: Impossible is temporary, impossible is nothing. Veränderung 2: ImpossiblX is tXmporary

62

3 Klassen des Java API Verwenden (Teil 1)

Abb. 3.4 Einige Methoden der Klasse String demonstriert. → Video K-3.1

Veränderung 3: ossi Veränderung 4: IMPOSSIBLE IS TEMPORARY Länge der Veränderung 3: 4 Original ist unverändert: Impossible is temporary

Beachten Sie insbesondere, dass das ursprüngliche String Objekt original nicht verändert worden ist.

3.4

Die Klasse Scanner

Es ist oftmals praktisch, Programme zu schreiben, die Daten interaktiv vom Benutzer einlesen können. So können je nach Eingabe des Benutzers bei jedem Programmdurchlauf neue Resultate berechnet werden. Sie erinnern sich z. B. an die Klassen SimpleStatistics oder LimitCheck aus dem vorigen Kapitel, in denen jeweils die entscheidenden Daten (z. B. points oder hours) im Quellcode festgehalten sind. Wir wollen dies nun ändern,

3.4

Die Klasse Scanner

63

so dass ein Benutzer die Anzahl Punkte oder Stunden interaktiv eingeben kann, ohne hierzu den Quellcode anpassen zu müssen. Die Klasse Scanner bietet Methoden an, um Eingaben vom Benutzer in ein Programm einlesen zu können. Die Eingabe kann dabei aus unterschiedlichen Quellen stammen. Zum Beispiel können die Daten interaktiv vom Benutzer über die Tastatur eingegeben werden oder die Daten können aus einer Datei gelesen werden. Die Klasse Scanner kann zudem dazu verwendet werden, um ein String Objekt (also eine Zeichenkette) in einzelne Teile zu zerlegen (z. B. um einen eingegebenen Satz in einzelne Wörter zu zerlegen). Die folgende Liste fasst einige wichtige Methoden der Klasse Scanner zusammen: • Scanner(InputStream source): Der Konstruktor der Klasse um ein neues Scanner Objekt zu instanziieren und Daten von einer bestimmten Quelle einzulesen. Analog dazu existieren Konstruktoren, die eine Datei (File source) oder eine Zeichenkette (String source) als Parameter erwarten. • String next(): Gibt das nächste Element der eingelesenen Zeichenkette als String Objekt zurück. • String nextLine(): Gibt die komplette nächste Zeile als String Objekt zurück. • int nextInt(): Gibt das nächste Element der Eingabe als Wert vom Typ int zurück. Analog dazu existieren z. B. die Methoden nextDouble() oder nextBoolean(). Diese Methoden erzeugen einen Fehler, wenn das zu lesende Element nicht mit dem angegebenen Datentypen übereinstimmt. • Scanner useDelimiter(String pattern): Definiert das Trennzeichen pattern, mit dem die Eingabe in einzelne Teile zerlegt werden soll. • boolean hasNext(): Gibt true zurück, wenn das Scanner Objekt ein weiteres Element lesen kann (wenn kein Element mehr vorhanden ist, wird false zurückgegeben). Um die Funktionalität der Klasse Scanner nutzen zu können (d. h. deren Methoden aufzurufen), muss zuerst mit dem new-Operator und dem Konstruktor Scanner ein entsprechendes Objekt instanziiert werden. Folgende Programmieranweisung deklariert eine Objektvariable mit Bezeichner scan und weist dieser ein neu instanziiertes Objekt vom Typ Scanner zu: Scanner scan = new Scanner(System.in);

Der Konstruktor der Klasse Scanner erwartet als Parameter die Quelle der Eingabe. Wir übergeben dem Konstruktor in obigem Beispiel das Objekt System.in, welches der Tastatur des Computers entspricht. Ist ein Objekt vom Typ Scanner instanziiert, so können die Methoden wie gewohnt mit dem Punkt-Operator aufgerufen werden. Folgende Anweisung führt zum Beispiel die Methode nextLine auf dem Objekt scan aus (die Methode nextLine liest die Ein-

64

3 Klassen des Java API Verwenden (Teil 1)

gabe bis zum ersten Zeilenumbruch und gibt dann die komplette Zeile ungeteilt als String Objekt zurück). scan.nextLine();

Beispiel 3.7 In Abb, 3.5 ist ein Beispielprogramm gezeigt, das ein Objekt vom Typ Scanner instanziiert und dieses (bzw. die Methode nextLine) verwendet. Eine mögliche Ein- und Ausgabe des Programmes sieht folgendermassen aus: Ihr Text: Hallo Java! Mein Echo: "Hallo Java!"

Die Klasse Scanner ist Teil des Packages java.util und muss importiert werden. Beachten Sie hierzu die import Deklaration auf Zeile 1: Diese Deklaration teilt dem Programm mit, dass wir die Klasse Scanner in unserem Quellcode verwenden wollen. Ein Scanner Objekt verwendet den Leerraum in der Eingabe (also Leerzeichen, Tabulatoren oder Zeilenumbrüche), um die Eingabe in Einzelteile (sogenannte Token) zu zerlegen. Beim Aufruf der Methode next wird der nächste Token eingelesen und von der Methode zurückgegeben. Das heisst, ist die Eingabe z. B. eine Reihe von Wörtern separiert durch Leerschläge, gibt jeder Aufruf von next das nächste Wort dieser Eingabe zurück.

Abb. 3.5 Instanziierung eines Objektes vom Typ Scanner zur Ausgabe eines Echos. →Video K-3.2

3.4

Die Klasse Scanner

65

Die Zeichen, welche definieren, wie eine Eingabe in einzelne Token zerlegt werden soll, nennt man Trennzeichen (engl. Delimiter). Mit der Methode useDelimiter kann die Standardzerlegung geändert werden. Möchten wir z. B. ein Komma als Trennzeichen definieren, führen wir auf einem instanziierten Objekt scan folgenden Methodenaufruf aus: scan.useDelimiter(",");

Beispiel 3.8 In Abb. 3.6 ist ein weiteres Programm gezeigt, welches ein Objekt vom Typ Scanner instanziiert und verwendet. In diesem Beispiel soll der Benutzer die Parkzeit in Stunden eingeben und dann wird die entsprechende Parkgebühr berechnet. Ab der sechsten Stunde wird die 1.5-fache Parkgebühr erhoben (hierzu wird ein if-else eingesetzt). Beachten Sie, dass wir in diesem Beispiel die Methode nextInt verwenden, um eine ganze Zahl vom Benutzer einzulesen. Eine mögliche Ein- und Ausgabe sieht folgendermas-

Abb. 3.6 Mit einem Objekt vom Typ Scanner die Parkzeit erfassen und zur Berechnung der Parkgebühr verwenden. → Video K-3.3

66

3 Klassen des Java API Verwenden (Teil 1)

sen aus: Parkzeit in ganzen Stunden eingeben: 6 Zu bezahlen: 16.25 CHF

Das Programm ParkingFee stürzt ab, wenn der Benutzer nicht eine ganze Zahl eingibt (wenn er also zum Beispiel 4.5 oder zwei eingibt). Das Auffangen solcher Probleme behandeln wir später. Zudem haben wir bei beiden Beispielen die Objekte der Klasse Scanner jeweils mit dem Parameter System.in instanziiert, was einer Entgegennahme der Eingaben via Tastatur entspricht. Wir werden später auf die Klasse Scanner zurückkommen, um Eingaben aus Dateien zu lesen.

3.5

Die Klasse Random

In Programmen müssen relativ häufig zufällige Entscheidungen getroffen werden (z. B. beim Werfen eines Würfels in einem Spiel oder bei der zufälligen Zusammenstellung von Fragen in einem Quiz). Die Klasse Random, die Teil des Packages java.util ist, repräsentiert einen Zufallszahl-Generator. Die folgende Liste fasst vier wichtige Methoden der Klasse Random zusammen: • Random(): Der Konstruktor der Klasse (ohne Parameter) – instanziiert ein neues Objekt vom Typ Random zur Erzeugung von Zufallszahlen. • float nextFloat(): Gibt eine zufällige Zahl zwischen 0.0 (inklusive) und 1.0 (exklusive) zurück. • int nextInt(): Gibt eine zufällige ganze Zahl zwischen -2’147’483’648 und 2’147’483’647 zurück. • int nextInt(int max): Gibt eine zufällige ganze Zahl zwischen 0 und max-1 zurück. Diese drei Methoden reichen tatsächlich aus, um Zufallszahlen aus vielfältigen Intervallen zu generieren. Um zum Beispiel eine zufällige ganze Zahl von 1 bis 6 zu erhalten (um z. B. einen Würfelwurf zu simulieren), können wir zum Ergebnis von nextInt(6) noch 1 addieren. Um eine Gleitkommazahl in einem grösseren Bereich als [0,1[ zu erhalten, kann mit einer Multiplikation der Zufallszahl das Intervall vergrössert werden (z. B. erzeugt der Ausdruck 100 * nextFloat() eine zufällige Gleitkommazahl im Intervall [0,100[). Beispiel 3.9 In Abb. 3.7 wird in der Methode main der Klasse RandomNumbers ein Objekt vom Typ Random instanziiert und der Referenzvariablen random zugewiesen. Danach werden mit Hilfe der Methoden der Klasse Random zufällige Zahlen in unterschiedlichen Intervallen erzeugt.

3.5

Die Klasse Random

67

Abb. 3.7 Mit einem Objekt vom Typ Random können Zufallszahlen in unterschiedlichen Intervallen generiert werden

Eine mögliche Ausgabe des Programmes lautet: Eine zufällige ganze Zahl: 424791840 0 oder 1?: 0 Zufallszahl zwischen 0 und 9: 0 Zufallszahl zwischen 1 und 10: 6 Zufallszahl zwischen -10 und +10: -3 Zufallszahl aus [0.0, 1.0[: 0.777391791343689 Zufallszahl aus [0.0, 5.0[: 2.8571033477783203

Beispiel 3.10 Die Klasse GuessingGame in Abb. 3.8 demonstriert den Einsatz der beiden Klassen Scanner und Random (nach einer Idee aus [1]). Die Idee des Programmes ist, dass der Computer eine Zufallszahl zwischen 1 und 3 generiert und der Variablen answer

68

3 Klassen des Java API Verwenden (Teil 1)

Abb. 3.8 Die Klasse GuessingGame demonstriert den Einsatz der beiden Klassen Scanner und Random. →Video K-3.4

zuweist. Danach wird die geratene Zahl des Benutzers eingelesen und der Variablen guess zugewiesen. Wenn die getippte Zahl des Benutzers der zuvor generierten Zufallszahl entspricht, wird ein entsprechender Text ausgegeben. Wenn nicht, werden zwei Ausgaben generiert (gesammelt in einer Blockanweisung). Erstens wird Leider daneben. und zweitens die tatsächlich gesuchte Zahl ausgegeben.

3.6

Kopieren von Objekten – Aliase

Wie zu Beginn dieses Kapitels erläutert, speichern Objektvariablen Adressen (oder Referenzen) und nicht das Objekt selber – aus diesem Grund ist bei der Arbeit mit Objektvariablen manchmal Vorsicht geboten – dies gilt insbesondere beim Kopieren von Variablen.

3.6

Kopieren von Objekten – Aliase

69

Betrachten wir zunächst den Effekt einer Variablenzuweisung und des Kopierens im Fall von primitiven Datentypen. Beispiel 3.11 Wir deklarieren und initialisieren zwei int Variablen i und j mit den Werten 17 und 99: int i = 17; int j = 99;

Danach weisen wir eine Kopie des Wertes, der in i gespeichert ist, der Variablen j zu. j = i;

Der ursprüngliche Wert 99 der Variablen j wird mit dem Wert 17 überschrieben. Die beiden Variablen i und j beinhalten nun den Wert 17, befinden sich aber weiterhin an zwei separaten und unabhängigen Stellen im Speicher. Dies wird insbesondere deutlich, wenn wir nun eine der beiden Variablen durch eine Zuweisung verändern. Zum Beispiel mit i = 56892429;

Nach dieser Zuweisung speichert die Variable j weiterhin den Wert 17. Dies wird auch mit der Illustration in Abb. 3.9 deutlich. Im folgenden Beispiel machen wir im Wesentlichen das selbe wie oben – aber dieses mal mit Objektvariablen. Zur Illustration verwenden wir hierzu Objektvariablen der Klasse Point aus dem Package java.awt (man könnte hierzu aber (fast) jede andere Klasse aus dem Java API verwenden). Objekte der Klasse Point repräsentieren Punkte, die eine Position in einem (x, y)Koordinatenraum darstellen. Der Konstruktor der Klasse Point erwartet zwei ganze Zahlen als Parameter, welche die x- und die y-Koordinate des Punktes repräsentieren. Mit folgender Programmieranweisung instanziieren wir z. B. einen Punkt an Position (3, 4): Point point1 = new Point(3, 4);

i

17

j

99

j = i;

i

17

j

17

i = 56892429;

i

56892429

j

17

Abb. 3.9 Kopieren und Verändern von Variablen im Fall primitiver Datentypen

70

3 Klassen des Java API Verwenden (Teil 1)

Mit der Methode setLocation, welche ebenfalls zwei ganze Zahlen als Parameter entgegennimmt, können wir die Position eines instanziierten Objektes vom Typ Point nachträglich anpassen. Zum Beispiel können wir den eben gerade instanziierten Punkt point1 an die Position (99, 99) verschieben: point1.setLocation(99, 99);

Beispiel 3.12 Wir deklarieren und initialisieren zunächst zwei Objektvariablen point1 und point2 vom Typ Point folgendermassen: Point point1 = new Point(3,4); Point point2 = new Point(-1,1);

Wir weisen im Folgenden eine Kopie des Wertes, der in point1 gespeichert ist, der Variablen point2 zu. point2 = point1;

Der ursprüngliche Wert der Variablen point2 wird überschrieben mit dem Wert der in der Objektvariablen point1 gespeichert ist. Objektvariablen beinhalten aber nur eine Adresse des Objektes (und nicht das Objekt selber). Das heisst, in diesem Fall wird lediglich die Adresse auf das Objekt, aber nicht das Objekt selber kopiert. Ursprünglich haben die beiden Variablen zwei unterschiedliche Objekte referenziert – nach der Zuweisung point2 = point1; beinhalten aber beide Variablen die gleiche Adresse und zeigen demnach auf das gleiche Objekt (siehe auch Abb. 3.10). Die beiden Variablen point1 und point2 sind jetzt Aliase voneinander, da diese auf das selbe Objekt zeigen. Aliase können unter Umständen unerwünschte Effekte produzieren: Wenn wir eines der beiden Objekte ändern, so ändern wir die andere Referenz genau so (weil sie auf ein und dasselbe Objekt zeigen). Dies wird insbesondere deutlich, wenn wir nun eine der beiden Variablen verändern. Zum Beispiel mit point1.setLocation(99, 99);

Nach Ausführung dieser Programmieranweisung sind die Koordinaten der Objektvariablen point2 ebenso auf (99, 99) geändert. Auf das Objekt vom Typ Point an Position (-1, 1) zeigt nun keine Objektvariable mehr. Wenn die letzte Referenz auf ein Objekt gelöscht wird (also wenn keine Variable mehr auf das Objekt zeigt), wird das entsprechende Objekt automatisch als Müll (engl. garbage) markiert und somit zum Kandidaten für die sogenannte garbage collection. Von Zeit zu

3.6

Kopieren von Objekten – Aliase

point1

(3, 4)

point2

(-1, 1)

71

point2 = point1;

point1

(3, 4)

point2

(-1, 1)

point1.setLocation(99,99);

point1

point2

(99, 99)

(-1, 1) garbage

Abb. 3.10 Kopieren und Verändern von Variablen im Fall von Objektvariablen

Zeit führt Java zur Laufzeit eines Programmes automatisch eine Methode aus, die alle mit garbage markierten Objekte sammelt und deren Speicher für das System wieder frei gibt. Bleibt noch die Frage, wie wir eine unabhängig Kopie eines Objektes erstellen können, ohne Aliase zu erzeugen. Eine Möglichkeit ist, dass alle relevanten Variablen des zu kopierenden Objektes einzeln kopiert werden. Statt also das ganze Objekt mit point2 = point1;

zu kopieren, könnten wir in diesem Beispiel zunächst die (x, y)-Koordinaten aus point1 auslesen und diese in zwei Variablen x und y vom Typ int zwischenspeichern (hierzu bietet die Klasse Point die Methoden getX() und getY() an). Mit diesen Angaben und der Methode setLocation kann nun das Objekt point2 verschoben werden: double x = point1.getX(); double y = point1.getY(); point2.setLocation(x, y);

Beide Punkte point1 und point2 besitzen jetzt zwar auch die gleichen Koordinaten (sind also Kopien voneinander), sind aber keine Aliase – das heisst, wenn wir nun einer der beiden Punkte verändern, verändern wir wirklich nur dieses Objekt – das Vorgehen und die Effekte sind in Abb. 3.11 illustriert.

72

3 Klassen des Java API Verwenden (Teil 1)

point1

(3, 4)

point2

(-1, 1)

double x = point1.getX(); double y = point1.getY(); point2.setLocation(x, y);

point1

(3, 4)

point2

(3, 4)

point1.setLocation(99,99);

point1

(99, 99)

point2

(3, 4)

Abb. 3.11 Unabhängige Kopien von Objekten zu erstellen ist möglich, aber unter Umständen mit etwas grösserem Aufwand verbunden

Aufgaben und Übungen zu Kap. 3 Theorieaufgaben 1. Der new-Operator zusammen mit dem Konstruktor einer Klasse erledigt zwei Dinge. Was genau? 2. Informieren Sie sich in der Java API Dokumentation über die Klasse ArrayList (welche eine Liste für Objekte repräsentiert). • • • • •

Wie instanziieren Sie eine solche Liste? Wie fügen Sie ein Objekt zur Liste hinzu? Wie greifen Sie auf ein Objekt an Position i zu? Wie löschen Sie den gesamten Inhalt der Liste? Wie können Sie überprüfen, ob ein bestimmtes Objekt in der Liste vorhanden ist?

3. Erläutern Sie anhand der Klasse String und des Objektes "String" den Unterschied zwischen Klasse und Objekt. 4. Welche Ausgabe erzeugen folgende Anweisungen? String testString = "Think different"; System.out.println(testString.length()); System.out.println(testString.substring(0,4)); System.out.println(testString.toUpperCase());

3.6

Kopieren von Objekten – Aliase

73

System.out.println(testString.charAt(7)); System.out.println(testString); 5. Welche Ausgabe erzeugen folgende Anweisungen? String s1 = "Lionel"; String s2 = "Cristiano"; String s3 = "Mohamed"; System.out.println(s1); System.out.println(s2.replace(’C’, ’T’)); System.out.println(s2); s3 = s2.concat(s1); System.out.println(s2); System.out.println(s3); System.out.println(s1.charAt(2)); s3 = s3.substring(0, 4); System.out.println(s3.toUpperCase()); System.out.println(s3.length()); 6. Gegeben sei eine Objektvariable vom Typ Random mit Bezeichner rand. In welchen Intervallen suchen die folgenden Anweisungen eine Zufallszahl? • • • • • •

rand.nextInt(5); rand.nextInt(100) + 1; rand.nextInt(51) + 100; rand.nextInt(10) - 5; rand.nextInt(3) - 3; rand.nextInt(23) + 11;

7. Gegeben sei eine Objektvariable vom Typ Random mit Bezeichner rand. Schreiben Sie je eine Programmieranweisung, die Ihnen eine ganzzahlige Zufallszahl aus folgenden Intervallen erzeugt: • [0,100]; [1,3]; [5, 10]; [-10,0] 8. Was sind Aliase und weshalb können Aliase problematisch sein? Java Übungen 1. Erweitern Sie alle Programme aus den Übungen aus Kap. 2 so, dass diese Eingaben vom Benutzer entgegennehmen können. D.h. statt dass die wichtigen Angaben des Programmes jeweils direkt im Quellcode definiert werden, sollen diese vom Benutzer zur Laufzeit eingegeben werden können (also die zwei Operanden für die arithmetischen Operationen, die Zahl zur Überprüfung gerade/ungerade, die Temperatur und die Zahl

74

3 Klassen des Java API Verwenden (Teil 1)

zur Überprüfung grösser, kleiner, gleich dem Schwellwert). →Video V-3.1 2. Schreiben Sie ein Programm, das nach einem Namen fragt und dann folgende Ausgaben generiert: Die Länge des eingegeben Namens (also die Anzahl Zeichen), die ersten zwei und der letzte Buchstabe des Namens sowie der Name in Grossbuchstaben. →Video V-3.2 3. Schreiben Sie ein Programm, das nach einem Namen und einem Alter (als Zahl) fragt. Danach soll folgende Ausgabe generiert werden (die Benutzereingaben sollen die fett markierten Inhalte ersetzen): Hallo, mein Name ist name und bin age Jahre alt. →Video V-3.3 4. Schreiben Sie ein Programm, das nach einer Zeitspanne in Stunden, Minuten und Sekunden fragt und diese drei Angaben via Tastatur einliest. Danach rechnet Ihr Programm die so definierte Zeitspanne in Sekunden um und gibt das Resultat aus. →Video V-3.4 5. Schreiben Sie ein Programm, das nach der Länge und Breite eines Rechtecks fragt und danach die Fläche und den Umfang des Rechtecks berechnet und ausgibt. Zusätzlich soll Ihr Programm feststellen, ob es sich beim definierten Rechteck um ein Quadrat handelt oder nicht und eine entsprechende Ausgabe erzeugen. →Video V-3.5 6. Schreiben Sie ein Programm, das eine zufällige Additionsaufgabe mit zwei positiven Zahlen anzeigt. Die Summe der beiden Zahlen darf maximal 20 betragen. Der Benutzer soll dann ein Ergebnis eingeben können und das Programm soll überprüfen, ob die Eingabe korrekt war oder nicht und eine entsprechende Rückmeldung ausgeben. →Video V-3.6

Literatur 1. John Lewis and William Loftus. Java Software Solutions - Foundations of Program Design. Pearson Global Edition, 8th edition edition, 2015.

4

Eigene Klassen Programmieren (Teil 1)

4.1

Einführung

Im vorherigen Kapitel haben wir exemplarisch vier Klassen verwendet, die uns das Java API zur Verfügung stellt (die Klassen String, Scanner, Random und Point). Wir haben aus den Klassen Objekte instanziiert und die instanziierten Objekte bzw. deren Funktionalitäten verwendet (um z. B. Zeichenketten zu manipulieren, um Eingaben vom Benutzer entgegenzunehmen oder um Zufallszahlen zu generieren). Obschon das Java API viele nützliche Klassen zur Verfügung stellt, ist der Kern der objektorientierten Programmierung das Definieren und Programmieren eigener Klassen, welche die spezifischen Bedürfnisse an ein zu erstellendes Programm erfüllen. Wie Sie bereits wissen, sind Klassen eine Art Bauplan, aus denen Objekte generiert werden können. Die Klasse repräsentiert das allgemeine Konzept der Objekte und umgekehrt ist jedes Objekt eine konkrete Realisierung des allgemeinen Konzeptes. Z. B. repräsentiert die Klasse String das Konzept einer Zeichenkette und jedes Objekt vom Typ String repräsentiert eine spezifische Zeichenkette ("Hallo" oder "Welt" oder "Das ist ein Satz." sind verschiedene Objekte der Klasse String). Wir betrachten ein anderes Beispiel. Nehmen wir an, wir wollen in unserem Programm das Konzept eines Mitarbeitenden modellieren. Hierzu definieren wir in Java eine Klasse Employee, welche „nur“ das Modell eines Mitarbeitenden repräsentiert. Jedes instanziierte Objekt vom Typ Employee repräsentiert dann einen tatsächlich existierenden Mitarbeitenden (z. B. die Programmiererin Anna Muster mit Personalnummer 1234 oder den Projektleiter Hans Meier mit Personalnummer 2435). In einem Programm zur Verwaltung von Mitarbeitenden programmieren wir also eine Klasse Employee, aus der dann zur Laufzeit des Programmes mehrere Objekte vom Typ Employee instanziiert werden können. Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_4. © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_4

75

76

4 Eigene Klassen Programmieren (Teil 1)

Jedes Objekt besitzt einen Zustand, der durch die Werte der zum Objekt gehörenden Eigenschaften definiert ist. Mögliche Eigenschaften eines Mitarbeitenden wären z. B. Name, Adresse, Funktion, Gehalt, etc. In Java werden die Eigenschaften der Objekte als Variablen, die innerhalb der Klassen deklariert sind, programmiert. Indem wir die gewünschten Variablen in der Klasse Employee programmieren, geben wir vor, dass jedes Objekt vom Typ Employee eigene Werte für diese Eigenschaften speichern kann. Ein Objekt besitzt neben Eigenschaften oftmals auch ein Verhalten oder Funktionalitäten, welche das Objekt ausführen kann. Mögliche Funktionalitäten eines Objektes vom Typ Employee könnten sein: • • • • • •

Ändern der Adresse Ausgeben des Namens Berechnen der Sozialabgaben (basierend auf dem Gehalt) Ändern der Funktion Anpassen des Gehalts etc.

In Java wird die Funktionalität von Objekten mit Methoden innerhalb der entsprechenden Klasse programmiert. In der Klasse Employee programmieren wir also Methoden, welche dann auf (oder durch) ein gegebenes Objekt vom Typ Employee ausgeführt werden. Beispiel 4.1 Nehmen wir an, dass wir die Klasse Employee programmiert haben. In Employee haben wir zudem die Variable salary und die Methode changeSalary programmiert, mit der man das Gehalt des Mitarbeitenden (also die Variable salary) auf einen neuen Wert setzen kann. Ist ein Objekt emp1 aus Employee instanziiert, so können wir – wie gewohnt – mit dem Punkt-Operator die Methode aufrufen: emp1.changeSalary(150000);

In obigem Beispiel gehen wir davon aus, dass die Variable salary in der Methode changeSalary angepasst wird. Tatsächlich verändern Methoden oftmals die Variablen des Objektes (wie wir gleich sehen werden).

4.2

Aufbau einer Java Klasse

In all unseren bisherigen Beispielprogrammen haben wir jeweils eine einzige Klasse geschrieben, die eine einzige Methode enthalten hat (nämlich die Methode main). Diese Klassen repräsentieren kleine, komplette Programme. In diesen Programmen haben wir Objekte aus Klassen des Java APIs instanziiert und anschliessend die Methoden genutzt, die uns diese Objekte anbieten.

4.2

Aufbau einer Java Klasse

77

Die bereits geschriebenen Klassen des Java APIs waren immer Teil unserer Programme – aber wir haben uns nie explizit um diese gekümmert, wir haben sie nur benutzt (in der Hoffnung, dass diese die Funktionalität, welche sie versprechen, auch tatsächlich korrekt ausführen). Betrachten Sie die Klasse RollTheDice in Abb. 4.1, die ähnlich aufgebaut ist, wie unsere bisherigen Beispiele. Das heisst, diese Klasse beinhaltet auch nur die Methode main. Zunächst wird ein Objekt vom Typ Scanner instanziiert, um Eingaben vom Benutzer entgegennehmen zu können. Auf Zeile 14 wird dann ein Objekt dice vom Typ Dice mit dem new-Operator und dem zugehörigen Konstruktor Dice instanziiert – das Objekt dice repräsentiert einen herkömmlichen Spielwürfel mit sechs Seiten (und Punktezahlen von 1 bis 6). Auf den Zeilen 16 bis 18 wird der Benutzer über seine Möglichkeiten informiert und seine Entscheidung wird entgegengenommen (und der Variablen decision zugewiesen). Entscheidet sich der Benutzer für die Option 1, so wird der Würfel dice geworfen – hierzu existiert offenbar eine Methode roll, die auf Objekten vom Typ Dice aufgerufen werden

Abb. 4.1 Die Klasse RollTheDice

78

4 Eigene Klassen Programmieren (Teil 1)

kann. Entscheidet sich der Benutzer hingegen für Option 2, so kann dieser eine beliebige Punktezahl eingeben, auf die der Würfel gedreht werden soll – hierzu existiert scheinbar eine Methode setPoints, die eine ganze Zahl als Parameter entgegennimmt. In beiden Fällen wird zum Schluss des Programmes ausgegeben, was der Würfel dice nun aktuell anzeigt – dies geschieht mit der Methode getPoints. Eine mögliche Ein- und Ausgabe wäre demnach: Was wollen Sie mit dem Würfel machen? 1=Werfen; 2=Drehen 1 Würfel wird geworfen... Der Würfel zeigt nun: 5

Alternativ ist folgende Ein- und Ausgabe möglich: Was wollen Sie mit dem Würfel machen? 1=Werfen; 2=Drehen 2 Gewünschte Punktezahl für den Würfel eingeben: 4 Der Würfel zeigt nun: 4

Der Unterschied zwischen diesem Beispiel und den Beispielen aus den vorangegangenen Kapiteln ist, dass die Klasse Dice nicht Teil des Java APIs ist (so wie zum Beispiel die Klassen String, Point, Random, Scanner, etc.). Wir müssen die Klasse Dice selber programmieren (ansonsten kann die Klasse RollTheDice nicht kompiliert werden). Jede Klasse kann Deklarationen von Variablen und Methoden enthalten (siehe Abb. 4.2). Die Variablen repräsentieren die Eigenschaften, die in jedem Objekt gespeichert werden können. Die Methoden definieren die Funktionalität, welche die Objekte dieses Typs zur Verfügung stellen. Die Klassen, die wir bisher selber geschrieben haben, folgen auch diesem Muster. Der Unterschied ist, dass wir bisher keine Variablen ausserhalb von Methoden deklariert haben und dass wir immer nur eine einzige Methode (nämlich die Methode main) programmiert haben. Wir werden auch in den weiteren Beispielen immer wieder solche Klassen programmieren, um den Startpunkt und den Ablauf unserer Programme zu definieren. Solche Klassen nennt man auch Treiberklassen. Kernstück der objektorientierten Programmierung ist es, Klassen zu programmieren, die Objekte und deren Eigenschaften und Funktionalitäten modellieren – und zwar mit Variablen und Methoden. Ein Objekt vom Typ Dice zum Beispiel soll die maximal zulässige Punktezahl sowie die aktuell gezeigte Punktezahl des Würfels in Variablen speichern. Objekte vom Typ Dice sollen zudem Methoden zur Verfügung stellen, die den Würfel werfen, den Würfel manuell auf eine beliebige Seite drehen und die aktuelle Punktezahl des Würfels auslesen.

4.2

Aufbau einer Java Klasse

79

Abb. 4.2 Klassen enthalten Deklarationen von Variablen und Methoden

public class

Klassenname

{

Instanzvariable 1 Instanzvariable 2 Instanzvariable 3

Methode 1

Methode 2

Methode 3

}

In Abb. 4.3 ist die Klasse Dice gezeigt, welche diese Anforderungen erfüllt (nach einer Idee aus [1]). In den Beispielen, die wir in diesem Buch erarbeiten, werden wir jede Klasse in einer eigenen Datei speichern1 . Das bedeutet in diesem Beispiel, dass wir die Klasse RollTheDice in der Datei RollTheDice.java und die Klasse Dice in einer separaten Datei Dice.java speichern. Die Klasse Dice enthält drei Deklarationen von Variablen mit den Bezeichnern MAX, points und randomGenerator. Daneben beinhaltet die Klasse fünf Methoden mit den Bezeichnern Dice, roll, setPoints, getPoints und printMessage. Beachten Sie, dass oberhalb jeder Variablen und jeder Methode in einem Kommentar jeweils der Zweck der Variable oder der Methode knapp erläutert wird. Diese Kommentare helfen Programmiererinnen, den Quellcode schneller zu verstehen. Zudem kann man den Quellcode mit Kommentaren formatieren und strukturieren und so eine bessere Lesbarkeit erreichen. Anhand der Kommentare wissen wir, dass die Konstante MAX die maximal mögliche Punktezahl des Würfels definiert, die Variable points die aktuelle Punktezahl des Würfels speichert und die Variable randomGenerator verwendet werden kann, um Zufallszahlen zu generieren. Zudem sehen wir anhand der Kommentare, dass die Methode Dice ein Objekt vom Typ Dice instanziieren kann und die Methode roll die Punktezahl des Würfels mit einer Zufallszahl neu definiert. Mit der Methode setPoints kann man die Punktezahl des Würfels manuell definieren und die aktuelle Punktezahl des Würfels kann man mit der 1 Java erlaubt es grundsätzlich mehrere Klassen in einer Datei zu speichern. Wenn eine Datei mehrere

Klassen enthält, darf nur eine dieser Klassen mit dem reservierten Wort public deklariert werden. Ferner muss der Bezeichner der public Klasse dem Namen der Datei entsprechen.

80

Abb. 4.3 Die Klasse Dice. →Video K-4.1

4 Eigene Klassen Programmieren (Teil 1)

4.3

Instanzvariablen und Sichtbarkeitsmodifikatoren

81

Methode getPoints auslesen. Schliesslich ist die Methode printMessage dazu da, eine Systemmeldung zu generieren. In den folgenden Abschnitten wollen wir nun wichtige Konzepte der objektorientierten Programmierung anhand der Klasse Dice und deren Variablen und Methoden einführen und besprechen.

4.3

Instanzvariablen und Sichtbarkeitsmodifikatoren

Bis jetzt haben wir Variablen immer innerhalb von Methoden deklariert. Wird eine Variable innerhalb einer Methode deklariert, so kann auch nur innerhalb dieser Methode auf sie zugegriffen werden. Solche Variablen nennt man lokale Variablen. In der Klasse Dice ist sowohl die Konstante MAX als auch die Variablen points und randomGenerator innerhalb der Klasse, aber nicht innerhalb einer Methode deklariert. Solche Variablen nennt man Instanzvariablen. Instanzvariablen können von jeder Methode der entsprechenden Klasse verwendet werden. Wo also eine Variable deklariert wird, definiert den Bereich, in dem diese tatsächlich benutzt werden kann. Variablen wie z. B. points nennt man Instanzvariablen, weil für jede Instanz der Klasse eigener Speicherplatz für die Variable reserviert wird. Mit anderen Worten, jedes instanziierte Objekt vom Typ Dice besitzt eine eigene, unabhängige Variable points. So kann jedes Objekt seinen eigenen Zustand besitzen. Beispiel 4.2 Die Unabhängigkeit von Instanzvariablen von Objekten des gleichen Typs wird im Programm in Abb. 4.4 illustriert. In der Methode main werden drei Objekte vom Typ Dice instanziiert – die ersten beiden Würfel werden mit der Methode roll geworfen und der dritte Würfel wird auf die Seite mit der Punktezahl 6 gedreht. Eine mögliche Ausgabe des Programms lautet: Würfel 1: 2 Würfel 2: 5 Würfel 3: 6

Ein Würfel besitzt also aktuell z. B. die Punktezahl 5, während ein anderer Würfel aktuell 2 oder 6 in der Variablen points speichert. Instanzvariablen sollten nur durch das betreffende Objekt gelesen oder verändert werden können. Das heisst zum Beispiel, dass nur die Methoden der Klasse Dice den aktuellen Punktewert – also die Variable points – lesen oder ändern können sollten. Wir sollten es für Quellcode ausserhalb einer Klasse schwierig (oder sogar unmöglich) machen, in ein Objekt „hineinzugreifen“ und dort Werte von Variablen direkt zu ändern oder auszulesen [1]. Diese Strategie nennt man Kapselung.

82

4 Eigene Klassen Programmieren (Teil 1)

Abb. 4.4 Die Klasse DiceTester illustriert die Unabhängigkeit von Instanzvariablen von Objekten des gleichen Typs

Sogenannte Sichtbarkeitsmodifikatoren (engl. Visibility Modifier) kontrollieren die Kapselung der Instanzvariablen und die Zugriffsmöglichkeiten auf die Methoden einer Klasse. In Java werden hierzu die Sichtbarkeiten public und private verwendet2 . Hat eine Variable oder eine Methode die Sichtbarkeit public, so können diese auch von ausserhalb der Klasse referenziert werden (die Variable oder die Methode ist also über Klassengrenzen hinweg sichtbar). Besitzt ein Variable oder eine Methode hingegen die Sichtbarkeit private, so kann diese nur innerhalb der Klasse referenziert werden – ausserhalb der Klasse bleiben diese Variablen oder Methoden unsichtbar (können also nicht verwendet werden). Eine mit public modifizierte Variable verletzt das Prinzip der Kapselung – Quellcode ausserhalb der Klasse besitzt direkten Zugriff auf diese Variable und der Wert der Variablen kann somit unkontrolliert ausgelesen, gelöscht oder verändert werden. Instanzvariablen (also Variablen die innerhalb einer Klasse, aber nicht innerhalb einer Methode deklariert sind) sollten Sie deshalb i. d. R. mit dem Sichtbarkeitsmodifikator private deklarieren. Kapselung verlangt, dass Daten von Aussen nicht direkt veränderbar oder einsehbar sein sollen – Konstanten können aber per Definition nicht verändert werden (wegen dem Modifikator final in der Deklaration). Zudem müssen Konstanten oftmals von verschiedenen Klassen verwendet werden können. Es ist deshalb im allgemeinen akzeptabel, wenn man Konstanten mit dem Sichtbarkeitsmodifikator public deklariert. 2 Es existiert noch ein dritter Sichtbarkeitsmodifikator, protected, der relevant ist für das Konzept der Vererbung und den wir in späteren Kapiteln einführen werden.

4.3

Instanzvariablen und Sichtbarkeitsmodifikatoren

83

Beispiel 4.3 In der Klasse Dice haben wir die Konstante MAX mit Sichtbarkeit public deklariert – die Instanzvariablen points und randomGenerator hingegen sind durch den Sichtbarkeitsmodifikator private gegen Aussen gekapselt – siehe Abb. 4.5. Da sich die drei Deklarationen nicht innerhalb einer Methode befinden, sind dies nicht lokale Variablen sondern Instanzvariablen und somit haben alle Methoden der Klasse Dice Zugriff auf diese. Der Sichtbarkeitsmodifikator, den wir auf Methoden anwenden, hängt vom Zweck der Methode ab. Methoden, die Funktionalitäten für andere Objekte anbieten, müssen mit dem Modifikator public deklariert werden, so dass diese von Aussen aufgerufen werden können. Diese Methoden nennt man auch Service Methoden. Eine mit Sichtbarkeit private deklarierte Methode kann hingegen nicht von ausserhalb der Klasse aufgerufen werden. Der einzige Zweck einer so deklarierten Methode ist die Unterstützung anderer Methoden der selben Klasse. Man nennt diese Methoden auch Support Methoden. Beispiel 4.4 In der Klasse Dice sind die Methoden Dice, roll, setPoints und getPoints mit Sichtbarkeit public deklariert – dies sind die Service-Methoden, die auch ausserhalb der Klasse Dice aufrufbar sind. Tatsächlich haben wir diese ja auch von ausserhalb, nämlich von der Klasse RollTheDice, aufgerufen – z. B. mit dice.roll(). Die Methode printMessage hingegen, unterstützt die Methode setPoints und soll gegen Aussen nicht sichtbar sein – dies ist also eine Support-Methode. Es ist somit zum Beispiel nicht möglich, die Anweisung dice.printMessage("Nachricht") in der Klasse RollTheDice auszuführen. In Abb. 4.6 sind die Effekte der Sichtbarkeiten public, private und protected auf Variablen (bzw. Konstanten) und Methoden zusammengefasst.

Abb. 4.5 Die Instanzvariablen der Klasse Dice

84

4 Eigene Klassen Programmieren (Teil 1)

Abb. 4.6 Die Effekte der Sichtbarkeitsmodifikatoren public, private und protected auf Variablen (bzw. Konstanten) und Methoden zusammengefasst

public

private

protected

4.4

Variablen

Konstanten

Methoden

Verletzt das Prinzip der Kapselung

Konstante ist gegen Aussen sichtbar

gegen Aussen

Kapselung der Variablen

Kapselung der Konstanten

Bieten

Methoden dieser Klasse

Teilweise Kapselung bleibt erhalten

Methoden Aufrufen und dem Kontrollfluss Folgen

Eine Methode ist eine Gruppierung von Programmieranweisungen, die unter einem Bezeichner zusammengefasst werden. Eine Methode definiert innerhalb des Methodenrumpfes alle Programmieranweisungen, die ausgeführt werden sollen, wenn die Methode über den entsprechenden Methodennamen aufgerufen wird. In Java sind Methoden immer Teil einer bestimmten Klasse – Sie können also Methoden nicht ausserhalb von Klassen programmieren. Wird eine Methode aufgerufen, so wird der sogenannte Kontrollfluss des Programms an diese Methode abgegeben. Jetzt werden die Programmieranweisungen dieser Methode ausgeführt. Sind alle Anweisungen, die in der Methode zusammenfasst sind, ausgeführt, kehrt die Kontrolle zum Punkt des Programmes zurück, wo die Methode aufgerufen wurde. Die Methodenaufrufe befinden sich in den meisten Fällen innerhalb anderer Methoden. Methoden können also zwei Rollen einnehmen: aufrufend oder aufgerufen. Rufen wir z. B. in der Methode main die Methode setPoints auf, so ist main in diesem Fall die aufrufenden Methode und setPoints die aufgerufene Methode. Wenn sich die aufrufende und die aufgerufene Methode in der selben Klasse befinden, so ist für den Aufruf nur der Bezeichner der Methode nötig. Wenn die aufgerufene Methode aber Teil einer anderen Klasse ist als die aufrufende Methode, so ist neben dem Bezeichner der Methode eine Objektvariable und der Punkt-Operator zum Aufruf nötig [1]. Tatsächlich haben wir solche Methodenaufrufe über Klassengrenzen hinweg schon mehrfach angewendet: Zum Beispiel mit den Methodenaufrufen scan.next() oder string.toUpperCase(), etc. Beispiel 4.5 In Abb. 4.7 ist ein möglicher Verlauf des Kontrollflusses für das Programm RollTheDice abstrahiert. Bei jedem Aufruf einer Methode springt der Kontrollfluss aus der aufrufenden Methode zur aufgerufenen Methode.

4.4

Methoden Aufrufen und dem Kontrollfluss Folgen

85

RollTheDice main 1

9

2

8 Objekt dice instanziiert aus Klasse Dice setPoints 3

4

7 printMessage

6

5

Abb. 4.7 Der Kontrollfluss eines Programmes folgt den Aufrufen der Methoden

Aus der Methode main in der Klasse RollTheDice zum Beispiel wird die Methode setPoints auf dem instanziierten Objekt dice aufgerufen. Der Kontrollfluss springt jetzt also aus main raus in die Methode setPoints. Die Methode setPoints wiederum ruft – je nach Eingabe – die Methode printMessage auf, die sich in der gleichen Klasse befindet – hierzu reicht der Bezeichner der Methode aus. Immer wenn alle Anweisungen einer aufgerufenen Methode ausgeführt sind, kehrt der Kontrollfluss zur aufrufenden Methode zurück. Befinden sich die aufrufende und die aufgerufene Methode in der gleichen Klasse, so wird häufig mit der this-Referenz gearbeitet. Das heisst, statt dem Methodennamen – z. B. printMessage() – wird this.printMessage() geschrieben, um deutlich zu machen, dass sich die aufgerufene Methode auch in diesem Objekt befindet. Wir kommen später in diesem Kapitel nochmals auf das reservierte Wort this zurück.

86

4.5

4 Eigene Klassen Programmieren (Teil 1)

Der Methodenkopf

Eine Methode ist definiert durch einen Methodenkopf und einen Methodenrumpf (auch Methodenblock genannt). Der Methodenkopf umfasst: 1. Einen Sichtbarkeitsmodifikator (z. B. public oder private) 2. Den Datentypen der Rückgabe (z. B. int, double, String, Dice, etc.) oder das reservierte Wort void, das angibt, dass diese Methode keine Rückgabe erzeugt. 3. Einen Bezeichner der Methode (z. B. roll oder printMessage). Zur Erinnerung: Methodennamen beginnen nach gängiger Konvention mit einem Kleinbuchstaben und jedes interne Wort beginnt dann wieder mit einem Grossbuchstaben. 4. Nach dem Bezeichner der Methode folgen optional eine Reihe von Parametern in Klammern () (die Klammern sind auch dann nötig, wenn keine Parameter übergeben werden). Jeder Parameter wird beschrieben durch einen Datentypen und einen Bezeichner. Mehrere Parameter werden mit einem Komma separiert. Nach dem Methodenkopf folgt der Methodenrumpf, in dem mehrere Programmieranweisungen innerhalb geschweifter Klammern gruppiert werden können. Beispiel 4.6 In Abb. 4.8 sind drei Beispiele von Methodenköpfen gezeigt. 1. Die erste Methode ist auch ausserhalb der Klasse sichtbar (public), während die anderen beiden Methoden nur innerhalb dieser Klasse verwendet werden können (private). 2. Die ersten beiden Methoden geben nichts zurück (der Rückgabetyp ist void), während die letzte Methode eine ganze Zahl zurückgeben wird (der Rückgabetyp ist int).

Datentyp der Rückgabe Sichtbarkeit

Bezeichner der Methode Parameter (inkl. Datentyp)

Abb. 4.8 Drei Beispiele von Methodenköpfen

4.6

Konstruktoren

87

3. Die Bezeichner der drei Methoden sind doThis, make und help. 4. Die erste Methode nimmt keine Parameter entgegen, die zweite Methode nimmt einen Parameter vom Typ String und die dritte Methode zwei Parameter vom Typ int entgegen.

4.6

Konstruktoren

Wenn wir eine eigene Klasse programmieren, dann definieren wir üblicherweise auch einen Konstruktor. Sie erinnern sich, dass Konstruktoren spezielle Methoden sind, die den selben Bezeichner wie die Klasse haben (der Konstruktor der Klasse Scanner hat bspw. den Bezeichner Scanner und der Konstruktor der Klasse Dice muss zwingend Dice heissen). Konstruktoren unterscheiden sich in zwei Punkten von „normalen“ Methoden: 1. Ein Konstruktor besitzt keine Möglichkeit, Werte zurückzugeben und besitzt demnach keinen Rückgabetyp (nicht zu verwechseln mit void). Der Methodenkopf des Konstruktors besteht also nur aus dem Sichtbarkeitsmodifikator (üblicherweise wird für Konstruktoren die Sichtbarkeit public definiert), dem Bezeichner der Methode (z. B. Dice) und ggf. einer Reihe von Parametern in Klammern. 2. Der Konstruktor wird immer zusammen mit dem new-Operator aufgerufen, um ein neues Objekt aus der entsprechenden Klasse zu instanziieren. In unserem Beispiel wäre dies also mit der Anweisung new Dice(); der Fall. Der Konstruktor hat zwei Hauptaufgaben. Erstens soll der Konstruktor die Adresse (die Referenz) auf das eben instanziierte Objekt zurückzugeben, welche man dann z. B. einer deklarierten Objektvariablen zuweisen kann: Dice dice = new Dice();

Zweitens wird der Konstruktor üblicherweise so programmiert, dass dieser die Instanzvariablen initialisiert. Zum Beispiel wird der Konstruktor der Klasse Dice dazu verwendet, die Instanzvariable points mit 1 zu initialisieren und der Variablen randomGenerator ein neu instanziiertes Objekt vom Typ Random zuzuweisen (siehe Abb. 4.9). Beachten Sie die Verwendung des reservierten Wortes this bei beiden Zuweisungen. Das reservierte Wort this erlaubt einem Objekt sich selber zu referenzieren. Eine this Referenz bezieht sich also immer auf dieses Objekt (in diesem Objekt soll die Instanzvariable points mit 1 initialisiert werden und in diesem Objekt soll die Instanzvariable randomGenerator instanziiert werden).

88

4 Eigene Klassen Programmieren (Teil 1)

Abb. 4.9 Der Konstruktor der Klasse Dice initialisert die Instanzvariablen points und randomGenerator

Wir müssen nicht zwingend für jede Klasse, die wir selber programmieren, einen Konstruktor definieren. Jede Klasse besitzt nämlich automatisch einen Standardkonstruktor (ohne Parameter). Der Standardkonstruktor instanziiert Objekte aus der entsprechenden Klasse ohne Instanzvariablen zu initialisieren. Sobald Sie einen eigenen Konstruktor in einer Klasse programmiert haben, kann man den Standardkonstruktor für diese Klasse aber nicht mehr aufrufen.

4.7

Parameter für Methoden

Ein Parameter ist ein Wert, der einer Methode mitgegeben wird, wenn diese aufgerufen wird. Die Parameterliste im Methodenkopf definiert dabei die Typen der übergebenen Werte und deren Bezeichner. Diese Bezeichner können wir dann innerhalb der Methode verwenden, wenn wir die Werte der entsprechenden Parameter benötigen. Die Bezeichner der Parameter im Methodenkopf nennt man formale Parameter. Die Werte, die beim Aufruf an die Methode übergeben werden, nennt man tatsächliche Parameter oder auch Argumente. Formale Parameter sind Bezeichner, die als Variablen innerhalb einer Methode dienen und dessen initiale Werte den Argumenten beim Aufruf der Methode entsprechen. Das heisst, wenn eine Methode aufgerufen wird, wird jedes Argument dem entsprechenden formalen Parameter zugewiesen. Beispiel 4.7 Betrachten Sie z. B. die Methode setPoints(int points) in der Klasse Dice. Der formale Parameter ist points vom Datentyp int. Wird die Methode setPoints aufgerufen, so muss ein Wert oder eine Variable vom Typ int übergeben werden: Z. B. setPoints(4) oder setPoints(p), wobei p eine lokale Variable vom Typ int ist. Der übergebene Wert 4 oder die übergebene Variable p entsprechen den tatsächlichen Parametern (den Argumenten). Diese Werte werden beim Aufruf der Methode dem formalen

4.7

Parameter für Methoden

89

Parameter points zugewiesen. Bei einem Methodenaufruf setPoints(4) geschieht also eigentlich folgende Deklaration und Zuweisung: int points = 4;

Beim Aufruf einer Methode müssen die tatsächlichen Parameter (die Argumente) und die formalen Parameter im Methodenkopf übereinstimmen. Dies muss so sein, weil das erste Argument dem ersten formalen Parameter, das zweite Argument dem zweiten formalen Parameter, etc. zugewiesen wird. Beispiel 4.8 In Abb. 4.10 ist die Zuweisung der tatsächlichen zu den formalen Parametern illustriert. Beim Aufruf der Methode printInfo werden die tatsächlichen Parameter "Kaspar" und 41 an die Methode übergeben. Die tatsächlichen Parameter werden nun den formalen Parametern zugewiesen. In der Methode printInfo geschieht also folgendes: int a = 41; String name = "Kaspar";

Methodenaufruf mit

Die Ausgabe der Methode printInfo würde demnach lauten: Name: Kaspar, Alter: 41 Beachten Sie, dass die tatsächlichen und die formalen Parameter gleiche Bezeichner haben dürfen (z. B. name und name) – dies ist aber nicht zwingend (z. B. age und a)!

Methodenkopf mit formalen Parametern

entsprechenden formalen Parameter zugewiesen

Abb. 4.10 Parameterübergabe durch einen Methodenaufruf: Die Werte der tatsächlichen Parameter werden in die entsprechenden formalen Parameter kopiert

90

4 Eigene Klassen Programmieren (Teil 1)

Abb. 4.11 Lokale Variablen (und somit auch formale Parameter) dürfen den gleichen Bezeichner besitzen wie Instanzvariablen – zur Unterscheidung verwendet man das reservierte Wort this, mit dem dieses Objekt referenziert werden kann

Die formalen Parameter in einem Methodenkopf entsprechen lokalen Variablen. Diese Variablen existieren also nur innerhalb der entsprechenden Methode. Es ist möglich, eine lokale Variable (und somit auch ein formaler Parameter) mit dem gleichen Bezeichner zu deklarieren, wie er bereits für eine Instanzvariable verwendet wird. Zum Beispiel haben wir in der Klasse Dice eine Instanzvariable points deklariert und in der Methode setPoints gibt es eine lokale Variable points (in der Form eines formalen Parameters). Wird innerhalb der Methode setPoints der Bezeichner points verwendet, wird damit immer die lokale Variable angesprochen und nicht die Instanzvariable. Mit dem reservierten Wort this kann man aber erzwingen, dass die Instanzvariable referenziert wird. Das bedeutet also, dass folgende Zuweisung this.points = points;

der Instanzvariablen points dieses Objektes den Wert der lokalen Variablen points zuweist (siehe Abb. 4.11).

4.8

Die return Anweisung

Der Datentyp der Rückgabe, der im Methodenkopf angegeben wird, kann ein primitiver Datentyp sein (z. B. int, char, etc.), eine Klasse (z. B. String, Dice, etc.) oder aber das reservierte Wort void3 .

3 Nicht vergessen: Konstruktoren besitzen als einzige Methoden keinen Rückgabetyp (auch nicht void).

4.8

Die return Anweisung

91

Wird void als Rückgabetyp definiert, dann erzeugt die Methode keine Rückgabe. Zum Beispiel erzeugen die Methoden main oder setPoints in unserem Beispiel keine Rückgaben – der Rückgabetyp dieser Methoden ist deshalb void. Eine Methode, die einen anderen Rückgabetyp als void besitzt, muss mindestens eine return-Anweisung im Methodenrumpf beinhalten. Eine return-Anweisung besteht aus dem reservierten Wort return gefolgt von einem Wert oder einem Ausdruck, der zurückgegeben werden soll. Beachten Sie, dass der Datentyp der Rückgabe dem im Methodenkopf deklarierten Rückgabetyp entsprechen muss (wurde zum Beispiel int als Rückgabetyp definiert, so kann die Methode nicht den Wert 3.16 zurückgeben). Der Datentyp der Rückgabe von getPoints in der Klasse Dice ist zum Beispiel int (siehe Abb. 4.12) – die Methode getPoints gibt in unserem Beispiel this.points zurück. Dies entspricht der aktuell gespeicherten Punktezahl in diesem Würfel. Der Rückgabetyp der Methode roll ist auch int und macht fast dasselbe wie getPoints – bevor aber der aktuelle Wert des Würfels zurückgegeben wird, wird der Wert der Instanzvariablen points noch auf eine neue Zufallszahl zwischen 1 und 6 gesetzt. Wird eine return-Anweisung in einer Methode ausgeführt, so wird der definierte Ausdruck an die aufrufenden Methode zurückgegeben. Die Rückgabe einer Methode kann dann von der aufrufenden Methode entgegengenommen und verwendet werden. Z. B. kann man die Rückgabe ausgeben: System.out.println("Der Würfel zeigt: " + dice.getPoints());

Oder man weist die Rückgabe einer Variablen zu: int currentPoints = dice.getPoints();

Abb. 4.12 Die Methoden getPoints und roll deklarieren int als deren Rückgabetypen

92

4 Eigene Klassen Programmieren (Teil 1)

Die Rückgabe einer Methode kann aber auch ignoriert werden. Das heisst, die Rückgabe muss nicht zwingend von der aufrufenden Methode verarbeitet werden. Zum Beispiel haben wir in der Klasse RollTheDice die Methode roll aufgerufen (mit dice.roll()) und dabei die Rückgabe von roll nicht verwendet. Neben der Rückgabe eines Wertes an die aufrufende Methode hat die return-Anweisung noch einen anderen Effekt: Der Kontrollfluss wird sofort an die aufrufende Methode zurückgegeben. Eine Methode ohne Rückgabe (der Rückgabetyp der Methode ist also void) kehrt zur aufrufenden Methode zurück, sobald das Ende der Methode erreicht wird. Allerdings können Sie in Methoden mit Rückgabe void eine return-Anweisung ohne Rückgabe verwenden: return;

Wird dieses „leere“ return der Methode erreicht, kehrt der Kontrollfluss sofort zur aufrufenden Methode zurück. Die Anweisungen, die unterhalb einer return-Anweisung programmiert sind, werden also nicht mehr ausgeführt.

4.9

Getter und Setter

Wir haben gesehen, dass die Instanzvariablen in Objekten gekapselt sein sollen. Hierzu werden Instanzvariablen mit dem Sichtbarkeitsmodifikator private deklariert und sind somit ausserhalb der Klasse nicht direkt verwendbar. Die Interaktion zwischen diesen gekapselten Daten und anderen Objekten soll/kann nur über die von Ihnen programmierte Menge von Service-Methoden erfolgen – also über die Methoden mit Sichtbarkeit public. Diese Menge an sichtbaren Methoden definieren die Schnittstelle (engl. Interface) zwischen dem Objekt und dem Rest des Quellcodes. Die Prinzipien der Kapselung und der Schnittstellen ist in Abb. 4.13 illustriert. Objekte sind nicht berechtigt, direkt auf die Variablen eines anderen Objektes zuzugreifen (weil diese mit der Sichtbarkeit private deklariert sind). Stattdessen müssen diese die public Methoden des Objektes aufrufen, die wiederum mit den im Objekt gekapselten Variablen interagieren. Methoden wie z. B. getPoints nennt man Getter, da diese das Auslesen einer als private deklarierten Variablen ermöglichen. Die Methode setPoints ermöglicht das Ändern der als private deklarierten Variablen points. Diese Methode überschreibt den aktuellen Wert der Instanzvariablen points mit dem Wert, der dem formalen Parameter points zugewiesen wird. Solche Methoden nennt man auch Setter. Es ist gängige Konvention, dass die Getter und Setter für eine Instanzvariable mit Bezeichner var die Bezeichner getVar bzw. setVar erhalten. Einzige Ausnahme zu dieser Regel bilden die Getter für Instanzvariablen vom Typ boolean – z. B. benennt man den Getter für eine Variable full vom Typ boolean typischerweise mit isFull.

4.9

Getter und Setter

93

public class

private public class

Klasse 1

Klasse 2

{

Instanzvariable

{

private

Instanzvariable

public

Objekte interagieren

Instanzvariable sichtbaren Methoden

Methoden interagieren mit den eigenen Instanzvariablen

public private

Methode 3

}

Methode 1

public class

public Methode 2

Klasse 3

private

Instanzvariable

private

Instanzvariable

{

public

}

Methode 4

}

Abb. 4.13 Objekte interagieren mit anderen Objekten über die public Methoden

Abb. 4.14 Getter und Setter erlauben einem Objekt die alleinige Kontrolle über die Instanzvariablen

Der Nutzen des Umwegs beim Zugriff auf die Instanzvariablen über Getter und Setter ist Kontrolle. Beachten Sie zum Beispiel, dass mit einer if-Anweisung in der Methode setPoints sichergestellt wird, dass die Variable points nur dann geändert wird, wenn der übergebene Parameter points im gültigen Bereich liegt (also in diesem Fall zwischen 1 und 6). Andernfalls wird die Support-Methode printMessage aufgerufen, welche eine Fehlermeldung ausgibt – siehe Abb. 4.14.

94

4 Eigene Klassen Programmieren (Teil 1)

Würden wir den direkten Zugriff auf Instanzvariablen erlauben, könnte man Objekte unzulässig manipulieren oder unzulässigerweise Variablenwerte auslesen. Z. B. könnten wir so in der Methode main in der Klasse RollTheDice folgende Anweisung programmieren: dice.points = 17;

Mit dem Umweg über Getter und Setter haben wir als Programmierer und Programmiererinnen immer die volle Kontrolle darüber, was mit den Instanzvariablen gemacht werden darf und was nicht. Beispiel 4.9 Betrachten Sie z. B. folgende Ein- und Ausgabe für unser Beispielprogramm: Was wollen Sie mit dem Würfel machen? 1=Werfen; 2=Drehen 2 Gewünschte Punktezahl für den Würfel eingeben: 17 Unzulässige Eingabe. Würfel wird nicht verändert. Der Würfel zeigt nun: 1

Die unzulässige Eingabe des Benutzers wird also durch die sorgfältige Programmierung des Setters abgefangen. Betrachten Sie die Methode roll in der Klasse Dice. Diese Methode ändert den Wert der Instanzvariablen points zufällig und liefert dann den aktualisierten Wert der Variablen zurück (arbeitet also ähnlich wie ein Getter). Beachten Sie wiederum, dass der Quellcode der Methode roll dafür besorgt ist, dass der Wert der Variablen points zwar zufällig gewählt wird, aber in jedem Fall zwischen 1 und 6 liegt. Das heisst, die Methode roll ist so konstruiert, dass nur zulässige Änderungen an der Variablen points vorgenommen werden können.

4.10

UML Klassendiagramme

Wir werden in diesem Buch zwischendurch UML Diagramme verwenden, um die Struktur unserer Programme zu visualisieren. UML steht für Unified Modeling Language und ist die populärste Notationssprache für objektorientiertes Programmdesign [2]. Es existieren unterschiedlichste UML Typen – jeder Typ kann für unterschiedliche Aspekte bei der Visualisierung von objektorientierten Programmen genutzt werden. Wir fokussieren uns auf UML Klassendiagramme, um die Inhalte von Klassen und deren Beziehungen zu visualisieren.

4.10

UML Klassendiagramme

95

In einem UML Klassendiagramm wird jede Klasse in einem Rechteck mit drei Bereichen repräsentiert. Der erste Bereich enthält den Klassennamen, der zweite Bereich enthält die Variablen und der dritte Bereich die Methoden der Klasse. In Abb. 4.15 ist das Klassendiagramm für unser RollTheDice Programm gezeigt. Der Pfeil, der die beiden Klassen RollTheDice und Dice miteinander verbindet, zeigt an, dass eine Beziehung zwischen diesen beiden Klassen existiert. Eine gestrichelte Linie mit offenem Pfeilkopf bedeutet, dass die eine Klasse abhängig ist von der anderen Klasse (in diesem Fall verwendet die Klasse RollTheDice Methoden der Klasse Dice). Andere Beziehungstypen werden in UML mit anderen Typen von Pfeilen visualisiert (diese Pfeile werden im weiteren Verlaufe des Buches eingeführt). UML wurde nicht speziell für Java entwickelt (UML ist eine sprachunabhängige Notationsmöglichkeit). Die Syntax in UML Diagrammen ist deshalb nicht identisch mit der Syntax von Java. • In UML Klassendiagrammen werden die Sichtbarkeiten der Variablen und Methoden mit einem Plus (+) für public oder einem Minus (−) für private visualisiert. • Nach dem Bezeichner einer Variablen folgt nach einem Doppelpunkt der zugehörige Datentyp. Optional kann man nach einem Gleichheitszeichen noch einen initialen Wert der Variablen definieren. • Die Parameter einer Methode werden in Klammern nach dem Bezeichner der Methode ebenfalls nach dem Muster (Bezeichner : Datentyp) aufgelistet. • Nach den Parametern in Klammern wird nach einem Doppelpunkt der Rückgabetyp der Methode angegeben. Den Typen void gibt es in UML nicht. Das heisst, bei Methoden ohne Rückgabe wird in UML der Rückgabetyp einfach weggelassen.

Abb. 4.15 Ein UML Klassendiagramm für das Programm RollTheDice

RollTheDice + main(args : String[])

Dice + MAX : int = 6 - points : int - randomGenerator : Random + Dice() + roll() : int + setPoints(points : int) + getPoints() : int - printMessage(message : String)

96

4.11

4 Eigene Klassen Programmieren (Teil 1)

Ein Weiteres Beispiel einer Eigenen Klasse

Wir schauen im Folgenden ein weiteres Beispiel einer eigenen Klasse an. Dieses Beispiel – die Klasse PlayerCard – bildet die Basis für ein Programm, das wir im Rest dieses Buches Schritt für Schritt erweitern werden. Studieren Sie die Klasse PlayerCard in Abb. 4.16. Diese Klasse modelliert das Konzept einer Spielerkarte, die man sammeln und tauschen kann (z. B. Karten von Fussballoder Baseballspielern). Betrachten Sie zuerst die Deklarationen der zwei Instanzvariablen playerName vom Typ String und cardNumber vom Typ int. Für beide Instanzvariablen sind Getter und Setter programmiert. Beachten Sie insbesondere, wie die Methode setCardNumber programmiert ist: Die Variable cardNumber wird nur dann geändert, wenn die neue Kartennummer grösser ist als 0. Ansonsten sind diese Methoden recht ähnlich zu den Gettern und Settern aus dem vorigen Beispiel der Klasse Dice. Natürlich könnten wir jetzt noch weitere Informationen über Spieler in den Karten modellieren (zum Beispiel die Nationalität, die Position, auf der der Spieler spielt, der Name des Stammclubs, der Jahrgang des Spielers, etc.). Wir werden dies in den folgenden Kapiteln zum Teil nachholen. Wichtig ist für den Moment, dass Sie das allgemeine Prinzip verstanden haben – um Vollständigkeit kümmern wir uns vorerst nicht. Im Gegensatz zum Konstruktor der Klasse Dice, besitzt der Konstruktor PlayerCard zwei Parameter (playerName und cardNumber). Das heisst, wenn ein Objekt vom Typ PlayerCard instanziiert werden soll, müssen diese zwei Informationen als Argumente an den Konstruktor übergeben werden. Ein neues Objekt vom Typ PlayerCard kann also folgendermassen deklariert und instanziiert werden: PlayerCard pc1 = new PlayerCard("John", 27); Die Instanzvariablen playerName und cardNumber des Objektes pc1 werden dann im Konstruktor gemäss den übergebenen Parametern initialisiert (mit zwei Zuweisungen). Die Klasse PlayerCard verfügt über zwei Konstruktoren. Über den ersten Konstruktor werden beide Instanzvariablen gemäss den Parametern playerName und cardNumber initialisiert. Es ist aber durchaus vorstellbar, dass wir eine Spielerkarte ohne Kartennummer instanziieren wollen. Hierzu programmieren wir einen weiteren Konstruktor, so dass Instanziierungen auch nur mit dem Namen des Spielers möglich sind und die Kartennummer in diesem Fall standardmässig auf 0 gesetzt wird. Die zwei Konstruktoren in der Klasse PlayerCard unterscheiden sich in der Anzahl formaler Parameter. Welcher der beiden Konstruktoren durch den new-Operator ausgeführt wird, hängt davon ab, wie viele Argumente dem Konstruktor übergeben werden. Mit

4.11

Ein Weiteres Beispiel einer Eigenen Klasse

Abb. 4.16 Die Klasse PlayerCard. →Video K-4.2

97

98

4 Eigene Klassen Programmieren (Teil 1)

new PlayerCard("Mike", 17);

wird zum Beispiel der erste und mit new PlayerCard("Penny");

der zweite Konstruktor aufgerufen. Grundsätzlich sind Sie frei, so viele Konstruktoren zu definieren, wie Sie möchten – diese müssen sich aber in den formalen Parametern unterscheiden, damit bei jedem Aufruf des Konstruktors eindeutig ist, welcher der vorhandenen Konstruktoren angesteuert werden soll.

Abb. 4.17 Die Klasse PlayerCardTest. →Video K-4.3

4.11

Ein Weiteres Beispiel einer Eigenen Klasse

99

Schliesslich befindet sich noch eine Methode mit Bezeichner toString in der Klasse PlayerCard. In dieser Methode toString definieren wir eine Zeichenkette, die das entsprechende Objekt repräsentieren soll, und geben diese zurück (der Rückgabetyp der Methode toString ist also String). In unserem Beispiel geben wir beide Werte der Instanzvariablen playerName und cardNumber getrennt durch ein Komma als Zeichenkette zurück. Die Methode toString ist eine spezielle Methode. Diese wird nämlich in gewissen Situationen automatisch aufgerufen und zwar immer wenn man versucht, mit einer println oder print Anweisung eine Objektvariable auszugeben. Das heisst, das in folgender Anweisung System.out.println(pc1);

die Methode toString der Klasse PlayerCard aufgerufen wird, so dass Java weiss, welche Zeichenkette ausgegeben werden soll. Es ist meistens eine gute Idee, für eigene Klassen die Methode toString zu definieren (z. B. zu Testzwecken). Eigener Quellcode sollte immer ausführlich getestet und die erwarteten Ergebnisse mit den tatsächlichen Ergebnissen verglichen werden. In Abb. 4.17 wird die Klasse PlayerCard mit all ihren Funktionalitäten getestet. Hierzu werden drei Objekte vom Typ PlayerCard instanziiert, ausgegeben und modifiziert. Die Ausgabe dieses Programmes lautet: Alle Spielerkarten: John, 27 Mike, 17 Penny, 0 Kartennummer von John: 27 Kartennummer von John Foo: 55 -99 ist eine ungültige Kartennummer! Kartennummer bleibt 17? 17

Aufgaben und Übungen zu Kap. 4 Theorieaufgaben 1. Erstellen Sie je eine Liste von Variablen und Methoden, welche eine Klasse BankAccount sinnvollerweise zur Verfügung stellen sollte (BankAccount soll ein Bankkonto repräsentieren).

100

4 Eigene Klassen Programmieren (Teil 1)

2. Wo werden Instanzvariablen deklariert? Was sind lokale Variablen? Wo können Sie auf lokale Variablen und wo auf Instanzvariablen zugreifen? 3. Weshalb müssen lokale Variablen nicht mit einem Sichtbarkeitsmodifikator deklariert werden? 4. Was sind die Instanzvariablen der Klasse Dice? Welche Methoden der Klasse Dice können den Status eines instanziierten Dice Objektes ändern? 5. Wieso wird die Variable points in der Klasse Dice mit Sichtbarkeit private deklariert? Weshalb ist es in Ordnung, die Variable MAX mit Sichtbarkeit public zu modifizieren? 6. Was ist der Unterschied zwischen Support- und Service-Methoden? 7. Wie müssen Sie den Konstruktor Dice umprogrammieren, so dass dieser points nicht mit 1 sondern mit einer zufälligen Zahl zwischen 1 und 6 initialisiert? 8. Schreiben Sie für die Klasse Dice eine Methode getPointsOpposite, die die Punktezahl zurückgibt, die aktuell nach unten zeigt (also die Seite gegenüber der gezeigten Seite des Würfels). Hinweis: Die Summe der Punkte zweier gegenüberliegenden Seiten eines herkömmlichen Spielwürfels ergibt immer 7. 9. Was macht eine return-Anweisung? Es ist möglich, einer Methode mit Rückgabetyp void eine return-Anweisung hinzuzufügen – was macht dieses „leere“ return? 10. Was ist der Unterschied zwischen einem formalen und einem tatsächlichen Parameter? 11. Was geschieht, wenn man ein Objekt als Parameter an die Methode print oder println übergibt? 12. Wozu sind Konstruktoren da, wie werden diese definiert und was sind die Besonderheiten von Konstruktoren im Vergleich zu „normalen“ Methoden? 13. Betrachten Sie nochmals die Klasse PlayerCardTest. a) Wie viele PlayerCard Objekte werden instanziiert? b) Wie stellen wir sicher, dass eine Änderung einer Kartennummer bei der korrekten Spielerkarte ausgeführt wird? c) Wie viele tatsächliche Parameter werden an die Methoden setCardNumber und getPlayerName übergeben, wenn diese auf dem Objekt pc2 bzw. pc1 ausgeführt werden? Java Übungen 1. Programmieren Sie eine Klasse Car, die Eigenschaften eines Autos wie z. B. die Marke, das Modell oder den Benzinverbrauch modelliert. Der Konstruktor soll diese drei Instanzvariablen gemäss Parameterübergabe initialisieren – zudem schreiben Sie Getter und Setter für alle Instanzvariablen und eine toString Methode für eine einzeilige Repräsentation von Car Objekten als Zeichenkette. In der Methode main in einer zweiten Klasse Garage instanziieren und manipulieren Sie einige Car Objekte und geben diese

4.11

Ein Weiteres Beispiel einer Eigenen Klasse

101

aus. →Video V-4.1 2. Schreiben Sie eine Klasse Dog, die einen Hund mit Namen und Alter repräsentieren soll. Definieren Sie einen Konstruktor, der die Instanzvariablen gemäss den Parametern initialisiert und zudem schreiben Sie Getter und Setter für beide Instanzvariablen. Stellen Sie sicher, dass das Alter immer grösser-gleich 0 ist. Schreiben Sie zudem eine SupportMethode ageInPersonYears, die das Hundealter multipliziert mit 7 zurückgibt und eine Methode toString, die eine Zeichenkettenrepräsentation von Dog Objekten zurückgibt (z. B. Name: Wuffli; Alter: 3 (in Menschenjahren: 21)). →Video V-4.2 3. Schreiben Sie ein Programm DogControl, das den Benutzer nach einem Namen und einem Alter fragt und die entsprechenden Werte via Tastatur einliest. Mit den eingegebenen Werten soll danach ein Dog Objekt instanziiert und ausgegebenen werden. →Video V-4.3 4. Schreiben Sie eine Klasse Box, die Instanzvariablen für die Länge, Breite und Höhe einer Box enthält. Zusätzlich enthält die Klasse Box eine Instanzvariable full vom Typ boolean, die angibt, ob die Box gefüllt ist oder nicht. Der Konstruktor setzt die Länge, Breite und Höhe einer Box gemäss Parametern – neu instanziierte Box Objekte sollen standardmässig leer sein (full wird also nicht via Parameter definiert). Definieren Sie einen zweiten Konstruktor ohne Parameter, der eine Standard-Box mit Länge, Breite und Höhe 1 generiert. Definieren Sie Getter und Setter für alle Instanzvariablen und eine toString Methode. Zusätzlich definieren Sie eine Methode getCapacity, die das Volumen der Box berechnet und zurückgibt. Testen Sie die Klasse Box, indem Sie in einer weiteren Klasse BoxTest mehrere Box Objekte instanziieren, manipulieren und ausgeben. →Video V-4.4 5. Programmieren Sie eine Klasse BankAccount, welche ein Bankkonto modellieren soll. Das Bankkonto speichert zwei Konstanten: Die erste definiert den Zinssatz für dieses Konto (z. B. 0.015) und die zweite die Gebühr für Abhebungen vom Konto (z. B. 2.50). Als Instanzvariablen deklarieren Sie den Namen des Kontobesitzers, die Kontonummer und den Kontostand. Programmieren Sie zwei Konstruktoren: Der erste soll alle Instanzvariablen gemäss Parametern initialisieren und der zweite soll nur den Namen des Kontobesitzers und die Kontonummer entgegennehmen – in diesem Konstruktor wird der Kontostand standardmässig mit 0.0 initialisiert.

102

4 Eigene Klassen Programmieren (Teil 1)

Programmieren Sie eine Methode deposit, welche eine Einzahlung auf das Konto ermöglicht und eine Methode withdrawal, welche eine Abhebung vom Konto erlaubt (die Höhe der Einzahlung/Abhebung definieren Sie via Parameter). Bei Abhebungen wird der gewünschte Betrag vom Kontostand plus die festgelegte Gebühr vom Kontostand abgezogen. Die Abhebung soll nur dann durchgeführt werden, wenn der Kontostand grösser-gleich 0.0 bleibt – falls dies nicht gegeben ist, soll die Abhebung nicht durchgeführt werden (geben Sie in diesem Fall eine Fehlermeldung aus). Programmieren Sie dann noch eine Methode computeInterest, welche den Zins für das Konto berechnet und diesen dem Kontostand hinzufügt. Schliesslich programmieren Sie noch einen Getter für den Kontostand und eine Methode toString. →Video V-4.5 6. Programmieren Sie solange eigene Klassen mit mehreren Instanzvariablen, verschiedenen Konstruktoren, Gettern und Settern und toString Methoden bis Sie die Konzepte Klasse, Objekt, Methode, Instanzvariable, lokale Variable, Parameter, Konstruktor, Getter, Setter, Rückgabe, etc. solid verstanden haben (Vorschläge für eigene Klassen: Student, Lecture, Professor, QuizQuestion, Table, Room, Shoe, Device, Painting, etc.).

Literatur 1. John Lewis and William Loftus. Java Software Solutions – Foundations of Program Design. Pearson Global Edition, 8th edition edition, 2015. 2. https://www.uml.org.

5

Graphische Benutzeroberflächen (Teil 1)

5.1

Einführung

Die Unterstützung von graphischen Benutzeroberflächen (engl. graphical user interfaces [GUI]) durch Java hat mehrere Entwicklungen und Veränderungen hinter sich. JavaFX ist seit kurzem der bevorzugte Ansatz, um Java Programme mit GUIs zu entwickeln. JavaFX ist ein ganz neuer Ansatz und ist von Swing und AWT entkoppelt (Swing und AWT sind beides ältere Bibliotheken des Java APIs zur Erzeugung von GUIs). Anders als unsere bisherigen textbasierten Programme, sind Programme mit einem GUI oftmals besser zur Interaktion mit dem Benutzer geeignet. GUIs machen Programme meistens (aber nicht immer) effektiver bei deren Nutzung und damit fast immer interessanter für die Benutzer. Wir programmieren in diesem Kapitel erste Applikationen, die durch ein GUI unterstützt werden. Allerdings werden wir an die programmierten Oberflächen noch keine Funktionalität anbinden. Dies werden wir in späteren Kapiteln Schritt für Schritt nachholen. Tatsächlich sollten wir auch bei grösseren Projekten, die Programmierung des GUIs strikt von den anderen Teilen des Quellcodes trennen. In Abb. 5.1 ist ein erstes, einfaches JavaFX Programm gezeigt und in Abb. 5.2 ist die vom Quellcode erzeugte graphische Oberfläche ersichtlich. Dieses Programm erzeugt offensichtlich ein Programmfenster, das drei Textelemente beinhaltet und diese an verschiedenen Positionen anzeigt. Beachten Sie, dass die eigene Klasse MyFxApp von der Klasse Application erbt. Vererbung wird in Java im Klassenkopf mit dem reservierten Wort extends und dem Bezeichner der Klasse, die erweitert werden soll, ausgelöst: public class MyFXApp extends Application { Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_5. © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_5

103

104

5 Graphische Benutzeroberflächen (Teil 1)

Abb. 5.1 Die Klasse MyFxApp definiert ein einfaches Programm mit graphischer Oberfläche. → Video K-5.1 Abb. 5.2 Das von der Klasse MyFxApp erzeugte Programmfenster

5.1

Einführung

105

MyFXApp

Application

main

launch launch();

start(Stage stage)

1 Fenster vom Betriebsystem anfordern und dieses an die Methode start in Form eines Objektes stage vom Typ Stage

3

2

Betriebs system

start(stage);

1. Wurzel instanziieren

4

3. Szene in Fenster stage anzeigen

Abb. 5.3 Der generelle Ablauf beim Starten einer JavaFX Applikation (vereinfacht dargestellt)

Wir haben in Kap. 1 kurz über das Prinzip der Vererbung gesprochen und wir werden später noch ausführlich auf dieses Konzept eingehen. Wichtig für den Moment ist nur, dass Sie wissen, dass alle JavaFX Programme die Klasse Application durch Vererbung erweitern. Es befinden sich zwei Methoden in der Klasse MyFxApp. Die Methode main (wie wir sie von unseren bisherigen Programmen schon kennen) und die Methode start. Die Methode main wird nur verwendet, um die Methode launch der Klasse Application aufzurufen (dies entspricht Schritt 1 in der Illustration in Abb. 5.3). Dies wird bei allen Beispielen in diesem Buch so bleiben. Die Methode launch, die sich in der Klasse Application befindet und um die wir uns nicht selbst kümmern müssen, führt dann wichtige Operationen durch. Insbesondere organisiert uns JavaFX in der Klasse Application ein Fenster vom Betriebssystem, das wir dann mit unseren eigenen Inhalten füllen und dem Benutzer präsentieren können (Schritt 2 in Abb. 5.3). Das vom Betriebssystem erhaltene Fenster wird nun in ein Objekt vom Typ Stage verpackt. Dieses Objekt ist sozusagen die Bühne, auf der wir unsere Inhalte präsentieren können (JavaFX arbeitet nach der Metapher eines Theaters). Diese Bühne wird als nächstes an die Methode start als Parameter übergeben (Schritt 3 in Abb. 5.3). Der Kontrollfluss befindet sich nun in der Methode start unserer eigenen Klasse und wir als Programmierer und Programmiererinnen dieser Applikation haben nun die Kontrolle darüber, was die Bühne zeigen und ermöglichen soll. Das heisst, mit eigener Programmierung können wir nun das als Parameter erhaltene Bühnenobjekt stage mit Inhalten gestalten – dies ist Schritt 4 in Abb. 5.3, welcher drei Phasen durchläuft (diese drei Phasen sind im Quellcode in Abb. 5.1 als Kommentare markiert): 1. Zuerst instanziieren wir den Ursprung unserer graphischen Oberfläche (in diesem ersten Beispiel als Objekt vom Typ Group). Der Ursprung des GUIs – oft mit dem Variablennamen root bezeichnet – ist eine Art Behälter für graphische Elemente.

106

5 Graphische Benutzeroberflächen (Teil 1)

In unserem einfachen Beispiel enthält der Ursprung root (vom Typ Group) drei Objekte vom Typ Text. Dies geschieht, indem die drei instanziierten Text Objekte als Parameter an den Konstruktor Group übergeben werden: Group root = new Group(hello, question, ready);

Die Text Objekte hello, question und ready müssen natürlich vorher instanziiert werden. Zum Beispiel wird mit Text hello = new Text(60, 40, "Hallo.");

das Text Objekt hello an der Position 60, 40 und dem Inhalt "Hallo." instanziiert. 2. Als nächstes instanziieren wir die Szene, die wir auf der Bühne (also im Objekt stage) anzeigen wollen. Der Konstruktor der Klasse Scene erwartet als Parameter den Ursprung eines GUIs. Optional kann noch die gewünschte Breite und Höhe sowie die Hintergrundfarbe der Szene an den Konstruktor Scene übergeben werden: Scene scene = new Scene(root, 300, 120, Color.ALICEBLUE);

In unserem Beispiel soll das vorher instanziierte Objekt root vom Typ Group (das drei Textelemente enthält) in einer Fläche der Grösse 300 × 120 Pixel angezeigt werden. Die Hintergrundfarbe ist durch eine Variable ALICEBLUE in der Klasse Color definiert (wir referenzieren also direkt auf diese Variable in der Klasse Color, ohne vorher ein Objekt vom Typ Color instanziieren zu müssen – wir kommen im nächsten Kapitel auf diese Möglichkeit der Programmierung zu sprechen). 3. Im letzten Schritt wird nun mit der Methode setScene das Objekt scene an die Bühne (stage) übergeben: stage.setScene(scene);

Zudem kann man noch den Titel des Fensters definieren (mit der Methode setTitle) und die Bühne anzeigen (mit der Methode show): stage.setTitle("Ein JavaFX Fenster");

stage.show();

5.2

5.2

Kontroll- und Behälterelemente im Szenengraphen

107

Kontroll- und Behälterelemente im Szenengraphen

Eine graphische Oberfläche in JavaFX besteht im Wesentlichen aus Elementen zweier Kategorien: Kontrollelemente und Behälterelemente. Kontrollelemente (engl. controls) sind bspw. klickbare Menueinträge, Textelemente, Schaltflächen, Eingabefelder für Text und alle anderen graphischen Komponenten, die man in einem Programmfenster typischerweise sehen und bedienen kann. JavaFX enthält eine Vielzahl solcher Kontrollelemente, die wir für unsere GUIs verwenden können. JavaFX bietet auch unterschiedlichste Behälterelemente an, die den Inhalt (also zum Beispiel die Kontrollelemente) nach gewissen Mustern anordnen und z. T. weitere Funktionalitäten anbieten. Beachten Sie, dass ein Behälterelement neben Kontrollelementen auch wieder andere Behälter beinhalten kann. Diesem Prinzip folgend kann man beliebig komplexe GUIs hierarchisch aufbauen. Die Hierarchie von graphischen Inhalten kann man in einem Szenengraph (engl. Scene Graph) planen und festhalten. Die Knoten eines Szenengraphen können dabei in zwei Kategorien eingeteilt werden: • Verzweigungsknoten (engl. Branch Nodes) • Blattknoten (engl. Leaf Nodes) Verzweigungsknoten besitzen die Möglichkeit, andere Knoten zu enthalten und repräsentieren somit die Behälter für weitere Elemente. Blattknoten hingegen enthalten keine anderen Knoten (sind also keine Behälter für andere Elemente). Typischerweise sind die Kontrollelemente eines Programmes die Blattknoten im Szenengraphen. Es gibt aber auch Kontrollelemente, die weitere Kontrollelemente aufnehmen können und somit auch Behälter darstellen und durch Verzweigungsknoten repräsentiert werden (die Unterteilung zwischen Kontroll- und Behälterelementen ist also nicht immer trennscharf). Beispiel 5.1 Der Szenengraph zum Programm MyFxApp ist in Abb. 5.4 gezeigt. Die Wurzel des Graphen root ist ein Objekt vom Typ Group, welches drei Elemente beinhaltet, nämlich drei Text Objekte (der entsprechende Text wird in den drei Blattknoten angegeben). Auf

Abb. 5.4 Der Szenengraph zum Programm MyFxApp

root Group

(60, 40)

Text

(110, 70) Text

(85, 100)

Text

108

5 Graphische Benutzeroberflächen (Teil 1)

60

40

70

110

Abb. 5.5 Illustration zum Koordinationensystem eines Programmfensters

den Kanten, welche die Wurzel mit den drei Blattknoten verbinden, ist die Position der Text Objekte mit (x,y)-Koordinaten angegeben. Beachten Sie, dass die (x,y)-Koordinaten der linken oberen Ecke eines Programmfensters (0,0) sind. Die x-Koordinate wird grösser, je weiter wir im Fenster nach rechts gehen und die y-Koordinate wird grösser, je weiter wir uns im Fenster nach unten bewegen. Das Text Objekt mit Inhalt "Hallo." wird also 60 Pixel vom linken Rand und 40 Pixel vom oberen Rand entfernt platziert. Das zweite Textelement befindet sich 110 Pixel vom linken Rand und 70 Pixel vom oberen Fensterrand entfernt (siehe auch Abb. 5.5).

5.3

Kontrollelemente

Ein Kontrollelement ist ein Element, das Informationen anzeigt und/oder einem Benutzer eine Interaktion mit dem Programm ermöglicht. Im ersten Beispiel haben wir drei Objekte vom Typ Text in unserem Fenster angezeigt. Streng genommen gehören Objekte dieses Typs aber nicht zu den Kontrollelementen in JavaFX. Wir betrachten im Folgenden eine Auswahl von Kontrollelementen, die uns JavaFX zur Verfügung stellt (in Abb. 5.6 sind die in diesem Buch besprochenen Kontrollelemente in einem Programmfenster zu sehen). • Label: Objekte dieses Typs werden typischerweise dazu verwendet, um andere Kontrollelemente zu beschriften oder um Informationen anzuzeigen1 . Dem Konstruktor Label kann der Inhalt der Beschriftung als Zeichenkette mitgegeben werden:

1 Label Objekte können im Gegensatz zu Objekten vom Typ Text formatiert werden.

5.3

Kontrollelemente

109 Menu MenuBar

MenuItem

ListView

ImageView

TextField Label RadioButton

ChoiceBox CheckBox

Button

Abb. 5.6 Einige der JavaFX Kontrollelemente in einem Fenster angezeigt

Label gradeLabel = new Label("Note: 0");

Möchte man zur Laufzeit des Programmes den Inhalt der Beschriftung ändern, kann die Methode setText auf dem entsprechenden Label Objekt aufgerufen werden: gradeLabel.setText("Note: 5.5");

• TextField: Objekte vom Typ TextField entsprechen einem Texteingabeelement, das einem Benutzer ermöglicht, eine Zeile Text einzugeben: TextField inputField = new TextField();

Möchte man zur Laufzeit des Programmes den Inhalt eines Textfeldes auslesen, kann hierzu die Methode getText aufgerufen werden (und die Rückgabe z. B. einer String Variablen zugewiesen werden): String input = inputField.getText();

Beachten Sie, dass JavaFX mit den Klassen TextArea und PasswordField auch mehrzeilige und verdeckte Eingaben ermöglicht.

110

5 Graphische Benutzeroberflächen (Teil 1)

• Button: Objekte vom Typ Button definieren eine klickbare Schaltfläche. Der Konstruktor der Klasse Button erwartet eine Zeichenkette, welche die Beschriftung der Schaltfläche definiert: Button submitButton = new Button("Eingeben");

Typischerweise wird eine Aktion ausgelöst, wenn ein Objekt dieses Typs angeklickt wird. • CheckBox: Ein Objekt vom Typ CheckBox repräsentiert eine Box, die via Mausklick aus- oder abgewählt werden kann. Die Beschriftung der Auswahlmöglichkeit wird dem Konstruktor CheckBox mitgegeben: CheckBox bonusBox = new CheckBox("Bonuspunkte");

Möchte man zur Laufzeit des Programmes herausfinden, ob eine bestimmte Box zu diesem Zeitpunkt ausgewählt ist, existiert eine Methode isSelected, die einen boolean zurückgibt (also true, wenn die Box gewählt ist und sonst false): boolean bonusAvailable = bonusBox.isSelected();

CheckBox Objekte sind unabhängig voneinander. Das heisst, jedes CheckBox Objekt kann aus- oder abgewählt werden, ohne den Status der anderen CheckBox Objekte zu ändern. • RadioButton: Ein Objekt vom Typ RadioButton repräsentiert wie eine CheckBox eine Wahlmöglichkeit, die aus- oder abgewählt werden kann. Anders als eine CheckBox ist ein RadioButton aber meist nur zusammen mit anderen RadioButton Objekten sinnvoll einsetzbar. Zum Beispiel instanziieren wir mit folgenden Anweisungen zwei RadioButton Objekte (beschriftet mit "Ja" und "Nein"): RadioButton yesRadioButton = new RadioButton("Ja"); RadioButton noRadioButton = new RadioButton("Nein");

In einer Menge von RadioButton Objekten ist typischerweise nur eine Option gleichzeitig auswählbar (d. h. zu jedem Zeitpunkt ist genau eines der RadioButton Objekte ausgewählt). Wird ein RadioButton vom Benutzer ausgewählt, so soll der bisher gewählte RadioButton automatisch abgewählt werden. Dies geschieht mit Hilfe eines ToggleGroup Objektes, welches sich ausschliessende RadioButton Objekte zu einer Gruppe zusammenführt:

5.3

Kontrollelemente

111

ToggleGroup toggle = new ToggleGroup();

Um einen RadioButton einer solchen Gruppe hinzuzufügen, übergeben wir die Gruppe als Parameter an die Methode setToggleGroup auf den entsprechenden RadioButton Objekten: yesRadioButton.setToggleGroup(toggle);

noRadioButton.setToggleGroup(toggle); Auch für RadioButton Objekte kann die Methode isSelected verwendet werden, um herauszufinden, ob eine bestimmte Auswahl aktuell zutrifft oder nicht: boolean repetition = yesRadioButton.isSelected();

Mit der Methode setSelected kann man zudem eines der vorhandenen RadioButton Objekte auswählen (damit zum Beispiel beim Programmstart eine der vorhandenen Optionen bereits gewählt ist): noRadioButton.setSelected(true);

• ImageView: Die Klasse ImageView repräsentiert ein Kontrollelement zum Anzeigen eines Bildes und wird folgendermassen instanziiert: ImageView imageView = new ImageView();

Um ein Bild in unserem Programm zu platzieren, muss zunächst ein Bild (z. B. eine .jpg Datei) als Image Objekt geladen werden2 : Image studentImage = new Image("/images/riesen.jpg");

Das so geladene Bild kann nun mit der Methode setImage an die instanziierte ImageView übergeben werden: imageView.setImage(studentImage);

Alternativ können Sie ein geladenes Bild auch direkt als Parameter an den Konstruktor ImageView übergeben:

2 In diesem Beispiel befindet sich das Bild riesen.jpg im Ordner images im aktuellen Java Projekt.

112

5 Graphische Benutzeroberflächen (Teil 1)

ImageView imageView = new ImageView(studentImage);

• ChoiceBox: Eine ChoiceBox ist ein JavaFX Kontrollelement, das einem Benutzer die Auswahl aus verschiedenen Optionen ermöglicht. Wenn ein Benutzer auf ein Objekt vom Typ ChoiceBox klickt, wird ihm eine Liste von Optionen gezeigt, aus denen er dann genau eine auswählen kann. Das aktuell gewählte Element wird dann in der ChoiceBox angezeigt. Beachten Sie, dass man Objekte vom Typ ChoiceBox typisiert deklariert und instanziiert. Dies geschieht innerhalb von spitzen Klammern: ChoiceBox choiceBox = new ChoiceBox();

Mit einer solchen Typisierung geben wir an, welcher Typ in dieser Auswahl vorhanden sein darf (in diesem Fall soll das Objekt choiceBox nur Objekte vom Typ String aufnehmen können). Wir werden später in diesem Buch nochmals eingehend auf solche Typisierungen eingehen. Mit der Methode getItems können wir die Liste der vorhandenen Elemente in der ChoiceBox referenzieren. Diese ist zu Beginn noch leer. Auf dieser Liste können wir nun aber mit den Methoden add oder addAll eines oder gleich mehrere String Objekte zur ChoiceBox hinzufügen. choiceBox.getItems().addAll("Wirtschaftsinformatik",

"Betriebsökonomie", "Management"); Um zur Laufzeit eines Programmes herauszufinden, welches der Elemente aktuell gewählt ist, wird zuerst die Methode getSelectionModel aufgerufen – auf diesem Modell kann dann die Methode getSelectedItem aufgerufen werden: String subject = choiceBox.getSelectionModel().getSelectedItem();

Beachten Sie, dass die Rückgabe der Methode getSelectedItem in diesem Beispiel ein Objekt vom Typ String ist, da wir die choiceBox mit typisiert haben. • ListView: Eine ListView repräsentiert ein Kontrollelement zum Anzeigen einer Liste von Objekten. Objekte dieser Klasse sollten auch typisiert instanziiert werden: ListView listView = new ListView();

5.3

Kontrollelemente

113

In diesem Beispiel wird listView so definiert, dass diese Objekte vom Typ String aufnehmen und anzeigen kann. Das Hinzufügen von Objekten und das Auslesen des aktuell gewählten Objektes funktioniert dann nach den gleichen Prinzipien wie bei einer ChoiceBox. Also zum Beispiel: listView.getItems().addAll("Emilie Riesen",

"Kaspar Riesen", "Maxime Riesen"); String student = listView.getSelectionModel().getSelectedItem();

• MenuBar, Menu, MenuItem: Die Klasse MenuBar definiert eine Menuleiste, die sich traditionellerweise am oberen Rand eines Programmfensters befindet. Eine MenuBar kann ein oder mehrere Menu Objekte beinhalten. Die Klasse Menu definiert ein klickbares Menu, das eine Auswahlliste mit verschiedenen MenuItem Objekten beinhaltet. Diese MenuItem Objekte repräsentieren verschiedene Aktionen, aus denen ein Benutzer eine auswählen kann. Wir instanziieren zunächst die nötigen MenuItem Objekte. Also zum Beispiel: MenuItem newItem = new MenuItem("Neu");

MenuItem openItem = new MenuItem("Öffnen"); MenuItem saveItem = new MenuItem("Speichern"); Danach instanziieren wir ein neues Menu Objekt und fügen – ähnlich wie bei ListView und ChoiceBox – die drei MenuItem Objekte hinzu: Menu fileMenu = new Menu("Datei");

fileMenu.getItems().addAll(newItem, openItem, saveItem); Einem Objekt vom Typ MenuBar kann dann mit den Methoden getMenus und addAll das so definierte Menu Objekt (oder mehrere Menu Objekte) hinzugefügt werden: MenuBar menuBar = new MenuBar();

menuBar.getMenus().addAll(fileMenu); Wir haben nun einige JavaFX Kontrollelemente kennen gelernt. Dies sind die Kontrollelemente, die wir für unsere weiteren Beispiele verwenden werden – beachten Sie aber, dass JavaFX noch etliche weitere Kontrollelemente anbietet, die wir hier nicht betrachten

114

5 Graphische Benutzeroberflächen (Teil 1)

(z. B. die Klassen Slider, ToggleButton, ToggleSwitch, DatePicker und viele andere).

5.4

Behälterelemente

In unserem ersten Beispiel haben wir die drei Text Objekte in einem Group Objekt als Ursprung unseres GUIs zusammengefasst. Dabei mussten wir für jedes Textelement mit Koordinaten angeben, wo dieses im Fenster platziert werden soll. Für komplexere GUIs ist dieses Vorgehen zu unflexibel und zu mühsam. Wir betrachten in diesem Abschnitt Behälterelemente, welche die hinzugefügten Kontrollelemente (oder weitere Behälter) nach bestimmten Mustern anordnen. Tatsächlich haben wir bereits Behälter kennengelernt, die bei der Anordnung nach bestimmten Regeln vorgehen, nämlich die Klassen Menu und MenuBar. Ein Objekt vom Typ MenuBar kann eines oder mehrere Objekte vom Typ Menu aufnehmen und ordnet diese horizontal in einer Menüleiste an. Die Klasse Menu wiederum ist ein Behälter für MenuItem Objekte und gruppiert diese vertikal in einer Auswahlliste. Die Grundstruktur für die meisten GUIs wird in JavaFX mit acht Basisbehältern definiert. Diese Behälter bieten je ein eigenes Layout an und gruppieren die hinzugefügten Elemente gemäss bestimmten Regeln. Diese Behälter gehören nicht zu den Kontrollelementen, da diese keine Interaktion mit dem Benutzer ermöglichen. In Abb. 5.7 sind die Anordnungen dieser acht Behälter illustriert. • HBox: Legt die Elemente in einer einzigen horizontalen Reihe an. Die Reihenfolge der Elemente entspricht der Reihenfolge, in der diese der HBox hinzugefügt werden.

Abb. 5.7 Die acht Basisbehälter in JavaFX organisieren die Kontrollelemente nach gewissen Mustern

Kontrollelemente

FlowPane

TilePane

StackPane

AnchorPane

VBox

HBox

GridPane

BorderPane

5.4

Behälterelemente FlowPane

115

FlowPane

FlowPane

FlowPane

Abb. 5.8 Die Anordnung der Kontrollelemente passt sich in einer FlowPane der Grösse des Fensters an

• VBox: Funktioniert wie eine HBox, organisiert die Elemente aber vertikal. • FlowPane: Legt die Elemente eins nach dem anderen hintereinander ab (horizontale oder vertikale Anordnungen sind möglich). Die Anzahl Zeilen und Spalten passt sich dabei der Grösse des Fensters und der Anzahl vorhandener Elemente an. Wenn also z. B. in einer Zeile kein Platz mehr vorhanden ist, wird das nächste Element auf einer weiteren Zeile platziert – umgekehrt springen die Elemente unter Umständen auf die obere Zeile zurück, wenn das Fenster vergrössert wird (siehe Abb. 5.8). • TilePane: Funktioniert wie eine FlowPane mit dem Unterschied, dass die einzelnen Zellen der Elemente in einer TilePane alle die gleiche Grösse besitzen, während diese in einer FlowPane dynamisch den hinzugefügten Elementen angepasst werden. • GridPane: Legt die Elemente in einem Gitter aus Zellen ab. Jede Zelle ist über die Zeilen- und Spaltennummer direkt ansprechbar. • BorderPane Bietet fünf Regionen an, in denen Elemente abgelegt werden können: top, bottom, left, right und center. • StackPane: Legt die Elemente in einem Stapel an (ähnlich zu Karten in einem Kartenstapel). • AnchorPane: Funktioniert ähnlich wie die BorderPane im Sinn, dass dieses Layout auch Regionen anbietet (allerdings nur deren vier): left, right, bottom und top. Im Gegensatz zur BorderPane kann man aber mehrere Kontrollelemente gleichzeitig in einer dieser vier Region platzieren. Ein wichtiges Konzept beim Aufbau eines GUIs ist, dass ein Element, das einem bestimmten Behälter hinzugefügt wird, wiederum ein Behälter sein kann. Beispiel 5.2 In Abb. 5.9 ist eine VBox illustriert, die drei Elemente enthält. Während das erste und dritte Element ein Kontrollelement darstellt, ist das zweite Element ein Behälter vom Typ HBox. Der zu diesem GUI korrespondierende Szenengraph ist in der gleichen Abbildung gezeigt. In Abb. 5.10 ist ein weiteres Beispiel gezeigt. Hier enthält eine BorderPane vier Elemente, die allesamt auch Behälter sind (nämlich eine MenuBar, eine VBox, eine

116

5 Graphische Benutzeroberflächen (Teil 1)

Abb. 5.9 Diese VBox enthält drei Elemente – das zweite Element ist ein Behälter vom Typ HBox, das vier Kontrollelemente enthält

VBox

HBox

VBox

HBox

BorderPane MenuBar VBox

GridPane

HBox

BorderPane top

MenuBar

left

VBox

center

bottom

GridPane

HBox

Abb. 5.10 Diese BorderPane enthält vier Elemente – Jedes hinzugefügte Element ist wiederum ein Behälter, der mehrere Kontrollelemente enthält

GridPane und eine HBox). Diese Behälterelemente werden in den Regionen top, left, center und bottom platziert (die Region right wird in diesem Beispiel nicht benutzt). Wir programmieren im Folgenden einige Beispiele, um die Behälter und deren Anordnungen sowie das Prinzip der hierarchischen GUIs besser verstehen zu können.

5.4.1

Die Klassen VBox, HBox, FlowPane und TilePane

Das Hinzufügen von Kontrollelementen funktioniert in den Klassen VBox, HBox, FlowPane und TilePane genau gleich. Alle vier Klassen bieten hierzu zwei Methoden an: add und addAll. Beide Methoden müssen auf der Liste aufgerufen werden, in der

5.4

Behälterelemente

117

Abb. 5.11 Ein GUI, um einen Würfel zu steuern

sich alle bisher hinzugefügten Elemente befinden. Diese Liste kann man über die Methode getChildren referenzieren. Um also zum Beispiel eine FlowPane zu instanziieren und dieser ein Kontrollelement element hinzuzufügen, schreiben wir: FlowPane pane = new FlowPane();

pane.getChildren().add(element); Wir programmieren nun ein GUI, um unseren Würfel (programmiert in der Klasse Dice) steuern zu können. Das Ziel ist ein Programmfenster, wie es in Abb. 5.11 gezeigt ist. Es soll also möglich sein, einen Würfel per Knopfdruck zu werfen und diesen via Tastatureingabe auf eine beliebige Seite zu drehen. Als erstes programmieren wir eine Hauptklasse DiceApp, die dem gleichen Muster folgt, wie wir es in unserem ersten Beispiel gesehen haben (siehe Abb. 5.12). Der Unterschied zum ersten Beispiel ist, dass der Ursprung des GUIs nicht direkt in der Methode start programmiert ist. Die Variable root ist in diesem Beispiel vom Typ MainView statt Group. In dieser Klasse MainView, die wir selber definieren müssen, ist unser gesamtes GUI definiert3 . In Abb. 5.13 ist die Klasse MainView gezeigt. Wie Sie sehen, erbt MainView von VBox – das bedeutet, dass Objekte vom Typ MainView (erweiterte) Objekte vom Typ VBox sind. Der Ursprung des GUIs ist also eine VBox. Um ein Bild eines Würfels anzeigen zu können, müssen wir ein solches in unser Programm laden. Dies geschieht mit Hilfe der Klasse Image (das Bild four.jpg befinde 3 Beachten Sie folgendes: Bei JavaFX Programmen müssen Sie typischerweise sehr viele Klassen aus verschiedenen Packages importieren. Damit die Beispielbilder mit Quellcode nicht zu lang werden, haben wir uns entschieden, die Anweisungen für das Importieren mit import javafx.*; abzukürzen. Wenn Sie diese Beispiele nachprogrammieren wollen, müssen Sie aber alle nötigen Klassen bzw. Packages einzeln importieren.

118

5 Graphische Benutzeroberflächen (Teil 1)

Abb. 5.12 Die Klasse DiceApp startet unser Programm und zeigt das programmierte GUI in einem Programmfenster an. → Video K-5.2

sich dabei im Ordner img im aktuellen Projektordner und wir deklarieren das Bild FOUR in unserem Beispiel als Konstante): private final Image FOUR = new Image("/img/four.jpg");

Unser GUI soll fünf Kontrollelemente anzeigen: Ein Objekt vom Typ Label, ein Objekt vom Typ ImageView, zwei Objekte vom Typ Button und ein Objekt vom Typ TextField. Zudem werden wir einen weiteren Behälter, nämlich eine HBox, verwenden. All diese Elemente sind in der Klasse MainView als Instanzvariablen deklariert. Wir definieren das gesamte GUI im Konstruktor MainView. Zuerst werden alle Kontrollelemente instanziiert. Danach instanziieren wir den Teilbehälter subContainer vom Typ HBox und fügen diesem das Button Objekt setButton und das TextField Objekt pointsField hinzu. Dies geschieht zum Beispiel mit: this.subContainer.getChildren().add(this.setButton);

5.4

Behälterelemente

119

Abb. 5.13 Die Klasse MainView definiert das gesamte GUI, das wir anzeigen lassen wollen, in einer VBox. → Video K-5.2

120

5 Graphische Benutzeroberflächen (Teil 1)

Danach werden die anderen instanziierten Kontrollelemente und der Teilbehälter subContainer diesem Behälter (also dieser VBox) hinzugefügt: Dies geschieht zum Beispiel mit: this.getChildren().add(this.diceView);

In unserem Beispiel führen wir schliesslich noch folgende Formatierungen durch: • Für das Objekt title vom Typ Label wird mit der Methode setFont eine neue Schriftart ("Helvetica") und Schriftgrösse (22) definiert. • Für das Objekt pointsField vom Typ TextField wird mit der Methode setMaxWidth eine neue maximale Breite von 40 Pixel definiert. • Für das Objekt subContainer vom Typ HBox und für dieses Objekt vom Typ VBox wird der Abstand zwischen den hinzugefügten Elementen auf 5 Pixel definiert (mit der Methode setSpacing). • Zudem sollen die hinzugefügten Elemente vertikal und auch horizontal zentral im Behälter positioniert werden. Dies geschieht mit der Methode setAlignment und der Konstanten CENTER, die in der Klasse Pos deklariert ist. Beachten Sie, dass wir in unseren Beispielen jeweils nur minimale Formatierungen des GUIs vornehmen – das Gestalten der Oberfläche steht nicht im Vordergrund dieses Buches (zudem geschieht die Gestaltung und Formatierung typischerweise mit anderen, etwas mächtigeren Konzepten, als dass dies direkt im Quellcode vorgenommen wird).

5.4.2

Die Klasse GridPane

Die Klasse GridPane organisiert die Elemente in einem flexiblen Gitter mit Zeilen und Spalten. Jedes Element wird einer bestimmten Zelle im Gitter zugeordnet und kann dabei über mehrere Zeilen und/oder Spalten hinweg gehen. Die Breite bzw. die Höhe einer Spalte/Zeile wird durch das breiteste bzw. höchste Element in der entsprechenden Spalte/Zeile definiert. Die folgende Anweisung fügt dem Objekt grid vom Typ GridPane ein Kontrollelement element zur Zelle in Spalte 2 und Zeile 0 hinzu: grid.add(element, 2, 0);

Die Methode add kann mit zwei zusätzlichen Parametern aufgerufen werden, welche die maximale Grösse des Elements in Anzahl Spalten und Zeilen definiert. Der folgende Aufruf fügt zum Beispiel ein Kontrollelement element in die Zelle in Spalte 2 und Zeile 4 ein. Das hinzugefügte Element darf dabei maximal 2 Spalten und 3 Zeilen überdecken: grid.add(element, 2, 4, 2, 3);

5.4

Behälterelemente

121 (2, 3)

(1,1)

Hgap

Vgap

Abb. 5.14 Ein GUI, um Spielerdaten zu erfassen

In Abb. 5.14 ist das für unser nächstes Beispiel geplante GUI zu sehen. Dieses besteht aus folgenden Kontrollelementen: • Drei Label Objekte (mit den Inhalten "Nationalität", "Spielername" und "Club") • Ein ChoiceBox Objekt (zur Auswahl der Nationalität) • Zwei TextField Objekte (zur Eingabe des Namens und des Clubs) • Drei RadioButton Objekte (Zur Auswahl Stürmer, Verteidiger oder Torwart) • Ein Button Objekt beschriftet mit "Hinzufügen" In der gleichen Abbildung ist auf der rechten Seite das Gitter der Zellen der GridPane sichtbar gemacht. Die Zelle in der oberen linken Ecke des Fensters einer GridPane befindet sich in der 0-ten Spalte und 0-ten Zeile. In unserem Beispiel wird weder der 0-ten Spalte noch der 0-ten Zeile ein Element hinzugefügt. Deshalb hat diese Zeile bzw. diese Spalte die Grösse 0. Beachten Sie, dass eine GridPane das Definieren eines horizontalen und vertikalen Abstandes zwischen den Zellen erlaubt (mit den Methoden setHgap und setVgap). Diese Abstände sind in der Visualisierung ebenfalls ersichtlich. Somit sollten Sie sehen, dass sich das Label mit dem Inhalt "Nationalität" an Position (1, 1) befindet und das TextField Objekt zur Eingabe des Clubs an Position (2, 3) – also in der zweiten Spalte und dritten Zeile. In der Klasse PlayerCardApp starten wir unser JavaFX Programm nach dem gewohnten Muster (siehe Abb. 5.15). Als Ursprung des GUIs instanziieren wir ein Objekt vom Typ AddPlayerView, welche das gewünschte GUI in einer GridPane organisiert (siehe Abb. 5.16).

122

5 Graphische Benutzeroberflächen (Teil 1)

Abb. 5.15 Die Klasse PlayerCardApp startet unser Programm und zeigt das programmierte GUI in einem Programmfenster an. → Video K-5.3

Wir sollten wiederum beachten, dass AddPlayerView erbt – diesesmal von der Klasse GridPane. Das heisst, Objekte vom Typ AddPlayerView sind (erweiterte) Objekte vom Typ GridPane. Alle Kontrollelemente dieses GUIs sind als Instanzvariablen deklariert. Im Konstruktor AddPlayerView werden diese instanziiert und diesem Behälter hinzugefügt. Zum Beispiel wird das Label Objekt nationLabel mit der Anweisung this.add(this.nationLabel, 1, 1);

zu der Zelle an Position (1, 1) oder das Button Objekt addButton mit der Anweisung this.add(this.addButton, 2, 6);

zu der Zelle an Position (2, 6) in dieser GridPane hinzugefügt. Beachten Sie, dass die drei Objekte vom Typ RadioButton (forwardButton, backButton und goalieButton) in dem Behälter buttonBox vom Typ HBox zusammengefasst werden. Dieser Behälter wird dann der Zelle an Position (2, 5) zur GridPane hinzugefügt:

5.4

Behälterelemente

123

Abb.5.16 Die Klasse AddPlayerView definiert das gesamte GUI, das wir anzeigen lassen wollen, in einer GridPane. → Video K-5.3

124

5 Graphische Benutzeroberflächen (Teil 1)

this.buttonBox.getChildren().addAll(this.forwardButton, this.backButton, this.goalieButton); this.add(buttonBox, 2, 5);

5.4.3

Die Klasse BorderPane

Das letzte Layout, das wir im Detail anschauen, ist durch die Klasse BorderPane definiert. In diesem Layout existieren fünf Regionen (siehe Abb. 5.17). In jede Region kann genau ein Element gesetzt werden. Die Methoden hierzu lauten setTop, setLeft, setRight, setCenter und setBottom. Die Grössen der Regionen passen sich automatisch dem Inhalt an (wird einer Region nichts hinzugefügt, hat diese die Grösse 0). In Abb. 5.18 ist der Einsatz der Klasse BorderPane illustriert. Wir definieren hierzu eine eigene Klasse MainView, welche von der Klasse BorderPane erbt. Im Konstruktor setzen wir den Inhalt von drei der fünf Regionen. Hierzu instanziieren wir zunächst das Objekt menuBar vom Typ MenuBar, das ein Menu Objekt (mit zwei Objekten vom Typ MenuItem) beinhaltet. Danach instanziieren wir das Objekt addPlayerView vom Typ AddPlayerView, welches das GUI zur Eingabe der Spielerdaten definiert (in einer GridPane). Letztlich wird ein neues Label Objekt status mit dem Inhalt "Alles ok" instanziiert. Mit den Anweisungen this.setTop(this.menuBar); this.setCenter(this.addPlayerView); this.setBottom(this.status);

werden alle drei Elemente den entsprechenden Regionen dieser BorderPane hinzugefügt. Wird nun in der Klasse PlayerCardApp der Ursprung des GUIs root als neues MainView Objekt instanziiert MainView root = new MainView();

erhalten wir das in Abb. 5.19 gezeigte Programmfenster.

Abb. 5.17 Die fünf Regionen der BorderPane

top

left

center

bottom

right

5.4

Behälterelemente

Abb. 5.18 Die Klasse BorderPane illustriert. → Video K-5.4 Abb. 5.19 Das erzeugte JavaFX Programmfenster durch die Klasse MainView

125

126

5 Graphische Benutzeroberflächen (Teil 1) BorderPane top

bottom

center

MenuBar

Label

GridPane

Menu (1, 1)

MenuItem

MenuItem

Label

(2, 1)

ChoiceBox

(1, 2)

Label

(2, 2)

TextField

(1, 3)

Label

(2, 3)

(2, 5)

TextField

(2, 6)

HBox

Button

ToggleGroup

RadioButton

RadioButton

RadioButton

Abb. 5.20 Der Szenengraph zu unserem GUI

Der zugehörige Szenengraph ist in Abb. 5.20 gezeigt. Beachten Sie, dass in dieser Visualisierung genügend Informationen vorhanden sind, so dass eine Programmiererin das GUI eindeutig programmieren könnte mit allen nötigen Bezeichnern, allen Typen der Kontroll- und Behälterelemente und allfällige Zusatzinformationen (wie z. B. dass die drei RadioButton Objekte zu einer ToggleGroup gehören).

5.4.4

Andere Behälterelemente

Neben den acht JavaFX Klassen, welche Layouts ohne integrierte Funktionalität anbieten, existieren noch weitere Klassen, die als Behälter dienen können. Im Gegensatz zu den oben besprochenen Behältern, besitzen diese aber bereits gewisse Funktionalitäten. Die Klassen MenuBar und Menu haben wir in diesem Zusammenhang schon angesprochen – diese Elemente gehören zu den Kontrollelementen, weil sie zum Beispiel klickbar sind, aber sie sind eben auch Behälterelemente, das sie andere Elemente aufnehmen können. Im Folgenden betrachten wir ein weiteres Beispiel eines Behälters mit integrierter Funktion: Die Klasse TabPane. Eine TabPane besitzt selektierbare Reiter (engl. Tabs), zwischen denen hin- und hergewechselt werden kann. Dies ist insbesondere nützlich, wenn ein Programm verschiedene Sichten auf eine Anwendung erlauben soll. Damit wir den Einsatz einer TabPane sinnvoll illustrieren können, definieren wir zuerst eine weitere Schnittstelle zu den Spielerkarten. Mit der Klasse AddPlayerView haben wir bereits ein GUI zur Eingabe von Spielerdaten programmiert. In Abb. 5.21 ist die Klasse SearchPlayerView gezeigt, welche ein GUI zur Suche von Spielerkarten definiert. Diese erbt von der Klasse VBox und ordnet die folgenden drei Kontrollelemente vertikal an: • Ein Objekt vom Typ TextField • Ein Objekt vom Typ Button

5.4

Behälterelemente

127

Abb. 5.21 Die Klasse SearchPlayerView definiert ein weiteres GUI, das wir anzeigen lassen wollen, in einer VBox. → Video K-5.5

• Eine ListView, um PlayerCard Objekte in Listenform anzeigen zu können (beachten Sie die Typisierung der Instanzvariablen resultList mit ). Für den Moment zeigen wir in der ListView zwei Objekte vom Typ PlayerCard an, die wir direkt im Quellcode definieren. Hierzu instanziieren wir zwei PlayerCard Objekte und fügen diese der Liste hinzu: this.resultList.getItems().addAll(new PlayerCard("Cristiano", 3456), new PlayerCard("Lionel", 4562));

128

5 Graphische Benutzeroberflächen (Teil 1)

Später soll in dieser Listenansicht dann das Resultat einer vom Benutzer durchgeführten Suche angezeigt werden. Neben einigen bereits besprochenen Formatierungsanweisungen wird im Konstruktor SearchPlayerView mit this.setPadding(new Insets(10, 10, 10, 10));

der Inhalt des Behälters oben, rechts, unten und links mit je 10 Pixel Abstand vom Rand entfernt platziert.

Abb. 5.22 Die Klasse TabPane illustriert. → Video K-5.6

5.4

Behälterelemente

129

Da wir nun zwei GUIs (AddPlayerView und SearchPlayerView) definiert haben, können wir den Einsatz der Klasse TabPane illustrieren (siehe Abb. 5.22). Die eigene Klasse TabView erbt von der Klasse TabPane. Wir deklarieren zwei Instanzvariablen addPlayerView und searchPlayerView vom Typ AddPlayerView und SearchPlayerView – unsere beiden GUIs, die wir bereits programmiert haben. Zudem werden zwei Tab Objekte deklariert – addTab und searchTab – in diesen werden die beiden GUIs dann platziert werden. Im Konstruktor TabView werden die beiden GUIs addPlayerView und searchPlayerView instanziiert. Danach instanziieren wir die beiden Tab Objekte und fügen die GUIs mit der Methode setContent zu diesen hinzu: this.addPlayerView = new AddPlayerView(); this.addTab = new Tab("Hinzufügen"); this.addTab.setContent(this.addPlayerView);

Beachten Sie, dass Sie dem Konstruktor Tab eine Zeichenkette mitgeben können, um den resultierenden Reiter zu beschriften. Um dann die instanziierten und gefüllten Tab Objekte zu dieser TabView hinzuzufügen, verwenden wir die Methoden getTabs zusammen mit add oder addAll: this.getTabs().addAll(this.addTab, this.searchTab);

Schliesslich bestimmen wir mit der Methode setClosable, dass die beiden Tab Objekte nicht geschlossen werden können. Die Klasse MainView (siehe Abb. 5.23), ist fast identisch mit der vorherigen Version, die in Abb. 5.18 gezeigt ist (die drei Anpassungen sind in der Abbildung markiert). Statt dass wir ein AddPlayerView Objekt in die Region center der BorderPane setzen, wird jetzt die eben gerade definierte TabView instanziiert und mit der Methode setCenter im Zentrum der BorderPane platziert. Die Klasse MainView wird in der Methode start der Klasse PlayerCardApp als Ursprung root des GUIs definiert (siehe Abb. 5.24). Wird dieses Programm gestartet, wird ein Fenster wie es in Abb. 5.25 gezeigt ist, generiert. Der Benutzer des Programms kann nun zwischen den beiden Ansichten Hinzufügen und Suchen über die entsprechenden Reiter hin- und herwechseln. Das gesamte GUI ist in dem Szenengraphen in Abb. 5.26 zusammengefasst.

130

5 Graphische Benutzeroberflächen (Teil 1)

Aufgaben und Übungen zu Kap. 5 Theorieaufgaben 1. Was enthält der Wurzelknoten eines GUIs in JavaFX? Was entspricht der Bühne in JavaFX? Was zeigt diese Bühne an? 2. Skizzieren Sie den Szenengraphen zum Programm DiceApp aus Kap. 5. 3. Skizzieren Sie den Szenengraphen zu folgendem Programm.

4. Was sind die zwei Hauptunterschiede zwischen einer BorderPane und einer AnchorPane? 5. Wie fügen Sie einem Behälter vom Typ HBox Kontrollelemente hinzu? Machen Sie ein Beispiel (fügen Sie zum Beispiel ein Button und ein TextField Objekt zu Ihrem Behälter hinzu). 6. Deklarieren und instanziieren Sie ein Objekt vom Typ TabPane und fügen Sie diesem zwei Tab Objekte hinzu, die beide je eine leere VBox beinhalten.

5.4

Behälterelemente

131

7. Welche der folgenden Auswahlmöglichkeiten würden Sie mit einer Gruppe von RadioButton Objekten programmieren. Begründen Sie Ihre Antwort! a) b) c) d)

Zutaten für einen Pizza Lieblingssportart Sportarten, die Sie gelegentlich am TV schauen Alter: 0–12, 13–18, 19–29, 30–50, 50–65, 65+

8. Deklarieren und instanziieren Sie ein Objekt vom Typ ChoiceBox, welches die Werte "Eins", "Zwei", "Drei" zur Auswahl anzeigt. 9. Erstellen Sie einen Spickzettel (maximal eine A4 Seite), auf dem Sie in tabellarischer Form alle Methoden der besprochenen Kontroll- und Behälterelemente für Sie verständlich zusammenfassen. Dies könnte zum Beispiel folgendermassen aussehen: Klasse Label

K/B K

TextField

K

Button

K

Methoden new Label("Beschriftung"); → Konstruktor setText("Update"); → Text in Label ändern new TextField(); → Konstruktor getText(); → Text auslesen setMaxWidth(25); → Breite des Feldes definieren ...

Java Übungen 1. Programmieren Sie das folgende GUI. Erstellen Sie hierzu eine Klasse BoxView, welche Sie dann in der Klasse Main als Wurzel root instanziieren und der Szene übergeben (siehe unten).

→ Video V-5.1

132

5 Graphische Benutzeroberflächen (Teil 1)

VBox

Label

HBox

HBox

Button

ToggleGroup

Label

TextField

Label

TextField

Label

TextField

RadioButton

RadioButton

2. Programmieren Sie das folgende GUI. Erstellen Sie hierzu eine Klasse AccountView, welche Sie dann in einer Hauptklasse als Wurzel root instanziieren und der Szene übergeben.

→ Video V-5.2

5.4

Behälterelemente

133

GridPane GridPane

(1,1)

(1,2)

Label

Label

(1,3)

TextField

(2,3)

(1,4)

Button

(2,4)

(2,5)

Button

TextField

Button

3. Programmieren Sie das folgende GUI. Erstellen Sie hierzu eine Klasse HighLowView, welche Sie dann in einer Hauptklasse als Wurzel root instanziieren und der Szene übergeben.

→ Video V-5.3

BorderPane

VBox

MenuBar

Menu

MenuItem

Label

MenuItem

Label

TextField

Button

Label

Button

134

5 Graphische Benutzeroberflächen (Teil 1)

Abb. 5.23 Die Klasse MainView (eine BorderPane) platziert die TabView in der Region center. → Video K-5.6 (Fortsetzung)

5.4

Behälterelemente

135

Abb. 5.24 Die Hauptklasse unseres Programmes mit den Methoden main und start. → Video K-5.6 (Fortsetzung)

Abb. 5.25 Die BorderPane enthält eine MenuBar (in der Region top), eine TabPane mit den zwei GUIs AddPlayerView und SearchPlayerView (in der Region center) und ein Label (in der Region bottom)

136

5 Graphische Benutzeroberflächen (Teil 1) BorderPane top

Label

TabPane

MenuBar

Tab

Tab

Menu

MenuItem

bottom

center

MenuItem

VBox

GridPane

Button

TextField (1, 1)

Label

ChoiceBox

(2, 1)

(1, 2)

Label

(2, 2)

TextField

(1, 3)

Label

(2, 3)

(2, 5)

TextField

(2, 6)

HBox

Button

ToggleGroup

RadioButton

Abb. 5.26 Der Szenengraph zur gesamten GUI

.

RadioButton

RadioButton

ListView

6

Klassen des Java API Verwenden (Teil 2)

In diesem Kapitel wenden wir uns nochmals einigen Klassen aus dem Java API zu. Die in diesem Kapitel besprochenen Klassen sind dabei so ausgewählt, dass man an ihnen einige erweiterte Konzepte der Programmierung illustrieren kann. Insbesondere betrachten wir die Klasse Math, welche sogenannt statische Methoden und Konstanten für verschiedene mathematische Berechnungen zur Verfügung stellt. Danach betrachten wir eine Klasse zur Formatierung von numerischem Text – die Klasse DecimalFormat. Als nächstes führen wir sogenannte Wrapper-Klassen ein (Integer, Double, Boolean, etc.), welche die primitiven Datentypen von Java mit einer Klasse „umhüllen“. Schliesslich betrachten wir die Klasse ArrayList, die es uns ermöglicht, Listen von Objekten anzulegen und zu verwalten.

6.1

Die Klasse Math

Die Klasse Math aus dem Java API bietet eine Vielzahl mathematischer Funktionen sowie einige nützliche Konstanten an. Diese Klasse gehört dem Package java.lang an (muss also nicht via import Anweisung importiert werden). Einige wichtige Methoden der Klasse Math sind: • static double abs(double num): Gibt den Absolutwert von num zurück. • static double cos(double angle): Gibt den Cosinus des Winkels angle zurück (analog dazu existieren die Methoden sin und tan, um den Sinus und Tangens eines Winkels berechnen zu können). Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_6.

© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_6

137

138

6 Klassen des Java API Verwenden (Teil 2)

• static long round(double num): Rundet num auf die am nächsten liegende ganze Zahl auf oder ab. • static double ceil(double num): Gibt die kleinste ganze Zahl zurück, die grösser oder gleich num ist (aufrunden). • static double floor(double num): Gibt die grösste ganze Zahl zurück, die kleiner oder gleich num ist (abrunden). • static double pow(double x, double y): Gibt xy zurück. √ • static double sqrt(double x): Gibt x zurück. • static double min(double x, double y): Gibt das Minimum von x und y zurück (analog dazu existiert die Methode max). • static double random(): Gibt eine Zufallszahl zwischen 0.0 (inklusive) und 1.0 (exklusive) zurück. Das Besondere an der Klasse Math ist, dass alle Methoden statisch deklariert sind (statische Methoden werden auch Klassenmethoden genannt). Dies sieht man am Schlüsselwort static, welches nach dem Sichtbarkeitsmodifikator im Methodenkopf zu finden ist. Zum Beispiel sieht der Methodenkopf der Methode pow folgendermassen aus: public static double pow(double x, double y)

Statische Methoden können aufgerufen werden, ohne dass vorher ein Objekt aus der entsprechenden Klasse instanziiert werden muss. Solche Methodenaufrufe programmiert man, indem der Klassenname (statt der Objektname) vor den Punktoperator und den Methodenbezeichner gestellt wird. Beispiel 6.1 Um zum Beispiel in einem Programm einen Wert num auf die nächste ganze Zahl abzurunden (und diesen Wert einer Variablen rounded zuzuweisen), können wir folgende Programmieranweisung definieren1 : int rounded = (int) Math.floor(num);

In diesem Fall verwenden wir also den Klassennamen Math vor dem Punktoperator statt dem Bezeichner eines instanziierten Objektes. Die Werte, welche die Methoden der Klasse Math zurückgeben, können direkt in arithmetischen Ausdrücken weiterverwendet werden.

1 Beachten Sie den Cast Operator, der die Rückgabe von floor – ein double – in einen int konvertiert.

6.1

Die Klasse Math

139

Beispiel 6.2 Die folgende Programmieranweisung

Math.sqrt(1 - Math.pow(Math.sin(alpha), 2)); entspricht zum Beispiel der Formel 

1 − (sin α)2

Beachten Sie, dass man den Methoden der Klasse Math eine ganze Zahl als Parameter mitgeben kann (obschon diese eigentlich einen Wert vom Typ double erwarten). Java konvertiert in diesem Fall die ganze Zahl automatisch in einen double. Folgende Anweisungen sind also beispielsweise erlaubt: Math.pow(2, 3); Math.pow(2, 0.5);

Neben mathematischen Funktionen speichert die Klasse Math auch wichtige mathematische Konstanten. Diese sind ebenfalls statisch deklariert und können somit auch via Klassennamen referenziert werden. Zum Beispiel kann die Konstante π folgendermassen verwendet werden, um die Fläche eines Kreises mit Radius r zu berechnen: double area = Math.pow(r, 2) * Math.PI;

Beispiel 6.3 In Abb. 6.1 ist ein interaktives Programm gezeigt (ohne graphische Oberfläche), das die Lösungen einer quadratischen Gleichung der Form ax 2 + bx + c findet. Das Programm verwendet hierzu die Lösungsformel2 √ b2 − 4ac x1,2 = . 2a Eine mögliche Ein- und Ausgabe des Programmes wäre bspw: −b ±

Koeffizient a von x^2: 5 Koeffizient b von x: 3 2 Diese Formel ist auch als „Mitternachtsformel“ bekannt, weil Schülerinnen und Schüler sie aufsagen können sollen, selbst wenn man sie um Mitternacht weckt und nach der Lösungsformel fragt.

140

6 Klassen des Java API Verwenden (Teil 2)

Abb. 6.1 Mit Hilfe der Klasse Math können zum Beispiel die Lösungen einer quadratischen Gleichung gefunden werden. →Video K-6.1

Konstante c: -4 Lösung: x_1: 0.6433981132056603 x_2: -1.2433981132056604

6.2

Die Klasse DecimalFormat

Es ist oftmals nötig, Text zu formatieren, so dass dieser korrekt oder „schön“ angezeigt wird. Das Java API enthält einige Klassen, welche verschiedene Formatierungsmöglichkeiten anbieten. Wir betrachten im Folgenden die Klasse DecimalFormat, welche das Formatieren von Zahlen nach einem benutzerdefinierten Muster ermöglicht (diese Klasse ist Bestandteil des Packages java.text und muss importiert werden).

6.2

Die Klasse DecimalFormat

141

Die drei wichtigsten Methoden der Klasse DecimalFormat sind: • DecimalFormat(String p): Der Konstruktor der Klasse nimmt als Parameter eine Zeichenkette p entgegen, die das Muster repräsentiert, nachdem die Ausgaben formatiert werden sollen. • String format(double num): Formatiert die Zahl num gemäss dem aktuell definierten Muster. • void applyPattern(String p): Definiert das Formatierungsmuster neu. Das Muster p, das dem Konstruktor DecimalFormat oder der Methode applyPattern übergeben wird, kann sehr ausgeklügelt sein und verschiedene Sonderzeichen beinhalten. Für einfache Formatierungen sind die Null "0", die Interpunktionszeichen "." und "," sowie das Zeichen "#" wichtig. Zum Beispiel definiert die Zeichenkette "#,#00.0#" folgendes Formatierungsmuster: • Komma als Tausendertrennzeichen (das Zeichen "#" wird hierbei als Platzhalter verwendet), • mindestens 2 ganzzahlige Ziffern (erzwungen mit "00.") • mindestens 1 Nachkommastelle (erzwungen mit ".0") • maximal 2 Nachkommastellen Mit diesem Muster werden also bspw. folgende Formatierungen vorgenommen: • • • •

1234.564 → 1,234.56 14.032 → 14.03 1.803 → 01.8 17 → 17.0

Beispiel 6.4 Das Programm Circle in Abb. 6.2 berechnet die Fläche und den Umfang eines Kreises mit einem Radius, der vom Benutzer des Programmes eingegeben wird. Die Resultate der Berechnung werden mit dem Objekt formater der Klasse DecimalFormat formatiert. Eine mögliche Ein- und Ausgabe des Programmes lautet: Radius: 2.3 Kreisfläche: 16.62 Kreisumfang: 14.45

142

6 Klassen des Java API Verwenden (Teil 2)

Abb. 6.2 Mit der Klasse DecimalFormat können Zahlen nach bestimmten Mustern formatiert werden. →Video K-6.2

Die Formatierungsmuster der Klasse DecimalFormat dürfen auch weitere Zeichen enthalten. Zum Beispiel: DecimalFormat formater = new DecimalFormat("CHF 0.00");

Das Objekt formater formatiert dann eine Zahl wie bspw. 84.5 zu CHF 84.50. Zudem erlaubt die Klasse DecimalFormat auch die Formatierung als Prozentwert: DecimalFormat formater = new DecimalFormat("00.00 %");

Dieses Objekt formatiert eine Zahl wie bspw. 0.556 zu 55.60 %.

6.3 Wrapper Klassen

6.3

143

Wrapper Klassen

Wie bereits diskutiert, speichert Java Daten in zwei verschiedenen Variablentypen: mit Hilfe von primitiven Variablen (wie z. B. int, double, char oder boolean) oder mit Objektvariablen. Die Tatsache, dass Java zwei Kategorien von Variablen verwendet, kann in gewissen Situationen problematisch sein. Beispiel 6.5 Sie erinnern sich an die Typisierung der Klasse ChoiceBox aus dem letzten Kapitel. Zur Erinnerung: Folgende Anweisung deklariert und instanziiert zum Beispiel ein Objekt vom Typ ChoiceBox, das String Objekte aufnehmen und anzeigen kann: ChoiceBox choice = new ChoiceBox();

Typisierungen erfordern die Angabe einer Klasse in spitzen Klammern – ein primitiver Datentyp kann hierfür nicht verwendet werden. Folgende Anweisung ist also zum Beispiel nicht erlaubt: ChoiceBox choice = new ChoiceBox();

Java bietet für dieses und ähnliche Probleme die Möglichkeit, eine primitive Variable mit einem Objekt zu umhüllen (engl. to wrap). Sogenannte Wrapper-Klassen repräsentieren oder umhüllen jeweils einen bestimmten primitiven Datentypen. Beispielsweise könnten Objekte der Klasse Integer in Abb. 6.3 ganze Zahlen (also int Werte) repräsentieren. Etwas genauer: Ein Objekt vom Typ Integer speichert einen Wert vom Typ int als Instanzvariable value. Der Konstruktor dieser Wrapper-Klasse akzeptiert den primitiven Datenwert, den das Objekt umhüllen soll, als Parameter. Zum Beispiel: Integer number = new Integer(40); Nach dieser Deklaration und Instanziierung repräsentiert das Objekt number nun die ganze Zahl 40 als Objekt vom Typ Integer. Die so umhüllte Zahl kann nun überall dort in einem Programm verwendet werden, wo ein Objekt statt ein primitiver Datentyp nötig ist. Tatsächlich müssen wir keine eigenen Wrapper-Klassen schreiben, da im Java API für jeden primitiven Datentypen bereits eine zugehörige Wrapper-Klasse vorhanden ist (alle definiert im Package java.lang): Primitiver Datentyp byte

Wrapper Klasse Byte

144 short int long float double char boolean

6 Klassen des Java API Verwenden (Teil 2) Short Integer Long Float Double Character Boolean

Die Hauptaufgabe der Wrapper-Klassen ist es, einen primitiven Datenwert als Variable in einem Objekt zu speichern. Hierzu wird der zu umhüllende Wert dem entsprechenden Konstruktor mitgegeben.

Beispiel 6.6 Folgende Anweisungen umhüllen den Wahrheitswert true bzw. die Zahl 4.567 mit einem Objekt vom Typ Boolean und vom Typ Double: new Boolean(true); new Double(4.567);

Das Hin- und Herwechseln zwischen primitivem Datentyp und dem entsprechenden Wrapper Objekt ist in Java sehr einfach. Autoboxing bezeichnet die automatische Konvertierung eines primitiven Datentypen in ein entsprechendes Wrapper Objekt.

Abb. 6.3 Mit der Klasse Integer umhüllen wir eine ganze Zahl in einem Objekt

6.3 Wrapper Klassen

145

Beispiel 6.7 Mit folgender Programmieranweisung wird der int Wert 69 in ein Integer Objekt konvertiert:

Integer obj = 69; // automatische Instanziierung Die umgekehrte Operation (genannt Unboxing) geschieht auch automatisch. Beispiel 6.8 Mit folgenden Anweisungen wird ein umhüllter Wert aus dem Wrapper Objekt obj extrahiert: Integer obj = new Integer(69); int num = obj; // automatische Extraktion

Hinweis: Autoboxing und Unboxing ist natürlich nur zwischen primitiven Datentypen und den zugehörigen Wrapper-Klassen möglich. Neben dem Umhüllen von primitiven Datentypen bieten die Wrapper-Klassen auch zahlreiche Methoden zur Verwaltung und Veränderung der gespeicherten primitiven Daten an. Viele dieser Methoden sind statisch deklariert und können somit ohne instanziiertes Objekt verwendet werden. Beispielsweise enthält die Klasse Integer eine statische Methode mit Bezeichner parseInt, die eine ganze Zahl, die in einem Objekt vom Typ String gespeichert ist, in den korrespondierenden int Wert konvertiert. Beispiel 6.9 In folgenden Anweisungen wird die statische Methode parseInt dazu verwendet, das Objekt str vom Typ String in einen int Wert mit Bezeichner num zu konvertieren: String str = "987"; int num = Integer.parseInt(str);

Die Variable num speichert nun die ganze Zahl 9873 . Die anderen Wrapper-Klassen bieten ähnliche Methoden an (z. B. bieten die Klassen Double oder Boolean die Methoden parseDouble oder parseBoolean an).

3 Befindet sich in str keine ganze Zahl, löst parseInt einen Fehler zur Laufzeit aus.

146

6 Klassen des Java API Verwenden (Teil 2)

Die umgekehrte Operation – also das Konvertieren vom primitiven Datentypen in eine Zeichenkette – wird auch von allen Wrapper-Klassen angeboten – mit der statischen Methode toString. Beispiel 6.10 Folgende Anweisungen konvertieren eine ganze Zahl i in eine Zeichenkette: int i = 987; String str = Integer.toString(i);

Einige der Wrapper-Klassen enthalten zudem statische Konstanten, die oftmals hilfreich sein können beim Programmieren. Beispielsweise enthalten die Klassen Integer und Double je zwei statische Konstanten MIN_VALUE und MAX_VALUE, welche den kleinstund grösstmöglichen int bzw. double Wert speichern.

6.4

Die while-Anweisung

Wird ein Programm ausgeführt, so werden im Prinzip alle Programmieranweisungen der Methode main genau einmal ausgeführt (siehe Abb. 6.4). Wir haben schon gesehen, dass man mit einer if-Anweisung gewisse Teile des Quellcodes überspringen kann, bzw. aus zwei (oder mehr) Optionen eine ausführen lassen kann. In diesem Abschnitt wollen wir nun erstmals Schleifen programmieren. Schleifen erlauben uns, gewisse Programmieranweisungen mehrfach ausführen zu lassen (ohne diese mehrfach zu programmieren). Eine while-Anweisung evaluiert eine Boolesche Bedingung genau gleich wie eine ifAnweisung. Wenn die Bedingung wahr ist, so wird die zugehörige Programmieranweisung ausgeführt.

Kontrollfluss des Programmes

main

main

main if {

while {

} else { }

}

Abb. 6.4 Der Kontrollfluss eines Programmes kann mit if- und while-Anweisungen beeinflusst werden

6.4

Die while-Anweisung

147

Abb. 6.5 Die Logik von while-Schleifen

Bedingung

wahr falsch Anweisung

Anders als bei einer if-Anweisung wird der Kontrollfluss danach aber nicht zur nächsten Anweisung unterhalb der while-Anweisung springen, sondern die Boolesche Bedingung wird nochmals evaluiert. Falls diese immer noch wahr ist, wird die zugehörige Programmieranweisung nochmals ausgeführt. Diese Wiederholung wird solange fortgesetzt, bis die Bedingung irgendwann falsch wird. Erst dann wird mit der nächsten Anweisung unterhalb der while-Anweisung fortgefahren (siehe Abb. 6.5). Beispiel 6.11 Die folgende Schleife gibt zum Beispiel die Werte von 1 bis 3 aus: int count = 1; while (count = 0 sein! Anzahl Schüsse auf’s Tor eingeben: 28 Anzahl gehaltener Schüsse eingeben: 31 Ungültiger Wert! Anzahl gehaltener Schüsse eingeben: 25 Fangquote in Prozent: 89.29 %

Beachten Sie den Cast auf Zeile 30 im Quellcode – weshalb ist dieser hier zwingend nötig?

150

6 Klassen des Java API Verwenden (Teil 2)

Abb. 6.7 Die Klasse SavePercentage nutzt eine while-Schleife zur Validierung von Benutzereingaben. → Video K-6.4

6.4.1

Endlosschleifen

Es liegt in der Verantwortung der Programmiererin, dass die Boolesche Bedingung einer Schleife irgendwann falsch wird. Wenn diese nie falsch wird, so werden die Anweisungen der while-Schleife für immer ausgeführt (bzw. bis das Programm abgebrochen wird). Eine solche Situation nennt man Endlosschleife und ist ein recht häufiger Programmierfehler.

6.4

Die while-Anweisung

151

Beispiel 6.14 Hier ein Beispiel einer Endlosschleife: int count = 1; while (count MAX) ? points + 1 : points * 2;

Vor dem Fragezeichen (?) steht eine Boolesche Bedingung. Danach folgen zwei Ausdrücke, die mit einem Doppelpunkt (:) getrennt sind. Der gesamte Operator gibt den Wert des ersten Ausdruckes zurück, wenn die Boolesche Bedingung wahr ist und sonst den zweiten Wert. Üblicherweise möchte man etwas mit dieser Rückgabe tun, diese z. B. einer Variablen zuweisen: points = (points > MAX) ? points + 1 : points * 2;

Man kann diese Anweisung auch in eine if-else Anweisung übersetzen: if (points > MAX) points = points + 1; else points = points * 2;

Beispiel 7.4 Im Programm ConditionalTest (siehe Abb. 7.2) wird die Ausgabe der Anzahl Versuche mit einem Conditional Operator grammatikalisch korrekt definiert. Bei einer Eingabe von 1 ist die Ausgabe z. B.: Anzahl: 1 1 Versuch

Während bei einer Eingabe von 0 oder einer Eingabe, die grösser als 1 ist, die Ausgabe angepasst wird: Anzahl: 17

17 Versuche

168

7 Bedingungen und Schleifen

Abb. 7.2 Das Programm ConditionalTest demonstriert den Nutzen eines Conditional Operators. → Video K-7.2

7.3

Die do-Anweisung

Sie erinnern sich, dass while-Anweisungen zuerst eine Boolesche Bedingung evaluieren und danach die Anweisungen in der zugehörigen Blockanweisung so lange wiederholen, wie die Bedingung wahr ist. Eine do-Schleife startet mit dem reservierten Wort do. Danach folgt die Anweisung (oder mehrere Anweisungen in einem Block), die ausgeführt werden soll. Mit einer Booleschen Bedingung wird am Ende der do-Schleife überprüft, ob die do-Anweisung weiter ausgeführt werden soll oder nicht. Beispiel 7.5 Die folgende Schleife gibt die Werte von 1 bis 3 mit einer do-Schleife aus: int count = 1; do { System.out.println(count); count++; } while (count 100) { System.out.print("Anzahl erreichte Punkte (0 bis 100): "); points = scan.nextInt(); }

Offensichtlich muss sowohl die print Anweisung als auch das Einlesen mit nextInt in jedem Fall mindestens einmal durchgeführt werden. Mit Hilfe einer do-Schleife lässt sich das eleganter programmieren: do { System.out.print("Anzahl erreichte Punkte (0 bis 100): "); points = scan.nextInt(); } while (points < 0 || points > 100);

7.4

Die for-Anweisung

Die while- und do-Anweisung sind dann nützlich, wenn man initial nicht weiss, wie oft eine bestimmte Schleife durchgeführt werden soll. Die for-Anweisung ist eine weitere Möglichkeit, Anweisungen mehrmals ausführen zu lassen. Diese Schleife ist insbesondere in Situationen geeignet, in denen bereits zu Beginn klar ist, wie oft die Schleife durchlaufen werden soll. Beispiel 7.7 Folgendes Code Fragment gibt die Zahlen 1 bis 3 mit Hilfe einer for-Schleife aus: for (int count = 1; count b) then 6: a = a - b 7: else 8: b = b - a 9: end if 10: end while 11: ggT = a 12: end if 13: return ggT

Beispiel 10.2 Wir suchen zum Beispiel den ggT von a = 44 und b = 12: Sobald a = b ist, wird bei der nächsten Iteration der while-Schleife b = 0 und die Schleife bricht ab – der ggT befindet sich nun in a.

a a a b a b

= = = = = =

a a a b a b

– – – – – –

b b b a b a

a 44 32 20 8 8 4 4

b 12 12 12 12 4 4 0

242

10 Methoden Planen und Programmieren

Abb. 10.2 Der Konstruktor der Klasse RationalNumber ruft die Support-Methode reduce auf, welche die Methode gcd aufruft. →Video K-10.1

Beispiel 10.3 In Abb. 10.2 sind die beiden Algorithmen nun als Methoden reduce und gcd in Java umgesetzt. Im Konstruktor RationalNumber wird die Methode reduce aufgerufen. Diese Methode wiederum ruft als erstes die Methode gcd auf, welche den grössten gemeinsamen Teiler gcd des Zählers und Nenners berechnet und zurückgibt. Mit Hilfe des berechneten gcd kann der Bruch dann gekürzt werden.

10.2

Parameter an Methoden Übergeben

243

Beachten Sie, dass beide Methoden reduce und gcd mit Sichtbarkeit private deklariert sind. Dies sind reine Support-Methoden, welche nicht von Aussen aufrufbar sein sollen (jede rationale Zahl wird automatisch bei deren Instanziierung gekürzt). Natürlich könnte man alle nötigen Anweisungen zum Kürzen der rationalen Zahl auch direkt im Konstruktor programmieren. Dies würde aber bedeuten, dass der Konstruktor die Verantwortung für drei Aufgaben übernimmt: • Berechnen des grössten gemeinsamen Teilers ggT des Zählers und Nenners • Kürzen des Bruches mit dem ggT • Initialisieren der Instanzvariablen in einem neuen Objekt vom Typ RationalNumber Im besten Fall übernimmt jede Methode die Verantwortung für genau eine Aufgabe, während Teilaufgaben an Support-Methoden delegiert werden. Dies hat auch den Vorteil, dass mehrere Methoden, die ähnliche Teilprobleme lösen müssen, auf die gleichen Support-Methoden zugreifen können. Zusammengefasst: Komplexes Verhalten sollten Sie wenn möglich in mehrere, kleinere Methoden zerlegen, um . . . 1. . . . besser verständlichen Quellcode zu erstellen. 2. . . . duplizierten Quellcode zu vermeiden.

10.2

Parameter an Methoden Übergeben

Ein weiterer wichtiger Aspekt bei der Planung und Programmierung von Methoden ist die Parameterübergabe. Parameterübergaben sind implizite Zuweisungen: Dem formalen Parameter (der Variablen, die im Methodenkopf deklariert ist) wird eine Kopie des Wertes des tatsächlichen Parameters zugewiesen. Im Fall von primitiven Datentypen ist der formale Parameter dabei eine separate Kopie des Wertes, der übergeben wurde. Das heisst, Änderungen, die am formalen Parameter gemacht werden, haben keinen Einfluss auf den Wert des tatsächlichen Parameters. Geht der Kontrollfluss zurück zur aufrufenden Methode, besitzt der tatsächliche Parameter immer noch den gleichen Wert wie vorher. Beispiel 10.4 Nehmen Sie an, dass Sie ein Methode changeValue programmiert haben, die einen Parameter vom Typ int erwartet. In dieser Methode wird der übergebene Wert dann verändert (z. B. auf den Wert –1).

244

10 Methoden Planen und Programmieren

Die Ausgabe der folgenden Anweisungen ist aber 17, da die Änderung nur am formalen Parameter durchgeführt wurde. int num = 17; this.changeValue(num); System.out.print(num);

Eine Möglichkeit, die Variable num mit Hilfe der Methode changeValue tatsächlich zu verändern, ist es, den veränderten Wert von changeValue zurückzugeben und diese Rückgabe dann num zuzuweisen. Also beispielsweise: int num = 17; num = this.changeValue(num); System.out.print(num);

Wenn wir einer Methode aber ein Objekt als Parameter übergeben, übergeben wir lediglich eine Referenz auf dieses Objekt (und nicht das Objekt selber). Der Wert, der also in den formalen Parameter kopiert wird, ist eine Adresse. Das heisst, in diesem Fall werden der formale und der tatsächliche Parameter Aliase voneinander. Wenn wir den Status des Objektes dann innerhalb der aufgerufenen Methode ändern, so ändern wir auch den Status des tatsächlichen Parameters. Beispiel 10.5 Das Programm in Abb. 10.3 illustriert diese Unterschiede bei der Parameterübergabe an Methoden (nach einer Idee aus [1]). Die Klasse ParameterTester enthält die Methode main, welche die statische Methode changeValues aufruft. Zwei der tatsächlichen Parameter, die an changeValues übergeben werden, sind Objekte der Klasse RationalNumber, die wir schon in früheren Beispielen verwendet haben (in Abb. 10.4 ist eine reduzierte Version der Klasse zu sehen). Der erste tatsächliche Parameter ist ein primitiver Datentyp vom Typ double. Beim Aufruf der Methode changeValues werden nun die drei tatsächlichen Parameter num1, num2 und num3 in die formalen Parameter f1, f2 und f3 kopiert. Innerhalb der Methode changeValues werden die drei formalen Parameter verändert. Der Variablen f1 wird ein neuer double Wert zugewiesen, die Variable f2 wird mit der Methode setNumerator verändert, während der letzten Variablen f3 ein neu instanziiertes RationalNumber Objekt zugewiesen wird. Danach kehrt der Kontrollfluss zurück zu main und die Werte der tatsächlichen Parameter werden erneut ausgegeben.

10.2

Parameter an Methoden Übergeben

Abb. 10.3 Die Klasse ParameterTester demonstriert den Effekt von Parameterübergaben

245

246

10 Methoden Planen und Programmieren

Abb. 10.4 Die Klasse RationalNumber

Das Programm führt zu folgender Ausgabe: Initiale Werte: num1 num2 0.75 3/4

num3 1/2

Werte bei Start der Methode: f1 f2 f3 0.75 3/4 1/2 Werte beim Verlassen der Methode: f1 f2 f3 -999.0 1/4 7/8 Werte am Ende von main: num1 num2 num3 0.75 1/4 1/2

10.3

Methoden Überladen

247

Die double Variable num1 wird also nicht verändert, da die Änderung an einer echten Kopie f1 von num1 vorgenommen wurde. Der letzte Parameter num3 referenziert immer noch das Originalobjekt mit dem Originalwert. Dies kommt daher, weil das neue RationalNumber Objekt, das in der Methode changeValues instanziiert wurde, nur dem formalen Parameter f3 zugewiesen worden ist. Kehrt die Methode zurück, wird f3 wieder „zerstört“ (d. h. an den Garbage Collector übergeben). Die einzige permanente Änderung ist die, die am zweiten tatsächlichen Parameter num2 vorgenommen wurde. Die Änderung an f2 wirkt sich auch auf dem kopierten Objekt num2 aus (da diese Aliase voneinander sind).

10.3

Methoden Überladen

In Java kann man in einer Klasse den gleichen Bezeichner für unterschiedliche Methoden verwenden. Diese Technik heisst Methodenüberladung (engl. method overloading). Methodenüberladung ist insbesondere hilfreich, wenn man gleiche Methoden für unterschiedliche Datentypen oder unterschiedliche Anzahlen Parameter programmieren möchte. Wird eine Methode durch deren Bezeichner aufgerufen, wird der Kontrollfluss an diese Methode übergeben. Wird die letzte Zeile in der aufgerufenen Methode erreicht, kehrt der Kontrollfluss an die Stelle des Quellcodes zurück, wo der Aufruf herkam. Java muss bei jedem Aufruf einer Methode fähig sein, zu entscheiden, wo der Kontrollfluss hin springen soll. Wenn also durch Methodenüberladung ein Methodenname für zwei oder mehr Methoden identisch ist, brauchen wir zusätzliche Informationen, um eindeutig bestimmen zu können, welche Methode tatsächlich ausgeführt werden soll. In Java geschieht dies über die Parameter. Alle Methoden, die den gleichen Bezeichner haben, müssen sich zwingend bezüglich deren formalen Parameter unterscheiden (also zum Beispiel eine andere Anzahl Parameter besitzen oder andere Datentypen verwenden). Wir können in einer Klasse beispielsweise folgende Methode programmieren: public int sum(int num1, int num2) { return num1 + num2; }

In der gleichen Klasse könnten wir dann eine zweite Methode mit gleichem Bezeichner aber mit anderen Datentypen für die Parameter programmieren: public double sum(double num1, double num2) { return num1 + num2; }

248

10 Methoden Planen und Programmieren

Oder eine dritte Methode mit gleichem Bezeichner aber einer anderen Anzahl Parameter: public int sum(int num1, int num2, int num3) { return num1 + num2 + num3; }

Wird jetzt die Methode sum aufgerufen, wird Java überprüfen, wie viele tatsächliche Parameter übergeben werden bzw. welche Datentypen die Parameter besitzen. Dann wird die Methode ausgeführt, bei der die tatsächlichen Parameter mit den formalen Parametern übereinstimmen. Beispielsweise wird mit dem Aufruf sum(25, 69, 13);

die dritte Version der Methode sum aufgerufen. Während sum(25.0, 69.0);

die zweite Version aufruft. Der Bezeichner einer Methode zusammen mit der Anzahl, den Datentypen und der Ordnung der Parameter heisst Signatur einer Methode. Java verwendet immer die Signatur, um die korrekte Methode aufzurufen. Versucht man zwei Methoden mit gleicher Signatur in einer Klasse zu programmieren, wird der Kompilierer eine Fehlermeldung generieren – innerhalb der gleichen Klasse müssen Methodensignaturen eindeutig sein. Beachten Sie, dass der Datentyp der Rückgabe nicht zur Signatur einer Methode gehört. Das heisst, zwei überladene Methoden können sich nicht nur im Rückgabetyp unterscheiden (der Rückgabetyp wird ja beim Aufruf einer Methode nicht mitgegeben und kann somit nicht zur Unterscheidung von überladenen Methoden berücksichtigt werden). Die Methode println ist ein typisches Beispiel einer mehrfach überladenen Methode. Hier ein paar Beispiele von Signaturen: • println(String s) • println(int i) • println(double d) • println(boolean b)

Das heisst, dass die folgenden zwei Anweisungen tatsächlich zwei unterschiedliche Methoden mit dem gleichen Bezeichner aufrufen: System.out.println("Anzahl Fehler: "); System.out.println(17);

10.4

Ein Zusammenfassendes Beispiel

249

Wäre Überladung nicht erlaubt, müssten für alle Versionen einer Methode jeweils eigene Bezeichner gewählt werden – dies könnte sehr schnell unübersichtlich und mühsam werden (z. B. gäbe es dann die Methoden printString, printInt, etc. statt mehrere überladene Methoden mit dem Bezeichner print). Hinweis: Wir haben bereits in vorderen Kapiteln Überladung von Methoden verwendet, nämlich bei der Definition verschiedener Konstruktoren. Wir haben zum Teil mehrere Konstruktoren für eine Klasse programmiert, die sich jeweils in den formalen Parametern unterschieden haben. Überladene Konstruktoren erlauben einem, Objekte auf unterschiedliche Arten zu instanziieren.

10.4

Ein Zusammenfassendes Beispiel

Wir betrachten in diesem Abschnitt ein Beispiel, welches die verschiedenen Aspekte der Methodenprogrammierung nochmals illustrieren und zusammenfassen soll.

10.4.1 Organisieren der Klassen In Kap. 5 haben wir eine graphische Oberfläche mit unterschiedlichen Behältern und Kontrollelementen programmiert. Insbesondere haben wir die Klassen MainView, TabView, AddPlayerView und SearchPlayerView programmiert. Zudem haben wir eine Klasse PlayerCardApp programmiert, welche die verschiedenen Ansichten in einem Fenster darstellt. In den weiteren Kapiteln des Buches haben wir dann nach und nach eine recht komplexe Struktur von Klassen zur Verwaltung von Spielerkarten programmiert. Diese Struktur umfasst die Klassen Player, PlayerCard, PlayerCardCollection und die beiden enum Nationality und Position. Zudem haben wir eine Hilfsklasse Initializer programmiert (zum Einlesen einer Datei mit Spielerinformationen) und die Klasse PlayerCardAppWithoutGUI, um die verschiedenen Klassen zu testen. Diese Klassen gehören zwar alle zum selben Projekt, sollten aber in unterschiedlichen Packages organisiert werden. Eine mögliche Organisation ist in Abb. 10.5 gezeigt. Alle Klassen, die zum Erstellen der graphischen Oberfläche verwendet werden, sind im Package view organisiert. Im Package util haben wir in diesem Beispiel die Hilfsklasse Initializer platziert. Alle Klassen, die unsere Datenstruktur und deren grundlegenden Funktionalitäten modellieren (Player, PlayerCard, PlayerCardCollection und die beiden enum Nationality und Position) sind im Package model zu finden. Schliesslich ist in diesem Beispiel noch ein Package application mit den Hauptklassen PlayerCardApp bzw. PlayerCardAppWithoutGUI und ein Package resources mit der Textdatei players.txt definiert.

250

10 Methoden Planen und Programmieren

Abb. 10.5 Eine mögliche Organisation der Klassen in verschiedene Packages. →Video K-10.2

Die genaue Definition der Aufteilung der einzelnen Klassen in unterschiedliche Packages kann natürlich von Projekt zu Projekt variieren. Eine wichtige Regel, die Sie auf jeden Fall beachten sollten, ist, dass die Klassen zur Erstellung der graphischen Oberflächen strikte von den restlichen Klassen getrennt werden sollten. Organisiert man die Klassen in unterschiedlichen Packages, so muss in jeder Klasse noch vor den import-Anweisungen, der entsprechende Name des Packages angegeben werden. Bei einer Klasse, die zum Package model gehört, ist die erste Zeile somit zwingend (siehe auch Abb. 10.6): package model; Werden dann in einer Klasse bestimmte Klassen aus anderen Packages benötigt, müssen diese importiert werden. Dies geschieht genau gleich wie bei Importierungen von Klassen aus dem Java API mit einer import-Anweisung (siehe auch Abb. 10.6): import model.Nationality;

Abb. 10.6 Der Name des Packages muss auf die erste Zeile jeder Klasse und Klassen aus anderen Packages müssen ggf. importiert werden

10.4

Ein Zusammenfassendes Beispiel

251

10.4.2 Erweitern und Zusammenführen der Klassen Wir erweitern zunächst das Beispielprogramm PlayerCardAppWithoutGUI wie in Abb. 10.7 und 10.8 gezeigt.

Abb. 10.7 Die Klasse PlayerCardAppWithoutGUI (I/II) →Video K-10.3

252

10 Methoden Planen und Programmieren

Abb. 10.8 Die Klasse PlayerCardAppWithoutGUI (II/II) →Video K-10.4

10.4

Ein Zusammenfassendes Beispiel

253

Beachten Sie, dass wir in der Klasse PlayerCardAppWithoutGUI eine statische Variable scan vom Typ Scanner und eine statische Variable myCards vom Typ PlayerCardCollection deklariert haben. Beide Variablen werden von mehreren statischen Methoden dieser Klasse verwendet. Die Variable myCards wird in der Methode main initialisiert. Dies geschieht mit Hilfe eines statischen Methodenaufrufs: Initializer.readPlayerFile(); Die Klasse Initializer ist in Abb. 10.9 gezeigt. Diese ist fast identisch mit der Version, die wir im letzten Kapitel studiert haben. Die Unterschiede sind, dass hier die Methode readPlayerFile eine Support-Methode readLine aufruft, um aus einer eingelesenen Zeile ein Objekt vom Typ PlayerCard zu instanziieren (Zerlegung von komplexen Methoden). Zudem gibt diese neue Version der Methode readPlayerFile eine PlayerCardCollection zurück (statt einer ArrayList). Innerhalb der Methode main der Klasse PlayerCardAppWithoutGUI kann der Benutzer aus vier möglichen Optionen auswählen (Karte hinzufügen, Sammlung ausgeben, Karte suchen oder das Programm beenden). Drei dieser Funktionalitäten sind in statischen Support-Methoden addPlayerCard, printCards und searchCard programmiert. Die Auswahl des Benutzers (selection) wird in einer switch-Anweisung verwendet, um zur passenden Methode innerhalb der Klasse PlayerCardAppWithoutGUI zu springen1 . Statt also die gesamte Funktionalität in der Methode main zu programmieren, sind die verschiedenen Funktionalitäten (wie zum Beispiel das Hinzufügen oder das Suchen) in separate Methoden ausgelagert, die im entsprechenden case aufgerufen werden. In Abb. 10.8 ist die Fortsetzung der Klasse PlayerCardAppWithoutGUI gezeigt. Für jede Option, die in der switch-Anweisung in der Methode main aufgerufen werden kann, existiert eine entsprechende Methode. Die Methoden printCards (ohne Parameter) sowie die Methode addPlayerCard sind schnell erklärt: • Die Methode addPlayerCard erfragt vom Benutzer den Spielernamen, den Namen des Clubs, die Nationalität und die Position des Spielers. Mit diesen Angaben wird zunächst ein neues Objekt vom Typ Player und danach ein neues Objekt vom Typ PlayerCard instanziiert. Dieses Objekt wird der Sammlung myCards mit Hilfe der Methode addPlayerCard hinzugefügt. • Die Methode printCards führt einen Aufruf der Methode println aus und übergibt dieser die Sammlung myCards. D. h. hier wird automatisch die Methode toString der Klasse PlayerCardCollection aufgerufen. 1 Hinweis: Nachdem mit nextInt die Wahl des Benutzers eingelesen wurde, muss noch eine nextLine Anweisung erfolgen (auf Zeile 22), da sonst beim nächsten Aufruf von nextLine in der Methode addPlayerCard der Variablen playerName die leere Zeichenkette zugewiesen wird.

254

10 Methoden Planen und Programmieren

Abb. 10.9 Die Klasse Initializer. →Video K-10.2 (Fortsetzung)

10.4

Ein Zusammenfassendes Beispiel

255

Die Funktionalität zum Suchen einer Spielerkarte ist etwas komplexer und muss deshalb genauer geplant werden. Das geplante Vorgehen ist in Algorithmus 3 zu sehen. Die grundlegende Idee ist, dass der Benutzer einen Suchbegriff eingeben kann und dass dann alle Karten der Sammlung nach diesem Suchbegriff durchsucht werden. Wir versuchen diesen Plan nun in unseren Klassen umzusetzen. Die Methode searchCard in der Klasse PlayerCardAppWithoutGUI erfragt vom Benutzer einen Suchbegriff und weist die Eingabe der Variablen searchString zu. Danach wird mit diesem Suchbegriff als Parameter die Methode search auf dem Objekt myCards aufgerufen. Algorithm 3 searchCard(collection) 1: Einlesen eines Suchbegriffs searchString vom Benutzer 2: Initialisieren einer leeren Liste searchResult 3: for jede Spielerkarte playerCard aus der Sammlung collection do 4: 5: 6:

if ein Attribut von playerCard enthält searchString then Hinzufügen der Karte playerCard zu searchResult end if

7: end for 8: Ausgeben des Inhaltes der Liste searchResult

Also trägt die Klasse PlayerCardCollection selbst die Verantwortung, dass man diese mit einem Suchbegriff durchsuchen kann. Offensichtlich ist die Rückgabe der Methode search eine ArrayList aus PlayerCard Objekten, die der Variablen searchResult zugewiesen wird (um diese Rückgabe kümmern wir uns später). Als nächstes betrachten wir die Methode search in der Klasse PlayerCardCollection (siehe Abb. 10.10). Diese Methode instanziiert zuerst eine neue Liste searchResult zum Speichern des Suchergebnisses. Danach geht die Methode mit einer for-Schleife durch alle vorhandenen Objekte vom Typ PlayerCard in der Sammlung collection hindurch. Für jede Karte playerCard, die sich in der ArrayList collection befindet, wird die Methode contains mit dem Parameter searchString aufgerufen. Diese Methode contains – programmiert in der Klasse PlayerCard (siehe Abb. 10.11) – gibt true zurück, wenn entweder der Name, die Nationalität oder der Clubname der Karte playerCard den Suchbegriff searchString enthält oder die Kartennummer dem Suchbegriff searchString entspricht.

256

10 Methoden Planen und Programmieren

Abb. 10.10 Die Klasse PlayerCardCollection. →Video K-10.5

10.4

Ein Zusammenfassendes Beispiel

Abb. 10.11 Die Klasse PlayerCard. →Video K-10.5 (Fortsetzung)

257

258

10 Methoden Planen und Programmieren

Studieren Sie genau, wie diese Funktionalität in der Klasse PlayerCard programmiert ist. Beachten Sie zum Beispiel, wie in der if-Anweisung die contains Aufrufe mit einem logischen OR (||) miteinander kombiniert werden oder wie die Kartennummer cardNumber mit Hilfe der statischen Methode toString der Klasse Integer in eine Zeichenkette umgewandelt wird. Wichtig ist auch, dass Sie erkennen, dass die Klasse PlayerCard bzw. die Objekte dieser Klasse die Funktionalität anbieten, dass man diese fragen kann: „Enthältst du den Suchbegriff searchString – ja oder nein?“. Die Verantwortung für dieses Verhalten trägt also die Klasse PlayerCard. Gehen wir wieder zurück zur Methode search in der Klasse PlayerCardCollection. Jede Karte playerCard, bei der die Methode contains den Wert true zurückgibt, wird zur Liste searchResult hinzugefügt. Diese Liste wird schliesslich der aufrufenden Methode searchCard der Klasse PlayerCardAppWithoutGUI zurückgegeben. Die zurückgegebene Liste wird nun der Variablen searchResult zugewiesen und als Parameter an die Methode printCards übergeben. Falls searchResult etwas enthält, wird die Methode printCards den Inhalt der Liste mit einer for-Schleife ausgegeben und sonst eine Meldung generieren, dass keine passende Karte gefunden werden konnte. Beachten Sie, dass die Methode printCards somit überladen ist. Es existiert eine Version der Methode, die keine Parameter erwartet und einfach die aktuelle Sammlung myCards ausgibt und eine zweite Version, die eine ArrayList aus PlayerCard Objekten als Parameter erwartet und den Inhalt dieser Liste ausgibt. Eine mögliche Ein- und Ausgabe des Programms lautet: Was möchten Sie tun? 1=Spielerkarte hinzufügen; 2=Sammlung ausgeben; 3=Karte suchen; 0=Programm beenden 2 Anzahl Karten: 3 Meine Karten: John, Brasilien, Torwart, FC Foo, 1 Mike, Estland, Stürmer, FC Bar, 2 Fynn, Holland, Stürmer, FC Foo, 3 Was möchten Sie tun? 1=Spielerkarte hinzufügen; 2=Sammlung ausgeben; 3=Karte suchen; 0=Programm beenden 1 Spielername? Jim Club? AC Bar Nationalität? Brasilien Position? Verteidiger Was möchten Sie tun? 1=Spielerkarte hinzufügen; 2=Sammlung ausgeben; 3=Karte suchen; 0=Programm beenden 3 Suchbegriff? Bras Gefundene Karten:

10.4

Ein Zusammenfassendes Beispiel

John, Brasilien, Torwart, FC Foo, 1 Jim, Brasilien, Verteidiger, AC Bar, 4 Was möchten Sie tun? 1=Spielerkarte hinzufügen; 2=Sammlung ausgeben; 3=Karte suchen; 0=Programm beenden 3 Suchbegriff? 2 Gefundene Karten: Mike, Estland, Stürmer, FC Bar, 2 Was möchten Sie tun? 1=Spielerkarte hinzufügen; 2=Sammlung ausgeben; 3=Karte suchen; 0=Programm beenden 0 Programm wird beendet.

Aufgaben und Übungen zu Kap. 10 Theorieaufgaben 1. Übersetzen Sie folgenden Pseudocode in eine statische Methode in Java: Algorithm 4 shuffle(Liste list mit ganzen Zahlen) 1: n = Grösse von list - 1 2: for i = n, n-1, . . . , 2, 1 do 3: 4:

r = Zufallszahl zwischen 0 und i (i inklusive) tausche die Elemente in der Liste an Position i und r

5: end for 6: gib die gemischte Liste list zurück

259

260

10 Methoden Planen und Programmieren

2. Zerlegen Sie die folgende Methode getMaxAvg in zwei kleinere Methoden (so dass der duplizierte Quellcode eliminiert wird).

3. In der Klasse RationalNumber sind die Methoden reduce und gcd mit Sichtbarkeit private deklariert – weshalb? 4. Was sind die zwei Hauptvorteile, wenn man komplexes Verhalten in mehrere, kleine Methoden zerlegt? 5. Machen Sie ein Beispiel, das den Unterschied zwischen primitiven Datentypen und Objekten im Fall von Parameterübergaben an Methoden verdeutlicht. 6. Welche Ausgabe erzeugt folgendes Programm:

10.4

Ein Zusammenfassendes Beispiel

261

7. Welche der folgenden Methodenköpfe repräsentieren tatsächlich unterschiedliche Signaturen: a) public String describe(String name, int count) public String describe(int count, String name) b) public int count() public void count() c) public int howMany(int compareValue) public int howMany(int upper) d) public boolean greater(int val1) public boolean greater(int val1, int val2) e) public void say() private void say()

Java Übungen 1. Programmieren Sie das zusammenfassende Beispiel aus Kap. 10 komplett nach. →Video K-10.2 →Video K-10.3 →Video K-10.4 →Video K-10.5 2. Ergänzen Sie das Programm PlayerCardAppWithoutGUI mit der Funktionalität, dass der Benutzer eine Karte aus der Sammlung löschen kann. Hierzu wird der Benutzer aufgefordert, eine Kartennummer einzugeben – falls eine Karte mit dieser Nummer in der Sammlung vorhanden ist, soll diese gelöscht werden. Eine mögliche Ein- und Ausgabe sieht also folgendermassen aus: Was möchten Sie tun? 1=Spielerkarte hinzufügen; 2=Sammlung ausgeben, 3=Karte suchen; 4=Karte löschen; 0=Programm beenden 4 Kartennummer? 2 Was möchten Sie tun? 1=Spielerkarte hinzufügen; 2=Sammlung ausgeben, 3=Karte suchen; 4=Karte löschen; 0=Programm beenden 2 Anzahl Karten: 2 Meine Karten:

262

10 Methoden Planen und Programmieren

1, John, FC Foo, Brasilien, Torwart 3, Fynn, FC Foo, Holland, Stürmer

→Video V-10.1

Literatur 1. John Lewis and William Loftus. Java Software Solutions – Foundations of Program Design. Pearson Global Edition, 8th edition edition, 2015.

Graphische Benutzeroberflächen (Teil 2)

11

In Kap. 5 haben wir besprochen, wie man graphische Oberflächen mit JavaFX gestalten kann. In diesem Kapitel wollen wir nun die Kontrollelemente dieser Oberflächen mit den Funktionalitäten der Klassen unseres „eigentlichen“ Programmes verbinden. Konkret heisst das, dass wir die Methoden unserer Klassen mit den Kontrollelementen der graphischen Oberflächen steuern wollen.

11.1

Auf Ereignisse Reagieren

Ein Objekt vom Typ Event repräsentiert in Java ein Ereignis, das unser Programm möglicherweise interessieren könnte. Sehr oft werden Objekte vom Typ Event durch Benutzeraktionen generiert. Kontrollelemente generieren zum Beispiel Event Objekte, um anzuzeigen, dass eine Benutzeraktion auf ihnen stattgefunden hat. Zum Beispiel wird ein Objekt vom Typ Button ein Event generieren, sobald dieser gedrückt wird oder ein Objekt vom Typ TextField erzeugt ein Event, sobald in diesem bestimmte Eingaben erfolgen. Es existieren unterschiedliche Möglichkeiten, wie man mit den generierten Event Objekten umgehen kann. Die Programmiererin definiert letztendlich die Beziehung zwischen der Komponente, die ein Event generiert und dem sogenannten Event-Handler, der auf das Ereignis reagiert. Beispiel 11.1 Das Programm CountApp in Abb. 11.1 instanziiert den Wurzelknoten root als Objekt der Klasse CountAppView und zeigt diesen schliesslich in einem JavaFX Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_11.

© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_11

263

264

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.1 Die Klasse CountApp. → Video K-11.1

Fenster an. Diese Hauptklasse folgt dem bekannten Muster, das wir schon in Kap. 5 mehrfach angewendet haben: 1. Wurzel des Szenengraphen instanziieren 2. Wurzel an Szene übergeben 3. Szene in Fenster anzeigen Die Klasse CountAppView (siehe Abb. 11.2) ist ein Behälter vom Typ FlowPane. In dieser Klasse werden drei Instanzvariablen deklariert: count vom Typ int, push vom Typ Button und label vom Typ Label. Im Konstruktor CountAppView wird der Zähler count mit 0 initialisiert und die beiden Kontrollelemente push und label werden instanziiert und dann im Behälter platziert. Das resultierende Programmfenster ist in Abb. 11.3 zu sehen. Mit der Methode setOnAction, welche man auf verschiedenen Kontrollelementen (wie z. B. einem Button) aufrufen kann, können wir angeben, wer auf die Ereignisse dieses Kontrollelementes reagieren soll. Dies geschieht mit folgender Syntax:

11.1

Auf Ereignisse Reagieren

Abb. 11.2 Die Klasse CountAppView. →Video K-11.1 Abb. 11.3 Das JavaFX Fenster zum Programm CountApp

265

266

11 Graphische Benutzeroberflächen (Teil 2)

control.setOnAction(object::method);

Hierbei sei control ein bestimmtes Kontrollelement, das einen Event generieren kann. Der Methode setOnAction können wir als Parameter eine Referenz auf eine Methode mitgeben, welche ausgeführt werden soll, sobald ein Event auf control registriert wird. Hierzu benötigen wir eine Referenz auf ein Objekt object und den Bezeichner der Methode method, die aufgerufen werden soll. Der Operator :: im Parameter definiert dabei eine sogenannte Methodenreferenz. In unserem Beispiel zeigt die Methodenreferenz für das Kontrollelement push auf die Methode processButtonPress, die sich in diesem Objekt (this) befindet: this.push.setOnAction(this::processButtonPress);

In diesem Beispiel ist die Klasse CountAppView also selber für die Behandlung der Ereignisse verantwortlich, die auf dem Button push registriert werden. Jedes Mal wenn der Benutzer den Button push anklickt, wird nun die eigens geschriebene Methode processButtonPress in diesem Objekt ausgeführt. Die Methode processButtonPress inkrementiert die Instanzvariable count um eins und macht danach ein Update des Objektes label (mit Hilfe der Methode setText). Sowohl count als auch label sind als Instanzvariablen deklariert. Dies ist nötig, so dass sowohl der Konstruktor CountAppView als auch die Methode processButtonPress auf diese Objekte zugreifen können. Betrachten Sie den Parameter der Methode processButtonPress. Jede Methode, die von einem Kontrollelement über eine Methodenreferenz aufgerufen werden kann, erwartet ein Objekt vom Typ Event als Parameter. Dieses Objekt entspricht dem Ereignis, welches den Methodenaufruf ausgelöst hat. Im Moment ignorieren wir diesen formalen Parameter noch – es ist aber dennoch notwendig, ihn in der Methode anzugeben. Das Programm CountApp verwendet eine Methodenreferenz, um das Event, generiert durch den Button, zu behandeln. Es existieren andere Möglichkeiten, Event Objekte zu bearbeiten: Mit Hilfe einer eigenen Klasse, welche das Event behandelt (typischerweise als private innere Klasse programmiert) oder mit Hilfe sogenannter Lambda Ausdrücke. Wir verwenden in diesem Buch aber ausschliesslich Methodenreferenzen. Wir betrachten ein weiteres Beispiel. Dieses beinhaltet ein Objekt vom Typ TextField, das dazu verwendet werden kann, Benutzereingaben entgegenzunehmen. Zudem verbinden wir in diesem Beispiel das GUI mit einer weiteren Klasse. Beispiel 11.2 Das Programm SalaryApp aus Abb. 11.4 zeigt das Programmfenster in Abb. 11.5 an. Das GUI beinhaltet ein Textfeld zur Eingabe der geleisteten Stunden und ein Ausgabetext, welcher das berechnete Gehalt anzeigt.

11.1

Auf Ereignisse Reagieren

267

Abb. 11.4 Die Klasse SalaryApp. →Video K-11.2

Abb. 11.5 Das Programmfenster zum Programm SalaryApp

Die eigentliche Definition der graphischen Oberfläche geschieht in der separaten Klasse SalaryView (siehe Abb. 11.6). Die Klasse SalaryView erweitert die Klasse GridPane. Es werden drei Instanzvariablen deklariert: resultLabel vom Typ Label, hoursField vom Typ TextField und das Objekt fmt vom Typ DecimalFormat. Im Konstruktor SalaryView werden die drei Instanzvariablen instanziiert. Zudem werden noch zwei lokale Variablen inputLabel und outputLabel (beide vom Typ Label) deklariert und instanziiert. Danach werden alle Kontrollelemente zu diesem Behälter hinzugefügt. Wie beim vorigen Beispiel verwenden wir die Methode setOnAction und eine Methodenreferenz für die Behandlung der Ereignisse, die auf dem Textfeld hoursField ausgelöst werden können:

268

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.6 Die Klasse SalaryView. → Video K-11.2 (Fortsetzung)

11.2

Mehrere Ereignisse Behandeln

269

Abb. 11.7 Die Klasse SalaryComputation this.hoursField.setOnAction(this::processReturn);

Eine Methodenreferenz auf Objekten vom Typ TextField wird aktiv, sobald der Benutzer die Eingabetaste (Return) auf seiner Tastatur drückt. Die Methode processReturn, die sich in diesem Objekt (this) befindet, wird also bei jedem Anschlag der Eingabetaste ausgeführt. Die eigene Methode processReturn liest den aktuellen Text aus dem Objekt hoursField mit Hilfe der Methode getText. Die Methode getText gibt einen String zurück, welcher mit der Wrapper Klasse Integer und der Methode parseInt in eine ganze Zahl konvertiert wird. Danach wird die Berechnung des Gehalts vorgenommen: Dies geschieht mit Hilfe der statischen Methode computeSalary, die in der Klasse SalaryComputation programmiert ist (siehe Abb. 11.7 – nach einer Idee aus [1]). Das berechnete Gehalt wird schliesslich mit der Methode setText im Label resultLabel formatiert angezeigt.

11.2

Mehrere Ereignisse Behandeln

Unsere Methoden, die auf eine Benutzeraktion reagieren, behandeln bisher jeweils nur ein Event – das wollen wir im folgenden Beispiel erweitern. Beispiel 11.3 Betrachten Sie die Klassen AddConcApp und AddConcView in den Abb. 11.8 und 11.9. Dieses Programm zeigt zwei Objekte vom Typ TextField und zwei

270

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.8 Die Klasse AddConcApp. →Video K-11.3

Objekte vom Typ Button an (markiert mit "Konkatenieren" und "Addieren"). Wird einer der beiden Schaltflächen gedrückt, werden die Eingaben in den Textfeldern entweder addiert oder konkateniert (siehe Abb. 11.10). In diesem Beispiel verwenden beide Button Objekte die gleiche Methodenreferenz: this.concButton.setOnAction(this::processButton); this.addButton.setOnAction(this::processButton);

Wenn einer der beiden Schaltflächen gedrückt wird, so wird in jedem Fall die Methode processButton in diesem Objekt aufgerufen. In der Methode processButton werden zunächst die Inhalte der beiden Textfelder op1Field und op2Field ausgelesen (mit der Methode getText). Danach verwenden wir eine if-Anweisung, um herauszufinden, was als nächstes getan werden soll.

11.2

Mehrere Ereignisse Behandeln

Abb. 11.9 Die Klasse AddConcView. →Video K-11.3

271

272

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.10 Das Programmfenster zur Klasse AddConcApp

Wie in den bisherigen Beispielen wird auch hier ein Objekt event vom Typ Event an die entsprechende Methode als Parameter übergeben. In den vorigen Beispielen haben wir diesen Parameter immer ignoriert – in diesem Beispiel verwenden wir die Methode getSource, um herauszufinden, welches Kontrollelement den Event tatsächlich ausgelöst hat. Die Quelle eines Objektes event vom Typ Event kann also mit der Methode getSource referenziert werden. Falls das Objekt concButton den event ausgelöst hat – also wenn der Boolesche Ausdruck (event.getSource() == this.concButton)

wahr ist – so werden die Eingaben in den Textfeldern konkateniert. Andernfalls muss das Objekt addButton den Event ausgelöst haben und die Eingaben in den Textfeldern werden addiert. Für die Addition werden die String Objekte text1 und text2 zunächst in zwei int Werte konvertiert (mit parseInt). Das Resultat der Addition wird dann mit setText im Label result angezeigt. Beachten Sie, dass die Methode setText eine Zeichenkette als Parameter erwartet. Deshalb konkatenieren wir an das Resultat von op1 + op2 die leere Zeichenkette "", so dass das Gesamtresultat von (op1 + op2 + "") eine Zeichenkette wird. Statt nur einer Methode zur Behandlung der Ereignisse, könnten wir auch zwei separate Methoden programmieren und den entsprechenden Button Objekten via Methodenreferenzen hinzufügen: this.concButton.setOnAction(this::processConcButton); this.addButton.setOnAction(this::processAddButton);

In diesem Fall könnten wir auf das Auslesen der Quelle mit getSource sowie auf die Fallunterscheidung mit einer if-Anweisung verzichten. Ob wir mehrere Methoden für die

11.3

Ereignisse der Maus und der Tastatur

273

verschiedenen Kontrollelemente programmieren oder nur eine Methode, welche dann die Quelle des Event Objektes bestimmt, ist eine Entscheidung der Programmiererin.

11.3

Ereignisse der Maus und der Tastatur

Bis jetzt haben wir nur Event Objekte betrachtet, die von Kontrollelementen ausgelöst werden. Es existieren aber noch weitere Event Objekte. Wenn der Benutzer zum Beispiel einen Mausklick ausführt, so werden verschiedene Event Objekte generiert, nämlich mouse pressed, mouse released und mouse clicked (es existieren sogar noch weitere Event Objekte, die vom Zeiger der Maus ausgelöst werden können, z. B. mouse entered oder mouse moved). Für jedes dieser Ereignisse kann man eine eigene Methode definieren, die zum Zeitpunkt des Event ausgeführt werden soll. Wenn wir zum Beispiel eine ganze Szene „klickbar“ machen wollen, so können wir auf dem Objekt scene (vom Typ Scene) die Methode setOnMouseClicked oder setOnMouseMoved aufrufen und mit einer Methodenreferenz an eine bestimmte Methode assoziieren. Das als Parameter übergebene Objekt ist in diesem Fall vom Typ MouseEvent, aus dem verschiedene Informationen ausgelesen werden können (z. B. die (x, y)-Position des Zeigers zum Zeitpunkt des Ereignisses). Beispiel 11.4 In Abb. 11.11 ist ein Beispiel gezeigt. Die Klasse MouseApp deklariert eine Instanzvariable root vom Typ Group (ein einfacher Behälter ohne Layoutvorgabe). In der Methode start wird wie gewohnt der Wurzelknoten root instanziiert und an die Szene scene übergeben. Die so definierte Szene wird schliesslich im Fenster stage angezeigt. Auf Zeile 29 definieren wir eine Methodenreferenz mit der Methode setOnMouseClicked auf dem Objekt scene: scene.setOnMouseClicked(this::processMouseClick);

Die eigene Methode processMouseClick in diesem Objekt wird ausgeführt, sobald der Benutzer einen Mausklick auf der Szene ausführt (für andere Ereignisse der Maus könnten wir weitere Methodenreferenzen definieren – zum Beispiel setOnMouseMoved). Beachten Sie, dass die Methode processMouseClick nun ein Objekt event vom Typ MouseEvent als Parameter erhält. Mit den Methoden getX und getY, welche auf dem Objekt event aufgerufen werden können, werden die aktuellen Koordinaten der Maus im Fenster ausgelesen. Mit Hilfe dieser Koordinaten wird in unserem Beispiel ein Kreisobjekt (ein JavaFX Objekt vom Typ Circle) mit Radius 5 instanziiert und zum Wurzelbehälter root hinzugefügt. Bei jedem Klick in das Fenster erscheint also bei der Mausposition ein neuer Kreis. Wie das Programmfenster nach einigen Klicks aussehen kann, ist in Abb. 11.12 gezeigt.

274

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.11 Ereignisse vom Typ MouseEvent an einem Beispiel illustriert. →Video

K-11.4

11.4

Ein Erstes Beispiel mit einem Model, einer View und einem Controller

275

Abb. 11.12 Das JavaFX Programmfenster zur Klasse MouseApp

Ähnlich zu Ereignissen, die von der Maus ausgelöst werden, kann auch die Tastatur spezifische Event Objekte generieren. Ein KeyEvent wird generiert, sobald ein Zeichen auf der Tastatur eingegeben wird. Mit einem Methodenaufruf und einer Methodenreferenz, also z. B. mit scene.setOnKeyTyped(this::processKey);

wird dann bei jedem Tastaturanschlag die Methode processKey aufgerufen, der ein KeyEvent Objekt übergeben wird. Mit der Methode getCharacter kann dann zum Beispiel das Zeichen der gedrückten Taste ausgelesen werden. Wir gehen in diesem Buch nicht weiter auf diese spezifischeren Event Objekte ein.

11.4

Ein Erstes Beispiel mit einem Model, einer View und einem Controller

In unseren bisherigen Beispielen haben wir jeweils eine graphische Oberfläche programmiert und dann die Behandlung der Event Objekte direkt in der gleichen Klasse programmiert (die Methoden haben wir dann via Methodenreferenz an die Kontrollelemente der graphischen Oberfläche assoziiert). Oftmals können die Klassen einer Applikation in drei Kategorien eingeteilt werden: Klassen des Models, der View und des Controllers. In den Klassen der View definieren wir die graphische(n) Oberfläche(n) der Applikation, in den Klassen der Kategorie Controller programmieren wir die Methoden, welche bei Benutzeraktionen auf der Oberfläche ausgeführt werden sollen und in den Klassen der Kategorie Model definieren wir die grundlegenden Klassen und Datenstrukturen unserer Applikation. Diese Aufteilung nennt man MVC-Pattern und es existieren zahlreiche Varianten hiervon. Eine wichtige Faustregel besagt, dass das Model und die View nicht direkt miteinander

276 Abb. 11.13 Eine mögliche Ausprägung des MVC-Patterns

11 Graphische Benutzeroberflächen (Teil 2)

model

controller

view

Abb. 11.14 Das Programmfenster zur Applikation DiceApp

interagieren sollen – die Interaktion soll immer über die Klassen der Kategorie Controller erfolgen. Eine mögliche Variante soll im Folgenden illustriert werden. In dieser Variante gehen wir davon aus, dass der Controller je eine Referenz auf das Model und auf die View besitzt. Die View kann auch auf den Controller zugreifen, während das Model aber keinen Zugriff auf den Controller besitzt (siehe Abb. 11.13). Wir betrachten nun ein Beispiel, welches dieses Muster umsetzt: Beispiel 11.5 Sie erinnern sich an das Programmfenster in Abb. 11.14, das wir in Kap. 5 programmiert haben. Diese View wollen wir nun mit dem Model – nämlich der Klasse Dice (aus Kap. 4) – verbinden. In der Klasse DiceApp (siehe Abb. 11.15) wird die Applikation gestartet. Wir deklarieren in dieser Hauptklasse drei Instanzvariablen: dice vom Typ Dice (das Model), diceView vom Typ DiceView (die View) und diceController vom Typ DiceController (der Controller). In der Methode start wird zunächst das Model dice instanziiert. Danach wird der Controller diceController instanziiert und erhält als Parameter eine Referenz auf das Objekt dice. Das heisst, das Objekt diceController kann somit auf die Methoden der Klasse Dice zugreifen. Schliesslich instanziieren wir diceView und übergeben dieser den Controller diceController als Parameter – mit einer Methode setView, die wir im Controller programmieren müssen, übergeben wir umgekehrt dem Objekt diceController eine Referenz auf diceView. Das bedeutet also, dass diceView auf den Controller und umgekehrt diceController auf die View zugreifen kann.

11.4

Ein Erstes Beispiel mit einem Model, einer View und einem Controller

277

Abb. 11.15 Die Klasse DiceApp. →Video K-11.5

In diesem Beispiel besteht das Model aus einer einzigen Klasse, nämlich der Klasse Dice (siehe Abb. 11.16 – diese Klasse ist identisch mit der Klasse Dice aus Kap. 4). Die Klasse modelliert einen herkömmlichen Spielwürfel mit sechs Seiten, den man werfen kann (mit der Methode roll), den man manuell auf eine Seite drehen kann (mit der Methode setPoints) und bei dem man die aktuelle Punktezahl auslesen kann (mit der Methode getPoints). In der Klasse DiceView – gezeigt in den Abb. 11.17 und 11.18 – sind die Details der graphischen Oberfläche programmiert. Diese Klasse entspricht im Wesentlichen der Version

278

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.16 Die Klasse Dice. →Video K-11.5 (Fortsetzung)

11.4

Ein Erstes Beispiel mit einem Model, einer View und einem Controller

Abb. 11.17 Die Klasse DiceView. (1/2) →Video K-11.5 (Fortsetzung)

279

280

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.18 Die Klasse DiceView (2/2). →Video K-11.5 (Fortsetzung)

der Klasse, die wir in Kap. 5 vorbereitet haben. Die Anpassungen sind in den Abbildungen umrahmt. In Abb. 11.17 sind zwei Anpassungen ersichtlich. Zunächst benötigen wir nun sechs Ansichten eines Spielwürfels, welche in den Bildern one.jpg, two.jpg, etc. gespeichert sind (im Ordner /img). Diese Bilder werden einzeln als Konstanten vom Typ Image gespeichert. Zudem erhält der Konstruktor DiceView nun neu eine Referenz auf das instanziierte Objekt diceController vom Typ DiceController (als Parameter). Der Konstruktor DiceView instanziiert und platziert dann im Wesentlichen die Kontrollelemente zur Anzeige des Würfels, zum Werfen des Würfels und zur manuellen Eingabe der Punktezahl des Würfels.

11.4

Ein Erstes Beispiel mit einem Model, einer View und einem Controller

281

Zusätzlich werden nun an die beiden Objekte rollButton und setButton vom Typ Button je eine Methodenreferenz hinzugefügt (siehe Abb. 11.18): this.rollButton.setOnAction(diceController::rollTheDice); this.setButton.setOnAction(diceController::setTheDice);

Das bedeutet also, sobald ein Benutzer eine der beiden Schaltflächen betätigt, wird entweder die Methode rollTheDice oder setTheDice auf dem Objekt diceController ausgeführt (dieses Objekt haben wir als Parameter erhalten). Beide Methoden schauen wir gleich im Detail an. Im Gegensatz zu den ersten Beispielen dieses Kapitels befindet sich also die Methode, die referenziert wird, nicht in diesem Objekt, sondern in einem Objekt einer anderen Klasse – nämlich dem Controller. Die Klasse DiceView enthält neu auch noch zwei zusätzliche Methoden updatePoints und getPoints. Die Methode updatePoints besteht aus einer switch-Anweisung, die je nach übergebenem Wert points ein anderes Würfelbild in diceImage platziert (mit der Methode setImage). Die Methode getPoints liest den aktuellen Text aus pointsField aus und konvertiert diesen Text in eine ganze Zahl (mit der Methode parseInt). Die ausgelesene Punktezahl wird danach zurückgegeben. Wir werden gleich sehen, dass beide Methoden – updatePoints und getPoints – vom Objekt diceController verwendet werden. Betrachten wir nun noch die Klasse DiceController (siehe Abb. 11.19). Als Parameter wird dem Konstruktor DiceController die Referenz auf das Model dice mitgegeben. Das heisst, wir haben von dieser Klasse aus Zugriff auf das Objekt dice. Zusätzlich haben wir dank dem Aufruf von setView auch Zugriff auf das Objekt diceView – sowohl das Model dice als auch die View diceView sind als Instanzvariablen deklariert. Sie erinnern sich, dass die Methoden rollTheDice und setTheDice von den beiden Button Objekten in der View ausgelöst werden können. Diese beiden Methoden „orchestrieren“ nun den genauen Ablauf der Aktionen und die Interaktion zwischen Model und View. Die Methode rollTheDice wirft zuerst den Würfel dice (mit der Methode roll, die in der Klasse Dice zur Verfügung steht), liest die aktuelle Punktezahl des Würfels aus (mit getPoints) und übergibt diesen Wert als Parameter an die Methode updatePoints zurück an das Objekt diceView. Das heisst, bei jedem Klick auf den Button rollButton wird der Würfel geworfen und das Würfelbild in der View wird der aktuellen Punktezahl des Models angepasst. Die Methode setTheDice liest zuerst mit Hilfe der Methode getPoints, die wir in der Klasse DiceView programmiert haben, die Punktezahl des Eingabefeldes pointsField aus. Danach wird in dice die Punktezahl neu gesetzt (mit der Methode setPoints). Beachten Sie hier nun insbesondere, dass in dice die Punktezahl nur dann

282

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.19 Die Klasse DiceController. →Video K-11.5 (Fortsetzung)

geändert wird, wenn die übergebene Zahl points einem gültigen Wert entspricht (diesen Setter haben wir im Model so programmiert). Die aktuelle Punktezahl des Würfels wird schliesslich als Parameter an die Methode updatePoints zurück an das Objekt diceView gegeben. Das heisst, das Würfelbild in der View wird wiederum der aktuellen Punktezahl des Models angepasst.

11.5

11.5

Die Spielerkarten App

283

Die Spielerkarten App

Wir betrachten in diesem Abschnitt ein etwas komplexeres und umfassenderes Beispiel. Sie erinnern sich an die Klassen Player, PlayerCard oder PlayerCardCollection (und an die enum Nationality und Position) – dies entspricht unserem Model einer Spielerkarten Applikation. Auch die View zu diesem Programm haben wir bereits programmiert – mit den Klassen MainView, TabView, AddPlayerView und SearchPlayerView. Im Folgenden wollen wir nun das Model und die verschiedenen Views miteinander verbinden. Wir starten mit der Hauptklasse PlayerCardApp, welche die Applikation startet (siehe Abb. 11.20) – wir folgen dem gleichen Muster wie beim vorigen Beispiel: 1. Wir instanziieren zunächst das Model: In diesem Fall verwenden wir hierzu die von uns programmierte statische Methode readPlayerFile aus der Klasse Initializer, um eine Datei mit Spielerinformationen einzulesen und aus den eingelesenen Zeilen je ein Objekt vom Typ PlayerCard zu instanziieren. Jedes instanziierte Objekt wird dann in einer PlayerCardCollection myCards organisiert. 2. Danach instanziieren wir den Controller und übergeben diesem eine Referenz auf das eben instanziierte Model myCards. Der Controller kann also auf die Funktionalitäten des Models zugreifen (z. B. können wir aus der Klasse Controller die Methoden search oder addPlayerCard auf myCards aufrufen). 3. Schliesslich instanziieren wir die View dieser Applikation – dies entspricht der Klasse MainView, welche zwei weitere Views (nämlich die AddPlayerView und die SearchPlayerView) in einer TabPane organisiert. Beachten Sie wiederum, dass wir der View die Referenz auf den Controller mitgeben und dass wir danach die View mit der Methode setViews an den Controller übergeben. Sind diese Anweisungen ausgeführt, sind alle Bestandteile unseres Programmes vorhanden und aufgesetzt. Allerdings sind die View und das Model noch nicht miteinander verbunden – es ist also noch nicht möglich, mit Aktionen auf der View, das Model zu manipulieren oder zu benutzen. Dieses Zusammenspiel zwischen View und Model werden wir im Controller organisieren. Bevor wir dies aber tun können, sind noch einige Anpassungen in den bereits geschriebenen Klassen der View und des Models nötig. In der Klasse SearchPlayerView (siehe Abb. 11.21) programmieren wir die folgenden vier Anpassungen (alle Änderungen sind markiert): • Der Konstruktor SearchPlayerView erhält als Parameter eine Referenz auf das Objekt controller vom Typ Controller.

284

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.20 Die Klasse PlayerCardApp. →Video K-11.6

• Der Button searchButton und das TextField searchField sind beide mit einer Methodenreferenz mit dem controller verbunden: this.searchButton.setOnAction(controller::searchPlayers); this.searchField.setOnAction(controller::searchPlayers);

11.5

Die Spielerkarten App

Abb. 11.21 Die Klasse SearchPlayerView. →Video K-11.6 (Fortsetzung)

285

286

11 Graphische Benutzeroberflächen (Teil 2)

Sobald ein Ereignis auf einem der beiden Kontrollelemente stattfindet (ein Mausklick auf der Schaltfläche bzw. eine Drücken der Eingabetaste im Textfeld), wird die Methode searchPlayers im Objekt controller aufgerufen. Diese Methode wird das Suchen nach einem Spieler im Model und die Anzeige des Suchresultates in der View orchestrieren. • Die Methode setResult nimmt eine ArrayList foundPlayers aus PlayerCard Objekten als Parameter entgegen. Danach werden als erstes alle Elemente in der ListView resultList entfernt (mit der Methode clear), um danach die erhaltenen PlayerCard Objekte foundPlayers zur resultList hinzuzufügen. • Mit der Methode getSearchString kann der aktuelle Text im TextField searchField ausgelesen werden. In der Klasse AddPlayerView (siehe Abb. 11.22 und 11.23) programmieren wir die folgenden Anpassungen (alle Änderungen sind markiert): • Der Konstruktor AddPlayerView erhält als Parameter eine Referenz auf das Objekt controller vom Typ Controller. • Der Inhalt der ChoiceBox nationalityChoiceBox wird dynamisch an die Aufzählung im enum Nationality angepasst – hierzu greifen wir auf das Array zu, das man aus dem enum Nationality mit Hilfe der statischen Methode values anfordern kann: Nationality.values():



• • • • •

Mit einer for-Schleife gehen wir dann durch die aufgezählten Nationality Objekte hindurch und fügen diese einzeln zur nationalityChoiceBox hinzu. Der Button addButton ist mit einer Methodenreferenz mit dem controller verbunden. Sobald ein Mausklick auf der Schaltfläche registriert wird, wird die Methode addPlayer im Objekt controller aufgerufen. Diese Methode wird das Hinzufügen einer neuen Spielerkarte im Model mit den in dieser View erfassten Informationen orchestrieren. Mit der Methode getPlayerName kann der aktuelle Text im TextField nameField ausgelesen werden. Mit der Methode getNationality kann die aktuell ausgewählte Nationality aus der ChoiceBox nationalityChoiceBox ausgelesen werden. Mit der Methode getPosition kann die aktuell ausgewählte Position aus den drei RadioButton Objekten ermittelt werden. Mit der Methode getClubName kann der aktuelle Text im TextField clubField ausgelesen werden. Die Methode reset macht alle gemachten Eingaben in der View rückgängig.

In der Klasse MainView (siehe Abb. 11.24) programmieren wir die folgenden Anpassungen (alle Änderungen sind markiert):

11.5

Die Spielerkarten App

Abb. 11.22 Die Klasse AddPlayerView (1/2). →Video K-11.6 (Fortsetzung)

287

288

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.23 Die Klasse AddPlayerView (2/2). →Video K-11.6 (Fortsetzung)

11.5

Die Spielerkarten App

Abb. 11.24 Die Klasse MainView. →Video K-11.6 (Fortsetzung)

289

290

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.25 Die Klasse TabView. →Video K-11.6 (Fortsetzung)

11.5

Die Spielerkarten App

291

• Der Konstruktor MainView erhält als Parameter eine Referenz auf das Objekt controller vom Typ Controller. • Das als Parameter erhaltene Objekt controller wird an den Konstruktor von TabView weitergeleitet. • Die Methode setStatus nimmt eine Zeichenkette entgegen und zeigt diese im Label status an. • Die beiden Methoden getAddPlayerView und getSearchPlayerView geben die in der TabView tabView gespeicherten Views AddPlayerView und SearchPlayerView zurück. Hierzu werden zwei Getter in tabView aufgerufen. In der Klasse TabView (siehe Abb. 11.25) programmieren wir die folgenden zwei Anpassungen (beide Änderungen sind markiert): • Das von MainView erhaltene Objekt controller wird an die beiden Konstruktoren AddPlayerView und SearchPlayerView weitergeleitet. Wie wir oben gesehen haben, wird das Objekt controller tatsächlich in den beiden Objekten addPlayerView und searchPlayerView verwendet werden und zwar in den Methodenreferenzen – z. B. mit this.addButton.setOnAction(controller::addPlayer);

• Die beiden Methoden getAddPlayerView und getSearchPlayerView geben die Objekte addPlayerView bzw. searchPlayerView zurück. Nun können wir die Klasse Controller umsetzen (siehe Abb. 11.26). • Dem Konstruktor Controller wird das Objekt myCards vom Typ PlayerCardCollection mitgegeben. Der Controller hat somit Zugriff auf das Model. • Die Methode setViews erhält als Parameter eine Referenz mainView auf die Hauptsicht des Programms. Mit dieser Referenz werden dann mit den Methoden getAddPlayerView und getSearchPlayerView die Referenzen auf die beiden Sichten AddPlayerView und SearchPlayerView gesetzt. Der Controller hat somit Zugriff auf die View mainView vom Typ MainView und die beiden Views addPlayerView und searchPlayerView vom Typ AddPlayerview bzw. SearchPlayerView. • Die Methode searchPlayers wird aufgerufen, sobald ein Ereignis auf dem Button searchButton oder dem TextField searchField im Objekt searchPlayerView auftritt.

292

11 Graphische Benutzeroberflächen (Teil 2)

Abb. 11.26 Die Klasse Controller. →Video K-11.6 (Fortsetzung)

11.5

Die Spielerkarten App

293

Zuerst wird mit der Methode getSearchString der Suchbegriff aus searchPlayerView gelesen. Mit dem ausgelesenen Suchbegriff searchString kann nun auf dem Model myCards eine Suche durchgeführt werden (mit der Methode search). Das Resultat der Suche – eine ArrayList aus PlayerCard Objekten – wird als Parameter an die Methode setResult an die View zurückgegeben. Diese Methode im Objekt searchPlayerView kümmert sich dann – wie wir bereits gesehen haben – um die Anzeige des Suchresultates. Mit dem Aufruf der Methode setStatus auf dem Objekt mainView wird die Statusmeldung in der Hauptsicht angepasst (es wird "Suche durchgeführt" angezeigt). • Die Methode addPlayer wird aufgerufen, sobald ein Ereignis auf dem Button addButton im Objekt addPlayerView auftritt. Ist dies der Fall, werden der Clubname, der Spielername, die Nationalität und die Position aus der View addPlayerView gelesen. Hierzu haben wir in der Klasse AddPlayerView die Methoden getClubName, getPlayerName, getNationality und getPosition programmiert. Mit den eingelesenen Informationen kann nun ein neues Player Objekt und danach ein neues PlayerCard Objekt instanziiert werden. Dieses Objekt wird dann dem Model myCards hinzugefügt und der Statustext in der Hauptsicht wird angepasst (auf "Spieler hinzugefügt"). Schliesslich führen wir noch die Methode reset auf der addPlayerView aus, damit alle Eingaben auf dem GUI rückgängig gemacht werden.

Aufgaben und Übungen zu Kap. 11 Theorieaufgaben 1. Gegeben sei ein Objekt button vom Typ Button. Erstellen Sie eine Methodenreferenz zur Methode processButtonClick, welche sich in dieser Klasse befindet. 2. Gleiche Aufgabe wie oben, aber dieses mal befindet sich die Methode processButtonClick in der Klasse Controller (eine Objektvariable contr vom Typ Controller sei hierzu verfügbar). 3. Wie können Sie in einer Methode, die ein bestimmtes Event event bearbeitet, die Quelle des Ereignisses bestimmen?

294

11 Graphische Benutzeroberflächen (Teil 2)

4. Mit welchen Methoden können Sie Text aus einem TextField lesen bzw. Text in einem Label neu setzen? 5. Wie können Sie Ereignisse der Tastatur oder der Maus behandeln? Machen Sie je ein Beispiel. 6. Wie können Sie bei einem Objekt cb vom Typ CheckBox herausfinden, ob dieses selektiert ist oder nicht? 7. Wie können Sie bei einem Objekt cb vom Typ ChoiceBox herausfinden, was aktuell in cb ausgewählt ist? 8. Betrachten Sie die Methode setTheDice aus der Klasse DiceController:

Was würde gegen folgende Lösung sprechen?

9. Betrachten Sie die Klasse CalculatorView, welche die Szene im folgenden Fenster generiert:

11.5

Die Spielerkarten App

295

Zur Addition bzw. Subtraktion stehen zwei statische Methoden aus der Klasse Calculation zur Verfügung:

Ergänzen Sie die Klasse CalculatorView so, dass die Summe bzw. die Differenz der beiden eingegebenen Zahlen berechnet und angezeigt wird, sobald der Benutzer auf die entsprechende Schaltfläche klickt.

296

11 Graphische Benutzeroberflächen (Teil 2)

Java Übungen 1. Schreiben Sie ein JavaFX Programm, das es dem Benutzer ermöglicht, auf Knopfdruck Zufallszahlen zwischen 1 und 100 zu erzeugen und diese anzuzeigen.

→Video V-11.1 2. Übernehmen Sie die Klasse Box aus Übungsserie 4 und die Klassen Main und BoxView aus Übungsserie 5. Passen Sie die Klasse BoxView so an, dass bei jedem Klick auf die Schaltfläche createButton ein neues Objekt vom Typ Box instanziiert wird (Achtung: die Variable full kann nicht über den Konstruktor Box definiert werden: Entweder passen Sie den Konstruktor an oder Sie rufen die Setter auf). Geben Sie eine Systemmeldung aus, dass das Instanziieren geklappt hat (entweder mit System.out.println() auf der Konsole oder in einem zusätzlichen Label, das Sie zur BoxView hinzufügen). Zudem soll Ihre Meldung die Kapazität der instanziierten Box ausgeben. →Video V-11.2 3. Übernehmen Sie die Klassen Main und HighLowView aus Übungsserie 5. Erweitern Sie Ihr Programm so, dass der Benutzer eine Zahl eingeben kann und diese dann auf Knopfdruck überprüft wird. Hat er richtig geraten, wird ihm dies angezeigt (im Label hintLabel). Ansonsten erhält der Benutzer eine Nachricht, ob die gesuchte Zahl grösser oder kleiner ist als die geratene Zahl. Drückt der Benutzer den Button anotherButton, soll eine neue Zufallszahl generiert werden und das Spiel von vorne beginnen (löschen Sie in diesem Fall alle Eingaben und auch das Label hintLabel). Nutzen Sie für diese Funktionalitäten die folgende Klasse HighLow:

→Video V-11.3

11.5

Die Spielerkarten App

297

4. Studieren Sie die folgenden Klassen MenuApp, MenuAppView, CreateMenuPane und ShowMenusPane und übernehmen Sie diese in Ihrem Projekt. Wenn Sie das Programm MenuApp starten, sollten Sie folgendes Fenster erhalten:

298

11 Graphische Benutzeroberflächen (Teil 2)

Nun kopieren Sie die Klassen Menu und MenuCard aus Übungsserie 8 in das gleiche Package und nehmen folgende zwei Anpassungen vor:

Literatur

299

– In der Klasse Menu passen Sie den Konstruktor so an, dass dieser die Instanzvariablen name und price gemäss den Parametern initialisiert. – In der Klasse MenuCard passen Sie die Methode addMenu so an, dass diese ein Menu menu als Parameter entgegennimmt und an der richtigen Stelle im Array platziert (der Aufruf des Konstruktors Menu ist also nicht mehr nötig). Verknüpfen Sie nun die Klassen der View mit der Funktionalität der Klasse MenuCard. Das heisst, es soll möglich sein, dass Benutzer neue Menüs eingeben und erfassen können. Weiter soll der Benutzer die aktuelle Menükarte anzeigen lassen können. Tipps: – Instanziieren Sie in der Methode start ein neues Objekt menuCard vom Typ MenuCard und geben Sie dieses als Parameter an den Konstruktor der Klasse MenuAppView mit (passen Sie den Konstruktor entsprechend an). Dort wiederum geben Sie das Objekt an die Konstruktoren von CreateMenuPane und ShowMenusPane weiter. In beiden Klassen (CreateMenuPane und ShowMenusPane) speichern Sie dann den Parameter menuCard als Instanzvariable. – In der Klasse CreateMenuPane programmieren Sie eine Methodenreferenz für den Button addButton: Die entsprechende Methode soll alle Informationen aus den TextField Objekten auslesen und damit ein neues Menu Objekt instanziieren. Das neu instanziierte Menu Objekt können Sie dann der menuCard hinzufügen. Zeigen Sie eine entsprechende Meldung im Label controlInformation an (z. B. Menu erfolgreich hinzugefügt). – In der Klasse ShowMenusPane zeigen Sie den Inhalt der MenuCard im Label menuText an. Bei jedem Klick auf den Button refresh soll dieses Label gemäss menuCard.toString() neu definiert werden.

→Video V-11.4

Literatur 1. John Lewis and William Loftus. Java Software Solutions – Foundations of Program Design. Pearson Global Edition, 8th edition edition, 2015.

Schnittstellen und Vererbung

12.1

12

Schnittstellen

Bisher haben wir die Terminologie Schnittstelle (engl. Interface) verwendet, um die Menge aller mit Sichtbarkeit public deklarierten Methoden zu beschreiben (also die Methoden, die man auch von ausserhalb der Klasse aufrufen kann). Wir formalisieren nun das Konzept einer Schnittstelle mit einem speziellen Element der Programmiersprache Java. Die Struktur einer Schnittstelle erinnert an eine Klasse (oder an ein enum), nur steht an Stelle von class (bzw. enum) das Schlüsselwort interface. Ein interface kann Konstanten (also Variablen deklariert mit dem Modifikator final) und/oder abstrakte Methoden enthalten. Eine abstrakte Methode besteht nur aus dem Methodenkopf abgeschlossen mit einem Semikolon (besitzt also keinen Methodenrumpf). Beispiel 12.1 In Abb. 12.1 ist zum Beispiel die Schnittstelle Buyable mit einer abstrakten Methoden getPrice gezeigt. Eine abstrakte Methode kann in einer Schnittstelle explizit mit den Modifikatoren public und abstract deklariert werden: public abstract double getPrice();

Alle Methoden in Schnittstellen sind automatisch abstrakt und haben die Sichtbarkeit public. Deshalb gibt man beides oftmals nicht an. Eine Schnittstelle kann man – im

Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_12.

© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_12

301

302

12 Schnittstellen und Vererbung

Abb. 12.1 Ein interface beinhaltet abstrakte Methoden (und/oder Konstanten). →Video K-12.1

Gegensatz zu Klassen – nicht instanziieren. Eine Schnittstelle darf deshalb keinen Konstruktor deklarieren. Man sagt: Eine Klasse implementiert eine Schnittstelle, wenn diese alle abstrakten Methoden der Schnittstelle mit einem Methodenrumpf ergänzt. Eine Klasse, die ein bestimmtes interface implementiert, benutzt das reservierte Wort implements gefolgt vom Bezeichner der Schnittstelle im Klassenkopf. Der folgende Klassenkopf gibt also zum Beispiel an, dass die Klasse Pasta die Schnittstelle Buyable implementiert: public class Pasta implements Buyable {

Eine implementierende Klasse muss immer alle Methoden der Schnittstelle programmieren (ansonsten wird der Kompilierer eine Fehlermeldung erzeugen). Beispiel 12.2 In Abb. 12.2 ist die Klasse Pasta gezeigt, die die Schnittstelle Buyable implementiert. Die Methoden der Schnittstelle (in diesem Fall die Methode getPrice) sind nun implementiert. Das heisst, der von der Schnittstelle vorgegebene Methodenkopf erhält nun einen Methodenrumpf. Die Verwendung einer Schnittstelle garantiert, dass die im interface deklarierten Methoden in allen implementierenden Klassen vorhanden sind. Es besteht aber keine Einschränkung, dass nicht noch weitere Methoden programmiert werden dürfen. Die Klasse Pasta zum Beispiel muss die Methode getPrice enthalten, darf daneben aber beliebig erweitert werden. In UML wird eine Schnittstelle wie eine Klasse dargestellt, mit dem Unterschied, dass oberhalb des Bezeichners der Schnittstelle steht. Eine gestrichelte Linie mit einem geschlossenen Pfeil wird von der implementierenden Klasse zur Schnittstelle gezeichnet (siehe Abb. 12.3).

12.1

Schnittstellen

303

Abb. 12.2 Die Klasse Pasta implementiert die Schnittstelle Buyable. →Video K-12.1 (Fortsetzung) Abb. 12.3 Ein UML Diagramm, das zeigt, dass die Klasse Pasta die Schnittstelle Buyable implementiert

Buyable + getPrice() : double

Pasta - name : String - boliingTime : int - price : double

+ Pasta(name : String, boilingTime : int, price : double) + getPrice() : double + toString() : String

304

12 Schnittstellen und Vererbung

Abb. 12.4 Die Klasse Toothpaste implementiert auch die Schnittstelle Buyable. →Video K-12.1 (Fortsetzung)

Mehrere Klassen können die gleiche Schnittstelle implementieren. Jede dieser Klassen kann alternative Versionen der durch die Schnittstelle vorgegebenen Methoden implementieren. Beispielsweise könnten wir eine Klasse Toothpaste programmieren, die ebenfalls die Schnittstelle Buyable implementiert (siehe Abb. 12.4). In diesem Beispiel ist die Methode getPrice zwar genau gleich programmiert wie in der Klasse Pasta, aber das ist nicht zwingend: Eine Schnittstelle gibt nur vor, dass eine Methode vorhanden sein muss, gibt aber nicht an, wie die Methode tatsächlich programmiert werden soll. Schnittstellen dienen u. a. dazu, unterschiedlichen Objekten ein gemeinsames Verhalten oder eine gemeinsame Sicht zu geben. Offensichtlich sind Pasta und Zahnpasta zwei unterschiedliche Objekte, aber sie besitzen eine Gemeinsamkeit: Sie sind beide käuflich. Wir formalisieren diese Tatsache, indem die Klassen Pasta und Toothpaste eine gemeinsame Schnittstelle Buyable implementieren. Beispiel 12.3 In der Klasse ShoppingCart in Abb. 12.5 deklarieren wir fünf Objekte als statische Variablen – drei Objekte vom Typ Pasta und zwei Objekte vom Typ Toothpaste. In der Methode main wird dann eine Liste purchase instanziiert, welche Objekte vom Typ Buyable aufnehmen kann1 . Danach fügen wir der Liste purchase mit der Methode 1 Etwas genauer: Objekte, deren Klassen die Schnittstelle Buyable implementieren.

12.1

Schnittstellen

305

Abb. 12.5 Die Klasse ShoppingCart. →Video K-12.2

add drei Objekte vom Typ Pasta und ein Objekt vom Typ Toothpaste hinzu. Ohne die Idee der Schnittstellen könnten wir keine solche Liste erstellen, da eine ArrayList immer nur Objekte eines Typs aufnehmen kann. Als nächstes wollen wir für purchase die Preissumme berechnen. Hierzu programmieren wir eine statische Methode priceSum, welche ein Liste purchase als Parameter entgegennimmt (siehe Abb. 12.6 – nach einer Idee aus [1]). Der Methode priceSum kann es egal sein, was sich tatsächlich in der Liste befindet – wichtig ist nur, dass die entsprechenden Klassen die Schnittstelle Buyable implementieren. Dies garantiert nämlich, dass der Zugriff auf die benötigte Methode getPrice funktioniert (da diese Methode von der Schnittstelle vorgegeben ist). Mit Hilfe von Schnittstellen können wir ganz unterschiedliche Sichten auf ein Objekt beschreiben. Jede Schnittstelle ermöglicht eine neue Sicht auf das Objekt – eine Art Rolle.

306

12 Schnittstellen und Vererbung

Abb.12.6 Die Methode priceSum erwartet eine Liste von Objekten, deren Klassen die Schnittstelle Buyable implementieren. →Video K-12.2

Implementiert eine Klasse diverse Schnittstellen, können ihre Instanzen in verschiedenen Rollen auftreten. Um anzugeben, dass eine Klasse mehrere Schnittstellen implementiert, werden diese mit Kommas separiert hinter implements aufgezählt: public class Pasta implements Buyable, Cookable {

12.1.1 Klassen Tauschen – Schnittstellen Beibehalten Einer der grössten Vorteile von Schnittstellen ist, dass sie Klassen austauschbar machen. Am besten veranschaulicht man sich das an einem Beispiel. Beispiel 12.4 Wir haben in vorigen Kapiteln zwei Versionen der Klasse PlayerCardCollection programmiert. In einer Version werden die PlayerCard Objekte in einer ArrayList organisiert und in einer zweiten Version benutzen wir hierzu ein Array PlayerCard[]. Eigentlich sollte es keine Rolle spielen, in welcher Version die Idee einer Spielerkartensammlung programmiert wird, so lange die Schnittstelle auf diese Sammlung eingehalten wird. Aus diesem Grund überlegen wir uns zunächst, welche Funktionalität eine PlayerCardCollection gegen Aussen anbieten sollte und definieren dann ein entsprechendes interface (siehe z. B. Abb. 12.7). Diese Schnittstelle gibt vor, dass eine implementierende Klasse die Methoden addPlayerCard und search anbieten muss.

12.1

Schnittstellen

307

In Abb. 12.8 sind die Klassen PlayerCardList und PlayerCardArray angedeutet (beide Klassen haben wir bereits in früheren Beispielen im Buch besprochen). Da beide Klassen die Schnittstelle PlayerCardCollection implementieren, bieten beide die Methoden addPlayerCard und search an. In unserem Beispielprogramm PlayerCardApp müssen wir uns bei der Instanziierung des Models entscheiden, welche Version wir nun tatsächlich benutzen wollen. Das heisst, wir müssen definieren, mit welcher Klasse wir die Schnittstelle PlayerCardCollection tatsächlich instanziieren wollen: PlayerCardCollection myCards = new PlayerCardList();

oder PlayerCardCollection myCards = new PlayerCardArray();

Und das ist alles – der Rest des geschriebenen Quellcodes bleibt identisch. Mit anderen Worten: Dank des interface PlayerCardCollection können wir die tatsächlich implementierende Klasse beliebig ändern oder austauschen – wichtig ist nur, dass die Schnittstelle gegen Aussen beibehalten wird.

Abb. 12.7 Die Schnittstelle PlayerCardCollection. →Video K-12.3

308

12 Schnittstellen und Vererbung

Abb. 12.8 Die Klassen PlayerCardList und PlayerCardArray implementieren beide die Schnittstelle PlayerCardCollection. →Video K-12.3 (Fortsetzung)

12.1

Schnittstellen

309

12.1.2 Die Schnittstelle Comparable Das Java API beinhaltet neben Klassen auch Schnittstellen. Die Schnittstelle Comparable ist zum Beispiel im Package java.lang definiert. Diese Schnittstelle gibt nur die Methode compareTo vor, welche ein Objekt als Parameter entgegennimmt und einen Wert vom Typ int zurückgibt. Die Absicht dieser Schnittstelle ist es, dass implementierende Klassen einen Vergleich von Objekten anbieten müssen. Die Methode compareTo kann man auf einem Objekt obj1 aufrufen und dabei ein zweites Objekt obj2 des gleichen Typs als Parameter übergeben: obj1.compareTo(obj2);

Wie der Dokumentation der Schnittstelle zu entnehmen ist, soll die Zahl, die compareTo zurückgibt, kleiner als 0 sein, wenn obj1 kleiner ist als obj2. Wenn beide Objekte gleich gross sind, soll 0 zurückgegeben werden und wenn obj1 grösser ist als obj2, wird eine Zahl grösser 0 zurückgegeben. Beispiel 12.5 In Abb. 12.9 ist der Einsatz der Schnittstelle Comparable illustriert. Die Klasse Pasta implementiert jetzt zwei Schnittstellen: Buyable und Comparable. Das heisst, Pasta muss jetzt zwingend die Methode compareTo (vorgegeben durch die Schnittstelle Comparable) implementieren. Grundsätzlich erwartet die Methode compareTo ein Objekt als Parameter. Mit spitzen Klammern hinter dem Bezeichner der Schnittstelle Comparable können wir die Schnittstelle typisieren und damit festlegen, von welchem Typ der Objektparameter sein soll (ähnlich wie wir die ArrayList typisieren). Die Methode compareTo erwartet nun also ein Objekt vom Typ Pasta als Parameter. Hat das aufrufende Pasta Objekt den gleichen Preis wie das Objekt other im Parameter, wird die im Beispiel programmierte Methode compareTo 0 zurückgegeben. Wenn dieses Objekt teurer ist als das Objekt other, liefert compareTo einen Wert grösser als 0 und umgekehrt wird der Rückgabewert kleiner als 0. Es liegt an der Programmiererin jeder Klasse, zu entscheiden, wann eine Objekt kleiner als, grösser als oder gleich gross ist wie ein anderes Objekt. In Abb. 12.10 sind zwei alternative Versionen der Methode compareTo für Objekte vom Typ Pasta gezeigt. Die erste Alternative gibt in jedem Fall 0 zurück – dies bedeutet, dass alle Objekte vom Typ Pasta als gleich angesehen werden. Die zweite Version vergleicht Objekte vom Typ Pasta auf Basis der Kochzeiten. Diese Version der Methode compareTo gibt . . . • 0 zurück, wenn beide Objekte die gleiche Kochzeit aufweisen, • einen Wert grösser bzw. kleiner 0 zurück, wenn dieses Objekt eine längere bzw. kürzere Kochzeit hat als das Objekt other.

310

12 Schnittstellen und Vererbung

Abb. 12.9 Die Klasse Pasta soll auch noch die Schnittstelle Comparable implementieren. → Video K-12.4

12.2 Vererbung

311

Abb. 12.10 Zwei alternative Versionen für die Methode compareTo in der Klasse Pasta. → Video K-12.4 (Fortsetzung)

Im Java API existiert eine Klasse Collections, welche u. a. eine statische Methode sort beinhaltet. Als Parameter erwartet diese Methode eine Menge von Objekten (z. B. in einer ArrayList organisiert) – die Methode wird dann die Menge aufsteigend sortieren. Eine Illustration hierzu ist in der Klasse PastaSort in Abb. 12.11 gezeigt. Die Ausgabe dieses Programmes lautet (wir verwenden die Version der Methode compareTo, welche Vergleiche auf Basis der Kochzeit durchführt): Liste vor dem Sortieren: [Pasta Spagetti - Kochzeit: 10, Preis: 2.5, Pasta Penne Kochzeit: 11, Preis: 2.9, Pasta Linguine - Kochzeit: 9, Preis: 2.9] Liste nach dem Sortieren: [Pasta Linguine - Kochzeit: 9, Preis: 2.9, Pasta Spagetti - Kochzeit: 10, Preis: 2.5, Pasta Penne - Kochzeit: 11, Preis: 2.9]

Der Methode sort können unterschiedlich typisierte Listen übergeben werden. Das heisst, die Klasse der Objekte in der Liste ist austauschbar, solange diese die Schnittstelle Comparable implementieren.

12.2

Vererbung

Vererbung ist eines der Kernkonzepte der objektorientierten Programmierung. Tatsächlich haben wir Vererbung schon an einigen Stellen in diesem Buch angewendet. Im Allgemeinen können neue Klassen via Vererbung schneller und einfacher erstellt werden, als wenn diese immer wieder von Grund auf neu programmiert werden. Bedenken Sie beim Programmieren immer, dass einige der Klassen, die Sie programmieren müssen, vielleicht bereits existieren könnten. Manchmal bilden diese die Anforderungen der neuen Klassen nicht perfekt ab, aber sie sind möglicherweise ähnlich genug, um als Basis für eine neue Klasse dienen zu können.

312

12 Schnittstellen und Vererbung

Abb. 12.11 Die Klasse Collections enthält die statische Methode sort, welche Objekte sortieren kann, wenn deren Klasse die Schnittstelle Comparable implementiert. → Video K-12.4 (Fortsetzung)

Das Wort Klasse (class) lehnt sich an die Idee der Klassifizierung von Objekten auf Basis derer Charakteristiken an. Solche Klassifikationen verwenden oftmals Hierarchien, um Klassen in Verbindung zu stellen. Zum Beispiel teilen alle Bücher gewisse Charakteristiken (Titel, Autor(en), Anzahl Seiten, etc.). Eine Teilmenge von Büchern sind z. B. Sachbücher, Lexikone oder Romane. Alle Lexikone sind Bücher – ein Lexikon besitzt aber auch eigene charakteristische Merkmale, die es von anderen Büchern (z. B. einer Gedichtsammlung) unterscheidet. Übersetzen wir diese Idee in die Programmierung: Gegeben sei eine Klasse Book (mit Variablen, die das Buch beschreiben und Methoden, die z. B. dessen Status verändern können). Eine Klasse Dictionary kann nun aus der Klasse Book abgeleitet werden. In Java kann man dies mit dem Schlüsselwort extends angeben:

12.2 Vererbung

313

public class Dictionary extends Book {

Dictionary besitzt automatisch alle Variablen und Methoden, die wir bereits in Book programmiert haben. In der Klasse Dictionary können wir also alle Variablen und Methoden der Klasse Book verwenden (ohne sie nochmals in Dictionary zu deklarieren). Zusätzliche Variablen und Methoden, die nur für die Klasse Dictionary nötig sind, können einfach hinzugefügt werden. Die Klasse, die benutzt wird, um eine neue Klasse abzuleiten, wird Superklasse genannt. Die abgeleitete Klasse nennt man Subklasse. Der Prozess des Vererbens etabliert eine ist-ein-Beziehung zwischen zwei Klassen (Z. B.: Ein Lexikon ist-ein Buch). Für jede Klasse X, die aus Y abgeleitet wird, muss man sagen können „X ist ein Y“. Falls eine solche Aussage keinen Sinn machen sollte, ist die Beziehung zwischen X und Y möglicherweise nicht geeignet für eine Vererbung. In der Klasse Book (siehe Abb. 12.12) speichern wir den Titel und die Anzahl Seiten dieses Buches als Instanzvariablen title und pages (nach einer Idee aus [2]). Beide Instanzvariablen werden via Konstruktor initialisiert und es gibt einen Getter und einen Setter für title und pages sowie eine Methode toString. Jede Methode oder Variable, die in der Superklasse mit der Sichtbarkeit public deklariert wird, kann in der Subklasse verwendet werden. Andererseits werden Methoden oder Variablen, die mit private deklariert sind, nicht vererbt. Wenn wir aber eine Instanzvariable mit public modifizieren würden, so dass die Subklasse darauf zugreifen kann, verletzen wir das Prinzip der Kapselung (nur das Objekt soll auf seine Instanzvariablen zugreifen können). Deshalb bietet Java einen dritten Sichtbarkeitsmodifikator an: protected. Die Variablen title und pages in der Klasse Book sind mit Sichtbarkeit protected deklariert. Diese Sichtbarkeit erlaubt es, einen Teil der Kapselung zu bewahren. So ist der Zugriff auf eine mit protected deklarierte Variable oder Methode nur innerhalb des Packages möglich, in dem sich die Klasse selbst befindet – protected kann als Mittelweg zwischen public und private angesehen werden: • private: Zugriff ausschliesslich aus der eigenen Klasse. • protected: Zugriff aus Klassen aus dem selben Package. • public: Zugriff aus jeder Klasse. Ein Mittelweg zwischen private und protected wäre eigentlich für die Vererbung optimal, um die Variablen nur für die Subklassen sichtbar zu machen. Die Klasse Dictionary (siehe Abb. 12.13 – nach einer Idee aus [2]) ist abgeleitet aus der Klasse Book. Damit erbt die Klasse Dictionary die Methoden getTitle, setPages und toString, sowie die Instanzvariablen title und pages. Es ist also so, als ob diese in Dictionary deklariert wären.

314

12 Schnittstellen und Vererbung

Abb. 12.12 Die Klasse Book verwaltet zwei Eigenschaften (title und pages), die alle Bücher teilen. →Video K-12.5

Beachten Sie zum Beispiel, die Methode definitionsPerPage. Diese Methode berechnet die durchschnittliche Anzahl Definitionen pro Seite (die Klasse Dictionary deklariert hierzu eine Instanzvariable definitions vom Typ int für die Anzahl Definitionen). Mit this.pages wird in der Methode definitionsPerPage die Variable pages referenziert, obschon diese nicht in Dictionary sondern in der Superklasse Book deklariert ist. Vererbung ist eine „Einbahnstrasse“. Die Klasse Book kann keine Variablen oder Methoden verwenden, die in der Klasse Dictionary definiert sind (z. B. können wir in einem Objekt vom Typ Book die Methode definitionsPerPage nicht verwenden). Diese Restriktion macht Sinn, da die Subklasse eine spezifischere Version der Superklasse ist. Ein

12.2 Vererbung

315

Abb. 12.13 Die Klasse Dictionary ist auch ein Book, bzw. erbt die Variablen und Methoden der Klasse Book. Zusätzlich besitzt ein Dictionary aber Definitionen (eine zusätzliche Instanzvariable definitions). →Video K-12.5 (Fortsetzung)

Lexikon hat Seiten, da alle Bücher Seiten haben, aber obschon ein Lexikon Definitionen beinhaltet, enthalten nicht alle Bücher Definitionen. In der Klasse Library (siehe Abb. 12.14) wird ein neues Objekt lexicon der Klasse Dictionary instanziiert und danach werden auf dem Objekt drei Methoden aufgerufen: getTitle, definitionsPerPage und die Methode toString. Die Methode getTitle ist geerbt, die anderen beiden Methoden sind in der Klasse Dictionary deklariert. Die Ausgabe des Programmes lautet: Titel des Buches: Das Wörterbuch Anzahl Definitionen pro Seite: 112.64396887159533

316

12 Schnittstellen und Vererbung

Abb. 12.14 Die Klasse Library verwendet geerbte und nicht geerbte Methoden. →Video K-12.5 (Fortsetzung)

Das Wörterbuch, 514 Seiten und 57899 Definitionen

In Abb. 12.15 ist die Vererbungshierarchie zwischen Book und Dictionary in UML gezeigt. Mit einem Pfeil mit weissem Kopf zeigt man von der Subklasse zur Superklasse. Die Sichtbarkeit protected wird in UML mit einem vorangestellten # markiert. Im Folgenden betrachten wir drei Aspekte der Vererbung, die wir im obigen Beispiel noch nicht besprochen haben: Den Konstruktor einer Subklasse, das Überschreiben von Methoden und den Aufbau von Klassenhierarchien.

12.2.1 Der Konstruktor einer Subklasse Der „Bauplan“ einer Subklasse basiert auf dem Plan der Superklasse. Wird ein Objekt einer Subklasse instanziiert, so werden die Baupläne beider Klassen (also Sub- und Superklasse) benötigt. Das heisst, dass die Klasse Book nötig ist, um ein Objekt vom Typ Dictionary zu instanziieren. Beachten Sie, dass es keinen Sinn machen würde, den Konstruktor der Superklasse an eine Subklasse zu vererben. Der Konstruktor einer Subklasse ist aber dafür verantwortlich,

12.2 Vererbung Abb. 12.15 UML Diagramm zur Vererbung: Die Klasse Dictionary erbt von der Klasse Book

317

Book # title : String # pages : int + Book(title : String, pages : int) + getTitle() : String + setPages(pages : int) + toString() : String

Dictionary

+ toString() : String

dass als erstes der Konstruktor der Superklasse aufgerufen wird. Dies geschieht mit folgender Anweisung: super();

In der ersten Zeile eines Konstruktors einer Subklasse sollte immer dieser Aufruf des Konstruktors der Superklasse programmiert werden. Falls diese Anweisung nicht vorhanden ist, wird Java den Konstruktor der Superklasse automatisch aufrufen. Diese Regelung garantiert, dass eine Superklasse zuerst seine Instanzvariablen initialisiert, bevor die Subklasse daraus abgeleitet wird. Falls der Konstruktor der Superklasse Parameter erwartet, können Sie sich nicht mehr auf den automatischen Aufruf des Superkonstruktors verlassen. In diesem Fall müssen Sie super selber aufrufen und die nötigen Parameter an den Superkonstruktor mitgeben. Im obigen Beispiel wird im Konstruktor Dictionary als erstes der Konstruktor der Superklasse Book aufgerufen. In diesem Fall erwartet der Konstruktor Book zwei Parameter vom Typ String und int, um den Titel und die Anzahl Seiten des Buches zu initialisieren: super(title, pages);

Sind in der Subklasse noch weitere Initialisierungen nötig, können diese nach dem Aufruf des Superkonstruktors programmiert werden.

318

12 Schnittstellen und Vererbung

12.2.2 Überschreiben von Geerbten Methoden Wenn eine Subklasse eine Methode deklariert, die den gleichen Methodenkopf besitzt wie eine Methode in der Superklasse, dann sagen wir, dass die Methode der Subklasse die Methode der Superklasse überschreibt (engl. method overriding)2 . Die Methode toString der Klasse Book steht durch Vererbung auch in Objekten des Typs Dictionary zur Verfügung. Im obigen Beispiel haben wir die Methode toString in Dictionary aber überschrieben (das heisst, neu definiert). In einer Subklasse haben wir also immer die Möglichkeit, eine geerbte Methode an die tatsächlichen Gegebenheiten anzupassen. Die Zeichenkette, welche die Methode toString in Objekten vom Typ Dictionary zurückgeben soll, beinhaltet nicht nur den Titel und die Anzahl Seiten sondern auch noch die Anzahl Definitionen. Da wir auf den Titel und die Anzahl Seiten zugreifen können, als wären diese in Dictionary deklariert, könnten wir in der neuen Version der Methode toString folgende return Anweisung programmieren: return this.title + ", " + this.pages + " Seiten und " + this.definitions + " Definitionen";

Die Klasse eines Objektes bestimmt jeweils, welche Methode ausgeführt wird (die Originalversion oder die überschreibende Version). Wenn die Methode toString auf einem Objekt vom Typ Book ausgeführt wird, so wird die Originalversion ausgeführt. Wird die gleiche Methode auf einem Objekt vom Typ Dictionary ausgeführt, so wird die überschreibende Version der Methode ausgeführt. Mit der Referenz super können wir immer auf die Methoden der Superklasse zugreifen. Folgende Anweisung ruft zum Beispiel die Methode toString einer Superklasse auf: super.toString();

Aufrufe von Methoden der Superklasse sind besonders beim Überschreiben von Methoden hilfreich. Zum Beispiel nutzen wir dies in der gezeigten Methode toString in der Klasse Dictionary. Zuerst wird mit super.toString() die Originalversion der Methode toString aufgerufen. Die Rückgabe wird der Variablen s zugewiesen, dann mit " und " + this.definitions + " Definitionen" konkateniert und schliesslich zurückgegeben. In Java stammen alle Klassen direkt oder indirekt von der Klasse Object ab3 . Wenn eine bestimmte Klasse keine Angabe macht, von welcher Klasse diese erbt (d. h. kein extends deklariert), so wird diese automatisch von der Klasse Object abgeleitet. 2 Hinweis: Verwechseln Sie das Überladen von Methoden nicht mit dem Überschreiben von Methoden! 3 Die Klasse Object gehört zum Package java.lang des Java APIs.

12.2 Vererbung

319

Da jede Klasse direkt (oder indirekt über mehrere Stufen hinweg) von der Klasse Object erbt, stehen in allen Klassen die mit Sichtbarkeit public deklarierten Methoden der Klasse Object zur Verfügung. Zwei wichtige Methoden der Klasse Object sind: • boolean equals(Object o): Gibt true zurück, wenn das aufrufende Objekt und der Parameter o Aliase sind. • String toString(): Gibt den Klassennamen des Objektes gefolgt von einer ID zurück. Üblicherweise wird die Methode toString überschrieben, um eine geeignete Zeichenkettenrepräsentation des Objektes zu definieren. Auch equals wird oft überschrieben, um spezifischere Gleichheitstests zu definieren. Z. B. können zwei Objekte als gleich angesehen werden, wenn die Werte aller (oder bestimmter) Instanzvariablen übereinstimmen. Eine Methode kann mit dem Modifikator final deklariert werden. (z. B. public final void message()). Eine Subklasse kann in diesem Fall diese Methode nicht überschreiben. Diese Technik kann dazu verwendet werden, um sicherzustellen, dass alle Subklassen die gleiche Version einer Methode verwenden. Der Modifikator final kann auch auf ganze Klassen angewendet werden (z. B. public final class Parameters). Aus einer solchen Klasse kann keine andere Klasse durch Vererbung abgeleitet werden.

12.2.3 Klassenhierarchien In Java ist nur die Einfachvererbung (engl. single inheritance) erlaubt. Einfachvererbung schreibt vor, dass eine Klasse von maximal einer Klasse direkt erben kann. Umgekehrt können aber mehrere Klassen von der gleichen Klasse abgeleitet werden. Eine Subklasse A einer Superklasse B kann wiederum Superklasse einer Klasse C sein. Vererbung entwickelt sich deshalb oftmals zu ganzen Klassenhierarchien. In Abb. 12.16 ist ein Beispiel einer Klassenhierarchie für Getränke gezeigt. Der Vererbungsmechanismus in Java ist transitiv. Das heisst, Variablen und Methoden werden entlang einer Kette von Super- zu Subklassen weitervererbt (jede geerbte Variable oder Methode wird wiederum weitervererbt). Das bedeutet also, dass eine geerbte Eigenschaft vom direkten Vorgänger stammen kann oder möglicherweise von einer Superklasse, die mehrere Stufen oberhalb der betrachteten Klasse liegt. Beachten Sie: Es gibt kein Maximum für die Anzahl Kinder (Subklassen) oder für die Anzahl der Stufen in einer Hierarchie und es gibt i.a. keine „beste“ Organisation für eine Klassenhierarchie – die Entscheidung, wie eine Hierarchie aufgebaut wird, liegt bei der Programmiererin. Abstrakte Klassen dienen als Platzhalter in einer Klassenhierarchie. Wie der Name impliziert, ist eine abstrakte Klasse eine abstrakte Entität, die üblicherweise ungenügend definiert

320

12 Schnittstellen und Vererbung

Abb. 12.16 Eine einfache Klassenhierarchie für Getränke

Beverage

SoftDrink

Alcoholic

Beer

Wine

ist, um selber nützlich zu sein4 . Stattdessen ist eine abstrakte Klasse eine Teilbeschreibung eines Konzeptes, die dann von den abgeleiteten Klassen geerbt wird. Mit dem Modifikator abstract wird eine abstrakte Klasse deklariert: public abstract class Beverage {

Eine abstrakte Klasse kann man nicht instanziieren und enthält deshalb keinen Konstruktor. Eine abstrakte Klasse enthält üblicherweise eine oder mehrere abstrakte Methoden (Methodenköpfe ohne Methodenrümpfe): public abstract double getAlcoholContent();

Eine abstrakte Klasse darf auch nicht abstrakte Methoden enthalten. Wir betrachten als Beispiel das Konzept eines Getränkes (also zum Beispiel ein Bier, ein Glas Wein oder eine Cola). Wir definieren für dieses Konzept eine Klasse Beverage (siehe Abb. 12.17). Diese Klasse repräsentiert kein konkretes Getränk und soll nicht instanziiert werden können und deshalb ist die Klasse abstract deklariert. Die abstrakte Klasse Beverage dient als „Anker“ für sämtliche Getränke der Anwendung und enthält Informationen, die auf alle Typen von Getränken anwendbar sind: Den Namen des Getränkes als String und das Volumen in Zentilitern als int. Beide Instanzvariablen sind mit Sichtbarkeit protected deklariert, so dass diese vererbt werden. Neben den Getter Methoden für die zwei Instanzvariablen enthält die Klasse Beverage auch noch drei abstrakte Methoden: getAlcoholContent, die keine Parameter erwartet und einen Wert vom Typ double zurückgibt, die Methode isBeer, die einen boolean zurückgibt und die Methode toString (für eine Zeichenkettenrepräsentation eines Getränkes). Die Subklassen von Beverage werden dann ihre eigenen spezifischen Definitionen dieser Methoden zur Verfügung stellen. 4 In UML werden die Bezeichner von abstrakten Klassen und abstrakten Methoden kursiv dargestellt.

12.2 Vererbung

321

Abb. 12.17 Die abstrakte Klasse Beverage. →Video K-12.6

Die Klasse Alcoholic (siehe Abb. 12.18) erbt von Beverage und ist ebenfalls abstract deklariert. Das heisst, auch aus dieser Klasse sollen keine Objekte instanziiert werden können. Trotzdem werden nun die beiden abstrakten Methoden getAlcoholContent und toString ausformuliert (Hinweis: Das ist nicht zwingend – solange die Subklassen einer abstrakten Klasse auch abstract sind, müssen die abstrakten Methoden nicht unbedingt ausformuliert werden – die Methode isBeer bleibt tatsächlich abstrakt). In der Methode getAlcoholContent wird die Instanzvariable alcoholContent zurückgegeben, welche alle alkoholischen Getränke zur Verfügung stellen sollen. Die Zeichenkettenrepräsentation in der Methode toString gibt den Namen und das Volumen

322

12 Schnittstellen und Vererbung

Abb. 12.18 Die abstrakte Klasse Alcoholic. →Video K-12.6 (Fortsetzung)

des Getränkes zusammen mit dem Alkoholgehalt zurück (der Alkoholgehalt wird mit dem statischen Objekt fmt vom Typ DecimalFormat formatiert). Die Klasse SoftDrink (siehe Abb. 12.19) erbt auch von der Klasse Beverage ist aber nicht abstrakt. Wir fügen die Information, ob das Getränk kohlensäurehaltig ist oder nicht, als Instanzvariable sparkling vom Typ boolean hinzu. Zudem programmieren wir für diese Klasse einen Konstruktor, der die drei Instanzvariablen (name, cl und sparkling) gemäss den Parametern initialisiert. Da wir SoftDrink als nicht abstrakte Klasse programmieren, zwingt uns der Kompilierer die in Beverage definierten abstrakten Methoden getAlcoholContent, isBeer und toString zu programmieren. Die Methode getAlcoholContent gibt einfach 0.0 zurück, isBeer gibt false zurück und die Methode toString definiert eine Zeichenkettenrepräsentation für alkoholfreie Getränke. Die Klasse Beer (siehe Abb. 12.20) ist eine konkrete Subklasse der abstrakten Klasse Alcoholic. Deshalb definieren wir auch hier einen Konstruktor, der den Namen des Biers und die Zentiliter gemäss den Parametern initialisiert. Der geerbten Variablen alcoholContent wird die Konstante ALC_CONTENT zugewiesen. Der Unterschied der Klasse Wine (siehe Abb. 12.21) zur Klasse Beer ist, dass der Konstruktor Wine den Alkoholgehalt als Parameter erwartet und den übergebenen Wert dann in der Instanzvariablen

12.2 Vererbung

323

Abb. 12.19 Die Klasse SoftDrink. →Video K-12.6 (Fortsetzung)

alcoholContent speichert. Zudem gibt die Methode isBeer hier natürlich false zurück. Beachten Sie, dass die beiden Klassen Beer und Wine die von Beverage vorgegebenen abstrakten Methoden getAlcoholContent und toString nicht implementieren müssen, da diese bereits in der direkten Superklasse Alcoholic definiert sind. Wenn wir eine Variable mit einem bestimmten Klassennamen deklarieren, so kann diese benutzt werden, um Objekte dieser Klasse zu referenzieren (das haben wir bisher immer so gemacht).

324

Abb. 12.20 Die Klasse Beer. →Video K-12.6 (Fortsetzung)

Abb. 12.21 Die Klasse Wine. →Video K-12.6 (Fortsetzung)

12 Schnittstellen und Vererbung

12.2 Vererbung

325

Beispiel 12.6 Wir deklarieren zum Beispiel eine Variable beverage vom Typ Beverage: Beverage beverage;

Wäre die Klasse Beverage nicht abstrakt, so könnten wir nun ein Objekt vom Typ Beverage instanziieren und der Variablen beverage zuweisen: beverage = new Beverage();

Durch die Vererbungshierarchie können wir der Variablen beverage aber auch beliebige Objekte eines Typs einer Subklasse von Beverage zuweisen. Zum Beispiel: beverage = new SoftDrink("Cola", 33, true);

Diese Zuweisungen funktionieren durch die gesamte Klassenhierarchie. Da die Klasse Alcoholic von Beverage erbt und die Klasse Wine von Alcoholic abgeleitet ist, ist die folgende Zuweisung ebenso gültig: beverage = new Wine("Bordeaux", 10, 0.168);

Die Variable beverage ist somit polymorph, da diese Objekte verschiedener Typen referenzieren kann (zum Beispiel Objekte vom Typ SoftDrink oder Wine). In der Klasse Bar in Abb. 12.22 befindet sich die Methode main zum Testen der Klassenhierarchie ausgehend von Beverage. Zuerst wird ein Array beverages instanziiert, das sechs Objekte vom Typ Beverage (bzw. Subklassen von Beverage) aufnehmen kann. Danach wird das Array mit Objekten vom Typ SoftDrink, Wine und Beer gefüllt (alles direkte oder indirekte Subklassen von Beverage, weshalb diese Zuweisungen zulässig sind). Das Array beverages wird also mit polymorphen Referenzen gefüllt. Mit einer ersten for-Schleife gehen wir schliesslich durch das Array beverages hindurch und geben jedes Objekt bev1 aus. Beachten Sie, dass hierzu in jedem Objekt bev1 die toString Methode aufgerufen wird. Wird diese Methode von einem Objekt des Typs Wine ausgeführt, unterscheidet sich diese z. B. von Objekten des Typs SoftDrink – Der Aufruf ist aber immer der selbe. In der zweiten for-Schleife rufen wir für alle Objekte in beverages die Methode isBeer auf – Vererbung garantiert uns, dass alle Getränkearten diese Methode anbieten werden. Nur wenn die Methode isBeer true zurückgibt, wird das entsprechende Getränk ausgeben. Wir könnten z. B. auch bev2.getAlcoholContent() für jedes Objekt im Array beverages aufrufen – diese Methode muss nämlich auch in jeder konkreten Subklasse von Beverage vorhanden sein.

326

12 Schnittstellen und Vererbung

Abb. 12.22 Die Klasse Bar testet die Klassenhierarchie. →Video K-12.6 (Fortsetzung)

Die Ausgabe des Programms lautet: Alle Getränke Cola, 33cl, mit Kohlensäure, Alkoholfrei Orangensaft, 25cl, ohne Kohlensäure, Alkoholfrei Bordeaux, 10cl, Vol. 16.8 % Chardonnay, 10cl, Vol. 15.8 % Felsenau Junker, 33cl, Vol. 4.5 % Quöllfrisch Lager, 50cl, Vol. 4.5 % Alle Biere

12.2 Vererbung

327

Felsenau Junker, 33cl, Vol. 4.5 % Quöllfrisch Lager, 50cl, Vol. 4.5 %

Aufgaben und Übungen zu Kap. 12 Theorieaufgaben 1. Definieren Sie ein interface mit dem Bezeichner Nameable. Klassen, die diese Schnittstelle implementieren, müssen eine Methode setName anbieten, die einen String als Parameter entgegennimmt und nichts zurückgibt. Zudem muss eine Methode getName definiert sein, die keinen Parameter entgegennimmt und einen String zurückgibt. 2. Wir nehmen an, dass die Klassen Person, Animal und Device je die Schnittstelle Nameable implementieren. Instanziieren Sie ein Array things, das Objekte vom Typ Person, Animal und Device aufnehmen kann. Fügen Sie things Objekte vom Typ Person, Animal und Device hinzu und Iterieren Sie mit einer for-Schleife durch das Array und rufen dabei für alle gespeicherten Objekte die Methode getName auf. 3. Gegeben sei eine Klasse A, in deren Konstruktor der Buchstabe "A" ausgegeben wird. Via Vererbung sei aus der Klasse A die Klasse B abgeleitet. Im Konstruktor von B wird der Buchstabe "B" ausgegeben. Was wird ausgegeben, wenn die Klasse B instanziiert wird? Wie erklären Sie sich Ihre Beobachtung? 4. Betrachten Sie die Klasse Dictionary. Weshalb kann man sich in diesem Beispiel nicht auf den automatischen Aufruf des Konstruktors der Superklasse verlassen? Was passiert, wenn Sie den Konstruktoraufruf super(title, pages) nicht zum Konstruktor von Dictionary hinzufügen? 5. Wozu kann die Referenz super verwendet werden? 6. Zeichnen Sie ein UML Klassen Diagramm, das eine Vererbungshierarchie für verschiedene Zahlungsmöglichkeiten in einem Geschäft zeigt. 7. Was ist die Rolle von abstrakten Klassen? Geben Sie ein Beispiel in Ihrem UML Diagramm aus der vorigen Aufgabe. 8. Welches ist die einzige Klasse in Java, welche keine Superklasse besitzt. 9. Weshalb enthält jede Klasse die Methoden toString und equals? 10. Was ist der Widerspruch bei einer Klasse die final und abstract deklariert wird? 11. Nehmen Sie an, dass die Klasse Card die Superklasse von PlayerCard ist. Welche der folgenden Zuweisungen ist möglich? Begründen Sie! – Card card = new PlayerCard(); – PlayerCard card = new Card();

328

12 Schnittstellen und Vererbung

12. Was ist der Hauptzweck von Schnittstellen und was der Hauptzweck von Vererbung – vergleichen Sie. 13. Richtig oder falsch? a) Ein interface kann nur abstrakte Methoden enthalten, sonst nichts. b) Eine abstrakte Methode ist eine Methode, die nur einen Methodenkopf aber keinen Methodenrumpf besitzt. c) Alle Methoden einer Schnittstelle müssen abstrakt sein. d) Alle Methoden einer abstrakten Klasse müssen abstrakt sein. e) Eine Klasse, die eine Schnittstelle implementiert, darf ausschliesslich die vorgegebenen Methoden der Schnittstelle implementieren. f) Mehrere Klassen können das gleiche interface implementieren. g) Eine Klasse kann mehrere Schnittstellen implementieren. h) Mehrere Klassen können aus der gleichen Klasse durch Vererbung abgeleitet werden. i) Eine Klasse kann von mehreren Klassen erben. j) Ein interface gibt vor, wie die Methoden in implementierenden Klassen programmiert werden müssen. k) Eine geerbte Methode kann nicht verändert werden. l) Vererbung kann nicht eingeschränkt werden. Java Übungen 1. Programmieren Sie eine Schnittstelle Area, die zwei Methoden getLength und getWidth vorgibt, die je einen Wert vom Typ double zurückgeben. Die zwei Klassen Paper und Table sollen beide Area implementieren. Für die Klasse Paper sollen die Breite und Länge eines DIN A4 Blattes zurückgegeben werden (21 cm × 29.7 cm). Bei Objekten vom Typ Table werden die Länge und Breite im Konstruktor über die Parameter definiert. In einer Klasse InterfaceTest programmieren Sie eine statische Methode computeArea, welche ein Objekt vom Typ Area entgegennehmen kann und dann die Fläche des übergebenen Objektes berechnet und zurückgibt. Rufen Sie die Methode computeArea zweimal auf: einmal mit einem Objekt vom Typ Paper und einmal mit einem Objekt vom Typ Table. →Video V-12.1 2. Programmieren Sie eine Klasse GreetingsList, die von ArrayList erbt. In einer Methode main in einer zweiten Klasse instanziieren Sie ein Objekt greetings vom Typ GreetingsList und überprüfen, ob die Methoden der Klasse ArrayList (z. B. add oder clear) auch auf dem Objekt greetings aufrufbar sind.

12.2 Vererbung

329

Überschreiben Sie in der Klasse GreetingsList die Methode add. Ihre neue Methode soll beliebig viele String Objekte entgegennehmen können, welche dann einzeln zur Liste hinzugefügt werden. Es sollen also bspw. folgende Aufrufe möglich sein: greetings.add("Hallo"); greetings.add("Hola", "Ciao", "Hello");

→Video V-12.2 3. Betrachten Sie die Vererbungshierarchie in folgender Abbildung: Money

Coin

Banknote

Implementieren Sie diese Hierarchie in Java. Die Klasse Money soll abstrakt sein und eine Variable value vom Typ int und eine Variable currency vom Typ String vorgeben (schreiben Sie für beide Variablen Getter und Setter). Definieren Sie zudem eine abstrakte Methode toString. Die Klasse Coin soll einen Konstruktor beinhalten, der value und currency gemäss Parametern initialisiert. Zudem soll eine Methode flip zufällig entweder 0 oder 1 zurückgeben. Die Methode toString soll implementiert sein – diese ruft bei jedem Aufruf die Methode flip auf und gibt die Information ob Kopf oder Zahl (0 oder 1) ebenfalls aus (mögliche Rückgabe: Münze: 2 Franken 0). Die Klasse Banknote besitzt eine zusätzliche Variable serialNr vom Typ String. Der Konstruktor von Money soll alle geerbten Variablen gemäss Parametern initialisieren. Die Variable serialNr soll automatisch definiert werden (die erste Banknote soll die Seriennummer 0001, die zweite Banknote die Nummer 0002, etc. erhalten). Die toString Methode soll ähnlich sein wie in der Klasse Coin, aber die Seriennummer ebenfalls berücksichtigen. Testen Sie alle Methoden beider Klassen ausführlich in einer zusätzlichen Klasse MoneyTest. Testen Sie insbesondere die Möglichkeit, sowohl Coin als auch Banknote Objekte in einer ArrayList moneyBag zu verwalten. →Video V-12.3

330

12 Schnittstellen und Vererbung

Literatur 1. Christian Ullenboom. Java ist auch eine Insel. Rheinwerk, 2017. 2. John Lewis and William Loftus. Java Software Solutions – Foundations of Program Design. Pearson Global Edition, 8th edition edition, 2015.

Laufzeitfehler – Die Klasse Exception

13

In diesem Buch haben wir an einigen Stellen von sogenannten Laufzeitfehlern gesprochen. Typische Situationen, welche einen Laufzeitfehler auslösen können, sind zum Beispiel: • • • • •

Division durch 0 Zugriff auf ein Element in einem Array mit ungültigem Index „Falsche“ Eingabe des Benutzers (z. B. Zeichen statt Zahl) Lesen einer Datei, die nicht gefunden werden kann Aufrufen einer Methode auf einem noch nicht instanziierten Objekt

In Java werden fehlerhafte oder unerwünschte Situationen mit Objekten der Klasse Exception (bzw. Subklassen von Exception) modelliert. Tritt eine dieser Situationen auf, sagt man, dass ein Objekt vom Typ Exception „geworfen“ wird (engl. throw an exception). Wir haben grundsätzlich drei Möglichkeiten, mit einem geworfenen Objekt vom Typ Exception umzugehen: Wir behandeln die geworfene Exception . . . 1. . . . nicht (wir ignorieren diese also) 2. . . . genau da, wo diese aufgetreten ist 3. . . . an einer anderen Stelle (geben die Exception also weiter) In den nächsten drei Abschnitten besprechen wir diese drei Möglichkeiten.

Elektronisches Zusatzmaterial: Die elektronische Version dieses Kapitels enthält Zusatzmaterial, das berechtigten Benutzern zur Verfügung steht. https://doi.org/10.1007/978-3-658-30313-6_13.

© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_13

331

332

13 Laufzeitfehler – Die Klasse Exception

Abb. 13.1 Vier Code Fragmente, die zur Laufzeit eine Exception werfen. →Video K-13.1

13.1

Nicht Behandeln einer Exception

Wird eine geworfene Exception nicht behandelt, wird das Programm normalerweise terminieren und es wird eine Fehlermeldung produziert (in der Standardausgabe – der Konsole). Diese Fehlermeldung beinhaltet den Namen der Exception, möglicherweise eine Nachricht, welche den Fehler umschreibt, sowie eine genau Angabe, wo im Quellcode die Exception geworfen wurde. Betrachten Sie die vier Code-Fragmente in Abb. 13.1. Die Anweisung numerator / denominator im ersten Fragment wirft eine sogenannte ArithmeticException (eine Subklasse von Exception). Diese Klasse repräsentiert die Situation, dass eine ungültige arithmetische Operation durchgeführt wird (in diesem Fall eine Division durch 0). Beachten Sie, dass die zweite println Anweisung nicht ausgeführt wird, da die geworfene Exception nicht behandelt wird. Das heisst, das Programm terminiert und gibt eine Beschreibung und den Ursprung des Problems aus. In diesem Beispiel wäre das folgende Ausgabe: exception in thread "main"java.lang.ArithmeticException: / by zero at ExceptionDemo.main(ExceptionDemo.java:11)

Die erste Zeile der Fehlerausgabe bei einer geworfenen Exception gibt an, welche Subklasse von Exception tatsächlich geworfen wurde. Oftmals haben diese Subklassen selbsterklärende Bezeichner, so dass der Grund für das Problem bereits dadurch klar werden kann.

13.2

Die try-catch Anweisung

333

Die drei anderen Code-Fragmente in Abb. 13.1 werfen zum Beispiel jeweils eine • ArrayIndexOutOfBoundsException, • FileNotFoundException und eine • NullPointerException

Beachten Sie, dass auch in diesen drei Beispielen die zweite println Anweisung jeweils nicht ausgeführt wird, da das Programm vorher mit einer Fehlermeldung terminiert. Möglicherweise wird der Bezeichner der tatsächlich geworfenen Exception in der Fehlerausgabe mit einer Nachricht ergänzt, welche das Problem noch etwas genauer beschreibt (in obigem Beispiel entspricht / by zero dieser Nachricht). Die restlichen Zeilen der Fehlermeldung entsprechen dem sogenannten call stack trace. Dieser gibt an, wo im Quellcode die Exception geworfen wurde. In unserem einfachen Beispiel umfasst der call stack trace nur gerade eine Zeile: at ExceptionDemo.main(ExceptionDemo.java:11)

Das bedeutet, dass der Fehler auf Zeile 11 in der Methode main in der Klasse ExceptionDemo geworfen wurde. Im Allgemeinen kann der call stack trace mehrere Zeilen umfassen. Diese Zeilen geben an, welche Methoden alle aufgerufen wurden, bis schliesslich der Methodenaufruf erfolgte, der zur Exception führte. In obigem Beispiel wurde offensichtlich nur eine Methode aufgerufen (nämlich main und diese hat auch gleich die Exception geworfen). Beispiel 13.1 Folgender call stack trace gibt an, dass die Methode main (in der Klasse ExceptionDemo) die Methode fooMethod (aus der Klasse Foo) aufgerufen hat, welche wiederum barMethod (aus der Klasse Bar) aufgerufen hat – und dort wurde die Exception geworfen (und zwar auf Zeile 10). at Bar.barMethod(Bar.java:10) at Foo.fooMethod(Foo.java:8) at ExceptionDemo.main(ExceptionDemo.java:17)

13.2

Die try-catch Anweisung

Im Folgenden wollen wir eine geworfene Exception mit einer try-catch Anweisung auffangen und behandeln – und zwar genau dort, wo diese auch geworfen wird. Die try-catch Anweisung besteht aus zwei Blöcken: Dem try Block und dem catch Block. Im try Block programmieren wir die kritischen Anweisungen. Das sind also Anweisungen, von denen die Programmiererin weiss, dass diese möglicherweise eine Exception

334

13 Laufzeitfehler – Die Klasse Exception

werfen werden. Im catch Block werden dann die Anweisungen programmiert, die ausgeführt werden sollen, sobald eine bestimmte Exception tatsächlich geworfen wird. Wird eine try-catch Anweisung ausgeführt, so werden zuerst die Anweisungen im try Block ausgeführt. Wird dabei keine Exception geworfen, fährt der Kontrollfluss unterhalb der gesamten try-catch Anweisung weiter (d. h. der catch-Block wird einfach übersprungen). Diese Situation entspricht dem normalen Programmablauf. Wird aber während der Abarbeitung der Anweisungen im try Block eine Exception geworfen, wird der Kontrollfluss direkt an den entsprechenden catch Block weitergeleitet (wenn dieser vorhanden ist). Das heisst, der Kontrollfluss geht an genau den catch Block über, deren Exception Klasse mit der geworfenen Exception übereinstimmt. Nach der Ausführung der Anweisungen im catch Block, fährt der Kontrollfluss unterhalb der gesamten try-catch Anweisung weiter – das heisst also, das Programm terminiert nicht, sondern läuft normal weiter. Eine try-catch Anweisung kann mit einem finally Block erweitert werden. Dieser Block wird ausgeführt, ungeachtet wie der try Block verlassen wird. Der finally Block wird oftmals verwendet, um sicherzustellen, dass bestimmte Teile einer Methode in jedem Fall ausgeführt werden (d. h. im Normalfall und im Fehlerfall). Beispiel 13.2 In Abb. 13.2 ist in der Methode main eine try-catch Anweisung gezeigt (inklusive finally Block). Wird dieses Programm mit einer korrekten Eingabe ausgeführt, wird folgendes ausgegeben: Geben Sie einen Zähler ein 7 Geben Sie einen Nenner ein 2 Resultat der ganzzahligen Division in Java: 3 Das wird in *jedem* Fall ausgeführt... Das Programm läuft hier weiter...

Beachten Sie, dass der catch Block übersprungen wird, da im try Block keine Exception geworfen wird. Der finally Block wird aber durchgeführt und danach läuft das Programm normal weiter. Gibt der Benutzer zur Laufzeit des Programmes statt einer ganzen Zahl eine andere Zeichenfolge ein (z. B. abc), generiert das Programm folgende Ausgabe: Geben Sie einen Zähler ein abc Das war keine gültige Eingabe! Das wird in *jedem* Fall ausgeführt... Das Programm läuft hier weiter...

13.2

Die try-catch Anweisung

335

Abb. 13.2 Die try-catch Anweisung demonstriert. →Video K-13.2

Auf Zeile 15 wird die Exception geworfen – um genau zu sein, eine InputMismatchException. Für diese Exception haben wir einen entsprechenden catch Block programmiert: catch (InputMismatchException exception) {

Die geworfene InputMismatchException wird also in diesem Block aufgefangen. In diesem Beispiel wird hier einfache eine println Anweisungen durchgeführt (Das war keine gültige Eingabe!). Der finally Block wird auch in diesem Fall durchgeführt und das Programm läuft danach normal weiter. Beachten Sie aber, dass der Kontrollfluss nach dem Auffangen einer Exception unterhalb der try-catch Anweisung fortfährt – in unserem Beispiel heisst das, dass die Benutzerinteraktion im try Block nach dem Werfen der Exception nicht wiederholt wird. Indem wir die try-catch Anweisung innerhalb einer while-Schleife platzieren, könnten wir dieses Ziel erreichen.

336

13 Laufzeitfehler – Die Klasse Exception

Abb. 13.3 Die try-catch Anweisung wird solange wiederholt, bis die letzte Zeile des try Blockes tatsächlich erreicht wird. →Video K-13.3

In Abb. 13.3 ist dieses Prinzip veranschaulicht. Die Variable inputOK wird nur dann true, wenn die letzte Zeile des try Blockes tatsächlich erreicht wird – in diesem Fall wurde also keine Exception geworfen und die Schleife kann verlassen werden. Im Fall einer geworfenen Exception wird die gesamte try-catch Anweisung wiederholt. Um den Nutzen des finally Blockes zu verdeutlichen, geben wir im nächsten Durchlauf des Programmes IntegerDivision eine 0 als Nenner ein. Dies führt zu einer ArithmeticException auf Zeile 19 (Division durch 0). Für diese Exception haben wir keinen catch Block programmiert – dies entspricht also einem von uns nicht erwarteten Fehler und die geworfene Exception wird dementsprechend nicht aufgefangen. Bevor das Programm nun mit einer Fehlermeldung terminiert, werden in diesem Fall noch sämtliche Anweisungen im finally Block ausgeführt: Geben Sie einen Zähler ein 7 Geben Sie einen Nenner ein 0 Das wird in *jedem* Fall ausgeführt... Exception in thread "main"java.lang.ArithmeticException: / by zero at ch13.IntegerDivision.main(IntegerDivision.java:19)

Ein try Block kann mehrere catch Blöcke besitzen. Das heisst, im obigen Beispiel könnten wir einen weiteren Block mit folgendem Kopf erstellen: catch (ArithmeticException exception)

In diesem Block können wir dann die Situation behandeln, dass der Benutzer für den Nenner eine 0 eingegeben hat. Beispiel 13.3 Nehmen wir an, dass wir ein Programm zum Einlesen von Codes programmieren wollen (nach einer Idee aus [1]). Ein gültiger Code bestehe dabei aus vier Ziffern. Das Programm in Abb. 13.4 liest Eingaben vom Benutzer ein, zählt die gültigen Eingaben und gibt am Schluss alle gültigen Codes aus.

13.2

Die try-catch Anweisung

337

Abb. 13.4 Die Klasse CodeReader. →Video K-13.4

Die Anweisungen im try Block versuchen die Zeichen an Position 0 bis 3 aus der Benutzereingabe code zu lesen (mit Hilfe der Methode substring). Danach wird aus dieser Zeichenkette eine ganze Zahl ausgelesen (mit Hilfe der Methode parseInt). Gibt es bei diesen Anweisungen Probleme, wird eine Exception geworfen. Zum Beispiel wird eine StringIndexOutOfBoundsException durch die Methode substring geworfen, wenn der eingegebene Code zu kurz ist. Die Methode parseInt

338

13 Laufzeitfehler – Die Klasse Exception

wird eine NumberFormatException werfen, falls sich an einer der Positionen 0 bis 3 keine Zahl befinden sollte. Für beide Fälle ist ein entsprechender catch Block programmiert und in beiden Fällen wird eine entsprechende Nachricht ausgegeben. Das Programm läuft danach aber normal weiter. Beachten Sie, dass die Zahl number nur dann zur Liste validCodes hinzugefügt wird, wenn keine Exception geworfen wird. Im Fall, dass eine der beiden Exception geworfen wird, springt der Kontrollfluss unmittelbar in den entsprechenden catch Block und die restlichen Anweisungen im try Block werden übersprungen. Eine mögliche Einund Ausgabe wäre z. B.: Code-Eingabe Aus den ersten vier Ziffern Ihrer Eingabe wird jeweils der Code erstellt. Mindestens vier Ziffern eingeben Mindestens Vier Ziffern eingeben Code zu kurz! Mindestens Vier Ziffern eingeben Mindestens Vier Ziffern eingeben Code darf nur Ziffern enthalten! Mindestens Vier Ziffern eingeben Mindestens Vier Ziffern eingeben

– q zum Beenden: 1979 – q zum Beenden: 123 – q zum Beenden: 12345 – q zum Beenden: A123 – q zum Beenden: 2019 – q zum Beenden: q

Anzahl gültige Codes: 3 Gültige Codes: [1979, 1234, 2019]

13.3

Weitergeben einer Exception

Wird eine Exception in einer Methode nicht dort aufgefangen und behandelt, wo diese auch geworfen wird, so wird der Kontrollfluss unmittelbar an die aufrufende Methode zurückgegeben, sobald die Exception geworfen wird. Wir können unseren Quellcode so konzipieren, dass die Exception dann dort aufgefangen und behandelt wird (innerhalb einer try-catch Anweisung). Wird die geworfene Exception aber auch dort nicht aufgefangen, so wird diese erneut weitergegeben (wiederum an die aufrufende Methode). Diese Weitergabe einer geworfenen Exception wird solange fortgesetzt, bis die geworfene Exception entweder aufgefangen und behandelt wird oder letztlich die main Methode erreicht wird. Falls in main die Exception ebenfalls nicht behandelt wird (d. h. auch main gibt die Exception weiter), terminiert das Programm nicht ordnungsgemäss und es wird eine entsprechende Fehlermeldung ausgegeben.

13.3 Weitergeben einer Exception

339

Damit eine Methode eine geworfene Exception weitergeben kann, muss deren Methodenkopf mit einem throws und dem Bezeichner der Exception erweitert werden1 . Folgender Methodenkopf gibt zum Beispiel an, dass die Methode method möglicherweise eine NumberFormatException werfen wird: public void method() throws NumberFormatException {

Eine von der Methode method geworfene NumberFormatException kann nun ausserhalb von method aufgefangen werden. Hierzu wird der Aufruf der Methode method innerhalb einer try-catch Anweisung platziert: String name = this.nameField.getText(); if (name.length() == 0) throw new EmptyFieldException("Kein Name angegeben!");

Alternativ kann der Methodenkopf der aufrufenden Methode ebenfalls mit einem throws erweitert werden, so dass diese die Exception auch weitergeben kann. Mit dieser Technik können wir für unterschiedliche Exception Typen unterschiedliche Stellen in unserem Quellcode definieren, wo diese aufgefangen und behandelt werden sollen. Wir betrachten hierzu ein Beispiel. Beispiel 13.4 Die Klasse FileReader in Abb. 13.5 demonstriert das Weitergeben einer Exception. Die Methode readFile wirft möglicherweise eine FileNotFoundException während die Methode extractParameters möglicherweise eine NoSuchElementException werfen wird (beides ist in den Methodenköpfen ersichtlich). Die statische Methode readFile instanziiert zunächst ein Objekt vom Typ Scanner, das aus einer Datei lesen kann. Dieser Aufruf kann eine FileNotFoundException werfen, da die Datei möglicherweise nicht gefunden werden kann. In diesem Fall wird die geworfene FileNotFoundException an die aufrufende Methode weitergegeben. Die Methode main in der Klasse Main (siehe Abb. 13.6) ruft die Methode readFile auf. Der Aufruf von readFile ist innerhalb eines try Blockes platziert und für die mögliche FileNotFoundException ist ein catch Block vorhanden. Falls die Datei parameters nicht im Ordner src/ gefunden werden kann, wird die von readFile 1 Dies ist eigentlich nicht ganz korrekt: Man unterscheidet zwischen sogenannt überprüften und

unüberprüften Exception. Eine überprüfte Exception muss entweder direkt aufgefangen werden (innerhalb einer try-catch Anweisung) oder der Methodenkopf muss die Exception hinter throws angeben. Eine unüberprüfte Exception erfordert kein throws im Methodenkopf. Die Klasse NumberFormatException ist zum Beispiel eine unüberprüfte Exception. Es ist aber auch nicht falsch, wenn man im Methodenkopf auch für unüberprüfte Exception jeweils ein throws definiert.

340

13 Laufzeitfehler – Die Klasse Exception

Abb. 13.5 Die Klasse FileReader. →Video K-13.5

erhaltene Exception also hier aufgefangen und die restlichen Anweisungen im try Block werden nicht ausgeführt. Dies führt zu folgender Ausgabe: Fataler Fehler: Die Datei mit den Parametern konnte nicht geladen werden Fehlermeldung: src/parameters (No such file or directory) Der Call Stack Trace:

13.3 Weitergeben einer Exception

341

Abb. 13.6 Die Klasse Main. →Video K-13.5 (Fortsetzung)

java.io.FileNotFoundException: src/parameters (No such file or directory) at java.io.FileInputStream.open0(Native Method) at java.io.FileInputStream.open(FileInputStream.java:195) at java.io.FileInputStream.(FileInputStream.java:138) at java.util.Scanner.(Scanner.java:611) at ch13.FileReader.readFile(FileReader.java:10) at ch13.Main.main(Main.java:14) Das Programm kann nicht starten Das Programm wird beendet...

Beachten Sie, dass wir in diesem catch Block zwei Methoden aufrufen, die alle Subklassen von Exception zur Verfügung stellen (durch Vererbung): getMessage und

342

13 Laufzeitfehler – Die Klasse Exception

printStackTrace. Die erste Methode liest die gespeicherte Fehlermeldung im entsprechenden Exception Objekt aus, während die zweite Methode den call stack trace ausgibt. Nachdem der catch Block ausgeführt ist, springt der Kontrollfluss auf Zeile 28 der Klasse Main und das Programm wird beendet. Das bedeutet insbesondere, dass keine weiteren Anweisungen der Methode readFile ausgeführt werden – das Lesen der Datei ging schief und der Kontrollfluss hat mit der geworfenen Exception die Methode readFile unmittelbar verlassen. Angenommen die Datei parameters existiert und enthält die drei Zahlen 17 3 5 auf einer Zeile. In diesem Fall wird diese Zeile in der Methode readFile eingelesen und der Variablen line zugewiesen. Danach erfolgt innerhalb eines try Blockes der Aufruf der Methode extractParameters. In dieser Methode werden mit Hilfe eines weiteren Scanner Objektes und einer forSchleife die drei Zahlen aus line ausgelesen und im statischen int[] parameters gespeichert. Da alles geklappt hat, springt der Kontrollfluss danach zurück in die Methode readFile, wo der catch Block übersprungen wird. Zurück in main werden die geladenen Parameter ausgegeben: Das Programm startet mit den Parametern: 17 3 5 Das Programm wird beendet...

In der Methode extractParameters können unterschiedliche Dinge schief gehen. Befinden sich zum Beispiel weniger als 3 Elemente in line, wird die Methode next auf Zeile 27 eine NoSuchElementException werfen. In diesem Fall wird das Einlesen der Parameter direkt unterbrochen und die Exception an die aufrufende Methode – also readFile – weitergegeben. Die Methode readFile fängt die NoSuchElementException in einem catch Block auf und generiert eine entsprechende Warnung. Danach kehrt der Kontrollfluss zurück in die Methode main und dort werden die bis zum Werfen der Exception geladenen Parameter ausgegeben. Enthält die Datei parameters zum Beispiel die zwei Zahlen 17 3, lautet die Ausgabe: WARNUNG: Zu wenige Parameter in der Datei Das Programm startet mit den Parametern: 17 3 0 Das Programm wird beendet...

13.4

Eine Eigene Exception Programmieren und Werfen

343

Ist eines der drei Elemente in line nicht eine ganze Zahl (z. B. 17 3.1, 5), wird die Methode parseInt auf Zeile 29 eine NumberFormatException werfen. Diese Exception wird von extractParameters nicht weitergegeben, sondern direkt aufgefangen (im entsprechenden catch wird eine Warnung ausgegeben). Die for-Schleife fährt danach aber normal weiter, da wir die Exception ja nicht weitergegeben haben. Die Ausgabe lautet also zum Beispiel: WARNUNG: Parameter 3.1 an Position 1 ist keine ganze Zahl Das Programm startet mit den Parametern: 17 0 5 Das Programm wird beendet...

13.4

Eine Eigene Exception Programmieren und Werfen

Wir können eigene Exception Klassen definieren, indem wir von der Klasse Exception (oder einer Subklasse davon) erben. Beispiel 13.5 In Abb. 13.7 haben wir die Klasse EmptyFieldException aus der Klasse Exception abgeleitet. Sehr oft sind eigene Exception Klassen nicht komplexer, als die in diesem Beispiel gezeigte: Eine direkte Erweiterung der existierenden Klasse Exception, welche eine bestimmte Fehlermeldung speichert (eine Beschreibung der fehlerhaften Situation). In diesem Fall deklarieren wir den Konstruktor EmptyFieldException so, dass dieser eine Zeichenkette message als Parameter erwartet. Diese Zeichenkette wird via super

Abb. 13.7 Die Klasse EmptyFieldException. →Video K-13.6

344

13 Laufzeitfehler – Die Klasse Exception

an den Konstruktor der Superklasse weitergegeben. Die Superklasse Exception speichert dann diese Fehlermeldung und man kann diese mit der geerbten Methode getMessage bei Bedarf auslesen. Haben wir eine eigene Exception programmiert, können wir diese wenn nötig werfen. Hierzu verwenden wir das Schlüsselwort throw. Beispiel 13.6 Nehmen wir an, wir wollen den Text aus einem TextField Objekt nameField auslesen (mit der Methode getText). Falls nameField leer ist, werden wir ein neues Objekt vom Typ EmptyFieldException werfen: String name = this.nameField.getText(); if (name.length() == 0) throw new EmptyFieldException("Kein Name angegeben!");

Abb. 13.8 Die Methoden getPlayerName, getClubName und getNationality können eine EmptyFieldException werfen. →Video K-13.6 (Fortsetzung)

13.4

Eine Eigene Exception Programmieren und Werfen

345

Beispiel 13.7 Wir erweitern ein letztes Mal unser Programm zur Verwaltung von Spielerkarten. In der Klasse AddPlayerView überprüfen wir bei jedem der drei Getter (getPlayerName, getClubName und getNationality), ob der Benutzer auch tatsächlich etwas in die Textfelder geschrieben hat bzw. ob er eine Nationalität in der ChoiceBox nationalityChoiceBox ausgewählt hat (siehe Abb. 13.8). Falls nicht, wird in allen Methoden eine neue EmptyFieldException (mit unterschiedlicher Fehlermeldung) geworfen. In der Methode addPlayer in der Klasse Controller werden die drei Methoden getPlayerName, getClubName und getNationality aufgerufen, um eine neue Spielerkarte zu instanziieren und der Liste myCards hinzuzufügen. Da jetzt diese Methoden möglicherweise eine EmptyFieldException werfen werden, platzieren wir diese Aufrufe innerhalb eines try Blockes (siehe Abb. 13.9).

Abb. 13.9 Die Methode addPlayer fängt die EmptyFieldException auf. →Video K-13.6 (Fortsetzung)

346

13 Laufzeitfehler – Die Klasse Exception

Abb. 13.10 Die Fehlermeldung des geworfenen EmptyFieldException Objektes wird im GUI angezeigt

Eine geworfene EmptyFieldException wird im catch Block aufgefangen und die Fehlermeldung (via getMessage aus dem Objekt e ausgelesen) wird in der Statusleiste der Hauptsicht angezeigt (siehe Abb. 13.10).

Aufgaben und Übungen zu Kap. 13 Theorieaufgaben 1. Die erste der folgenden Anweisungen wird möglicherweise eine ClassCastException, die zweite möglicherweise eine FileNotFoundException werfen. Fangen Sie diese je mit einer try-catch Anweisung ab. • CDPlayer cdPlayer = (CDPlayer) mPlayer; • Scanner fileScan = new Scanner(new File("/url/to/file.txt"));

2. Die folgende Methode increaseSemester soll eine NotInitializedException werfen, falls s noch nicht instanziiert ist (also null referenziert). Ergänzen Sie die Methode so, dass die geworfene NotInitializedException an die aufrufende Methode zurückgegeben wird: public void increaseSemester(Student s) { …

13.4

Eine Eigene Exception Programmieren und Werfen

347

3 Definieren Sie eine Klasse FatalException. Wird diese Exception geworfen, soll folgende Fehlermeldung ausgegeben und im entsprechenden Objekt gespeichert werden: Fataler Fehler aufgetreten.

4. Illustrieren Sie an folgender Skizze, wie eine Exception . . . obj main

doThis

obj.doThis();

help

help();

critical Operation

• . . . gar nicht behandelt wird. • . . . dort aufgefangen wird, wo diese auftritt. • . . . an einer andere Stelle aufgefangen wird (z. B. in doThis). Hierbei nehmen wir an, dass in der Methode help eine Anweisung ausgeführt wird, die möglicherweise schief geht – falls dies der Fall sein sollte, wird eine neue OwnException geworfen. 5. Welche Ausgabe generiert die main Methode in der Klasse ExceptionTest mit . . . 1. 2. 3. 4.

. . . index . . . index . . . index . . . index

= = = =

-1. 1. 2. 5.

348

13 Laufzeitfehler – Die Klasse Exception

Die Klassen Review, IllegalIndexException und NegativeIndexException sind auf der nächsten Seite definiert. 6. Wie müssen Sie das obige Beispiel anpassen, dass das Programm auch für den Fall index = 5 ordnungsgemäss terminiert. Java Übungen 1. Schreiben Sie ein Programm, das dem Benutzer die Eingabe von beliebig vielen ganzen Zahlen ermöglicht. Die eingegebenen Zahlen sollen in einer Liste gespeichert und am Schluss ausgegeben werden. Fangen Sie die Situation ab, in der der Benutzer eine Gleitkommazahl oder sonstige Zeichen eingibt. Hierzu können Sie die NumberFormatException verwenden, die von der Methode parseInt geworfen wird, falls die zu konvertierende Zeichenkette keine ganze Zahl ist. Gibt der Benutzer q ein, soll das Programm terminieren.

→Video V-13.1

13.4

Eine Eigene Exception Programmieren und Werfen

349

2. Programmieren Sie eine eigene Klasse StringTooLongException, die von Exception erbt. Schreiben Sie dann in einer Testklasse eine main Methode, die eine statische Methode readInput aufruft. Die Methode readInput bittet den Benutzer, eine Zeichenkette mit maximal 20 Zeichen einzugeben. Falls eine zu lange Zeichenkette eingegeben wird, soll diese Methode eine StringTooLongException werfen, die weder hier noch in der main Methode abgefangen wird. Testen Sie verschiedene Abläufe Ihres Programmes! Modifizieren Sie Ihr Programm so, dass die StringTooLongException in main aufgefangen wird. Geben Sie eine Fehlermeldung aus und danach soll das Programm

350

13 Laufzeitfehler – Die Klasse Exception

normal terminieren. →Video V-13.2

Literatur 1. John Lewis and William Loftus. Java Software Solutions – Foundations of Program Design.Pearson Global Edition, 8th edition edition, 2015.

Und Jetzt?

14

Wenn Sie es bis hierhin geschafft haben und insbesondere wenn Sie die Übungen ernst genommen haben, sollten Sie jetzt den Einstieg in die Programmierung gemeistert haben und Sie dürfen von sich behaupten, dass Sie mit Java objektorientiert programmieren können. Allerdings war das erst der Anfang. Viele technische Details oder etwas komplexere Konzepte sind in diesem Buch ganz bewusst weggelassen oder abstrahiert. Ich empfehle Ihnen, als nächstes folgende Themen anzugehen: • Wir haben die ArrayList als möglichen Behälter für Objekte kennen gelernt. Das Java API bietet noch eine Vielzahl weiterer Behälter für Objekte an (z. B. die Klasse Stack, Queue, HashMap oder TreeSet). Eine gute Programmiererin sollte all diese Behälter einsetzen können und deren Vor- und Nachteile kennen. • Wir haben unsere Beispielprogramme nicht systematisch getestet – Testen ist sehr wichtig bei der Programmierung. Als Stichworte empfiehlt es sich, den Debugger kennen zu lernen und sich mit sogenannten Unit Tests vertraut zu machen. • Wir haben ausschliesslich „normale“ Kommentare verwendet. Informieren Sie sich, wie man mit Javadoc kommentieren und dokumentieren kann. • Wir haben in diesem Buch nur sehr einfache Algorithmen programmiert. Das Entwickeln von eigenen Algorithmen ist eine Kernkompetenz von guten Programmiererinnen. Starten Sie mit dem Programmieren von Such- und Sortieralgorithmen und informieren Sie sich dabei auch über rekursive Programmierung. • Neben den besprochenen Kontrollelementen von JavaFX existieren zahlreiche weitere Möglichkeiten (z. B. die Klassen ScrollPane oder ComboBox). Zudem haben wir nur auf Event Objekte reagiert (mit dem MVC-Pattern). Es gibt weitere Konzepte, wie man das Zusammenspiel zwischen View und Model organisieren kann (z. B. mit einem sogenannten Listener). • Lambda Ausdrücke und Funktionale Programmierung mit Java sind zwei weitere Themen, mit denen Sie fortfahren können. © Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6_14

351

352

14 Und Jetzt?

• Weitere spannenden Themen sind die Datenbankanbindung mit JDBC, innere Klassen, Threads (nebenläufige Programmierung) oder das Archivformat JAR. Um sich in diese (und weitere) Themen einzuarbeiten, ist ein Java Buch für Fortgeschrittene empfehlenswert (z. B. [1] oder [2]). Ausserdem finden Sie viele Tutorials im WWW. Schliesslich empfiehlt es sich, eher zu früh als zu spät eine weitere Programmiersprache zu erlernen (z. B. Python, Ruby, oder . . .).

Literatur 1. John Lewis and William Loftus. Java Software Solutions – Foundations of Program Design. Pearson Global Edition, 8th edition, 2015. 2. Christian Ullenboom. Java ist auch eine Insel. Rheinwerk, 2017.

Musterlösungen

Lösungen zu den Aufgaben aus Kapitel 1 1. Der erste Kommentar kommentiert etwas „offensichtliches“ und ist deshalb überflüssig. Der zweite Kommentar ist nicht eindeutig: Was muss geändert werden, weshalb muss es geändert werden, wann muss es geändert werden, wie soll es geändert werden, . . . ? 2. Die erste Anweisung gibt Hallo aus. Die zweite Zeile entspricht keiner Anweisung sondern einem Kommentar. Alles was auf einer Zeile nach // folgt, wird vom Kompilierer als Kommentar interpretiert und somit ignoriert. Die zweite Zeile erzeugt also keine Ausgabe. 3. Ausgabe: Ha Hi Ho 4. Ungültige Bezeichner: quiz Grade (Bezeichner bestehen aus einem Wort) 2ndGrade (erstes Zeichen ist eine Ziffer), grade# (nicht erlaubtes Sonderzeichen), grades1&2 (nicht erlaubtes Sonderzeichen). 5. Mögliche Bezeichner • • • •

Exam totalPoints oder points oder examPoints oder . . . computeAveragePoints oder averagePoints oder . . . MAX_POINTS (als Konstante definiert)

6. Ein Java Kompilierer erzeugt zunächst aus dem Java Quellcode Bytecode. Dies kann auf einer beliebigen Plattform geschehen, solange ein Java Kompilierer installiert ist.

© Springer Fachmedien Wiesbaden GmbH, ein Teil von Springer Nature 2020 K. Riesen, Java in 14 Wochen, https://doi.org/10.1007/978-3-658-30313-6

353

354

Musterlösungen

Der so erzeugte Bytecode kann nun auf verschiedenen Plattformen ausgeführt werden – einzige Voraussetzung hierfür ist, dass die ausführenden Maschinen Java installiert haben (die sogenannte Java Virtual Machine interpretiert den plattformunabhängigen Bytecode und führt dann die entsprechenden plattformspezifischen Maschinenbefehle aus). Mögliche Skizze: INTERPRETER

COMPILER

Java Quellcode

Java Bytecode

INTERPRETER

COMPILER

.class

.java

INTERPRETER

COMPILER

7. Kompilierfehler (K) oder Logischer Fehler (L): • • • • • • •

Ein reserviertes Wort wird falsch geschrieben. K Division durch 0. L Fehlerhafte Ausgabe. L Am Ende einer Programmieranweisung fehlt das Semikolon. K Fehlerhafte Ausgabe. L Falsche Klammer verwendet. K Fehlerhafte Berechnung. L

8. Die fünf Syntaxfehler sind:

{

// }

; “

Lösungen zu den Aufgaben aus Kapitel 2

355

9. Mögliche Variablen und Methoden:

Klasse Flight

Student

Variablen Flugnummer, Pünktlich (Ja/Nein)?, Zielflughafen, ... Name, Vorname, Matrikelnummer, . . .

Methoden Verspätung Hinzufügen, Flugnummer ändern, Reservation Hinzufügen, . . . Hauptfach ändern, Telefonnummer hinzufügen, Semester erhöhen, . . .

Lösungen zu den Aufgaben aus Kapitel 2 1. Ausgaben: Ich bin: 10 Ich bin: 55 10 bin ich. Ich bin: 25 2. Ausgabe: Eine "Klasse"kann enthalten: Variablen, Methoden, oder beides. 3. System.out.println("\"Mein Name ist Winston Wolfe.\n"+ "Ich löse Probleme!\ ", stellte er sich vor."); 4. a. Vier Variablen und eine Konstante sind deklariert. Die Variablen value und total sind noch nicht mit einem Wert initialisiert – sind also noch undefiniert. b. Nicht korrekt sind: MAX = 999; → Konstanten können nicht geändert werden count = 17; → Die Variable count ist nicht deklariert total = 1.423; → einer Variablen vom Typ int kann man keine Gleitkommazahl zuweisen total = 2147483648; → der grösstmögliche int Wert ist 2147483647

356

Musterlösungen

5. Zwei Deklarationen: int numOfStudents = 0; final int AVAILABLE_SEATS = 50; 6. Eine Variable vom Typ int kann immer nur einen Wert gleichzeitig speichern. 7. Nach dieser Anweisung speichert die Variable radius immer noch 7. Wollen wir den Wert von radius ändern, so müssen wir eine entsprechende Zuweisung programmieren: radius = radius * 2; 8. Resultate: • 4 (Der Rest der Division 29/5 ist 4) • 3 (Werden zwei ganze Zahlen dividiert, wird der Nachkommabereich abgeschnitten) 9. Reihenfolgen: • • • •

a a a a

+(1) +(3) /(2) *(4)

b -(2) c +(3) d b /(1) c -(4) d (b +(1) c) +(4) (b *(3) (c +(2)

-(4) e *(2) e d %(3) e (d +(1) e)))

10. 11. 12. 13. 14.

12 90 3 result speichert immer noch 17.51 und value speichert 17. a. 5 b. 5.0 c. 2.4 d. 2 e. 5.4 15. Werte der Booleschen Ausdrücke: a. b. c. d. e. f.

val1 = val2 → true val1 < val2 / 2 → false val1 != val2 → true !(val1 == val2) → true (val1 < val2) || ok → true

Lösungen zu den Aufgaben aus Kapitel 3

357

g. (val1 > val2) || ok → false h. (val1 < val2) && !ok → true i. ok || !ok → true 16. Wahrheitstabelle: (num > MAX) true true false false

ok true false true false

!ok false true false true

(num > MAX) && !ok false true false false

17. Ausgaben: • rot weiss gelb • rot blau gelb • weiss gelb 18. Ausgaben: • • • • •

1 1 3 3 3

2 2 4 5 7

3 4 8 3 4 8 8 6 8 8

Lösungen zu den Aufgaben aus Kapitel 3 1. Mit dem new-Operator wird der Konstruktor einer Klasse aufgerufen, welcher ein neues Objekt der entsprechenden Klasse instanziiert. Der new-Operator gibt danach die Adresse auf das instanziierte Objekt zurück. 2. Die gesuchten Methoden der Klasse ArrayList: • ArrayList list = new ArrayList(); • list.add("Hello!");

358

Musterlösungen

• list.get(i); • list.clear(); • list.contains("Hello!"); 3. Die Klasse String repräsentiert das allgemeine Konzept einer Zeichenkette. Dieses Konzept besagt, dass eine Zeichenkette aus einer Reihe von Zeichen besteht, welche mit bestimmten Operationen manipuliert werden können.

4.

5.

6.

7.

8.

Das Objekt "String" ist eine konkrete Instanz des Konzeptes String. Aus einer Klasse können beliebig viele Objekte instanziiert werden: Also zum Beispiel die Objekte "Hi!", "Macintosh", "Never hide...", "Et voila!" – dies sind alles unterschiedliche Objekte der gleichen Klasse String. 15 Thin THINK DIFFERENT i Think different Lionel Tristiano Cristiano Cristiano CristianoLionel o CRIS 4 • {0, 1, 2, 3, 4 } • {1, 2, 3, . . ., 99, 100 } • {100, 101, 102, . . ., 149, 150 } • {-5, -4, -3, -2, -1, 0, 1, 2, 3, 4} • {-3, -2, -1} • {11, 12, 13, . . ., 32, 33 } • rand.nextInt(101); • rand.nextInt(3) + 1; • rand.nextInt(6) + 5; • rand.nextInt(11) - 10; Zwei Variablen, die auf das gleiche Objekt zeigen, z.B. obj1 und obj2. Wird nun obj1 geändert, so wird obj2 gleichermassen verändert.

Lösungen zu den Aufgaben aus Kapitel 4

359

Lösungen zu den Aufgaben aus Kapitel 4 1. Mögliche Variablen und Methoden: Klasse BankAccount

Variablen Kontonummer, Besitzername, Gebühren, . . .

Kontostand, Zinsatz,

Methoden Einzahlen, Abheben, Hinzufügen, . . .

Zins

2. Instanzvariablen werden innerhalb einer Klasse, aber nicht innerhalb einer Methode deklariert. Auf Instanzvariablen kann man von allen Methoden der Klasse zugreifen. Lokale Variablen werden innerhalb einer Methode deklariert. Auf lokale Variablen kann nur in der entsprechenden Methode zugegriffen werden. Weiterer Unterschied: Tatsächlich erhalten Instanzvariablen primitiver Datentypen beim Instanziieren eines Objektes automatisch Initialwerte: Bei numerischen Variablen (z.B. int oder double) ist dieser Initialwert 0 bzw. 0.0, bei char das Leerzeichen, und bei boolean ist es false. Bei lokalen Variablen gibt es keine solchen automatischen Initialwerte. Die Programmiererin sollte aber sowieso in beiden Fällen immer explizit eine Initialisierung der Variablen vornehmen. 3. Da lokale Variablen nur innerhalb einer Methode “sichtbar” sind, muss man deren Sichtbarkeit gegenüber anderen Klassen nicht definieren. 4. Die Instanzvariablen der Klasse Dice sind MAX, points und randomGenerator. Die Methoden roll und setPoints können den Status des Würfels ändern (beide Methoden verändern möglicherweise die Variable points). 5. Die Variable points wird durch den Modifikator private gekapselt – ein direkter Zugriff auf die Variable points von ausserhalb der Klasse Dice ist somit unmöglich. Dies verhindert zum Beispiel eine unzulässige Änderung der Variablen points. Da MAX als Konstante deklariert ist (mit dem Modifikator final), kann diese Variable sowieso nicht geändert werden – die Sichtbarkeit public ist deshalb hier in Ordnung. 6. Service-Methoden bieten gewisse Funktionalitäten an, die auch von anderen Objekten/Klassen verwendet werden können. Support-Methoden hingegen können nur von Methoden der entsprechenden Klasse aufgerufen werden (diese Methoden unterstützen also die public Methoden bei der Erfüllung derer Funktionalitäten). 7. Zwei Möglichkeiten: public Dice() { this.randomGenerator = new Random(); this.points = this.randomGenerator.nextInt(MAX) + 1; }

Oder (eleganter):

360

Musterlösungen

public Dice() { this.randomGenerator = new Random(); this.roll(); }

8. Neue Methode (ggf. könte man die Magic Number 7 mit dem Ausdruck (this.MAX + 1) ersetzen): public int getPointsOpposite() { return 7 - this.points; }

9. Eine return Anweisung gibt den nachfolgenden Wert an die aufrufende Methode zurück. Z.B. gibt return 3.14; den Wert 3.14 zurück (nach dem return kann auch ein Bezeichner einer Variablen folgen). Beachten Sie: Der Wert, der mit return zurückgegeben wird, muss dem Datentypen entsprechen, der im Methodenkopf deklariert ist: public double returnDouble(){ In dieser Methode muss also eine return Anweisung stehen, welche eine Gleitkommazahl (double) zurückgibt. Das „leere“ return, also return; kann eingesetzt werden, wenn der Kontrollfluss zurück an die aufrufende Methode zurückgehen soll (in diesem Fall bedeutet return zurückkehren und nicht zurückgeben). 10. Die Bezeichner in den Klammern im Methodenkopf sind die formalen Parameter: public void method(double val1, int num1, String s1){ In diesem Beispiel haben wir drei formale Parameter val1, num1 und s1 (vom Typ double, int und String). Wird die Methode method aufgerufen, müssen ein double Wert, ein int Wert und ein String übergeben werden: method(3.17, 4, "Hallo");

Lösungen zu den Aufgaben aus Kapitel 4

361

Diese drei Werte (3.17, 4, "Hallo") entsprechen den tatsächlichen Parametern. Bei einem solchen Aufruf, werden die tatsächlichen Parameter in die formalen Parameter kopiert. Es geschieht also im Wesentlichen folgendes: double val1 = 3.17; int num1 = 4; String s1 = "Hallo"; Die drei Variablen val1, num1 und s1 haben jetzt also einen Wert zugewiesen bekommen und können nun innerhalb der Methode verwendet werden. 11. Es wird automatisch die Methode toString der entsprechenden Klasse aufgerufen – in dieser Methode kann man definieren, wie eine Zeichenkettenrepräsentation des Objektes aussehen soll. 12. Konstruktoren sind spezielle Methoden, welche aus einer Klasse Objekte erzeugen. Konstruktoren haben den gleichen Namen wie die Klasse und werden aufgerufen, wenn der new Operator verwendet wird: Dice dice = new Dice(); Konstruktoren sind in der Regel mit Sichtbarkeit public deklariert, erzeugen keine Rückgabe und besitzen demnach keinen Rückgabetyp (nicht zu verwechseln mit void). Der Zweck von Konstruktoren ist a) eine Referenz auf das instanziierte Objekt zurückzugeben und b) den Instanzvariablen eines Objektes initiale Werte zuzuweisen. Nachdem wir ein Objekt einer bestimmten Klasse instanziiert haben, können wir via Punktoperator auf alle public Methoden der entsprechenden Klasse zugreifen. Also z.B. dice.roll(); 13. Lösungen a. Es werden drei PlayerCard Objekte instanziiert. b. Indem die Methode setCardNumber auf einem bestimmten Objekt (z.B. pc1) ausgeführt wird. c. Es wird ein tatsächlicher Parameter an die Methode setCardNumber übergeben, wenn diese auf dem Objekt pc1 ausgeführt wird (nämlich 55). An die Methode getPlayerName werden keine Parameter übergeben.

362

Musterlösungen

Lösungen zu den Aufgaben aus Kapitel 5 1 Der Wurzelknoten entspricht in den meisten Fällen einem Behälter (z.B. einem Objekt vom Typ BorderPane). In diesem Behälter sind dann typischerweise weitere Behälter organisiert, die dann die Kontrollelemente des GUIs anordnen. Man kann also sagen, dass der Wurzelknoten das gesamte GUI enthält (organisiert in einem Szenengraphen). Das Programmfenster, das vom Betriebssystem zur Verfügung gestellt wird, entspricht der Bühne in JavaFX. Diese Bühne zeigt ein Objekt vom Typ Scene an (dieses wiederum enthält den Wurzelknoten). 2. Szenengraph:

VBox

Button

Label

ImageView

HBox

Button

Image four.jpg

TextField

3. Szenengraph (auf der nächsten Seite): 4. Eine BorderPane besitzt fünf Regionen eine AnchorPane nur vier. Einer Region in einer BorderPane kann nur ein Element hinzugefügt werden. In eine Region in einer AnchorPane können mehrere Elemente gleichzeitig hinzugefügt werden und ein Element kann auch gleichzeitig an zwei Regionen geankert werden (z.B. unten links). BorderPane center

bottom

FlowPane

HBox

Label

HBox

VBox

ToggleGroup

RadioButton

RadioButton

TextField

Button

5. HBox box = new HBox(); box.getChildren().add(new Button("klick"));

Lösungen zu den Aufgaben aus Kapitel 6

6.

7.

8. 9.

363

TextField input = new TextField(); box.getChildren().add(input); Tab tab1 = new Tab("Tab 1"); Tab tab2 = new Tab("Tab 2"); tab1.setContent(new VBox()); tab2.setContent(new VBox()); TabPane tabPane = new TabPane(); tabPane.getTabs().addAll(tab1, tab2); Lieblingssportart und Alter würde man mit einer Gruppe vom RadioButton Objekten umsetzen. Bei diesen beiden Auswahlen darf genau eine Möglichkeit ausgewählt werden (bei Zutaten und Sportarten sind Mehrfachauswahlen erlaubt – dies könnte man eher mit einer Gruppe von CheckBox Objekten umsetzen). ChoiceBox choiceBox = new ChoiceBox(); choiceBox.getItems().addAll("Eins", "Zwei", "Drei"); Eine Musterlösung für diese Aufgabe ist nicht sinnvoll.

Lösungen zu den Aufgaben aus Kapitel 6 1. Alle Methoden der Klasse Math sind statisch deklariert. Statische Methoden können aufgerufen werden, ohne dass man vorher ein Objekt aus der Klasse instanziieren muss. Ein Konstruktor ist deshalb nicht nötig. Statt der Objektvariablen verwendet man beim Methodenaufruf einfach den Klassennamen. Z.B. werden die statischen Methoden der Klasse Math mit dem Klassennamen und dem Punktoperator aufgerufen: Math.pow(5, 2);. 2. Resultate der Berechnungen: • • • • • •

125 9 2 5 4 5

3. double amount = 0.963; DecimalFormat fmt = new DecimalFormat("00.00%"); System.out.println(fmt.format(amount)); 4. System.out.println(Integer.MAX_VALUE); 5. Double doubleObject = new Double(3.456);

364

Musterlösungen

oder Double doubleObject = 3.456; 6. boolean on = Boolean.parseBoolean(input); 7. Da die Variable count niemals 10 wird, bleibt die Boolesche Bedingung immer wahr. 8. Ausgaben: • 1 2 • 3 3 • 1 1 9. Code-Fragment:

10. Code-Fragment:

Lösungen zu den Aufgaben aus Kapitel 7

365

11. Code-Fragment:

12. Ausgabe: [Emilie, Noelle, Maxime] 13. ArrayList dices = new ArrayList(); dices.add(new Dice()); Dice dice = new Dice(); dices.add(dice);

Lösungen zu den Aufgaben aus Kapitel 7 1. Ist ein default Fall programmiert, wird dieser Fall ausgeführt. Ansonsten wird die gesamte switch-Anweisung übersprungen. 2. Wenn dieser case abgearbeitet ist, springt der Kontrollfluss zum nachfolgenden case (falls einer vorhanden ist). Nur wenn eine break Anweisung in einem case vorhanden ist, wird nach der Abarbeitung des Falls die switch-Anweisung verlassen. 3. Äquivalente switch-Anweisung: switch (num) { case 1 : c = ’A’; break; case 2 : c = ’B’; break; case 3 : c = ’C’; break; default : c = ’Z’; }

4. System.out.println((val