Java 17
Java 17
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/
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.
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
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
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
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
11 AUSNAHMEBEHANDLUNG 529
11.1 Prävention und Beispielprogramm 530
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
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
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
ANHANG 907
A. Operatorentabelle 907
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.
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
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
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
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 = tut sowie v = tvt
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 = tut sowie (u – v) = tdt
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.
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.
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.:
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:
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:
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
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:
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
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
https://de.wikipedia.org/wiki/Instruktionen_pro_Sekunde
https://en.wikipedia.org/wiki/Instructions_per_second
24 Kapitel 1 Einleitung
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
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
https://jakarta.ee/
2
https://de.wikipedia.org/wiki/Spring_(Framework)
28 Kapitel 1 Einleitung
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
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
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
https://jaxenter.de/java-jdk-release-zyklus-75402
36 Kapitel 2 Werkzeuge zum Entwickeln von Java-Programmen
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.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
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.:
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
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:
• 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.
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
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
Ein derart außer Kontrolle geratenes Konsolenprogramm kann man unter Windows z. B. mit der
Tastenkombination Strg+C beenden.
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:
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
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
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
Dann bittet JetBrains um die Erlaubnis, diagnostische Informationen zur Verwendung von IntelliJ
anonym übertragen zu dürfen:
Ggf. wird die Übernahme von Einstellungen einer früheren Version angeboten:
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.
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():
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.
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.
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.:
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.
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.
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.
Ü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
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:
Wir vereinbaren den SDK-Namen OpenJDK 11 und nötigenfalls den folgenden Documentation
Path
https://docs.oracle.com/en/java/javase/11/docs/api/
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:
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.
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.
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:
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) Experimentieren Sie mit dem Hallo-Beispielprogramm aus dem Abschnitt 2.2.1, z. B. indem Sie
weitere Ausgabeanweisungen ergänzen.
3.1 Einstieg
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
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
}
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
Methodenrumpf
{ }
Anweisung
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.
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
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 . ;
*
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
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
Darin bedeuten:
1
Alternativ kann die äquivalente PrintStream-Methode format() benutzt werden.
Abschnitt 3.2 Ausgabe bei Konsolenanwendungen 93
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
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
}
}
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:
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 mit einem einzigen Buchstaben sollten nur in speziellen Fällen verwendet
werden (z. B. als Laufvariable von Wiederholungsanweisungen, siehe unten).
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
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.
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
Bruch@87a5cc 0 1
b2 Bruch-Objekt
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.
• 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;
}
}
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,40282351038
IEEE-754 mit einer Genauigkeit von Maximum: 1 für das Vorz.,
8 für den Expon.,
mindestens 7 Dezimalstellen in der 3,40282351038 23 für die Mantisse
Mantisse. Kleinster positiver Betrag:
Beispiel: 1.4012984610-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,797693134862315710308
IEEE-754 (64 Bit) mit einer Genauig- Maximum: 1 für das Vorz.,
11 für den Expon.,
keit von mindestens 15 signifikanten 1,797693134862315710308 52 für die Mantisse
Dezimalstellen in der Mantisse. Kleinster positiver Betrag:
Beispiel: 4,940656458412465410-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.
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,4012984610-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,17510-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,87747210-39 (-1)1 2-126 2-1 1 00000000 10000000000000000000000
1,40129810-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.
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.
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
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
{ 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
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
hervorgehobene Endklammer
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
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
oktal 0 System.out.println(011); 9
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:
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;
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
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
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
Der Übernahme in das Projekt bzw. Modul Prog, das zum Üben von diversen elementaren Spra-
chelementen dient, kann zugestimmt werden:
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
Nun können die statischen Methoden der Klasse Simput im Projekt genutzt werden.
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
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
• 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
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)));
}
}
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,010-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
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.
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.
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.:
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
( 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
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
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
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);
}
}
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);
}
}
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
! 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.
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.
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
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.
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);
}
}
Er ist aus Kompatibilitätsgründen weiterhin erlaubt, bewirkt aber ab Java 17 nur noch eine Compi-
ler-Warnung.
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.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
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.
Log. Ausdruck
true false
Anweisung 1 Anweisung 2
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
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
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.
switch-Anweisung
switch ( switch-Argument ) {
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
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
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
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.:
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
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;
};
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++;