0% fanden dieses Dokument nützlich (0 Abstimmungen)
126 Ansichten971 Seiten

Java 17

Hochgeladen von

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

Java 17

Hochgeladen von

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

ZIMK - Zentrum für

Informations-, Medien- und


Kommunikationstechnologie

Bernhard Baltes-Götz & Johannes Götz

Einführung in das
Programmieren mit
Java 17
Die aktuelle, um ca. 200 Seiten erweiterte
Version dieses Manuskripts (inklusive Daten-
bankprogrammierung mit JDBC und JPA)
wird hier angeboten:

https://bebagoe.de/java/

2022 (Rev. 220607)


Herausgeber: Zentrum für Informations-, Medien- und Kommunikationstechnologie (ZIMK)
an der Universität Trier
Universitätsring 15
D-54286 Trier
WWW: zimk.uni-trier.de
E-Mail: [email protected]
Autoren: Bernhard Baltes-Götz & Johannes Götz
E-Mail: [email protected]
Copyright © 2022; ZIMK
Vorwort
Dieses Manuskript basiert auf der Begleitlektüre zum Java-Einführungskurs, den das Zentrum für
Informations-, Medien- und Kommunikationstechnologie (ZIMK) an der Universität Trier im Win-
tersemester 2021/2022 angeboten hat, ist aber auch für das Selbststudium geeignet.

Inhalte und Lernziele


Die von der Firma Sun Microsystems (mittlerweile von der Firma Oracle übernommen) entwickelte
und 1995 veröffentlichte Programmiersprache Java hat sich als universelle, für vielfältige Zwecke
einsetzbare Sprache etabliert und kann insbesondere als attraktivste Lösung für die plattformunab-
hängige Entwicklung gelten. Java gehört zur ersten Liga der objektorientierten Programmierspra-
chen, und das objektorientierte Paradigma der Software-Entwicklung hat sich praktisch in der ge-
samten Branche als Standard etabliert.
Die Entscheidung der Firma Sun, Java beginnend mit der Version 6 als Open Source unter die GPL
(General Public License) zu stellen, ist in der Entwicklerszene positiv aufgenommen worden und
trägt zum anhaltenden Erfolg der Programmiersprache bei.
Allerdings steht Java nicht ohne Konkurrenz da. Nach dem fehlgeschlagenen Versuch, Java unter
der Bezeichnung J++ als Windows-Programmiersprache zu etablieren, hat die Firma Microsoft
mittlerweile mit der Programmiersprache C# für die .NET-Plattform ein ebenbürtiges Gegenstück
geschaffen (siehe z. B. Baltes-Götz 2021). Beide Konkurrenten inspirieren sich gegenseitig und
treiben so den Fortschritt voran. An diesem Fortschritt sind aber noch viele andere Programmier-
sprachen beteiligt. Obwohl immer wieder neue Sprachen auf den Markt drängen, sind die härtesten
Java-Konkurrenten ebenso alt oder sogar noch älter (C/C++ und Python).
Außerdem sind mittlerweile neben Java etliche weitere Sprachen zur Entwicklung von Programmen
für die Java-Laufzeitumgebung entstanden (z. B. Clojure, Groovy, JRuby, Jython, Kotlin, Scala).
Sie bieten dieselbe Plattformunabhängigkeit wie Java und können teilweise alternative Program-
miertechniken wie das funktionale Programmieren (früher) unterstützen, weil sie nicht zur Ab-
wärtskompatibilität verpflichtet sind. Diese Vielfalt (vergleichbar mit der Wahlfreiheit von Pro-
grammiersprachen für die .NET - Plattform) ist grundsätzlich zu begrüßen. Allerdings ist Java für
allgemeine Einsatzzwecke nicht zuletzt wegen der großen Verbreitung und Unterstützung weiterhin
zu bevorzugen. Nachhaltig relevante Programmiertechniken sind früher oder später auch in Java
verfügbar. So ist z. B. das funktionale Programmieren seit der Version 8 auch in Java möglich.
Das Manuskript beschränkt sich auf die Java Standard Edition (JSE) zur Entwicklung von Anwen-
dersoftware für Arbeitsplatzrechner, auf die viele weltweit populäre Softwarepakete setzen (z. B.
IBM SPSS Statistics, Matlab). Daneben gibt es sehr erfolgreiche Java-Editionen bzw. - Frameworks
für unternehmensweite oder serverorientierte Lösungen. Neben der Java Enterprise Edition (JEE),
die jüngst von der Firma Oracle an die Open Source Community (vertreten durch die Eclipse Foun-
dation) übergeben wurde, ist hier vor allem das Spring-Framework zu erwähnen. Eher auf dem
Rückzug ist die Java Micro Edition (JME) für Kommunikationsgeräte mit beschränkter Leistung.
Moderne Smartphones und Tablets zählen nicht mehr zu den Geräten mit beschränkter Leistung.
Sofern diese Geräte das Betriebssystem Android benutzen, kommt auch hier zur Software-
Entwicklung sehr oft die Programmiersprache Java zum Einsatz (siehe z. B. Baltes-Götz 2018). Im
Sommersemester 2022 bietet das ZIMK einen Kurs zur Android-Programmierung mit Java an.
iv Vorwort

Im Java-Kurs geht es nicht um Kochrezepte zur schnellen Erstellung effektvoller Programme, son-
dern um die systematische Einführung in das Programmieren. Dabei werden wichtige Konzepte und
Methoden der objektorientierte Software-Entwicklung vorgestellt.

Voraussetzungen bei den Teilnehmenden1


• Programmierkenntnisse
Programmierkenntnisse werden nicht vorausgesetzt. Leser mit Programmiererfahrung wer-
den sich bei den ersten Kapiteln eventuell etwas langweilen.
• EDV-Plattform
Im Manuskript wird zur Demonstration ein PC unter Microsoft Windows 10 verwendet. Al-
lerdings sind alle verwendeten Programme und Bibliotheken auch für Linux und macOS
verfügbar.

Software zum Üben


Für die unverzichtbaren Übungen verwenden wir das Java SE Development Kit in den Versionen 8
und 17 sowie die Entwicklungsumgebung IntelliJ IDEA der Firma JetBrains in der Community Edi-
tion 2021.x. Die genannte Software ist kostenlos für alle signifikanten Plattformen (z. B. Linux,
macOS, Windows) im Internet verfügbar. Nähere Hinweise zum Bezug, zur Installation und zur
Verwendung folgen im Manuskript.

Aktuelles Manuskript und Dateien zum Kurs


Die aktuelle Version dieses Manuskripts sowie IntelliJ-Projekte mit den Beispielprogrammen bzw.
mit Lösungsvorschlägen zu den Übungsaufgaben sind auf dem Webserver der Universität Trier hier
zu finden:
https://www.uni-trier.de/index.php?id=22787
Leider blieb zu wenig Zeit für eine sorgfältige Kontrolle des Textes, sodass einige Fehler und Män-
gel verblieben sein dürften. Entsprechende Hinweise an die Mail-Adresse
[email protected]
werden dankbar entgegengenommen.2

Trier und Bruchsal, im Juni 2022 Bernhard Baltes-Götz und Johannes Götz

1
Zur Vermeidung von sprachlichen Umständlichkeiten wird in diesem Manuskript meist die männliche Form ver-
wendet. Die „Teilnehmenden“ sind stilistisch durchaus akzeptabel. Im nächsten Satz stünden aber die umständliche
Formulierung „Leser und Leserinnen“ oder die zumindest ungewohnte Formulierung „Lesende“ zur Wahl. Trotz
großer Sympathie für das Ziel einer geschlechtsneutralen Sprache scheint uns gegenwärtig die männliche Form das
kleinere Übel zu sein.
2
Für zahlreiche Hinweise auf mittlerweile behobene Fehler möchten wir uns bei Paul Frischknecht, Andreas
Hanemann, Peter Krumm, Michael Lehnen, Lukas Nießen, Rolf Schwung und Jens Weber herzlich bedanken.
Inhaltsverzeichnis

VORWORT III

1 EINLEITUNG 1
1.1 Beispiel für die objektorientierte Software-Entwicklung mit Java 1
1.1.1 Objektorientierte Analyse und Modellierung 1
1.1.2 Objektorientierte Programmierung 7
1.1.3 Algorithmen 8
1.1.4 Startklasse und main() - Methode 10
1.1.5 Zusammenfassung zum Abschnitt 1.1 12

1.2 Java-Programme ausführen 13


1.2.1 Java-Laufzeitumgebung installieren 13
1.2.2 Konsolenprogramme ausführen 17
1.2.3 Ausblick auf Anwendungen mit grafischer Bedienoberfläche 19
1.2.4 Ausführung auf einer beliebigen unterstützten Plattform 22

1.3 Architektur und Eigenschaften von Java-Software 22


1.3.1 Herkunft und Bedeutung der Programmiersprache Java 22
1.3.2 Quellcode, Bytecode und Maschinencode 23
1.3.3 Standardklassenbibliothek 26
1.3.4 Java-Editionen für verschiede Einsatzszenarien 27
1.3.5 Update- und Lizenzpolitik der Firma Oracle 29
1.3.6 Eigenschaften von Java-Software 29
1.3.6.1 Objektorientierung mit funktionalen Erweiterungen 29
1.3.6.2 Portabilität 30
1.3.6.3 Sicherheit 30
1.3.6.4 Robustheit 31
1.3.6.5 Einfachheit 32
1.3.6.6 Multithreading 32
1.3.6.7 Performanz 33
1.3.6.8 Beschränkungen 33

1.4 Übungsaufgaben zum Kapitel 1 33

2 WERKZEUGE ZUM ENTWICKELN VON JAVA-PROGRAMMEN 35


2.1 Aktuelles OpenJDK installieren 35
2.1.1 OpenJDK 17 der Firma Oracle 36
2.1.2 OpenJDK-Distributionen mit langfristiger Update-Versorgung 37

2.2 Java-Entwicklung mit dem JDK und einem Texteditor 37


2.2.1 Editieren 37
2.2.2 Übersetzen 40
2.2.3 Ausführen 42
2.2.4 Suchpfad für class-Dateien 43
2.2.5 Programmfehler beheben 45

2.3 IntelliJ IDEA Community installieren 46

2.4 Java-Entwicklung mit IntelliJ IDEA 50


2.4.1 Erster Start 50
2.4.2 Projekt anlegen 52
vi Inhaltsverzeichnis

2.4.3 Quellcode-Editor 55
2.4.3.1 Syntaxerweiterung 55
2.4.3.2 Code-Inspektion und Quick-Fixes 57
2.4.3.3 Live Templates 58
2.4.3.4 Orientierungshilfen 59
2.4.3.5 Refaktorieren 61
2.4.3.6 Sonstige Hinweise 61
2.4.4 Übersetzen und Ausführen 61
2.4.5 Sichern und Wiederherstellen 63
2.4.6 Konfiguration 64
2.4.6.1 SDKs einrichten 64
2.4.6.2 Struktur des aktuellen Projekts 66
2.4.6.3 Einstellungen für IntelliJ oder das aktuelle Projekt 67
2.4.6.4 Einstellungen für neue Projekte 70
2.4.7 Übungsprojekte zum Kurs verwenden 71

2.5 OpenJFX und Scene Builder installieren 71

2.6 Übungsaufgaben zum Kapitel 2 74

3 ELEMENTARE SPRACHELEMENTE 77
3.1 Einstieg 77
3.1.1 Aufbau eines Java-Programms 77
3.1.2 Projektrahmen zum Üben von elementaren Sprachelementen 78
3.1.3 Syntaxdiagramme 80
3.1.3.1 Klassendefinition 82
3.1.3.2 Methodendefinition 83
3.1.4 Hinweise zur Gestaltung des Quellcodes 84
3.1.5 Kommentare 86
3.1.5.1 Zeilenrestkommentar 86
3.1.5.2 Mehrzeilenkommentar 86
3.1.5.3 Dokumentationskommentar 87
3.1.6 Namen 89
3.1.7 Vollständige Klassennamen und Import-Deklaration 90

3.2 Ausgabe bei Konsolenanwendungen 91


3.2.1 Ausgabe einer (zusammengesetzten) Zeichenfolge 91
3.2.2 Formatierte Ausgabe 92

3.3 Variablen und Datentypen 94


3.3.1 Strenge Compiler-Überwachung bei Java-Variablen 95
3.3.2 Variablennamen 97
3.3.3 Primitive Datentypen und Referenztypen 98
3.3.4 Klassifikation der Variablen nach Zuordnung 100
3.3.5 Eigenschaften einer Variablen 101
3.3.6 Primitive Datentypen in Java 102
3.3.7 Darstellung von Gleitkommazahlen im Arbeitsspeicher 104
3.3.7.1 Binäre Gleitkommadarstellung 104
3.3.7.2 Dezimale Gleitkommadarstellung 106
3.3.8 Variablendeklaration, Initialisierung und Wertzuweisung 108
3.3.9 Blöcke und Sichtbarkeitsbereiche für lokale Variablen 111
3.3.10 Finalisierte lokale Variablen 113
3.3.11 Literale 114
3.3.11.1 Ganzzahlliterale 114
3.3.11.2 Gleitkommaliterale 116
3.3.11.3 boolean-Literale 117
3.3.11.4 char-Literale 117
3.3.11.5 Zeichenfolgenliterale 118
3.3.11.6 Referenzliteral null 119
Inhaltsverzeichnis vii

3.4 Eingabe bei Konsolenprogrammen 120


3.4.1 Die Klassen Scanner und Simput 120
3.4.2 Eine globale Bibliothek mit der Klasse Simput in IntelliJ einrichten 123

3.5 Operatoren und Ausdrücke 126


3.5.1 Arithmetische Operatoren 128
3.5.2 Methodenaufrufe 130
3.5.3 Vergleichsoperatoren 131
3.5.4 Identitätsprüfung bei Gleitkommawerten 133
3.5.5 Logische Operatoren 136
3.5.6 Bitorientierte Operatoren 138
3.5.7 Typumwandlung (Casting) bei primitiven Datentypen 139
3.5.7.1 Automatische erweiternde Typanpassung 139
3.5.7.2 Explizite Typumwandlung 140
3.5.8 Zuweisungsoperatoren 141
3.5.9 Konditionaloperator 144
3.5.10 Auswertungsreihenfolge 145
3.5.10.1 Regeln 145
3.5.10.2 Operatorentabelle 148

3.6 Über- und Unterlauf bei numerischen Variablen 149


3.6.1 Überlauf bei Ganzzahltypen 150
3.6.2 Unendliche und undefinierte Werte bei den Typen float und double 152
3.6.3 Unterlauf bei den Gleitkommatypen 154
3.6.4 Modifikator strictfp 155

3.7 Anweisungen (zur Ablaufsteuerung) 156


3.7.1 Überblick 156
3.7.2 Bedingte Anweisung und Fallunterscheidung 157
3.7.2.1 if-Anweisung 157
3.7.2.2 if-else - Anweisung 158
3.7.2.3 switch-Anweisung 163
3.7.2.4 switch-Ausdruck (ab Java 14) 169
3.7.2.5 Mustervergleich (Vorschau in Java 17) 171
3.7.3 Wiederholungsanweisung 175
3.7.3.1 Zählergesteuerte Schleife (for) 177
3.7.3.2 Iterieren über die Elemente von Arrays oder Kollektionen 178
3.7.3.3 Bedingungsabhängige Schleifen 179
3.7.3.4 Endlosschleifen 181
3.7.3.5 Schleifen(durchgänge) vorzeitig beenden 182

3.8 Entspannungs- und Motivationseinschub: GUI-Standarddialoge 184

3.9 Übungsaufgaben zum Kapitel 3 188


Abschnitt 3.1 (Einstieg) 188
Abschnitt 3.2 (Ausgabe bei Konsolenanwendungen) 188
Abschnitt 3.3 (Variablen und Datentypen) 189
Abschnitt 3.4 (Eingabe bei Konsolen) 190
Abschnitt 3.5 (Operatoren und Ausdrücke) 190
Abschnitt 3.6 (Über- und Unterlauf bei numerischen Variablen) 191
Abschnitt 3.7 (Anweisungen (zur Ablaufsteuerung)) 192

4 KLASSEN UND OBJEKTE 195


4.1 Überblick, historische Wurzeln, Beispiel 196
4.1.1 Einige Kernideen und Vorzüge der OOP 196
4.1.1.1 Datenkapselung und Modularisierung 196
4.1.1.2 Vererbung 198
4.1.1.3 Polymorphie 200
4.1.1.4 Realitätsnahe Modellierung 201
4.1.2 Strukturierte Programmierung und OOP 201
4.1.3 Auf-Bruch zu echter Klasse 202
viii Inhaltsverzeichnis

4.2 Instanzvariablen (Felder) 206


4.2.1 Sichtbarkeitsbereich, Existenz und Ablage im Hauptspeicher 206
4.2.2 Deklaration mit Modifikatoren für den Zugriffsschutz und für andere Zwecke 207
4.2.3 Automatische Initialisierung auf den Voreinstellungswert 210
4.2.4 Verwendung in klasseneigenen und fremden Methoden 210
4.2.5 Finalisierte Instanzvariablen 212

4.3 Instanzmethoden 213


4.3.1 Methodendefinition 214
4.3.1.1 Modifikatoren 215
4.3.1.2 Rückgabewert und return-Anweisung 216
4.3.1.3 Namen 217
4.3.1.4 Formalparameter 217
4.3.1.5 Methodenrumpf 222
4.3.2 Methodenaufruf und Aktualparameter 222
4.3.3 Debug-Einsichten zu (verschachtelten) Methodenaufrufen 225
4.3.4 Methoden überladen 230

4.4 Objekte 232


4.4.1 Referenzvariablen deklarieren 232
4.4.2 Objekte erzeugen 233
4.4.3 Konstruktoren 235
4.4.4 Instanzinitialisierer 238
4.4.5 Objekte aus der Fabrik 239
4.4.6 Objektreferenzen verwenden 239
4.4.6.1 Rückgabe mit Referenztyp 240
4.4.6.2 this als Referenz auf das aktuelle Objekt 240
4.4.7 Abräumen überflüssiger Objekte durch den Garbage Collector 240
4.4.8 finalize() und Cleaner 242

4.5 Klassenvariablen und -methoden 244


4.5.1 Klassenvariablen 244
4.5.2 Wiederholung zur Kategorisierung von Variablen 246
4.5.3 Klassenmethoden 247
4.5.4 Statische Initialisierer 248

4.6 Rekursive Methoden 250

4.7 Komposition 252

4.8 Mitgliedsklassen und lokale Klassen 254


4.8.1 Mitgliedsklassen 255
4.8.1.1 Innere Klassen 255
4.8.1.2 Statische Mitgliedsklassen 258
4.8.2 Lokale Klassen 259

4.9 Bruchrechnungsprogramm mit JavaFX-GUI 261


4.9.1 JavaFX-Projekt mit dem OpenJDK 8 anlegen 261
4.9.2 Bedienoberfläche bzw. FXML-Datei mit dem Scene Builder gestalten 265
4.9.3 Klasse Bruch einbinden 271
4.9.4 Controller-Klasse vervollständigen 272
4.9.5 Programmstart 276

4.10 Übungsaufgaben zum Kapitel 4 277

5 WICHTIGE SPEZIELLE KLASSEN 283


5.1 Arrays 283
5.1.1 Array-Variablen deklarieren 284
5.1.2 Array-Objekte erzeugen 285
5.1.3 Kovariante Einbindung von Arrays in die Klassenhierarchie 286
5.1.4 Arrays verwenden 287
Inhaltsverzeichnis ix

5.1.5 Array-Kopien mit neuer Länge erstellen 288


5.1.6 Nützliche Methoden in der Klasse Arrays 288
5.1.7 Beispiel: Beurteilung des Java-Pseudozufallszahlengenerators 288
5.1.8 Initialisierungslisten 290
5.1.9 Objekte als Array-Elemente 291
5.1.10 Mehrdimensionale Arrays 291

5.2 Klassen für Zeichenfolgen 293


5.2.1 Die Klasse String für konstante Zeichenfolgen 294
5.2.1.1 Erzeugen von String-Objekten 294
5.2.1.2 String als WORM - Klasse 295
5.2.1.3 Interner String-Pool und Identitätsvergleich 295
5.2.1.4 Methoden für String-Objekte 297
5.2.1.5 Aufwand beim Inhalts- bzw. Referenzvergleich 301
5.2.2 Die Klassen StringBuilder und StringBuffer für veränderliche Zeichenfolgen 303
5.2.3 Mehrzeilige Textblöcke 305

5.3 Verpackungsklassen für primitive Datentypen 307


5.3.1 Wrapper-Objekte erstellen 307
5.3.2 Auto(un)boxing 308
5.3.3 Empfehlungen zur Verwendung von Verpackungsklassen 309
5.3.3.1 Identitätsoperator vermeiden 309
5.3.3.2 Wrapper-Objekte nicht für variable Werte verwenden 311
5.3.4 Konvertierungsmethoden 311
5.3.5 Konstanten für Grenz- bzw. Spezialwerte 312
5.3.6 Character-Methoden zur Zeichen-Klassifikation 313

5.4 Aufzählungstypen 313


5.4.1 Einfache Enumerationstypen 314
5.4.2 Erweiterte Enumerationstypen 316
5.4.3 Innere und lokale Aufzählungstypen 317

5.5 Records 319


5.5.1 Einfach Datenklassendefinition 319
5.5.2 Optionen und Einschränkungen bei der Definition von Record-Klassen 322
5.5.2.1 Vererbung 322
5.5.2.2 Expliziter Konstruktor 322
5.5.2.3 Überschreiben der automatisch erstellten Methoden 323
5.5.2.4 Zusätzliche Record-Member 323
5.5.2.5 Innere und lokale Record-Klassen 324

5.6 Übungsaufgaben zum Kapitel 5 325


Abschnitt 5.1 (Arrays) 325
Abschnitt 5.2 (Klassen für Zeichen) 327
Abschnitt 5.3 (Verpackungsklassen für primitive Datentypen) 329
Abschnitt 5.4 (Aufzählungstypen) 330
Abschnitt 5.5 (Records) 330

6 PAKETE UND MODULE 331


6.1 Pakete 333
6.1.1 Pakete erstellen 333
6.1.1.1 package-Deklaration und Paketordner 333
6.1.1.2 Standardpaket 335
6.1.1.3 Unterpakete 335
6.1.1.4 Paketunterstützung in IntelliJ 337
6.1.1.5 Regeln und Konventionen für Paketnamen 339
6.1.2 Pakete verwenden 341
6.1.2.1 Verfügbarkeit der class-Dateien (Klassenpfad) 341
6.1.2.2 Typen aus fremden Paketen ansprechen 343
6.1.2.3 Startklasse in einem benannten Paket 345
x Inhaltsverzeichnis

6.1.3 Traditionelle jar-Dateien (Java 8) 347


6.1.3.1 Eigenschaften von Java-Archivdateien 348
6.1.3.2 Archivdateien mit dem JDK-Werkzeug jar erstellen 349
6.1.3.3 Archivdateien verwenden 350
6.1.3.4 Ausführbare jar-Dateien 351
6.1.3.5 Archivunterstützung in IntelliJ 353
6.1.3.6 Ausführbare jar-Datei für ein Projekt mit OpenJFX 8 354

6.2 Module 358


6.2.1 Moduldeklarationsdatei module-info.java 360
6.2.1.1 Modulnamen 360
6.2.1.2 requires-Deklaration 360
6.2.1.3 exports-Deklaration 362
6.2.1.4 uses- und provides-Deklaration 363
6.2.1.5 opens-Deklaration 364
6.2.2 Quellcode-Organisation 364
6.2.3 Übersetzung in ein explodiertes Modul und den Moduldeskriptor 367
6.2.4 Modulpfad 368
6.2.5 Ausführen 369
6.2.6 Modulare jar-Dateien 370
6.2.7 Unterstützung für Java-Module in IntelliJ IDEA 373
6.2.7.1 Modul de.uni_trier.zimk.util 374
6.2.7.2 Modul de.uni_trier.zimk.matrain 377
6.2.7.3 Hauptmodul de.uni_trier.zimk.ba 379
6.2.8 Eigenständige Anwendungen mit maßgeschneiderter Laufzeitumgebung 382
6.2.9 Kompatibilität und Migration 383
6.2.9.1 Automatische Module 384
6.2.9.2 Das unbenannte Modul 384
6.2.9.3 Notlösung 385
6.2.9.4 Moduldeklaration zu vorhandenem Quellcode erstellen 385
6.2.10 Das modulare API der Java Standard Edition 386
6.2.11 Modul-Taxonomie 388

6.3 Zugriffsschutz 388


6.3.1 Sichtbarkeit von Top-Level - Typen 389
6.3.2 Sichtbarkeit von Typmitgliedern 390

6.4 Übungsaufgaben zum Kapitel 6 392

7 VERERBUNG UND POLYMORPHIE 393


7.1 Definition einer abgeleiteten Klasse 395

7.2 Der Zugriffsmodifikator protected 396

7.3 Basisklassenkonstruktoren und Initialisierungsmaßnahmen 397

7.4 Überschreiben und Überdecken 398


7.4.1 Überschreiben von Instanzmethoden 398
7.4.2 Überdecken von statischen Methoden 401
7.4.3 Finalisierte Methoden 402
7.4.4 Felder überdecken 403

7.5 Verwaltung von Objekten über Basisklassenreferenzen 404

7.6 Der instanceof-Operator 405

7.7 Polymorphie 406

7.8 Abstrakte Methoden und Klassen 407


Inhaltsverzeichnis xi

7.9 Das Liskovsche Substitutionsprinzip 409

7.10 Unerwünschte Abhängigkeiten durch Vererbung 410


7.10.1 Risiken für abgeleitete Klassen 410
7.10.2 Nachteile für potentielle Basisklassen 411

7.11 Erzwungene und optionale Einschränkungen beim Vererben 412


7.11.1 Keine Mehrfachvererbung 412
7.11.2 Finale Klassen 412
7.11.3 Versiegelte Klassen 413

7.12 Übungsaufgaben zum Kapitel 7 415

8 GENERISCHE KLASSEN UND METHODEN 417


8.1 Generische Klassen 417
8.1.1 Vorzüge und Verwendung generischer Klassen 417
8.1.1.1 Veraltete Technik mit Risiken und Umständlichkeiten 417
8.1.1.2 Generische Klassen bringen Typsicherheit und Bequemlichkeit 419
8.1.2 Technische Details und Komplikationen 420
8.1.2.1 Typlöschung und Rohtyp 420
8.1.2.2 Spezialisierungsbeziehungen bei parametrisierten Klassen und bei Arrays 422
8.1.2.3 Keine Array-Kreation mit einem nicht-reifizierbaren Elementtyp 424
8.1.2.4 Serienparameter mit einem parametrisierten Typ 425
8.1.3 Definition von generischen Klassen 427
8.1.3.1 Unbeschränkte Typformalparameter 427
8.1.3.2 Beschränkte Typformalparameter 432

8.2 Generische Methoden 435

8.3 Wildcard-Datentypen 438


8.3.1 Beschränkte Wildcard-Typen 439
8.3.1.1 Beschränkung nach oben 439
8.3.1.2 Beschränkung nach unten 440
8.3.2 Unbeschränkte Wildcard-Typen 441
8.3.3 Verwendungszwecke für Wildcard-Datentypen 441

8.4 Einschränkungen der Generizitätslösung in Java 442


8.4.1 Konkretisierung von Typformalparametern nur durch Referenztypen 442
8.4.2 Typlöschung und die Folgen 442
8.4.2.1 Keine Typparameter bei der Definition von statischen Mitgliedern 442
8.4.2.2 Member aus einer per Typparameter bestimmten Klasse sind verboten 443
8.4.2.3 Member aus einer per Typparameter konkretisierten generischen Klasse sind erlaubt 444

8.5 Übungsaufgaben zum Kapitel 8 445

9 INTERFACES 447
9.1 Überblick 447
9.1.1 Beispiel 447
9.1.2 Primäre Funktion 448
9.1.3 Mögliche Bestandteile 450

9.2 Interfaces definieren 450


9.2.1 Kopf einer Schnittstellen-Definition 452
9.2.2 Vererbung bzw. Erweiterung bei Schnittstellen 452
9.2.3 Schnittstellen-Methoden 453
9.2.3.1 Abstrakte Instanzmethoden 453
9.2.3.2 Instanzmethoden mit default-Implementierung 453
9.2.3.3 Statische Methoden 456
9.2.3.4 Private Interface-Methoden 457
xii Inhaltsverzeichnis

9.2.4 Konstanten 458


9.2.5 Statische Mitgliedstypen 459
9.2.6 Lokale Schnittstellen 460
9.2.7 Zugriffsschutz bei Mitgliedern von Schnittstellen 460
9.2.8 Marker - Interfaces 460

9.3 Interfaces implementieren 460


9.3.1 Mehrere Schnittstellen implementieren 462
9.3.2 Geerbte Interface-Implementationen 463
9.3.3 Implementieren von Schnittstellen und Sichtbarkeit von Klassen 465

9.4 Interfaces als Referenzdatentypen verwenden 466

9.5 Versiegelte Interfaces 467

9.6 Annotationen 468


9.6.1 Definition 469
9.6.2 Zuweisung 471
9.6.3 Runtime-Annotationen per Reflexion auswerten 471
9.6.4 API-Annotationen 472
9.6.5 Annotationen bei Record-Klassen 475

9.7 Übungsaufgaben zum Kapitel 9 477

10 JAVA COLLECTIONS FRAMEWORK 479


10.1 Arrays versus Kollektionen 479

10.2 Zur Rolle von Schnittstellen beim JCF-Design 481

10.3 Das Interface Collection<E> 483


10.3.1 Basiskompetenzen einer Kollektion 483
10.3.2 Optionale Operationen 486
10.3.3 Anforderungen an den Elementtyp 486

10.4 Listen 487


10.4.1 Das Interface List<E> 488
10.4.2 Beispiel 490
10.4.3 Listenarchitekturen 492
10.4.3.1 Array als Hintergrundspeicher 492
10.4.3.2 Verkette Objekte 493
10.4.4 Leistungsunterschiede und Einsatzempfehlungen 494

10.5 Iteratoren 496

10.6 Mengen 498


10.6.1 Das Interface Set<E> 499
10.6.2 Leistungsvorteil bei der Existenzprüfung 501
10.6.3 Hashtabellen 501
10.6.4 Balancierte Binärbäume 503
10.6.5 Interfaces für geordnete Mengen 506

10.7 Abbildungen 510


10.7.1 Das Interface Map<K,V> 511
10.7.2 Die Klasse HashMap<K,V> 516
10.7.3 Interfaces für Abbildungen mit geordneten Schlüsseltypen 518
10.7.4 Die Klasse TreeMap<K,V> 520

10.8 Vergleich der Kollektionsarchitekturen 521

10.9 Warteschlangen 522


Inhaltsverzeichnis xiii

10.10 Nützliche Methoden in der Klasse Collections 523

10.11 Übungsaufgaben zum Kapitel 10 525

11 AUSNAHMEBEHANDLUNG 529
11.1 Prävention und Beispielprogramm 530

11.2 Unbehandelte Ausnahmen 532

11.3 Ausnahmen abfangen 533


11.3.1 Die try-catch-finally - Anweisung 533
11.3.1.1 Ausnahmebehandlung per catch-Block 534
11.3.1.2 Aufräumarbeiten im finally-Block 537
11.3.2 Programmablauf bei der Ausnahmebehandlung 539
11.3.3 Diagnostische Ausgaben 541

11.4 Ausnahmeobjekte im Vergleich zur Fehlerkommunikation per Rückgabe 543


11.4.1 Traditionelle Rückgabewerte 543
11.4.2 Rückgabetyp Optional<T> 546

11.5 Ausnahmen und Fehler 547


11.5.1 Error 548
11.5.2 Geprüfte und ungeprüfte Ausnahmen 551
11.5.2.1 Unterschiedliche Behandlung durch den Compiler 551
11.5.2.2 Eine schwierige Unterscheidung 553

11.6 Ausnahmen in einer eigenen Methode werfen und ankündigen 554


11.6.1 Ausnahmen auslösen (throw), ankündigen (throws) und dokumentieren 554
11.6.2 Compiler-Intelligenz beim erneuten Werfen von abgefangenen Ausnahmen 557

11.7 Ausnahmen bei der Parameter-Validierung 558

11.8 Vollständige Beschreibung der try-catch-finally - Ausführung 559

11.9 Ausnahmen definieren 561

11.10 Freigabe von Ressourcen 563


11.10.1 Traditionelle Lösung per finally-Block 563
11.10.2 Try With Resources 565

11.11 Übungsaufgaben zum Kapitel 11 566

12 FUNKTIONALES PROGRAMMIEREN 569


12.1 Lambda-Ausdrücke 569
12.1.1 Traditionelle und moderne Realisation von Funktionsobjekten 570
12.1.1.1 Funktionale Schnittstellen 570
12.1.1.2 Anonyme Klassen 572
12.1.1.3 Compiler-Kompetenz statt Boilerplate-Code 576
12.1.2 Definition von Lambda-Ausdrücken 578
12.1.2.1 Formalparameterliste 578
12.1.2.2 Rumpf 579
12.1.2.3 Definitionsumgebungen 580
12.1.3 Methoden- und Konstruktorreferenzen 582
12.1.3.1 Methodenreferenzen 582
12.1.3.2 Konstruktorreferenzen 584

12.2 Ströme 585


12.2.1 Elementare Begriffe und Beispiel 585
12.2.2 Externe versus interne Iteration 587
xiv Inhaltsverzeichnis

12.2.3 Eigenschaften von Strömen 589


12.2.3.1 Datentyp der Elemente 589
12.2.3.2 Sequentiell oder parallel 589
12.2.4 Erstellung von Stromobjekten 589
12.2.4.1 Stromobjekt aus einer Kollektion (stream, parallelStream) 589
12.2.4.2 Stromobjekt aus einem Array oder aus einer Serie von Werten (stream, of) 590
12.2.4.3 Stromobjekte mit einer Sequenz ganzer Zahlen (range, rangeClosed) 590
12.2.4.4 Unendliche Ströme (iterate, generate) 590
12.2.4.5 Sonstige Erstellungsmethoden 591
12.2.5 Stromoperationen 592
12.2.5.1 Intermediäre und terminale Stromoperationen 592
12.2.5.2 Faulheit ist nicht immer dumm 593
12.2.5.3 Intermediäre Operationen 594
12.2.5.4 Terminale Operationen 599

12.3 Empfehlungen für erfolgreiches funktionales Programmieren 608


12.3.1.1 Deklarieren statt Kommandieren 608
12.3.1.2 Veränderliche Variablen vermeiden 609
12.3.1.3 Seiteneffekte vermeiden 610
12.3.1.4 Ausdrücke bevorzugen gegenüber Anweisungen 610
12.3.1.5 Verwendung von Funktionen höherer Ordnung 610

12.4 Übungsaufgaben zum Kapitel 12 611

13 GUI-PROGRAMMIERUNG MIT JAVAFX 613


13.1 Einordnung 613
13.1.1 Vergleich von Konsolen- und GUI-Programmen 613
13.1.2 Desktop-GUI-Lösungen in Java 615

13.2 Einstieg in JavaFX 617


13.2.1 Beispiel Anwesenheitsliste 617
13.2.2 Lebenszyklus einer JavaFX-Anwendung 621
13.2.3 Bühne, Szene und Szenengraph 624

13.3 Anwendung mit All-In-One - Architektur 625

13.4 Anwendung mit Model-View-Controller - Architektur (MVC) 630


13.4.1 Das Model-View-Controller - Konzept 630
13.4.2 Projekt anlegen 631
13.4.3 Model 632
13.4.4 GUI-Gestaltung per Scene Builder 633
13.4.5 FXML 635
13.4.6 Controller 637
13.4.7 Anwendungsklasse 640

13.5 Properties mit Änderungssignalisierung und automatischer Synchronisation 642


13.5.1 Basiswissen über Properties 642
13.5.1.1 Traditionelle JavaBean-Eigenschaften 642
13.5.1.2 Property-Klassen von JavaFX 643
13.5.1.3 Vermeidung von überflüssigen Objektkreationen 646
13.5.2 Invalidierungs- und Veränderungmitteilungen 647
13.5.3 Automatische Synchronisation von Property-Objekten 648
13.5.3.1 Uni- und bidirektionale Synchronisation von Property-Objekten 649
13.5.3.2 Property-Objekt an ein Berechnungsergebnis binden 650
13.5.3.3 Beobachtbare Listen 654

13.6 Ereignisse 658


13.6.1 Ereignishierarchie 658
13.6.2 Ereignisverarbeitung 660
13.6.2.1 Top-Down - Route 660
13.6.2.2 Bottom-Up - Route 662
Inhaltsverzeichnis xv

13.6.3 Ereignis-Properties und Bequemlichkeitsmethoden 663

13.7 Layoutmanager 664


13.7.1 GridPane 665
13.7.1.1 Beispiel 665
13.7.1.2 GridPane-Eigenschaften zur Gestaltung von Abständen 665
13.7.1.3 Anzeigeeinstellungen für Kindelemente (layout constraints) 666
13.7.1.4 Dynamische Platzverteilung auf mehrere Spalten bzw. Zeilen 667
13.7.2 AnchorPane 668
13.7.3 HBox und VBox 669
13.7.4 BorderPane 670
13.7.5 FlowPane 672
13.7.6 StackPane 673

13.8 Elementare Steuerelemente 674


13.8.1 Label 674
13.8.2 Button 676
13.8.3 Einzeiliges Texteingabefeld 678
13.8.4 Umschalter 680
13.8.4.1 Kontrollkästchen 680
13.8.4.2 Radioschalter 682

13.9 Modulare JavaFX-Anwendung ausliefern 683

13.10 Übungsaufgaben zum Kapitel 13 685

14 EIN- UND AUSGABE ÜBER DATENSTRÖME 689


14.1 Grundlagen 689
14.1.1 Datenströme 689
14.1.2 Beispiel 690
14.1.3 Klassifikation der Stromverarbeitungsklassen 691
14.1.4 Aufbau und Verwendung der Transformationsklassen 692
14.1.5 Zum guten Schluss 694

14.2 Verwaltung von Dateien und Verzeichnissen 696


14.2.1 Dateisystemzugriffe über das NIO.2 - API 697
14.2.1.1 Repräsentation von Dateisystemeinträgen 697
14.2.1.2 Existenzprüfung 699
14.2.1.3 Verzeichnis anlegen 700
14.2.1.4 Datei explizit erstellen 700
14.2.1.5 Attribute von Dateisystemobjekten ermitteln 700
14.2.1.6 Zugriffsrechte für Dateien ermitteln 702
14.2.1.7 Attribute ändern 702
14.2.1.8 Über Verzeichniseinträge iterieren 703
14.2.1.9 Datei und Ordner kopieren 703
14.2.1.10 Umbenennen und Verschieben 704
14.2.1.11 Löschen 705
14.2.1.12 Informationen über Dateisysteme ermitteln 705
14.2.1.13 Weitere Optionen 706
14.2.2 Dateisystemzugriffe über die Klasse File aus dem Paket java.io 707
14.2.2.1 Verzeichnis anlegen 707
14.2.2.2 Dateien explizit erstellen 708
14.2.2.3 Informationen über Dateien und Ordner ermitteln 708
14.2.2.4 Atribute ändern 709
14.2.2.5 Verzeichnisinhalte auflisten 709
14.2.2.6 Umbenennen 710
14.2.2.7 Löschen 710
xvi Inhaltsverzeichnis

14.3 Klassen zur Verarbeitung von Byte-Strömen 711


14.3.1 Die OutputStream-Hierarchie 711
14.3.1.1 Überblick 711
14.3.1.2 FileOutputStream 712
14.3.1.3 OutputStream mit Dateianschluss per NIO.2 - API 714
14.3.1.4 DataOutputStream 715
14.3.1.5 BufferedOutputStream 716
14.3.1.6 PrintStream 718
14.3.2 Die InputStream-Hierarchie 720
14.3.2.1 Überblick 720
14.3.2.2 FileInputStream 722
14.3.2.3 InputStream mit Dateianschluss per NIO.2 - API 723
14.3.2.4 DataInputStream 723

14.4 Klassen zur Verarbeitung von Zeichenströmen 724


14.4.1 Die Writer-Hierarchie 724
14.4.1.1 Überblick 724
14.4.1.2 Brückenklasse OutputStreamWriter 725
14.4.1.3 FileWriter 728
14.4.1.4 BufferedWriter 729
14.4.1.5 PrintWriter 731
14.4.1.6 BufferedWriter mit Dateianschluss per NIO.2 - API 733
14.4.2 Die Reader-Hierarchie 734
14.4.2.1 Überblick 734
14.4.2.2 Brückenklasse InputStreamReader 735
14.4.2.3 FileReader und BufferedReader 736
14.4.2.4 BufferedReader mit Dateianschluss per NIO.2 - API 737

14.5 Zahlen und Zeichenfolgen aus einer Textdatei lesen 738

14.6 Objektserialisierung 742


14.6.1 Objektserialisierung und Sicherheit 743
14.6.2 Beispiel für eine serialisierbare Klasse 743
14.6.3 Versionskontrolle und Kompatibilitätsprobleme 744
14.6.4 Objekte in eine Datei schreiben und von dort lesen 745
14.6.5 Von der Serialisierung ausgeschlossene Felder 747
14.6.6 Mehr Kontrolle und Eigenverantwortung 748
14.6.7 Serialisierung und Vererbung 749
14.6.8 Serialisierung bei Record-Klassen 750
14.6.9 Bewertung der Objektserialisierung und mögliche Alternativen 751

14.7 Daten lesen und schreiben über die NIO.2 - Klasse Files 752
14.7.1 Öffnungsoptionen 752
14.7.2 Lesen und Schreiben von kleinen Dateien 753
14.7.3 Datenstrom zu einem Path-Objekt erstellen 754
14.7.4 MIME-Type einer Datei ermitteln 755
14.7.5 Stream<String> mit den Zeilen einer Textdatei erstellen 755

14.8 Empfehlungen zur Ein- und Ausgabe 755


14.8.1 Ausgabe in eine Textdatei 756
14.8.2 Textzeilen einlesen 757
14.8.3 Zahlen und andere Tokens aus einer Textdatei lesen 758
14.8.4 Eingabe von der Konsole 759
14.8.5 Werte mit primitiven Datentypen in eine Binärdatei schreiben 759
14.8.6 Werte mit primitiven Datentypen aus einer Binärdatei lesen 760
14.8.7 Binäre Objekt(de)serialisierung 761

14.9 Übungsaufgaben zum Kapitel 14 762


Inhaltsverzeichnis xvii

15 MULTITHREADING 765
15.1 Start und Ende eines Threads 767
15.1.1 Die Klasse Thread 767
15.1.2 Das Interface Runnable 772

15.2 Threads koordinieren 773


15.2.1 Fehlerhafte oder veraltete Daten 773
15.2.1.1 Fehlerhafte Daten aufgrund von nicht-atomaren Operationen 773
15.2.1.2 Veraltete Daten im lokalen Cache eines Threads 775
15.2.2 Per Monitor synchronisierte Code-Bereiche 775
15.2.2.1 Synchronisierte Methoden und Blöcke 776
15.2.2.2 Koordination per wait(), notify() und notifyAll() 778
15.2.3 Explizite Lock-Objekte 780
15.2.3.1 Interface Lock und Klasse ReentrantLock 780
15.2.3.2 Koordination per await(), signal() und signalAll() 783
15.2.4 Automatisierte Thread-Koordination für Produzenten-Konsumenten - Konstellationen 785
15.2.4.1 BlockingQueue<E> 785
15.2.4.2 PipedOutputStream und PipedInputStream 787
15.2.5 Klassen zur Thread-Synchronisation 789
15.2.5.1 Semaphore 789
15.2.5.2 CountDownLatch 792
15.2.5.3 CyclicBarrier 793
15.2.5.4 Phaser 795
15.2.6 Nicht-blockierende Koordination 797
15.2.6.1 Modifikator volatile 797
15.2.6.2 Atomare Variablen 798

15.3 Direkt gestartete Threads verwalten 800


15.3.1 Weck mich, wenn Du fertig bist (join) 800
15.3.2 Andere Threads unterbrechen, fortsetzen oder abbrechen 801
15.3.2.1 Unterbrechen und fortsetzen 801
15.3.2.2 Abbrechen 802
15.3.3 Thread-Lebensläufe 804
15.3.3.1 Scheduling und Prioritäten 804
15.3.3.2 Zustände von Threads 805
15.3.4 Deadlock 806

15.4 Aufgaben per Threadpool erledigen 807


15.4.1 ExecutorService 807
15.4.2 Die Schnittstellen Callable<T> und Future<T> 810
15.4.3 Threadpools mit Timer-Funktionalität 812

15.5 Datenparallelität mit Hilfe des Fork-Join - Frameworks 814


15.5.1 Direkte Verwendung des Fork-Join - Frameworks 814
15.5.2 Parallele Aggregatoperationen mit Strömen 818

15.6 Aufgabenparallelität mit Hilfe der Klasse CompletableFuture<T> 819


15.6.1 Asynchrone Verarbeitung einer einzelnen Aufgabe 820
15.6.1.1 Aufgaben ohne Rückgabewert 820
15.6.1.2 Aufgaben mit Rückgabewert 821
15.6.2 Folgeaufgaben 822
15.6.2.1 Unproduktiver Nachfolger zu einer Aufgabe ohne Rückgabewert 822
15.6.2.2 Produktiver Nachfolger zu einer Aufgabe mit Rückgabewert 825
15.6.2.3 Konsumierender Nachfolger zu einer Aufgabe mit Rückgabewert 827
15.6.3 Folgeaufgaben mit zwei Vorgängern 829
15.6.3.1 AND-Zusammenführung von zwei Aufgaben mit Rückgabe 829
15.6.3.2 OR-Zusammenführung von zwei Aufgaben mit Rückgabe 831
15.6.3.3 Zusammenführung von zwei Aufgaben ohne Rückgabe 832
xviii Inhaltsverzeichnis

15.6.4 Ausnahmebehandlung 833


15.6.4.1 Ausnahme diagnostizieren 833
15.6.4.2 Ausnahme behandeln und Verarbeitungskette fortsetzen 835
15.6.4.3 Ausnahme protokollieren 838
15.6.5 Auf eine Serie von Aufgaben warten 839
15.6.5.1 Auf die Beendigung aller Aufgaben warten 839
15.6.5.2 Auf die Beendigung eine beliebigen Aufgabe warten 841

15.7 Thread-sichere Kollektionen im Paket java.util.concurrent 842

15.8 Threads und JavaFX 845


15.8.1 JavaFX-Komponenten aus einem Hintergrund-Thread modifizieren 845
15.8.2 Das JavaFX-Multithreading - API 846
15.8.3 Die Klasse Task<V> 848

15.9 Reaktive Ströme (Flow-API) 851


15.9.1 Flow-Schnittstellen 851
15.9.1.1 Publisher<T> 852
15.9.1.2 Subscriber<T> 852
15.9.1.3 Subscription 853
15.9.1.4 Processor<T,R> 853
15.9.2 Zentrale Prinzipien der reaktiven Stromverarbeitung 854
15.9.3 Klasse SubmissionPublisher<T> 854
15.9.3.1 Versorgung eines einzelnen Subscribers per submit() 855
15.9.3.2 Auslieferungsmethode offer() als Alternative zur blockierenden Methode submit() 857

15.10 Sonstige Thread-Themen 860


15.10.1 Daemon-Threads 860
15.10.2 Thread-Gruppen 860

15.11 Übungsaufgaben zum Kapitel 15 860

16 NETZWERKPROGRAMMIERUNG 863
16.1 Elementare Konzepte der Netzwerktechnologie 864
16.1.1 Das OSI-Modell 864
16.1.2 Zur Funktionsweise von Protokollstapeln 869
16.1.3 Optionen zur Netzwerkprogrammierung in Java 870

16.2 Internet-Ressourcen per HTTP-Protokoll nutzen 871


16.2.1 Uniform Resource Locator 871
16.2.2 HTTP/1.1 - API 872
16.2.2.1 URL 872
16.2.2.2 URLConnection 876
16.2.2.3 HttpsURLConnection 878
16.2.3 Dynamisch erstellte Webinhalte anfordern 880
16.2.3.1 CGI-Software 880
16.2.3.2 GET 882
16.2.3.3 POST 883
16.2.4 HTTP/2 - API 885
16.2.4.1 Synchrone Anforderung einer statischen Webseite 886
16.2.4.2 Asynchrone Anforderung einer statischen Webseite 889
16.2.4.3 Synchrone POST-Anforderung 890
16.2.5 Nutzung der Browser-Komponente WebView in JavaFX-Anwendungen 892

16.3 IP-Adressen bzw. Host-Namen ermitteln 895


Inhaltsverzeichnis xix

16.4 Socket-Programmierung 896


16.4.1 TCP-Klient 897
16.4.1.1 Socket-Konstruktorüberladungen 898
16.4.1.2 Ein Datetime-Klient 898
16.4.1.3 Timeout setzen 899
16.4.1.4 Socket schließen 899
16.4.2 TCP-Server 900
16.4.2.1 Firewall-Ausnahme für einen TCP-Server unter Windows 10 900
16.4.2.2 Singlethreading-Server 900
16.4.2.3 Multithreading-Server 902

16.5 Übungsaufgaben zu Kapitel 16 905

ANHANG 907
A. Operatorentabelle 907

B. Lösungsvorschläge zu den Übungsaufgaben 908


Kapitel 1 (Einleitung) 908
Kapitel 2 (Werkzeuge zum Entwickeln von Java-Programmen) 908
Kapitel 3 (Elementare Sprachelemente) 909
Abschnitt 3.1 (Einstieg) 909
Abschnitt 3.2 (Ausgabe bei Konsolenanwendungen) 910
Abschnitt 3.3 (Variablen und Datentypen) 911
Abschnitt 3.4 (Eingabe bei Konsolen) 912
Abschnitt 3.5 (Operatoren und Ausdrücke) 913
Abschnitt 3.6 (Über- und Unterlauf bei numerischen Variablen) 914
Abschnitt 3.7 (Anweisungen (zur Ablaufsteuerung)) 914
Kapitel 4 (Klassen und Objekte) 916
Kapitel 5 (Wichtige spezielle Klassen) 918
Abschnitt 5.1 (Arrays) 918
Abschnitt 5.2 (Klassen für Zeichen) 919
Abschnitt 5.3 (Verpackungsklassen für primitive Datentypen) 919
Abschnitt 5.4 (Aufzählungstypen) 920
Abschnitt 5.5 (Records) 920
Kapitel 6 (Pakete und Module) 920
Kapitel 7 (Vererbung und Polymorphie) 921
Kapitel 8 (Generische Klassen und Methoden) 922
Kapitel 9 (Interfaces) 922
Kapitel 10 (Java Collections Framework) 923
Kapitel 11 (Ausnahmebehandlung) 924
Kapitel 12 (Funktionales Programmieren) 925
Kapitel 13 (GUI-Programmierung mit JavaFX) 926
Kapitel 14 (Ein- und Ausgabe über Datenströme) 926
Kapitel 15 (Multithreading) 927
Abschnitt 16 (Netzwerkprogrammierung) 928

LITERATUR 929

INDEX 933
1 Einleitung
Im ersten Kapitel geht es zunächst um die Denk- und Arbeitsweise der objektorientierten Program-
mierung. Danach wird Java als Software-Technologie vorgestellt.

1.1 Beispiel für die objektorientierte Software-Entwicklung mit Java


In diesem Abschnitt soll eine Vorstellung davon vermittelt werden, was ein Computerprogramm (in
Java) ist. Dabei kommen einige Grundbegriffe der Informatik zur Sprache, wobei wir uns aber nicht
unnötig lange von der Praxis fernhalten wollen.
Ein Computerprogramm besteht im Wesentlichen (von Medien und anderen Ressourcen einmal
abgesehen) aus einer Menge von wohlgeformten und wohlgeordneten Definitionen und Anweisun-
gen zur Bewältigung bestimmter Aufgaben. Ein Programm muss ...
• den betroffenen Anwendungsbereich modellieren
Beispiel: In einem Programm zur Verwaltung einer Spedition sind z. B. Kunden, Aufträge,
Mitarbeiter, Fahrzeuge, Einsatzfahrten, (Ent-)ladestationen etc. und
kommunikative Prozesse (Nachrichten zwischen beteiligten Akteuren) zu reprä-
sentieren.
• Algorithmen realisieren, die in endlich vielen Schritten und unter Verwendung von endlich
vielen Betriebsmitteln (z. B. Speicher, CPU-Leistung) bestimmte Ausgangszustände in ak-
zeptable Zielzustände überführen.
Beispiel: Im Speditionsprogramm muss u. a. für jede Tour zu den meist mehreren (Ent-)la-
destationen eine optimale Route ermittelt werden (hinsichtlich Kraftstoffver-
brauch, Fahrtzeit, Mautkosten etc.).
Wir wollen präzisere und komplettere Definitionen zum komplexen Begriff eines Computerpro-
gramms den Informatik-Lehrbüchern überlassen (siehe z. B. Goll & Heinisch 2016) und stattdessen
ein Beispiel im Detail betrachten, um einen Einstieg in die Materie zu finden.
Bei der Suche nach einem geeigneten Java-Einstiegsbeispiel tritt ein Dilemma auf:
• Einfache Beispiele sind angenehm, aber für das Programmieren mit Java nicht besonders re-
präsentativ. Z. B. ist von der Objektorientierung außer einem gewissen Formalismus nichts
vorhanden.
• Repräsentative Java-Programme eignen sich in der Regel wegen ihrer Länge und Komplexi-
tät (aus der Sicht eines Anfängers) nicht für den Einstieg. Beispielsweise können wir das
eben zur Illustration einer realen Aufgabenstellung verwendete, aber potentiell sehr aufwän-
dige Speditionsverwaltungsprogramm jetzt nicht vorstellen.
Wir analysieren ein Beispielprogramm, das trotz angestrebter Einfachheit nicht auf objektorientier-
tes Programmieren (OOP) verzichtet. Seine Aufgabe besteht darin, elementare Operationen mit
Brüchen auszuführen (z. B. Kürzen, Addieren), womit es etwa einem Schüler beim Anfertigen der
Hausaufgaben (zur Kontrolle der eigenen Lösungen) nützlich sein kann. Das Beispiel wird in suk-
zessive ausgebauter Form im Kurs noch oft verwendet.

1.1.1 Objektorientierte Analyse und Modellierung


Einer objektorientierten Programmierung geht die objektorientierte Analyse der Aufgabenstellung
voraus mit dem Ziel einer Modellierung durch kooperierende Klassen. Man identifiziert per Abs-
traktion die beteiligten Kategorien von Individuen bzw. Objekten und definiert für sie jeweils eine
Klasse.
In unserem Bruchrechnungsbeispiel ergibt sich bei der objektorientierten Analyse, dass vorläufig
nur eine Klasse zum Modellieren von Brüchen benötigt wird (Name: Bruch). Beim möglichen
2 Kapitel 1 Einleitung

Ausbau des Programms zu einem Bruchrechnungstrainer kommen jedoch weitere Klassen hinzu
(z. B. Aufgabe, Schüler).
Eine Klasse ist gekennzeichnet durch:
• Eigenschaften bzw. Zustände
Die Objekte bzw. Instanzen der Klasse und auch die Klasse selbst besitzen jeweils einen
Zustand, der durch Eigenschaften gekennzeichnet ist. Im Beispiel der Klasse Bruch ...
o besitzt ein Objekt die Eigenschaften Zähler und Nenner,
o gehört zu den Eigenschaften der Klasse die z. B. Anzahl der bei einem Pro-
grammeinsatz bereits erzeugten Brüche.
Im letztlich entstehenden Programm landet jede Eigenschaft in einer sogenannten Variab-
len. Darunter versteht man einen benannten Speicherplatz, der Werte eines bestimmten Typs
(z. B. ganze Zahlen, Zeichenfolgen) aufnehmen kann. Variablen zum Speichern der Eigen-
schaften von Objekten oder Klassen werden in Java meist als Felder bezeichnet.
• Handlungskompetenzen
Analog zu den Eigenschaften sind auch die Handlungskompetenzen entweder individuellen
Objekten bzw. Instanzen oder der Klasse selbst zugeordnet. Im Beispiel der Klasse Bruch ...
o hat ein Objekt z. B. die Fähigkeit zum Kürzen von Zähler und Nenner,
o kann die Klasse z. B. über die Anzahl der bereits erzeugten Brüche informieren.
Im letztlich entstehenden Programm sind die Handlungskompetenzen durch sogenannte Me-
thoden repräsentiert. Diese ausführbaren Programmbestandteile enthalten die oben ange-
sprochenen Algorithmen. Die Kommunikation zwischen Klassen und Objekten besteht da-
rin, ein Objekt oder eine Klasse aufzufordern, eine bestimmte Methode auszuführen.
Eine Klasse …
• beinhaltet meist einen Bauplan für konkrete Objekte, die im Programmablauf nach Bedarf
erzeugt und mit der Ausführung bestimmter Methoden beauftragt werden,
• andererseits aber auch Akteur sein (Methoden ausführen und aufrufen).
Bei einer nur einfach zu besetzenden Rolle kann eine Klasse zum Einsatz kommen, die nicht zum
Instanziieren (Erzeugen von Objekten) gedacht ist, aber als Akteur mitwirkt.
Dass Zähler und Nenner die zentralen Eigenschaften eines Bruch-Objekts sind, bedarf keiner nä-
heren Erläuterung. Sie werden in der Klassendefinition durch Felder zum Speichern von ganzen
Zahlen (Java-Datentyp int) mit den folgenden Namen repräsentiert:
• zaehler
• nenner
Eine wichtige, auf den ersten Blick leicht zu übersehende Entscheidung der Modellierungsphase
besteht darin, beim Zähler und beim Nenner eines Bruchs auch negative ganze Zahlen zu erlauben.1
Alternativ könnte man ...
• beim Nenner negative Werte verbieten, um folgende Beispiele auszuschließen:
2 −2
,
−3 −3
• beim Zähler und beim Nenner negative Werte verbieten, weil ein Bruch als Anteil aufgefasst
und daher stets größer oder gleich null sein sollte.
Auf die oben als Möglichkeit genannte klassenbezogene Eigenschaft mit der Anzahl bereits erzeug-
ter Brüche wird vorläufig verzichtet.

1
Auf die Möglichkeit, alternative Bruch-Definitionen in Erwägung zu ziehen, hat Paul Frischknecht hingewiesen.
Abschnitt 1.1 Beispiel für die objektorientierte Software-Entwicklung mit Java 3

Im objektorientierten Paradigma ist jede Klasse für die Manipulation ihrer Eigenschaften selbst ver-
antwortlich. Diese sollten eingekapselt und so vor dem direkten Zugriff durch fremde Klassen ge-
schützt sein. So kann sichergestellt werden, dass nur sinnvolle Änderungen der Eigenschaften auf-
treten. Außerdem wird aus später zu erläuternden Gründen die Produktivität der Software-Entwick-
lung durch die Datenkapselung gefördert.
Demgegenüber sind die Handlungskompetenzen (Methoden) einer Klasse in der Regel von ande-
ren Akteuren (Klassen, Objekten) ansprechbar, wobei es aber auch private Methoden für den aus-
schließlich internen Gebrauch gibt. Die öffentlichen Methoden einer Klasse bilden ihre Schnittstel-
le zur Kommunikation mit anderen Klassen. Man spricht auch vom API (Application Programming
Interface) einer Klasse.
Die folgende, an Goll & Heinisch (2016) angelehnte Abbildung zeigt für eine Klasse ...
• im gekapselten Bereich ihre Felder sowie eine private Methode
• die Kommunikationsschnittstelle mit den öffentlichen Methoden
de
tho

Methode
Me

Me
e

Merkmal
od

Me
al

th
Feld
th
rkm

od
rk
Me

ma

e
Me

FeldKlasse AFeld
l
l
Me

a
rkm
Me

e
rkm

od

priv. Methode
Me
th

th
al
od

Merkmal
Me
e

de

Methode
tho
Me

Für die Objekte einer Klasse wird in der objektorientierten Analyse und Modellierung die die Befä-
higung eingeplant, auf eine Reihe von Nachrichten mit einem bestimmten Verhalten zu reagieren.
In unserem Beispiel sollten die Objekte der Klasse Bruch z. B. eine Methode zum Kürzen besitzen.
Dann kann einem konkreten Bruch-Objekt durch Aufrufen dieser Methode die Nachricht zugestellt
werden, Zähler und Nenner zu kürzen.
Sich unter einem Bruch ein Objekt vorzustellen, das Nachrichten empfängt und mit einem passen-
den Verhalten beantwortet, ist etwas gewöhnungsbedürftig. In der realen Welt sind Brüche, die sich
selbst auf ein Signal hin kürzen, nicht unbedingt alltäglich, wenngleich möglich (z. B. als didakti-
sches Spielzeug). Das objektorientierte Modellieren eines Anwendungsbereichs ist nicht unbedingt
eine direkte Abbildung, sondern eine Rekonstruktion. Einerseits soll der Anwendungsbereich im
Modell gut repräsentiert sein, andererseits soll eine möglichst stabile, gut erweiterbare und wieder-
verwendbare Software entstehen.
Um (Objekten aus) fremden Klassen trotz Datenkapselung die Veränderung einer Eigenschaft zu
erlauben, müssen entsprechende Methoden (mit geeigneten Kontrollmechanismen) angeboten wer-
den. Unsere Bruch-Klasse sollte also über Methoden zum Verändern von Zähler und Nenner ver-
fügen (z. B. mit den Namen setzeZaehler() und setzeNenner()). Bei einer gekapselten Ei-
genschaft ist auch der direkte Lesezugriff ausgeschlossen, sodass im Bruch-Beispiel auch noch Me-
thoden zum Ermitteln von Zähler und Nenner erforderlich sind (z. B. mit den Namen gib-
4 Kapitel 1 Einleitung

Zaehler() und gibNenner()). Eine konsequente Umsetzung der Datenkapselung erzwingt also
eventuell eine ganze Serie von Methoden zum Lesen und Setzen von Eigenschaftswerten.
Mit diesem Aufwand werden aber gravierende Vorteile realisiert:
• Stabilität
Die Eigenschaften sind vor unsinnigen und gefährlichen Zugriffen geschützt, wenn Verän-
derungen nur über die vom Klassendesigner sorgfältig entworfenen Methoden möglich sind.
Treten trotzdem Fehler auf, sind diese relativ leicht zu identifizieren, weil nur wenige Me-
thoden verantwortlich sein können. Gelegentlich kann es auch wichtig sein, dass Eigen-
schaftsausprägungen von anderen Klassen nicht ermittelt werden können.
• Produktivität
Durch Datenkapselung wird die Modularisierung unterstützt, sodass große Softwaresyste-
me beherrschbar werden und zahlreiche Programmierer möglichst reibungslos zusammenar-
beiten können. Der Klassendesigner trägt die Verantwortung dafür, dass die von ihm ent-
worfenen Methoden korrekt arbeiten. Andere Programmierer müssen beim Verwenden einer
Klasse lediglich die Methoden der Schnittstelle kennen. Das Innenleben einer Klasse kann
vom Designer nach Bedarf geändert werden, ohne dass andere Programmbestandteile ange-
passt werden müssen. Bei einer sorgfältig entworfenen Klasse stehen die Chancen gut, dass
sie in mehreren Software-Projekten genutzt werden kann (Wiederverwendbarkeit). Beson-
ders günstig ist die Recycling-Quote bei den Klassen der Java-Standardbibliothek (siehe
Abschnitt 1.3.3), von denen alle Java-Programmierer regen Gebrauch machen. Auch die
Klasse Bruch aus dem Beispielprojekt besitzt einiges Potential zur Wiederverwendung.
Nach obigen Überlegungen sollten die Objekte der Klasse Bruch folgende Methoden beherrschen:
• setzeZaehler(int z), setzeNenner(int n)
Ein Objekt wird beauftragt, seinen zaehler bzw. nenner auf einen bestimmten Wert zu
setzen. Ein direkter Zugriff auf die Eigenschaften soll fremden Klassen nicht erlaubt sein
(Datenkapselung). Bei dieser Vorgehensweise kann das Objekt z. B. verhindern, dass sein
Nenner auf null gesetzt wird.
Wie die Beispiele zeigen, wird dem Namen einer Methode eine in runden Klammern einge-
schlossene, eventuell leere Parameterliste angehängt. Methodenparameter, mit denen wir
uns noch ausführlich beschäftigen werden, haben einen Namen (bei setzeNenner() z. B.
n) und einen Datentyp. Im Beispiel erlaubt der Datentyp int ganze Zahlen als Werte.
• gibZaehler(), gibNenner()
Ein Bruch-Objekt wird beauftragt, den Wert seiner Zähler- bzw. Nenner-Eigenschaft mitzu-
teilen. Diese Methoden sind im Beispiel erforderlich, weil bei gekapselten Eigenschaften
weder schreibende noch lesende Direktzugriffe möglich sind.
• kuerze()
Ein Objekt wird beauftragt, zaehler und nenner zu kürzen. Welcher Algorithmus dazu
benutzt wird, bleibt dem Klassendesigner überlassen.
• addiere(Bruch b)
Ein Objekt wird beauftragt, den als Parameter übergebenen Bruch zum eigenen Wert zu ad-
dieren. Wir werden uns noch ausführlich damit beschäftigen, wie man beim Aufruf einer
Methode ihr Verhalten durch die Übergabe von Parametern (Argumenten) steuert.
• frage()
Ein Objekt wird beauftragt, zaehler und nenner beim Anwender via Konsole (Einga-
beaufforderung) zu erfragen.
• zeige()
Ein Objekt wird beauftragt, zaehler und nenner auf der Konsole anzuzeigen.
Abschnitt 1.1 Beispiel für die objektorientierte Software-Entwicklung mit Java 5

In realen (komplexeren) Programmen wird keinesfalls jedes gekapselte Feld über ein Methodenpaar
zum Lesen und geschützten Schreiben für die Außenwelt zugänglich gemacht.
Beim Eigenschaftsbegriff ist eine (ungefährliche) Zweideutigkeit festzustellen, die je nach Anwen-
dungsbeispiel mehr oder weniger spürbar wird (beim Bruchrechnungsbeispiel überhaupt nicht).
Man kann unterscheiden:
• real definierte, meist gekapselte Felder
Diese sind für die Außenwelt (für andere Klassen) irrelevant und unbekannt. In diesem Sinn
wurde der Begriff oben eingeführt.
• nach außen dargestellte Eigenschaften
Eine solche Eigenschaft ist über Methoden zum Lesen und/oder Schreiben zugänglich und
nicht unbedingt durch ein einzelnes Feld realisiert.
Wir sprechen im Manuskript meist über Felder und Methoden, wobei keinerlei Mehrdeutigkeit be-
steht.
Man verwendet für die in einer Klasse definierten Bestandteile oft die Bezeichnung Member, gele-
gentlich auch die deutsche Übersetzung Mitglieder. Unsere Klasse Bruch hat folgende Mitglieder:
• Felder
zaehler, nenner
• Methoden
setzeZaehler(), setzeNenner(), gibZaehler(), gibNenner(),
kuerze(), addiere(), frage() und zeige()
Von kommunizierenden Objekten und Klassen mit Handlungskompetenzen zu sprechen, mag als
übertriebener Anthropomorphismus (als Vermenschlichung) erscheinen. Bei der Ausführung von
Methoden sind Objekte und Klassen selbstverständlich streng determiniert, während Menschen bei
Kommunikation und Handlungsplanung ihren freien Willen einbringen, Spontanität, Kreativität und
auch Emotionen besitzen. Fußball spielende Roboter (als besonders anschauliche Objekte aufge-
fasst) zeigen allerdings mittlerweile schon recht weitsichtige und auch überraschende Spielzüge.
Was sie noch zu lernen haben, sind vielleicht Strafraumschwalben, absichtliches Handspiel etc.
Nach diesen Randbemerkungen kehren wir zum Programmierkurs zurück, um möglichst bald
freundliche und kompetente Objekte definieren zu können.
Um die durch objektorientierte Analyse gewonnene Modellierung eines Anwendungsbereichs stan-
dardisiert und übersichtlich zu beschreiben, wurde die Unified Modeling Language (UML) entwi-
ckelt, die bevorzugt mit Diagrammen arbeitet.1 Hier wird eine Klasse durch ein Rechteck mit drei
Abschnitten dargestellt:
• Oben steht der Name der Klasse.
• In der Mitte stehen die Eigenschaften (Felder).
Hinter dem Namen einer Eigenschaft gibt man ihren Datentyp an (z. B. int für ganze Zah-
len).
• Unten stehen die Handlungskompetenzen (Methoden).
In Anlehnung an eine in vielen Programmiersprachen (wie z. B. Java) übliche Syntax zur
Methodendefinition gibt man für die Argumente eines Methodenaufrufs sowie für den
Rückgabewert (falls vorhanden) den Datentyp an. Was mit dem letzten Satz genau gemeint
ist, werden Sie bald erfahren.

1
Während die UML im akademischen Bereich nachdrücklich empfohlen wird, ist ihre Verwendung in der Software-
Branche allerdings noch entwicklungsfähig, wie empirische Studien gezeigt haben (siehe z. B. Baltes & Diehl 2014,
Petre 2013).
6 Kapitel 1 Einleitung

Für die Bruch-Klasse erhält man die folgende Darstellung:

Bruch

zaehler: int
nenner: int

setzeZaehler(int zpar)
setzeNenner(int npar):boolean
gibZaehler():int
gibNenner():int
kuerze()
addiere(Bruch b)
frage()
zeige()

Sind bei einer Anwendung mehrere Klassen beteiligt, dann sind auch die Beziehungen zwischen
den Klassen wesentliche Bestandteile des Modells. In einem UML-Klassendiagramm können u. a.
die folgenden Beziehungen zwischen Klassen (bzw. zwischen den Objekten von Klassen) darge-
stellt werden:
• Spezialisierung bzw. Vererbung („Ist-ein - Beziehung“)
Beispiel: Ein Lieferwagen ist ein spezielles Auto.
• Komposition („Hat - Beziehung“)
Beispiel: Ein Auto hat einen Motor.
• Assoziation („Kennt - Beziehung“)
Beispiel: Ein (intelligentes, autonomes) Auto kennt eine Liste von Parkplätzen.
Nach der sorgfältigen Modellierung per UML muss übrigens die Programmierung nicht am Punkt
null beginnen, weil UML-Entwicklungswerkzeuge üblicherweise Teile des Quellcodes automatisch
aus dem Modell erzeugen können. Die kostenpflichtige Ultimate-Version der im Kurs bevorzugte
Java-Entwicklungsumgebung IntelliJ IDEA (siehe Abschnitt 2.4) unterstützt die UML-
Modellierung und erstellt automatisch den Quellcode zu einem UML-Diagramm. Das mit IntelliJ
Ultimate erstellte Diagramm der Klasse Bruch zeigt für die Klassen-Member auch die Art (Feld
bzw. Methode) sowie den Zugriffsschutz an (privat bzw. öffentlich):

Die fehlende UML-Unterstützung der Community Edition von IntelliJ wird sich im Kurs nicht
nachteilig auswirken.
Abschnitt 1.1 Beispiel für die objektorientierte Software-Entwicklung mit Java 7

Weiterführende Informationen zur objektorientierten Analyse und Modellierung bieten z. B. Balzert


(2011) und Booch et al. (2007).

1.1.2 Objektorientierte Programmierung


In unserem einfachen Beispielprojekt soll nun die Klasse Bruch in der Programmiersprache Java
codiert werden, wobei die Felder (Eigenschaften) zu deklarieren und die Methoden zu implementie-
ren sind. Es resultiert der sogenannte Quellcode, der in einer Textdatei namens Bruch.java unter-
gebracht werden muss.
Zwar sind Ihnen die meisten Details der folgenden Klassendefinition selbstverständlich jetzt noch
fremd, doch sind die Felddeklarationen und Methodenimplementationen als zentrale Bestandteile
leicht zu erkennen. Außerdem sind Sie nach den ausführlichen Erläuterungen zur Datenkapselung
sicher an der technischen Umsetzung interessiert. Die beiden Felder (zaehler, nenner) werden
über den Modifikator private vor direkten Zugriffen durch fremde Klassen geschützt. Demgegen-
über werden die Methoden über den Modifikator public für die Verwendung in klassenfremden
Methoden freigegeben. Für die Klasse selbst wird mit dem Modifikator public die Verwendung in
beliebigen Java-Programmen erlaubt.1
public class Bruch {
private int zaehler; // wird automatisch mit 0 initialisiert
private int nenner = 1;

public void setzeZaehler(int z) {


zaehler = z;
}

public boolean setzeNenner(int n) {


if (n != 0) {
nenner = n;
return true;
} else
return false;
}

public int gibZaehler() {


return zaehler;
}

public int gibNenner() {


return nenner;
}

1
Bei der Zugriffsberechtigung spielen in Java die Pakete (und ab Java 9 zusätzlich die Module) eine wichtige Rolle.
Jede Klasse gehört zu einem Paket, und per Voreinstellung (ohne Vergabe eines Zugriffsmodifikators) haben die
anderen Klassen im selben Paket Zugriff auf eine Klasse und ihre Member (Felder und Methoden). In der Regel
sollten die Felder einer Klasse auch vor dem Zugriff durch andere Klassen im selben Paket geschützt sein.
8 Kapitel 1 Einleitung

public void kuerze() {


// Größten gemeinsamen Teiler mit dem euklidischen Algorithmus bestimmen
if (zaehler != 0) {
int az = Math.abs(zaehler);
int an = Math.abs(nenner);
while (az != an)
if (az > an)
az = az - an;
else
an = an - az;
zaehler = zaehler / az;
nenner = nenner / az;
} else
nenner = 1;
}

public void addiere(Bruch b) {


zaehler = zaehler * b.nenner + b.zaehler * nenner;
nenner = nenner * b.nenner;
kuerze();
}
public void frage() {
int n;
do {
System.out.print("Zähler: ");
setzeZaehler(Simput.gint());
} while (Simput.checkError());
do {
System.out.print("Nenner: ");
n = Simput.gint();
if (n == 0 && !Simput.checkError())
System.out.println("Der Nenner darf nicht null werden!\n");
} while (n == 0);
setzeNenner(n);
}

public void zeige() {


System.out.printf(" %d\n -----\n %d\n", zaehler, nenner);
}
}
Allerdings ist das Programm schon zu umfangreich für die bald anstehenden ersten Gehversuche
mit der Software-Entwicklung in Java.
Wie Sie bei späteren Beispielen erfahren werden, dienen in einem objektorientierten Programm
beileibe nicht alle Klassen zur Modellierung des Aufgabenbereichs. Es sind auch Objekte aus der
Welt des Computers zu repräsentieren (z. B. Fenster der Bedienoberfläche, Netzwerkverbindungen,
Störungen des normalen Programmablaufs).

1.1.3 Algorithmen
Am Anfang von Abschnitt 1.1 wurden mit der Modellierung des Anwendungsbereichs und der Rea-
lisierung von Algorithmen zwei wichtige Aufgaben der Software-Entwicklung genannt, von denen
die letztgenannte bisher kaum zur Sprache kam. Auch im weiteren Verlauf des Manuskripts wird
die explizite Diskussion von Algorithmen (z. B. hinsichtlich Korrektheit, Terminierung und Auf-
wand) keinen großen Raum einnehmen. Wir werden uns intensiv mit der Programmiersprache Java
sowie der zugehörigen Standardbibliothek beschäftigen und dabei mit möglichst einfachen Bei-
spielprogrammen (Algorithmen) arbeiten. Damit die Beschäftigung mit Algorithmen im Kurs nicht
Abschnitt 1.1 Beispiel für die objektorientierte Software-Entwicklung mit Java 9

ganz fehlt, werden wir im Rahmen des Bruchrechnungsbeispiels alternative Verfahren zum Kürzen
von Brüchen betrachten.
Unser Einführungsbeispiel verwendet in der Methode kuerze() den bekannten und nicht gänzlich
trivialen euklidischen Algorithmus, um den größten gemeinsamen Teiler (GGT) von Zähler und
Nenner eines Bruchs zu bestimmen, durch den zum optimalen Kürzen beide Zahlen zu dividieren
sind. Im euklidischen Algorithmus wird die leicht zu beweisende Aussage genutzt, dass für zwei
natürliche Zahlen (1, 2, 3, …) u und v (u > v > 0) der GGT gleich dem GGT von v und (u - v) ist:
Ist t ein Teiler von u und v, dann gibt es natürliche Zahlen tu und tv mit tu > tv und
u = tut sowie v = tvt
Folglich ist t auch ein Teiler von (u - v), denn:
u - v = (tu - tv)t
Ist andererseits t ein Teiler von u und (u – v), dann gibt es natürliche Zahlen tu und td mit tu > td
und
u = tut sowie (u – v) = tdt
Folglich ist t auch ein Teiler von v:
u – (u – v) = v = (tu - td)t
Weil die Paare (u, v) und (v, u - v) dieselben Mengen gemeinsamer Teiler besitzen, sind auch die
größten gemeinsamen Teiler identisch.
Beim Übergang von
(u, v) mit u > v > 0
zu
(v, u - v) mit v > 0 und u - v > 0
wird die größere von den beiden Zahlen durch eine echt kleinere Zahl ersetzt, während der GGT
identisch bleibt.
Wenn v und (u - v) in einem Prozessschritt identisch werden, ist der GGT gefunden. Das muss nach
endlich vielen Schritten passieren, denn:
• Solange die beiden Zahlen im aktuellen Schritt k noch verschieden sind, resultieren im
nächsten Schritt k+1 zwei neue Zahlen mit einem echt kleineren Maximum.
• Alle Zahlen bleiben > 0.
• Das Verfahren endet in endlich vielen Schritten, eventuell mit v = u - v = 1.
Weil die Zahl 1 als trivialer Teiler zugelassen ist, existiert zu zwei natürlichen Zahlen immer ein
größter gemeinsamer Teiler, der eventuell gleich 1 ist.
Diese Ergebnisse werden in der Methode Kuerze() folgendermaßen ausgenutzt:
Es wird geprüft, ob Zähler und Nenner identisch sind. Trifft dies zu, ist der GGT gefunden (iden-
tisch mit Zähler und Nenner). Anderenfalls wird die größere der beiden Zahlen durch deren Dif-
ferenz ersetzt, und mit diesem vereinfachten Problem startet das Verfahren neu.
Man erhält auf jeden Fall in endlich vielen Schritten zwei identische Zahlen und damit den GGT.
Der beschriebene Algorithmus eignet sich dank seiner Einfachheit gut für das Einführungsbeispiel,
ist aber in Bezug auf den erforderlichen Berechnungsaufwand nicht optimal. In einer Übungs-
aufgabe zu Abschnitt 3.7 sollen Sie eine erheblich effizientere Variante implementieren.
10 Kapitel 1 Einleitung

Nachdem der euklidische Algorithmus mit seiner Schrittfolge und seinen Bedingungen vorgestellt
wurde, sind Sie vielleicht doch daran interessiert, wie der Algorithmus in der Methode kuerze()
der Klasse Bruch realisiert wird:
public void kuerze() {
// Größten gemeinsamen Teiler mit dem euklidischen Algorithmus bestimmen
if (zaehler != 0) {
int az = Math.abs(zaehler);
int an = Math.abs(nenner);
while (az != an)
if (az > an)
az = az - an;
else
an = an - az;
zaehler = zaehler / az;
nenner = nenner / az;
} else
nenner = 1;
}
In der Methode werden lokale Variablen deklariert (z.B. az zur Aufbewahrung des Betrags des
Zählers), die von den Feldern der Klasse zu unterscheiden sind, und Anweisungen ausgeführt. Der
Begriff einer Java-Anweisung wird im Manuskript erst nach ca. 200 Seiten vollständig entwickelt
sein. Trotzdem sind die folgenden Hinweise wohl schon jetzt zu verdauen. Einige Anweisungen
enthalten einfache Wertzuweisungen an Variablen, z. B.:
az = az - an;
Zwei Anweisungen sorgen in der Methode kuerze() für eine situationsadäquate und prägnante
Beschreibung des Lösungswegs:
• Verzweigung
Die if-Anweisung enthält eine Bedingung sowie ...
o einen Blockanweisung, die bei erfüllter Bedingung ausgeführt wird
o eine Anweisung, die bei nicht erfüllter Bedingung ausgeführt wird.
• Wiederholung
Die while-Schleife enthält eine Bedingung und eine Anweisung, die wiederholt ausgeführt
wird, solange die jeweils vor dem nächsten potentiellen Schleifendurchgang geprüfte Be-
dingung erfüllt ist.
Mit diesen beiden Anweisungen zur Ablaufsteuerung werden wir uns später noch ausführlich be-
schäftigen.

1.1.4 Startklasse und main() - Methode


Bislang wurde im Einführungsbeispiel aufgrund einer objektorientierten Analyse des Aufgabenbe-
reichs die Klasse Bruch entworfen und in Java realisiert. Wir verwenden nun die Klasse Bruch in
einer Konsolenanwendung zur Addition von zwei Brüchen. Dabei bringen wir einen Akteur ins
Spiel, der in einer simplen Anweisungssequenz Bruch-Objekte erzeugt und ihnen Nachrichten zu-
stellt, die (zusammen mit dem Verhalten des Anwenders) den Programmablauf voranbringen.
In diesem Zusammenhang ist von Bedeutung, dass es in jedem Java - Programm eine Startklasse
geben muss, die eine Methode mit dem Namen main() in ihrem klassenbezogenen Handlungsreper-
toire besitzt. Beim Starten eines Programms wird die (aufgrund des Startkommandos bekannte)
Startklasse aufgefordert, ihre Klassenmethode main() auszuführen. Wegen der besonderen Rolle
dieser Methode ist die Bezeichnung Hauptmethode durchaus berechtigt.
Abschnitt 1.1 Beispiel für die objektorientierte Software-Entwicklung mit Java 11

Es bietet sich an, die oben angedachte Anweisungssequenz des Bruchadditionsprogramms in der
obligatorischen main() - Methode der Startklasse unterzubringen.
Obwohl prinzipiell möglich, ist es nicht sinnvoll, die auf Wiederverwendbarkeit hin konzipierte
Klasse Bruch mit der Startmethode für eine sehr spezielle Anwendung zu belasten. Daher definie-
ren wir eine zusätzliche Klasse namens Bruchaddition, die nicht als Bauplan für Objekte dienen
soll und auch kaum Recycling-Chancen besitzt. Ihr Handlungsrepertoire kann sich auf die Klas-
senmethode main() zur Ablaufsteuerung im Bruchadditionsprogramm beschränken. Indem wir eine
neue Klasse definieren und dort Bruch-Objekte verwenden, wird u. a. gleich demonstriert, wie
leicht das Hauptergebnis unserer bisherigen Arbeit (die Klasse Bruch) für verschiedene Projekte
genutzt werden kann.
In der Bruchaddition - Methode main() werden zwei Objekte (Instanzen) aus der Klasse Bruch
erzeugt und mit der Ausführung verschiedener Methoden beauftragt. Beim Erzeugen der Objekte
mit Hilfe des new-Operators ist mit dem sogenannten Konstruktor (siehe unten) eine spezielle
Methode der Klasse Bruch beteiligt, die den Namen der Klasse trägt:
Eingabe (grün, kursiv) und
Quellcode
Ausgabe
class Bruchaddition { 1. Bruch
public static void main(String[] args) { Zähler: 20
Bruch b1 = new Bruch(), b2 = new Bruch(); Nenner: 84
5
System.out.println("1. Bruch"); -----
b1.frage(); 21
b1.kuerze();
b1.zeige();
2. Bruch
System.out.println("\n2. Bruch"); Zähler: 12
b2.frage(); Nenner: 36
b2.kuerze(); 1
b2.zeige(); -----
3
System.out.println("\nSumme");
b1.addiere(b2);
b1.zeige(); Summe
} 4
} -----
7

Wir haben zur Lösung der Aufgabe, ein Programm für die Addition von zwei Brüchen zu erstellen,
zwei Klassen mit der folgenden Rollenverteilung definiert:
• Die Klasse Bruch enthält den Bauplan für die wesentlichen Akteure im Aufgabenbereich.
Dort alle Eigenschaften und Handlungskompetenzen von Brüchen zu konzentrieren, hat fol-
gende Vorteile:
o Die Klasse kann in verschiedenen Programmen eingesetzt werden (Wiederverwend-
barkeit). Dies fällt vor allem deshalb so leicht, weil die Objekte sowohl Handlungs-
kompetenzen (Methoden) als auch die erforderlichen Eigenschaften (Felder) besit-
zen.
Wir müssen bei der Definition dieser Klasse ihre allgemeine Verfügbarkeit explizit
mit dem Zugriffsmodifikator public genehmigen. Per Voreinstellung ist eine Klasse
nur im eigenen Paket verfügbar (siehe Kapitel 6).
12 Kapitel 1 Einleitung

o Beim Umgang mit den Bruch-Objekten sind wenige Probleme zu erwarten, weil nur
klasseneigene Methoden direkten Zugang zu kritischen Eigenschaften haben (Daten-
kapselung). Sollten doch Fehler auftreten, sind die Ursachen in der Regel schnell ge-
funden.
• Die Klasse Bruchaddition dient nicht als Bauplan für Objekte, sondern enthält eine Klas-
senmethode main(), die beim Programmstart automatisch aufgerufen wird und dann für ei-
nen speziellen Einsatz von Bruch-Objekten sorgt. Mit einer Wiederverwendung des
Bruchaddition-Quellcodes in anderen Projekten ist kaum zu rechnen.
In der Regel bringt man den Quellcode jeder Klasse in einer eigenen Datei unter, die den Namen
der Klasse trägt, ergänzt um die Namenserweiterung .java, sodass im Beispielsprojekt die Quell-
codedateien Bruch.java und Bruchaddition.java entstehen. Weil die Klasse Bruch mit dem Zu-
griffsmodifikator public definiert wurde, muss ihr Quellcode in einer Datei mit dem Namen
Bruch.java gespeichert werden (siehe unten). Es wäre erlaubt, aber nicht sinnvoll, den Quellcode
der Klasse Bruchaddition ebenfalls in der Datei Bruch.java unterzubringen.
Wie aus den beiden vorgestellten Klassen bzw. Quellcodedateien ein ausführbares Programm ent-
steht, erfahren Sie im Abschnitt 2.2.

1.1.5 Zusammenfassung zum Abschnitt 1.1


Im Abschnitt 1.1 sollten Sie einen ersten Eindruck von der Software-Entwicklung mit Java gewin-
nen. Alle dabei erwähnten Konzepte der objektorientierten Programmierung und die technischen
Details der Realisierung in Java werden bald systematisch behandelt und sollten Ihnen daher im
Moment noch keine Kopfschmerzen bereiten. Trotzdem kann es nicht schaden, an dieser Stelle ei-
nige Kernaussagen von Abschnitt 1.1 zu wiederholen:
• Vor der Programmentwicklung findet die objektorientierte Analyse der Aufgabenstellung
statt. Dabei werden per Abstraktion die beteiligten Klassen identifiziert.
• Ein Programm besteht aus Klassen. Unsere Beispielprogramme zum Erlernen von elementa-
ren Java-Sprachelementen werden oft mit einer einzigen Klasse auskommen. Praxisgerechte
Programme bestehen in der Regel aus mehreren Klassen.
• Eine Klasse ist charakterisiert durch Eigenschaften (Felder) und Handlungskompetenzen
(Methoden).
• Eine Klasse dient in der Regel als Bauplan für Objekte, kann aber auch selbst aktiv werden
(Methoden ausführen und aufrufen).
• Ein Feld bzw. eine Methode ist entweder den Objekten einer Klasse oder der Klasse selbst
zugeordnet.
• In den Methodendefinitionen werden Algorithmen realisiert. Dabei kommen selbst erstellte
Klassen zum Einsatz, aber auch vordefinierte Klassen aus diversen Bibliotheken.
• Im Programmablauf kommunizieren die Akteure (Objekte und Klassen) durch den Aufruf
von Methoden miteinander, wobei in der Regel noch „externe Kommunikationspartner“
(z. B. Benutzer, andere Programme) beteiligt sind.
• Beim Programmstart wird die Startklasse vom Laufzeitsystem aufgefordert, die Methode
main() auszuführen. Ein Hauptzweck dieser Methode besteht oft darin, Objekte zu erzeugen
und somit „Leben auf die objektorientierte Bühne zu bringen“.
Abschnitt 1.2 Java-Programme ausführen 13

1.2 Java-Programme ausführen


Wer sich schon jetzt von der Nützlichkeit des im Abschnitt 1.1 vorgestellten Bruchadditionspro-
gramms überzeugen möchte, findet eine ausführbare Version an der im Vorwort angegebenen Stelle
im Ordner
...\BspUeb\Einleitung\Bruchaddition\Konsole

1.2.1 Java-Laufzeitumgebung installieren


Um das Programm auf einem Rechner ausführen zu können, muss dort eine Java Virtual Machine
(JVM), die auch als Java Runtime Environment (JRE) bezeichnet wird, mit hinreichend aktueller
Version (ab Java 8) installiert sein. Mit den technischen Grundlagen und Aufgaben dieser Ausfüh-
rungsumgebung für Java-Programme werden wir uns im Abschnitt 1.3.2 beschäftigen.
Um unter Windows festzustellen, ob eine JVM installiert ist, und welche Version diese besitzt, star-
tet man eine Eingabeaufforderung und schickt dort das folgende Kommando
>java -version
ab, z. B.:

Die Firma Oracle liefert seit ihrer Änderung ihrer Lizenzpolitik im Jahr 2019 keine langfristig durch
Updates unterstützte und frei verwendbare JVM mehr aus (siehe Abschnitt 1.3.5). Glücklicherweise
sind einige IT-Firmen in die Presche gesprungen. Dabei wird stets das umfassende Java Develop-
ment Kit (JDK) geliefert, das neben einer JVM z. B. auch den Quellcode der Java-
Standardbibliothek und einen Java-Compiler enthält. Damit geht es geht über den Bedarf eines An-
wenders von Java-Software hinaus, was aber bis auf ca. 100 MB verschwendeten Massenspeicher-
platz keine Nachteile hat.
Um eine Java-Laufzeitumgebung bequem und ohne Lizenzunsicherheit auf einen Windows-
Rechner zu befördern, kommt z. B. die vom Open Source - Projekt ojdkbuild auf der Webseite1
https://github.com/ojdkbuild/ojdkbuild
angebotene OpenJDK-Distribution von Java 8 (alias 1.8) in Frage:2
java-1.8.0-openjdk-1.8.0.302-1.b08.ojdkbuild.windows.x86_64.msi
Diese Distribution bietet folgende Vorteile.
• Keine lizenzrechtlichen Einschränkungen
• Long Term Support (LTS) bis Mai 2026
• Das im Kurs als Bibliothek für Programme mit grafischer Bedienoberfläche verwendete Ja-
vaFX (alias OpenJFX) ist eine Option im Installationsprogramm, also ohne separaten
Download bequem verfügbar.

1
Das Open Source - Projekt ojdkbuild wird von der Firma Red Hat gesponsert.
2
Eine in der Java-Szene seit Jahrzehnten gepflegte Marotte besteht in zwei parallelen Versionierungen. Für die Ver-
sion 8 erscheint in vielen Dateinamen die Version 1.8. Diese Spiel begann mit Java 2 (alias 1.2) im Jahr 1998. Mit
der Version 9 wurde die Doppel-Versionierung aufgegeben.
14 Kapitel 1 Einleitung

• Eine weitere Option im Installationsprogramm ist ein Hilfsprogramm, das nötigenfalls zum
Update auffordert. Unter Windows taucht im Infobereich das folgende Symbol auf, wenn
ein OpenJDK-Update ansteht. Nach einem Mausklick auf das Symbol erscheint ein Fenster
mit Download-Link, z. B.:

Eine heruntergeladene und ausgeführte MSI-Datei installierte die neue ojdkbuild-Version


und entfernt die alte.
Am 20.10.2021 wird auf der oben angegebenen Webseite auch die LTS-Version von Java 11 ange-
boten:

Die seit dem 14.9.2021 bei Oracle verfügbare LTS-Version JDK 17 wird vermutlich in Kürze auch
von Red Hat angeboten.
Obwohl das Alter gegen Java 8 (alias 1.8) spricht und jüngere LTS-Versionen verfügbar sind, be-
stehen Argumente für die oben beschriebene JDK 8 - Distribution:
• Java 8 ist nach einer aktuellen Umfrage der Firma JetBrains (Hersteller der im Kurs bevor-
zugten Entwicklungsumgebung IntelliJ IDEA) unter Entwicklern die Java-Version mit der
größten Verbreitung.1 Das liegt eventuell auch daran, dass in Java 9 mit dem Modulsystem
(siehe Kapitel 6) eine wesentlich Architekturveränderung Einzug gehalten hat, die von vie-
len Entwicklern und Anwendern noch mit Zurückhaltung betrachtet wird. Bei der Software-
Entwicklung kommt für uns das JDK 8 als Laufzeitumgebung dann in Frage, wenn wir uns
auf minimale Voraussetzungen auf der Kundenseite beschränken und keine JVM mit unserer
Anwendung ausliefern wollen. Um Neuerungen der Java-Technik (Programmiersprache,
Standardbibliothek, Laufzeitumgebung) nutzen zu können, werden wir aber auch die LTS -
Java-Version 17 verwenden (siehe Abschnitt 2.1).

1
https://www.jetbrains.com/lp/devecosystem-2021/java/
Abschnitt 1.2 Java-Programme ausführen 15

• In der aktuell ebenfalls verfügbaren ojdkbuild-Distribution mit JDK 11 LTS fehlen die GUI-
Bibliothek JavaFX und ein Programm, das automatisch auf anstehende Updates hinweist.
Diese Komponenten sind zwar grundsätzlich nachrüst- bzw. verzichtbar, doch das OpenJDK
8 - Paket aus dem ojdkbuild-Projekt macht den Einstieg in die Java-Technik besonders be-
quem.
• Die Firma Red Hat unterstützt das OpenJDK 8 LTS bis Mai 2026, das OpenJDK 11 LTS
hingegen nur bis Oktober 2024.
Nachdem die OpenJDK 8 - Installation per Doppelklick auf die heruntergeladene MSI-Datei gestar-
tet worden ist, meldet unter Windows 10 eventuell die SmartScreen-Funktion Bedenken gegen das
Installationsprogramm an:

Vorsichtige Menschen lassen in dieser Situation die Datei zunächst von einem Virenschutzpro-
gramm überprüfen. Weil es sich um ein großes Archiv mit ca. 70.000 Dateien handelt, nimmt die
Prüfung einige Zeit in Anspruch. Nach bestandenem Test klickt man zunächst auf den Link Weite-
re Informationen und dann auf den Schalter Trotzdem ausführen:

Nach der freundlichen Begrüßung im ersten Dialog des OpenJDK-Installationsprogramms


16 Kapitel 1 Einleitung

werden die Lizenzbedingungen vorgelegt:

Die vorgelegte
GNU GPL, Version 1991, mit CLASSPATH-Ausnahme
ist beim OpenJDK üblich und erlaubt eine liberale, auch kommerzielle Nutzung.1
Im Dialog Custom Setup sollten die OpenJFX Runtime und der Update Notifier aktiviert
werden:

1
https://github.com/ojdkbuild/ojdkbuild/blob/master/LICENSE, https://openjdk.java.net/legal/gplv2+ce.html
Abschnitt 1.2 Java-Programme ausführen 17

Außerdem kann nach einem Klick auf Browse der Installationsordner eingestellt werden. Es
spricht nichts dagegen, die Voreinstellung zu verwenden:
C:\Program Files\ojdkbuild\java-1.8.0-openjdk-1.8.0.302-1\
Nach Mausklicks auf Next und Install sowie einer positiven Antwort auf die Nachfrage der Benut-
zerkontensteuerung von Windows (UAC) ist die Installation schnell erledigt:

1.2.2 Konsolenprogramme ausführen


Nach der Installation einer Java-Laufzeitumgebung (JVM) machen wir uns endlich daran, das
Bruchadditionsprogramm zu starten. Kopieren Sie von der oben angegebenen Quelle
...\BspUeb\Einleitung\Bruchaddition\Konsole
die Dateien Bruch.class, Bruchaddition.class und Simput.class mit ausführbarem Java-Bytecode
(siehe Abschnitt 1.3.2) auf einen eigenen Datenträger.
Weil die Klasse Bruch wie viele andere im Manuskript verwendete Beispielklassen mit konsolen-
orientierter Benutzerinteraktion die nicht zur Java-Standardbibliothek gehörige Klasse Simput
verwendet, muss auch die Klassendatei Simput.class übernommen werden. Sobald Sie die zur Ver-
18 Kapitel 1 Einleitung

einfachung der Konsoleneingabe (Simple Input) für den Kurs entworfene Klasse Simput in eigenen
Programmen einsetzen sollen, wird sie näher vorgestellt. Im Abschnitt 2.2.4 lernen Sie eine Mög-
lichkeit kennen, die in mehreren Projekten benötigten class-Dateien zentral abzulegen und durch
eine passende Definition der Windows-Umgebungsvariablen CLASSPATH allgemein verfügbar zu
machen. Dann muss die Datei Simput.class nicht mehr in den Ordner eines Projekts kopiert wer-
den, um sie dort nutzen zu können.
Gehen Sie folgendermaßen vor, um die Klasse Bruchaddition zu starten:
• Öffnen Sie ein Konsolenfenster (auch Eingabeaufforderung genannt), z. B. so:
o Tastenkombination Windows + R
o Befehl cmd eintragen und mit OK ausführen lassen:

o
• Wechseln Sie zum Ordner mit den class-Dateien, z. B.:
>u:
>cd \Eigene Dateien\Java\BspUeb\Einleitung\Bruchaddition\Konsole
• Starten Sie die Java Runtime Environment über das Programm java.exe, und geben Sie als
Kommandozeilenargument die Startklasse an, wobei die Groß/Kleinschreibung zu beachten
ist:
>java Bruchaddition
Damit das zur Java-Laufzeitumgebung gehörende Programm java.exe wie im Beispiel ohne
Angabe des Installationsordners aufgerufen werden kann, muss der bin-Unterordner der O-
penJDK-Installation
C:\Program Files\ojdkbuild\java-1.8.0-openjdk-1.8.0.302-1\bin
in die Windows-Umgebungsvariable PATH eingetragen worden sein, was bei der im Ab-
schnitt 1.2.1 beschriebenen Installation automatisch geschieht. Wie man nötigenfalls einen
fehlenden PATH-Eintrag manuell vornehmen kann, wird im Abschnitt 2.2.2 beschrieben.
Ab jetzt sind Bruchadditionen kein Problem mehr:
Abschnitt 1.2 Java-Programme ausführen 19

1.2.3 Ausblick auf Anwendungen mit grafischer Bedienoberfläche


Das seit dem Abschnitt 1.1 vorgestellte Beispielprogramm arbeitet der Einfachheit halber mit einer
konsolenorientierten Ein- und Ausgabe. Nachdem wir im Kurs bzw. Manuskript im Rahmen einfa-
cher Konsolenprogramme grundlegende Java-Sprachelemente kennengelernt haben, werden wir uns
natürlich auch mit der Programmierung von grafischen Bedienoberflächen beschäftigen. Im folgen-
den Programm zur Addition von Brüchen wird ebenfalls die Klasse Bruch verwendet, wobei an-
stelle ihrer Methoden frage() und zeige() jedoch grafikorientierte Techniken zum Einsatz
kommen:

Mit dem Quellcode zur Gestaltung der grafischen Bedienoberfläche könnten Sie im Moment noch
nicht allzu viel anfangen. Am Ende des Kurses bzw. nach der Lektüre des Manuskripts werden Sie
derartige Anwendungen aber mit Leichtigkeit entwickeln, zumal die Erstellung grafischer Bedien-
oberflächen durch die GUI-Technologie JavaFX (alias OpenJFX) und den Fensterdesigner Scene
Builder erleichtert wird (siehe Abschnitt 2.5).
Zum Ausprobieren des Programms startet man mit Hilfe einer Java-Laufzeitumgebung mit Open-
JFX-Unterstützung (vgl. Abschnitt 1.2.1 zur Installation) aus dem Ordner
...\BspUeb\Einleitung\Bruchaddition\JavaFX\out\production\Bruchaddition
die Klasse Bruchaddition:

Um das Programm unter Windows per Doppelklick starten zu können, legt man eine Verknüpfung
zum konsolenfreien JVM-Startprogramm javaw.exe an, z. B. über das Kontextmenü zu einem
Fenster des Windows-Explorers (Befehl Neu > Verknüpfung):

Weil das Programm keine Konsole benötigt, sondern ein Fenster als Bedienoberfläche anbietet,
verwendet man bei der Verknüpfungsdefinition als JVM-Startprogramm die Variante javaw.exe
(mit einem w am Ende des Namens). Bei Verwendung von java.exe als JVM-Startprogramm würde
zusätzlich zum grafischen Bruchadditionsprogramm ein leeres Konsolenfenster erscheinen:
20 Kapitel 1 Einleitung

Während das Konsolenfenster beim normalen Programmablauf leer bleibt, erscheinen dort bei einen
Laufzeitfehler hilfreiche diagnostische Ausgaben. Daher ist ein Programmstart mit Konsolenfenster
(per java.exe) bei der Fehlersuche durchaus sinnvoll.
Im nächsten Dialog des Assistenten für neue Verknüpfungen trägt man den gewünschten Namen
der Link-Datei ein:

Im Eigenschaftsdialog zur fertiggestellten Verknüpfungsdatei ergänzt man in Feld Ziel hinter


javaw.exe den Namen der Startklasse und trägt im Feld Ausführen in den Ordner ein, in dem sich
die Startklasse befindet, z. B.:
Abschnitt 1.2 Java-Programme ausführen 21

Nun genügt zum Starten des Programms ein Doppelklick auf die Verknüpfung:

Beim eben beschriebenen Verfahren muss die verwendete JVM eine JavaFX-Unterstützung enthal-
ten, was z. B. nach den im Abschnitt 1.2.1 beschriebenen Installation der OpenJDK-Distribution 8
aus dem ojdkbuild-Projekt der Fall ist. Ist z. B. die ohne JavaFX (alias OpenJFX) ausgelieferte
OpenJDK-Distribution 17.0.1 der Firma Oracle gemäß Abschnitt 2.1 im Ordner
C:\Program Files\Java\OpenJDK-17
und die hier
https://gluonhq.com/products/javafx/
frei verfügbare JavaFX-Distribution 17.0.1 der Firma Gluon gemäß Abschnitt 2.5 im Ordner1
C:\Program Files\Java\OpenJFX-SDK-17
installiert, dann taugt das folgende Kommando zum Starten des Programms
>"C:\Program Files\Java\OpenJDK-17\bin\javaw.exe" --module-path "C:\Program
Files\Java\OpenJFX-SDK-17\lib" --add-modules javafx.controls,javafx.fxml
Bruchaddition
aus einer Konsole, die auf den Ordner mit der Datei Bruchaddition.class positioniert ist. Den An-
wendern sollte dieses Kommando z. B. mit Hilfe einer Verknüpfung erspart werden.
Professionelle Java-Programme werden oft als Java-Archivdatei (mit der Namenserweiterung .jar,
siehe Abschnitte 6.1.3 und 6.2.6) ausgeliefert und sind unter Windows nach einer korrekten JVM-
Installation über einen Doppelklick auf diese Datei zu starten. Im Ordner
...\BspUeb\Einleitung\Bruchaddition\JavaFX\out\artifacts\Bruchaddition
finden Sie die (von IntelliJ erstellte) Datei Bruchaddition.jar mit dem grafischen Bruchadditions-
programm. Es kann auf einem Windows-Rechner per Doppelklick gestartet werden, wenn z. B. ...
• die im Abschnitt 1.2.1 beschriebene Installation der OpenJDK 8 - Distribution aus dem
ojdkbuild-Projekt ausgeführt wurde (inklusive OpenJFX),
• in der Windows-Registry ...
o der Schlüssel
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\.jar
im Standardwert den Eintrag jarfile besitzt,
o der Schlüssel
HKEY_LOCAL_MACHINE\SOFTWARE\Classes\jarfile\shell\open\command
im Standardwert den folgenden Eintrag besitzt:
"C:\Program Files\ojdkbuild\java-1.8.0-openjdk-1.8.0.302-1\bin\javaw.exe" -jar "%1" %*
Professionelle Java-Programme bringen oft eine JVM mit (z. B. basierend auf dem OpenJDK) und
sind damit nicht darauf angewiesen, dass sich auf dem Kundenrechner eine JVM befindet. Damit
bürden sich die Anbieter professioneller Java-Programme aber die Pflicht auf, Sicherheitsupdates
für die integrierte JVM zu liefern. Seit Java 9 ermöglicht das JPMS (siehe Abschnitt 6.2) die Erstel-
lung einer angepassten modularen Laufzeitumgebung, die ausschließlich vom Programm benötigte
Module enthält (siehe Abschnitt 6.2.8). Das spart viel Platz und reduziert die Gefahr, von einer ent-
deckten Sicherheitslücke betroffen zu sein. Für kleinere Programme bleibt es aber eine sinnvolle
Option, dem Anwender der (meist kostenlosen) Software die Installation und Wartung einer JVM
zu überlassen (siehe Abschnitt 1.2.1). Dann entfällt für den Programmanbieter die Verpflichtung,

1
Im Manuskript werden die Bezeichnungen JavaFX und OpenJFX synonym verwendet.
22 Kapitel 1 Einleitung

auf Sicherheitsprobleme in der Java-Laufzeitumgebung zu reagieren. Auch in der Java - Software-


Technik kommen Sicherheitsprobleme vor, allerdings vergleichsweise selten (siehe dazu Abschnitt
1.3.6.3).

1.2.4 Ausführung auf einer beliebigen unterstützten Plattform


Dank der Portabilität (Binärkompatibilität) von Java können wir z. B. das im letzten Abschnitt vor-
geführte, unter Windows entwickelte grafische Bruchadditionsprogramm auch unter anderen Be-
triebssystemen ausführen, z. B. unter macOS. Wird die am Ende von Abschnitt 1.2.3 erwähnte Da-
tei Bruchaddition.jar auf einen Mac mit installierter Java-Laufzeitumgebung ab Version 8 (inkl.
JavaFX-Unterstützung) kopiert, dann lässt sich das Programm dort per Doppelklick starten. Es er-
scheint die vertraute Bedienoberfläche mit dem macOS - üblichen Fensterdekor:

1.3 Architektur und Eigenschaften von Java-Software


Bisher war von der Programmiersprache Java und gelegentlich etwas ungenau vom Laufzeitsystem
die Rede. Nach der Lektüre dieses Abschnitts werden Sie ein gutes Verständnis von den drei Säu-
len der Java-Softwaretechnik besitzen:
• Die Programmiersprache mit dem Compiler, der Quellcode in Bytecode (siehe Abschnitt
1.3.2) übersetzt
• Die Standardklassenbibliothek mit ausgereiften Lösungen für (fast) alle Routineaufgaben
• Die Java Virtual Maschine (JVM) mit zahlreichen Funktionen bei der Ausführung von
Bytecode (z. B. optimierender JIT-Compiler, Klassenlader, Sicherheitsüberwachung)
Im Abschnitt 1.3 stehen technische Merkmale der Java-Software in Vordergrund, doch werden auch
Lizenzfragen geklärt.

1.3.1 Herkunft und Bedeutung der Programmiersprache Java


Weil auf der indonesischen Insel Java eine auch bei Programmierern hoch geschätzte Kaffees-Sorte
wächst, kam die in diesem Manuskript vorzustellende Programmiersprache Gerüchten zufolge zu
ihrem Namen.
Java wurde ab 1990 von einem Team der Firma Sun Microsystems unter Leitung von James Gos-
ling entwickelt. Nachdem erste Pläne zum Einsatz in Geräten aus dem Bereich der Unterhaltungs-
elektronik (z. B. Set-Top-Boxen für TV-Geräte) wenig Erfolg brachten, orientierte man sich stark
am boomenden Internet. Das zuvor auf die Darstellung von Texten und Bildern beschränkte WWW
(Word Wide Web) wurde um die Möglichkeit bereichert, kleine Java-Programme (Applets genannt)
von einem Server zu laden und ohne lokale Installation im Fenster des Internet-Browsers auszufüh-
ren. Ein erster Durchbruch gelang 1995, als die Firma Netscape die Java-Technologie in die Versi-
on 2.0 ihres WWW-Navigators integrierte. Kurze Zeit später wurden mit der Version 1.0 des Java
Development Kits Werkzeuge zum Entwickeln von Java-Applets und -Anwendungen frei verfüg-
bar.
Abschnitt 1.3 Architektur und Eigenschaften von Java-Software 23

Mittlerweile hat sich Java als sehr vielseitig einsetzbare Programmiersprache etabliert, die als Stan-
dard für die plattformunabhängige Entwicklung gelten kann und einen hohen Verbreitungsgrad be-
sitzt. Laut der aktuellen Entwicklerbefragung durch die Firma JetBrains ist Java in Deutschland die
meistbenutzte Programmiersprache. Auch in anderen Rankings belegt Java stets vordere Plätze:
• Populäre Programmiersprachen in Deutschland (2021, Java-Rangplatz: 1)
https://www.jetbrains.com/lp/devecosystem-2021/
• Nachfrage nach Programmiersprachen auf dem deutschen Arbeitsmarkt (2021, Platz 1)
https://www.get-in-it.de/magazin/bewerbung/it-skills/welche-programmiersprache-lernen
• TIOBE Programming Community Index (Oktober 2021, Java-Rangplatz: 3)
http://www.tiobe.com/index.php/content/paperinfo/tpci/index.html
• PYPL PopularitY of Programming Language (Oktober 2021, Java-Rangplatz: 2)
http://pypl.github.io/PYPL.html
• RedMonk Programming Language Rankings (Juni 2021, Java-Rangplatz: 3)
https://redmonk.com/sogrady/2021/08/05/language-rankings-6-21/
• IEEE Spectrum: The Top Programming Languages (2021, Java-Rangplatz: 2)
https://spectrum.ieee.org/top-programming-languages/
Außerdem ist Java relativ leicht zu erlernen und daher für den Einstieg in die professionelle Pro-
grammierung eine gute Wahl.
Die Java-Designer haben sich stark an den Programmiersprachen C und C++ orientiert, sodass sich
Umsteiger von diesen sowohl im Windows- als auch im Linux/UNIX - Bereich verbreiteten Spra-
chen schnell in Java einarbeiten können. Wesentliche Ziele bei der Weiterentwicklung waren Ein-
fachheit, Robustheit, Sicherheit und Portabilität.

1.3.2 Quellcode, Bytecode und Maschinencode


Im Abschnitt 1.1 haben Sie Java als eine Programmiersprache kennengelernt, die Ausdrucksmittel
zur Modellierung des Anwendungsbereichs und zur Formulierung von Algorithmen bereitstellt.
Unter einem Programm wurde dabei der vom Entwickler zu formulierende Quellcode verstanden.
Während Sie derartige Texte bald mit Leichtigkeit lesen und begreifen werden, kann die CPU
(Central Processing Unit) eines Rechners nur einen maschinenspezifischen Satz von Befehlen ver-
stehen, die als Folge von Nullen und Einsen (= Maschinencode) formuliert werden müssen. Die
ebenfalls CPU-spezifische Assembler-Sprache stellt eine für Menschen besser lesbare Form des
Maschinencodes dar. Mit dem Assembler- bzw. Maschinenbefehl
mov eax, 4
einer CPU aus der x86-Familie wird z. B. der Wert 4 in das EAX-Register (ein Speicherort im Pro-
zessor) geschrieben. Die CPU holt sich einen Maschinenbefehl nach dem anderen aus dem Haupt-
speicher und führt ihn aus, wobei heutzutage (2021) die CPU eines handelsüblichen Arbeitsplatz-
rechners (mit GHz-Taktfrequenz und zahlreichen Kernen/Threads) mehrere hundert Milliarden Be-
fehle pro Sekunde (Instructions Per Second, IPS) schafft.1 Ein Quellcode-Programm muss also erst
in Maschinencode übersetzt werden, damit es von einem Rechner ausgeführt werden kann. Dies
geschieht bei Java aus Gründen der Portabilität und Sicherheit in zwei Schritten:

1
https://de.wikipedia.org/wiki/Instruktionen_pro_Sekunde
https://en.wikipedia.org/wiki/Instructions_per_second
24 Kapitel 1 Einleitung

Übersetzen: Quellcode → Bytecode


Der (z. B. mit einem beliebigen Texteditor verfasste) Quellcode wird vom Java-Compiler in den
Bytecode übersetzt. Dieser besteht aus den Befehlen einer von der Firma Sun Microsystems bzw.
vom Nachfolger Oracle definierten virtuellen Maschine, die sich durch ihren vergleichsweise ein-
fachen Aufbau gut auf aktuelle Hardware-Architekturen abbilden lässt. Wenngleich der Bytecode
von den heute üblichen Prozessoren noch nicht direkt ausgeführt werden kann, hat er doch bereits
die meisten Verarbeitungsschritte auf dem Weg vom Quell- zum Maschinencode durchlaufen. Sein
Name geht darauf zurück, dass die Instruktionen der virtuellen Maschine jeweils genau ein Byte (=
8 Bit) lang sind.
Aufgrund der geistigen Leistung der Java-Designer dürfen sich die Software-Entwickler Klassen
und Objekte vorstellen, die durch ihre freundliche und kompetente Interaktion auf der objektorien-
tierten Bühne in Kooperation mit dem Benutzer den Programmablauf voranbringen. Dieses mit un-
serer Alltagserfahrung verwandte Modell hilft uns bei der Lösung komplexer Aufgaben durch
Computer-Programme. Tatsächlich werden in einem Computer aber elementare binäre Daten zwi-
schen Speicherstationen bewegt und durch Operationen modifiziert:
• durch arithmetischen Operationen (Addition, Subtraktion, Multiplikation, Division)
• durch logische Operationen (OR, AND, NOT, exklusives OR)
Vom objektorientierten Quellcode bis zum Bytecode mit den Befehlen einer virtuellen Maschine,
die elementare Operationen auf binäre Daten anwendet, hat ein Java-Compiler eine immense Über-
setzungsarbeit zu leisten. Wir bewegen uns meist im objektorientierten Modell, müssen aber gele-
gentlich doch die reale Computer-Technik berücksichtigen (z.B. die Anzahl der vorhandenen CPU-
Kerne bei der Multithreading-Programmierung für die parallele Programmausführung).
Ansätze zur Entwicklung von realen Java-Prozessoren, die Bytecode direkt (in Hardware) ausfüh-
ren können, haben bislang keine nennenswerte Bedeutung erlangt. Die CPU-Schmiede ARM, deren
Prozessoren auf mobilen und eingebetteten Systemen stark verbreitetet sind, hat eine Erweiterung
namens Jazelle DBX (Direct Bytecode eXecution) entwickelt, die zumindest einen großen Teil der
Bytecode-Instruktionen in Hardware unterstützt.1 Allerdings macht das auf Geräten mit ARM-
Prozessor oft eingesetzte (und überwiegend mit der Ausführung von Java-Bytecode beschäftigte)
Betriebssystem Android der Firma Google von Jazelle DBX keinen Gebrauch. In aktuellen ARM-
Prozessoren spielt die mittlerweile als veraltet und überflüssig betrachtete Jazelle-Erweiterung keine
Rolle mehr (Langbridge 2014, S. 48).
Das kostenlos verfügbare Java Development Kit (JDK), das von der Firma Oracle und der Java-
Community gemeinsam entwickelt wird, enthält einen Compiler (unter Windows in der Datei ja-
vac.exe), den auch Java-Entwicklungsumgebungen im Hintergrund einsetzen, z. B. die im Kurs
bevorzugte Entwicklungsumgebung IntelliJ IDEA. Die OpenJDK 8 - Distribution aus dem
ojdkbuild-Projekt haben wir schon im Abschnitt 1.2.1 installiert. Im Abschnitt 2.1 folgt noch die
OpenJDK-Version 17.
Quellcode-Dateien tragen in Java die Namenserweiterung .java, Bytecode-Dateien die Erweiterung
.class.

Interpretieren: Bytecode → Maschinencode


Abgesehen von den seltenen Systemen mit realem Java-Prozessor muss für jede Betriebssys-
tem/CPU - Kombination mit Java-Unterstützung ein (naturgemäß plattformabhängiger) Interpreter
erstellt werden, der den Bytecode zur Laufzeit in die jeweilige Maschinensprache übersetzt. Man
verwendet die eben im Sinne eines Quasi-Hardware - Designs eingeführte Bezeichnung virtuelle
Maschine (Java Virtual Machine, JVM) auch für die an der Ausführung von Java-Bytecode betei-

1
http://en.wikipedia.org/wiki/Jazelle
Abschnitt 1.3 Architektur und Eigenschaften von Java-Software 25

ligte Software, also sozusagen für die Emulation des Java-Prozessors in Software. Man benötigt
also für jede reale Maschine eine partiell vom jeweiligen Betriebssystem und von der konkreten
CPU abhängige JVM, um den Java-Bytecode auszuführen. Diese Software wird meist in der Pro-
grammiersprache C++ realisiert.
Für viele Desktop-Betriebssysteme (Linux, macOS, Solaris, Windows) ist eine Java-
Laufzeitumgebung kostenlos verfügbar. Über eine lange Zeit haben die Firma Sun und der Aufkäu-
fer Oracle ein zur Ausführung, aber nicht zur Entwicklung von Java-Programmen geeignetes Soft-
warepaket namens Java Runtime Environment (JRE) kostenlos zur Verfügung gestellt und durch
Updates unterstützt. Für Entwickler wurde das Java Development Kit (inklusive Compiler) kos-
tenlos angeboten. Mit Java 8 endet die JRE-Verteilung durch Oracle, und seit April 2019 darf die
JRE 8 der Firma Oracle nur noch für private Zwecke kostenlos genutzt werden. Beginnend mit der
Java-Version 9 ist nur noch das JDK-Paket verfügbar, das aber auch eine Java-Laufzeitumgebung
enthält. Java-Entwickler sind mit dem JDK gut bedient, und Java-Anwender müssen lediglich einen
irrelevanten Massenspeicher-Mehrverbrauch hinnehmen. Außerdem liefert die Firma Oracle nur
noch 6 Monate lang (bis zum Erscheinen der nächsten Hauptversion) kostenlose Updates für ein
JDK. Einige Java-Versionen erhalten aber über die 6 Monate hinaus eine mindestens 5 Jahre dau-
ernde Langzeitunterstützung (LTS), also eine Versorgung durch Updates. Das gilt aktuell für die
Versionen 8, 11 und 17. Während bei Oracle der LTS auf die private Nutzung beschränkt ist, ge-
währen andere Firmen den LTS auch für kommerziell genutzte JDK-Installationen (siehe Abschnitt
1.3.5).
Die wichtigsten Komponenten der Java-Laufzeitumgebung sind:
• JVM
Neben der Bytecode-Übersetzung erledigt die JVM bei der Ausführung eines Java-
Programms noch weitere Aufgaben, z. B.:
o Der Klassenlader befördert die vom Programm benötigten Klassen in den Speicher
und nimmt dabei eine Bytecode-Verifikation vor, um potentiell gefährliche Aktionen
zu verhindern.
o Die Speicherverwaltung entfernt automatisch die im Programmablauf überflüssig
gewordenen Objekte (Garbage Collection).
• Java-Standardbibliothek mit Klassen für (fast) alle Routineaufgaben (siehe Abschnitt 1.3.3)
Wie Sie bereits aus dem Abschnitt 1.2 wissen, startet man unter Windows mit java.exe bzw. ja-
vaw.exe die Ausführungsumgebung für ein Java-Programm (mit Konsolen- bzw. Fensterbedienung)
und gibt als Parameter die Startklasse des Programms an.
Mittlerweile kommen bei der Ausführung von Java-Programmen leistungssteigernde Techniken
(Just-in-Time - Compiler, HotSpot - Compiler mit Analyse des Laufzeitverhaltens) zum Einsatz,
die die Bezeichnung Interpreter fraglich erscheinen lassen. Allerdings ändert sich nichts an der
Aufgabe, aus dem plattformunabhängigen Bytecode den zur aktuellen CPU passenden Maschinen-
code zu erzeugen. So wird wohl keine Verwirrung gestiftet, wenn in diesem Manuskript weiterhin
vom Interpreter die Rede ist.
In der folgenden Abbildung sind die beiden Übersetzungen auf dem Weg vom Quell- zum Maschi-
nencode durch den Compiler javac.exe und den Interpreter java.exe, die beide im JDK enthalten
sind, am Beispiel des Bruchrechnungsprojekts (vgl. Abschnitt 1.1) im Überblick zu sehen:
26 Kapitel 1 Einleitung

Quellcode Bytecode Maschinencode

public class Bruch {


... Bruch.class
}
Interpreter
public class Simput {
Compiler, (virtuelle
... z.B.
} Simput.class Maschine), Maschinencode
javac.exe
z.B.
class Bruchaddition {
... java.exe
}
Bruchaddition.class

Bibliotheken
(z.B. Java-API)

Dank der Plattformunabhängigkeit von Java lässt sich der Bytecode unter verschiedenen Betriebs-
systemen ausführen:

Interpreter ARM
für Linux Maschinencode

Compiler,
Interpreter M1
Quellcode z.B. Bytecode
für macOS Maschinencode
javac.exe

Interpreter x86
für Windows Maschinencode

Es wäre nicht grob falsch, auch das Smartphone-Betriebssystem Android in die Abbildung aufzu-
nehmen, wenngleich dort eine andere Java-Klassenbibliothek (siehe Abschnitt 1.3.3) und eine alter-
native Bytecode-Technik zum Einsatz kommen (siehe z. B. Baltes-Götz 2018). Für die verwendete
Bytecode-Technik muss sich ein Anwendungsprogrammierer kaum interessieren. Die Besonderhei-
ten eines Mobil-Betriebssystems wirken sich allerdings auf die Klassenbibliothek und damit auf den
Quellcode aus.

1.3.3 Standardklassenbibliothek
Damit die Programmierer nicht das Rad (und ähnliche Dinge) ständig neu erfinden müssen, bietet
die Java-Plattform eine Standardbibliothek mit fertigen Klassen für nahezu alle Routineaufgaben,
die oft als API (Application Program Interface) bezeichnet wird. Im Manuskript werden Sie zahl-
reiche API-Klassen kennenlernen; eine vollständige Behandlung ist wegen des enormen Umfangs
unmöglich und auch nicht erforderlich.
Bevor man selbst eine Klasse oder Methode entwickelt, sollte man unbedingt die Standardbiblio-
thek auf die Existenz einer Lösung untersuchen, denn die Lösungen in der Standardbibliothek ...
• sind leistungsoptimiert und sorgfältig getestet,
• werden ständig weiterentwickelt.
Durch die Verwendung der Standardbibliothek steigert man in der Regel die Qualität der entstehen-
den Software, spart viel Zeit und verbessert auch noch die Lesbarkeit des Quellcodes, weil die Lö-
sungen der Standardbibliothek vielen Entwicklern vertraut sind (Bloch 2018, S. 267ff).
Abschnitt 1.3 Architektur und Eigenschaften von Java-Software 27

Wir halten fest, dass die Java-Technologie einerseits auf einer Programmiersprache basiert, dass
andererseits aber die Funktionalität im Wesentlichen von einer umfangreichen Standardbibliothek
beigesteuert wird, deren Klassen in jeder virtuellen Java-Maschine zur Verfügung stehen.
Die Java-Designer waren bestrebt, sich auf möglichst wenige, elementare Sprachelemente zu be-
schränken und alle damit formulierbaren Konstrukte in der Standardbibliothek unterzubringen. Es
resultierte eine sehr kompakte Sprache (siehe Gosling et al. 2021), die nach ihrer Veröffentlichung
im Jahr 1995 lange Zeit nahezu unverändert blieb.
Neue Funktionalitäten werden in der Regel durch eine Erweiterung der Java-Klassenbibliothek rea-
lisiert, sodass hier erhebliche Änderungen stattfinden. Einige Klassen sind mittlerweile als depre-
cated (überholt, nicht mehr zu benutzen) eingestuft worden. Gelegentlich stehen für eine Aufgabe
verschiedene Lösungen aus unterschiedlichen Entwicklungsstadien zur Verfügung (z. B. bei den
Multithreading-Lösungen für die parallele Programmausführung).
Mit der 2004 erschienenen Version 5 (alias 1.5) hat auch die Programmiersprache Java substantielle
Veränderungen erfahren (z. B. generische Typen, Auto-Boxing). Auch die Version 8 (alias 1.8) hat
mit der funktionalen Programmierung (den Lambda-Ausdrücken) eine wesentliche Erweiterung der
Programmiersprache Java gebracht.
In Kurs bzw. Manuskript steht zunächst die Programmiersprache Java im Vordergrund. Mit wach-
sender Kapitelnummer geht es aber auch darum, wichtige Pakete der Standardbibliothek mit Lö-
sungen für Routineaufgaben kennenzulernen, z. B.:
• Kollektionen zur Verwaltung von Listen, Mengen oder (Schlüssel-Wert) - Tabellen
• Lesen und Schreiben von Dateien
• Multithreading
Neben der sehr umfangreichen Standardbibliothek, die integraler Bestandteil der Java-Plattform ist,
sind aus anderen Quellen unzählige Java-Klassen für diverse Aufgaben verfügbar.

1.3.4 Java-Editionen für verschiede Einsatzszenarien


Weil die Java-Plattform so mächtig und vielgestaltig geworden ist, wurden drei Editionen für spezi-
elle Einsatzfelder definiert, wobei sich vor allem die jeweiligen Standardklassenbibliotheken unter-
scheiden:
• Java Standard Edition (JSE) zur Entwicklung von Software für Arbeitsplatzrechner
Darauf wird sich das Manuskript beschränken.
• Java bzw. Jakarta Enterprise Edition (JEE) für unternehmensweite oder serverorientierte
Lösungen
Bei der Java Enterprise Edition (JEE) kommt exakt dieselbe Programmiersprache wie bei
der Java Standard Edition (JSE) zum Einsatz. Für die erweiterte Funktionalität sorgt eine
entsprechende Variante der Standardklassenbibliothek. Beide Editionen verfügen über eine
eigenständige Versionierung (im Oktober 2021: JSE 17 und JEE 9). Die JEE ist im Herbst
2017 von der Firma Oracle an die Open Source Community (vertreten durch die Eclipse
Foundation) übergeben worden und heißt seitdem Jakarta EE.1 Als Alternative zur JEE für
die Entwicklung von Java-Unternehmenslösungen hat sich das Spring-Framework etabliert.2

1
https://jakarta.ee/
2
https://de.wikipedia.org/wiki/Spring_(Framework)
28 Kapitel 1 Einleitung

• Java Micro Edition (JME) für Kommunikationsgeräte und eingebettete Lösungen


Diese Edition wurde einst für Mobiltelefone mit beschränkter Leistung konzipiert. Bei heu-
tigen Smartphones kann aber von eingeschränkter Leistung kaum noch die Rede sein, und
die JME ist dementsprechend ins Hintertreffen geraten. Neben großer Skepsis zu den Über-
lebenschance der JME gibt es Bestrebungen zu einer Neuausrichtung für den Einsatz bei
eingebetteten Lösungen (Stichwort: Internet der Dinge).1 Die aktuelle Version 8.3 stammt
aus dem Jahr 2018.
Wir werden uns im Manuskript weder mit der JEE noch mit der JME beschäftigen, doch sind er-
worbene Java-Programmierkenntnisse natürlich dort uneingeschränkt verwendbar, und elementare
Klassen der JSE-Standardbibliothek sind auch in den anderen Editionen verfügbar.
Weil sich die Standardklassenbibliotheken der Editionen unterschieden, muss man z. B. vom JSE-
API sprechen, wenn man die JSE-Standardbibliothek meint. Im Manuskript, das sich auf die JSE
beschränkt, wird gelegentlich für das JSE-API etwas ungenau die Bezeichnung Java-API verwen-
det.
Im Marktsegment der Smartphones und Tablet-Computer hat sich eine Entwicklung vollzogen, die
die ursprüngliche Konzeption der Java-Editionen durcheinander gewirbelt hat. Einfache Mobiltele-
fone wurden von Smartphones mit GHz-Prozessoren verdrängt. Während die Firma Apple bisher in
ihrem Betriebssystemen für Smartphones (iOS) und Tablets (iPadOS) keine Java-Unterstützung
bietet, hat der Konkurrent Google in seinem Smartphone - und Tablet - Betriebssystem Android
Java lange als Standardsprache zur Anwendungsentwicklung eingesetzt. Neuerdings tendiert
Google zur Programmiersprache Kotlin, die vom IntelliJ-Urheber JetBrains entwickelt wird, doch
ist kein Ende des Java-Supports in Android abzusehen. Kotlin wird wie Java in Bytecode für eine
virtuelle Maschine übersetzt, sodass eine hohe Kompatibilität zwischen den beiden Programmier-
sprachen besteht. In Android kommt eine alternative Bytecode-Technik zum Einsatz mit einer vir-
tuellen Maschine namens Dalvik (bis Android 4.4) bzw. ART (seit Android 5.0).2
Es spricht für das Potential von Java, dass diese Sprache auch eine sehr wichtige Rolle bei der Ent-
wicklung von Android-Apps spielt. Android trägt erheblich zur Attraktivität von Java bei, denn
Android besitzt auf dem Markt für Smartphone-Betriebssysteme einen Marktanteil von über 70%3
und ist auch auf dem Tablet-Markt mit einem Anteil von ca. 50% erfolgreich.4 Somit ist Android
eine sehr relevante Plattform für die klientenseitige Java-Programmierung. Mit den Lernerfahrun-
gen aus dem Kurs bzw. Manuskript können Sie zügig in die Software-Entwicklung für Android
einsteigen, müssen sich aber mit einer speziellen Software-Architektur auseinandersetzen, die zum
Teil aus der Smartphone-Hardware resultiert (z. B. kleines Display, Zwang zum Energiesparen we-
gen der begrenzten Akkukapazität). Auch zur Einführung in die Entwicklung von Android-Apps in
Java bietet das ZIMK ein Manuskript an (Baltes-Götz 2018).

1
https://www.oracle.com/java/technologies/javame-embedded/javame-embedded-getstarted.html
2
Die in Smartphone-CPUs mit ARM-Design vorhandene reale Java-Maschine namens Jazelle DBX wird von Andro-
id ignoriert und von der Prozessor-Schmiede ARM mittlerweile als veraltet und überflüssig betrachtet (Langbridge
2014, S. 48). Aktuelle ARM-Prozessoren setzen auf einen Befehlssatz namens ThumbEE, der sich gut für die JIT -
Übersetzung von Bytecode in Maschinencode eignet.
3
https://www.statista.com/statistics/272698/global-market-share-held-by-mobile-operating-systems-since-2009/
4
https://gs.statcounter.com/os-market-share/tablet/worldwide
Abschnitt 1.3 Architektur und Eigenschaften von Java-Software 29

1.3.5 Update- und Lizenzpolitik der Firma Oracle


Seit Java 7 ist das OpenJDK (mit Compiler, Laufzeitumgebung und Standardbibliothek) die offizi-
elle Referenzimplementation der Java Standard Edition. An der Weiterentwicklung ist neben der
Firma Oracle auch die Java-Community beteiligt, zu der u. a. zahlreiche namhafte Firmen gehören
(z. B. Red Hat1, IBM2, Microsoft3). Das OpenJDK steht unter der liberalen (GPLv2 & CPE) - Li-
zenz und darf frei verwendet werden.4
Allerdings ist die OpenJDK-Unterstützung durch Oracle auf die 6 Monate bis zum Erscheinen der
nächsten Java-Hauptversion beschränkt. Von der Firma Oracle ist also stets ein aktuelles und siche-
res OpenJDK zu beziehen, doch muss man alle 6 Monate auf eine neue Hauptversion umsteigen.
Ein problemloses Update ist zwar wahrscheinlich, aber nicht garantiert. Wird das OpenJDK zu-
sammen mit einer eigenen Anwendung ausgeliefert, muss den Kunden ein Update-Verfahren auf
die neue Java-Hauptversion angeboten werden.
OpenJDK-Hauptversionen erscheinen seit der Version 9 in einem halbjährlichen Release-Zyklus,
um die Weiterentwicklung zu beschleunigen.5 Die jeweils aktuelle Version wird nur ein halbes Jahr
lang (bis zum Erscheinen der nächsten Hauptversion) mit Updates versorgt. So sind wir aktuell
(Oktober 2021) bei Version 17 angekommen.
Für einige Java-Versionen wird ein Long Term Support (LTS) zugesichert (aktuell für die Versio-
nen 8, 11 und Version 17), sodass die Versorgung mit (Sicherheits-)Updates über einen Zeitraum
von mindestens 5 Jahren garantiert ist. Damit bestehen für Entwickler und Anwender stabile Ver-
hältnisse, und es fallen lediglich (Sicherheits-)Updates ohne Kompatibilitätsrisiko an. Während die
Firma Oracle bei LTS-Versionen nach Ablauf des 6-monatigen Standard-Supports für die nicht-
private Verwendung Lizenzgebühren verlangt, versorgen andere Firmen die LTS-Versionen ohne
Kosten und Lizenzeinschränkungen mit Updates. In der Regel werden dabei verschiedene Betriebs-
systeme unterstützt und bequeme Installationsprogramme geliefert (siehe Abschnitt 2.1.2).
Java ist trotz der neuen Lizenzpolitik der Firma Oracle eine frei verfügbare Entwicklungsplattform,
wenngleich der Zugriff auf eine mit Updates versorgte Version nun für Entwickler und Anwender
etwas mehr Aufmerksamkeit erfordert. Es ist in der IT-Industrie durchaus üblich, dass mit Open
Source - Software Geld verdient wird, und man kann es der Firma Oracle nicht verdenken, dass sie
sich auch um ihre Bilanzen kümmert.

1.3.6 Eigenschaften von Java-Software


In diesem Abschnitt werden zentrale Eigenschaften der Java-Software beschrieben, wobei Vorgriffe
auf später ausführlich behandelte Themen nicht zu vermeiden sind.

1.3.6.1 Objektorientierung mit funktionalen Erweiterungen


Java wurde als objektorientierte Sprache konzipiert und erlaubt im Unterschied zu hybriden Spra-
chen wie C++ oder Delphi außerhalb von Klassendefinitionen keine Anweisungen. In unserem Ein-
leitungsbeispiel wurde einiger Aufwand in Kauf genommen, um einen realistischen Eindruck von
objektorientierter Programmierung (OOP) zu vermitteln (siehe Abschnitt 1.1). Oft trifft man auf
Einleitungsbeispiele, die zwar angenehm einfach aufgebaut sind, aber außer gewissen Formalitäten

1
https://jaxenter.de/red-hat-openjdk-java-82758
Red Hat wurde mittlerweile von IBM übernommen.
2
https://developer.ibm.com/blogs/ibm-and-java-looking-forward-to-the-future/
3
https://blogs.microsoft.com/blog/2019/08/19/microsoft-acquires-jclarity-to-help-optimize-java-workloads-on-azure/
https://www.theserverside.com/opinion/Microsoft-vs-IBM-A-major-shift-in-Java-support
4
https://openjdk.java.net/legal/gplv2+ce.html
5
https://jaxenter.de/java-jdk-release-zyklus-75402
30 Kapitel 1 Einleitung

kaum Merkmale der objektorientierten Programmierung aufweisen. Hier wird die gesamt Funktio-
nalität in die main() - Methode der Startklasse und eventuell in weitere statische Methoden der
Startklasse gezwängt. Später werden auch wir solche pseudo-objektorientierten (POO-) Pro-
gramme benutzen, um elementare Java-Sprachelemente in möglichst einfacher Umgebung kennen-
zulernen. Aus den letzten Ausführungen ergibt sich u. a., dass Java zwar eine objektorientierte Pro-
grammierweise nahelegen und unterstützen, aber nicht erzwingen kann.
Nachdem das objektorientierte Paradigma die Software-Entwicklung über Jahrzehnte dominiert hat,
gewinnt das ältere, aber lange Zeit auf akademische Diskurse beschränkte funktionale Paradigma
in den letzten Jahren an Bedeutung. Ein wesentlicher Grund ist seine gute Eignung für die zur opti-
malen Nutzung moderner Mehrkern-CPUs erforderliche nebenläufige Programmierung (Horstmann
2014b). Seit der Version 8 unterstützt Java wichtige Techniken bzw. Prinzipien der funktionalen
Programmierung (z. B. Lambda-Ausdrücke).

1.3.6.2 Portabilität
Die im Abschnitt 1.3.2 beschriebene Übersetzungsprozedur führt zusammen mit der Tatsache, dass
sich Bytecode-Interpreter für aktuelle IT-Plattformen relativ leicht implementieren lassen, zur guten
Portabilität von Java. Man mag einwenden, dass sich der Quellcode vieler Programmiersprachen
(z. B. C++) ebenfalls auf verschiedenen Rechnerplattformen kompilieren lässt. Diese Quellcode-
Portabilität aufgrund weitgehend genormter Sprachdefinitionen und verfügbarer Compiler ist je-
doch auf einfache Anwendungen mit textorientierter Benutzerschnittstelle beschränkt und stößt
selbst dort auf manche Detailprobleme (z. B. durch verschiedenen Zeichensätze). C++ wird zwar
auf vielen verschiedenen Plattformen eingesetzt, doch kommen dabei in der Regel plattformab-
hängige Funktions- bzw. Klassenbibliotheken zum Einsatz (z. B. GTK unter Linux, MFC unter
Windows).1 Bei Java besitzt hingegen bereits die zuverlässig in jeder JVM verfügbare Standardbib-
liothek mit ihren insgesamt ca. 4000 Klassen weitreichende Fähigkeiten für die Gestaltung grafi-
scher Bedienoberflächen, für Datenbank- und Netzwerkzugriffe usw., sodass sich plattformunab-
hängige Anwendungen mit modernem Funktionsumfang und Design realisieren lassen.
Weil der von einem Java-Compiler erzeugte Bytecode von jeder JVM (mit passender Version) aus-
geführt werden kann, bietet Java nicht nur Quellcode- sondern auch Binärportabilität. Ein Pro-
gramm ist also ohne erneute Übersetzung auf verschiedenen Plattformen einsetzbar.
Microsoft strebt mit seiner .NET - Plattform dasselbe Ziel an und verspricht für die im Herbst 2021
erscheinende Version 6 sogar eine Multiplattform - GUI-Lösung namens MAUI (siehe z. B. Baltes-
Götz 2021). Während Linux generell von .NET unterstützt wird, bleibt dieses Betriebssystem bei
MAUI aber außen vor.

1.3.6.3 Sicherheit
Beim Design der Java-Technologie wurde das Thema Sicherheit gebührend berücksichtigt. Weil ein
als Bytecode übergebenes Programm durch die beim Empfänger installierte virtuelle Maschine vor
der Ausführung auf unerwünschte Aktivitäten geprüft wird, können viele Schadwirkungen verhin-
dert werden.
Leider hat sich die Sicherheitstechnik der Java-Laufzeitumgebung im Jahr 2013 mehrfach als löch-
rig erwiesen. Von den Risiken, die oft voreilig und unreflektiert auf die gesamte Java-Technik be-
zogen wurden, waren allerdings überwiegend die von Webservern bezogenen Applets betroffen, die

1
Dass es grundsätzlich möglich ist, eine C++ - Klassenbibliothek mit umfassender Funktionalität (z. B. auch für die
Gestaltung grafischer Bedienoberflächen) für verschiedene Plattformen herzustellen und so für Quellcode-
Portabilität bei modernen, kompletten Anwendungen zu sorgen, beweist die Firma Trolltech mit ihrem Produkt Qt.
Abschnitt 1.3 Architektur und Eigenschaften von Java-Software 31

im Internet-Browser - Kontext mit Hilfe von Plugins ausgeführt wurden. Diese Java-Applets sind
strikt von lokal installierten Java-Anwendungen für Desktop-Rechner zu unterscheiden. Noch weni-
ger als Java-Anwendungen für Desktop-Rechner waren die außerordentlich wichtigen Server-
Anwendungen (z. B. erstellt mit der Java Enterprise Edition oder alternativen Frameworks wie
Spring) von der damaligen Sicherheitsmisere betroffen. Moderne Browser unterstützen keine
Plugins mehr, sodass Java-Applets (wie auch Anwendungen für Flash und Silverlight) keine Rolle
mehr spielen. Oracle hat die Applet-Technik seit dem JDK 9 als veraltet (engl. deprecated) gekenn-
zeichnet und seit der Version 11 aus dem JDK entfernt.1
Generell ist natürlich auch Java-Software nicht frei von Sicherheitsproblemen und muss (wie das
Betriebssystem, die Browser, der Virenschutz und viele andere Programme) stets aktuell gehalten
werden. Das gilt insbesondere für die JVM-Installationen.
Offenbar hat die Firma Oracle aus den ärgerlichen und peinlichen Problemen des Jahres 2013 ge-
lernt. Das Bundesamt für Sicherheit in der Informationstechnik stellt in seinem Jahresbericht 2018
zur IT-Sicherheit in Deutschland bei der Java-Laufzeitumgebung (Oracle JRE) relativ wenige kriti-
sche Schwachstellen (kritische CVE-Einträge) bei stark sinkender Tendenz fest:2

In aktuelleren BSI-Jahresberichten ist leider kein vergleichbarer Überblick enthalten.

1.3.6.4 Robustheit
In diesem Abschnitt werden Gründe für die hohe Robustheit (Stabilität) von Java-Software genannt,
wobei auch die anschließend noch separat behandelte Einfachheit eine große Rolle spielt.
Die Programmiersprache Java verzichtet auf Merkmale von C++, die erfahrungsgemäß zu Fehlern
verleiten, z. B.:
• Pointer-Arithmetik
• Benutzerdefiniertes Überladen von Operatoren
• Mehrfachvererbung

1
https://www.oracle.com/technetwork/java/javase/javaclientroadmapupdatev2020may-6548840.pdf
2
https://www.bmi.bund.de/SharedDocs/downloads/DE/publikationen/themen/it-digitalpolitik/bsi-lagebericht-
2018.pdf?__blob=publicationFile&v=3
32 Kapitel 1 Einleitung

Außerdem werden die Programmierer zu einer systematischen Behandlung der bei einem Metho-
denaufruf potentiell zu erwartenden Ausnahmefehler gezwungen.
Schließlich leistet die hohe Qualität der Java-Standardbibliothek einen Beitrag zur Stabilität der
Software.

1.3.6.5 Einfachheit
Schon im Zusammenhang mit der Robustheit wurden einige komplizierte und damit fehleranfällige
C++ - Bestandteile erwähnt, auf die Java bewusst verzichtet. Zur Vereinfachung trägt auch bei, dass
Java keine Header-Dateien benötigt, weil die Bytecode-Datei einer Klasse alle erforderlichen Me-
tadaten enthält. Weiterhin kommt Java ohne Präprozessor-Anweisungen aus, die in C++ den
Quellcode vor der Übersetzung modifizieren oder die Arbeitsweise des Compilers beeinflussen
können.1
Wenn man dem Programmierer eine Aufgabe komplett abnimmt, kann er dabei keine Fehler ma-
chen. In diesem Sinn wurde in Java der sogenannte Garbage Collector (dt.: Müllsammler) imple-
mentiert, der den Speicher nicht mehr benötigter Objekte automatisch freigibt. Im Unterschied zu
C++, wo die Freigabe durch den Programmierer zu erfolgen hat, sind damit typische Fehler bei der
Speicherverwaltung ausgeschlossen:
• Ressourcenverschwendung durch überflüssige Objekte (Speicherlöcher)
• Programmabstürze beim Zugriff auf voreilig entsorgte Objekte
Insgesamt ist Java im Vergleich zu C/C++ deutlich einfacher zu beherrschen und damit für Einstei-
ger eher zu empfehlen.
Es existieren mehrere hochwertige Java-Entwicklungsumgebungen (z. B. Eclipse, IntelliJ IDEA,
NetBeans), die das Erstellen des Quellcodes erleichtern (z. B. durch Vorschläge zur Code-
Erweiterung, Refaktorierung), Beiträge zur Qualitätssteigerung leisten (z. B. durch Code-Analyse,
Testunterstützung) und meist kostenlos verfügbar sind.

1.3.6.6 Multithreading
Java bietet eine gute Unterstützung für Anwendungen mit mehreren, parallel laufenden Ausfüh-
rungsfäden (Threads). Solche Anwendungen bringen erhebliche Vorteile für den Benutzer, der z. B.
mit einem Programm interagieren kann, während es im Hintergrund aufwändige Berechnungen aus-
führt oder auf die Antwort eines Netzwerk-Servers wartet. Andererseits kommt es vor, dass zwar
nur eine Aufgabe ansteht (z. B. Transkodieren eines Films, Überprüfung vieler Dateien auf Schäd-
linge), dabei jedoch durch Beteiligung mehrerer Threads eine erhebliche Beschleunigung im Ver-
gleich zum traditionellen Single-Thread-Betrieb erzielt werden kann. Weil mittlerweile Mehrkern-
bzw. Mehrprozessor-Systeme üblich sind, wird für Programmierer die Multithreading-
Beherrschung immer wichtiger.
Die zur Erstellung nebenläufiger Programme attraktive funktionale Programmierung wird in Java
seit der Version 8 unterstützt.

1
Der Gerüchten zufolge im früher verbreiteten Textverarbeitungsprogramm StarOffice (Vorläufer der Open Source
Programme OpenOffice und LibreOffice) über eine Präprozessor-Anweisung realisierte Unfug, im Quellcode den
Zugriffsmodifikator private vor der Übergabe an den Compiler durch die schutzlose Alternative public zu ersetzen,
ist also in Java ausgeschlossen.
Abschnitt 1.4 Übungsaufgaben zum Kapitel 1 33

1.3.6.7 Performanz
Der durch Sicherheit (z. B. Bytecode-Verifikation), Stabilität (z. B. Garbage Collector) und Portabi-
lität verursachte Performanznachteil von Java-Programmen (z. B. gegenüber C++) ist durch die
Entwicklung leistungsfähiger virtueller Java-Maschinen mittlerweile weitgehend irrelevant gewor-
den, wenn es nicht gerade um performanzkritische Anwendungen (z. B. Spiele) geht. Mit unserer
Entwicklungsumgebung IntelliJ IDEA werden Sie eine komplett in Java erstellte, sehr komplexe
und dabei flott agierende Anwendung kennenlernen.

1.3.6.8 Beschränkungen
Wie beim Designziel der Plattformunabhängigkeit nicht anders zu erwarten, lassen sich in Java-
Programmen sehr spezielle Eigenschaften eines Betriebssystems schlecht verwenden (z. B. die
Windows-Registrierungsdatenbank). Wegen der Einschränkungen beim freien Speicher- bzw.
Hardware-Zugriff eignet sich Java außerdem kaum zur Entwicklung von Treiber-Software (z. B. für
eine Grafikkarte). Für System- bzw. Hardware-nahe Programme ist z. B. C (bzw. C++) besser ge-
eignet.

1.4 Übungsaufgaben zum Kapitel 1


1) Warum steigt die Produktivität der Software-Entwicklung durch objektorientiertes Programmie-
ren?

2) Welche der folgenden Aussagen sind richtig bzw. falsch?


1. Die Programmiersprache Java ist relativ leicht zu erlernen, weil beim Design Einfachheit
angestrebt wurde.
2. In Java muss jede Klasse eine Methode namens main() enthalten.
3. Die meisten aktuellen CPUs können Java-Bytecode direkt ausführen.
4. Java eignet sich für eine sehr breite Palette von Anwendungen, von Smartphone-Apps über
Anwendungsprogramme für Arbeitsplatzrechner bis zur unternehmenswichtigen Server-
Software.
2 Werkzeuge zum Entwickeln von Java-Programmen
In diesem Kapitel werden kostenlose Werkzeuge zum Entwickeln von Java-Anwendungen be-
schrieben. Zunächst beschränken wir uns puristisch auf einen Texteditor und das Java Develop-
ment Kit (Standard Edition). In dieser sehr übersichtlichen „Entwicklungsumgebung“ werden die
grundsätzlichen Arbeitsschritte und einige Randbedingungen besonders deutlich.
Anschließend gönnen wir uns erheblich mehr Luxus in Form der Open Source - Entwicklungsum-
gebung IntelliJ IDEA Community, die auf vielfältige Weise die Programmentwicklung unter-
stützt. IntelliJ IDEA bietet u. a.:
• einen Editor mit ...
o farblicher Unterscheidung verschiedener Syntaxbestandteile
o Vorschläge zur Syntaxerweiterung
o Unterschlängeln von Fehlern
o Refaktorierungs-Unterstützung (z.B. beim Umbenennen von Klassen oder Variablen)
o usw.
• einen Debugger zur Unterstützung bei der Fehlersuche
• Assistenten zum automatischen Erstellen von Quellcode für Routineaufgaben
Anschließend werden die für Kursteilnehmer bzw. Leser empfohlenen Installationen beschrieben.
Alle Pakete sind kostenlos für alle relevanten Betriebssysteme verfügbar.

2.1 Aktuelles OpenJDK installieren


Wir haben im Abschnitt 1.2.1 bereits ein Java Development Kit installiert, waren dabei aber in ers-
ter Linie an der enthaltenen Java Virtual Machine (JVM) interessiert. Unsere Entscheidung fiel auf
das OpenJDK in der Version 8 (alias 1.8), zu der das ojdkbuild-Projekt eine komfortable Distributi-
on mit folgenden Vorteilen pflegt:
• Kein relevante Lizenzeinschränkung
• Langzeit-Support bis Mai 2026
• JavaFX (alias OpenJFX) als Option enthalten
• Update-Unterstützung
Das JDK erscheint seit der Version 9 in einem halbjährlichen Release-Zyklus, um die Weiterent-
wicklung zu beschleunigen.1 So sind wir aktuell (im Oktober 2021) bei der Version 17 angekom-
men. Obwohl der Abstand zwischen den Versionen 17 und 8 weniger gewaltig ist, als es der nume-
rische Unterschied vermuten lässt, sind doch einige Neuerungen der Java-Softwaretechnik bzw. der
Java-Programmiersprache für uns relevant, z. B.:
• das mit Java 9 eingeführte Modulsystem
• die mit Java 10 eingeführte Typinferenz für lokale Variablen
• die mit Java 11 eingeführte Erweiterung der Typinferenz für Lambda-Parameter
• die mit Java 14 eingeführten switch-Ausdrücke
• die mit Java 15 eingeführten Textblöcke
• die mit Java 16 eingeführten Record-Typen
• die in Java 16 vorgenommene Verbesserung des instanceof-Operators durch Musterverglei-
che (engl. pattern matching)
• die mit Java 17 eingeführten versiegelten Klassen und Schnittstellen
• die mit Java 17 (allerdings nur als Preview) auch für switch-Ausdrücke eingeführten Mus-
tervergleiche

1
https://jaxenter.de/java-jdk-release-zyklus-75402
36 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Daher werden wir im Kurs auch mit dem OpenJDK 17 arbeiten.

2.1.1 OpenJDK 17 der Firma Oracle


Die OpenJDK 17 - Distribution der Firma Oracle ist auf dieser Webseite
https://jdk.java.net/17/
frei verfügbar und ermöglicht es uns, die aktuelle, am 14.09.2021 erschienene Java-Version 17 zu
verwenden, die auch von unserer Entwicklungsumgebung IntelliJ IDEA 2021.2 unterstützt wird
(siehe Abschnitt 2.3). Das OpenJDK von Oracle steht unter der (GPLv2 & CPE) - Lizenz und darf
zur Software-Entwicklung sowie im produktiven Einsatz frei verwendet werden. Allerdings ist die
Unterstützung durch Updates auf die 6 Monate bis zum Erscheinen der nächsten Java-Hauptversion
beschränkt. Die Java-Version 17 wird eine Langzeitunterstützung durch Updates erhalten (LTS,
Long Term Support), die bei Oracle für die nicht-private Nutzung kostenpflichtig ist, nach den bis-
herigen Erfahrungen mit den LTS-Versionen 8 und 11 bei anderen Anbietern (z.B. Amazon, Micro-
soft, Red Hat) aber sehr wahrscheinlich kostenlos zu haben sein wird (siehe Abschnitt 2.1.2).
Eine OpenJDK-Installation auf Basis der Oracle-Distribution erfordert etwas Handarbeit, hat aber
den Vorteil der hohen Transparenz. Anschließend wird die Installation unter Windows 10 (64 Bit)
beschrieben:
• Laden Sie von der oben genannten Webseite das für Windows x64 angebotene ZIP-Archiv
mit der aktuellen Version herunter. Am 20.10.2021 wird die Version 17.0.1 in der Datei
openjdk-17.0.1_windows-x64_bin.zip geliefert.
• Beim Auspacken des ZIP-Archivs entsteht der Ordner jdk-17.0.1.
• Erstellen Sie bei Bedarf den Ordner C:\Program Files\Java.
• Kopieren Sie den OpenJDK-Ordner unter dem Namen OpenJDK-17 in den Ordner
C:\Program Files\Java:

Eine OpenJDK-Distribution (von Oracle oder von einem alternativen Anbieter, siehe Abschnitt
2.1.2) enthält u. a. (im bin-Unterordner) …
• den Java-Compiler javac.exe, der Java-Quellcode in Java-Bytecode übersetzt
• den Java-Interpreter java.exe, der Java-Programme ausführt (Bytecode in Maschinencode
übersetzt)
• zahlreiche Werkzeuge (z. B. den Dokumentationsgenerator javadoc.exe und den Archivge-
nerator jar.exe)
Abschnitt 2.2 Java-Entwicklung mit dem JDK und einem Texteditor 37

2.1.2 OpenJDK-Distributionen mit langfristiger Update-Versorgung


Wer mit dem OpenJDK der Firma Oracle arbeitet, ist stets auf dem neuesten Stand, muss aber alle 6
Monate auf eine neue Hauptversion umsteigen. Für die LTS-Versionen (Long Term Support) von
Java (aktuell die Versionen 8, 11 und 17) wird die Versorgung mit Updates über einen Zeitraum
von mindestens 5 Jahren zugesichert. Während die Firma Oracle für LTS-Versionen bei nicht-
privater Verwendung Lizenzgebühren verlangt, ist der Service bei anderen Anbietern kostenlos zu
haben (vgl. Abschnitt 1.3.5). Ohne Anspruch auf Vollständigkeit sollen genannt werden:
• OpenJDK aus dem ojdkbuild-Projekt
Das von der Firma Red Hat, einem Anbieter professioneller Linux-Lösungen, gesponserte
Open Source - Projekt ojdkbuild bietet auf der Webseite
https://github.com/ojdkbuild/ojdkbuild
u. a. OpenJDK-Distributionen mit den LTS-Versionen 8 und 11 an. Wir haben im Abschnitt
1.2.1 das OpenJDK 8 aus dem ojdkbuild-Projekt installiert.
• Amazon Corretto
Die Firma Amazon bietet auf der Webseite
https://aws.amazon.com/de/corretto/
unter dem Namen Corretto OpenJDK-Distributionen mit den LTS-Versionen 8 und 11 an.
• OpenJDK Build von Microsoft
Die Firma Microsoft bietet auf der Webseite
https://docs.microsoft.com/en-us/java/openjdk/download
u. a. eine OpenJDK-Distribution mit der LTS-Version 11 an.
Sehr wahrscheinlich werden alle Anbieter in Kürze eine OpenJDK-Distribution mit der LTS-
Version 17 bereithalten.
Leider besitzen die OpenJDK-Distributionen mit Ausnahme der OpenJDK 8 - Distribution aus dem
ojdkbuild-Projekt in der Regel keine automatische Update-Unterstützung, sodass sich Entwickler
und Anbieter über Sicherheitsupdates informieren müssen.

2.2 Java-Entwicklung mit dem JDK und einem Texteditor

2.2.1 Editieren
Um das Erstellen, Übersetzen und Ausführen von Java-Programmen ohne großen Aufwand üben zu
können, erstellen wir das unvermeidliche Hallo-Programm, das vom bereits erwähnten POO-Typ
ist (pseudo-objektorientiert):
Quellcode Ausgabe
class Hallo { Hallo allerseits!
public static void main(String[] args) {
System.out.println("Hallo allerseits!");
}
}

Im Unterschied zu hybriden Programmiersprachen wie C++ und Delphi, die neben der objektorien-
tierten auch die strukturierte Programmierung (vgl. Abschnitt 4.1.2) erlauben, verlangt Java auch
für solche Trivialprogramme eine Klassendefinition. Im Beispiel genügt eine einzige Klasse, die
den Namen Hallo erhält. Es muss eine startfähige Klasse sein, weil eine solche in jedem Java-
Programm benötigt wird. In der somit erforderlichen Methode main() erzeugt die Klasse Hallo
aber keine Objekte, wie es die Startklasse Bruchaddition im Einstiegsbeispiel tat, sondern be-
schränkt sich auf eine Bildschirmausgabe.
38 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Immerhin kommt dabei ein vordefiniertes Objekt (System.out) zum Einsatz, das durch Aufruf sei-
ner println() - Methode mit der Ausgabe beauftragt wird. Durch einen Parameter vom Zeichenfol-
gentyp wird der Auftrag beschrieben.
Das POO-Programm eignet sich aufgrund seiner Kürze zum Erläutern wichtiger Regeln, an die Sie
sich so langsam gewöhnen sollten. Alle Themen werden aber später noch einmal systematisch und
ausführlich behandelt:
• Nach dem Schlüsselwort class folgt der frei wählbare Klassenname. Hier ist wie bei allen
Bezeichnern zu beachten, dass Java streng zwischen Groß- und Kleinbuchstaben unterschei-
det. Nach einer weitgehend eingehaltenen Konvention beginnt in Java ein Klassenname mit
einem Großbuchstaben.
Weil bei den Klassen der POO-Übungsprogramme im Unterschied zur eingangs vorgestell-
ten Bruch-Klasse eine Nutzung durch andere Klassen nicht in Frage kommt, wird in der
Klassendefinition auf den Modifikator public verzichtet. Manche Autoren von Java-
Beschreibungen entscheiden sich für die systematische Verwendung des public-
Modifikators, z. B.:
public class Hallo {
public static void main(String[] args) {
System.out.println("Hallo allerseits!");
}
}
Das vorliegende Manuskript orientiert sich am Verhalten der Java-Urheber: Gosling et al.
(2021) lassen bei Startklassen, die nur von der JVM angesprochen werden, den Modifikator
public systematisch weg. Später werden klare und unvermeidbare Gründe für die Verwen-
dung des Klassen-Modifikators public beschrieben.
• Dem Kopf der Klassendefinition folgt der mit geschweiften Klammern eingerahmte Rumpf.
• Weil die Klasse Hallo startfähig sein soll, muss sie eine Methode namens main() besitzen.
Diese wird von der JVM beim Programmstart ausgeführt und dient bei „echten“ OOP-
Programmen (direkt oder indirekt) dazu, Objekte zu erzeugen (siehe die Klasse Bruchad-
dition im Abschnitt 1.1.4).
• Die Definition der Methode main() wird von drei obligatorischen Schlüsselwörtern einge-
leitet, deren Bedeutung Sie auch jetzt schon (zumindest teilweise) verstehen können:
o public
Wie eben erwähnt, wird die Methode main() beim Programmstart von der JVM ge-
sucht und ausgeführt. Sie muss den Zugriffsmodifikator public erhalten. Anderen-
falls reagiert die JVM auf den Startversuch mit einer Fehlermeldung:

o static
Mit diesem Modifikator wird main() als statische, d .h. der Klasse zugeordnete Me-
thode gekennzeichnet. Im Unterschied zu den Instanzmethoden der Objekte werden
die statischen Methoden von der Klasse selbst ausgeführt. Die beim Programmstart
automatisch ausgeführte main() - Methode der Startklasse muss auf jeden Fall durch
den Modifikator static als Klassenmethode gekennzeichnet werden.
Abschnitt 2.2 Java-Entwicklung mit dem JDK und einem Texteditor 39

o void
Die Methode main() hat den Rückgabetyp void, weil sie keinen Rückgabewert lie-
fert.1
Die beiden Modifikatoren public und static stehen in beliebiger Reihenfolge am Anfang der
Methodendefinition. Ihnen folgt der Rückgabetyp, der unmittelbar vor dem Methodennamen
stehen muss.
• Die Namen von Methoden starten in Java nach einer weitgehend eingehaltenen Konvention
mit einem Kleinbuchstaben.
• Auf den Namen einer Methode folgt durch runde Klammern begrenzt die Parameterliste
mit Daten und Informationen zur Steuerung der Ausführung. Wir werden uns später aus-
führlich mit diesem wichtigen Thema beschäftigen und beschränken uns hier auf zwei Hin-
weise:
o Für Neugierige und/oder Vorgebildete
Der main() - Methode werden über einen Array mit String-Elementen die Spezifika-
tionen übergeben, die der Anwender in der Kommandozeile beim Programmstart an-
gegeben hat. In unserem Beispiel kümmert sich die Methode main() allerdings nicht
um solche Anwenderwünsche.
o Für Alle
Bei einer main() - Methode ist die im Beispiel verwendete Parameterliste obligato-
risch, weil die JVM ansonsten die Methode beim Programmstart nicht erkennt und
mit derselben Fehlermeldung reagiert wie bei einem fehlenden public-Modifikator
(siehe oben). Den Parameternamen (im Beispiel: args) darf man allerdings beliebig
wählen.
• Dem Kopf einer Methodendefinition folgt der mit geschweiften Klammern eingerahmte
Rumpf mit Variablendeklarationen und sonstigen Anweisungen. Das minimalistische Bei-
spielprogramm beschränkt sich auf eine einzige Anweisung, die einen Methodenaufruf ent-
hält.
• In der main() - Methode unserer Hallo-Klasse wird die println() - Methode des vordefi-
nierten Objekts System.out dazu benutzt, einen Text an die Standardausgabe zu senden. Der
Auftrag geht an das statische Objekt out in der Klasse System. Zwischen der Objektbe-
zeichnung System.out und dem Methodennamen println() steht ein Punkt. Bei einem Me-
thodenaufruf handelt es sich um eine Anweisung, die folglich mit einem Semikolon abzu-
schließen ist.
Es dient der Übersichtlichkeit, zusammengehörige Programmteile durch eine gemeinsame Ein-
rücktiefe zu kennzeichnen. Man realisiert die Einrückungen am einfachsten mit der Tabulatortaste,
aber auch Leerzeichen sind erlaubt. Für den Compiler sind die Einrückungen irrelevant.
Schreiben Sie den Quellcode mit einem beliebigen Texteditor, unter Windows z. B. mit Notepad,
und speichern Sie Ihr Quellprogramm unter dem Namen Hallo.java in einem geeigneten Verzeich-
nis, z. B. in
U:\Eigene Dateien\Java\BspUeb\Einleitung\Hallo\JDK
Beachten Sie bitte:

1
Die Programmiersprachen C, C++ und C# besitzen ebenfalls eine Funktion bzw. Methode namens main() bzw.
Main(), und dort wird (optional) der Rückgabetype int verwendet, der beim Verlassen des Programms die Übergabe
eines Returncodes an das Betriebssystem erlaubt. In Java hat die main() - Methode obligatorisch den Rückgabetyp
void, doch kann z. B. mit der statischen Methode exit() der Klasse System ein Returncode an das Betriebssystem
übergeben werden (siehe Abschnitt 11.2).
40 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

• Die Dateinamenserweiterung muss .java lauten.


• Der Dateinamensstamm (vor dem Punkt) sollte unbedingt mit dem Klassennamen überein-
stimmen. Ein aus der Missachtung dieser Regel resultierendes Problem ist im Abschnitt
2.2.2 zu sehen. Die vom Compiler erzeugte Bytecode-Datei übernimmt auf jeden Fall den
Namen der Klasse. Es resultiert also eine Namensabweichung zwischen Quellcode- und
Bytecode-Datei, wenn die Quellcodedatei nicht den Namen der Klasse (mit angehängter
Erweiterung .java) trägt.
• Unter Windows ist beim Dateinamen die Groß-/Kleinschreibung zwar irrelevant, doch sollte
auch hier auf exakte Übereinstimmung mit dem Klassennamen geachtet werden.

2.2.2 Übersetzen
Öffnen Sie ein Konsolenfenster, und wechseln Sie in das Verzeichnis mit dem neu erstellten Quell-
programm Hallo.java. Lassen Sie das Programm vom Compiler javac.exe im OpenJDK 17 über-
setzen, das wir im Abschnitt 2.1.1 installiert haben, z. B.:
>"C:\Program Files\Java\OpenJDK-17\bin\javac" Hallo.java
Falls beim Übersetzen keine Probleme auftreten, dann meldet sich der Rechner nach kurzer Ar-
beitszeit mit einem neuer Kommandoaufforderung zurück, und die Quellcodedatei Hallo.java er-
hält Gesellschaft durch die Bytecode-Datei Hallo.class, z. B.:

Wenn Sie im Compiler-Aufruf den Ordnernamen weglassen,


>javac Hallo.java
dann kommt vermutlich der Compiler der gemäß Abschnitt 1.2.1 installierten OpenJDK-Version 8
zum Zug, der im Windows-Suchpfad für ausführbare Programme enthalten ist. Bei den im Ab-
schnitt 2.2 beschriebenen Übungen verhalten sich die OpenJDK-Versionen 8 und 17 gleich.
Damit unter Windows der Compiler aus dem OpenJDK 17 ohne Pfadangabe von jedem Verzeichnis
aus gestartet werden kann, muss das bin-Unterverzeichnis der OpenJDK 17 - Installation (mit dem
Compiler javac.exe) vorrangig in die Definition der Umgebungsvariablen PATH aufgenommen
werden, was unter Windows 10 z. B. so geschehen kann:1
• Suchen: Umgebungsvariablen
• Starten: Systemumgebungsvariablen bearbeiten

1
Bei der Software-Entwicklung mit IntelliJ IDEA wirkt sich ein Verzicht auf den PATH-Eintrag nicht aus.
Abschnitt 2.2 Java-Entwicklung mit dem JDK und einem Texteditor 41

• Klick auf Umgebungsvariablen


Bei der Umgebungsvariablen PATH erlaubt Windows eine System- und eine Benutzervari-
ante, wobei die Systemvariable Vorrang hat. Enthält die (immer vorhandene) Systemvariab-
le PATH einen Ordner mit javac.exe (z. B. aufgrund der im Abschnitt 1.2.1 beschriebenen
Installation der OpenJDK-Version 8), dann muss diese Variable modifiziert werden, was an-
schließend beschrieben wird. Ansonsten genügt es, das bin-Unterverzeichnis der zu ver-
wendenden OpenJDK -Installation in die PATH-Benutzervariable einzutragen.
• Systemvariable PATH Bearbeiten
• Neuen Eintrag mit C:\Program Files\Java\OpenJDK-17\bin erstellen und ganz nach
oben befördern
Beim Kompilieren des Quellcodes zu einer Klasse A wird auch der Quellcode zu einer in A benutz-
ten Klasse B neu übersetzt, wenn ...
• der Quellcode zu B im aktuellen Ordner vorhanden, der Bytecode zu B aber nicht verfügbar
ist,
• der Quellcode zu B im aktuellen Ordner vorhanden und der Bytecode zu B verfügbar ist (im
aktuellen Ordner vorhanden oder via CLASSPATH auffindbar, vgl. Abschnitt 2.2.4), wobei
die Bytecode-Datei älter ist als die Quellcodedatei
Sind etwa im Bruchadditionsbeispiel die Quellcodedateien Bruch.java und Bruchaddition.java
geändert worden, dann genügt der folgende Compiler-Aufruf, um beide Dateien neu zu übersetzen:
>javac Bruchaddition.java
Die benötigten Quellcode-Dateinamen (z. B. Bruch.java) konstruiert der Compiler aus den ihm
bekannten Klassenbezeichnungen (z. B. Bruch). Bei Missachtung der Quellcodedatei-Benennungs-
regeln (siehe Abschnitt 2.2.1) muss der Compiler bei seiner Suche also scheitern.
Man kann den OpenJDK-Compiler javac.exe über das Jokerzeichen * auch beauftragen, alle Java-
Quellcodedateien zu übersetzen, z. B.:
>javac *.java
Bei der Ausführung der Klasse Bruchaddition zeigt sich ein Problem mit der Codierung bzw.
Darstellung der deutschen Umlaute:

Die Quellcodedatei Bruch.java mit den im Programm falsch dargestellten Umlauten verwendet die
(in Windows 10 voreingestellte) UTF-8 - Codierung. Per Voreinstellung geht der Java-Compiler im
OpenJDK aber von der traditionell in Windows voreingestellten ANSI-Codierung aus (korrekte
Bezeichnung: Windows-1252). Durch die Compiler-Option -encoding kann der Compiler über die
tatsächlich verwendete UTF-8 - Codierung informiert werden:
>javac -encoding utf8 *.java
Nach dieser Übersetzung ist das Problem behoben:
42 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Eine weitere Beschäftigung mit der Syntax von javac.exe ist nicht erforderlich, weil wir komplex-
ere Projekte mit Hilfe unserer Entwicklungsumgebung IntelliJ erstellen lassen. Wenn dabei in den
Erstellungsprozess eingegriffen werden soll, dann bietet sich die Verwendung eines Erstellungs-
werkzeugs wie Ant, Gradle oder Maven an. Im Manuskript werden diese Werkzeuge aber nicht
behandelt.

2.2.3 Ausführen
Wie Sie bereits wissen, wird zum Ausführen von Java-Programmen eine Java Virtual Machine
(JVM) mit dem Interpreter java.exe und der Standardklassenbibliothek benötigt. Aufgrund der
2019 von der Firma Oracle geänderten Lizenzpolitik ist oft auf dem Rechner eines Anwenders eine
OpenJDK-Distribution installiert. Diese enthält auch eine JVM, sodass aus Anwendersicht eine
OpenJDK-Installation äquivalent ist zur früher üblichen Installation einer puren Ausführungsumge-
bung (JRE).
Lassen Sie das Programm (bzw. die Klasse) Hallo.class von der JVM ausführen. Der Aufruf
>java Hallo
sollte zum folgenden Ergebnis führen:

Beim Programmstart ist zu beachten:


• Beim Aufruf des Interpreters wird der Name der auszuführenden Klasse als Argument ange-
geben, nicht der zugehörige Dateiname. Wer den Dateinamen (samt Namenserweiterung
.class) angibt, sieht eine Fehlermeldung:

• Weil beim Programmstart der Klassenname anzugeben ist, muss die Groß-/Kleinschreibung
mit der Klassendeklaration übereinstimmen (auch unter Windows!). Java-Klassennamen be-
ginnen meist mit großem Anfangsbuchstaben, und genau so müssen die Namen auch beim
Programmstart geschrieben werden.
Wird java.exe ohne Pfad angesprochen, hängt es von der Windows-Umgebungsvariablen PATH ab,
• ob java.exe gefunden wird,
• welche Version zum Zug kommt, wenn mehrere Versionen von java.exe vorhanden sind.
Abschnitt 2.2 Java-Entwicklung mit dem JDK und einem Texteditor 43

Wie man unter Windows für einen korrekten PATH-Eintrag sorgt, wird im Abschnitt 2.2.2 be-
schrieben.
Seit Java 11 kann der Interpreter aus einer Datei bestehende Programme als Quellcode entgegen-
nehmen, im Hauptspeicher übersetzen und dann ausführen, z. B.:

Diese Option hat aber keine allzu große Bedeutung, weil Java-Programme in der Regel aus vielen
Quellcode-Dateien bestehen.

2.2.4 Suchpfad für class-Dateien


Compiler und Interpreter benötigen Zugriff auf die Bytecode-Dateien der Klassen, die im zu über-
setzenden Quellcode bzw. im auszuführenden Programm angesprochen werden und nicht als Quell-
code vorliegen. Mit Hilfe der Umgebungsvariablen CLASSPATH kann man eine Liste von Ver-
zeichnissen, JAR-Archiven (siehe Abschnitt 6.1.3) oder ZIP-Archiven spezifizieren, die nach class-
Dateien durchsucht werden sollen, z. B.:

Bei einer Verzeichnisangabe sind Unterverzeichnisse nicht einbezogen. Sollten sich z. B. für einen
Compiler- oder Interpreter-Aufruf benötigte Dateien im Ordner U:\Eigene Dateien\Java\lib\sub
befinden, werden sie aufgrund der CLASSPATH-Definition in obiger Dialogbox nicht gefunden.
Wie man unter Windows 10 eine Umgebungsvariable setzen kann, wird im Abschnitt 2.2.2 be-
schrieben.
Befinden sich alle benötigten Klassen entweder in der Standardbibliothek (vgl. Abschnitt 1.3.3)
oder im aktuellen Verzeichnis, dann wird keine CLASSPATH-Umgebungsvariable benötigt. Ist sie
jedoch vorhanden (z. B. von irgendeinem Installationsprogramm unbemerkt angelegt), dann werden
außer der Standardbibliothek nur die Pfade in der CLASSTATH-Definition berücksichtigt. Dies
führt zu Problemen, wenn in der CLASSPATH-Definition das aktuelle Verzeichnis nicht enthalten
ist, z. B.:

In diesem Fall muss das aktuelle Verzeichnis (z. B. dargestellt durch einen einzelnen Punkt, s.o.) in
die CLASSPATH-Pfadliste aufgenommen werden, z. B.:
44 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

In vielen konsolenorientierten Beispielprogrammen des Manuskripts kommt die nicht zum Java-
API gehörige Klasse Simput in der Bytecode-Datei Simput.class (siehe unten) zum Einsatz. Über
die Umgebungsvariable CLASSPATH kann man dafür sorgen, dass der JDK-Compiler und der
Interpreter die Klasse Simput finden. Dies gelingt z. B. unter Windows 10 mit der oben abgebilde-
ten Dialogbox Neue Benutzervariable, wenn Sie die Datei
...\BspUeb\Simput\Standardpaket\Simput.class
in den Ordner U:\Eigene Dateien\Java\lib kopiert haben:

Achten Sie in der Dialogbox Neue Benutzervariable unbedingt darauf, den aktuellen Pfad über
einen Punkt in die CLASSPATH-Definition aufzunehmen.
Unsere Entwicklungsumgebung IntelliJ IDEA ignoriert die CLASSPATH-Umgebungsvariable,
bietet aber eine alternative Möglichkeit zur Definition des Klassenpfads für ein Projekt (siehe Ab-
schnitt 3.4.2).
Wenn sich nicht alle bei einem Compiler- oder Interpreter-Aufruf benötigten class-Dateien im aktu-
ellen Verzeichnis befinden und auch nicht auf die CLASSPATH-Variable vertraut werden soll,
dann können die nach class-Dateien zu durchsuchenden Pfade auch in den Startkommandos über
die Option -classpath (abzukürzen durch -cp) angegeben werden, z. B.:
>javac -cp ".;U:\Eigene Dateien\java\lib" Bruchaddition.java
>java -cp ".;U:\Eigene Dateien\java\lib" Bruchaddition
Auch hier muss das aktuelle Verzeichnis ausdrücklich (z. B. durch einen Punkt) aufgelistet werden,
wenn es in die Suche einbezogen werden soll.
Ein Vorteil der Option -cp gegenüber der Umgebungsvariablen CLASSPATH besteht darin, dass
für jede Anwendung eine eigene Suchliste eingestellt werden kann. Bei Verwendung der Option -cp
wird eine eventuell vorhandene CLASSPATH-Umgebungsvariable für den gestarteten Compiler-
oder Interpreter-Einsatz deaktiviert.
Die eben beschriebene Klassenpfadtechnik ist seit der ersten Java-Version im Einsatz, wird auch in
Java-Versionen mit Modulsystem (ab Version 9) noch unterstützt und genügt unseren vorläufig sehr
bescheidenen Ansprüchen beim Zugriff auf Bibliotheksklassen. Langfristig wird der Klassenpfad
vermutlich durch den mit Java 9 eingeführten Modulpfad ersetzt (siehe Abschnitt 6.2.4).
Abschnitt 2.2 Java-Entwicklung mit dem JDK und einem Texteditor 45

2.2.5 Programmfehler beheben


Die vielfältigen Fehler, die wir mit naturgesetzlicher Unvermeidlichkeit beim Programmieren ma-
chen, kann man einteilen in:
• Syntaxfehler
Diese verstoßen gegen eine Syntaxregel der verwendeten Programmiersprache, werden vom
Compiler reklamiert und sind daher leicht zu beseitigen.
• Logikfehler (Semantikfehler)
Hier liegt kein Syntaxfehler vor, aber das Programm verhält sich anders als erwartet, wie-
derholt z. B. ständig eine nutzlose Aktion („Endlosschleife“) oder stürzt mit einem Laufzeit-
fehler ab. In jedem Fall sind die Benutzer verärgert.
Die Java-Urheber haben dafür gesorgt, dass möglichst viele Fehler vom Compiler aufgedeckt wer-
den können. Während Syntaxfehler nur den Programmierer betreffen, automatisch entdeckt und
leicht beseitigt werden können, verursachen Logikfehler für Entwickler und Anwender oft einen
sehr großen Schaden. Simons (2004, S. 43) schätzt, dass viele Logikfehler tausendfach mehr Auf-
wand verursachen als der übelste Syntaxfehler.
Wir wollen am Beispiel eines provozierten Syntaxfehlers überprüfen, ob der JDK-Compiler hilfrei-
che Fehlermeldungen produziert. Wenn im Hallo-Programm der Klassenname System fälschli-
cherweise mit kleinem Anfangsbuchstaben geschrieben wird,
class Hallo {
public static void main(String[] args) {
system.out.println("Hallo allerseits!");
}
}

dann führt ein Übersetzungsversuch zur folgenden Reaktion:

Weil sich der Compiler bereits unmittelbar hinter dem betroffenen Wort sicher ist, dass ein Fehler
vorliegt, kann er die Schadstelle genau lokalisieren:
• In der ersten Fehlermeldungszeile liefert der Compiler den Namen der betroffenen Quell-
codedatei, die Zeilennummer und eine Fehlerbeschreibung.
• Anschließend protokolliert der Compiler die betroffene Zeile und markiert die Stelle, an der
die Übersetzung abgebrochen wurde.
Manchmal wird dem Compiler aber erst in einiger Distanz zur Schadstelle klar, dass ein Regelver-
stoß vorliegt, sodass statt der kritisierten Stelle eine frühere Passage zu korrigieren ist.
Im Beispiel fällt die Fehlerbeschreibung brauchbar aus, obwohl der Compiler (vermutlich aufgrund
des Kleinbuchstabens am Namensanfang) falsch vermutet, dass mit dem verunglückten Bezeichner
ein Paket gemeint sei (vgl. Abschnitt 6.1).
Weil sich in das simple Hallo-Beispielprogramm kaum ein Logikfehler einbauen lässt, betrachten
wir die im Abschnitt 1.1 vorgestellte Klasse Bruch. Wird z. B. in der Methode setzeNenner()
bei der Absicherung gegen Nullwerte das Ungleich-Operatorzeichen (!=) durch sein Gegenteil (==)
ersetzt, dann ist keine Java-Syntaxregel verletzt:
46 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

public boolean setzeNenner(int n) {


if (n == 0) {
nenner = n;
return true;
} else
return false;
}
In der main() - Methode der folgenden Klasse UnBruch erhält ein Bruch-Objekt aufgrund der un-
tauglichen Absicherung den kritischen Nennerwert 0 und wird anschließend zum Kürzen aufgefor-
dert:
class UnBruch {
public static void main(String[] args) {
Bruch b = new Bruch();
b.setzeZaehler(1);
b.setzeNenner(0);
b.kuerze();
}
}
Das Programm lässt sich fehlerfrei übersetzen, zeigt aber ein unerwünschtes Verhalten. Es gerät in
eine Endlosschleife (siehe unten) und verbraucht dabei reichlich Rechenzeit, wie der Windows-
Taskmanager (auf einem PC mit dem Intel-Prozessor Core i3 mit Dual-Core - Hyper-Threading-
CPU, also mit 4 logischen Kernen) belegt. Das Programm kann aufgrund seiner Single-Thread-
Technik nur einen logischen Kern nutzen und lastet diesen voll aus, sodass ca. 25% der CPU-
Leistung ver(sch)wendet werden:

Ein derart außer Kontrolle geratenes Konsolenprogramm kann man unter Windows z. B. mit der
Tastenkombination Strg+C beenden.

2.3 IntelliJ IDEA Community installieren


Die Community Edition der Entwicklungsumgebung IntelliJ IDEA wird von der Firma JetBrains
auf der Webseite
https://www.jetbrains.com/idea/download/
unter der Apache-Lizenz 2.0 kostenlos angeboten. Wir werden im Manuskript meist den zweiten
Namensbestandteil weglassen und von der Entwicklungsumgebung IntelliJ sprechen.
Zu jeder IntelliJ-Version kann das Plugin EduTools jederzeit kostenlos nachgerüstet werden. Unter
dem Namen IntelliJ IDEA Edu bietet JetBrains eine Community-Version mit integriertem Edu-
Tools-Plugin an. Vermutlich werden wir das Plugin nicht benötigen.
Abschnitt 2.3 IntelliJ IDEA Community installieren 47

Während der mit IntelliJ 2021.2.3 gestarteten Arbeit an diesem Manuskript werden vermutlich eini-
ge IntelliJ-Updates erscheinen. Auf wesentliche Abhängigkeiten von der IntelliJ-Version wird ggf.
im Manuskript hingewiesen.
Die Systemvoraussetzungen für IntelliJ unter Windows dürfte praktisch jeder Rechner erfüllen:
• 64-Bit-Version von Windows 10 oder 8
• Mindestens 2 GB RAM
• 3,5 GB freier Festplattenspeicher für IntelliJ
• Minimale Display-Auflösung: 1024 x 768
Über einem Klick auf den Download-Schalter zur Community-Edition erhält man unter Windows
einen Installationsassistenten als ausführbares Programm (am 23.10.2021: ideaIC-2021.2.3.exe).
Nach einem Klick auf den daneben stehenden EXE-Schalter erlaubt ein Menü die Wahl zwischen
einem ausführbaren Programm und einem ZIP-Archiv, wobei die erste Variante etwas mehr Be-
quemlichkeit und die zweite Variante etwas mehr Kontrolle bietet.
Nach dem Einstieg über einen Doppelklick auf die heruntergeladene Programmdatei ideaIC-
2021.2.3.exe und einer positiven Antwort auf die UAC-Nachfrage (User Account Control) von
Windows startet der Installationsassistent:

Ein Motiv für die Änderung des vorgeschlagenen Installationsordners

liefern die zu erwartenden Updates. Lässt man diese Updates von IntelliJ durchführen (siehe unten),
dann wird die Installation im vorhandenen Ordner aktualisiert, sodass der voreingestellte Ordner-
name nicht mehr zur Version passt. Daher wird unter Windows der folgende Installationsordner
empfohlen:
48 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2021.2


Von den angebotenen Installationsoptionen ist vor allem Add "Open Folder as Project" in der
Praxis von Nutzen:

Im Effekt lässt sich per Windows-Explorer ein IntelliJ-Projekt über das Item
Open Folder as IntelliJ IDEA Community Edition Project
im Kontextmenü zu seinem Ordner öffnen.
Nach der wenig relevanten Entscheidung über den Startmenüordner

legt das Installationsprogramm los


Abschnitt 2.3 IntelliJ IDEA Community installieren 49

und endet nach wenigen Minuten mit der Erfolgsmeldung:

Zur späteren Kontrolle auf ein eventuell anstehendes Update wählt man in IntelliJ den Menübefehl
Help > Check for Updates
Ggg. erscheint ein Info-Fenster unten rechts, z. B.:

Nach einem Klick auf den Link Update kann man sich über das Update informieren

und seiner Installation über den Schalter Update and Restart zustimmen. Mit dem folgenden
Info-Fenster signalisiert IntelliJ seine Bereitschaft für das Update, das nun mit einem Klick auf den
Link Restart veranlasst werden kann:

Anschließend muss noch auf Nachfrage durch die UAC von Windows eine Änderung des Systems
durch das Programm elevator.exe erlaubt werden.
50 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

2.4 Java-Entwicklung mit IntelliJ IDEA


Wir müssen uns natürlich mit unserem wichtigsten Werkzeug, der Entwicklungsumgebung, vertraut
machen, haben aber nicht die Zeit, um eines der über IntelliJ geschriebenen Bücher komplett zu
studieren.

2.4.1 Erster Start


Die Verknüpfung zum Starten von IntelliJ IDEA findet man unter Windows 10 in der Startme-
nügruppe JetBrains. Beim ersten Start müssen die Lizenzbedingungen akzeptiert werden:

Dann bittet JetBrains um die Erlaubnis, diagnostische Informationen zur Verwendung von IntelliJ
anonym übertragen zu dürfen:

Anschließend wird die Erkennungsgrafik der gestarteten IntelliJ-Version angezeigt:


Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 51

Ggf. wird die Übernahme von Einstellungen einer früheren Version angeboten:

Nun erscheint der Welcome-Dialog mit dem voreingestellten Dracula-Farbschema:

Im Manuskript wird das Farbschema IntelliJ Light verwendet, das nach einem Klick auf Cus-
tomize gewählt werden kann:
52 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Wir klicken auf der Projects-Seite des Welcome-Dialogs auf den Schalter New Project, um mit
dem ersten IntelliJ-Projekt zu beginnen.

2.4.2 Projekt anlegen


Wir legen ein neues Projekt aus der voreingestellten Kategorie Java an:

Ein Projekt benötigt ein SDK (Software Development Kit), das die Standardbibliothek, den Compi-
ler und die JVM zur Ausführung des Projekts innerhalb der Entwicklungsumgebung bereitstellt.
Weil wir im Abschnitt 1.2.1 die OpenJDK 8 - Distribution der Firma Red Hat und im Abschnitt 2.1
die OpenJDK 17 - Distribution der Firma Oracle installiert haben, stehen uns per Drop-Down-Liste
zwei SDKs zur Verfügung:
• OpenJDK 8 (alias 1.8, mit maximaler Kompatibilität)
• OpenJDK 17 (mit maximaler Aktualität)
Beim ersten Projekt entscheiden wir uns für das OpenJDK 8 (alias 1.8).
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 53

Bei Groovy und Kotlin handelt es sich um alternative Programmiersprachen für die JVM, an denen
wir in diesem Kurs nicht interessiert sind, sodass wir im Bereich Additional Libraries and
Frameworks keine Markierungen vornehmen.
Im nächsten Dialog markieren wir das Kontrollkästchen Create project from template und ak-
zeptieren die einzige Option (Command Line App) per Next:

Die Wahl dieser Projektvorlage hat zur Folge, dass im entstehenden Projekt automatisch eine zu
unserer Zielsetzung passende Java-Klasse samt main() - Methode angelegt wird, sodass wir an-
schließend etwas Aufwand sparen.
Wir wählen einen Projektnamen, übernehmen den resultierenden Projektordner und verzichten auf
ein Basispaket, z. B.:1

Nach einem Klick auf Finish erscheint die Entwicklungsumgebung, ist aber noch ein Weilchen mit
Projektvorbereitungsarbeiten beschäftigt (siehe Fortschrittsbalken zum Indizieren in der Statuszei-
le):

1
Durch den Verzicht auf ein Basispaket ergibt sich eine einfache Lernumgebung. Soll ein Programm veröffentlicht
werden, ist ein Basispaket sehr zu empfehlen (siehe Kapitel 6).
54 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Schließlich ist IntelliJ einsatzbereit und präsentiert im Editor den basierend auf der Vorlagendefini-
tion (Command Line App) erstellten Quellcode der Klasse Main mit bereits vorhandener Start-
methode main():

Im Project-Fenster sind u. a. zu sehen:


• HalloIntelliJ
Dieser Knoten (mit dem Symbol ) repräsentiert das Projekt und das primäre (automatisch
erstellte) Modul. Ein IntelliJ-Projekt kann mehrere Module enthalten, z. B. bei einer Server-
Client - Lösung jeweils ein Modul für die Server- und die Client-Komponente.1 Bei den In-
telliJ-Projekten im Kurs werden wir aber meist mit einem IntelliJ-Modul auskommen. Das
primäre Modul hat per Voreinstellung denselben Namen wie das Projekt, kann aber umbe-
nannt werden. Dann erscheint der Modulname hinter dem Projektnamen zwischen eckigen
Klammern.

1
In IntelliJ IDEA konnten Projekte schon immer mehrere Module enthalten, wobei diese Module im Sinne der Ent-
wicklungsumgebung nicht verwechselt werden dürfen mit den seit Java 9 vorhandenen Modulen der Programmier-
sprache (siehe Abschnitt 6.2). Letztere ergänzen die Pakete durch eine zusätzliche Ebene zur Zusammenfassung und
Abschottung von Java-Typen.
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 55

o .idea
Dieser Ordner enthält die projekt-bezogene Einstellungen in mehreren XML-
Dateien.
o src
In diesem Ordner befinden sich die Quellcodedateien des primären Moduls.
o HalloIntelliJ.iml
In dieser Datei befindet sich die Konfiguration des primären Moduls.
• External Libraries
Als externe Bibliothek verwendet unser Projekt nur die Standardbibliothek aus dem einge-
stellten SDK.
Weitere Projekte lassen sich entweder über den IntelliJ-Startdialog oder über den folgenden Menü-
befehl anlegen:
Start > New > Project

2.4.3 Quellcode-Editor
Um das von IntelliJ erstellte Programm zu vollenden, müssen wir im Editor noch die Ausgabean-
weisung
System.out.println("Hallo allerseits!");
verfassen (vgl. Abschnitt 2.2.1).

2.4.3.1 Syntaxerweiterung
Dabei ist die Syntaxerweiterung von IntelliJ eine große Hilfe. Wir löschen den aktuellen Inhalt der
Zeile 4 (einen Kommentar), nehmen durch zwei Tabulatorzeichen (Taste ) eine Einrückung
1
vor und beginnen, den Klassennamen System zu schreiben. IntelliJ IDEA erkennt unsere Ab-
sicht und präsentiert eine Liste möglicher Erweiterungen, in der die am besten passende Erweite-
rung hervorgehoben und folglich per Enter-Taste wählbar ist:

Sobald wir einen Punkt hinter den Klassennamen System setzen, erscheint eine neue Liste mit allen
zulässigen Fortsetzungen, wobei wir uns im Beispiel für die Klassenvariable out entscheiden, die
auf ein Objekt der Klasse PrintStream zeigt:2

1
Bei Bedarf lassen sich die Zeilennummern folgendermaßen einschalten:
File > Settings > Editor > General > Appearance > Show line numbers
2
In der ersten Vorschlagsliste mit Bestandteilen der Klasse System erscheint die von uns häufig benötigte Klassenva-
riable out noch nicht an der bequemen ersten Position, doch passt sich IntelliJ schnell an unsere Gewohnheiten an.
56 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Wir übernehmen das Ausgabeobjekt per Enter-Taste oder Doppelklick und setzen einen Punkt hin-
ter seinen Namen (out). Jetzt werden u. a. die Instanzmethoden der Klasse PrintStream aufgelistet,
und wir wählen die Variante (spätere Bezeichnung: Überladung) der Methode println() mit einem
Parameter vom Typ String, die sich zur Ausgabe einer Zeichenfolge eignet:

Ein durch doppelte Hochkommata begrenzter Text komplettiert den println() - Methodenaufruf,
den wir objektorientiert als Nachricht an das Objekt System.out auffassen.
Die Syntaxerweiterung von IntelliJ macht Vorschläge für Variablen, Typen, Methoden usw. Sollte
sie nicht spontan tätig werden, kann sie mit der folgenden Tastenkombination angefordert werden:
Strg + Leertaste
Soll mit Hilfe der Syntaxerweiterung eine Anweisung nicht fortgesetzt, sondern geändert werden,
dann quittiert man einen Vorschlag nicht per Enter-Taste oder Doppelklick, sondern per Tabulator-
taste ( ). Auf diese Weise wird z. B. ein Methodenname ersetzt, in dem sich die Einfügemarke
gerade befindet, statt durch Einfügen des neuen Namens ein fehlerhaftes Gebilde zu erzeugen.
Mit der Tastenkombination
Strg + Umschalt + Enter
fordert man die Vervollständigung einer Anweisung an, z. B.:
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 57

vorher nachher

Obwohl das vervollständigte Programm auf der rechten Seite fehlerfrei ist, unterschlängelt IntelliJ
das Wort „allerseits“. Kommentare, Klassennamen etc. werden per Voreinstellung von der Entwick-
lungsumgebung auf die Einhaltung der englischen Rechtschreibung überprüft. Wir schreiben Deng-
lisch (mal deutsch, mal englisch) und kümmern uns nicht um die Kritik an unserer Orthographie.

2.4.3.2 Code-Inspektion und Quick-Fixes


IntelliJ führt Code-Inspektionen on the fly durch, macht auf potentielle Probleme aufmerksam und
schlägt QuickFix-Korrekturen vor.
Wenn IntelliJ IDEA eine Code-Änderung vorschlagen möchte, dann erscheint eine gelbe Birne
links neben der betroffenen Stelle, z. B.:

Zeigt die Maus auf die Birne, kann ein Drop-Down - Menü mit Korrekturvorschlägen geöffnet wer-
den, z. B.:

Statt das Drop-Down - Menü zur gelben Birne zu öffnen, kann man auch die Einfügemarke auf den
markierten Syntaxbestandteil setzen und die Tastenkombination Alt + Enter betätigen, um dieselbe
Vorschlagsliste zu erhalten. Im Beispiel muss der Name der Methode main() klein geschrieben
werden, damit sie als Startmethode akzeptiert wird.
Wenn IntelliJ IDEA einen Syntaxfehler findet, dann erscheint eine rote Birne links neben der be-
troffenen, durch rote Schrift markierten Stelle, z. B.:

Zeigt die Maus auf die Birne, kann ein Drop-Down - Menü mit Korrekturvorschlägen geöffnet wer-
den, z. B.:
58 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Statt das Drop-Down - Menü zur roten Birne zu öffnen, kann man auch die Einfügemarke auf den
rot gefärbten Syntaxbestandteil setzen und die Tastenkombination Alt + Enter betätigen, um die-
selbe Vorschlagsliste zu erhalten. Im Beispiel muss der Name der Klassenvariablen out korrekt
geschrieben werden.

2.4.3.3 Live Templates


Häufig benötigte Code-Schnipsel (z. B. System.out.println();) kann IntelliJ über sogenannte
Live Templates (neudeutsch: Live-Vorlagen) produzieren. Wenn Sie eine neue Zeile mit dem Buch-
staben s starten, dann erscheint eine Liste mit allen durch Vervollständigung herstellbaren Live
Templates. Nötigenfalls kann die Liste mit der Tastenkombination Strg + J angefordert werden.
Wählen Sie aus der Liste

die Alternative sout per Enter-Taste, per Doppelklick oder per Tabulatortaste ( ). Daraufhin
erstellt IntelliJ einen Methodenaufruf, den Sie nur noch um die auszugebende Zeichenfolge erwei-
tern müssen:

Wenn Sie die Vorlagenbezeichnung sout komplett eintippen, präsentiert IntelliJ eine Vorschlagslis-
te mit dem passenden Element in führender Position:
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 59

Um die Anweisung
System.out.println();
zu erstellen, müssen Sie also nur sout schreiben und den markierten Vorschlag per Enter-Taste, per
Doppelklick oder per Tabulatortaste ( ) übernehmen.
Nach
File > Settings > Editor > Live Templates
kann man im folgenden Dialog

die vorhandenen Java-Vorlagen einsehen und konfigurieren sowie neue Vorlagen erstellen.

2.4.3.4 Orientierungshilfen
Zeigt man bei gedrückter Strg-Taste mit dem Mauszeiger auf eine Methode, dann erscheint der
Definitionskopf, z. B.:
60 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Wenn für das Projekt-SDK ein Pfad zur Online-Dokumentation eingetragen wurde (siehe Abschnitt
2.4.6.1), dann kann man die Dokumentation zu einem API-Bestandteil (z.B. zu einer Klasse oder
Methode) folgendermaßen in einem Browser-Fenster öffnen:
• Einfügemarke auf den interessierenden API-Bestandteil setzen
• Tastenkombination Umschalt + F1
Setzt man bei gedrückter Strg-Taste einen Mausklick auf einen Bezeichner (z. B. Klasse, Variable,
Methode), dann springt IntelliJ zur Implementierung des angefragten Syntaxbestandteils. Nötigen-
falls wird der Quellcode der zugehörigen Klasse in ein neues Registerblatt des Editors geladen,
z. B.:

Zum selben Ziel kommt man auch ohne Mausbeteiligung:


• Einfügemarke auf den interessierenden Bezeichner setzen
• Tastenkombination Strg + B
Über die Tastenkombination Strg + F12 oder mit dem Menübefehl
Navigate > File Structure
erhält man einen Dialog mit der Struktur der aktuell im Editor bearbeiteten Klasse und kann Be-
standteile per Mausklick ansteuern:
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 61

Um den Quellcode einer beliebigen Klasse aus dem Projekt-SDK anzufordern, trägt man ihren
Namen(sanfang) nach der Tastenkombination Strg + N in das Suchfeld des folgenden Dialogs ein

und wählt (z. B. per Doppelklick) ein Element aus der Liste mit kompatiblen Namen.

2.4.3.5 Refaktorieren
Um z. B. einen Variablen- oder Klassennamen an allen Auftrittsstellen im Projekt über das soge-
nannte Refaktorieren zu ändern, setzt man die Einfügemarke auf ein Vorkommen des Namens,
drückt die Tastenkombination Umschalt + F6, ändert den Namen und quittiert mit der Eingabetas-
te. Im Menüsystem ist die Refaktorierungsfunktion hier zu finden:
Refactor > Rename
Die im Refactor-Menü zahlreich vorhandenen weiteren IntelliJ-Kompetenzen zur Quellcode-
Umgestaltung werden wir im Kurs nicht benötigen.

2.4.3.6 Sonstige Hinweise


Im IntelliJ-Quellcodeeditor lassen sich die letzten Änderungen zurücknehmen mit der von vielen
Programmen gewohnten Tastenkombination Strg + Z. Soll eine zurückgenommenen Änderung
wiederhergestellt werden, ist offiziell die folgende Tastenkombination zu verwenden:
Strg + Umschalt + Z
In dieser Situation sollte nicht die (z. B. aus Microsoft Office) gewohnte Tastenkombination Strg +
Y verwendet werden, weil IntelliJ daraufhin die aktuelle Zeile löscht.

2.4.4 Übersetzen und Ausführen


Nun soll die im Editorfenster angezeigte Startklasse unseres Beispielprogramms übersetzt und aus-
geführt werden. Genau genommen entscheidet die eingestellte Configuration über die zu verwen-
dende Startklasse. Im Beispiel sollte die aktive Konfiguration den Namen Main haben und die
gleichnamige Startklasse verwenden (siehe Symbolleiste über dem Editor):

Zum Starten klicken wir auf den grünen Run-Schalter neben der Konfiguration oder verwenden die
Tastenkombination
Umschalt + F10
IntelliJ verwendet per Voreinstellung den Compiler im Projekt-SDK, um im Beispiel aus der Quell-
codedatei Main.java die Bytecode-Datei Main.class zu erstellen:
62 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Nach der Übersetzung, die im Rahmen einer Projekterstellung stattfindet, erscheint das Run - Fens-
ter von IntelliJ:

Es zeigt in der Statuszeile das Erstellungsergebnis mit Zeitaufwand und im Inhaltsbereich ...
• die ausführende Laufzeitumgebung (JVM),1
• die Ausgabe des Programms
• und den Exit-Code, wobei die 0 für eine fehlerfreie Ausführung steht.
Der beim Erstellen erzeugte Ausgabeordner wird im Project-Fenster angezeigt:

1
Das zum Starten des Programms verwendete Kommando verrät, dass ein sogenannter Java-Agent im Spiel ist, wenn
das Programm innerhalb der Entwicklungsumgebung ausgeführt wird:

Er kann Daten über das Programm sammeln (z. B. zum Speicherbedarf von Objekten) und der Entwicklungsumge-
bung zur Verfügung stellen. Darum müssen wir uns im Augenblick nicht kümmern.
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 63

Öffnet man das Build-Fenster per Mausklick auf die gleichnamige Schaltfläche über der Statuszei-
le, dann erfährt man u. a., dass IntelliJ den Compiler javac.exe aus dem OpenJDK benutzt hat:

Bei der Ausführung einer unveränderten Quelle ist keine Übersetzung erforderlich, und im Build
Output wird dementsprechend keine Compiler-Version angezeigt. Um in dieser Lage eine Doku-
mentation des Compilers im Build Output zu erhalten, kann man ...
• vor der nächsten Ausführung den Quellcode modifizieren
• oder mit dem Menübefehl Build > Rebuild Project eine Übersetzung erzwingen.

2.4.5 Sichern und Wiederherstellen


Bei der Arbeit mit IntelliJ muss man sich um das Sichern von Quellcode und anderen im Editor
bearbeiteten Dateien kaum Gedanken machen, weil die Entwicklungsumgebung bei jeder passenden
Gelegenheit (z. B. beim Erstellen des Projekts) automatisch sichert.
Außerdem ist ein VCS (Version Control System) integriert, das lokal arbeiten und mit Cloud-
Diensten wie GitHub kooperieren kann. Wegen der zahlreichen Funktionen ist ein eigenes Haupt-
menü namens VCS vorhanden.
Für eine Quellcodedatei sind über
VCS > VCS Operations > Local History > Show History
oder über
File > Local History > Show History
alle Zwischenstände verfügbar, z. B.:

Um zu einem vorherigen Zustand zurückzukehren, wählt man aus seinem Kontextmenü das Item
Revert.
64 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

2.4.6 Konfiguration
Von den zahlreichen Einstellungsmöglichkeiten in unserer Entwicklungsumgebung wird anschlie-
ßend nur eine kleine Auswahl beschrieben.

2.4.6.1 SDKs einrichten


Ein IntelliJ-Projekt benötigt ein SDK (Software Development Kit), das die Standardbibliothek, den
Compiler und die JVM zur Ausführung des Programms innerhalb der Entwicklungsumgebung fest-
legt. Wir haben im Abschnitt 2.4.2 bei der Kreation des ersten Projekts ein von IntelliJ automatisch
angelegtes SDK basierend auf dem im Abschnitt 1.2.1 installierten OpenJDK 8 gewählt. Ebenso hat
IntelliJ das gemäß Abschnitt 2.1.1 installierte OpenJDK 17 entdeckt und als SDK eingerichtet. Eine
Liste der bekannten und in Projekten wählbaren SDKs erhält man z. B. über
File > Project Structure > Platform Settings > SDKs
im folgenden Dialog, der primär dazu gedacht ist, für neue Projekte ein voreingestelltes SDK fest-
zulegen:

Über diesen Dialog können aber auch SDKs konfiguriert oder ergänzt werden. Es ist z. B. sinnvoll,
die vorhandenen SDKs so zu benennen (siehe Bildschirmfoto), dass die Kursbeispiele problemlos
geöffnet werden können (vgl. Abschnitt 2.4.7).
Außerdem sollte zu jedem SDK eine Internet-Adresse mit der offiziellen API-Beschreibung als
Documentation Path eingetragen werden. Klickt man bei aktiver Registerkarte Documenta-
tion Paths auf den Schalter mit Plussymbol und Weltkugel, dann erscheint ein Fenster mit ei-
nem Textfeld für die Dokumentationsadresse. Beim OpenJDK 17 bewährt sich z. B. der folgende
Eintrag:1

1
Als Text für die Übernahme per Copy & Paste:
OpenJDK 8: https://docs.oracle.com/javase/8/docs/api/
OpenJDK 11: https://docs.oracle.com/en/java/javase/11/docs/api/
OpenJDK 17: https://docs.oracle.com/en/java/javase/17/docs/api/
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 65

Aufgrund der resultierenden Einstellung

kann IntelliJ bei der Arbeit mit dem Quellcode-Editor nach der Tastenkombination Umschalt +
F1 zu dem die Einfügemarke enthaltenen Java-Bezeichner die API-Dokumentation in einem exter-
nen Browser-Fenster liefern.
Wir verwenden im Kurs meist ...
• das gemäß Abschnitt 1.2.1 installierte OpenJDK 8, wenn minimale Voraussetzungen bzgl.
der Laufzeitumgebung erwünscht sind,
• das gemäß Abschnitt 2.1.1 installierte OpenJDK 17, wenn alle aktuellen Java-
Sprachmerkmale genutzt werden sollen.
Die in IntelliJ 2021.2 enthaltene OpenJDK-Version 11.0.12 mit dem Startverzeichnis
C:\Program Files\JetBrains\IntelliJ IDEA Community Edition 2021.2\jbr
kann ebenfalls als SDK für Projekte verwendet werden. Damit stehen uns die drei momentan ver-
fügbaren LTS-Version von Java (8, 11 und 17) als SDKs zur Verfügung. Wenn Sie in Ihrer IntelliJ-
Installation SDKs mit diesen Hauptversionen und mit den Namen OpenJDK8, OpenJDK11 sowie
OpenJDK17 einrichten, dann sollten Sie alle im Kurs angebotenen Beispielprojekte problemlos in
IntelliJ öffnen können.
Um das in IntelliJ enthaltene OpenJDK 11 als SDK - Option für neue Projekte zu vereinbaren, kli-
cken wir auf das - Symbol am oberen Fensterrand und wählen den Typ JDK:

Dann wählen wir den oben angegebenen SDK-Basisordner:


66 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Wir vereinbaren den SDK-Namen OpenJDK 11 und nötigenfalls den folgenden Documentation
Path
https://docs.oracle.com/en/java/javase/11/docs/api/

2.4.6.2 Struktur des aktuellen Projekts


Nach dem Menübefehl
File > Project Structure
sind im folgenden Dialog

unter Project Settings wichtige Konfigurationen möglich:


• Project name
Hier lässt sich der Projektname ändern.
• Project SDK
Hier wählt man das für die Übersetzung und Ausführung des aktuellen Projekts zu verwen-
dende SDK. Man legt also den Compiler und die JVM fest.
• Project language level
Hier legt man die vom Compiler zu unterstützende Java-Version und damit auch das Verhal-
ten der IntelliJ-Syntaxerweiterung fest. Dabei muss auf Kompatibilität geachtet werden. Es
macht z. B. keinen Sinn, das OpenJDK 8 als SDK zu verwenden und gleichzeitig das
Sprachniveau 11 zu verlangen. Es ist hingegen möglich, ein Sprachniveau unterhalb der ein-
gestellten SDK-Version zu wählen. Dann kann man sich in selbst definierten Klassen auf die
ältere Syntax beschränken, aber trotzdem Bibliotheksklassen einbinden, die mit einem höhe-
ren language level (also von einer entsprechenden Compiler-Version) übersetzt worden
sind.
Damit ein ausgeliefertes Programm auf einem Kundenrechner von der dortigen JVM ausgeführt
werden kann, dürfen alle ausgelieferten Klassen maximal das Compiler-Niveau der angetroffenen
JVM haben. Bei einer mit dem Sprachniveau 11 erstellten Anwendung scheitert der Start auf einem
Kundenrechner mit der JVM 8 verständlicherweise mit einer Fehlermeldung:
Exception in thread "main" java.lang.UnsupportedClassVersionError: Bruchaddition
has been compiled by a more recent version of the Java Runtime
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 67

2.4.6.3 Einstellungen für IntelliJ oder das aktuelle Projekt


Über den Menübefehl
File > Settings
oder die Tastenkombination Strg + Alt + S erreicht man den Settings-Dialog:

Hier lassen sich diverse Einstellungen modifizieren, die sich entweder auf die Entwicklungsumge-
bung oder auf das aktuelle Projekt beziehen. In der Abteilung Editor gehört z. B. die Schriftart
(Font) zu den IDE-Einstellungen und die Dateicodierung (File Encodings) zu den Projekt-
Einstellungen, die am Symbol zu erkennen sind:

IntelliJ verwendet unter Windows für Java-Quellcodedateien per Voreinstellung die UTF-8 - Codie-
rung (ohne Byte Order Mark, BOM), sodass bei der Übertragung der Dateien auf einen Entwick-
lungsrechner mit einem anderen Betriebssystem (macOS, Linux oder UNIX) keine Codierungsin-
kompatibilität stört.
Nach
File > Settings > Editor > File Encodings
wird bei einem neuen Projekt windows-1252 als Project Encoding angezeigt:

Für die im neuen Projekt entstehenden Java-Quellcodedateien wird aber trotzdem die UTF-8 - Co-
dierung verwendet. Damit bei neuen Projekten UTF-8 als Project Encoding erscheint,
68 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

muss nach
File > New Projects Setup > Settings for New Projects > Editor > File Encodings
die gewünschte Einstellung bei Project Encoding vorgenommen werden:

Nach
File > Settings > Build, Execution, Deployment > Compiler > Java-Compiler
kann im folgenden Dialog z. B. für das aktuelle Projekt der voreingestellte Compiler javac.exe aus
dem Projekt-SDK durch den Compiler aus der Open Source - Entwicklungsumgebung Eclipse er-
setzt werden:
Abschnitt 2.4 Java-Entwicklung mit IntelliJ IDEA 69

Die IDE-Konfiguration zu IntelliJ 2021.2.x für den Windows-Benutzer otto landet im folgenden
Ordner:
C:\Users\otto\AppData\Roaming\JetBrains
Weitere benutzer-bezogene Einstellungen befinden sich im Ordner:
C:\Users\otto\AppData\Local\JetBrains\IdeaIC2021.2
Die Einstellungen zu einem Projekt befinden sich im .idea - Unterordner des Projekts, also z. B. in:
C:\Users\otto\IdeaProjects\Hallo\.idea
Sollte die IDE-Konfiguration einmal außer Kontrolle geraten, kann man über den Menübefehl
File > Manage IDE Settings > Restore Default Settings
den Ausgangszustand wiederherstellen:

Allerdings werden dabei nur die Einstellungen unter


...\Appdata\Roaming\JetBrains
berücksichtigt, die Einstellungen unter
...\Appdata\Local\JetBrains
hingegen nicht. Wird z. B. ...
• ein mit Hilfe der Vorlage Command Line App erstelltes IntelliJ-Projekt geschlossen,
• IntelliJ beendet, der Projektordner gelöscht und IntelliJ neu gestartet,
• ein neues Projekt mit dem alten Namen am alten Ort unter Verwendung der Vorlage Com-
mand Line App erstellt,
dann resultiert eine defekte Projekt-Konfiguration, z. B.:

Wird in dieser Situation nach dem Beenden von IntelliJ der Ordner
...\Appdata\Local\JetBrains
gelöscht, dann lässt sich das neue Projekt mit dem gewünschten alten Namen fehlerfrei anlegen.
Man verliert dabei die von IntelliJ benötigten und bei Bedarf automatisch erstellten Indizes zu den
in Projekten verwendeten JDKs, sodass für jeden neu zu erstellenden Index ca. eine Minute Warte-
zeit anfällt.
In diesem Zusammenhang ist das IntelliJ-Angebot zur Beschleunigung der Indizierung durch das
Herunterladen gemeinsamer Indizes zu erwähnen:
70 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

In der Community Edition von IntelliJ können die pre-built shared indices allerdings nur 30
Tage lang genutzt werden.1 Weil ein in mehreren Projekten verwendetes JDK nur einmal indiziert
werden muss, können wir problemlos auf die pre-built shared indices verzichten.

2.4.6.4 Einstellungen für neue Projekte


Über
File > New Projects Setup > Settings for New Projects
legt man die Einstellungen für neue Projekte fest. Man kann z. B. im Abschnitt
Editor > Code Style > Java
dafür sorgen, dass im Editor für Java-Quellcode das Tabulatorzeichen nicht durch mehrere Leerzei-
chen ersetzt wird:

Diese Einstellung bleibt allerdings ohne Effekt, wenn IntelliJ in einer bereits vorhandenen Datei
durch Leerzeichen realisierte Einrückungen antrifft. Soll die Tabulatortaste auch dort ein Tabulator-
zeichen produzieren, muss im Abschnitt
Editor > Code Style
des Settings-Dialogs das Kontrollkästchen bei Detect and use existing file indents for edit-
ing entfernt werden:

1
https://www.jetbrains.com/help/idea/shared-indexes.html#plugin-note
Abschnitt 2.5 OpenJFX und Scene Builder installieren 71

Gerade wurden Einstellungen zur Tabulatorbehandlung bei neuen Projekten beschrieben. Für vor-
handene Projekte sind nach
File > Settings > Editor > Code Style
analoge Einstellungen möglich.

2.4.7 Übungsprojekte zum Kurs verwenden


Die im Kurs angebotenen IntelliJ-Übungsprojekte lassen sich auf Ihrem Rechner mit der dortigen
IntelliJ-Installation aus den kopierten Projektordnern öffnen, wenn auf Ihrem Rechner ...
• das OpenJDK 8 (zum Bezug und zur Installation siehe Abschnitt 1.2.1), das OpenJDK 11
(siehe Abschnitt 2.4.6.1) sowie das OpenJDK 17 (zum Bezug und zur Installation siehe Ab-
schnitt 2.1.1) installiert sind,
• und in IntelliJ für diese SDKs die Namen OpenJDK 8, OpenJDK 11 sowie OpenJDK 13
vereinbart sind.
Zur Einrichtung von SDKs in IntelliJ siehe Abschnitt 2.4.6.1.
Einige Übungsprojekte verwenden die selbst erstellte, nicht zum Java-API gehörige Klasse Simput
zur Vereinfachung der Konsoleneingabe. Im Abschnitt 3.4.2 ist zu erfahren, wie die Java-
Archivdatei Simput.jar mit der Klasse Simput als IntelliJ-globale Bibliothek eingerichtet wird,
sodass die Klasse Simput in einem Projekt durch das Einbinden dieser Bibliothek bequem zu nut-
zen ist.

2.5 OpenJFX und Scene Builder installieren


Wir wollen bei der Entwicklung von Programmen mit grafischer Bedienoberfläche die JavaFX-
Bibliothek einsetzen. Seit Java 11 ist diese Bibliothek kein JDK-Bestandteil mehr, wird aber unter
dem Namen OpenJFX als Open Source (unter derselben Lizenz wie das OpenJDK) aktiv weiter-
entwickelt, wobei sich besonders die Firma Gluon engagiert.1 Hier
https://gluonhq.com/products/javafx/
steht am 20.10.2021 die OpenJFX-Version 17.0.1 zur Verfügung. Wir beziehen die Variante Ja-
vaFX Windows SDK in der Datei openjfx-17.0.1_windows-x64_bin-sdk.zip und packen diese im
folgenden Ordner aus:
C:\Program Files\Java\OpenJFX-SDK-17

1
Im Manuskript werden die Bezeichnungen JavaFX und OpenJFX synonym verwendet.
72 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

Wie es der Bestandteil windows im Namen der heruntergeladenen Datei vermuten lässt, sind die
mit diesem SDK erstellten JavaFX-Programme wegen der enthaltenen nativen Bibliotheken (DLL-
Dateien) nur unter Windows zu verwenden. Um eine Multi-Plattform - JavaFX-Anwendung zu er-
stellen, muss man auch die SDK-Varianten für Linux und macOS herunterladen.
Weil die gemäß Abschnitt 1.2.1 installierte OpenJDK 8 - Distribution aus dem Open Source - Pro-
jekt ojdkbuild ein OpenJFX-SDK enthält, können wir JavaFX-Anwendungen für die LTS-
Versionen Java 8 und 17 erstellen.
Beim Einsatz der JavaFX-Technik wird die Bedienoberfläche in der Regel in einer FXML-Datei
deklariert. Deren Gestaltung wird erheblich erleichtert durch das unter der BSD-Lizenz stehende
Programm Scene Builder, das von der Firma Oracle entwickelt wurde und mittlerweile von der
Firma Gluon gepflegte wird. Es steht auf der folgenden Webseite zur Verfügung:
https://gluonhq.com/products/scene-builder/
Aktuell (im Oktober 2021) werden die Version 8.5.0 (für Java 8) sowie die Version 17.0.0 (für Java
ab Version 11) angeboten. Wir beschränken uns auf die Version 17.0.0, wählen das Format
Windows Installer und erhalten somit die Datei SceneBuilder-17.0.0.msi.
Wir starten die Installation per Doppelklick auf diese MSI-Datei und akzeptieren die Lizenzbedin-
gungen:

Es wird eine Installation im Windows-Profil des angemeldeten Benutzers (also mit vorhandenen
Schreibrechten) vorgeschlagen. Das ist akzeptabel, sofern nicht mehrere Personen mit dem Pro-
gramm arbeiten sollen:

Die Installation belegt ca. 120 MB ist flott erledigt:


Abschnitt 2.5 OpenJFX und Scene Builder installieren 73

Damit IntelliJ IDEA mit dem Scene Builder kooperieren kann, muss nach dem Menübefehl
File > Settings > Languages & Frameworks > JavaFX
der Pfad zum ausführbaren Programm bekanntgegeben werden, z. B.:1

Eine erste Verwendung des Scene Builders werden Sie im Abschnitt 4.9 erleben. Ein Blick auf die
Arbeitsoberfläche des GUI-Designers mit dem geöffneten Fenster des im Abschnitt 1.2.3 vorge-
stellten Bruchadditionsprogramms lässt erkennen, dass wir zur Entwicklung attraktiver Programme
ein modernes Werkzeug zur Verfügung haben:

1
Von Rolf Schwung stimmt der Tipp, bei der Installation und Konfiguration von JavaFX in IntelliJ unter Linux die
Anleitung auf der folgenden Webseite von Michael Kofler zu beachten:
https://kofler.info/java-11-javafx-intellij-idea-und-linux/
74 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen

2.6 Übungsaufgaben zum Kapitel 2


1) Führen Sie nach Möglichkeit auf Ihrem eigenen PC die in den Abschnitten 1.2.1 (OpenJDK 8),
2.1 (OpenJDK 17) und 2.3 (IntelliJ 2021.2.2, inkl. OpenJDK 11) beschriebenen Installationen aus.
Richten Sie in IntelliJ basierend auf den installierten OpenJDK-Versionen jeweils ein SDK ein (sie-
he Abschnitt 2.4.6.1).
OpenJFX (im OpenJDK 8 aus dem ojdkbuild-Projekt enthalten, im OpenJDK 11 und im OpenJDK
17 aber nicht) sowie den Scene Builder benötigen wir erst zur Erstellung von Programmen mit gra-
fischer Bedienoberfläche. Auf die im Abschnitt 2.5 beschriebenen Installationen können Sie also
vorläufig verzichten.

2) Experimentieren Sie mit dem Hallo-Beispielprogramm aus dem Abschnitt 2.2.1, z. B. indem Sie
weitere Ausgabeanweisungen ergänzen.

3) Beseitigen Sie die Fehler in der folgenden Variante des Hallo-Programms:


class Hallo {
static void mein(String[] args) {
System.out.println("Hallo allerseits!);
}
Abschnitt 2.6 Übungsaufgaben zum Kapitel 2 75

4) Welche der folgenden Aussagen sind richtig bzw. falsch?


1. Beim Übersetzen einer Java-Quellcodedatei mit dem OpenJDK-Compiler javac.exe muss
man den Dateinamen samt Erweiterung (.java) angeben.
2. Beim Starten eines Java-Programms muss man den Namen der auszuführenden Klasse samt
Extension (.class) angeben.
3. Damit der Aufruf des OpenJDK-Compilers javac.exe (ohne Pfadangabe) von jedem Ver-
zeichnis aus klappt, muss unter Windows das bin-Unterverzeichnis der OpenJDK-
Installation vorrangig in die Definition der Umgebungsvariablen PATH aufgenommen wer-
den.
4. Die main() - Methode der Startklasse eines Java-Programms muss einen Parameter mit dem
Datentyp String[] und dem Namen args besitzen, damit sie von der JVM erkannt wird.

5) Kopieren Sie die Bytecode-Datei


…\BspUeb\Simput\Standardpaket\Simput.class
mit der Klasse Simput auf Ihren PC, und tragen Sie das Zielverzeichnis in den CLASSPATH ein
(siehe Abschnitt 2.2.4). Testen Sie den Zugriff auf die class-Datei z. B. mit der Konsolenvariante
des Bruchadditionsprogramms (siehe Abschnitt 1.2.2).
Alternativ können Sie auch die Java-Archivdatei
…\BspUeb\Simput\Standardpaket\Simput.jar
auf Ihren PC kopieren und in den Klassenpfad aufnehmen. Mit Java-Archivdateien werden wir uns
noch ausführlich beschäftigen.
3 Elementare Sprachelemente
Im Kapitel 1 wurde anhand eines halbwegs realistischen Beispiels ein erster Eindruck von der ob-
jektorientierten Software-Entwicklung mit Java vermittelt. Nun erarbeiten wir uns die Details der
Programmiersprache Java und beginnen dabei mit elementaren Sprachelementen. Diese dienen zur
Realisation von Algorithmen innerhalb von Methoden und sehen bei Java nicht wesentlich anders
aus als bei älteren, nicht objektorientierten Sprachen (z. B. C).

3.1 Einstieg

3.1.1 Aufbau eines Java-Programms


Zunächst soll unser bisheriges Wissen über die Struktur von Java-Programmen zusammengefasst
werden:
• Ein Java-Programm besteht aus Klassen. Für das Bruchrechnungsbeispiel im Abschnitt 1.1
wurden die Klassen Bruch und Bruchaddition definiert. In den Methoden der beiden
Klassen kommen weitere Klassen zum Einsatz:
o Klassen aus der Standardbibliothek (z. B. System, Math)
o Die zur Erleichterung von Benutzereingaben in Konsolenprogrammen selbst erstellte
Klasse Simput
Meist verwendet man für den Quellcode einer Klasse jeweils eine eigene Textdatei mit der
Namenserweiterung .java.1 Der Compiler erzeugt grundsätzlich für jede Klasse eine eigene
Bytecode-Datei mit der Namenserweiterung .class.
• Eine Klassendefinition besteht aus …
o dem Kopf
Er enthält nach dem Schlüsselwort class den Namen der Klasse. Soll eine Klasse für
beliebige andere Klassen (aus fremden Paketen, siehe Kapitel 6) nutzbar sein, dann
muss dem Schlüsselwort class der Zugriffsmodifikator public vorangestellt werden,
z. B.:
public class Bruch {
. . .
}
o und dem Rumpf
Begrenzt durch ein Paar geschweifter Klammern befinden sich hier …
▪ die Deklarationen der Instanz- und Klassenvariablen (Eigenschaften)
▪ und die Definitionen der Methoden (Handlungskompetenzen).
• Auch eine Methodendefinition besteht aus …
o dem Kopf
Hier werden vereinbart: Modifikatoren, Rückgabetyp, Name der Methode, Parame-
terliste. All diese Bestandteile werden noch ausführlich erläutert.

1
Unsere Entwicklungsumgebung IntelliJ verwendet unter Windows für Quellcodedateien per Voreinstellung die
UTF-8 - Codierung (ohne Byte Order Mark, BOM), sodass bei der Übertragung der Dateien auf einen Entwick-
lungsrechner mit einem anderen Betriebssystem (macOS, Linux oder UNIX) keine Codierungsinkompatibilität stört.
Eine Änderung der Codierung ist möglich über:
File > Settings > Editor > File Encodings
78 Kapitel 3 Elementare Sprachelemente

o und dem Rumpf


Begrenzt durch ein Paar geschweifte Klammern befinden sich hier Anweisungen,
mit denen zur Realisation von Algorithmen z. B. Instanzvariablen das agierenden
Objekts verändert werden, wobei lokale Variablen zum Speichern von Zwischener-
gebnissen zum Einsatz kommen. Der Unterschied zwischen Instanzvariablen (Eigen-
schaften von Objekten), statischen Variablen (Eigenschaften von Klassen) und loka-
len Variablen von Methoden wird im Abschnitt 3.3 erläutert.
• Eine Anweisung ist die kleinste ausführbare Einheit eines Programms.
In Java sind bis auf wenige Ausnahmen alle Anweisungen mit einem Semikolon abzu-
schließen.
• Von den Klassen eines Programms muss eine startfähig sein.
Dazu benötigt sie eine Methode mit dem Namen main(), dem Rückgabetyp void, einer be-
stimmten Parameterliste (String[] args) sowie den Modifikatoren public und static. Im
Bruchrechnungsbeispiel im Abschnitt 1.1 ist die Klasse Bruchaddition startfähig.

3.1.2 Projektrahmen zum Üben von elementaren Sprachelementen


Während der Beschäftigung mit elementaren Java-Sprachelementen werden wir der Einfachheit
halber mit einer relativ untypischen, jedenfalls nicht sonderlich objektorientierten Programmstruk-
tur arbeiten, die Sie schon aus dem Hallo-Beispiel kennen (siehe Abschnitt 2.2.1). Es wird nur eine
Klasse definiert, und diese enthält nur eine einzige Methodendefinition. Weil die Klasse startfähig
sein muss, liegt der einzige Methodenkopf nach den im letzten Abschnitt wiederholten Regeln fest.
Weil die Klasse nicht für andere Klassen ansprechbar sein soll, ist der Zugriffsmodifikator public
für die Klasse überflüssig, und wir erhalten die folgende Programmstruktur:
class Prog {
public static void main(String[] args) {
//Platz für elementare Sprachelemente
}
}
Damit die pseudo-objektorientierten (POO-) Programme Ihren Programmierstil nicht prägen, wurde
an den Beginn des Manuskripts ein Beispiel gestellt (Bruchrechnung), das bereits etliche OOP-Prin-
zipien realisiert.
Für die meist kurzzeitige Beschäftigung mit bestimmten elementaren Sprachelementen lohnt sich
selten ein spezielles IntelliJ-Projekt. Legen Sie daher für solche Zwecke mit dem Menübefehl
File > New > Project
analog zu Abschnitt 2.4.2 ...
• ein Java-Projekt
• basierend auf dem OpenJDK 8
• unter Verwendung des Templates Command Line App
• mit dem Namen Prog
• ohne Base package
an. Den überflüssigen class-Modifikator public im automatisch erstellten Klassendefinitionskopf
können Sie löschen oder belassen.
Ändern Sie mit der im Abschnitt 2.4.3.5 beschriebenen Refaktorierung den Namen der vordefinier-
ten Klasse von Main in Prog, z. B. so:
Abschnitt 3.1 Einstieg 79

• Einfügemarke im Editor auf den alten Namen setzen


• Tastenkombination Umschalt + F6
• Neuen Namen eintragen und mit Enter quittieren
Wie die Beschriftung des Editorfensters zeigt, ist beim Refaktorieren ist auch der Name der Quell-
codedatei geändert worden. Weil die Quellcodedatei den empfohlenen Namen Prog.java trägt, wird
im src-Knoten des Project-Fensters nur der Klassenname angezeigt:

Wäre die Klasse Prog nicht in einer Datei namens Prog.java untergebracht, dann würde das Pro-
ject-Fenster beide Namen anzeigen, z. B.:

Das Symbol zur Klasse Prog enthält übrigens ein grünes Dreieck in der rechten oberen Ecke ( ),
weil diese Klasse startfähig ist.
Zum Üben elementarer Sprachelemente werden wir im Rumpf der main() - Methode passende An-
weisungen einfügen, z. B.:

Über das Symbol oder die Tastenkombination Umschalt + F10 lassen wir das Programm über-
setzen und ausführen:
80 Kapitel 3 Elementare Sprachelemente

Dabei wird die vorgegebene Ausführungskonfiguration verwendet. Wenn wir das Drop-Down -
Menü zur Ausführungskonfiguration öffnen und das Item Edit Configuration

wählen, dann stellt sich heraus, dass IntelliJ beim Refaktorieren auch die Start- bzw. Hauptklasse
(mit der Methode main()) angepasst hat. Man kann die Ausführungskonfiguration umbenennen
oder weitere Konfigurationen anlegen (z. B. mit Kommandozeilenargumenten):

3.1.3 Syntaxdiagramme
Um für Java-Sprachbestandteile (z. B. Definitionen oder Anweisungen) die Bildungsvorschriften
kompakt und genau zu beschreiben, werden wir im Manuskript u. a. sogenannte Syntaxdiagramme
einsetzen, für die folgende Vereinbarungen gelten:
Abschnitt 3.1 Einstieg 81

• Man bewegt sich in Pfeilrichtung durch das Syntaxdiagramm und gelangt dabei zu Recht-
ecken, die die an der jeweiligen Stelle zulässigen Sprachbestandteile angeben, wie z. B. im
folgenden Syntaxdiagramm zum Kopf einer Klassendefinition:

class Name

Modifikator

• Bei einer Verzweigung kann man sich für eine Richtung entscheiden, wenn nicht per Pfeil
eine Bewegungsrichtung vorgeschrieben ist. Zulässige Realisationen zum obigen Segment
sind also z. B.:
o class Bruchaddition
o public class Bruch
Verboten sind hingegen z. B. die folgenden Sequenzen:
o class public Bruchaddition
o Bruchaddition public class
• Für konstante (terminale) Sprachbestandteile, die aus einem Rechteck exakt in der angege-
benen Form in konkreten Quellcode zu übernehmen sind, wird fette Schrift verwendet.
• Platzhalter sind an kursiver Schrift zu erkennen. Im konkreten Quellcode muss anstelle des
Platzhalters eine zulässige Realisation stehen, und die zugehörigen Bildungsregeln sind an
anderer Stelle (z. B. in einem anderen Syntaxdiagramm) erklärt.
• Als Klassenmodifikator ist uns bisher nur der Zugriffsmodifikator public begegnet, der für
die allgemeine Verfügbarkeit einer Klasse sorgt. Später werden wir noch weitere Klassen-
modifikatoren kennenlernen. Sicher kommt niemand auf die Idee, z. B. den Modifikator
public mehrfach zu vergeben und damit gegen eine Java-Syntaxregel zu verstoßen. Das obi-
ge (möglichst einfach gehaltene) Syntaxdiagrammsegment lässt diese offenbar sinnlose Pra-
xis zu. Es bieten sich zwei Lösungen an:
o Das Syntaxdiagramm mit einem gesteigerten Aufwand präzisieren
o Durch eine generelle Regel die Mehrfachverwendung eines Modifikators verbieten
Im Manuskript wird die zweite Lösung verwendet.
Als Beispiele betrachten wir anschließend die Syntaxdiagramme zur Definition von Klassen und
Methoden. Aus didaktischen Gründen zeigen die Diagramme nur solche Sprachbestandteile, die im
Beispielprogramm von Abschnitt 1.1 (mit der Klasse Bruch) verwendet wurden. Durch den engen
Bezug zum Beispiel sollte es in diesem Abschnitt gelingen, …
• Syntaxdiagramme als metasprachliche Hilfsmittel einzuführen
• und gleichzeitig zur allmählichen Klärung der wichtigen Begriffe Klasse und Methode bei-
zutragen.
82 Kapitel 3 Elementare Sprachelemente

3.1.3.1 Klassendefinition
Wir arbeiten vorerst mit dem folgenden, leicht vereinfachten Klassenbegriff:
Klassendefinition

class Name { }

Modifikator
Felddeklaration

Methodendefinition

Solange man sich auf zulässigen Pfaden bewegt (immer in Pfeilrichtung, eventuell auch in Schlei-
fen), an den Stationen (Rechtecken) entweder den konstanten Sprachbestandteil exakt übernimmt
oder den Platzhalter auf zulässige (an anderer Stelle erläuterte) Weise ersetzt, entsteht eine syntak-
tisch korrekte Klassendefinition.
Als Beispiel betrachten wir die Klasse Bruch aus dem Abschnitt 1.1:
Modifikator Name

public class Bruch {

Feld- private int zaehler;


deklarationen private int nenner = 1;

public void setzeZaehler(int z) {


zaehler = z;
}

public boolean setzeNenner(int n) {


if (n != 0) {
nenner = n;
return true;
} else
return false;
}

public int gibZaehler() {return zaehler;}

Methoden- public int gibNenner() {return nenner;}


definitionen public void kuerze() {
. . .
}

public void addiere(Bruch b) {


. . .
}

public void frage() {


. . .
}

public void zeige() {


. . .
}

}
Abschnitt 3.1 Einstieg 83

3.1.3.2 Methodendefinition
Weil ein Syntaxdiagramm für die komplette Methodendefinition etwas unübersichtlich wäre, be-
trachten wir separate Diagramme für die Begriffe Methodenkopf und Methodenrumpf:
Methodendefinition

Methodenkopf Methodenrumpf

Methodenkopf

Rückgabetyp Name Parameter-


( )
deklaration
Modifikator
,

Methodenrumpf

{ }

Anweisung

Als Beispiel betrachten wir die Definition der Bruch-Methode addiere():


Rückgabe-
Modifikator Name Parameterdeklaration
typ

public void addiere(Bruch b) {


zaehler = zaehler * b.nenner + b.zaehler * nenner;
Anweisungen nenner = nenner * b.nenner;
kuerze();
}

Zur Erläuterung des Begriffs Parameterdeklaration beschränken wir uns vorläufig auf das Beispiel
in der addiere() - Definition. Es enthält einen Datentyp (Klasse Bruch) und einen Parameterna-
men (b).
In vielen Methoden werden sogenannte lokale Variablen (vgl. Abschnitt 3.3.4) deklariert, z. B. in
der Bruch-Methode kuerze():
public void kuerze() {
if (zaehler != 0) {
int az = Math.abs(zaehler);
int an = Math.abs(nenner);
. . .
zaehler = zaehler / az;
nenner = nenner / az;
} else
nenner = 1;
}
84 Kapitel 3 Elementare Sprachelemente

Weil wir bald u. a. die Variablendeklarationsanweisung kennenlernen werden, benötigt das Syn-
taxdiagramm zum Methodenrumpf jedoch (im Unterschied zum Klassendefinitionsdiagramm) kein
separates Rechteck für die Variablendeklaration.

3.1.4 Hinweise zur Gestaltung des Quellcodes


Zur Formatierung von Java - Programmen haben sich Konventionen entwickelt, die wir bei passen-
der Gelegenheit besprechen werden. Der Compiler ist hinsichtlich der Formatierung des Quellcodes
sehr tolerant und beschränkt sich auf folgende Regeln:
• Die einzelnen Bestandteile einer Definition oder Anweisung müssen in der richtigen Rei-
henfolge stehen.
• Zwischen zwei Sprachbestandteilen muss im Prinzip ein Trennzeichen stehen, wobei das
Leerzeichen, das Tabulatorzeichen und der Zeilenumbruch erlaubt sind. Diese Trennzeichen
dürfen sogar in beliebigen Anzahlen und Kombinationen auftreten. Innerhalb eines Sprach-
bestandteils (z. B. Namens) sind Trennzeichen (z. B. Zeilenumbruch) natürlich verboten.
• Zeichen mit festgelegter Bedeutung wie z. B. "{", ";", "(", "+", ">" sind selbstisolierend, d .h.
davor und danach sind keine Trennzeichen nötig (aber erlaubt).
Manche Programmierer setzen die öffnende geschweifte Klammer zum Rumpf einer Klassen- oder
Methodendefinition ans Ende der Kopfzeile (siehe linkes Beispiel), andere bevorzugen den Anfang
der Folgezeile (siehe rechtes Beispiel):
class Hallo { class Hallo
public static void main(String[] par) { {
System.out.print("Hallo"); public static void main(String[] par)
} {
} System.out.print("Hallo");
}
}

Unsere Entwicklungsumgebung verwendet per Voreinstellung die linke Variante, kann aber mit
Gültigkeit für das aktuelle Projekt nach
File > Settings > Editor > Code Style > Java> Scheme=Project
bzw. mit Gültigkeit für neue Projekte nach
File > Settings > Editor > Code Style > Java > Scheme=Default
umgestimmt werden, z. B.:
Abschnitt 3.1 Einstieg 85

Damit die (geänderten) Projekteinstellungen für eine vorhandene Quellcodedatei realisiert werden,
muss bei aktivem Editorfenster die folgende akrobatische Tastenkombination
Umschalt + Strg + Alt + L
betätigt und anschließend die folgende Dialogbox mit Run quittiert werden:

IntelliJ unterstützt die Einhaltung der Layout-Regeln nicht dadurch, dass beim Editieren Abwei-
chungen verhindert werden, sondern ...
• durch die beschriebene Möglichkeit zur automatisierten Layout-Anpassung
• und durch das Verhalten von Assistenten, die Quellcode erstellen.
Die im Manuskript verwendete Syntaxgestaltung durch Farben und Textattribute stammt von Intel-
liJ, wobei nach
File > Settings > Editor > Color Scheme > General
das Schema Classic Light gewählt wurde:
86 Kapitel 3 Elementare Sprachelemente

3.1.5 Kommentare
Kommentare unterstützen die spätere Verwendung (z. B. Weiterentwicklung) des Quellcodes und
werden vom Compiler ignoriert. Java bietet drei Möglichkeiten, den Quellcode zu kommentieren:

3.1.5.1 Zeilenrestkommentar
Alle Zeichen vom ersten doppelten Schrägstrich (//) bis zum Ende der Zeile gelten als Kommentar,
z. B.:
private int zaehler; // wird automatisch mit 0 initialisiert
Im Beispiel wird eine Variablendeklarationsanweisung in derselben Zeile kommentiert.
Um in IntelliJ einen markierten Zeilenblock als Kommentar zu deklarieren, wählt man den Menübe-
fehl
Code > Comment with Line Comment
oder drückt die Strg-Taste zusammen mit der Divisionstaste im numerischen Ziffernblock:1
Strg +
Anschließend werden doppelte Schrägstriche vor jede Zeile des Blocks gesetzt. Bei Anwendung des
Menü- bzw. Tastenbefehls auf einen zuvor mit Doppelschrägstrichen auskommentierten Block ent-
fernt IntelliJ die Kommentar-Schrägstriche.

3.1.5.2 Mehrzeilenkommentar
Ein durch /* eingeleiteter Kommentar muss explizit durch */ terminiert werden. In der Regel wird
diese Syntax für einen ausführlichen Kommentar verwendet, der sich über mehrere Zeilen erstreckt,
z. B.:

1
Die laut IntelliJ-Dokumentation zu verwendende Tastenkombination Strg + / klappt nur mit einem US-
Tastaturlayout.
Abschnitt 3.1 Einstieg 87

/*
Ein Bruch-Objekt verhindert, dass sein Nenner auf 0
gesetzt wird, und hat daher stets einen definierten Wert.
*/
public boolean setzeNenner(int n) {
if (n != 0) {
nenner = n;
return true;
} else
return false;
}
Ein mehrzeiliger Kommentar eignet sich auch dazu, einen Programmteil (vorübergehend) zu deak-
tivieren, ohne ihn löschen zu müssen.
Weil der explizit terminierte Kommentar (jedenfalls ohne farbliche Hervorhebung der auskommen-
tierten Passage) unübersichtlich ist, wird er selten verwendet.

3.1.5.3 Dokumentationskommentar
Vor der Definition bzw. Deklaration von Klassen, Interfaces (siehe unten), Methoden oder Variab-
len darf ein Dokumentationskommentar stehen, eingeleitet mit /** und beendet mit */. Im Quell-
code der API-Klasse System befindet sich z. B. der folgende Dokumentationskommentar zum Aus-
gabeobjekt out, das Sie schon kennengelernt haben:1
/**
* The "standard" output stream. This stream is already
* open and ready to accept output data. Typically this stream
* corresponds to display output or another output destination
* specified by the host environment or user. The encoding used
* in the conversion from characters to bytes is equivalent to
* {@link Console#charset()} if the {@code Console} exists,
* {@link Charset#defaultCharset()} otherwise.
. . .
* @see Console#charset()
* @see Charset#defaultCharset()
*/
public static final PrintStream out = null;
Ein Dokumentationskommentar kann mit dem JDK-Werkzeug javadoc in eine HTML-Datei extra-
hiert werden. Die strukturierte Dokumentation wird über Markierungen für Methodenparameter,
Rückgabewerte usw. unterstützt.2 So sieht die von javadoc aus dem Dokumentationskommentar zu
System.out erstellte HTML-Passage aus:

1
Die Quellcodedatei System.java steckt im API-Quellcodearchiv src.zip. Wo diese Archivdatei bei der Installation
landet, wird gleich beschrieben. Die Klasse System.java befindet sich im Paket java.lang. Ab Java 9 muss man zu-
sätzlich wissen, dass das Paket java.lang zum Modul java.base gehört. Der im Text wiedergegebene Dokumentati-
onskommentar stammt aus dem OpenJDK 17.
2
Siehe z. B.: https://docs.oracle.com/en/java/javase/17/docs/specs/man/javadoc.html
88 Kapitel 3 Elementare Sprachelemente

Eine solche API-Dokumentation kann aus IntelliJ aufgerufen werden, sofern sich die Einfügemarke
des Editors in dem interessierenden Bezeichner (im Beispiel: out) befindet, und dann eine von den
folgenden Tastenkombinationen gedrückt wird:
• Umschalt + F1
Nach dieser Tastenkombination (oder nach dem Menübefehl View > External Documen-
tation) versucht IntelliJ, die HTML-Datei mit der Dokumentation in einem externen Brow-
ser-Fenster zu öffnen und den Fokus passend zu setzen (siehe obiges Bildschirmfoto). Damit
dies gelingt, muss ein Documentation Path in der SDK-Konfiguration gesetzt sein (siehe
Abschnitt 2.4.6.2).
• Strg + Q
Über diese Tastenkombination (oder den Menübefehl View > Quick Documentation)
erhält man in IntelliJ ein PopUp-Fenster mit Informationen zum interessierenden API-
Bestandteil, z. B.:

Dieses PopUp-Fenster erscheint auch, wenn bei aktivem IntelliJ-Fenster der Mauszeiger ei-
ne kurze Zeitspanne über dem interessierenden API-Bestandteil verharrt. IntelliJ kann die
Informationen über den Documentation Path in der SDK-Konfiguration beschaffen (sie-
he Abschnitt 2.4.6.2) oder die per Voreinstellung als JDK-Bestandteil installierte Datei
Abschnitt 3.1 Einstieg 89

src.zip mit dem Quellcode der Standardbibliothek auswerten. Bei der im Abschnitt 1.2.1 be-
schriebenen OpenJDK 8 - Installation landet die Datei src.zip im Installationsordner. Bei
der im Abschnitt 2.1.1 beschriebenen OpenJDK 17 - Installation landet sie im Unterordner
lib.
Während vielleicht noch einige Zeit vergeht, bis Sie den ersten Dokumentationskommentar zu einer
eigenen Klasse schreiben, sind die Techniken zum Zugriff auf die Dokumentation der API-Klassen
von Beginn an im Alltag der Software-Entwicklung unverzichtbar.

3.1.6 Namen
Für Klassen, Methoden, Felder, Parameter und sonstige Elemente eines Java-Programms benötigen
wir Namen, wobei folgende Regeln gelten:
• Die Länge eines Namens ist nicht begrenzt.
Zwar fördern kurze Namen die Übersicht im Quellcode, doch ist die Verständlichkeit eines
Namens noch wichtiger als die Kürze.
• Das erste Zeichen muss ein Buchstabe, Unterstrich oder Dollar-Zeichen sein, danach dürfen
außerdem auch Ziffern auftreten.
• Damit sind insbesondere das Leerzeichen sowie Zeichen mit spezieller syntaktischer Bedeu-
tung (z. B. -, (, *) als Namensbestandteile verboten.
• Java-Programme werden intern im Unicode-Zeichensatz dargestellt. Daher erlaubt Java in
Namen auch Umlaute oder sonstige nationale Sonderzeichen, die als Buchstaben gelten,
z. B.:
public static void main(String[] args) {
int möglich = 13;
System.out.println(möglich);
}
• Die Groß-/Kleinschreibung ist signifikant. Für den Java-Compiler sind also z. B.
Anz anz ANZ
grundverschiedene Namen.
• Die folgenden reservierten Schlüsselwörter dürfen nicht als Namen verwendet werden:
abstract assert boolean break byte case catch
char class const continue default do double
else enum extends false final finally float
for goto if implements import instanceof int
interface long native new null package private
protected public return short static strictfp super
switch synchronized this throw throws transient true
try void volatile while
Die Schlüsselwörter const und goto sind reserviert, werden aber derzeit nicht verwendet.
• In der letzten Zeit (vor allem in den Java-Versionen 9 und 17) sind kontextabhängige
Schlüsselwörter dazugekommen, die in einem bestimmten Umfeld als Namen verboten
sind:
exports module non-sealed open opens permits provides
requires sealed to transitive uses var with
yield
• Seit Java 9 ist ein isolierter Unterstrich ("_") nicht mehr als Name erlaubt.
• Namen müssen innerhalb ihres Kontexts (siehe unten) eindeutig sein.
90 Kapitel 3 Elementare Sprachelemente

3.1.7 Vollständige Klassennamen und Import-Deklaration


Jede Java-Klasse gehört zu einem Paket (siehe Kapitel 6), und dem Namen der Klasse ist grund-
sätzlich der Paketname voranzustellen. Dies gilt natürlich auch für die API-Klassen, also z. B. für
die im folgenden Beispielprogramm verwendete Klasse Random aus dem Paket java.util.1 Objekte
dieser Klasse beherrschen u. a. die Methode nextInt(), die eine Pseudozufallszahl mit dem Daten-
typ int liefert, z. B.:
Quellcode Ausgabe
class Prog { 1053985008
public static void main(String[] args) {
java.util.Random zuf = new java.util.Random();
System.out.println(zuf.nextInt());
}
}

Keine Mühe mit dem Paketnamen hat man bei …


• den Klassen im Paket der aktuellen Quellcodedatei
Eine Quellcodedatei kann per package-Deklaration einem Paket zugeordnet werden, sodass
alle in der Datei definierten Klassen zu diesem Paket gehören (siehe Abschnitt 6.1.1). Die in
einer Quellcodedatei ohne package-Deklaration definierten Klassen gehören zum Stan-
dardpaket.
• den Klassen aus dem API-Paket java.lang (z. B. Math)
Die Klassen dieses Pakets werden automatisch in jede Quellcodedatei importiert (siehe un-
ten).
Um in einer Quellcodedatei bei Klassen aus anderen (API-)Paketen die lästige Angabe von Paket-
namen zu vermeiden, kann man einzelne Klassen und/oder komplette Pakete importieren. Die zu-
ständigen import-Deklarationen sind an den Anfang der Quellcodedatei zu setzen, z. B. zum Im-
portieren der Klasse java.util.Random:
Quellcode Ausgabe
import java.util.Random; 1053985008
class Prog {
public static void main(String[] args) {
Random zuf = new Random();
System.out.println(zuf.nextInt());
}
}

Um alle Klassen eines Pakets zu importieren, gibt man einen Stern an Stelle des Klassennamens an,
z. B.:
import java.util.*;
Unterpakete (siehe Kapitel 6) sind dabei nicht einbezogen.
Zur Erläuterung der Import-Deklaration hätten die beiden Beispiele eigentlich genügt, und das fol-
gende Syntaxdiagramm ist ziemlich überflüssig:

1
Ab Java 9 befindet sich das Paket java.util im Modul java.base. Das gilt bis auf wenige Ausnahmen für alle im
Manuskript verwendeten Pakete, sodass der Hinweis auf die Modulzugehörigkeit bald nur noch in den Ausnahme-
fällen erscheint.
Abschnitt 3.2 Ausgabe bei Konsolenanwendungen 91

Import-Deklaration
Klasse
import Paket . ;
*

In vergleichbaren Fällen werden wir zukünftig auf ein Syntaxdiagramm verzichten.


Mit der Anzahl importierter Bezeichner steigt das Risiko für eine Namenskollision. Die Entwick-
lungsumgebung und der Compiler meckern, ...
• wenn zwei namensgleiche Klassen aus verschiedenen Paketen explizit importiert werden,
• wenn eine in der Quellcodedatei definierte und eine explizit importierte Klasse denselben
Namen besitzen.
Wenn aufgrund der Platzhaltersyntax aus mehreren Paketen namensgleiche Klassen importiert wor-
den sind, dann muss bei der Verwendung einer Klasse durch Voranstellen des Paketnamens für
Eindeutigkeit gesorgt werden.
Das Importieren von Klassen bzw. kompletten Paketen kann nur dann zum gewünschten Ergebnis
führen, wenn die zugehörigen Bytecode-Dateien von der Entwicklungsumgebung bzw. vom Compi-
ler (beim Übersetzen des Quellcodes) und von der JVM (bei der Ausführung des Programms) ge-
funden werden. Bei den Klassen aus dem Java-API ist dies garantiert. Damit das Importieren ande-
rer Klassen klappt, müssen die Entwicklungsumgebung bzw. der Compiler und die Runtime darüber
informiert werden, an welchen Orten gesucht werden soll (siehe Abschnitte 2.2.4 bzw. 3.4.2).

3.2 Ausgabe bei Konsolenanwendungen


In diesem Abschnitt beschäftigen wir uns mit der Ausgabe von Zeichen in einem Konsolenfenster.
Eine einfache Möglichkeit zur Konsoleneingabe wird im Abschnitt 3.4 vorgestellt.

3.2.1 Ausgabe einer (zusammengesetzten) Zeichenfolge


Um eine einfache Konsolenausgabe in Java zu bewerkstelligen, bittet man das Objekt System.out
(aus der Klasse PrintStream) seine print() - oder seine println() - Methode auszuführen.1 Im Un-
terschied zu print() schließt println() die Ausgabe mit einem Zeilenwechsel ab, sodass die nächs-
ten Aus- oder Eingabe in einer neuen Zeile erfolgt. Folglich ist print() zu bevorzugen, …
• wenn eine Benutzereingabe unmittelbar hinter einer Ausgabe in derselben Zeile ermöglicht
werden soll,
• wenn die von mehreren Methodenaufrufen veranlassten Ausgaben in einer Zeile erscheinen
sollen.
Beide Methoden erwarten ein einziges Argument, wobei erlaubt sind:

1
Für eine genauere Erläuterung reichen unsere bisherigen OOP-Kenntnisse noch nicht ganz aus. Wer aus anderen
Quellen Vorkenntnisse besitzt, kann die folgenden Sätze vielleicht jetzt schon verdauen: Wir benutzen bei der Kon-
solenausgabe die im Paket java.lang definierte und damit automatisch in jedem Java-Programm verfügbare Klasse
System. Unter den Membern dieser Klasse befindet sich das statische (klassenbezogene) Feld out, das als Referenz-
variable auf ein Objekt aus der Klasse PrintStream zeigt. Dieses Objekt beherrscht u. a. die Methoden print() und
println(), die jeweils ein einziges Argument von beliebigem Datentyp erwarten und zur Standardausgabe befördern.
92 Kapitel 3 Elementare Sprachelemente

• eine Zeichenfolge, in durch doppelte Anführungszeichen zu begrenzen ist


Beispiel: System.out.print("Hallo allerseits!");
• ein sonstiger Ausdruck (siehe Abschnitt 3.5)
Dessen Wert wird automatisch in eine Zeichenfolge gewandelt.
Beispiele: - System.out.println(ivar);
Hier wird der Wert der Variablen ivar ausgegeben.
- System.out.println(i==13);
An die Möglichkeit, als print() - bzw. println() - Parameter nahezu beliebige
Ausdrücke anzugeben, müssen sich Einsteiger erst gewöhnen. Im Beispiel wird
der Wert eines Vergleichs (der Variablen i mit der Zahl 13) ausgegeben. Bei
Identität erscheint auf der Konsole das Wort true, ansonsten das Wort false.
Besonders angenehm ist die Möglichkeit, mehrere Teilausgaben mit dem Plusoperator zu verketten,
z. B.:
System.out.println("Ergebnis: " + netto*MWST);
Im Beispiel wird der numerische Wert von netto*MWST (Produkt aus zwei Variablen) in eine Zei-
chenfolge gewandelt und dann mit "Ergebnis: " verknüpft.

3.2.2 Formatierte Ausgabe


Gelegentlich sind bei einer Konsolenausgabe die Gestaltungsmöglichkeiten der PrintStream-
Methoden print() und println() unzureichend, weil sich z. B. die Anzahl der bei einer Zahl ausge-
gebenen Dezimalstellen nicht beeinflussen lässt. Dann bietet sich die PrintStream-Methode
printf() an, die eine formatierte Ausgabe von mehreren Ausdrücken erlaubt.1 Weil System.out ein
Objekt der Klasse PrintStream ist, beherrscht es auch die Methode printf(), z. B.:
Quellcode Ausgabe
class Prog { Pi = 3,142
public static void main(String[] args) { Pi = 3,1415927, e = 2,7182818
System.out.printf("Pi = %9.3f%n", Math.PI);
System.out.printf("Pi = %9.7f, e = %9.7f",
Math.PI, Math.E);
}
}

Als erster Parameter wird an printf() eine Zeichenfolge übergeben, die Formatierungsangaben für
die restlichen Parameter enthält. Für die Formatierungsangabe zu einem Ausgabeparameter ist die
folgende Syntax zu verwenden, wobei Leerzeichen zwischen ihren Bestandteilen verboten sind:
Platzhalter für die formatierte Ausgabe

% Nummer $ Optionen Breite . Präzision Format

Darin bedeuten:

1
Alternativ kann die äquivalente PrintStream-Methode format() benutzt werden.
Abschnitt 3.2 Ausgabe bei Konsolenanwendungen 93

Nummer Nummer des auszugebenden Arguments (mit 1 beginnend)


Die Angabe einer Nummer ist z. B. dann von Nutzen, wenn ein Ar-
gument mehrfach ausgegeben werden soll. Den nicht über ihre Num-
mer angesprochenen Argumenten werden sukzessive die Formatie-
rungsangaben ohne Nummern zugordnet, solange der Vorrat reicht.
Unversorgte Argumente werden nicht ausgegeben.
Optionen Formatierungsoptionen, u. a. sind erlaubt:
- bewirkt eine linksbündige Ausgabe statt der voreingestellten
rechtsbündigen Ausgabe
, ist nur für Zahlen erlaubt und bewirkt eine Zifferngruppierung
(z. B. Ausgabe von 12.123,33 statt 12123,33)
Breite Ausgabebreite für das zugehörige Argument
Präzision Anzahl der Nachkommastellen oder sonstige Präzisionsangabe
(abhängig vom Format)
Format Formatspezifikation gemäß anschließender Tabelle
Es werden u. a. die folgenden Formate unterstützt:
Beispiele
Format Beschreibung
printf() - Parameterliste Ausgabe
d ganze Zahl ("%7d", 4711) 4711
("%-7d", 4711) 4711
("%1$d %1$,d", 4711) 4711 4.711

f Kommazahl mit einer festen Anzahl von ("%5.2f", 4.711) 4,71


Nachkommastellen
Präzision: Anzahl der Nachkommastellen
(Voreinstellung: 6)
e Kommazahl in wissenschaftlicher ("%e", 47.11) 4,711000e+01
Notation ("%.2e", 47.11) 4,71e+01
("%12.2e", 47.11) 4.71e+01
Präzision: Anzahl Stellen in der Mantisse
(Voreinstellung: 6)
c ein einzelnes Zeichen // x ist eine char- Inhalt von x: h
// Variable
("Inhalt von x: %c", x)

n plattformspezifische Zeilentrennung ("Inhalt von x:%n%c",x) Inhalt von x:


h

s eine Zeichenfolge // str ist eine String- Text: abc


// Variable
("Text: %-7s", str)

Wie print() produziert auch printf() keinen automatischen Zeilenwechsel nach der Ausgabe. Im
obigen Beispielprogramm wird daher in der Formatierungszeichenfolge des ersten printf() - Auf-
rufs durch die Formatspezifikation %n für einen Zeilenwechsel gesorgt.
Im Unterschied zu print() und println() gibt printf() das landesübliche Dezimaltrennzeichen aus,
z. B.:
94 Kapitel 3 Elementare Sprachelemente

Quellcode Ausgabe
class Prog { 3.141592653589793
public static void main(String[] args) { 3,1415927
System.out.println(Math.PI);
System.out.printf("%-12.7f", Math.PI);
}
}

Eben wurde eine kleine Teilmenge der Syntax einer Java-Formatierungszeichenfolge vorgestellt.
Die komplette Information findet sich in der API-Dokumentation zur Klasse Formatter (Paket ja-
va.util, ab Java 9 im Modul java.base).1 Zur Online-Version dieser Dokumentation gelangen Sie
z. B. auf dem folgenden Weg:
• Öffnen Sie z. B. die HTML-Startseite der API-Dokumentation zu Java 17 über die Adresse
https://docs.oracle.com/en/java/javase/17/docs/api/index.html
• Tragen Sie den Klassennamen Formatter in das SEARCH-Feld ein (oben rechts), und
wählen Sie aus der Trefferliste den Typ java.util.Formatter.
Noch bequemer klappt es mit Hilfe von IntelliJ z. B. so:2
• Im Quellcodeeditor die Einfügemarke auf den Namen der Methode printf() setzen
• Tastenbefehl Umschalt + F1
• Im auftauchenden Browser-Fenster Klick auf den Link Format string syntax

3.3 Variablen und Datentypen


Während ein Programm läuft, müssen zahlreiche Daten im Arbeitsspeicher des Rechners abgelegt
werden und anschließend mehr oder weniger lange für lesende und schreibende Zugriffe verfügbar
sein, z. B.:
• Die Eigenschaftsausprägungen eines Objekts werden aufbewahrt, solange das Objekt exis-
tiert.
• Die in einer Methode benötigten Daten werden bis zum Ende der Methodenausführung ge-
speichert.
Zum Speichern eines Werts (z. B. einer ganzen Zahl) wird eine sogenannte Variable verwendet,
worunter Sie sich einen benannten Speicherplatz für einen Wert mit einem bestimmten Daten-
typ (z. B. Ganzzahl) vorstellen können.
Eine Variable erlaubt (bei bestehender Zugriffsberechtigung) über ihren Namen den lesenden
und/oder schreibenden Zugriff auf die zugehörige Stelle im Arbeitsspeicher, z. B.:

1
Mit den Modulen und Paketen der Standardklassenbibliothek werden wir uns später ausführlich beschäftigen. An
dieser Stelle dient die Angabe der Modul- und Paketzugehörigkeit dazu, eine Klasse eindeutig zu identifizieren und
die Standardklassenbibliothek allmählich kennenzulernen.
2
Wird mit IntelliJ 2021.2 in einem Projekt mit dem JDK 17 als SDK über den Tastenbefehl Umschalt + F1 die
API-Dokumentation zu einer Methode angefordert, denn werden im folgenden Fenster neben dem korrekten Link
auch überflüssige Duplikate und defekte Links angeboten:
Abschnitt 3.3 Variablen und Datentypen 95

class Prog {
public static void main(String[] args) {
int ivar = 4711; //schreibender Zugriff auf ivar
System.out.println(ivar); //lesender Zugriff auf ivar
}
}

3.3.1 Strenge Compiler-Überwachung bei Java-Variablen


Um die Details bei der Verwaltung der Variablen im Arbeitsspeicher müssen wir uns nicht küm-
mern. Allerdings verlangt Java beim Umgang mit Variablen im Vergleich zu anderen Programmier-
oder Skriptsprachen einige Sorgfalt, letztlich mit dem Ziel, Fehler zu vermeiden:
• Variablen müssen explizit deklariert werden, z. B.:
int ivar = 13;
Wenn Sie versuchen, eine nicht deklarierte Variable zu verwenden, wird beim Überset-
zungsversuch ein Fehler gemeldet, z. B. vom Compiler javac.exe aus dem OpenJDK 17:

Unsere Entwicklungsumgebung IntelliJ erkennt und dokumentiert das Problem unmittelbar


nach der Eingabe im Editor. Wenn sich der Mauszeiger im Umfeld des Fehlers befindet, er-
scheint eine Erläuterung:

Nach einem Mausklick an derselben Stelle geht Ihnen ein rotes Licht auf. Durch einen Klick
auf die rote Glühbirne oder mit der Tastenkombination Alt + Enter erhalten Sie in dieser
Situation eine Liste mit Reparaturvorschlägen:
96 Kapitel 3 Elementare Sprachelemente

Nach der Wahl des ersten Vorschlags nimmt IntelliJ im Beispiel die fehlende Variablende-
klaration vor und empfiehlt dabei einen Datentyp:

Durch den Deklarationszwang werden z. B. Programmfehler wegen falsch geschriebener


Variablennamen verhindert. Würde der Java-Compiler den folgenden Quellcode übersetzen,
dann käme es in einem realen Programm irgendwann zu einem Logikfehler, weil die Variab-
le ivar nicht den erwarteten Wert hätte:

• Java ist streng und statisch typisiert.1


Für jede Variable ist bei der Deklaration ein fester (später nicht mehr änderbarer) Datentyp
anzugeben. Er legt fest, …
o welche Informationen (z. B. ganze Zahlen, Zeichen, Adressen von Bruch-Objekten)
in der Variablen gespeichert werden können,
o welche Operationen auf die Variable angewendet werden dürfen.
Der Compiler kennt zu jeder Variablen den Datentyp und kann daher Typsicherheit garan-
tieren, d. h. die Zuweisung von Werten mit ungeeignetem Datentyp verhindern. Außerdem
kann auf (zeitaufwändige) Typprüfungen zur Laufzeit verzichtet werden. In der folgenden
Anweisung
int ivar = 4711;

1
Halten Sie bitte die eben erläuterte statische Typisierung (im Sinn von unveränderlicher Typfestlegung) in begriffli-
cher Distanz zu den bereits erwähnten statischen Variablen (im Sinn von klassenbezogenen Variablen). Das Wort
statisch ist eingeführter Bestandteil bei beiden Begriffen, sodass es mir nicht sinnvoll erschien, eine andere Be-
zeichnung vorzunehmen, um die Doppelbedeutung zu vermeiden.
Abschnitt 3.3 Variablen und Datentypen 97

wird die Variable ivar vom Typ int deklariert, der sich für ganze Zahlen im Bereich
von -2147483648 bis 2147483647 eignet.
Im Unterschied zu vielen Skriptsprachen arbeitet Java mit einer statischen Typisierung, so-
dass der einer Variablen zugewiesene Typ nicht mehr geändert werden kann.
In der obigen Anweisung erhält die Variable ivar beim Deklarieren gleich den Initialisierungs-
wert 4711. Auf diese oder andere Weise müssen Sie jeder lokalen, d .h. innerhalb einer Methode
deklarierten Variablen einen Wert zuweisen, bevor Sie zum ersten Mal lesend darauf zugreifen (vgl.
Abschnitt 3.3.8). Weil die zu einem Objekt oder zu einer Klasse gehörenden Variablen (siehe un-
ten) automatisch initialisiert werden, hat in Java jede Variable beim Lesezugriff stets einen definier-
ten Wert.
Seit der Version 10 können Java-Compiler den Typ von lokalen (in Methoden definierten) Variab-
len aus einem zugewiesenen Initialisierungswert erschließen (Typinferenz), und man darf in der
Variablendeklaration die Typangabe durch das Schlüsselwort var ersetzen, z. B.:

Im Beispiel gelingt die Inferenz, weil die zugewiesene Zahl 4711, die wir später als Ganzzahlliteral
bezeichnen werden, den Typ int besitzt (vgl. Abschnitt 3.3.11.1). Wie man bei gedrückter Strg-
Taste für die in der Nähe des Mauszeigers befindliche Variable erfährt, kennt der Compiler (bzw.
die Entwicklungsumgebung) den Datentyp:

Das Schlüsselwort var ist in vielen Situationen bequem, doch sollte die Lesbarkeit des Quellcodes
nicht leiden.

3.3.2 Variablennamen
Es sind beliebige Bezeichner gemäß Abschnitt 3.1.6 erlaubt. Eine Beachtung der folgenden Kon-
ventionen verbessert aber die Lesbarkeit des Quellcodes, insbesondere auch für andere Program-
mierer (vgl. Bloch 2018, S. 289ff; Gosling et al. 2021, Abschnitt 6.1):
98 Kapitel 3 Elementare Sprachelemente

• Variablennamen beginnen mit einem Kleinbuchstaben.


• Besteht ein Name aus mehreren Wörtern (z. B. numberOfObjects), dann schreibt man ab
dem zweiten Wort die Anfangsbuchstaben groß (Camel Casing). Das zur Vermeidung von
Urheberrechtsproblemen handgemalte Tier kann hoffentlich trotz ästhetischer Mängel zur
Begriffsklärung beitragen:

• Variablennamen mit einem einzigen Buchstaben sollten nur in speziellen Fällen verwendet
werden (z. B. als Laufvariable von Wiederholungsanweisungen, siehe unten).

3.3.3 Primitive Datentypen und Referenztypen


In der objektorientierten Programmierung werden neben den traditionellen (elementaren, primiti-
ven) Variablen zur Aufbewahrung von Zahlen, Zeichen oder Wahrheitswerten auch Variablen be-
nötigt, die die Adresse eines Objekts aufnehmen und so die Kommunikation mit dem Objekt er-
möglichen. Wir unterscheiden also in Java bei den Datentypen von Variablen zwei übergeordnete
Kategorien:
• Primitive Datentypen
Die Variablen mit einem primitiven Datentyp sind auch in Java unverzichtbar (z. B. als Fel-
der von Klassen oder als lokale Variablen in Methoden), obwohl sie „nur“ zur Verwaltung
ihres Inhalts dienen und keine Rolle bei der Kommunikation mit Objekten spielen.
In der Bruch-Klassendefinition (siehe Abschnitt 1.1.2) haben die Felder für Zähler und
Nenner eines Objekts den primitiven Typ int, können also eine Ganzzahl im Bereich
von -2147483648 bis 2147483647 (-231 bis 231 - 1) aufnehmen. Diese Felder werden in den
folgenden Anweisungen deklariert, wobei das Feld nenner auch noch einen expliziten Ini-
tialisierungswert erhält:
private int zaehler;
private int nenner = 1;
Beim Feld zaehler wird auf die explizite Initialisierung verzichtet, sodass die automati-
sche Null-Initialisierung von Feldern greift. Für ein frisch erzeugtes Bruch-Objekt befinden
sich also im Arbeitsspeicher die folgenden Instanzvariablen (Felder):
zaehler nenner

0 1

Zur Realisation der Datenkapselung erhalten die beiden Felder den Zugriffsmodifikator pri-
vate.
In der Bruch-Methode kuerze() tritt u. a. die lokale Variable az auf, die ebenfalls den
primitiven Typ int besitzt:
int az = Math.abs(zaehler);
Bei den lokalen Variablen einer Methode ist eine Datenkapselung weder erforderlich, noch
möglich. Somit ist hier der Modifikator private überflüssig und verboten. Im Abschnitt
3.3.6 werden zahlreiche weitere primitive Datentypen vorgestellt.
Abschnitt 3.3 Variablen und Datentypen 99

• Referenztypen
Besitzt eine Variable einen Referenztyp, dann kann ihr Speicherplatz die Adresse eines Ob-
jekts aus einer bestimmten Klasse aufnehmen. Sobald ein solches Objekt erzeugt und seine
Adresse der Referenzvariablen zugewiesen worden ist, kann das Objekt über die Referenz-
variable angesprochen werden. Von den Variablen mit primitivem Typ unterscheidet sich
eine Referenzvariable also …
o durch ihren speziellen Inhalt (eine Objektadresse)
o und durch ihre Rolle bei der Kommunikation mit Objekten.
Man kann jede Klasse (aus dem Java-API oder selbst definiert) als Referenzdatentyp ver-
wenden, also Referenzvariablen dieses Typs deklarieren. In der main() - Methode der Klas-
se Bruchaddition (siehe Abschnitt 1.1.4) werden z. B. die Referenzvariablen b1 und b2
vom Datentyp Bruch deklariert:
Bruch b1 = new Bruch(), b2 = new Bruch();
Sie erhalten als Initialisierungswert jeweils eine Referenz auf ein (per new-Operator, siehe
Abschnitt 4.4) neu erzeugtes Bruch-Objekt. Daraus resultiert im Arbeitsspeicher die fol-
gende Situation:
Bruch-Objekt

zaehler nenner
b1
0 1
Bruch@87a5cc

b2 Bruch-Objekt

Bruch@1960f05 zaehler nenner

0 1

Das von b1 referenzierte Bruch-Objekt wurde bei einem konkreten Programmlauf von der
JVM an der Speicheradresse 0x87a5cc (ganze Zahl, ausgedrückt im Hexadezimalsystem)
untergebracht. Wir müssen diese Adresse nicht kennen, sondern sprechen das dort abgelegte
Objekt über die Referenzvariable an, z. B. in der folgenden Anweisung aus der main() - Me-
thode der Klasse Bruchaddition:
b1.frage();
Jedes Bruch-Objekt besitzt die Felder (Instanzvariablen) zaehler und nenner vom primi-
tiven Datentyp int.
Zur Beziehung der Begriffe Objekt und Variable halten wir fest:
• Ein Objekt enthält im Allgemeinen mehrere Instanzvariablen (Felder) von beliebigem Da-
tentyp. So enthält z. B. ein Bruch-Objekt die Felder zaehler und nenner vom primitiven
Typ int (zur Aufnahme einer Ganzzahl). Bei einer späteren Erweiterung der Bruch-
Klassendefinition werden ihre Objekte auch eine Instanzvariable mit Referenztyp erhalten.
• Eine Referenzvariable dient zur Aufnahme einer Objektadresse. So kann z. B. eine Variable
vom Datentyp Bruch die Adresse eines Bruch-Objekts aufnehmen und zur Kommunikation
mit diesem Objekt dienen. Es ist ohne weiteres möglich und oft sinnvoll, dass mehrere Refe-
renzvariable die Adresse desselben Objekts enthalten. Das Objekt existiert unabhängig vom
Schicksal einer konkreten Referenzvariablen, wird jedoch überflüssig (und damit zum po-
tentiellen Opfer des Garbage Collectors der JVM), wenn im gesamten Programm keine ein-
zige Referenz (Kommunikationsmöglichkeit) mehr vorhanden ist.
100 Kapitel 3 Elementare Sprachelemente

Wir werden im Kapitel 3 überwiegend mit Variablen von primitivem Typ arbeiten, können und
wollen dabei aber den Referenzvariablen (z. B. zur Ansprache des Objekts System.out aus der
Klasse PrintStream bei der Konsolenausgabe, siehe Abschnitt 3.2) nicht aus dem Weg gehen.

3.3.4 Klassifikation der Variablen nach Zuordnung


In Java unterscheiden sich Variablen nicht nur hinsichtlich des Datentyps (Inhalts), sondern auch
hinsichtlich der Zuordnung zu einer Methode, zu einem Objekt oder zu einer Klasse:
• Lokale Variablen
Sie werden innerhalb einer Methode deklariert. Ihre Gültigkeit (Verwendbarkeit) beschränkt
sich auf die Methode bzw. auf einen Anweisungsblock (siehe Abschnitt 3.3.9) innerhalb der
Methode.
Solange eine Methode ausgeführt wird, befinden sich ihre Variablen in einem Bereich des
programmeigenen Arbeitsspeichers, den man als Stack (deutsch: Stapel) bezeichnet.
• Instanzvariablen (nicht-statische Felder)
Instanzvariablen werden außerhalb jeder Methode deklariert. Jedes Objekt (synonym: jede
Instanz) einer Klasse verfügt über einen vollständigen Satz der Instanzvariablen der Klasse.
So besitzt z. B. jedes Objekt der Klasse Bruch einen zaehler und einen nenner.
Solange ein Objekt existiert, befinden es sich mit all seinen Instanzvariablen in einem Be-
reich des programmeigenen Arbeitsspeichers, den man als Heap (deutsch: Haufen) bezeich-
net.
• Klassenvariablen (statische Felder)
Klassenvariablen werden außerhalb jeder Methode deklariert und erhalten dabei den Modi-
fikator static. Diese Variablen beziehen sich auf eine Klasse insgesamt, nicht auf einzelne
Instanzen der Klasse. Z. B. kann man in einer Klassenvariablen festhalten, wie viele Objekte
der Klasse bereits bei einem Programmeinsatz erzeugt worden sind. In unserem Bruchrech-
nungsbeispiel haben wir der Einfachheit halber bisher auf statische Felder verzichtet. Aller-
dings sind uns schon statische Felder aus anderen Klassen begegnet:
o Aus der Klasse System kennen wir das statische Feld out. Es zeigt auf ein Objekt
der Klasse PrintStream, das wir häufig mit Konsolenausgaben beauftragen.
o In einem Beispielprogramm im Abschnitt 3.2.2 über die formatierte Ausgabe haben
wir die trigonometrische Konstante  ( 3.1416) aus dem statischen Feld PI der
Klasse Math gelesen.1
Während jedes Objekt einer Klasse über einen eigenen Satz mit allen Instanzvariablen ver-
fügt, die beim Erzeugen des Objekts auf dem Heap angelegt werden, existieren Klassenvari-
ablen nur einmal. Sie werden beim Laden der Klasse in der sogenannten Method Area des
Arbeitsspeichers abgelegt.
Die im Wesentlichen schon aus dem Abschnitt 3.3.3 bekannte Abbildung zur Lage im Arbeitsspei-
cher bei Ausführung der main() - Methode der Klasse Bruchaddition aus unserem OOP-
Standardbeispiel (vgl. Abschnitt 1.1) wird anschließend ein wenig präzisiert. Durch Farben und
Ortsangaben wird für die beteiligten lokalen Variablen bzw. Instanzvariablen die Zuordnung zu
einer Methode bzw. zu einem Objekt und die damit verbundene Speicherablage verdeutlicht:

1
Die statischen Felder out (aus der API-Klasse System) und PI (aus der API-Klasse Math) sind finalisiert (siehe
Abschnitt 4.5.1), können also nicht geändert werden. Außerdem kommt keine Änderung des Datentyps in Betracht.
In dieser Situation ist eine Ausnahme vom Prinzip der Datenkapselung sinnvoll, um den Zugriff zu vereinfachen.
Die Deklaration der Referenzvariablen out kennen Sie schon aus dem Abschnitt 3.1.5
public static final PrintStream out = null;
Hier ist die Deklaration der Variablen PI (mit dem primitiven Datentyp double) zu sehen:
public static final double PI = 3.14159265358979323846;
Abschnitt 3.3 Variablen und Datentypen 101

Stack Heap

lokale Variablen der Bruchaddition-


Bruch-Objekt

Methode main() (Datentyp: Bruch)


zaehler nenner
b1

Bruch@87a5cc 0 1

b2 Bruch-Objekt

Bruch@1960f05 zaehler nenner

0 1

Die lokalen Referenzvariablen b1 und b2 der Methode main() befinden sich im Stack-Bereich des
programmeigenen Arbeitsspeichers und enthalten jeweils die Adresse eines Bruch-Objekts. Jedes
Bruch-Objekt besitzt die Felder (Instanzvariablen) zaehler und nenner vom primitiven Typ int
und befindet sich im Heap-Bereich des programmeigenen Arbeitsspeichers.
Auf Instanz- und Klassenvariablen kann in allen Methoden der eigenen Klasse zugegriffen werden.
Wenn (als gut begründete Ausnahme vom Prinzip der Datenkapselung) entsprechende Rechte ein-
geräumt wurden, ist dies auch in Methoden fremder Klassen möglich.
Im Kapitel 3 werden wir überwiegend mit lokalen Variablen arbeiten, aber z. B. auch das statische
Feld out der Klasse System benutzen, das auf ein Objekt der Klasse PrintStream zeigt. Im Zu-
sammenhang mit der systematischen Behandlung der objektorientierten Programmierung werden
die Instanz- und Klassenvariablen ausführlich erläutert.
Im Unterschied zu anderen Programmiersprachen (z. B. C++) ist es in Java nicht möglich, soge-
nannte globale Variablen außerhalb von Klassen zu definieren.

3.3.5 Eigenschaften einer Variablen


Als Eigenschaften einer Java-Variablen haben Sie nun kennengelernt:
• Zuordnung
Eine Variable gehört entweder zu einer Methode, zu einem Objekt oder zu einer Klasse. Da-
raus resultiert ihr Ablageort im Arbeitsspeicher. Als wichtige Speicherregionen unter-
scheiden wir Stack, Heap und Method Area. Dieses Hintergrundwissen hilft z. B., wenn ein
StackOverflowError gemeldet wird.
• Datentyp
Damit sind festgelegt: Zulässige Werte (hinsichtlich Typ und Größe), Speicherplatzbedarf
und zulässige Operationen. Besonders wichtig ist die Unterscheidung zwischen den primiti-
ven Datentypen und den Referenztypen.
• Name
Es sind beliebige Bezeichner gemäß Abschnitt 3.1.6 erlaubt, wobei die Empfehlungen aus
dem Abschnitt 3.3.2 beachtet werden sollten.
102 Kapitel 3 Elementare Sprachelemente

• Aktueller Wert
Im folgenden Beispiel taucht eine lokale Variable namens ivar auf, die zur Methode
main() gehört, vom primitiven Typ int ist und den Wert 5 besitzt:
public class Prog {
public static void main(String[] args) {
int ivar = 5;
}
}

3.3.6 Primitive Datentypen in Java


Als primitiv bezeichnet man in Java die (auch in älteren Programmiersprachen bekannten) Datenty-
pen zur Aufnahme von einzelnen Zahlen, Zeichen oder Wahrheitswerten. Speziell für Zahlen exis-
tieren diverse Datentypen, die sich hinsichtlich Speichertechnik, Wertebereich und Platzbedarf un-
terscheiden. Von der folgenden Tabelle sollte man sich vor allem merken, wo sie zu finden ist,
wenn für eine konkrete Aufgabe ein möglichst sparsamer Datentyp gefragt ist, der alle zu erwarten-
den Werte aufnehmen kann. Eventuell sind Sie aber auch jetzt schon neugierig auf einige Details:

Speicher-
Typ Beschreibung Werte
bedarf in Bits
byte -128 … 127 8
Diese Variablentypen speichern ganze
short Zahlen. -32768 … 32767 16
int Beispiel: -2147483648 ... 2147483647 32
int alter = 31;
long -9223372036854775808 … 64
9223372036854775807
float Variablen vom Typ float speichern Minimum: 32
Gleitkommazahlen nach der Norm -3,40282351038
IEEE-754 mit einer Genauigkeit von Maximum: 1 für das Vorz.,
8 für den Expon.,
mindestens 7 Dezimalstellen in der 3,40282351038 23 für die Mantisse
Mantisse. Kleinster positiver Betrag:
Beispiel: 1.4012984610-45
float pi = 3.141593f;
float-Literale (siehe Beispiel) benöti-
gen das Suffix f (oder F).
double Variablen vom Typ double speichern Minimum: 64
Gleitkommazahlen nach der Norm -1,797693134862315710308
IEEE-754 (64 Bit) mit einer Genauig- Maximum: 1 für das Vorz.,
11 für den Expon.,
keit von mindestens 15 signifikanten 1,797693134862315710308 52 für die Mantisse
Dezimalstellen in der Mantisse. Kleinster positiver Betrag:
Beispiel: 4,940656458412465410-324
double ph = 1.57079632679490;
Abschnitt 3.3 Variablen und Datentypen 103

Speicher-
Typ Beschreibung Werte
bedarf in Bits
char Variablen vom Typ char speichern Unicode-Zeichen 16
eine Unicode-Zeichen. Im Speicher Tabellen mit allen Unicode-
landet aber nicht die Gestalt des Zei- Zeichen sind z. B. auf der
chens, sondern seine Nummer im Webseite des Unicode-
Unicode-Zeichensatz. Daher zählt char Konsortiums verfügbar:
zu den integralen (ganzzahligen) Da- http://www.unicode.org/charts/
tentypen.
Beispiel:
char zeichen = 'j';
char-Literale sind durch einfache An-
führungszeichen zu begrenzen (siehe
Beispiel).
boolean Variablen vom Typ boolean können true, false 8
Wahrheitswerte aufnehmen.
Beispiel:
boolean cond = true;

Eine Variable mit einem integralen Datentyp (z. B. int oder byte) speichert eine ganze Zahl (z. B.
4711) exakt, sofern es nicht durch eine Wertebereichsüberschreitung zu einem Überlauf und damit
zu einem sinnlosen Speicherinhalt kommt (siehe Abschnitt 3.6).
Eine Variable zur Aufnahme einer Gleitkommazahl (synonym: Gleitpunkt- oder Fließkommazahl,
englisch: floating point number) dient zur approximativen Darstellung einer reellen Zahl. Dabei
werden drei Bestandteile separat gespeichert: Vorzeichen, Mantisse und Exponent. Diese ergeben
nach folgender Formel den dargestellten Wert, wobei b für die Basis eines Zahlensystems steht
(meist verwendet: 2 oder 10):
Wert = Vorzeichen  Mantisse  bExponent
Bei dieser von Konrad Zuse entwickelten Darstellungstechnik1 resultiert im Vergleich zur Fest-
kommadarstellung bei gleichem Speicherplatzbedarf ein erheblich größerer Wertebereich. Während
die Mantisse für die Genauigkeit sorgt, speichert der Exponent die Größenordnung, z. B.:
-0,0000001252612 = (-1)  1,252612  10-7
1252612000000000 = (1)  1,252612  1015
Durch eine Änderung des Exponenten könnte man das Dezimalkomma durch die Mantisse „gleiten“
lassen. Allerdings wird in der Regel durch eine Restriktion der Mantisse (z. B. auf das Intervall
[1; 2)) für eine eindeutige Darstellung gesorgt.
Weil der verfügbare Speicher für Mantisse und Exponent begrenzt ist (siehe obige Tabelle), bilden
die Gleitkommazahlen nur eine endliche (aber für die meisten praktischen Zwecke ausreichende)
Teilmenge der reellen Zahlen. Nähere Informationen über die Darstellung von Gleitkommazahlen
im Arbeitsspeicher eines Computers folgen für speziell interessierte Leser gleich im Abschnitt
3.3.7.
Im Vergleich zu den Programmiersprachen C, C++ und C# fällt auf, dass Java auf vorzeichenfreie
Ganzzahltypen verzichtet.

1
Quelle: http://de.wikipedia.org/wiki/Konrad_Zuse
104 Kapitel 3 Elementare Sprachelemente

Die abwertend klingende Bezeichnung primitiv darf keinesfalls so verstanden werden, dass elemen-
tare Datentypen nach Möglichkeit in Java-Programmen zu vermeiden wären. Sie sind bei den Fel-
dern von Klassen und bei den lokalen Variablen von Methoden unverzichtbar.

3.3.7 Darstellung von Gleitkommazahlen im Arbeitsspeicher


Dieser Abschnitt kann beim ersten Lesen des Manuskripts übersprungen werden. Er enthält wichti-
ge Details zu binären Gleitkommatypen, ist also relevant für Software, die solche Typen in wesent-
lichem Umfang verwendet (z. B. für mathematische oder naturwissenschaftliche Aufgaben).

3.3.7.1 Binäre Gleitkommadarstellung


Bei den binären Gleitkommatypen float und double werden auch „relativ glatte“ Zahlen im Allge-
meinen nur approximativ gespeichert, wie das folgende Programm zeigt:
Quellcode Ausgabe
class Prog { 1,3000000
public static void main(String[] args) { 1,29999995
float f130 = 1.3f; 1,250000000000000000
float f125 = 1.25f;
System.out.printf("%9.7f", f130);
System.out.println();
System.out.printf("%10.8f", f130);
System.out.println();
System.out.printf("%20.18f", f125);
}
}

Bei einer Ausgabe mit mehr als sieben Nachkommastellen zeigt sich, dass die float-Zahl 1,3 nicht
exakt abgespeichert worden ist. Demgegenüber tritt bei der float-Zahl 1,25 keine Ungenauigkeit
auf.
Diese Ergebnisse sind durch das Speichern der Zahlen im binären Gleitkommaformat nach der
vom Institute of Electrical and Electronics Engineers (IEEE) veröffentlichten Norm IEEE-754 zu
erklären, wobei jede Zahl als Produkt aus drei getrennt gespeicherten Faktoren dargestellt wird:1
Vorzeichen  Mantisse  2Exponent
Im ersten Bit einer float- oder double - Variablen wird das Vorzeichen gespeichert (0: positiv, 1:
negativ).
Für die Ablage des Exponenten (zur Basis 2) als Ganzzahl stehen 8 (float) bzw. 11 (double) Bits
zur Verfügung, die jeweils die Werte 0 oder 1 repräsentieren. Das i-te Exponenten-Bit (von rechts
nach links mit 0 beginnend nummeriert) hat die Wertigkeit 2i, sodass ein Wertebereich von 0 bis
255 (= 28-1) bzw. von 0 bis 2047 (= 211-1) resultiert:
7 bzw. 10

 b 2 , b {0, 1}
i =0
i
i
i

Allerdings sind im Exponenten die Werte 0 und 255 (float) bzw. 0 und 2047 (double) für Spezial-
fälle (z. B. denormalisierte Darstellung, +/- Unendlich) reserviert (siehe unten). Um auch die für
Zahlen mit einem Betrag kleiner 1 benötigten negativen Exponenten darstellen zu können, werden
die Exponenten mit einer Verschiebung (Bias) um den Wert 127 (float) bzw. 1023 (double) abge-
speichert und interpretiert. Bei einer float-Variablen wird z. B. für den Exponenten 0 der Wert 127
und für den Exponenten -2 der Wert 125 im Speicher abgelegt.

1
https://de.wikipedia.org/wiki/IEEE_754
Abschnitt 3.3 Variablen und Datentypen 105

Abgesehen von betragsmäßig sehr kleinen Zahlen (siehe unten) werden die float- und double-
Werte normalisiert, d .h. auf eine Mantisse im Intervall [1; 2) gebracht, z. B.:
24,48 = 1,53  24
0,2448 = 1,9584  2-3
Zur Speicherung der Mantisse werden 23 (float) bzw. 52 (double) Bits verwendet. Das i-te Mantis-
sen-Bit (von links nach rechts mit 1 beginnend nummeriert) hat die Wertigkeit 2-i, sodass sich der
dezimale Mantissenwert folgendermaßen ergibt:
23 bzw. 52
1 + m mit m = b 2
i =1
i
−i
, bi {0,1}

Der Summenindex i startet mit 1, weil die führende 1 (= 20) der normalisierten Mantisse nicht abge-
speichert wird (hidden bit). Daher stehen alle Bits für die Restmantisse (die Nachkommastellen) zur
Verfügung mit dem Effekt einer verbesserten Genauigkeit. Oft wird daher die Anzahl der Mantis-
sen-Bits mit 24 (float) bzw. 53 (double) angegeben.
Eine float- bzw. double-Variable mit den Speicherinhalten
• v (0 oder 1) für das Vorzeichen
• e für den Exponenten
• m für die Mantisse
repräsentiert also bei normalisierter Darstellung den Wert:
(-1)v  (1 + m)  2e-127 bzw. (-1)v  (1 + m)  2e-1023
In der folgenden Tabelle finden Sie einige normalisierte float-Werte:
float-Darstellung (normalisiert)
Wert
Vorz. Exponent Mantisse
0,75 = (-1)0  2(126-127)  (1+0,5) 0 01111110 10000000000000000000000
1,0 = (-1)0  2(127-127)  (1+0,0) 0 01111111 00000000000000000000000
1,25 = (-1)0  2(127-127)  (1+0,25) 0 01111111 01000000000000000000000
-2,0 = (-1)1  2(128-127)  (1+0,0) 1 10000000 00000000000000000000000
2,75 = (-1)0  2(128-127)  (1+0,25+0,125) 0 10000000 01100000000000000000000
-3,5 = (-1)1  2(128-127)  (1+0,5+0,25) 1 10000000 11000000000000000000000
Nun kommen wir endlich zur Erklärung der eingangs dargestellten Genauigkeitsunterschiede beim
Speichern der Zahlen 1,25 und 1,3. Während die Restmantisse
0,25 = 0  2 -1 + 1 2 -2
1 1
= 0  + 1
2 4
perfekt dargestellt werden kann, gelingt dies bei der Restmantisse 0,3 nur approximativ:
0,3 = 0  2 −1 + 1 2 −2 + 0  2 −3 + 0  2 −4 + 1 2 −5 + ...
1 1 1 1 1
= 0  + 1 + 0  + 0  + 1 + ...
2 4 8 16 32
Sehr aufmerksame Leser werden sich darüber wundern, wieso die Tabelle mit den elementaren Da-
tentypen im Abschnitt 3.3.6 z. B.
1,4012984610-45
als betragsmäßig kleinsten positiven float-Wert nennt, obwohl der minimale Exponent nach obigen
Überlegungen -126 (= 1 - 127) beträgt, was zum (gerundeten) dezimalen Exponentialfaktor
106 Kapitel 3 Elementare Sprachelemente

1,17510-38
führt. Dahinter steckt die denormalisierte (synonym: subnormale) Gleitkommadarstellung, die als
Ergänzung zur bisher beschriebenen normalisierten Darstellung eingeführt wurde, um eine bessere
Annäherung an die Zahl 0 zu erreichen. Alle Exponenten-Bits sind auf 0 gesetzt, und dem Exponen-
tialfaktor wird der feste Wert 2-126 (float) bzw. 2-1022 (double) zugeordnet. Die Mantissen-Bits haben
dieselben Wertigkeiten (2-i) wie bei der normalisierten Darstellung (siehe oben). Weil es kein hid-
den bit gibt, stellen sie aber nun einen dezimalen Wert im Intervall [0, 1) dar. Eine float- bzw. dou-
ble-Variable mit dem Vorzeichen v (0 oder 1), mit komplett auf 0 gesetzten Exponenten-Bits und
mit dem gespeicherten Mantissenwert m repräsentiert also bei denormalisierter Darstellung die
Zahl:
(-1)v  2-126  m bzw. (-1)v  2-1022  m
In der folgenden Tabelle finden Sie einige denormalisierte float-Werte:
float-Darstellung (denormalisiert)
Wert
Vorz. Exponent Mantisse
0,0 = (-1)0  2-126  0 0 00000000 00000000000000000000000
-5,87747210-39  (-1)1  2-126  2-1 1 00000000 10000000000000000000000
1,40129810-45  (-1)0  2-126  2-23 0 00000000 00000000000000000000001
Weil die Mantissen-Bits auch zur Darstellung der Größenordnung verwendet werden, schwindet die
Genauigkeit mit der Annäherung an die Null.1
IntelliJ-Projekte mit Java-Programmen zur Anzeige der Bits einer (de)normalisierten float- bzw.
double-Zahl finden Sie in den Ordnern
…\BspUeb\Elementare Sprachelemente\Bits\FloatBits
…\BspUeb\Elementare Sprachelemente\Bits\DoubleBits
Weil im Quellcode der Programme mehrere noch unbekannte Sprachelemente auftreten, wird hier
auf eine Wiedergabe verzichtet. Einer Nutzung der Programme steht aber nichts im Wege. Hier
wird z. B. mit dem Programm FloatBits das Speicherabbild der float-Zahl -3,5 ermittelt (vgl. obige
Tabelle):
float: -3,5

Bits:
1 76543210 12345678901234567890123
1 10000000 11000000000000000000000
Zur Verarbeitung von binären Gleitkommazahlen wurde die binäre Gleitkommaarithmetik entwi-
ckelt, normiert und zur Verbesserung der Verarbeitungsgeschwindigkeit sogar teilweise in Compu-
ter-Hardware realisiert.

3.3.7.2 Dezimale Gleitkommadarstellung


Wenn die Speicher- und Rechengenauigkeit der binären Gleitkommatypen für eine Anwendung
nicht reicht, dann kommt in Java die Klasse BigDecimal aus dem Paket java.math in Frage. Objek-
te dieser Klasse können Dezimalzahlen mit fast beliebiger Genauigkeit speichern und verwenden
eine dezimale Gleitkommaarithmetik.

1
Bei einer formatierten Ausgaben in wissenschaftlicher Notation (vgl. Abschnitt 3.2.2) liegt die Anzahl der signifi-
kanten Dezimalstellen in der Mantisse deutlich unter 7.
Abschnitt 3.3 Variablen und Datentypen 107

Gespeichert werden:
• Eine Ganzzahl beliebiger Größe für den unskalierten Wert (uv)
• Eine Ganzzahl mit 32 Bit für die Anzahl der Nachkommastellen (scale)
Bei der Zahl
1,3 = 13  10-1
gelingt eine verlustfreie Speicherung mit:
uv = 13, scale = 1
Die Ausgabe des folgenden Programms
import java.math.BigDecimal;
class Prog {
public static void main(String[] args) {
BigDecimal bdd = new BigDecimal(1.3);
System.out.println(bdd);
BigDecimal bds = new BigDecimal("1.3");
System.out.println(bds);
}
}

belegt zunächst als Nachtrag zum Abschnitt 3.3.7.1, dass auch eine double-Variable den Wert 1,3
nur approximativ speichern kann:
1.3000000000000000444089209850062616169452667236328125
1.3
Zwar zeigt die Variable bdd auf ein Objekt vom Typ BigDecimal, doch wird zur Erstellung dieses
Objekts ein double-Wert verwendet, der im Speicher nicht exakt abgelegt werden kann.
Erfolgt die Kreation des BigDecimal-Objekts über eine Zeichenfolge, dann kann die Zahl 1,3 exakt
gespeichert werden, wie die zweite Ausgabezeile belegt.
Allerdings hat der Typ BigDecimal auch Nachteile im Vergleich zu den binären Gleitkommatypen
float und double:
• Höherer Speicherbedarf
• Höherer Zeitaufwand bei arithmetischen Operationen
• Aufwändigere Syntax
Bei der Aufgabe,
1000000000
1700000000 - 1,7
i =1

zu berechnen, ergeben sich für die Datentypen double und BigDecimal die folgenden Genauig-
keits- und Laufzeitunterschiede (gemessen auf einem PC mit der Intel-CPU Core i3 mit 3,2 GHz):1

1
Ein IntelliJ-Projekt mit dem Java-Programm, das die Berechnungen angestellt hat, ist hier zu finden:
…\BspUeb\Elementare Sprachelemente\BigDecimalDouble
108 Kapitel 3 Elementare Sprachelemente

double:
Abweichung: -29.96745276451111
Zeit in Millisekunden: 1206

BigDecimal:
Abweichung: 0.0
Zeit in Millisekunden: 8929
Die gut bezahlten Verantwortlichen vieler Banken, die sich gerne als „Global Player“ betätigen und
dabei den vollen Sinn der beiden Worte ausschöpfen (mit Niederlassungen in Schanghai, New
York, Mumbai etc. und einem Verhalten wie im Spielcasino) wären heilfroh, wenn nach einem
Spiel mit 1,7 Milliarden Euro Einsatz nur 30 Euro in der Kasse fehlen würden. Generell sind im
Finanzsektor solche Fehlbeträge aber unerwünscht, sodass man bei finanzmathematischen Aufga-
ben trotz des erhöhten Zeitaufwands (im Beispiel: Faktor ca. 7) die Klasse BigDecimal verwenden
sollte.
Sind in einem Algorithmus nur die Addition und die Subtraktion von ganzen Zahlen (z. B. Rech-
nungsbeträge in Cent) erforderlich, dann taugen auch die Ganzzahltypen int und long für monetäre
Berechnungen. Sie verursachen sehr wenig Aufwand und bieten eine perfekte Genauigkeit, sofern
ihr Wertebereich nicht verlassen wird.

3.3.8 Variablendeklaration, Initialisierung und Wertzuweisung


In einem Java-Programm muss jede Variable vor ihrer ersten Verwendung deklariert werden, wobei
auf jeden Fall ein Datentyp und ein Name anzugeben sind. Wir betrachten vorläufig nur lokale Va-
riablen, die innerhalb einer Methode existieren. Ihre Deklaration darf im Methodenquellcode an
beliebiger Stelle vor der ersten Verwendung erscheinen. Um den (im Abschnitt 3.3.9 behandelten)
Gültigkeitsbereich einer lokalen Variablen zur Vermeidung von Fehlern zu minimieren, sollte sie
unmittelbar vor der ersten Verwendung deklariert werden (Bloch 2018, S. 261).
Es folgt das Syntaxdiagramm zur Deklaration einer lokalen Variablen, wobei zunächst der Über-
sichtlichkeit halber die mit Java 10 eingeführte Typinferenz (siehe unten) ignoriert wird:
Deklaration einer lokalen Variablen

Datentyp Variablenname = Ausdruck ;

Als Datentypen kommen in Frage (vgl. Abschnitt 3.3.3):


• Primitive Datentypen, z. B.
int wasser;
• Referenztypen, also Klassen (aus dem Java-API oder selbst definiert), z. B.
Bruch b1;
Neu deklarierte lokale Variablen kann man optional gleich initialisieren, also auf einen gewünsch-
ten Wert bringen, z. B.:
int wasser = 4711;
Bruch b1 = new Bruch();
Im zweiten Beispiel wird per new-Operator ein Bruch-Objekt erzeugt und dessen Adresse in die
Referenzvariable b1 geschrieben. Mit der Objektkreation und auch mit der Konstruktion von gülti-
Abschnitt 3.3 Variablen und Datentypen 109

gen Ausdrücken, die einen Wert von passendem Datentyp liefern müssen, werden wir uns noch aus-
führlich beschäftigen.
Es ist üblich, Variablennamen mit einem Kleinbuchstaben beginnen zu lassen (vgl. Abschnitt
3.3.2), sodass man sie im Quelltext z. B. gut von Klassennamen unterscheiden kann, die per Kon-
vention mit einem Großbuchstaben beginnen.
Weil lokale Variablen nicht automatisch initialisiert werden, muss man ihnen vor dem ersten lesen-
den Zugriff einen Wert zuweisen. Auch im Umgang mit uninitialisierten lokalen Variablen zeigt
sich das Bemühen der Java-Designer um robuste Programme. Während C++ - Compiler in der Re-
gel nur warnen, produzieren Java-Compiler eine Fehlermeldung und erstellen keinen Bytecode.1
Dieses Verhalten wird durch das folgende Programm demonstriert:
class Prog {
public static void main(String[] args) {
int argument;
System.out.print("Argument = " + argument);
}
}
Der OpenJDK 17 - Compiler meint dazu:
Prog.java:4: error: variable argument might not have been initialized
System.out.print("Argument = " + argument);
^
1 error

IntelliJ markiert den Fehler und schlägt eine sinnvolle Reparaturmaßnahme vor:

Weil Instanz- und Klassenvariablen automatisch mit dem typspezifischen Nullwert initialisiert wer-
den (siehe unten), kann in einem Java-Programm kein Zugriff auf undefinierte Werte stattfinden.
Wie bereits erwähnt, können Java-Compiler seit der Version 10 den Typ von lokalen Variablen aus
einem zugewiesenen Initialisierungswert erschließen (Typinferenz), und man darf in der Variablen-
deklaration die Typangabe durch das Schlüsselwort var ersetzen, z. B.:

1
Der im Visual Studio 2019 enthaltene C++ - Compiler der Firma Microsoft produziert beim Lesezugriff auf eine
nicht-initialisierte lokale Variable z. B. die Warnung C4700, siehe https://docs.microsoft.com/de-de/cpp/error-
messages/compiler-warnings/compiler-warning-level-1-and-level-4-c4700?view=vs-2019.
110 Kapitel 3 Elementare Sprachelemente

Im Beispiel gelingt die Inferenz, weil die zugewiesene Zahl 4711, die wir im Abschnitt 3.3.11.1 als
Ganzzahlliteral bezeichnen werden, den Typ int besitzt.
Wie das Syntaxdiagramm zur Deklaration einer lokalen Variablen mit Typinferenz zeigt,
Deklaration einer lokalen Variablen mit Typinferenz

var Variablenname = Ausdruck ;

sind die folgenden Regeln einzuhalten:


• Es kann nur eine Variable deklariert werden.
• Es muss eine Initialisierung erfolgen.
Bei komplexen Typangaben, die im Kurs bisher noch nicht aufgetaucht sind, ermöglicht das
Schlüsselwort var eine Arbeitserleichterung. Wenn sich dabei die Lesbarkeit des Quellcodes ver-
schlechtert, wird aber die Nutzung des Codes (durch andere Programmierer) erschwert.
Durch lokale Variablen werden namensgleiche Instanz- bzw. Klassenvariablen überdeckt. Diese
bleiben jedoch über ein geeignetes Präfix weiter ansprechbar:
• this bei Instanzvariablen
• Klassenname bei statischen Variablen
Weil eine solche Benennungspraxis kaum sinnvoll ist, verzichten wir auf Beispiele.
Um den Wert einer Variablen im weiteren Programmablauf zu verändern, verwendet man eine
Wertzuweisung, die zu den einfachsten Java-Anweisungen gehört:
Wertzuweisungsanweisung

Variablenname = Ausdruck ;

Beispiel:
az = az - an;
Durch diese Wertzuweisungsanweisung aus der kuerze() - Methode unserer Klasse Bruch (siehe
Abschnitt 1.1.2) erhält die int-Variable az den neuen Wert az - an.
Es wird sich bald herausstellen, dass auch ein Ausdruck stets einen Datentyp besitzt. Bei der Wert-
zuweisung muss dieser Typ kompatibel zum Datentyp der Variablen sein.
Mittlerweile haben Sie Java-Anweisungen für die folgenden Zwecke kennengelernt:
• Deklaration einer lokalen Variablen
• Wertzuweisung
Abschnitt 3.3 Variablen und Datentypen 111

3.3.9 Blöcke und Sichtbarkeitsbereiche für lokale Variablen


Wie Sie bereits wissen, besteht der Rumpf einer Methodendefinition aus einem Block mit beliebig
vielen Anweisungen, abgegrenzt durch geschweifte Klammern. Innerhalb des Methodenrumpfes
können untergeordnete Anweisungsblöcke gebildet werden, wiederum durch geschweifte Klammen
begrenzt:
Block- bzw. Verbundanweisung

{ Anweisung }

Man spricht hier auch von einer Block- bzw. Verbundanweisung, und diese kann überall stehen,
wo eine einzelne Anweisung erlaubt ist.1
Unter den Anweisungen innerhalb eines Blocks dürfen sich selbstverständlich auch wiederum Ver-
bundanweisungen befinden. Einfacher ausgedrückt: Blöcke dürfen geschachtelt werden.
In der Regel verwendet man die Blockanweisung als Bestandteil einer bedingten Anweisung oder
einer Wiederholungsanweisung (siehe Abschnitt 3.7). Bei diesen Kontrollstrukturen wird eine An-
weisung unter einer Bedingung bzw. wiederholt ausgeführt. Sollen z. B. unter einer Bedingung
mehrere Anweisungen ausgeführt werden, wäre die Wiederholung der Bedingung für jede einzelne
Anweisung außerordentlich lästig. Stattdessen fasst man die Anweisungen zu einem Block zusam-
men, der als eine Anweisung gilt, sodass die Bedingung nur einmal formuliert werden muss. Dieses
sehr oft benötigte Muster ist z. B. in der Methode setzeNenner() der Klasse Bruch zu sehen:
public boolean setzeNenner(int n) {
if (n != 0) {
nenner = n;
return true;
} else
return false;
}

Anweisungsblöcke haben einen wichtigen Effekt auf die Sichtbarkeit (alias: Gültigkeit) der darin
deklarierten Variablen: Eine lokale Variable ist verfügbar von der deklarierenden Anweisung bis
zur schließenden Klammer des Blocks, in dem sich die Deklaration befindet. Nur in diesem Sicht-
barkeitsbereich (alias: Gültigkeitsbereich, engl. scope) kann sie über ihren Namen angesprochen
werden. Im folgenden (weitgehend sinnfreien) Beispielprogramm wird versucht, auf die Variable
wert2 außerhalb ihres Sichtbarkeitsbereichs zuzugreifen:
class Prog {
public static void main(String[] args) {
int wert1 = 1;
System.out.println("Wert1 = " + wert1);
if (wert1 == 1) {
int wert2 = 2;
System.out.println("Gesamtwert = " + (wert1 + wert2));
}
System.out.println("Wert2 = " + wert2);
}
}
Das veranlasst den OpenJDK 17 - Compiler zu der folgenden Fehlermeldung:

1
Ein Block ohne enthaltene Anweisung
{}
wird vom Compiler als Anweisung akzeptiert, z. B. als Rumpf einer Methode, die keinerlei Tätigkeit entfalten soll.
112 Kapitel 3 Elementare Sprachelemente

Prog.java:9: error: cannot find symbol


System.out.println("Gesamtwert = " + (wert1 + wert2));
^
symbol: variable wert2
location: class Prog
1 error

Wird die fehlerhafte Zeile auskommentiert, lässt sich das Programm übersetzen. In dem zur if-
Anweisung gehörenden Block ist die im übergeordneten Block der main() - Methode deklarierte
Variable wert1 also gültig.
Bei hierarchisch geschachtelten Blöcken ist es in Java nicht erlaubt, auf mehreren Stufen Variablen
mit identischem Namen zu deklarieren. Diese kaum sinnvolle Option ist z. B. in der Programmier-
sprache C++ vorhanden und erlaubt dort Fehler, die schwer aufzuspüren sind. In Java gehört ein
eingeschachtelter Block zum Gültigkeitsbereich des umgebenden Blocks.
Der Sichtbarkeitsbereich einer lokalen Variablen sollte möglichst klein gehalten werden, um die
Lesbarkeit und die Wartungsfreundlichkeit des Quellcodes zu verbessern. Vor allem wird auf diese
Weise das Risiko von Programmierfehlern reduziert. Wird eine Variable zu früh deklariert, beste-
hen viele Gelegenheiten für schädliche Wertzuweisungen. Aus einer längst überwundenen Ver-
pflichtung alter Programmiersprachen ist bei manchen Programmierern die Gewohnheit entstanden,
alle lokale Variablen am Blockbeginn zu deklarieren. Stattdessen sollten lokale Variablen zur Mi-
nimierung ihres Sichtbarkeitsbereichs unmittelbar vor der ersten Verwendung deklariert werden
(Bloch 2018, S. 261).
Zur übersichtlichen Gestaltung von Java-Programmen ist das Einrücken von Anweisungsblöcken
sehr zu empfehlen, wobei Sie die Position der einleitenden Blockklammer und die Einrücktiefe
nach persönlichem Geschmack wählen können, z. B.:
if (wert1 == 1) { if (wert1 == 1)
int wert2 = 2; {
System.out.println("Wert2 = " + wert2); int wert2 = 2;
} System.out.println("Wert2 = " + wert2);
}

In IntelliJ kann man Regeln zum Quellcode-Layout definieren und auf eine Quellcodedatei anwen-
den (siehe Abschnitt 3.1.4). Wie man einstellt, ob IntelliJ zum Einrücken ein Tabulatorzeichen oder
eine (wählbare) Anzahl von Leerzeichen verwenden soll, wurde im Abschnitt 2.4.6.4 beschrieben.
Ein markierter Block aus mehreren Zeilen kann in IntelliJ mit
Tab komplett nach rechts eingerückt
und mit
Umschalt + Tab komplett nach links ausgerückt
werden.
Außerdem kann man sich zu einer Blockklammer das Gegenstück anzeigen lassen:
Abschnitt 3.3 Variablen und Datentypen 113

Einfügemarke des Editors neben der Startklammer

hervorgehobene Endklammer

3.3.10 Finalisierte lokale Variablen


In der Regel sollten auch die im Programm benötigten konstanten Werte (z. B. für den Mehrwert-
steuersatz) in einer Variablen abgelegt und im Quellcode über ihren Variablennamen angesprochen
werden, denn:
• Bei einer späteren Änderung des Werts ist nur die Quellcodezeile mit der Variablendeklara-
tion und -initialisierung betroffen.
• Der Quellcode ist leichter zu lesen, wenn Variablennamen an Stelle von „magischen Zah-
len“ stehen.
Beispiel:
Quellcode Ausgabe
class Prog { Brutto: 119.0
public static void main(String[] args) {
final double mwst = 1.19;
double netto = 100.0, brutto;
brutto = netto * mwst;
System.out.println("Brutto: " + brutto);
}
}

Lokale Variablen, die nach ihrer Initialisierung auf denselben Wert fixiert bleiben sollen, deklariert
man als final. Für finalisierte lokale (in einer Methode deklarierte) Variablen erhalten wir folgendes
Syntaxdiagramm:
Deklaration einer finalisierten lokalen Variablen

final Datentyp Name = Ausdruck ;

Im Unterschied zur gewöhnlichen Variablendeklaration ist einleitend der Modifikator final zu set-
zen. Das Initialisieren einer finalisierten Variablen kann bei der Deklaration oder in einer späteren
Wertzuweisung erfolgen. Danach ist keine weitere Wertveränderung mehr erlaubt.
Auch für eine finalisierte lokale Variable kann bei der Deklaration aufgrund der Fähigkeit des
Compilers zur Typinferenz das Schlüsselwort var statt des Datentyps angegeben werden, z. B.:
final var mwst = 1.19;
114 Kapitel 3 Elementare Sprachelemente

Im Beispiel kann der Compiler für die Variable mwst den Datentyp double aus dem Initialisie-
rungswert ableiten (siehe Abschnitt 3.3.11.2).
Durch Verwendung des Modifikators final schützen wir uns davor, einen als fixiert geplanten Wert
versehentlich doch zu ändern. In manchen Fällen wird auf diese Weise ein unangenehmer und nur
mit großem Aufwand aufzuklärender Logikfehler zu einem harmlosen Syntaxfehler, der vom Com-
piler aufgedeckt, vom Entwickler ohne nennenswerten Aufwand beseitigt und vom Benutzer nie
erlebt wird (Simons 2004, S. 51).
Weitere Argumente für das Finalisieren:
• Andere Programmierer, die später ebenfalls mit einer Methode arbeiten, erhalten durch die
final-Deklaration eine wichtige Information zur intendierten Verwendung der betroffenen
Variablen.
• Im funktionalen Programmierstil werden finalisierte (unveränderliche) Variablen strikt be-
vorzugt (vgl. Kapitel 12). Unsere Entwicklungsumgebung trägt dem modernen Trend in der
Programmiertheorie Rechnung und macht durch Unterstreichen darauf aufmerksam, dass
der Wert einer Variablen geändert wird:

Daraus sollte auf keinen Fall die Empfehlung abgeleitet werden, auf veränderbare Variablen
zu verzichten.
Durch den systematischen Gebrauch des final-Modifikators für lokale Variablen wirken Beispiel-
programme allerdings etwas komplizierter, sodass im Manuskript oft der Einfachheit halber auf den
final-Modifikator verzichtet wird.
Neben lokalen Variablen können auch (statische) Felder einer Klasse als final deklariert werden
(siehe Abschnitte 4.2.5 und 4.5.1).
Die empfohlene Camel Casing - Namenskonvention (vgl. Abschnitt 3.3.2) gilt bei lokalen Variab-
len trotz final-Deklaration. Nur bei static-Feldern mit final-Modifikator ist es üblich, den Namen
komplett in Großbuchstaben zu schreiben (siehe Bloch 2018, S. 290).

3.3.11 Literale
Die im Quellcode auftauchenden expliziten Werte bezeichnet man als Literale. Wie Sie aus dem
Abschnitt 3.3.10 wissen, sollten Literale vorzugsweise bei der Initialisierung von finalen Variablen
verwendet werden, z. B.:
final double mwst = 1.19;
Auch die Literale besitzen in Java stets einen Datentyp, wobei einige Regeln zu beachten sind, die
gleich erläutert werden. Im aktuellen Abschnitt 3.3.11 haben manche Passagen Nachschlage-
charakter, sodass man beim ersten Lesen nicht jedes Detail aufnehmen muss bzw. kann.

3.3.11.1 Ganzzahlliterale
Für ein Ganzzahlliteral wird meist das dezimale Zahlensystem verwendet, z. B.:
final int wasser = 4711;
Java unterstützt aber auch alternative Zahlensysteme:
Abschnitt 3.3 Variablen und Datentypen 115

• das binäre (mit der Basis 2 und den Ziffern 0, 1),


• das oktale (mit der Basis 8 und den Ziffern 0, 1, 2, …, 7)
• und das hexadezimale (mit der Basis 16 und den Ziffern 0, 1, …, 9, A, B, C, D, E, F)
Wenn ein Ganzzahlliteral in einem nicht-dezimalen Zahlensystem interpretiert werden soll, muss
ein Präfix mit einleitender Null vorangestellt werden:
Beispiele
Zahlensystem Präfix
println() - Aufruf Ausgabe
binär 0b, 0B System.out.println(0b11); 3

oktal 0 System.out.println(011); 9

hexadezimal 0x, 0X System.out.println(0x11); 17

Für das Ganzzahlliteral 0x11 ergibt sich der dezimale Wert 17 aufgrund der Stellenwertigkeiten im
Hexadezimalsystem folgendermaßen:
11Hex = 1  161 + 1  160 = 1  16 + 1  1 = 17
Vermutlich fragen Sie sich, wozu man sich mit dem Hexadezimalsystem plagen sollte. Gelegentlich
ist ein ganzzahliger Wert (z. B. als Methodenparameter) anzugeben, den man (z. B. aus einer
Tabelle) nur in hexadezimaler Darstellung kennt. In diesem Fall spart man sich durch Verwendung
dieser Darstellung die Wandlung in das Dezimalsystem.
Tückisch ist der Präfix für die (selten benötigten) Literale im Oktalsystem. Die führende Null im
Ganzzahlliteral 011 ist keinesfalls irrelevant, sondern bewirkt eine oktale Interpretation:
11Oktal = 1  8 + 1  1 = 9
Unabhängig vom verwendeten Zahlensystem haben Ganzzahlliterale in Java den Datentyp int,
wenn nicht durch das Suffix L oder l der Datentyp long erzwungen wird. Das ist im folgenden Bei-
spiel
final long betrag = 2147483648L;
erforderlich, weil anderenfalls bei der Zwischenspeicherung des int-wertigen Ausdrucks rechts vom
Gleichheitszeichen ein Ganzzahlüberlauf (vgl. Abschnitt 3.6.1) auftreten würde:

Die schlussendliche Speicherung in der Variablen betrag vom Typ long (mit einem sehr viel grö-
ßeren Wertebereich) würde den Defekt im Zwischenergebnis nicht verhindern.
Der Kleinbuchstabe l ist leicht mit der Ziffer 1 zu verwechseln und daher als Suffix wenig geeignet.
Dass ein Ganzzahlliteral tatsächlich per Voreinstellung den Datentyp int besitzt, können Sie mit
Hilfe unserer Entwicklungsumgebung überprüfen. Befindet sich die Einfügemarke neben einem
(oder in einem) Ganzzahlliteral, dann liefert die Tastenkombination
Umschalt + Strg + P
den Datentyp, z. B.:
116 Kapitel 3 Elementare Sprachelemente

Seit Java 7 dürfen bei Ganzzahlliteralen zwischen zwei Ziffern Unterstriche zur optischen Gruppie-
rung gesetzt werden, z. B.:
final int wasser = 4_711;
Weil int-Literale als Bestandteile der im nächsten Abschnitt behandelten Gleitkommaliterale auftre-
ten, lässt sich die Zifferngruppierung durch Unterstriche auch dort verwenden.

3.3.11.2 Gleitkommaliterale
Zahlen mit Dezimalpunkt oder Exponent sind in Java vom Typ double, wenn nicht durch das Suf-
fix F oder f der Datentyp float erzwungen wird, z. B.:
final double mwst = 1.19;
final float ff = 9.78f;
Mit dem Suffix D oder d wird auch bei einer Zahl ohne Dezimalpunkt oder Exponent der Datentyp
double erzwungen. Warum das Suffix d im folgenden Beispiel für das korrekte Rechenergebnis
sorgt, erfahren Sie im Abschnitt 3.5.1 bei der Behandlung des Unterschieds zwischen der Ganzzahl-
und der Gleitkommaarithmetik:
Quellcode Ausgabe
class Prog { 2
public static void main(String[] args) { 2.5
System.out.println(5/2);
System.out.println(5d/2);
}
}

Die Java-Compiler achten bei Wertzuweisungen unter Verwendung von Gleitkommaliteralen streng
auf die Typkompatibilität. Z. B. führt die folgende Deklaration mit Initialisierung:
final float mwst = 1.19;
zu einer Fehlermeldung, weil das Gleitkommaliteral (und damit der Ausdruck rechts vom Gleich-
heitszeichen) den Typ double besitzt, die Variable mwst hingegen den Typ float:

Neben der alltagsüblichen Schreibweise (mit dem Punkt als Dezimaltrennzeichen) erlaubt Java bei
Gleitkommaliteralen auch die wissenschaftliche Exponentialnotation (mit der Basis 10), z. B. bei
der Zahl -0,00000000010745875):
Vorzeichen Vorzeichen
Mantisse Exponent

-1.0745875e-10
Mantisse Exponent

Eine Veränderung des Exponenten lässt das Dezimaltrennzeichen gleiten und macht somit die Be-
zeichnung Gleitkommaliteral (engl.: floating-point literal) plausibel.
Abschnitt 3.3 Variablen und Datentypen 117

In den folgenden Syntaxdiagrammen werden die wichtigsten Regeln für Gleitkommaliterale be-
schrieben:
Gleitkommaliteral
+ f

- d

Mantisse Exponent

Mantisse

int-Literal . int-Literal

Exponent
e -

int-Literal
E

Die in der Mantisse und im Exponenten auftretenden Ganzzahlliterale müssen das dezimale Zahlen-
system verwenden und den Datentyp int besitzen, sodass die im Abschnitt 3.3.11.1 beschriebenen
Präfixe (0, 0b, 0B, 0x, 0X) und Suffixe (L, l) verboten sind. Die Exponenten werden zur Basis 10
verstanden.
Der Einfachheit halber unterschlagen die Syntaxdiagramme die folgende, im letzten Beispielpro-
gramm benutzte Konstruktion eines Gleitkommaliterals über das Suffix d:
System.out.println(5d/2);

3.3.11.3 boolean-Literale
Als Literale vom Typ boolean sind nur die beiden reservierten Wörter true und false erlaubt, z. B.:
boolean cond = true;

3.3.11.4 char-Literale
char-Literale werden in Java durch einfache Hochkommata begrenzt. Es sind erlaubt:
• Einfache Zeichen
Beispiel:
char bst = 'b';
Das einfache Hochkomma kann allerdings auf diese Weise ebenso wenig zum char-Literal
werden wie der Rückwärts-Schrägstrich (\). In diesen Fällen benötigt man eine sogenannte
Escape-Sequenz:
• Escape-Sequenzen
Indem man ein Zeichen hinter einen einleitenden Rückwärts-Schrägstrich setzt (z. B. \',
\n) und damit eine sogenannte Escape-Sequenz bildet, kann man …
118 Kapitel 3 Elementare Sprachelemente

o ein Zeichen von seiner besonderen Bedeutung befreien (z. B. das zur Begrenzung
von char-Literalen dienende Hochkomma) und wie ein einfaches Zeichen behan-
deln, z. B.:
\'
\\
o ein Steuerzeichen für die Textausgabe im Konsolenfenster ansprechen, z. B.:
Neue Zeile \n
Horizontaler Tabulator \t
Space (Leerzeichen) \s
Backspace (Löschen nach links) \b
Wir werden die Escape-Sequenz \n oft in Zeichenfolgenliteralen (siehe Abschnitt
3.3.11.5) unter normale Zeichen mischen, um bei der Konsolenausgabe einen Zei-
lenwechsel anzuordnen.1
Beispiel:
final char rs = '\\';
• Unicode - Escape-Sequenzen
Eine Unicode - Escape-Sequenz enthält eine Unicode-Zeichennummer (vorzeichenlose
Ganzzahl mit 16 Bits, also im Bereich von 0 bis 216-1 = 65535) in hexadezimaler, vierstelli-
ger Schreibweise (ggf. links mit Nullen aufgefüllt) ohne Hexadezimal-Präfix) nach der Ein-
leitung durch \u (kleines u!). So lassen sich Zeichen ansprechen, die per Tastatur nicht ein-
zugeben sind, z. B.:
final char alpha = '\u03b1';
Im Konsolenfenster werden die Unicode-Zeichen oberhalb von \u00ff in der Regel als
Fragezeichen dargestellt. In einem GUI-Fenster erscheinen sie jedoch in voller Pracht (siehe
nächsten Abschnitt).

3.3.11.5 Zeichenfolgenliterale
Zeichenfolgenliterale werden (im Unterschied zu char-Literalen) durch doppelte Hochkommata
begrenzt. Ein Zeichenfolgenliteral kann einfache Zeichen, Escape-Sequenzen und Unicode - Es-
cape-Sequenzen enthalten (vgl. Abschnitt 3.3.11.4). Das einfache Hochkomma hat innerhalb eines
Zeichenfolgenliterals keine Sonderrolle, z. B.:
System.out.println("Otto's Welt");
Um ein doppeltes Hochkomma in eine Zeichenfolge aufzunehmen, ist die Escape-Sequenz \" zu
verwenden.
Zeichenkettenliterale sind vom Datentyp String, und später wird sich herausstellen, dass es sich bei
diesem Datentyp um eine Klasse aus dem Java-API handelt.
Während ein char-Literal stets genau ein Zeichen enthält, kann ein Zeichenkettenliteral aus belie-
big vielen Zeichen bestehen oder auch leer sein, z. B.:
final String leerStr = "";

1
Bei der Ausgabe in eine Textdatei sollte die Escape-Sequenz \n nicht verwendet werden, weil sie nicht auf allen
Plattformen bzw. von allen Editoren als Zeilenwechsel interpretiert wird. Durch \n wird nämlich auf allen Plattfor-
men dasselbe Byte (Bedeutung: Line Feed) in den Ausgabestrom befördert. In Textdateien wird unter den Betriebs-
systemen Linux, Unix und macOS durch \n eine Zeilentrennung signalisiert; unter Windows wird dieser Zweck hin-
gegen durch die Sequenz \r\n erreicht. Wird in eine auszugebende Zeichenfolge ein Zeilenwechsel mit Hilfe der
Formatspezifikation %n eingefügt (vgl. Abschnitt 3.2.2), dann landet auf jeder Plattform in einer Textausgabedatei
die plattformspezifische Zeilentrennung.
Abschnitt 3.3 Variablen und Datentypen 119

Das folgende Programm verwendet einen Aufruf der statischen Methode showMessageDialog() der
Klasse JOptionPane aus dem Paket javax.swing (seit Java 9 im Modul java.desktop) zur Anzeige
eines Zeichenfolgenliterals, das drei Unicode - Escape-Sequenzen enthält:1
class Prog {
public static void main(String[] args) {
javax.swing.JOptionPane.showMessageDialog(null, "\u03b1, \u03b2, \u03b3");
}
}
Beim Programmstart erscheint das folgende Fenster:

3.3.11.6 Referenzliteral null


Einer Referenzvariablen kann das Referenzliteral null zugewiesen werden, z. B.:2
Bruch b1 = null;
Damit ist sie nicht undefiniert, sondern zeigt explizit auf nichts.
Zeigt eine Referenzvariable aktuell auf ein existentes Objekt, kann man diese Referenz per null-
Zuweisung aufheben. Sofern im Programm keine andere Referenz auf dasselbe Objekt vorliegt, ist
es zum Abräumen durch den Garbage Collector der JVM freigegeben.

1
Im Manuskript wird überwiegend an Stelle des betagten GUI-Frameworks Swing die moderne Alternative JavaFX
(alias OpenJFX) verwendet. Beim aktuellen Beispiel verursacht die JavaFX-Variante aber erheblich mehr Aufwand
und Vorgriffe auf noch unbehandelte Kursthemen (z. B. Vererbung):
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
import javafx.application.Application;
import javafx.stage.Stage;

public class Prog extends Application {


@Override
public void start(Stage primaryStage) {
Alert alert = new Alert(AlertType.INFORMATION, "\u03b1, \u03b2, \u03b3");
alert.setHeaderText("");
alert.showAndWait();
}
public static void main(String[] args) {
launch(args);
}
}
Das Ergebnis:

2
Da Java eine streng typisierte Programmiersprache ist, und das Literal null einen Ausdruck darstellt (vgl. Abschnitt
3.5), muss es einen Datentyp besitzen. Es ist der Nulltyp (engl.: null type). Weil es in Java keinen Bezeichner für
den Nulltyp gibt, kann man keine Variable von diesem Typ deklarieren. Wie das folgende Zitat aus der aktuellen Ja-
va-Sprachspezifikation (Gosling et al. 2021, S. 54) belegt, müssen Sie sich um den Nulltyp keine Gedanken machen:
In practice, the programmer can ignore the null type and just pretend that null is merely a special literal that can
be of any reference type.
120 Kapitel 3 Elementare Sprachelemente

3.4 Eingabe bei Konsolenprogrammen


Konsolenprogramme sind ein geeignetes Umfeld, um die Programmiersprache Java zu erlernen und
mit der Standardbibliothek vertraut zu werden. Bald werden wir selbstverständlich auch die Erstel-
lung von Anwendungen mit grafischer Bedienoberfläche behandeln. Um mit Konsolenanwendun-
gen unsere didaktischen Ziele zu erreichen, benötigen wir eine Möglichkeit, Benutzereingaben ent-
gegenzunehmen. Im aktuellen Abschnitt wird eine Lösung vorgestellt, die sich mit geringem Auf-
wand in unseren Demonstrations- und Übungsprogrammen verwenden lässt.

3.4.1 Die Klassen Scanner und Simput


Für die Übernahme von Tastatureingaben in Konsolenprogrammen kann die API-Klasse Scanner
(Paket java.util, ab Java 9 im Modul java.base) verwendet werden.1 Im folgenden Beispielpro-
gramm zur Berechnung der Fakultät zu einer ganzen Zahl wird ein Scanner-Objekt per nextInt() -
Methodenaufruf gebeten, vom Benutzer eine int-Ganzzahl entgegenzunehmen:
import java.util.Scanner;
class Prog {
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.print("Argument: ");
int argument = input.nextInt();
double fakul = 1.0;
for (int i = 2; i <= argument; i++)
fakul = fakul * i;
System.out.println("Fakultät: " + fakul);
}
}
Zwei Hinweise zum Quellcode:
• Weil sich die Klasse Scanner im API-Paket java.util befindet, ist eine import-Deklaration
erforderlich, um die Klasse im Quellcode ohne Paket-Präfix ansprechen zu können.
• Die im Programm verwendete for-Wiederholungsanweisung wird im Abschnitt 3.7.3 be-
handelt.
Bei einer gültigen Eingabe arbeitet das Programm wunschgemäß, z. B.:
Argument: 4
Fakultät: 24.0

Auf ungültige Benutzereingaben reagiert die Methode nextInt() mit einem sogenannten Ausnah-
mefehler (siehe Kapitel 11), und das Programm „stürzt ab“, z. B.:
Argument: vier
Exception in thread "main" java.util.InputMismatchException
at java.base/java.util.Scanner.throwFor(Scanner.java:939)
at java.base/java.util.Scanner.next(Scanner.java:1594)
at java.base/java.util.Scanner.nextInt(Scanner.java:2258)
at java.base/java.util.Scanner.nextInt(Scanner.java:2212)
at Prog.main(Prog.java:6)

Es wäre nicht allzu aufwändig, in der Fakultätsanwendung ungültige Eingaben abzufangen. Aller-
dings stehen uns die erforderlichen Programmiertechniken (der Ausnahmebehandlung) noch nicht
zur Verfügung, und außerdem ist bei den möglichst kurzen Demonstrations- bzw. Übungspro-
grammen jeder Zusatzaufwand störend.

1
Mit den Paketen und Modulen der Standardbibliothek werden wir uns später ausführlich beschäftigen. An dieser
Stelle dient die Angabe der Paket- und Modulzugehörigkeit dazu, eine Klasse eindeutig zu identifizieren und die
Standardbibliothek allmählich kennenzulernen.
Abschnitt 3.4 Eingabe bei Konsolenprogrammen 121

Um Tastatureingaben in Konsolenprogrammen bequem und sicher bewerkstelligen zu können,


wurde für den Kurs eine Klasse namens Simput erstellt.1 Mit Hilfe der Klassenmethode
Simput.gint() lässt sich das Fakultätsprogramm einfacher und zugleich robust gegenüber Ein-
gabefehlern realisieren:
class Prog {
public static void main(String[] args) {
System.out.print("Argument: ");
int argument = Simput.gint();
double fakul = 1.0;
for (int i = 2; i <= argument; i++)
fakul = fakul * i;
System.out.println("Fakultät: " + fakul);
}
}
Weil die Klasse Simput keinem Paket zugeordnet wurde, gehört sie zum Standardpaket und kann
daher in anderen Klassen des Standardpakets bequem ohne Paket-Präfix bzw. Paket-Import ange-
sprochen werden (vgl. Abschnitt 3.1.7). In Klassen anderer Pakete steht Simput (wie alle anderen
Klassen des Standardpakets) jedoch nicht zur Verfügung. Im Kurs erstellen wir meist kleine De-
monstrationsprogramme und verwenden dabei der Einfachheit halber das Standardpaket, sodass die
Klasse Simput als bequemes Hilfsmittel genutzt werden kann. Für ernsthafte Projekte werden Sie
jedoch eigene Pakete definieren (siehe Kapitel 6), sodass die (kompilierte) Klasse Simput dort
nicht verwendbar ist. Diese Einschränkung ist aber leicht durch eine Änderung des Quellcodes in
der Datei Simput.java zu beheben.
Die statische Simput-Methode gint() erwartet vom Benutzer eine per Enter-Taste quittierte
Eingabe und versucht, diese als int-Wert zu interpretieren. Im Erfolgsfall erhält die aufrufende Me-
thode das Ergebnis als gint() - Rückgabewert. Anderenfalls sieht der Benutzer eine Fehlermel-
dung, und die aufrufende Methode erhält den (Verlegenheits-)Rückgabewert 0, z. B.
Argument: vier
Falsche Eingabe!

Fakultät: 1.0

Die Simput-Klassenmethode gint() besitzt den Zugriffsmodifikator public, liefert eine Rückga-
be vom Typ int und hat keine Parameter. Diese Eigenschaften werden durch den Methodenkopf
dokumentiert:
public static int gint()
Auch in der Java-API - Dokumentation wird zur Beschreibung einer Methode deren Definitions-
kopf angegeben, z. B. bei der statischen Methode exp() der Klasse Math im Paket java.lang:

1
Die Datei Simput.java ist im folgenden Verzeichnis zu finden (weitere Ortsangaben siehe Vorwort):
…\BspUeb\Simput\Standardpaket\IntelliJ-Projekt\src
Die zugehörige Bytecode-Datei Simput.class steckt im Ordner
...\BspUeb\Simput\Standardpaket\IntelliJ-Projekt\out\production\Simput
und der Bequemlichkeit auch im Ordner
…\BspUeb\Simput\Standardpaket
122 Kapitel 3 Elementare Sprachelemente

Ergänzend liefert die API-Dokumentation aber auch Informationen zur Arbeitsweise der Methode,
zur Rolle der Parameter, zum Inhalt der Rückgabe und ggf. zu den möglichen Ausnahmefehlern.
Bei gint() oder anderen Simput-Methoden, die auf Eingabefehler nicht mit einer Ausnahme rea-
gieren (vgl. Abschnitt 11), kann man sich durch einen Aufruf der Simput-Klassenmethode
checkError() mit Rückgabetyp boolean darüber informieren, ob ein Fehler aufgetreten ist
(Rückgabewert true) oder nicht (Rückgabewert false). Die Simput-Klassenmethode
getErrorDescription() hält im Fehlerfall darüber hinaus eine Erläuterung bereit.1 In obigem
Beispielprogramm ignoriert die aufrufende Methode main() allerdings die diagnostischen Informa-
tionen und liefert ggf. eine unpassende Ausgabe. Wir werden in vielen weiteren Beispielprogram-
men den gint() - Rückgabewert der Kürze halber ohne Fehlerstatuskontrolle benutzen. Bei An-
wendungen für den praktischen Einsatz sollte aber wie in der folgenden Variante des Fakultätspro-
gramms eine Überprüfung stattfinden. Die dazu erforderliche if-Anweisung wird im Abschnitt 3.7.2
behandelt.
Quellcode Eingabe (grün, kursiv) und Ausgabe
class Prog { Argument: vier
public static void main(String args[]) { Falsche Eingabe!
System.out.print("Argument: ");
int argument = Simput.gint(); Die Eingabe konnte nicht konvertiert werden.
double fakul = 1.0;
if (!Simput.checkError()) {
for (int i = 2; i <= argument; i += 1)
fakul = fakul * i;
System.out.println("Fakultät: "+fakul);
} else
System.out.println(
Simput.getErrorDescription());
}
}

1
Weil Simput der Einfachheit halber mit statischen Methoden arbeitet, darf die Klasse nicht simultan durch mehrere
Threads verwendet werden. Ansonsten könnten die Rückgaben von checkError() und getErrorDescription()
auf die zwischenzeitliche Tätigkeit eines anderen Threads zurückgehen. Mit dem Multithreading werden wir uns in
Kapitel 15 beschäftigen.
Abschnitt 3.4 Eingabe bei Konsolenprogrammen 123

Neben gint() besitzt die Klasse Simput noch analoge Methoden für andere Datentypen, u. a.:
• public static long glong()
Liest eine ganze Zahl vom Typ long von der Konsole
• public static double gdouble()
Liest eine Gleitkommazahl vom Typ double von der Konsole, wobei das erwartete Dezi-
maltrennzeichen vom eingestellten Gebietsschema des Benutzers abhängt. Bei der Einstel-
lung de_DE wird ein Dezimalkomma erwartet.
• public static char gchar()
Liest ein Zeichen von der Konsole

3.4.2 Eine globale Bibliothek mit der Klasse Simput in IntelliJ einrichten
Benutzt ein Programm die Klasse Simput, dann muss ...
• beim Übersetzen des Programms durch den OpenJDK-Compiler (javac.exe) entweder die
Quellcodedatei Simput.java im aktuellen Verzeichnis liegen, oder die Bytecode-Datei
Simput.class über den Klassenpfad auffindbar sein,
• bei der Ausführung des Programms durch die JVM (java.exe) die Bytecode-Datei Sim-
put.class über den Klassenpfad auffindbar sein.
Der Klassenpfad kann über die CLASSPATH-Umgebungsvariable oder durch die beim Compiler-
bzw. Interpreter-Aufruf verwendete classpath - Option definiert werden (vgl. Abschnitt 2.2.4).
Unsere Entwicklungsumgebung IntelliJ IDEA ignoriert die CLASSPATH-Umgebungsvariable,
bietet aber die äquivalente Möglichkeit zur Definition von Bibliotheken auf Modul-, Projekt- oder
IDE-Ebene.1 Beim Aufruf der Werkzeuge zum Übersetzen oder Starten von Java-Programmen
(z. B. javac.exe oder java.exe) erstellt IntelliJ jeweils eine -classpath - Option aus den Biblio-
theksdefinitionen.
Damit eine als Bytecode vorliegende Klasse bei der Übersetzung im Rahmen der Entwicklungsum-
gebung gefunden wird, sollte sie unbedingt in einer Java-Archivdatei vorliegen. Im Ordner
…\BspUeb\Simput\Standardpaket
(weitere Ortsangaben im Vorwort) finden Sie daher neben der Bytecode-Datei Simput.class auch
die Archivdatei Simput.jar. Wir werden uns im Kapitel 6 mit Java-Archivdateien beschäftigen.
Wir definieren nun die Datei Simput.jar als IDE-globale Bibliothek, die in beliebigen Projekten
genutzt werden kann. Nach
File > Project Structure > Global Libraries
klicken wir im folgenden Dialog

1
Mit der IDE-Ebene ist die Ebene der integrierten Entwicklungsumgebung (engl.: integrated development environ-
ment, Abkürzung: IDE) gemeint.
124 Kapitel 3 Elementare Sprachelemente

auf den Schalter , entscheiden uns für die Kategorie Java

und wählen anschließend die Datei Simput.jar:

Der Übernahme in das Projekt bzw. Modul Prog, das zum Üben von diversen elementaren Spra-
chelementen dient, kann zugestimmt werden:

Damit die nun definierte IDE-globale Bibliothek Simput


Abschnitt 3.4 Eingabe bei Konsolenprogrammen 125

in einem konkreten Projekt bzw. Modul benutzt werden kann, muss sie in die Liste der Abhängig-
keiten des Moduls aufgenommen werden. Für das aktuell geöffnete Projekt ist das eben schon ge-
schehen. Bei einem anderen Projekt öffnet man nach
File > Project Structure > Modules
im folgenden Fenster für das meist einzige vorhandene IntelliJ-Modul die Registerkarte Depend-
encies:

Nach einem Klick auf den Schalter über der Liste der Bibliotheken entscheidet man sich für die
Kategorie Library

und wählt dann die globale Bibliothek Simput,

die anschließend in der Liste der Abhängigkeiten erscheint:


126 Kapitel 3 Elementare Sprachelemente

Nun können die statischen Methoden der Klasse Simput im Projekt genutzt werden.

3.5 Operatoren und Ausdrücke


Im Zusammenhang mit der Variablendeklaration und der Wertzuweisung haben wir das Sprachele-
ment Ausdruck ohne Erklärung benutzt, und die soll nun nachgeliefert werden. Im aktuellen Ab-
schnitt 3.5 werden wir Ausdrücke als wichtige Bestandteile von Java-Anweisungen detailliert be-
trachten. Dabei lernen Sie elementare Datenverarbeitungsmöglichkeiten kennen, die von sogenann-
ten Operatoren mit ihren Argumenten realisiert werden, z. B. von den arithmetischen Operatoren
(+, -, *, /) für die Grundrechenarten. Im Verlauf des aktuellen Abschnitts werden Ihre Kenntnisse
über die Datenverarbeitung mit Java erheblich wachsen. Der dabei zu investierende Aufwand lohnt
sich, weil ein sicherer Umgang mit Operatoren und Ausdrücken eine unabdingbare Voraussetzung
für das erfolgreiche Implementieren von Methoden ist. Dort werden Algorithmen bzw. die Hand-
lungskompetenzen von Klassen bzw. Objekten realisiert.
Während die Variablen zur Speicherung von Werten dienen, geht es bei den Operatoren darum,
aus vorhandenen Variableninhalten und/oder anderen Argumenten neue Werte zu berechnen. Den
zur Berechnung eines Werts geeigneten, aus Operatoren und zugehörigen Argumenten aufgebauten
Teil einer Anweisung bezeichnet man als Ausdruck, z. B. in der folgenden Wertzuweisung:1
Operator

az = az - an;

Ausdruck
Durch diese Anweisung aus der kuerze() - Methode unserer Klasse Bruch (siehe Abschnitt 1.1)
wird der lokalen int-Variablen az der Wert des Ausdrucks az - an zugewiesen. Wie in diesem
Beispiel landen die Werte von Ausdrücken oft in Variablen, wobei Ausdruck und Variable typkom-
patibel sein müssen. Den Datentyp eines Ausdrucks bestimmen im Wesentlichen die Datentypen
der Argumente, manchmal beeinflusst aber auch der Operator den Typ des Ausdrucks (z.B. bei ei-
nem Vergleichsoperator).

1
Im Abschnitt 3.5.8 werden Sie eine Möglichkeit kennenlernen, diese Anweisung etwas kompakter zu formulieren.
Abschnitt 3.5 Operatoren und Ausdrücke 127

Schon bei einem Literal, einer Variablen oder einem Methodenaufruf haben wir es mit einem Aus-
druck zu tun.1
Beispiele:
• 1.5
Dieses Gleitkommaliteral ist ein Ausdruck mit dem Typ double und dem Wert 1,5.
• Simput.gint()
Dieser Methodenaufruf ist ein Ausdruck mit dem Typ int (= Rückgabetyp der Methode),
wobei die Eingabe des Benutzers über den Wert entscheidet (siehe Abschnitt 3.4.1 zur Be-
schreibung der Klassenmethode Simput.gint(), die nicht zum Java-API gehört).
Mit Hilfe diverser Operatoren entsteht ein komplexerer Ausdruck, dessen Typ und Wert von den
Argumenten und den Operatoren abhängen.
Beispiele:
• 2 * 1.5
Hier resultiert der double-Wert 3,0.
• 2 * 3
Hier resultiert der int-Wert 6.
• 2 > 1.5
Hier resultiert der boolean-Wert true.
In der Regel beschränken sich die Operatoren darauf, aus ihren Argumenten (Operanden) einen
Wert zu ermitteln und für die weitere Verarbeitung zur Verfügung zu stellen. Einige Operatoren
haben jedoch zusätzlich einen Nebeneffekt auf eine als Argument fungierende Variable, z. B.:
int i = 12;
int j = i++;
In der zweiten Anweisung des Beispiels tritt der Postinkrementoperator ++ mit der int-Variablen
i als Argument auf. Der Ausdruck i++ hat den Typ int und den Wert 12, welcher in der Zielvariab-
len j landet. Außerdem wird die Argumentvariable i beim Auswerten des Ausdrucks durch den
Postinkrementoperator auf den neuen Wert 13 gebracht.
Die meisten Operatoren verarbeiten zwei Operanden (Argumente) und heißen daher zweistellig
oder binär. Im folgenden Beispiel ist der Additionsoperator zu sehen, der zwei numerische Ar-
gumente erwartet:
a + b
Manche Operatoren begnügen sich mit einem Argument und heißen daher einstellig oder unär. Als
Beispiel haben wir eben schon den Postinkrementoperator kennengelernt. Ein weiteres ist der Nega-
tionsoperator, der durch ein Ausrufezeichen dargestellt wird, ein Argument vom Typ boolean er-
wartet und dessen Wahrheitswert umdreht (true und false vertauscht), z. B.:
!cond
Wir werden auch noch einen dreistelligen (ternären) Operator kennenlernen.
Weil Ausdrücke von passendem Ergebnistyp als Argumente einer Operation erlaubt sind, können
beliebig komplexe Ausdrücke aufgebaut werden. Unübersichtliche Exemplare sollten jedoch als
potentielle Fehlerquellen vermieden werden.

1
Besteht ein Ausdruck aus einem Methodenaufruf mit dem Pseudorückgabetyp void, dann liegt allerdings kein Wert
vor.
128 Kapitel 3 Elementare Sprachelemente

3.5.1 Arithmetische Operatoren


Die arithmetischen Operatoren sind für die Grundrechenarten zuständig, und ihre Operanden (Ar-
gumente) müssen einen primitiven Ganzzahl- oder Gleitkommatyp haben (byte, short, int, long,
char, float oder double). Die resultieren Ausdrücke haben wiederum einen numerischen Ergebnis-
typ und werden oft als arithmetische Ausdrücke bezeichnet.
Es hängt von den Datentypen der Operanden ab, ob bei den Berechnungen die Ganzzahl- oder die
Gleitkommaarithmetik zum Einsatz kommt. Besonders auffällig sind die Unterschiede im Verhal-
ten des Divisionsoperators (dargestellt durch einen Schrägstrich), z. B.:
Quellcode Ausgabe
class Prog { 0
public static void main(String[] args) { 0,66667
int i = 2, j = 3;
double a = 2.0;
System.out.printf("%10d\n", i / j);
System.out.printf("%10.5f", a / j);
}
}

Bei der Ganzzahldivision werden die Nachkommastellen abgeschnitten, was gelegentlich durchaus
erwünscht ist. Im Zusammenhang mit dem Über- bzw. Unterlauf (siehe Abschnitt 3.6) werden Sie
noch weitere Unterschiede zwischen der Ganzzahl- und der Gleitkommaarithmetik kennenlernen.
Trifft ein arithmetischer Operator auf Argumente mit unterschiedlichen Datentypen, dann findet vor
der Berechnung automatisch eine erweiternde Typanpassung statt, bei der z. B. ein ganzzahliges
Argument in einen Gleitkommatyp gewandelt wird (siehe Abschnitt 3.5.7.1). Im obigen Beispiel-
programm trifft der Divisionsoperator im Ausdruck
a / j
auf ein double- und ein int-Argument. In dieser Situation wird der int-Wert in den „größeren“ Typ
double gewandet, bevor schließlich die Gleitkommaarithmetik zum Einsatz kommt.
Wie der vom Compiler gewählte Arithmetiktyp und der Ergebnisdatentyp von den Datentypen der
Argumente abhängen, ist der folgenden Tabelle zu entnehmen:
Verwendete Datentyp des
Datentypen der Operanden
Arithmetik Ergebniswertes
Beide Operanden haben den Typ byte, short,
int
char oder int (nicht unbedingt denselben).
Ganzzahlarithmetik
Beide Operanden haben einen integralen Typ,
long
und mind. ein Operand hat den Datentyp long.
Mindestens ein Operand hat den Typ float, kei-
float
ner hat den Typ double. Gleitkomma-
Mindestens ein Operand hat den Datentyp arithmetik
double
double.
In der nächsten Tabelle werden alle arithmetischen Operatoren beschrieben, wobei die Platzhalter
Num, Num1 und Num2 für Ausdrücke mit einem numerischen Typ stehen, und Var für eine numeri-
sche Variable:
Abschnitt 3.5 Operatoren und Ausdrücke 129

Beispiel
Operator Bedeutung
Programmfragment Ausgabe
-Num Vorzeichenumkehr int i = 2, j = -3;
System.out.printf("%d %d",-i,-j); -2 3
Num1 + Num2 Addition System.out.println(2 + 3); 5
Num1 – Num2 Subtraktion System.out.println(2.6 - 1.1); 1.5
Num1 * Num2 Multiplikation System.out.println(4 * 5); 20
Num1 / Num2 Division System.out.println(8.0 / 5); 1.6
System.out.println(8 / 5); 1
Num1 % Num2 Modulo (Divisionsrest) System.out.println(19 % 5); 4
Sei GAD der ganzzahlige An- System.out.println(-19 % 5.25); -3.25
teil aus dem Ergebnis der Di-
vision (Num1 / Num2). Dann
ist Num1 % Num2 def. durch
Num1 - GAD  Num2
++Var Präinkrement bzw. int i = 4;
--Var -dekrement double a = 0.2;
System.out.println(++i + "\n" + 5
Als Argumente sind hier nur --a); -0.8
Variablen erlaubt.
++Var erhöht Var um 1 und
liefert Var + 1
--Var reduz. Var um 1 und
liefert Var - 1
Var++ Postinkrement bzw. int i = 4;
Var-- -dekrement System.out.println(i++ + "\n" + 4
i); 5
Als Argumente sind hier nur
Variablen erlaubt.
Var++ liefert Var und
erhöht Var um 1
Var-- liefert Var und
reduziert Var um 1
Bei den Inkrement- bzw. Dekrementoperatoren ist zu beachten, dass sie zwei Effekte haben:
• Das Argument wird ausgelesen, um den Wert des Ausdrucks zu ermitteln.
• Die als Argument fungierende numerische Variable wird vor oder nach dem Auslesen ver-
ändert. Wegen dieses Nebeneffekts sind Inkrement- bzw. Dekrementausdrücke im Unter-
schied zu den sonstigen arithmetischen Ausdrücken bereits vollständige Anweisungen (vgl.
Abschnitt 3.7.1), wenn man ein Semikolon dahinter setzt, z. B.:
Quellcode Ausgabe
class Prog { 13
public static void main(String[] args) {
int i = 12;
i++;
System.out.println(i);
}
}

Ein In- bzw. Dekrementoperator erhöht bzw. vermindert durch seinen Nebeneffekt den Wert einer
Variablen um 1 und bietet für diese oft benötigte Operation eine vereinfachte Schreibweise. So ist
z. B. die folgende Anweisung
j = ++i;
130 Kapitel 3 Elementare Sprachelemente

mit den beiden int-Variablen i und j äquivalent zu:


i = i + 1;
j = i;
Für den eventuell bei manchen Lesern noch wenig bekannten Modulo-Operator gibt es einige sinn-
volle Anwendungen, z. B.:
• Man kann für eine ganze Zahl bequem feststellen, ob sie gerade (durch 2 teilbar) ist. Dazu
prüft man, ob der Rest aus der Division durch 2 gleich 0 ist:
Quellcode-Fragment Ausgabe
int i = 19;
System.out.println(i % 2 == 0); false

• Man kann bei einer Gleitkommazahl den gebrochenen Anteil ermitteln bzw. abspalten:1
Quellcode-Fragment Ausgabe
double a = 7.124824;
double rest = a % 1.0;
double ganz = a - rest;
System.out.printf("%f = %1.0f + %f", a, ganz, rest); 7,124824 = 7 + 0,124824

Der Modulo-Operator wird meist auf zwei ganzzahlige Argumente angewendet, sodass nach der
Tabelle auf Seite 128 auch das Ergebnis einen ganzzahligen Typ besitzt. Wie der zweite Punkt in
der letzten Aufzählung zeigt, kann die Modulo-Operation aber auch auf Gleitkommaargumente an-
gewendet werden, wobei ein Ergebnis mit Gleitkommatyp resultiert.

3.5.2 Methodenaufrufe
Obwohl Ihnen eine gründliche Behandlung der Methoden noch bevorsteht, haben Sie doch schon
einige Erfahrungen mit diesen Handlungskompetenzen von Klassen bzw. Objekten gesammelt:
• Die Arbeitsweise einer Methode kann von Argumenten (Parametern) abhängen.
• Viele Methoden liefern ein Ergebnis an den Aufrufer. Die im Abschnitt 3.4.1 vorgestellte
Methode Simput.gint() liefert z. B. einen int-Wert. Bei der Methodendefinition ist der
Datentyp der Rückgabe anzugeben (siehe Syntaxdiagramm im Abschnitt 3.1.3.2). Liefert ei-
ne Methode dem Aufrufer kein Ergebnis, dann ist in der Definition der Pseudo-Rückgabetyp
void anzugeben.
• Neben der Wertrückgabe hat ein Methodenaufruf oft weitere Effekte, z. B. auf die Merk-
malsausprägungen des handelnden Objekts oder auf die Konsolenausgabe.
In syntaktischer Hinsicht ist festzuhalten, dass ein Methodenaufruf einen Ausdruck darstellt, wobei
seine Rückgabe den Datentyp und den Wert des Ausdrucks bestimmt.
Bei passendem Rückgabetyp darf ein Methodenaufruf auch als Argument für komplexere Ausdrü-
cke oder für Methodenaufrufe verwendet werden (siehe Abschnitt 4.3.1.2). Bei einer Methode ohne
Rückgabewert resultiert ein Ausdruck vom Typ void, der nicht als Argument für Operatoren oder
andere Methoden taugt.
Ein Methodenaufruf mit angehängtem Semikolon stellt eine Anweisung dar (vgl. Abschnitt 3.7),
was Sie z. B. bei den zahlreichen Einsätzen der statischen Methode println() in unseren Beispiel-
programmen beobachten konnten.

1
Der ganzzahlige Anteil eines double-Werts lässt sich auch über die statische Methode floor() aus der Klasse Math
ermitteln. Für eine double-Variable d mit einem nicht-negativen Wert ist d-Math.floor(d) identisch mit d%1.0.
Abschnitt 3.5 Operatoren und Ausdrücke 131

Mit den im Abschnitt 3.5.1 beschriebenen arithmetischen Operatoren lassen sich nur elementare
mathematische Probleme lösen. Darüber hinaus stellt Java eine große Zahl mathematischer Stan-
dardfunktionen (z. B. Potenzfunktion, Logarithmus, Wurzel, trigonometrische Funktionen) über
statische Methoden der Klasse Math im API-Paket java.lang (ab Java 9 im Modul java.base) zur
Verfügung.1 Im folgenden Programm wird die Methode pow() zur Berechnung der allgemeinen
Potenzfunktion ( b e ) genutzt:
Quellcode Ausgabe
class Prog { 8.0
public static void main(String[] args) {
System.out.println(Math.pow(2, 3));
}
}

Im Beispielprogramm liefert die Methode pow() einen Rückgabewert vom Typ double, der gleich
als Argument der Methode println() Verwendung findet. Solche Verschachtelungen sind bei Pro-
grammierern wegen ihrer Kompaktheit ähnlich beliebt wie die Inkrement- bzw. Dekrementoperato-
ren. Ein etwas umständliches, aber für Einsteiger leichter nachvollziehbares Äquivalent zum obigen
println() - Aufruf könnte z. B. so aussehen:
double d;
d = Math.pow(2.0, 3.0);
System.out.println(d);

3.5.3 Vergleichsoperatoren
Durch die Anwendung eines Vergleichsoperators auf zwei komparable (miteinander vergleichbare)
Ausdrücke entsteht ein Vergleich. Dies ist ein einfacher logischer Ausdruck (vgl. Abschnitt 3.5.5).
Folglich kann ein Vergleich die booleschen Werte true (wahr) und false (falsch) annehmen und zur
Formulierung einer Bedingung verwendet werden. Das folgende Beispiel dürfte verständlich sein,
obwohl die if-Anweisung noch nicht behandelt wurde:
if (arg > 0)
System.out.println(Math.log(arg));

1
Mit den Paketen und Modulen der Standardbibliothek werden wir uns später ausführlich beschäftigen. An dieser
Stelle dient die Angabe der Paket- und Modulzugehörigkeit dazu, eine Klasse eindeutig zu identifizieren und die
Standardbibliothek allmählich kennenzulernen. Das Paket java.lang wird im Unterschied zu allen anderen API-
Paketen automatisch in jede Quellcodedatei importiert.
132 Kapitel 3 Elementare Sprachelemente

In der folgenden Tabelle mit den von Java unterstützten Vergleichsoperatoren stehen
• Expr1 und Expr2 für komparable Ausdrücke
• Num1 und Num2 für numerische Ausdrücke (mit dem Datentyp byte, short, int, long, char,
float oder double)
Beispiel
Operator Bedeutung
Programmfragment Ausgabe
String s = "2.4";
Expr1 = = Expr2 gleich true
System.out.println(s == "2.4");
Expr1 != Expr2 ungleich System.out.println(2 != 3); true
Num1 > Num2 größer System.out.println(3 > 2); true
Num1 < Num2 kleiner System.out.println(3 < 2); false
Num1 >= Num2 größer oder gleich System.out.println(3 >= 3); true
Num1 <= Num2 kleiner oder gleich System.out.println(3 <= 2); false

Achten Sie unbedingt darauf, dass der Identitätsoperator durch zwei „=“ - Zeichen ausgedrückt
wird. Ein nicht ganz seltener Java-Programmierfehler besteht darin, beim Identitätsoperator nur ein
Gleichheitszeichen zu schreiben. Dabei muss nicht unbedingt ein harmloser Syntaxfehler entstehen,
der nach dem Studium einer Compiler-Fehlermeldung leicht zu beseitigen ist, sondern es kann auch
ein unangenehmer Logikfehler resultieren, also ein irreguläres Verhalten des Programms (vgl. Ab-
schnitt 2.2.5 zur Unterscheidung von Syntax- und Logikfehlern). Im ersten println() - Aufruf des
folgenden Beispielprogramms wird das Ergebnis eines Vergleichs auf die Konsole geschrieben:1
Quellcode Ausgabe
class Prog { false
public static void main(String[] args) { 1
int i = 1;
System.out.println(i == 2);
System.out.println(i);
}
}

Nach dem Entfernen eines Gleichheitszeichens wird aus dem logischen Ausdruck ein Wertzuwei-
sungsausdruck (siehe Abschnitt 3.5.8) mit dem Datentyp int und dem Wert 2:
Quellcode Ausgabe
class Prog { 2
public static void main(String[] args) { 2
int i = 1;
System.out.println(i = 2);
System.out.println(i);
}
}

Die versehentlich entstandene Zuweisung sorgt nicht nur für eine unerwartete Ausgabe, sondern
verändert auch den Wert der Variablen i, was im weiteren Verlauf eines Programms unangenehme
Folgen haben kann.

1
Wir wissen schon aus dem Abschnitt 3.2, dass println() einen beliebigen Ausdruck verarbeiten kann, wobei auto-
matisch eine Zeichenfolgen-Repräsentation erstellt wird.
Abschnitt 3.5 Operatoren und Ausdrücke 133

3.5.4 Identitätsprüfung bei Gleitkommawerten


Bei den binären Gleitkommatypen (float und double) sind simple Identitätstests wegen technisch
bedingter Abweichungen von der reinen Mathematik unbedingt zu unterlassen, z. B.:
Quellcode Ausgabe
class Prog { false
public static void main(String[] args) { true
final double epsilon = 1.0e-14;
double d1 = 10.0 - 9.9;
double d2 = 0.1;
System.out.println(d1 == d2);
System.out.println(Math.abs((d1 - d2)/d1) < epsilon);
}
}

Der Vergleich
10.0 - 9.9 == 0.1
führt trotz des Datentyps double (mit mindestens 15 signifikanten Dezimalstellen) zum Ergebnis
false. Wenn man die im Abschnitt 3.3.7.1 beschriebenen Genauigkeitsprobleme bei der Speiche-
rung von binären Gleitkommazahlen berücksichtigt, ist das Vergleichsergebnis durchaus nicht über-
raschend. Im Kern besteht das Problem darin, dass mit der binären Gleitkommatechnik auch relativ
„glatte“ rationale Zahlen (wie z. B. 0,1) nicht exakt gespeichert werden können. Im zwischenge-
speicherten Berechnungsergebnis 10,0 - 9,9 steckt ein anderer Fehler als im Speicherabbild der Zahl
0,1. Weil die Vergleichspartner nicht Bit für Bit identisch sind, meldet der Identitätsoperator das
Ergebnis false.
Bei der Ausgabe eines Gleitkommawerts, also bei der Wandlung des Werts in eine Zeichenfolge,
verwendet Java eine Glättungstechnik an, die eine Beurteilung der Verhältnisse im Hauptspeicher
erschwert. Obwohl z. B. die Zahl 0,1 im Hauptspeicher nicht exakt gespeichert werden kann, liefern
die folgenden Anweisungen
double tenth = 0.1;
System.out.println(tenth);
die Ausgabe
0.1
Daran ist wesentlich die Methode toString() der Klasse Double beteiligt, die zum double-Wert d
nach den folgenden Regeln eine Zeichenfolge produziert:1
• Es wird mindestens eine Nachkommastelle produziert.
• Es werden nur so viele weitere Nachkommastellen produziert, bis sich die Zeichenfolge zu d
von den Zeichenfolgen zum nächstkleineren bzw. nächstgrößeren möglichen double-Wert
unterscheidet.
Mit Hilfe der im Abschnitt 3.3.7 vorgestellten und insbesondere für Anwendungen im Bereich der
Finanzmathematik empfohlenen Klasse BigDecimal gewinnt man einen korrekten Eindruck von
den Verhältnissen im Hauptspeicher. Das folgende Programm

1
https://docs.oracle.com/en/java/javase/17/docs/api/java.base/java/lang/Double.html#toString(double)
134 Kapitel 3 Elementare Sprachelemente

import java.math.BigDecimal;
class Prog {
public static void main(String[] args) {
double tenth = 0.1;
BigDecimal tenthBD = new BigDecimal(tenth);
System.out.println(tenth);
System.out.println(tenthBD);
}
}
liefert die Ausgabe:
0.1
0.1000000000000000055511151231257827021181583404541015625

Verwendet man String-Parameter im Konstruktor der Klasse BigDecimal, dann entfallen die für
binäre Gleitkommawerte beschriebenen Probleme bei der Speichergenauigkeit und bei Identitäts-
vergleichen, z. B.:
Quellcode Ausgabe
import java.math.*; true
class Prog {
public static void main(String[] args) {
BigDecimal bd1 = new BigDecimal("10.0");
BigDecimal bd2 = new BigDecimal("9.9");
BigDecimal bd3 = new BigDecimal("0.1");
System.out.println(bd3.equals(bd1.subtract(bd2)));
}
}

Allerdings ist ein erhöhter Speicher- und Zeitaufwand in Kauf zu nehmen.


Den etwas anstrengenden Rest des Abschnitts kann überspringen, wer aktuell keinen Algorithmus
mit auf Identität zu prüfenden double-Werten zu implementieren hat.
Um eine praxistaugliche Identitätsbeurteilung von double-Werten zu erhalten, sollte eine an der
Rechen- bzw. Speichergenauigkeit orientierte Unterschiedlichkeitsschwelle verwendet werden.
Nach diesem Vorschlag werden zwei normalisierte (also insbesondere von null verschiedene)
double-Werte d1 und d2 (vgl. Abschnitt 3.3.7.1) dann als numerisch identisch betrachtet, wenn der
relative Abweichungsbetrag kleiner als 1,010-14 ist:
d1 − d 2
 1,0  10 −14
d1

Die Vergabe der d1-Rolle, also die Wahl des Nenners, ist beliebig. Um das Verfahren vollständig
festzulegen, wird die Verwendung der betragsmäßig größeren Zahl vorgeschlagen.
Ein Vorschlag zur Definition der numerischen Identität von zwei double-Werten muss die relative
Differenz zugrunde legen, weil die technisch bedingten Mantissen-Fehler bei zwei double-
Variablen mit eigentlich identischem Wert in Abhängigkeit vom Exponenten zu sehr unterschiedli-
chen Gesamtfehlern führen können. Vom gelegentlich anzutreffenden Vorschlag, die betragsmäßige
Differenz
d1 − d2
mit einer Schwelle zu vergleichen, ist daher abzuraten. Dieses Verfahren ist (bei geeignet gewählter
Schwelle) nur tauglich für Zahlen in einem engen Größenbereich. Bei einer Änderung der Größen-
ordnung muss die Schwelle angepasst werden.
Abschnitt 3.5 Operatoren und Ausdrücke 135

d1 − d 2
Zu einer Schwelle für die relative Abweichung gelangt man durch Betrachtung von zwei
d1
normalisierten double-Variablen d1 und d2, die bis auf ihre durch begrenzte Speicher- und Rechen-
genauigkeit bedingten Mantissenfehler e1 bzw. e2 denselben Wert (1 + m) 2k enthalten:
d1 = (1 + m + e1) 2k und d2 = (1 + m + e2) 2k
Bei einem normalisierten double-Wert (mit 52 Mantissen-Bits) kann aufgrund der begrenzten Spei-
chergenauigkeit als maximaler absoluter Mantissenfehler  der halbe Abstand zwischen zwei be-
nachbarten Mantissenwerten auftreten:
 = 2 −53  1,1 10 -16
Für den Betrag der technisch bedingten relativen Abweichung von zwei eigentlich identischen nor-
malisierten Werten (mit einer Mantisse im Intervall [1, 2)) gilt die Abschätzung:
d1 − d 2 e −e e + e2 2
= 1 2  1   2   ( wegen (1 + m + e1 )  [1, 2))
d1 1 + m + e1 1 + m + e1 1 + m + e1

Die oben vorgeschlagene Schwelle 1,010-14 berücksichtigt über den Speicherfehler hinaus auch
noch eingeflossene Rechnungsungenauigkeiten. Mit welcher Fehlerkumulation bzw. -verstärkung
zu rechnen ist, hängt vom konkreten Algorithmus ab, sodass die Unterschiedlichkeitsschwelle even-
tuell angehoben werden muss. Immerhin hängt sie (anders als bei einem Kriterium auf Basis des
Betrags der einfachen Differenz d1 − d 2 ) nicht von der Größenordnung der Zahlen ab.

An der vorgeschlagenen Identitätsbeurteilung mit Hilfe einer Schwelle für den relativen Abwei-
chungsbetrag ist u. a. zu bemängeln, dass eine Verallgemeinerung für die mit einer geringeren rela-
tiven Genauigkeit gespeicherten denormalisierten Werte (Betrag kleiner als 2-1022 beim Typ double,
siehe Abschnitt 3.3.7.1) benötigt wird.
Dass die definierte numerische Identität nicht transitiv ist, muss hingenommen werden. Für drei
double-Werte a, b und c kann also das folgende Ergebnismuster auftreten:
• a numerisch identisch mit b
• b numerisch identisch mit c
• a nicht numerisch identisch mit c
Für den Vergleich einer double-Zahl a mit dem Wert 0.0 ist eine Schwelle für die absolute Abwei-
chung (statt der relativen) sinnvoll, z. B.:
a  1,0 10-14

Die besprochenen Genauigkeitsprobleme sind auch bei den gerichteten Vergleichen (<, <=, >, >=)
relevant.
Bei vielen naturwissenschaftlichen oder technischen Problemen ist es generell wenig sinnvoll, zwei
Größen auf exakte Übereinstimmung zu testen, weil z. B. schon aufgrund von Messungenauigkeiten
eine Abweichung von der theoretischen Identität zu erwarten ist. Bei Verwendung einer anwen-
dungslogisch gebotenen Unterschiedsschwelle dürften die technischen Beschränkungen der Gleit-
kommatypen keine große Rolle mehr spielen. Präzisere Aussagen zur Computer-Arithmetik finden
sich z. B. bei Strey (2005).
136 Kapitel 3 Elementare Sprachelemente

3.5.5 Logische Operatoren


Aus dem Abschnitt 3.5.3 wissen wir, dass jeder Vergleich (z. B. arg > 0) bereits ein logischer
Ausdruck ist, also die Werte true und false annehmen kann. Durch Anwendung von logischen Ope-
ratoren (Negation, UND, (exklusives) ODER) auf bereits vorhandene logische Ausdrücke kann man
neue, komplexere logische Ausdrücke erstellen. Die Wirkungsweise der in Java unterstützten logi-
schen Operatoren wird anschließend in Wahrheitstafeln beschrieben, wobei die Platzhalter LA,
LA1 und LA2 für logische Ausdrücke stehen.
Um einen logischen Ausdruck LA zu negieren, also die Wahrheitswerte true und false zu vertau-
schen), wendet man den unären logischen Operator ! auf LA an:
Argument Negation
LA !LA
true false
false true

Mit den anschließend beschriebenen binären logischen Operatoren erstellt man aus zwei Argu-
mentausdrücken einen Ergebnisausdruck:
Argument 1 Argument 2 Logisches UND Logisches ODER Exklusives ODER
LA1 LA2 LA1 && LA2 LA1 || LA2 LA1 ^ LA2
LA1 & LA2 LA1 | LA2
true true true true false
true false false true true
false true false true true
false false false false false

Es folgt eine Tabelle mit Erläuterungen und Beispielen zu den logischen Operatoren:
Beispiel
Operator Bedeutung
Programmfragment Ausgabe
!LA Negation boolean erg = true;
Der Wahrheitswert wird durch sein System.out.println(!erg); false
Gegenteil ersetzt.
LA1 && LA2 Logisches UND mit bedingter int i = 3;
Auswertung boolean erg = false && i++ > 3;
System.out.println(erg + "\n"+i); false
LA1 && LA2 ist genau dann wahr, 3
wenn beide Argumente wahr sind.
Ist LA1 falsch, wird LA2 nicht erg = true && i++ > 3;
ausgewertet. System.out.println(erg + "\n"+i); false
4
LA1 & LA2 Logisches UND mit unbedingter int i = 3;
Auswertung boolean erg = false & i++ > 3;
System.out.println(erg + "\n"+i); false
LA1 & LA2 ist genau dann wahr, 4
wenn beide Argumente wahr sind.
Es werden auf jeden Fall beide
Ausdrücke ausgewertet.
Abschnitt 3.5 Operatoren und Ausdrücke 137

Beispiel
Operator Bedeutung
Programmfragment Ausgabe
LA1 || LA2 Logisches ODER mit bedingter int i = 3;
Auswertung boolean erg = true || i++ == 3;
System.out.println(erg + "\n"+i); true
LA1 || LA2 ist genau dann wahr, 3
wenn mindestens ein Argument
wahr ist. Ist LA1 wahr, wird LA2 erg = false || i++ == 3;
nicht ausgewertet. System.out.println(erg + "\n"+i); true
4
LA1 | LA2 Logisches ODER mit unbeding- int i = 3;
ter Auswertung boolean erg = true | i++ == 3;
System.out.println(erg + "\n"+i); true
LA1 | LA2 ist genau dann wahr, 4
wenn mindestens ein Argument
wahr ist. Es werden auf jeden Fall
beide Ausdrücke ausgewertet.
LA1 ^ LA2 Exklusives logisches ODER boolean erg = true ^ true;
LA1 ^ LA2 ist genau dann wahr, System.out.println(erg); false
wenn genau ein Argument wahr
ist, wenn also die Argumente ver-
schiedene Wahrheitswerte haben.

Der Unterschied zwischen den beiden logischen UND-Operatoren && und & bzw. zwischen den
beiden logischen ODER-Operatoren || und | ist für Einsteiger vielleicht wenig beeindruckend, weil
man spontan den nicht ausgewerteten logischen Ausdrücken keine Bedeutung beimisst. Allerdings
ist es in Java nicht unüblich, „Nebeneffekte“ in einen logischen Ausdruck einzubauen, z. B.
bv & i++ > 3
Hier erhöht der Postinkrementoperator beim Auswerten des rechten &-Arguments den Wert der
Variablen i. Eine solche Auswertung wird jedoch in der folgenden Variante des Beispiels (mit
&&-Operator) unterlassen, wenn bereits nach Auswertung des linken &&-Arguments das Gesamt-
ergebnis false feststeht:
bv && i++ > 3
Das vom Programmierer nicht erwartete Ausbleiben einer Auswertung (z. B. bei i++) kann erhebli-
che Auswirkungen auf die Programmausführung haben.
Dank der beim Operator && realisierten bedingten Auswertung kann man sich im rechten Operan-
den darauf verlassen, dass der linke Operand den Wert true besitzt, was im folgenden Beispiel aus-
genutzt wird. Dort prüft der linke Operand die Existenz und der rechte Operand die Länge einer
Zeichenfolge:
if(str != null && str.length() < 10) {...}
Wenn die Referenzvariable str vom Typ der Klasse String keine Objektadresse enthält, darf der
rechte Ausdruck nicht ausgewertet werden, weil eine Längenanfrage an ein nicht existentes Objekt
zu einem Laufzeitfehler führen würde.
Mit der Entscheidung, grundsätzlich die unbedingte Operatorvariante zu verwenden, verzichtet man
auf die eben beschriebene Option, im rechten Ausdruck den Wert true des linken Ausdrucks vo-
raussetzen zu können, und man nimmt (mehr oder weniger relevante) Leistungseinbußen durch
überflüssige Auswertungen des rechten Ausdrucks in Kauf. Eher empfehlenswert ist der Verzicht
auf Nebeneffekt-Konstruktionen im Zusammenhang mit bedingt arbeitenden Operatoren.
Wie der Tabelle auf Seite 149 zu entnehmen ist, unterscheiden sich die beiden UND-Operatoren
&& und & bzw. die beiden ODER-Operatoren || und | auch hinsichtlich der Bindungskraft auf Ope-
randen (Auswertungspriorität).
138 Kapitel 3 Elementare Sprachelemente

Die bedingte Auswertung wird gelegentlich als Kurzschlussauswertung bezeichnet (engl.: short-
circuiting).
Um die Verwirrung noch ein wenig zu steigern, werden die Zeichen & und | auch für bitorientierte
Operatoren verwendet (siehe Abschnitt 3.5.6). Diese Operatoren erwarten zwei integrale Argumen-
te (z. B. mit dem Datentyp int), während die logischen Operatoren den Datentyp boolean voraus-
setzen. Folglich kann der Compiler erkennen, ob ein logischer oder ein bitorientierter Operator ge-
meint ist.

3.5.6 Bitorientierte Operatoren


Über unseren momentanen Bedarf hinausgehend bietet Java einige Operatoren zur bitweisen Analy-
se und Manipulation von Variableninhalten. Statt einer systematischen Darstellung der verschiede-
nen Operatoren (siehe z. B. den Trail Learning the Java Language in den Java Tutorials, Oracle
2021a) beschränken wir uns auf ein Beispielprogramm, das zudem nützliche Einblicke in die Spei-
cherung von char-Werten im Arbeitsspeicher eines Computers erlaubt. Allerdings sind Beispiel und
zugehörige Erläuterungen mit einigen technischen Details belastet. Wenn Ihnen der Sinn momentan
nicht danach steht, können Sie den aktuellen Abschnitt ohne Sorge um den weiteren Kurserfolg an
dieser Stelle verlassen.
Das folgende Programm CharBits liefert die Unicode-Codierung zu einem vom Benutzer erfrag-
ten Zeichen Bit für Bit. Dabei kommt die statische Methode gchar() aus der im Abschnitt 3.4
beschriebenen Klasse Simput zum Einsatz, die das erste Element einer vom Benutzer eingetippten
und mit Enter quittierten Zeichenfolge abliefert. Außerdem kommt mit der for-Schleife eine Wie-
derholungsanweisung zum Einsatz, die erst im Abschnitt 3.7.3.1 offiziell vorgestellt wird. Im Bei-
spiel startet die Indexvariable i mit dem Wert 15, der am Ende jedes Schleifendurchgangs um 1
dekrementiert wird (i--). Ob es zum nächsten Schleifendurchgang kommt, hängt von der Fortset-
zungsbedingung ab (i >= 0):
Quellcode Eingabe (grün, kursiv) u. Ausgabe
class CharBits { Zeichen: x
public static void main(String[] args) { Unicode: 0000000001111000
char cbit; int-Wert: 120
System.out.print("Zeichen: ");
cbit = Simput.gchar();
System.out.print("Unicode: ");
for(int i = 15; i >= 0; i--) {
if ((1 << i & cbit) != 0)
System.out.print("1");
else
System.out.print("0");
}
System.out.println("\nint-Wert: " + (int)cbit);
}
}

Der Links-Shift-Operator << im Ausdruck


1 << i
verschiebt die Bits in der binären Repräsentation der Ganzzahl 1 um i Stellen nach links, wobei am
linken Rand i Stellen verworfen werden, und auf der rechten Seite i Nullen nachrücken. Von den
32 Bits, die ein int-Wert insgesamt belegt (siehe Abschnitt 3.3.6), interessieren im Augenblick nur
die rechten 16. Bei der 1 erhalten wir:
0000000000000001
Im 10. Schleifendurchgang (i = 6) geht dieses Muster z. B. über in:
Abschnitt 3.5 Operatoren und Ausdrücke 139

0000000001000000
Nach dem Links-Shift-Operator kommt der bitweise UND-Operator zum Einsatz:
1 << i & cbit
Das Operatorzeichen & wird in Java leider in doppelter Bedeutung verwendet: Wenn beide Argu-
mente vom Typ boolean sind, wird & als logischer Operator interpretiert (siehe Abschnitt 3.5.5).
Sind jedoch wie im vorliegenden Fall beide Argumente von integralem Typ, was auch für den Typ
char zutrifft, dann wird & als UND-Operator für Bits aufgefasst. Er erzeugt dann ein Bitmuster, das
an der Stelle k genau dann eine 1 enthält, wenn beide Argumentmuster an dieser Stelle eine 1 besit-
zen und anderenfalls eine 0. Hat in einem Programmablauf die char-Variable cbit z. B. den Wert
'x' erhalten (dezimale Unicode-Zeichensatznummer 120), dann ist dieses Bitmuster
0000000001111000
im Spiel, und 1 << i & cbit liefert z. B. bei i = 6 das Muster:
0000000001000000
Der von 1 << i & cbit erzeugte Wert hat den Typ int und kann daher mit dem int-Literal 0
verglichen werden:1
(1 << i & cbit) != 0
Dieser logische Ausdruck wird bei einem Schleifendurchgang genau dann wahr, wenn das zum ak-
tuellen i-Wert gehörende Bit in der Binärdarstellung des untersuchten Zeichens den Wert 1 besitzt.

3.5.7 Typumwandlung (Casting) bei primitiven Datentypen


Wie Sie aus dem Abschnitt 3.3.1 wissen, ist in Java der Datentyp einer Variablen unveränderlich,
und dieses Prinzip wird im aktuellen Abschnitt keineswegs aufgeweicht. Es gibt aber gelegentlich
einen Grund dafür, z. B. den Inhalt einer int-Variablen in eine double-Variable zu übertragen. Auf-
grund der abweichenden Speichertechniken ist dann eine Typanpassung fällig. Das geschieht
manchmal automatisch durch eine Initiative des Compilers, kann aber auch vom Programmierer
explizit angefordert werden.

3.5.7.1 Automatische erweiternde Typanpassung


Beim der Auswertung des Ausdrucks
2.0 / 7
trifft der Divisionsoperator auf ein double- und ein int-Argument, sodass nach der Tabelle im Ab-
schnitt 3.5.1 (Seite 128) die Gleitkommaarithmetik zum Einsatz kommt. Dazu wird für das int-
Argument eine automatische (implizite) Wandlung in den Datentyp double vorgenommen.
Java nimmt bei Bedarf für primitive Datentypen die folgenden erweiternden Typanpassungen
automatisch vor:
byte short int long float double
(8 Bit) (16 Bit) (32 Bit) (64 Bit) (32 Bit) (64 Bit)

char
(16 Bit)

1
Die runden Klammern sind erforderlich, um die korrekte Auswertungsreihenfolge zu erreichen (siehe Abschnitt
3.5.10).
140 Kapitel 3 Elementare Sprachelemente

Weil eine char-Variable die Unicode-Nummer eines Zeichens speichert, macht die Konvertierung
in numerische Typen kein Problem, z. B.:
Quellcode Ausgabe
class Prog { x/2 = 60
public static void main(String[] args) {
System.out.printf("x/2 = %5d", 'x'/2);
}
}

Noch eine Randnotiz zur impliziten Typanpassung bei numerischen Literalen: Während sich Java-
Compiler weigern, ein double-Literal in einer float-Variablen zu speichern, erlauben sie z. B. das
Speichern eines int-Literals in einer Variablen vom Typ byte (Ganzzahltyp mit 8 Bits), sofern der
Wertebereich dieses Typs nicht verlassen wird, z. B.:

3.5.7.2 Explizite Typumwandlung


Gelegentlich gibt es gute Gründe dafür, über den sogenannten Casting-Operator eine explizite
Typumwandlung zu erzwingen. Im nächsten Beispielprogramm wird mit
(int)'x'
die int-erpretation des (aus dem Abschnitt 3.5.6 bekannten) Bitmusters zum kleinen „x“ vorge-
nommen, damit Sie nachvollziehen können, warum das Beispielprogramm im vorigen Abschnitt
beim „Halbieren“ dieses Zeichens auf den Wert 60 kam:
Quellcode Ausgabe
class Prog { 120
public static void main(String[] args) { 3
System.out.println((int)'x'); 4
2147483647
double a = 3.7615926;
System.out.println((int)a);
System.out.println((int)(a + 0.5));

a = 7294452388.13;
System.out.println((int)a);
}
}

Manchmal ist es erforderlich, einen Gleitkommawert in eine Ganzzahl zu wandeln, weil z. B. bei
einem Methodenaufruf für einen Parameter ein ganzzahliger Datentyp benötigt wird. Dabei werden
die Nachkommastellen abgeschnitten. Soll auf die nächstgelegene ganze Zahl gerundet werden,
addiert man vor der Typumwandlung 0,5 zum Gleitkommawert.
Es ist auf jeden Fall zu beachten, dass dabei eine einschränkende Konvertierung stattfindet, und
dass die zu erwartende Gleitkommazahl im Wertebereich des Ganzzahltyps liegen muss. Wie die
letzte Ausgabe zeigt, sind kapitale Programmierfehler möglich, wenn die Wertebereiche der betei-
ligten Datentypen nicht beachtet werden, und bei der Zielvariablen ein Überlauf auftritt (vgl. Ab-
schnitt 3.6.1). So soll die Explosion der europäischen Rakete Ariane 5 am 4. Juni 1996 (Schaden:
ca. 500 Millionen Dollar)
Abschnitt 3.5 Operatoren und Ausdrücke 141

durch die Konvertierung eines double-Werts (mögliches Maximum: 1,797693134862315710308) in


einen short-Wert (mögliches Maximum: 215-1 = 32767) verursacht worden sein. Die kritische Ty-
pumwandlung hatte bei der langsameren Rakete Ariane 4 noch keine Probleme gemacht. Offenbar
sind profunde Kenntnisse über elementare Sprachelemente unverzichtbar für eine erfolgreiche Ra-
ketenforschung und -entwicklung.
Später wird sich zeigen, dass auch zwischen Referenztypen gelegentlich eine explizite Wandlung
erforderlich ist.
Welche expliziten Typkonvertierungen in Java erlaubt sind, ist der Sprachspezifikation zu entneh-
men (Gosling et al. 2021, Abschnitt 5.1).
Die Java-Syntax zur expliziten Typumwandlung:
Typumwandlungs-Operator

( Zieltyp ) Ausdruck

Am Rand soll noch erwähnt werden, dass die Wandlung in einen Ganzzahltyp keine sinnvolle
Technik ist, um die Nachkommastellen in einem Gleitkommawert zu entfernen oder zu extrahieren.
Dazu kann man den Modulo-Operator verwenden (vgl. Abschnitt 3.5.1), ohne ein Wertebereichs-
problem befürchten zu müssen, z. B.:1
Quellcode Ausgabe
class Prog { 85347483648,13
public static void main(String[] args) { 2147483647
double a = 85347483648.13, b; 85347483648,00
int i = (int) a;
b = a - a%1;
System.out.printf("%15.2f%n%12d%n%15.2f", a, i, b);
}
}

3.5.8 Zuweisungsoperatoren
Bei den ersten Erläuterungen zu Wertzuweisungen (vgl. Abschnitt 3.3.8) blieb aus didaktischen
Gründen unerwähnt, dass eine Wertzuweisung ein Ausdruck ist, dass wir es also mit dem binären
(zweistelligen) Operator „=“ zu tun haben, für den die folgenden Regeln gelten:

1
Der ganzzahlige Anteil eines double-Werts lässt sich auch über die statische Methode floor() aus der Klasse Math
ermitteln. Für eine double-Variable d mit einem nicht-negativen Wert ist d-Math.floor(d) identisch mit d%1.0.
142 Kapitel 3 Elementare Sprachelemente

• Auf der linken Seite muss eine Variable stehen.


• Auf der rechten Seite muss ein Ausdruck mit kompatiblem Typ stehen.
• Der zugewiesene Wert stellt auch den Ergebniswert des Ausdrucks dar.
Wie beim Inkrement- bzw. Dekrementoperator sind auch beim Zuweisungsoperator zwei Effekte zu
unterscheiden:
• Die als linkes Argument fungierende Variable erhält einen neuen Wert.
• Es wird ein Wert für den Ausdruck produziert.
Im folgenden Beispiel fungiert ein Zuweisungsausdruck als Parameter für einen println() - Metho-
denaufruf:
Quellcode Ausgabe
class Prog { 4711
public static void main(String[] args) { 4711
int ivar = 13;
System.out.println(ivar = 4711);
System.out.println(ivar);
}
}

Beim Auswerten des Ausdrucks ivar = 4711 entsteht der an println() zu übergebende Wert
(identisch mit dem zugewiesenen Wert), und die Variable ivar wird verändert.
Selbstverständlich kann eine Zuweisung auch als Operand in einen übergeordneten Ausdruck inte-
griert werden, z. B.:
Quellcode Ausgabe
class Prog { 8
public static void main(String[] args) { 8
int i = 2, j = 4;
i = j = j * i;
System.out.println(i + "\n" + j);
}
}

Beim mehrfachen Auftreten des Zuweisungsoperators erfolgt eine Abarbeitung von rechts nach
links (vgl. Tabelle im Abschnitt 3.5.10), sodass die Anweisung
i = j = j * i;
folgendermaßen ausgeführt wird:
• Weil der Multiplikationsoperator eine höhere Bindungskraft besitzt als der Zuweisungsope-
rator (siehe Abschnitt 3.5.10.1), wird zuerst der Ausdruck j * i ausgewertet, was zum
Zwischenergebnis 8 (mit Datentyp int) führt.
• Nun wird die rechte Zuweisung ausgeführt. Der folgende Ausdruck mit dem Wert 8 und
dem Typ int
j = 8
verschafft der Variablen j einen neuen Wert.
• In der zweiten Zuweisung (bei Betrachtung von rechts nach links) wird der Wert des Aus-
drucks j = 8 an die Variable i übergeben.
Anweisungen der Art
i = j = k;
Abschnitt 3.5 Operatoren und Ausdrücke 143

sind in Java-Programmen gelegentlich anzutreffen, weil Schreibaufwand gespart wird im Vergleich


zur Alternative
j = k;
i = k;
Wie wir seit dem Abschnitt 3.3.8 wissen, stellt ein Zuweisungsausdruck bereits eine vollständige
Anweisung dar, sobald man ein Semikolon dahinter setzt. Dies gilt auch für die Prä- und Pos-
tinkrementausdrücke (vgl. Abschnitt 3.5.1) sowie für Methodenaufrufe, jedoch nicht für die anderen
Ausdrücke, die im Abschnitt 3.5 vorgestellt werden.
Für die häufig benötigten Zuweisungen nach dem Muster
j = j * i;
(eine Variable erhält einen neuen Wert, an dessen Konstruktion sie selbst mitwirkt) bietet Java spe-
zielle Zuweisungsoperatoren für Schreibfaule, die gelegentlich auch als Aktualisierungsoperato-
ren oder als Verbundzuweisungs-Operatoren (engl.: compound assignment operators) bezeichnet
werden. In der folgenden Tabelle steht Var für eine numerische Variable (mit dem Datentyp byte,
short, int, long, char, float oder double) und Expr für einen numerischen Ausdruck:
Beispiel
Operator Bedeutung
Programmfragment Neuer Wert von i
Var += Expr Var erhält den neuen Wert int i = 2; 5
Var + Expr. i += 3;
Var -= Expr Var erhält den neuen Wert int i = 10, j = 3; 1
Var - Expr. i -= j * j;
Var *= Expr Var erhält den neuen Wert int i = 2; 10
Var * Expr. i *= 5;
Var /= Expr Var erhält den neuen Wert int i = 10; 2
Var / Expr. i /= 5;
Var %= Expr Var erhält den neuen Wert int i = 10; 0
Var % Expr. i %= 5;

Es ist eine vertretbare Entscheidung, in eigenen Programmen der Klarheit halber auf die Aktualisie-
rungsoperatoren zu verzichten. In fremden Programmen muss man aber mit diesen Operatoren
rechnen, und manche Entwicklungsumgebungen fordern sogar zu Ihrer Verwendung auf.
Ein weiteres Argument gegen die Aktualisierungsoperatoren ist die implizit darin enthaltene
Typwandlung. Während z. B. für die beiden Variablen
int ivar = 1;
double dvar = 3_000_000_000.0;
die folgende Zuweisung
ivar = ivar + dvar; // verboten
vom Compiler verhindert wird, weil der Ausdruck (ivar + dvar) den Typ double besitzt (vgl.
Tabelle mit den Ergebnistypen der arithmetischen Operationen im Abschnitt 3.5.1), akzeptiert der
Compiler die folgende Anweisung mit Aktualisierungsoperator:
ivar += dvar;
Es kommt zum Ganzzahlüberlauf (vgl. Abschnitt 3.6.1), und man erhält für ivar den ebenso sinn-
losen wie gefährlichen Wert 2147483647:
144 Kapitel 3 Elementare Sprachelemente

Quellcode Ausgabe
class Prog { 2147483647
public static void main(String[] args) {
int ivar = 1;
double dvar = 3_000_000_000.0;
ivar += dvar;
System.out.println(ivar);
}
}

In der Java-Sprachspezifikation (Gosling et al. 2021, Abschnitt 15.26.2) findet sich die folgende
Erläuterung zum Verhalten des Java-Compilers, der bei Aktualisierungsoperatoren eine untypische
und gefährliche Laxheit zeigt:
A compound assignment expression of the form E1 op= E2 is equivalent to E1 = (T)
((E1) op (E2)), where T is the type of E1, except that E1 is evaluated only once.
Der Ausdruck ivar += dvar steht also für
ivar = (int) (ivar + dvar)
und enthält eine riskante einschränkende Typanpassung.
Beim Einsatz eines Aktualisierungsoperators sollte der Wertebereich des rechten Operanden keines-
falls größer sein als der Wertebereich des linken Operanden, und es ist zu bedauern, dass keine ent-
sprechende Compiler-Regel existiert.

3.5.9 Konditionaloperator
Der Konditionaloperator erlaubt eine sehr kompakte Schreibweise, wenn beim neuen Wert für
eine Zielvariable bedingungsabhängig zwischen zwei Ausdrücken zu entscheiden ist, z. B.
i + j falls k  0
i=
i − j sonst
In Java ist für diese Zuweisung mit Fallunterscheidung nur eine einzige Zeile erforderlich:
Quellcode Ausgabe
class Prog { 3
public static void main(String[] args) {
int i = 2, j = 1, k = 7;
i = k>0 ? i+j : i-j;
System.out.println(i);
}
}

Eine Besonderheit des Konditionaloperators besteht darin, dass er drei Argumente verarbeitet, wel-
che durch die Zeichen ? und : getrennt werden:
Konditionaloperator

Logischer Ausdruck ? Ausdruck 1 : Ausdruck 2

Ist der logische Ausdruck wahr, liefert der Konditionaloperator den Wert von Ausdruck 1, anderen-
falls den Wert von Ausdruck 2.
Die Frage nach dem Datentyp eines Konditionalausdrucks ist etwas knifflig, und in der Java
Sprachspezifikation werden zahlreiche Fälle unterschieden (Gosling et al. 2021, Abschnitt 15.25).
Abschnitt 3.5 Operatoren und Ausdrücke 145

Es liegt an Ihnen, sich auf den einfachsten und wichtigsten Fall zu beschränken: Wenn der zweite
und der dritte Operand denselben Datentyp haben, dann ist dies auch der Datentyp des Konditional-
ausdrucks.

3.5.10 Auswertungsreihenfolge
Bisher haben wir zusammengesetzte Ausdrücke mit mehreren Operatoren und das damit verbunde-
ne Problem der Auswertungsreihenfolge nach Möglichkeit gemieden. Wie sich gleich zeigen wird,
sind für Schwierigkeiten und Fehler bei der Verwendung zusammengesetzter Ausdrücke die fol-
genden Gründe hauptverantwortlich:
• Komplexität des Ausdrucks (Anzahl der Operatoren, Schachtelungstiefe)
• Operatoren mit Nebeneffekten
Um Problemen aus dem Weg zu gehen, sollte man also eine übertriebene Komplexität vermeiden
und auf Nebeneffekte weitgehend verzichten.

3.5.10.1 Regeln
In diesem Abschnitt werden die Regeln vorgestellt, nach denen der Java-Compiler einen Ausdruck
mit mehreren Operatoren auswertet.

1) Runde Klammern
Wenn aus den anschließend erläuterten Regeln zur Bindungskraft und Assoziativität der beteiligten
Operatoren nicht die gewünschte Operandenzuordnung bzw. Auswertungsreihenfolge resultiert,
dann greift man mit runden Klammern steuernd ein, wobei auch eine Schachtelung erlaubt ist.
Durch Klammern werden Terme zu einem Operanden zusammengefasst, sodass die internen Opera-
tionen ausgeführt sind, bevor der Klammerausdruck von einem externen Operator verarbeitet wird.

2) Bindungskraft (Priorität)
Steht ein Operand (ein Ausdruck) zwischen zwei Operatoren, dann wird er dem Operator mit der
stärkeren Bindungskraft (siehe Tabelle im Abschnitt 3.5.10.2) zugeordnet. Mit den numerischen
Variablen a, b und c als Operanden wird z. B. der Ausdruck
a + b * c
nach der Regel „Punktrechnung geht vor Strichrechnung“ interpretiert als
a + (b * c)
In der Konkurrenz um die Zuständigkeit für den Operanden b hat der Multiplikationsoperator Vor-
rang gegenüber dem Additionsoperator.
Die implizite Klammerung kann durch eine explizite Klammerung dominiert werden:
(a + b) * c

3) Assoziativität (Orientierung)
Steht ein Operand zwischen zwei Operatoren mit gleicher Bindungskraft, dann entscheidet deren
Assoziativität (Orientierung) über die Zuordnung des Operanden:
• Mit Ausnahme der Zuweisungsoperatoren sind alle binären Operatoren links-assoziativ.
Z. B. wird
x – y – z
ausgewertet als
(x – y) – z
146 Kapitel 3 Elementare Sprachelemente

Diese implizite Klammerung kann durch eine explizite Klammerung dominiert werden:
x – (y – z)
• Die Zuweisungsoperatoren sind rechts-assoziativ. Z. B. wird
a += b -= c = d
ausgewertet als
a += (b -= (c = d))
Diese implizite Klammerung kann nicht durch eine explizite Klammerung geändert werden,
weil der linke Operand einer Zuweisung eine Variable sein muss.
In Java ist dafür gesorgt, dass Operatoren mit gleicher Bindungskraft stets auch die gleiche Assozia-
tivität besitzen, z. B. die im letzten Beispiel enthaltenen Operatoren +=, -= und =.
Für manche Operationen gilt das mathematische Assoziativitätsgesetz, sodass die Reihenfolge der
Auswertung irrelevant ist, z. B.:
(3 + 2) + 1 = 6 = 3 + (2 + 1)
Anderen Operationen fehlt diese Eigenschaft, z. B.:
(3 – 2) – 1 = 0  3 – (2 – 1) = 2
Während sich die Addition und die Multiplikation von Ganzzahltypen in Java tatsächlich assoziativ
verhalten, gilt das aus technischen Gründen nicht für die Addition und die Multiplikation von Gleit-
kommatypen (Gosling et al 2021, Abschnitt 15.7.3).

4) Links vor rechts bei der Auswertung der Argumente eines binären Operators
Bevor ein Operator ausgeführt werden kann, müssen erst seine Argumente (Operanden) ausgewertet
sein. Bei jedem binären Operator ist in Java sichergestellt, dass erst der linke Operand ausgewertet
wird, dann der rechte.1 Im folgenden Beispiel tritt der Ausdruck ++ivar als rechter Operand einer
Multiplikation auf. Die hohe Bindungskraft (Priorität) des Präinkrementoperators (siehe Tabelle im
Abschnitt 3.5.10.2) führt nicht dazu, dass sich der Nebeneffekt des Ausdrucks ++ivar auf den lin-
ken Operanden der Multiplikation auswirkt:
Quellcode Ausgabe
class Prog { 6
public static void main(String[] args) { 3
int ivar = 2;
int erg = ivar * ++ivar;
System.out.printf("%d%n%d", erg, ivar);
}
}

Die Auswertung des Ausdrucks ivar * ++ivar verläuft so:


• Zuerst wird der linke Operand der Multiplikation ausgewertet (Ergebnis: 2)
• Dann wird der rechte Operand der Multiplikation ausgewertet:
o Die Präinkrementoperation hat einen Nebeneffekt auf die Variable ivar.
o Der Ausdruck ++ivar hat den Wert 3.
• Die Ausführung der Multiplikationsoperation liefert schließlich das Endergebnis 6.

1
In den folgenden Fällen unterbleibt die Auswertung des rechten Operanden:
• Bei der Auswertung des linken Operanden kommt es zu einem Ausnahmefehler (siehe unten).
• Bei den logischen Operatoren mit bedingter Ausführung (&&, ||) verhindert ein bestimmter Wert des linken
Operanden die Auswertung des rechten Operanden (siehe Abschnitt 3.5.5).
Abschnitt 3.5 Operatoren und Ausdrücke 147

Das Beispiel zeigt auch, dass der Begriff der Bindungskraft gegenüber dem Begriff der Priorität zu
bevorzugen ist. Weil sich kein Operand zwischen den Operatoren * und ++ befindet, können deren
Bindungskräfte offensichtlich keine Rolle spielen. Der Begriff der Priorität suggeriert aber trotz-
dem, dass der Präinkrementoperator einen Vorrang bei der Ausführung hätte.
Wie eine leichte Variation des letzten Beispiels zeigt, kann sich ein Nebeneffekt im linken Operan-
den einer binären Operation auf den rechten Operanden auswirken:
Quellcode Ausgabe
class Prog { 9
public static void main(String[] args) { 3
int ivar = 2;
int erg = ++ivar * ivar;
System.out.printf("%d%n%d", erg, ivar);
}
}

Im folgenden Beispiel stehen a, b und c für beliebige numerische Operanden (z. B. ++ivar). Für
den Ausdruck
a+b*c
resultiert aus der Bindungskraftregel die folgende Zuordnung der Operanden:
a + (b * c)
Zusammen mit der Links-vor-rechts - Regel ergibt sich für die Auswertung der Operanden bzw.
Ausführung der Operatoren die folgende Reihenfolge:
a, b, c, *, +
Wenn als Operanden numerische Literale oder Variablen auftreten, wird bei der „Auswertung“ ei-
nes Operanden lediglich sein Wert ermittelt, und die Reihenfolge der Operandenauswertungen ist
belanglos. Im letzten Beispiel eine falsche Auswertungsreihenfolge zu unterstellen (z. B. b, c, *, a,
+), bleibt ungestraft. Wenn Operanden Nebeneffekte enthalten (Zuweisungen, In- bzw. Dekrement-
operationen oder Methodenaufrufe), dann ist die Reihenfolge der Operandenauswertungen jedoch
relevant, und eine falsche Vermutung kann gravierende Fehler verursachen. Im folgenden Beispiel
Quellcode Ausgabe
class Prog { 8
public static void main(String[] args) {
int ivar = 2;
System.out.print(ivar++ + ivar * 2);
}
}

resultiert für den Ausdruck ivar++ + ivar * 2 der Wert 8, denn:


• Zuerst wird der linke Operand der Addition ausgewertet:
o Der Ausdruck ivar++ hat den Wert 2.
o Die Postinkrementoperation hat einen Nebeneffekt auf die Variable ivar.
• Dann wird der linke Operand der Multiplikation ausgewertet (Ergebnis: 3).
• Dann wird der rechte Operand der Multiplikation ausgewertet (Ergebnis: 2).
• Dann wird die Multiplikation ausgeführt (Ergebnis: 6).
• Dann wird die Addition ausgeführt (Ergebnis: 8).
Auch bei einem rechts-assoziativen Operator wird der linke Operand vor dem rechten ausgewertet,
sodass im folgenden Beispiel mit der int-Variablen alfa
alfa += ++alfa
148 Kapitel 3 Elementare Sprachelemente

diese Auswertungs- bzw. Ausführungsreihenfolge resultiert:


alfa, ++alfa, +=
Als neuer Wert von alfa entsteht:
alfa + (alfa + 1)
Die oft anzutreffende Behauptung, Klammerausdrücke würden generell zuerst ausgewertet, ist
falsch, wie das folgende Beispiel zeigt:
Quellcode Ausgabe
class Prog { 16
public static void main(String[] args) {
int ivar = 2;
int erg = ivar * (++ivar + 5);
System.out.println(erg);
}
}

Die Auswertung des Ausdrucks ivar * (++ivar + 5) verläuft so:


• Wegen Regel 4 (links-vor-rechts bei der Auswertung der Operanden eines binären Opera-
tors) wird zuerst der linke Operand der Multiplikation ausgewertet (Ergebnis: 2)
• Dann wird der rechte Operand der Multiplikation ausgewertet (also der Klammerausdruck).
• Hier ist mit der Addition eine weitere binäre Operation vorhanden, und nach der Links-vor-
rechts - Regel wird zunächst deren linker Operand ausgewertet (Ergebnis: 3, Nebeneffekt
auf die Variable ivar). Dann wird der rechte Operand der Addition ausgewertet (Ergebnis:
5). Die Ausführung der Additionsoperation liefert für den Klammerausdruck den Wert 8.
• Schließlich führt die Multiplikation zum Endergebnis 16.

3.5.10.2 Operatorentabelle
In der folgenden Tabelle sind die bisher behandelten Operatoren mit absteigender Bindungskraft
(Priorität) aufgelistet. Gruppen von Operatoren mit gleicher Bindungskraft sind durch eine horizon-
tale Linie voneinander getrennt. In der Operanden-Spalte werden die zulässigen Datentypen der
Argumentausdrücke mit Hilfe der folgenden Platzhalter beschrieben:
N Ausdruck mit numerischem Datentyp (byte, short, int, long, char, float, double)
I Ausdruck mit integralem (ganzzahligem) Datentyp (byte, short, int, long, char)
L logischer Ausdruck (Typ boolean)
K Ausdruck mit kompatiblem Datentyp
S String (Zeichenfolge)
V Variable mit kompatiblem Datentyp
Vn Variable mit numerischem Datentyp (byte, short, int, long, char, float, double)
Abschnitt 3.6 Über- und Unterlauf bei numerischen Variablen 149

Operator Bedeutung Operanden

! Negation L
++, -- Prä- oder Postinkrement bzw. -dekrement Vn
- Vorzeichenumkehr N
(Typ) Typumwandlung K
*, / Punktrechnung N, N
% Modulo N, N
+, - Strichrechnung N, N
+ String-Verkettung S, K oder K, S
<<, >> Links- bzw. Rechts-Verschiebung I, I

>, <,
Vergleichsoperatoren N, N
>=, <=
==, != Gleichheit, Ungleichheit K, K
& Bitweises UND I, I
& Logisches UND (mit unbedingter Auswertung) L, L
^ Exklusives logisches ODER L, L
| Bitweises ODER I, I
| Logisches ODER (mit unbedingter Auswertung) L, L
&& Logisches UND (mit bedingter Auswertung) L, L
|| Logisches ODER (mit bedingter Auswertung) L, L
?: Konditionaloperator L, K, K
= Wertzuweisung V, K
+=, -=,
*=, /=, Wertzuweisung mit Aktualisierung Vn, N
%=

Im Anhang A finden Sie eine erweiterte Version dieser Tabelle, die zusätzlich alle Operatoren ent-
hält, die im weiteren Verlauf des Manuskripts noch behandelt werden.

3.6 Über- und Unterlauf bei numerischen Variablen


Wie Sie inzwischen wissen, haben die primitiven Datentypen für Zahlen jeweils einen bestimmten
Wertebereich (siehe Tabelle im Abschnitt 3.3.6). Dank strenger Typisierung kann der Compiler
verhindern, dass einer Variablen ein Ausdruck mit „zu großem Typ“ zugewiesen wird. So kann
z. B. einer int-Variablen kein Wert vom Typ long zugewiesen werden. Bei der Auswertung eines
Ausdrucks kann jedoch „unterwegs“ ein Wertebereichsproblem (z. B. ein Überlauf) auftreten. Im
150 Kapitel 3 Elementare Sprachelemente

betroffenen Programm ist mit einem mehr oder weniger gravierenden Fehlverhalten zu rechnen,
sodass Wertebereichsprobleme unbedingt vermieden bzw. rechtzeitig diagnostiziert werden müssen.
Im Zusammenhang mit Wertebereichsproblemen bieten sich gelegentlich die Klassen BigDecimal
und BigInteger aus dem Paket java.math als Alternativen zu den primitiven Datentypen an. Wenn
wir gleich auf einen solchen Fall stoßen, verzichten wir nicht auf eine kurze Beschreibung der je-
weiligen Vor- und Nachteile, obwohl die beiden Klassen nicht zu den elementaren Sprachelementen
gehören. Analog wurde schon im Abschnitt 3.3.7.2 demonstriert, dass die Klasse BigDecimal bei
finanzmathematischen Anwendungen wegen ihrer praktisch unbeschränkten Genauigkeit gegenüber
den binären Gleitkommatypen (double und float) zu bevorzugen ist.

3.6.1 Überlauf bei Ganzzahltypen


Wird z. B. zu einer ganzzahligen Variablen, die bereits den maximalen Wert ihres Datentyps be-
sitzt, eine positive Zahl addiert, dann kann das Ergebnis nicht mehr korrekt abgespeichert werden.
Ohne besondere Vorkehrungen stellt ein Java-Programm im Falle eines solchen Ganzzahlüberlaufs
keinesfalls seine Tätigkeit mit einem Ausnahmefehler ein, sondern es arbeitet munter weiter. Das
folgende Programm
class Prog {
public static void main(String[] args) {
int i = 2_147_483_647, j = 5, k;
k = i + j; // Überlauf!
System.out.println(i + " + " + j + " = " + k);
}
}
liefert ohne jede Warnung das sinnlose Ergebnis:
2147483647 + 5 = -2147483644
Um das Auftreten eines negativen „Ergebniswerts“ zu verstehen, machen wir einen kurzen Ausflug
in die Informatik. Die Werte eines Ganzzahltyps sind nach dem Zweierkomplementprinzip auf
einem Zahlenkreis angeordnet, und nach der größten positiven Zahl beginnt der Bereich der negati-
ven Zahlen (mit abnehmendem Betrag), z. B. beim Typ byte:
-2 -1 0 1 2

-126 -128 126

Speziell bei der Steuerung von Raketenmotoren (vgl. Abschnitt 3.5.7) ist also Vorsicht geboten,
weil ansonsten das Kommando „Mr. Spock, please push the engine.“ zum heftigen Rückwärtsschub
führen könnte.1 Es zeigt sich erneut, dass eine erfolgreiche Raketenforschung und -entwicklung
ohne die sichere Beherrschung der elementaren Sprachelemente kaum möglich ist.

1
Mr. Spock arbeitete jahrelang als erster Offizier auf dem Raumschiff Enterprise.
Abschnitt 3.6 Über- und Unterlauf bei numerischen Variablen 151

Natürlich kann nicht nur der positive Rand eines Ganzzahlwertebereichs überschritten werden, son-
dern auch der negative Rand, indem z. B. vom kleinstmöglichen Wert eine positive Zahl subtrahiert
wird:
Quellcode Ausgabe
class Prog { -2147483648 - 5 = 2147483643
public static void main(String[] args) {
int i = -2_147_483_648, j = 5, k;
k = i - j;
System.out.println(i+" - "+j+" = "+k);
}
}

Bei Wertebereichsproblemen durch eine betragsmäßig zu große Zahl wird im Manuskript generell
von einem Überlauf gesprochen. Unter einem Unterlauf soll später das Verlassen eines Gleitkom-
mawertebereichs in Richtung null durch eine betragsmäßig zu kleine Zahl verstanden werden (vgl.
Abschnitt 3.6.3).
Oft lässt sich ein Überlauf durch die Wahl eines geeigneten Datentyps verhindern. Mit den Deklara-
tionen
long i = 2_147_483_647, j = 5, k;
kommt es in der Anweisung
k = i + j;
nicht zum Überlauf, weil neben i, j und k nun auch der Ausdruck i+j den Typ long besitzt (siehe
Tabelle im Abschnitt 3.5.1). Die Anweisung
System.out.println(i + " + " + j + " = " + k);
liefert das korrekte Ergebnis:
2147483647 + 5 = 2147483652
Im Beispiel genügt es nicht, für die Zielvariable k den beschränkten Typ int durch long zu ersetzen,
weil der Überlauf beim Berechnen des Ausdrucks („unterwegs“) auftritt. Mit den Deklarationen
int i = 2_147_483_647, j = 5;
long k;
bleibt das Ergebnis falsch, denn …
• In der Anweisung
k = i + j;
wird der Ausdruck i + j berechnet, bevor die Zuweisung ausgeführt wird.
• Weil beide Operanden vom Typ int sind, erhält auch der Ausdruck diesen Typ (siehe Tabel-
le im Abschnitt 3.5.1), und die Summe kann nicht korrekt berechnet bzw. zwischenspeichert
werden.
• Schließlich wird der long-Variablen k das falsche Ergebnis zugewiesen.
Wenn auch der long-Wertebereich nicht ausreicht, und weiterhin mit ganzen Zahlen gerechnet wer-
den soll, dann bietet sich die Klasse BigInteger aus dem Paket java.math an.1 Das folgende Pro-
gramm

1
Ab Java 9 befindet sich das Paket java.util im Modul java.base. Das gilt bis auf wenige Ausnahmen für alle im
Manuskript verwendeten Pakete, sodass der Hinweis auf die Modulzugehörigkeit nur noch in den Ausnahmefällen
erscheint.
152 Kapitel 3 Elementare Sprachelemente

import java.math.*;
class Prog {
public static void main(String[] args) {
BigInteger bigint = new BigInteger("9223372036854775808");
bigint = bigint.multiply(bigint);
System.out.println("2 hoch 126 = " + bigint);
}
}
speichert im BigInteger-Objekt bigint die knapp außerhalb des long-Wertebereichs liegende
Zahl 263, quadriert diese auch noch mutig und findet selbstverständlich das korrekte Ergebnis:
2 hoch 126 = 85070591730234615865843651857942052864
Im Vergleich zu den primitiven Ganzzahltypen verursacht die Klasse BigInteger allerdings einen
höheren Speicher- und Rechenzeitaufwand.
Seit Java 8 bietet die Klasse Math im Paket java.lang statische Methoden für arithmetische Opera-
tionen mit Ganzzahltypen, die auf einen Überlauf mit einem Ausnahmefehler reagieren. Neben den
anschließend aufgelisteten Methoden für int-Argumente sind analog arbeitende Methoden für long-
Argumente vorhanden:
• public static int addExact(int x, int y)
• public static int subtractExact(int x, int y)
• public static int multiplyExact(int x, int y)
• public static int incrementExact(int a)
• public static int decrementExact(int a)
• public static int negateExact(int a)
Falls ein Ausnahmefehler nicht abgefangen wird, endet das betroffene Programm, statt mit sinnlo-
sen Zwischenergebnissen weiterzurechnen, z. B.:
Quellcode Ausgabe
class Prog { Exception in thread "main" java.lang.ArithmeticException:
integer overflow
public static void main(String[] a) {
at java.base/java.lang.Math.addExact(Math.java:825)
int i = 2147483647, j = 5, k; at Prog.main(Prog.java:4)
k = Math.addExact(i, j);
System.out.printf("%d + %d = %d",
i, j, k);
}
}

3.6.2 Unendliche und undefinierte Werte bei den Typen float und double
Auch bei den binären Gleitkommatypen float und double kann ein Überlauf auftreten, obwohl die
unterstützten Wertebereiche hier weit größer sind. Dabei kommt es aber weder zu einem sinnlosen
Zufallswert, sondern zu den speziellen Gleitkommawerten +/- Unendlich, mit denen anschließend
sogar weitergerechnet werden kann. Das folgende Programm
class Prog {
public static void main(String[] args) {
double bigd = Double.MAX_VALUE;
System.out.printf("Double.MAX_VALUE = %15e%n", bigd);
bigd = Double.MAX_VALUE * 10.0;
System.out.printf("Double.MaxValue * 10 = %15e%n", bigd);
System.out.printf("Unendlich + 10 = %15e%n", bigd + 10);
System.out.printf("Unendlich * (-1) = %15e%n",bigd * -1);
System.out.printf("13.0/0.0 = %15e", 13.0 / 0.0);
}
}
Abschnitt 3.6 Über- und Unterlauf bei numerischen Variablen 153

liefert die Ausgabe:


Double.MAX_VALUE = 1,797693e+308
Double.MaxValue * 10 = Infinity
Unendlich + 10 = Infinity
Unendlich * (-1) = -Infinity
13.0/0.0 = Infinity

Im Programm erhält die double-Variable bigd den größtmöglichen Wert ihres Typs. Anschließend
wird bigd mit dem Faktor 10 multipliziert, was zum Ergebnis +Unendlich führt. Mit diesem Zwi-
schenergebnis kann Java durchaus rechnen:
• Addiert man die Zahl 10, dann bleibt es beim Wert +Unendlich.
• Eine Multiplikation von +Unendlich mit (-1) führt zum Wert -Unendlich.
Mit Hilfe der Unendlich-Werte „gelingt“ offenbar bei der Gleitkommaarithmetik sogar die Division
durch null, während bei der Ganzzahlarithmetik ein solcher Versuch zu einem Laufzeitfehler (aus
der Klasse ArithmeticException) führt.
Bei den folgenden „Berechnungen“
Unendlich − Unendlich

Unendlich
Unendlich

Unendlich  0

0
0
resultiert der spezielle Gleitkommawert NaN (Not a Number), wie das nächste Beispielprogramm
zeigt:
class Prog {
public static void main(String[] args) {
double bigd = Double.MAX_VALUE * 10.0;
System.out.printf("Unendlich – Unendlich = %3f%n", bigd-bigd);
System.out.printf("Unendlich / Unendlich = %3f%n", bigd/bigd);
System.out.printf("Unendlich * 0.0 = %3f%n", bigd * 0.0);
System.out.printf("0.0 / 0.0 = %3f", 0.0/0.0);
}
}
Es liefert die Ausgaben:
Unendlich – Unendlich = NaN
Unendlich / Unendlich = NaN
Unendlich * 0.0 = NaN
0.0 / 0.0 = NaN

Zu den letzten Beispielprogrammen ist noch anzumerken, dass man über das öffentliche, statische
und finalisierte Feld MAX_VALUE der Klasse Double aus dem Paket java.lang den größten Wert
in Erfahrung bringt, der in einer double-Variablen gespeichert werden kann.
Über die statischen Double-Methoden
• public static boolean isInfinite(double arg)
• public static boolean isNaN(double arg)
154 Kapitel 3 Elementare Sprachelemente

mit Rückgabetyp boolean lässt sich für eine double-Variable prüfen, ob sie einen unendlichen oder
undefinierten Wert besitzt, z. B.:
Quellcode Ausgabe
class Prog { true
public static void main(String[] args) { true
System.out.println(Double.isInfinite(1.0/0.0));
System.out.print(Double.isNaN(0.0/0.0));
}
}

Für besonders neugierige Leser sollen abschließend noch die float-Darstellungen der speziellen
Gleitkommawerte angegeben werden (vgl. Abschnitt 3.3.7.1):
float-Darstellung
Wert
Vorz. Exponent Mantisse
+unendlich 0 11111111 00000000000000000000000
-unendlich 1 11111111 00000000000000000000000
NaN 0 11111111 10000000000000000000000
Wenn der double-Wertebereich längst in Richtung Infinity überschritten ist, kann man mit Objek-
ten der Klasse BigDecimal aus dem Paket java.math noch rechnen:
Quellcode Ausgabe
import java.math.*; Very Big: 1.057066e+3000
class Prog {
public static void main(String[] args) {
BigDecimal bigd = new BigDecimal("1000111");
bigd = bigd.pow(500);
System.out.printf("Very Big: %e", bigd);
}
}

Ein Überlauf ist bei BigDecimal-Objekten nicht zu befürchten, solange das Programm genügend
Hauptspeicher zur Verfügung hat.

3.6.3 Unterlauf bei den Gleitkommatypen


Bei den binären Gleitkommatypen float und double ist auch ein Unterlauf möglich, wobei eine
Zahl mit einem sehr kleinen Betrag nicht mehr dargestellt werden kann. In diesem Fall rechnet ein
Java-Programm mit dem Wert 0,0 weiter, was in der Regel akzeptabel ist, z. B.:
Quellcode Ausgabe
class Prog { 4.9E-324
public static void main(String[] args) { 0.0
double smalld = Double.MIN_VALUE;
System.out.println(smalld);
smalld /= 2.0;
System.out.println(smalld);
}
}

Das statische, öffentliche und finalisierte Feld MIN_VALUE der Klasse Double im Paket
java.lang enthält den betragsmäßig kleinsten Wert, der in einer double-Variablen gespeichert wer-
den kann (vgl. Abschnitt 3.3.6).
Abschnitt 3.6 Über- und Unterlauf bei numerischen Variablen 155

In unglücklichen Fällen wird aber ein deutlich von null verschiedenes Endergebnis grob falsch be-
rechnet, weil unterwegs ein Zwischenergebnis der Null zu nahe gekommen ist, z. B.
Quellcode Ausgabe
class Prog { 9.881312916824932
public static void main(String[] args) { 0.0
double a = 1E-323;
double b = 1E308;
double c = 1E16;
System.out.println(a * b * c);
System.out.print(a * 0.1 * b * 10.0 * c);
}
}

Das Ergebnis des Ausdrucks


a * b * c
wird halbwegs korrekt ermittelt (vgl. Abschnitt 3.3.7.1 zu den Genauigkeitsproblemen der binären
Gleitkommatypen). Bei der Berechnung des Ausdrucks
a * 0.1 * b * 10.0 * c
wird jedoch das Zwischenergebnis
a * 0.1 = 1E-324 < 4.9E-324
aufgrund eines Unterlaufs auf null gesetzt, und das korrekte Endergebnis 10 kann nicht mehr er-
reicht werden.
Mit Objekten der Klasse BigDecimal aus dem Paket java.math an Stelle von double-Variablen
kann ein Unterlauf zuverlässig verhindert werden:
import java.math.*;
class Prog {
public static void main(String[] args) {
BigDecimal a = new BigDecimal("1E-323");
BigDecimal b = new BigDecimal("1E308");
BigDecimal c = new BigDecimal("1E16");
BigDecimal nk1 = new BigDecimal("0.1");
BigDecimal zehn = new BigDecimal("10.0");
System.out.println(a.multiply(nk1).multiply(b).multiply(zehn).multiply(c));
}
}
Weil BigDecimal-Objekte als Argumente der arithmetischen Operatoren nicht zugelassen sind,
muss das Multiplizieren per Methodenaufruf erledigt werden. Als Gegenleistung für den Aufwand
erhält man das korrekte Ergebnis 10,0 ohne Unterlauf und ohne Genauigkeitsproblem (siehe oben).
Neben dem leicht zu verschmerzenden Schreibaufwand entsteht durch die Verwendung von Big-
Decimal-Objekten aber auch ein erhöhter Speicher- und Rechenzeitaufwand (siehe Abschnitt
3.3.7.2), sodass die binären Gleitkommatypen in vielen Situationen die erste Wahl bleiben.

3.6.4 Modifikator strictfp


In Java 1.2 wurde eine Abweichung von der strikten, an der Norm IEEE-754 orientierten Gleit-
kommaarithmetik zugelassen, um Schwächen der damals (1998) üblichen arithmetischen Coprozes-
soren zu kompensieren. Aufgrund der mittlerweile verbesserten Hardware ist die potentiell störende
Koexistenz zwischen zwei Gleitkommavarianten nicht länger erforderlich und sinnvoll. Daher ist ab
Java 17 nur noch die strikte Gleitkommaarithmetik erlaubt. Der Modifikator strictfp, mit dem zu-
vor für eine Klasse, Schnittstelle (siehe Kapitel 9) oder Methode eine an der strikten IEEE-754 -
Norm orientierte Gleitkommaarithmetik angeordnet werden konnte, ist damit überflüssig geworden.
156 Kapitel 3 Elementare Sprachelemente

Er ist aus Kompatibilitätsgründen weiterhin erlaubt, bewirkt aber ab Java 17 nur noch eine Compi-
ler-Warnung.

3.7 Anweisungen (zur Ablaufsteuerung)


Wir haben uns im Kapitel 3 über elementare Sprachelemente zunächst mit (lokalen) Variablen und
primitiven Datentypen vertraut gemacht. Dann haben wir gelernt, aus Variablen, Literalen und
Methodenaufrufen mit Hilfe von Operatoren mehr oder weniger komplexe Ausdrücke zu bilden.
Diese wurden entweder mit Hilfe des Objekts System.out auf der Konsole ausgegeben oder in
Wertzuweisungen verwendet.
In den meisten Beispielprogrammen traten nur wenige Sorten von Anweisungen auf (Variablende-
klarationen, Wertzuweisungen und Methodenaufrufe). Nun werden wir uns systematisch mit dem
allgemeinen Begriff einer Java-Anweisung befassen und vor allem die wichtigen Anweisungen zur
Ablaufsteuerung (Fallunterscheidungen und Schleifen) kennenlernen.

3.7.1 Überblick
Ein ausführbarer Programmteil, also der Rumpf einer Methode, besteht aus Anweisungen (engl.
statements).
Am Ende von Abschnitt 3.7 werden Sie die folgenden Sorten von Anweisungen kennen:
• Deklarationsanweisung für lokale Variablen
Die Anweisung zur Deklaration von lokalen Variablen wurde schon im Abschnitt 3.3.8 ein-
geführt.
Beispiel: int i = 1, j = 2, k;
• Ausdrucksanweisungen
Folgende Ausdrücke werden zu Anweisungen, sobald man ein Semikolon dahinter setzt:
o Wertzuweisung (vgl. Abschnitte 3.3.8 und 3.5.8)
Beispiel: k = i + j;
o Prä- bzw. Postinkrement- oder -dekrementoperation
Beispiel: i++;
Im Beispiel ist nur der „Nebeneffekt“ des Ausdrucks i++ von Bedeutung (vgl. Ab-
schnitt 3.5.1). Sein Wert bleibt ungenutzt.
o Methodenaufruf
Beispiel: System.out.println(k);
Besitzt die im Rahmen einer eigenständigen Anweisung aufgerufene Methode einen
Rückgabewert, dann wird dieser ignoriert.
• Leere Anweisung
Beispiel: ;
Die durch ein einsames (nicht anderweitig eingebundenes) Semikolon ausgedrückte leere
Anweisung hat keinerlei Effekte und kommt gelegentlich zum Einsatz, wenn syntaktisch ei-
ne Anweisung erforderlich ist, aber nichts geschehen soll.
• Blockanweisung
Eine Folge von Anweisungen, die durch geschweifte Klammern zusammengefasst bzw. ab-
gegrenzt werden, bildet eine Block- bzw. Verbundanweisung. Wir haben uns bereits im
Abschnitt 3.3.9 im Zusammenhang mit dem Gültigkeitsbereich für lokale Variablen mit der
Blockanweisung beschäftigt. Wie gleich näher erläutert wird, fasst man z. B. dann mehrere
Abweisungen zu einem Block zusammen, wenn diese Anweisungen unter einer gemeinsa-
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 157

men Bedingung ausgeführt werden sollen. Es wäre sehr unpraktisch, dieselbe Bedingung für
jede betroffene Anweisung wiederholen zu müssen.
• Anweisungen zur Ablaufsteuerung
Die main() - Methoden der bisherigen Beispielprogramme im Kapitel 3 bestanden meist aus
einer Sequenz von Anweisungen, die bei jedem Programmeinsatz komplett durchlaufen
wurde:

Anweisung 1

Anweisung 2

Anweisung 3
Oft möchte man jedoch ...
o die Ausführung einer Anweisung (eines Anweisungsblocks) von einer Bedingung
abhängig machen
o oder eine Anweisung (einen Anweisungsblock) wiederholt ausführen lassen.
Für solche Zwecke enthält Java etliche Anweisungen zur Ablaufsteuerung, die bald ausführ-
lich behandelt werden (bedingte Anweisung, Fallunterscheidung, Schleifen).
Blockanweisungen sowie Anweisungen zur Ablaufsteuerung enthalten andere Anweisungen und
werden daher auch als zusammengesetzte Anweisungen bezeichnet.
Anweisungen werden durch ein Semikolon abgeschlossen, sofern sie nicht mit einer schließenden
Blockklammer enden.

3.7.2 Bedingte Anweisung und Fallunterscheidung


Oft ist es erforderlich, dass eine Anweisung nur unter einer bestimmten Bedingung ausgeführt wird.
Etwas allgemeiner formuliert geht es darum, dass viele Algorithmen Fallunterscheidungen benöti-
gen, also an bestimmten Stellen in Abhängigkeit vom Wert eines steuernden Ausdrucks in unter-
schiedliche Pfade verzweigen müssen.

3.7.2.1 if-Anweisung
Nach dem folgenden Programmablaufplan (PAP) bzw. Flussdiagramm soll eine Anweisung nur
dann ausgeführt werden, wenn ein logischer Ausdruck den Wert true besitzt:

Log. Ausdruck

true false

Anweisung
158 Kapitel 3 Elementare Sprachelemente

Wir werden diese Darstellungstechnik ab jetzt verwenden, um einen Algorithmus bzw. Programm-
ablauf zu beschreiben. Die verwendeten Symbole sind hoffentlich anschaulich, entsprechen aber
keiner strengen Normierung.
Während der Programmablaufplan den Zweck (die Semantik) eines Sprachbestandteils erläutert,
beschreibt das vertraute Syntaxdiagramm, wie zulässige Exemplare des Sprachbestandteils zu bil-
den sind. Das folgende Syntaxdiagramm beschreibt die zur Realisation einer bedingten Ausführung
dienende if-Anweisung:
if-Anweisung

if ( Log. Ausdruck ) Anweisung

Die eingebettete (bedingt auszuführende) Anweisung darf keine Variablendeklaration (im Sinn von
Abschnitt 3.3.8) sein. Ein Block ist aber selbstverständlich erlaubt, und darin dürfen auch lokale
Variablen definiert werden.
Es ist übrigens nicht vergessen worden, ein Semikolon ans Ende des if-Syntaxdiagramms zu setzen.
Dort wird eine eingebettete Anweisung verlangt, wobei konkrete Beispiele oft mit einem Semikolon
enden, manchmal aber auch mit einer schließenden geschweiften Klammer.
Im folgenden Beispiel wird eine Meldung ausgegeben, wenn die Variable anz den Wert 0 besitzt:
if (anz == 0)
System.out.println("Die Anzahl muss > 0 sein!");
Der Zeilenumbruch zwischen dem logischen Ausdruck und der eingebetteten Anweisung dient nur
der Übersichtlichkeit und ist für den Compiler irrelevant.

3.7.2.2 if-else - Anweisung


Soll auch etwas passieren, wenn der steuernde logische Ausdruck den Wert false besitzt,

Log. Ausdruck

true false

Anweisung 1 Anweisung 2

dann erweitert man die if-Anweisung um eine else-Klausel.


Zur Beschreibung der if-else - Anweisung wird an Stelle eines Syntaxdiagramms eine alternative
Darstellungsform gewählt, die sich am typischen Java - Quellcode-Layout orientiert:
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 159

if (Logischer Ausdruck)
Anweisung 1
else
Anweisung 2
Wie bei den Syntaxdiagrammen gilt auch für diese Form der Syntaxbeschreibung:
• Für terminale Sprachbestandteile, die exakt in der angegebenen Form in konkreten Quell-
code zu übernehmen sind, wird fette Schrift verwendet.
• Platzhalter sind an kursiver Schrift zu erkennen.
Während die Syntaxbeschreibung im Quellcode-Layout relativ einfache Bildungsregeln (mit einer
einzigen zulässigen Sequenz) sehr anschaulich beschreibt, kann das manchmal weniger anschauli-
che Syntaxdiagramm bei einer komplizierten und variantenreichen Syntax alle zulässigen Sequen-
zen kompakt und präzise dokumentieren.
Bei den eingebetteten Anweisungen (Anweisung 1 bzw. Anweisung 2) darf es sich nicht um Variab-
lendeklarationen (im Sinn von Abschnitt 3.3.8) handeln. Wird ein Block als eingebettete Anweisung
verwendet, dann sind darin aber auch Variablendeklarationen erlaubt.
Im folgenden if-else - Beispiel wird der natürliche Logarithmus zu einer Zahl berechnet, falls diese
positiv ist. Anderenfalls erscheint eine Fehlermeldung. Das Argument wird vom Benutzer über die
Simput-Methode gdouble() erfragt (vgl. Abschnitt 3.4).1
Eingabe (grün, kursiv)
Quellcode
und Ausgabe
class Prog { Argument > 0: 2,4
public static void main(String[] args) { ln(2,400) = 0,875
System.out.print("Argument > 0: ");
double arg = Simput.gdouble();
if (arg > 0)
System.out.printf("ln(%.3f) = %.3f", arg, Math.log(arg));
else
System.out.println("Argument ungültig oder <= 0!");
}
}

Eine bedingt auszuführende Anweisung darf durchaus wiederum vom if- bzw. if-else - Typ sein,
sodass sich mehrere, hierarchisch geschachtelte Fälle unterscheiden lassen. Den folgenden Pro-
grammablauf mit „sukzessiver Restaufspaltung“

1
Bei einer irregulären Eingabe liefert gdouble() den (Verlegenheits-)Rückgabewert 0.0. Man kann sich aber durch
einen Aufruf der Simput-Klassenmethode checkError() mit Rückgabetyp boolean darüber informieren, ob ein
Fehler aufgetreten ist (Rückgabewert true) oder nicht (Rückgabewert false).
160 Kapitel 3 Elementare Sprachelemente

Log. Ausdr. 1

true false

Anweisung 1

Log. Ausdr. 2

true false

Anweisung 2

Log. Ausdr. 3

true false

Anweisung 3 Anweisung 4

realisiert z. B. eine if-else - Konstruktion nach diesem Muster:


if (Logischer Ausdruck 1)
Anweisung 1
else if (Logischer Ausdruck 2)
Anweisung 2
. . .
. . .
else if (Logischer Ausdruck k)
Anweisung k
else
Default-Anweisung
Wenn alle logischen Ausdrücke den Wert false annehmen, dann wird die else-Klausel zur letzten if-
Anweisung ausgeführt.
Bei einer Mehrfallunterscheidung ist die im Abschnitt 3.7.2.3 vorzustellende switch-Anweisung
gegenüber einer verschachtelten if-else - Konstruktion zu bevorzugen, wenn die Fallzuordnung über
die verschiedenen Werte eines Ausdrucks (z. B. vom Typ int) erfolgen kann.
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 161

Beim Schachteln von bedingten Anweisungen kann es zum genannten dangling-else - Problem1
kommen, wobei ein Missverständnis zwischen Programmierer und Compiler hinsichtlich der Zu-
ordnung einer else-Klausel besteht. Im folgenden Code-Fragment2
if (i > 0)
if (j > i)
k = j;
else
k = 13;
lassen die Einrücktiefen vermuten, dass der Programmierer die else-Klausel auf die erste if-
Anweisung bezogen zu haben glaubt:

i > 0 ?

true false

k = 13;

j > i ?

true false

k = j;

Der Compiler ordnet eine else-Klausel jedoch dem in Aufwärtsrichtung nächstgelegenen if zu, das
nicht durch Blockklammern abgeschottet ist und noch keine else-Klausel besitzt. Im Beispiel be-
zieht er die else-Klausel also auf die zweite if-Anweisung, sodass de facto der folgende Programm-
ablauf resultiert:

1
Deutsche Übersetzung von dangling: baumelnd.
2
Fügt man das Quellcodesegment mit den „fehlerhaften“ Einrücktiefen in ein Editorfenster unserer Entwicklungsum-
gebung IntelliJ ein, dann wird der „Layout-Fehler“ übrigens automatisch behoben. IntelliJ verhindert also, dass der
Logikfehler durch einen „Layout-Fehler“ getarnt wird.
162 Kapitel 3 Elementare Sprachelemente

i > 0 ?

true false

j > i ?

true false

k = j k = 13;

Bei i  0 geht der Programmierer vom neuen k-Wert 13 aus, der beim tatsächlichen Programmab-
lauf jedoch nicht unbedingt zu erwarten ist.
Mit Hilfe von Blockklammern kann man die gewünschte Zuordnung erzwingen:
if (i > 0)
{if (j > i)
k = j;}
else
k = 13;
Eine alternative Lösung besteht darin, auch dem zweiten if eine else-Klausel zu spendieren und
dabei die leere Anweisung zu verwenden:
if (i > 0)
if (j > i)
k = j;
else
;
else
k = 13;
Gelegentlich kommt als Alternative zu einer if-else - Anweisung, die zur Berechnung eines Wertes
bedingungsabhängig zwei unterschiedliche Ausdrücke benutzt, der Konditionaloperator (vgl. Ab-
schnitt 3.5.9) in Frage, z. B.:
if-else - Anweisung Konditionaloperator
double arg = 3, d; double arg = 3, d;
if (arg >= 0) d = arg >= 0 ? arg * arg : 0;
d = arg * arg;
else
d = 0;
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 163

3.7.2.3 switch-Anweisung
Wenn eine Fallunterscheidung mit mehr als zwei Alternativen wie im folgenden Flussdiagramm in
Abhängigkeit vom Wert eines Ausdrucks vorgenommen werden soll,

k = ?

1 2 3

Anweisung 1 Anweisung 2 Anweisung 3

dann ist eine switch-Anweisung weitaus handlicher als eine verschachtelte if-else - Konstruktion.
In Java 14 ist die switch-Syntax erheblich verbessert worden, und mit der bald zu erwartenden Ver-
fügbarkeit kostenfreier LTS-Distributionen von Java 17 besteht kein nennenswertes Argument ge-
gen die Verwendung der modernen switch-Syntax. Wir starten trotzdem mit der traditionellen,
schon angestaubten Syntax, die den Vorteil der maximalen Kompatibilität besitzt, also z. B. mit
einer JVM auf dem Stand von Java 8 genutzt werden kann.

3.7.2.3.1 Traditionelle Syntax


Als Datentyp für den steuernden Ausdruck, den man auch als switch-Argument oder als switch-
Selektor bezeichnet, erlaubt die die traditionelle Syntax:
• Integrale primitive Datentypen mit maximal 4 Bytes:
byte, short, char oder int (nicht long!)
• Verpackungsklassen (siehe Abschnitt 5.3) für integrale primitive Datentypen mit maximal 4
Bytes:
Byte, Short, Character oder Integer (nicht Long!)
• Aufzählungstypen (siehe Abschnitt 5.4)
• Ab Java 7 sind auch Zeichenfolgen (Objekte der Klasse String) erlaubt.
Wegen ihrer großen Variabilität wird die switch-Anweisung mit einem Syntaxdiagramm beschrie-
ben. Wer die Syntaxbeschreibung im Quellcode-Layout bevorzugt, kann ersatzweise einen Blick
auf die gleich folgenden Beispiele werfen.
164 Kapitel 3 Elementare Sprachelemente

switch-Anweisung

switch ( switch-Argument ) {

case Marke : Anweisung break ;

default : Anweisung }

Weil später noch ein praxisnahes (und damit auch etwas kompliziertes) Beispiel folgt, ist hier ein
ebenso einfaches wie sinnfreies Exemplar zur Erläuterung der Syntax angemessen:
Quellcode Ausgabe
class Prog { Fall 2 (mit Durchfall)
public static void main(String[] args) { Fälle 3 und 4
int zahl = 2;
final int marke1 = 1;
switch (zahl) {
case marke1:
System.out.println("Fall 1 (mit break-Stopper)");
break;
case marke1 + 1:
System.out.println("Fall 2 (mit Durchfall)");
case 3:
case 4:
System.out.println("Fälle 3 und 4");
break;
default:
System.out.println("Restkategorie");
}
}
}

Als case-Marken sind konstante Ausdrücke erlaubt, deren Wert schon der Compiler ermitteln kann
(Literale, finalisierte Variablen oder daraus gebildete Ausdrücke). Anderenfalls könnte der Compi-
ler z. B. nicht verhindern, dass mehrere Marken denselben Wert haben. Außerdem muss der Daten-
typ einer Marke natürlich kompatibel zum deklarierten Typ des switch-Arguments sein.
Stimmt beim Ablauf des Programms der Wert des switch-Arguments mit einer case-Marke überein,
dann wird die zugehörige Anweisung ausgeführt, ansonsten (falls vorhanden) die default-Anwei-
sung.
Nach der Ausführung einer Anweisung mit passender Marke wird die switch-Konstruktion nur
dann verlassen, wenn der Fall mit einer break-Anweisung abgeschlossen wird, oder wenn kein wei-
terer Fall mehr folgt. Ansonsten werden auch noch die Anweisungen der nächsten Fälle (ggf. inkl.
default) ausgeführt, bis der „Durchfall“ nach unten entweder durch eine break-Anweisung ge-
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 165

stoppt wird, oder die switch-Anweisung endet. Mit dem etwas gewöhnungsbedürftigen Durchfall-
Prinzip kann man für geeignet angeordnete Fälle mit wenig Schreibaufwand kumulative Effekte
kodieren, aber auch ärgerliche Programmierfehler durch vergessene break-Anweisungen produzie-
ren.
Neben der break-Anweisung stehen noch zwei weitere, bisher der Einfachheit verschwiegene Opti-
onen zum vorzeitigen Verlassen einer switch-Anweisung zur Verfügung, die Sie im weiteren Ver-
lauf des Kurses kennenlernen werden:
• return-Anweisung
Über die return-Anweisung (siehe Abschnitt 4.3.1.2) wird nicht nur die switch-Anweisung,
sondern auch die Methode verlassen, was im Fall der Methode main() einer Beendigung des
Programms gleichkommt.
• Werfen eines Ausnahmefehlers
Auch über das Werfen eines Ausnahmefehlers (siehe Kapitel 11) kann eine switch-
Anweisung verlassen werden, wobei das weitere Verhalten des Programms davon anhängt,
ob und wo der Ausnahmefehler aufgefangen wird.
Soll für mehrere Werte des switch-Arguments dieselbe Anweisung ausgeführt werden, setzt man
die zugehörigen case-Marken (inklusive Schlüsselwort case) hintereinander und lässt die Anwei-
sung auf die letzte Marke folgen. Leider gibt es keine Möglichkeit, eine Serie von Fällen durch An-
gabe der Randwerte (z. B. von a bis k) festzulegen. In Java 14 und Java 17 wird die Möglichkeit,
für mehrere Fälle dieselbe Behandlung anzuordnen, sukzessive verbessert (siehe Abschnitte
3.7.2.3.2 und 3.7.2.5).
Das folgende Beispielprogramm analysiert die Persönlichkeit des Benutzers anhand seiner Farb-
und Zahlpräferenzen. Während bei einer Vorliebe für Rot oder Schwarz die Diagnose sofort fest-
steht, wird bei den restlichen Farben auch noch die Lieblingszahl berücksichtigt:
class PerST {
public static void main(String[] args) {
String farbe = args[0].toLowerCase();
int zahl = Integer.parseInt(args[1]);
switch (farbe) {
case "rot":
System.out.println("Sie sind durchsetzungsfreudig und impulsiv.");
break;
case "schwarz":
System.out.println("Nehmen Sie nicht alles so tragisch.");
break;
default:
System.out.println("Ihre Emotionalität ist unauffällig.");
if (zahl%2 == 0)
System.out.println("Sie haben einen geradlinigen Charakter.");
else
System.out.println("Sie machen wohl gerne krumme Touren.");
}
}
}

Das Programm PerST demonstriert nicht nur die switch-Anweisung (hier mit einem steuernden
Ausdruck vom Typ String), sondern auch den Zugriff auf Programmargumente über den String[]
- Parameter der main() - Methode. Benutzer des Programms sollen beim Start ihre bevorzugte Far-
be sowie ihre Lieblingszahl über Programmargumente (Kommandozeilenparameter) angeben. Wer
z. B. die Farbe Blau und die Zahl 17 bevorzugt, sollte das Programm folgendermaßen starten:
>java PerST Blau 17
166 Kapitel 3 Elementare Sprachelemente

Im Programm wird jeweils nur eine Anweisung benötigt, um ein Programmargument in eine
String- bzw. int-Variable zu befördern. Die zugehörigen Erklärungen werden Sie mit Leichtigkeit
verstehen, sobald Methodenparameter sowie Arrays und Zeichenfolgen behandelt worden sind. An
dieser Stelle greifen wir späteren Erläuterungen mal wieder etwas vor (hoffentlich mit motivieren-
dem Effekt):
• Bei einem Array handelt es sich um ein Objekt, das eine Serie von Elementen desselben
Typs aufnimmt, auf die man per Index, d .h. durch die mit eckigen Klammern begrenzte
Elementnummer, zugreifen kann.
• In unserem Beispiel kommt ein Array mit Elementen vom Datentyp String zum Einsatz,
wobei es sich um Zeichenfolgen handelt. Literale mit diesem Datentyp sind uns schon öfter
begegnet (z. B. "Hallo").
• Über die Parameterliste kann man eine Methode mit Daten versorgen und/oder ihre Ar-
beitsweise beeinflussen.
• Die main() - Methode einer Startklasse besitzt einen (ersten und einzigen) Parameter vom
Datentyp String[] (Array mit String-Elementen). Der Datentyp dieses Parameters ist fest
vorgegeben, sein Name ist jedoch frei wählbar (im Beispiel: args). In der Methode main()
kann man auf args genauso zugreifen wie auf eine lokale Variable.
• Beim Programmstart werden der Methode main() von der Java Virtual Machine (JVM) als
Elemente des String[] - Arrays args die Programmargumente übergeben, die der Anwen-
der beim Start hinter den Namen der Startklasse, jeweils durch Leerzeichen getrennt, in die
Kommandozeile geschrieben hat (siehe obiges Beispiel).
• Das erste Programmargument landet im ersten Element des Zeichenfolgen-Arrays args und
wird mit args[0] angesprochen, weil Array-Elemente mit 0 beginnend nummeriert wer-
den. Als Objekt der Klasse String wird args[0] im Beispielprogramm aufgefordert, die
Methode toLowerCase() auszuführen:
String farbe = args[0].toLowerCase();
Diese Methode erstellt ein neues String-Objekt, das im Unterschied zum angesprochenen
Original auf Kleinschreibung normiert ist, was die spätere Verwendung im Rahmen der
switch-Anweisung erleichtert. Die Adresse dieses Objekts landet als toLowerCase() -
Rückgabewert in der lokalen String-Referenzvariablen farbe.
• Das zweite Element des Zeichenfolgen-Arrays args (mit der Nummer 1) enthält das zweite
Programmargument (falls vorhanden). Zumindest bei kooperativen Benutzern des Beispiel-
programms kann diese Zeichenfolge mit der statischen Methode parseInt() der Klasse Inte-
ger in eine Zahl vom Datentyp int gewandelt und anschließend der lokalen Variablen zahl
zugewiesen werden:
int zahl = Integer.parseInt(args[1]);
Nach einem Programmstart mit dem Aufruf
>java PerST Blau 17
landet der String-Array args als Objekt im Heap-Bereich des programmeigenen Speichers:1

1
Hier wird aus didaktischen Gründen ein wenig gemogelt: Die beiden Zeichenfolgen sind selbst Objekte und liegen
„neben“ dem Array-Objekt auf dem Heap. Die Array-Elemente sind Referenzen, die auf die zugehörigen String-
Objekte zeigen.
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 167

Heap

args[0] args[1]

B l a u 1 7

Damit ein Java-Programm innerhalb unserer Entwicklungsumgebung ausgeführt werden kann, wird
eine Run/Debug Configuration benötigt. Eine solche wird vom Assistenten für ein neues Intel-
liJ-Projekt automatisch angelegt, und wir hatten bisher kaum einen Anlass zur Nachbesserung (vgl.
Abschnitt 3.1.2). Für das oben vorgestellte Programm PerST müssen allerdings per Ausführungs-
konfiguration die vom Benutzer beim Programmstart übergebenen Argumente simuliert werden,
sodass die automatisch erstellte Ausführungskonfiguration zu erweitern ist.
Wenn wir das Drop-Down - Menü zur Ausführungskonfiguration öffnen und das Item Edit Con-
figurations

wählen, dann können wir im folgenden Dialog u. a. die gewünschten Programmargumente eintra-
gen

und der Ausführungskonfiguration einen neuen Namen geben.


Anschließend lässt sich das Programm innerhalb der Entwicklungsumgebung mit Kommandozei-
lenargumenten ausführen, z. B.:
168 Kapitel 3 Elementare Sprachelemente

3.7.2.3.2 Verbesserte Syntax (ab Java 14)


Mit Java 14 haben signifikante Verbesserungen der switch-Syntax in den Java-Sprachstandard Ein-
zug gehalten. Davon sind in erster Linie die im Abschnitt 3.7.2.4 beschriebenen switch-Ausdrücke
betroffen, doch auch die switch-Anweisungen profitieren von zwei Verbesserungen:
• Fallbearbeitung ohne Durchfall
Vor den Anweisungen zu den Fällen darf statt des Doppelpunkts auch der von Lambda-
Ausdrücken (siehe Kapitel Abschnitt 12) bekannte Pfeil (->) stehen. Bei Verwendung der
Pfeilsyntax findet kein Durchfall zu späteren Fällen statt. Die break-Anweisung ist bei
Verwendung der Pfeilsyntax überflüssig und verboten.
• Vereinfachte Auflistung mehrerer Werte zu einem Fall
Soll für mehrere Werte des switch-Arguments dieselbe Anweisung ausgeführt werden, dann
listet man die Werte hinter dem Schlüsselwort case auf. Das Schlüsselwort case muss also
nicht wiederholt werden. Die Auflistung von Werten lässt sich mit der Doppelpunkt- und
mit der Pfeilsyntax kombinieren.
Das folgende Programm demonstriert die Verbesserungen der switch-Anweisung:
Quellcode Ausgabe
class SwitchJ14 { Fall 1,
public static void main(String[] args) throws Exception { OHNE Durchfall
int zahl = 11;
final int marke1 = 1;
switch (zahl) {
case marke1 ->
System.out.println("Fall 1, \nOHNE Durchfall");
case 2, 3, 4 ->
System.out.println("Fälle 2, 3 und 4");
default ->
System.out.println("Restkategorie");
}
}
}

Die Doppelpunktsyntax ist in Java weiterhin erlaubt, darf aber nicht mit der Pfeilsyntax gemischt
werden. Ein Grund für die Verwendung der Doppelpunktsyntax besteht z. B. dann, wenn aus-
nahmsweise ein Durchfall tatsächlich erwünscht ist.
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 169

Ein erfolgreich unter Verwendung der modernen switch-Syntax übersetztes Programm kann z. B.
von einer JVM mit der Version 8 nicht ausgeführt werden:
UnsupportedClassVersionError: Prog has been compiled by a more recent version of the
Java Runtime (class file version 58.0), this version of the Java Runtime only
recognizes class file versions up to 52.0

3.7.2.4 switch-Ausdruck (ab Java 14)


Als Alternative zur switch-Anweisung (siehe Abschnitt 3.7.2.3) wurde in Java 14 der switch-
Ausdruck offiziell eingeführt, nachdem dieses Sprachmerkmal seit Java 12 im Vorschaumodus ver-
fügbar war.1
Den Datentyp eines switch-Ausdrucks ermittelt der Compiler aus der Verwendung (z. B. in einer
Wertzuweisung oder in einer return-Anweisung, siehe Abschnitt 4.3.1.2). Zu jedem Wert des
switch-Arguments ist ein Wert mit einem kompatiblen Datentyp zu liefern.2
Wie bei der switch-Anweisung kann man auch beim switch-Ausdruck seit Java 14 die Falldefiniti-
onen mit einem Doppelpunkt oder mit einem Lambda-Pfeil von den Fallbehandlungen trennen. Im
folgenden Beispiel wird der Doppelpunkt verwendet, und die Ergebniswerte zu den Fällen werden
über eine yield-Anweisung geliefert:
Quellcode Ausgabe
class SwitchJ14 { Fall 1,
public static void main(String[] args) { OHNE Durchfall
int zahl = 1;
final int marke1 = 1;

String swr = switch(zahl) {


case marke1:
yield "Fall 1,\nOHNE Durchfall";
case 2, 3, 4:
yield "Fälle 2, 3 und 4";
default:
yield "Restkategorie";
};
System.out.println(swr);
}
}

Im Beispiel wird der switch-Ausdruck im Rahmen einer Variablendeklarationsanweisung mit Ini-


tialisierung verwendet, die mit einem Semikolon enden muss. Für den switch-Ausdruck resultiert
der Datentyp String.
An der Stelle einer einfachen yield-Anweisung kann auch ein Anweisungsblock stehen, der mit
einer yield-Anweisung endet, z. B.:

1
Wir befinden uns gerade im Abschnitt über Anweisungen, und die switch-Ausdrücke hätten eigentlich im Abschnitt
3.5 behandelt werden müssen. Trotz dieses Arguments ist eine Behandlung der switch-Ausdrücke nach den traditio-
nellen (und noch stark verbreiteten) switch-Anweisungen aber didaktisch sinnvoller.
2
Statt für einen Fall einen Wert zu liefern, darf man aber auch mit der throw-Anweisung eine Ausnahme werfen
(siehe Kapitel 11 über die Ausnahmebehandlung).
170 Kapitel 3 Elementare Sprachelemente

String swr1 = switch (zahl) {


case marke1:
yield "Fall 1, \nOHNE Durchfall";
case 2, 3, 4:
yield "Fälle 2, 3 und 4";
default:
{System.out.print("default: "); yield "Restkategorie";}
};
Die yield-Anweisung muss am Ende des Blocks stehen, weil sie zum Verlassen des switch-
Ausdrucks führt. In einem Block hinter der yield-Anweisung stehende Anweisungen wären nicht
erreichbar.
Im Vergleich zur Verwendung der Doppelpunktsyntax in einer switch-Anweisung bestehen zwei
Unterschiede:
• Der für jeden Fall obligatorische Wert ist per yield-Anweisung anzugeben.
• Es muss kein Durchfall per break verhindert werden, wobei man allerdings von einer
Durchfall-Prävention per yield sprechen könnte.
Für einen switch-Ausdruck ist in der Regel die Lambda- oder Pfeilsyntax gegenüber der Doppel-
punktsyntax zu bevorzugen. Man schreibt den Ausdruck zu einem Fall unmittelbar hinter den Pfeil
und erspart sich die yield-Anweisung:
Quellcode Ausgabe
class SwitchJ14 { Fall 1 (OHNE Durchfall)
public static void main(String[] args) throws Exception {
int zahl = 1;
final int marke1 = 1;
String swr = switch (zahl) {
case marke1 -> "Fall 1 (OHNE Durchfall)";
case 2, 3, 4 -> "Fälle 2, 3 und 4";
default -> "Restkategorie";
};
System.out.println(swr);
}
}

An Stelle eines Ausdrucks darf auf einen Pfeil aber auch ein Anweisungsblock folgen, wobei dann
per yield-Anweisung der Wert zum Fall geliefert werden muss, z. B.:
String swr = switch (zahl) {
case marke1 -> "Fall 1 (OHNE Durchfall)";
case 2, 3, 4 -> "Fälle 2, 3 und 4";
default -> {System.out.print("default: "); yield "Restkategorie";}
};

Weil ein switch-Ausdruck zu jedem möglichen Wert des switch-Arguments ein Ergebnis liefern
muss (Exhaustivität), bestehen folgende Besonderheiten im Vergleich zur switch-Anweisung:
• In der Regel ist ein default-Fall erforderlich. Eine Ausnahme von dieser Regel erlaubt der
Compiler bei einem switch-Argument mit Aufzählungstyp, weil dann eine endliche Anzahl
bekannter Werte vorliegt (siehe Abschnitt 5.4).
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 171

• Ein Ausstieg aus einem switch-Ausdruck per break oder continue (siehe Abschnitt 3.7.3.5)
oder per return (siehe Abschnitt 4.3.1.2) ist verboten. Ein Ausstieg per Ausnahmefehler ist
aber möglich (siehe Kapitel 11), z. B.:

3.7.2.5 Mustervergleich (Vorschau in Java 17)


Der in Java 17 eingeführten Mustervergleich (engl.: pattern matching) erlaubt bei switch-
Anweisungen und -Ausdrücken eine flexible Falldefinition. Für das switch-Argument wird in der
Regel ein breiter Referenzdatentyp verwendet, zu dem viele Ableitungen (Spezialisierungen) exis-
tieren. Die switch-Fälle werden nicht durch eine Konstante oder eine Liste von Konstanten defi-
niert, sondern durch einen Referenztyp, der optional durch einen logischen Ausdruck näher be-
stimmt werden kann.
Im folgenden Beispiel mit einem switch-Ausdruck vom Typ boolean hat das Argument den maxi-
mal breiten (allgemeinen) Typ Object:
class Prog {
public static void main(String[] args) {
Object s = "123";
boolean result = switch (s) {
case String a -> a.length() == 3;
case Integer i -> i >= 5 && i <= 10;
default -> false;
};
System.out.println(result);
}
}

Von dem zu einem Fall gehörenden Referenzdatentyp wird eine sogenannte Mustervariable dekla-
riert, die nach einem erfolgreichen Typtest eine Kopie der Objektadresse erhält. Im ersten Fall des
Beispiels werden Zeichenfolgen (String-Objekte) behandelt:
case String a -> a.length() == 3;
Im Ausdruck zur Ermittlung der Rückgabe steht die Mustervariable zur Verfügung. Die Gültigkeit
einer Mustervariablen ist auf das switch-Muster beschränkt, in dem sie definiert wird.
Im zweiten Fall des Beispiels werden int-Werte (verpackt in Integer-Objekte, siehe Abschnitt 5.3)
versorgt:
case Integer i -> i >= 5 && i <= 10;
Wenn die bisher in den Ergebnisausdrücken untergebrachten Bedingungen in die case-Definitionen
wandern, wird der switch-Ausdruck übersichtlicher:
172 Kapitel 3 Elementare Sprachelemente

boolean result = switch (s) {


case String a && a.length() == 3 -> true;
case Integer i && i >= 5 && i <= 10 -> true;
default -> false;
};
Man kann also ein Typmuster durch einen logischen Ausdruck ergänzen und dabei die Mustervari-
able verwenden. So ist es z. B. möglich, in die Falldefinition zu einem numerischen Argument eine
Bereichsangabe aufzunehmen.
Im logischen Ausdruck dürfen auch Klammern zur Steuerung der Auswertungsreihenfolge auftreten
(vgl. Abschnitt 3.5.10), z. B.:
case Integer i && i%2 == 0 && (i < 10 || i > 100) -> true;
Für die beschriebenen Muster werden gelegentlich die folgenden Begriffe verwendet:
• Typmuster (engl.: type pattern), z. B.:
case Integer i -> i >= 5 && i <= 10;
• Geschütztes Muster (engl.: guarded pattern), z. B.:
case Integer i && i >= 5 && i <= 10 -> true;
• Klammermuster (engl.: parenthesized pattern), z. B.:
case Integer i && i%2 == 0 && (i < 10 || i > 100) -> true;

Bei switch-Ausdrücken ist generell die Exhaustivität gefordert, und der Compiler stellt auch bei
Mustervergleichen sicher, dass alle möglichen Werte des switch-Arguments versorgt sind. Zur De-
finition eines Falles für sonstige Werte bestehen zwei Optionen, die nicht gleichzeitig erlaubt sind:
• default-Fall (siehe obige Beispiele)
• ein Fall mit dem sogenannten totalen Muster (engl.: total pattern), z. B.:
boolean result = switch (s) {
case String a && a.length() == 3 -> true;
case Integer i && i >= 5 && i <= 10 -> true;
case Object obj -> false;
};
In die Definition des totalen Musters gehen leider Begriffe aus dem Kapitel 7 über die Ver-
erbung und aus dem Kapitel 8 über generische Typen ein. Ein Typmuster
Tt
ist total für einen Typ S, wenn die Typlöschung von S eine Spezialisierung der Typlöschung
von T ist.1
Zur Komplexität der Exhaustivitäts-Konsequenzen für Typmuster leisten auch die in Java 17 einge-
führten versiegelten Typen (siehe Abschnitt 7.11.3) einen Beitrag, der hier noch nicht beschrieben
werden kann.
Weil ein switch-Ausdruck nach einem zutreffenden Fall verlassen wird, darf kein Fall spezieller als
ein vorheriger Fall definiert sein. Der Compiler überwacht die verbotene Dominanz durch einen
früheren Fall, z. B.:

1
https://docs.oracle.com/en/java/javase/17/language/pattern-matching-switch-expressions-and-statements.html
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 173

Gupta (2021) weist darauf hin, dass der Compiler bei einer analogen Lösung durch verschachtelte
if-Anweisungen keine Dominanzüberwachung vornimmt.1
Während der Wert null für den switch-Ausdruck vor Java 17 unweigerlich zu einer NullPointer-
Exception geführt hat, kann dieser Wert nun berücksichtigt werden. Ein totales Muster (nicht aber
der default-Fall) bezieht den Wert null mit ein. Außerdem ist das Schlüsselwort null zur Falldefini-
tion erlaubt, um den Wert null exklusiv zu behandeln, z. B.:
int resultEx = switch (s) {
case String a && a.length() == 3 -> 1;
case Integer i && i >= 5 && i <= 10 -> 2;
default -> 3;
case null -> -99;
};

In den bisherigen Beispielen wurden Mustervergleiche im Rahmen von switch-Ausdrücken ver-


wendet. Man kann sie aber auch im Rahmen von switch-Anweisungen einsetzen, z. B.:
int resSt;
switch (s) {
case Integer i && i >= 5 && i <= 10 -> resSt = i;
case String a -> resSt = a.length();
default -> resSt = -99;
};

Werden in einer switch-Anweisung die modernen Mustervergleiche mit der traditionellen Doppel-
punkt-Syntax kombiniert, dann führt eine vergessene break-Anweisung zu einer Fehlermeldung
des Compilers statt zum gefürchteten Durchfall, z. B.:

Während der Compiler in einer switch-Anweisung mit Doppelpunktsyntax und einer Falldefinition
durch konstante Werte ein fehlendes break als geplanten Durchfall akzeptieren muss, ist bei einer
Falldefinition durch Muster ein Durchfall als Programmierfehler zu reklamieren. Im Beispiel würde
ein Integer-Objekt als String-Objekt angesprochen, könnte aber die Methode length() nicht aus-
führen.
Seit Java 17 wird von einer erweiterten switch-Anweisung (engl.: enhanced switch-statement) ge-
sprochen wenn eine von den beiden folgenden, seit Java 17 möglichen Bedingungen erfüllt ist:2
• Der Argumenttyp stammt nicht aus der traditionellen Typenliste (byte, short, char oder int,
Verpackungsklassen zu den vorgenannten Typen, Aufzählungstypen, String).
• Mindestens ein Fall ist durch ein Typmuster oder durch das Schlüsselwort null definiert.
Für eine erweiterte switch-Anweisung wird die Exhaustivität verlangt, z. B.:

Die Begründung für das neue Verhalten des Compilers ist mäßig überzeugend:

1
https://blog.jetbrains.com/idea/2021/09/java-17-and-intellij-idea/
2
https://docs.oracle.com/javase/specs/jls/se17/preview/specs/patterns-switch-jls.html
174 Kapitel 3 Elementare Sprachelemente

This is often the cause of difficult to detect bugs, where no switch label applies and the switch
statement will silently do nothing.
Es ist nicht allzu ungewöhnlich, wenn von mehreren bedingt auszuführenden Anweisungen keine
ausgeführt wird, weil keine Bedingung erfüllt ist. Aus Kompatibilitätsgründen fordert Java 17 die
Exhaustivität nur für die erweiterte switch-Anweisung.
Abschließend soll noch einmal herausgestellt werden, dass mit Hilfe von Mustervergleichen endlich
Fälle durch Wertintervalle für numerische switch-Argumente definiert werden können. Die folgen-
de statische Methode bildet jeden double-Wert in Abhängigkeit von seiner Intervallzugehörigkeit
auf eine ganze Zahl ab:
static int mapIntervals(Double dbl) {
return switch (dbl) {
case Double d && d <= 5.0 -> 1;
case Double d && d > 5.0 && d <= 10.0 -> 2;
case Double d && d > 10.0 && d <= 100.0 -> 3;
default -> 4;
};
}
Selbstverständlich kann man diese Methode auch durch verschachtelte if-else - Anweisungen reali-
sieren, z. B.:
static int mapIntervals(Double dbl) {
if (dbl <= 5.0)
return 1;
else if (dbl <= 10.0)
return 2;
else if (dbl <= 100)
return 3;
else
return 4;
}
Auf analoge Weise lassen sich Mustervergleiche generell durch eine traditionelle Syntax ersetzen,
wobei aber in der Regel die Lesbarkeit leidet und das Fehlerrisiko steigt.
Die in Java 17 eingeführten Mustervergleiche für switch-Anweisungen und -Ausdrücke haben noch
Vorschaustatus, d. h.:
• In späteren Java-Versionen können Details geändert werden. Prinzipiell könnten die switch-
Mustervergleiche wieder komplett aus dem Java-Sprachumfang entfernt werden.
• Weil die Mustervergleiche noch den Vorschaustatus besitzen, sind sie per Voreinstellung
blockiert und müssen beim Übersetzen sowie beim Ausführen eines Programms über Kom-
mandozeilenoptionen freigegeben werden, z. B.:
>javac.exe --release 17 --enable-preview Prog.java
>java.exe --enable-preview Prog
In IntelliJ 2021.2 müssen wir uns nicht um Kommandozeilenoptionen kümmern, doch ist
eine Experimental Feature Alert - Anfrage zu akzeptieren, nachdem für ein Projekt über
File > Project Structure > Project > Project language level
das Sprachniveau auf
17 (Preview) - Pattern matching for switch
gesetzt worden ist, z. B.:
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 175

3.7.3 Wiederholungsanweisung
Eine Wiederholungsanweisung (oder schlicht: Schleife) kommt dann zum Einsatz, wenn eine (Ver-
bund-)Anweisung mehrfach (eventuell mit systematischer Variation von Details) ausgeführt werden
soll, wobei sich in der Regel schon der Gedanke daran verbietet, die Anweisung entsprechend oft in
den Quelltext zu schreiben.
Im folgenden Flussdiagramm ist ein iterativer Algorithmus zu sehen, der die Summe der quadrier-
ten natürlichen Zahlen von 1 bis 5 berechnet:1

1
Das Verzweigungssymbol sieht aus darstellungstechnischen Gründen etwas anders aus als im Abschnitt 3.7.2, was
aber keine Verwirrung stiften sollte. Obwohl im Beispiel eine Steigerung der Laufgrenze für die Variable i kaum in
Frage kommt, soll an dieser Stelle das Thema Ganzzahlüberlauf (vgl. Abschnitt 3.6.1) in Erinnerung gerufen wer-
den. Weil die Variable i vom Typ long ist, kann der Algorithmus bis zur Laufgrenze 3037000499 verwendet wer-
den. Für größere i-Werte tritt beim Ausdruck i*i ein Überlauf auf, und das Ergebnis ist unbrauchbar. Eine einfa-
che Möglichkeit zur Steigerung der maximalen sinnvollen Laufgrenze besteht darin, für eine Berechnung der Sum-
manden per Gleitkommaarithmetik zu sorgen:
(double) i * i
176 Kapitel 3 Elementare Sprachelemente

double s = 0.0;

long i = 1;

false
i <= 5 ?

true

s += i*i;

i++;

Zur Realisation von iterativen Algorithmen enthält Java verschiedene Wiederholungsanweisungen


(jeweils bestehend aus einer Schleifensteuerung und einer wiederholt auszuführenden Anweisung),
die später in eigenen Abschnitten behandelt und hier mit vereinfachter Beschreibung im Überblick
präsentiert werden:
• Zählergesteuerte Schleife (for)
Bei der Ablaufsteuerung kommt eine Zähl- oder Laufvariable zum Einsatz, die vor dem
ersten Schleifendurchgang initialisiert und nach jedem Durchlauf aktualisiert (z. B. inkre-
mentiert) wird. Die zur Schleife gehörige (Verbund-)Anweisung wird ausgeführt, solange
die Zählvariable einen festgelegten Grenzwert nicht überschritten hat.
• Iterieren über die Elemente einer Kollektion (for)
Seit der Java-Version 5 (alias 1.5) ist es mit einer Variante der for-Schleife möglich, eine
Anweisung für jedes Element eines Arrays oder einer anderen Kollektion (siehe Kapitel 10)
ausführen zu lassen.
• Bedingungsabhängige Schleife (while, do)
Bei jedem Schleifendurchgang wird eine Bedingung überprüft, und das Ergebnis entschei-
det über das weitere Vorgehen:
o true: Die zur Schleife gehörige Anweisung wird ein weiteres Mal ausgeführt.
o false: Die Schleife wird beendet.
Bei der kopfgesteuerten while-Schleife wird die Bedingung vor Beginn eines Durchgangs
geprüft, bei der fußgesteuerten do-Schleife hingegen am Ende. Weil man z. B. nach dem 3.
Schleifendurchgang in keiner anderen Lage ist wie vor dem 4. Schleifendurchgang, geht es
bei der Entscheidung zwischen Kopf- und Fußsteuerung lediglich darum, ob auf jeden Fall
ein erster Schleifendurchgang stattfinden soll (do-Schleife) oder nicht (while-Schleife).
Die gesamte Konstruktion aus Schleifensteuerung und (Verbund-)anweisung stellt in syntaktischer
Hinsicht eine zusammengesetzte Anweisung dar.
Abschnitt 3.7 Anweisungen (zur Ablaufsteuerung) 177

3.7.3.1 Zählergesteuerte Schleife (for)


Die Anweisung einer for-Schleife wird ausgeführt, solange eine Bedingung erfüllt ist, die norma-
lerweise auf eine ganzzah