Kapitel 2: Java

Erste Java-Programme

Java ist eine objektorientierte Programmiersprache. Die Modellierung der Realwelt erfolgt durch in Klassenhierarchien zusammengefasste Objekte, beschrieben durch Datenfelder und Methoden. Datenfelder sind zur Klasse oder ihren Objekten gehörende Daten; Methoden sind Anweisungsfolgen, die auf den Datenfeldern operieren, um ihren Zustand zu manipulieren.

Was genau eine Klassenhierarchie ist, klären wir später. Zunächst genügt es zu wissen, dass Klassen Baupläne für Objekte sind.

Ein Programm wird in Java nicht umgangssprachlich beschrieben. Ein Regelwerk und eine Grammatik definieren die Syntax und die Semantik.

Von der Klasse zur Anweisung

Geben Sie folgenden Code in Ihrem Editor ein. Öffnen Sie dazu eine neue Datei namens HelloWorld.java. Achten Sie exakt auf Groß- und Kleinschreibung. Die Zeilennummern schreiben Sie nicht ab. Ich drucke Sie bei Programmcodes, die wir besprechen, mit aus, um mich in den Erläuterungen leichter und unmissverständlich auf bestimmte Programmteile beziehen zu können.

 1/**
 2 * Die Klasse {@code HelloWorld} gibt die Zeichenkette
 3 * "Hello, World!" auf der Standardausgabe im Terminal
 4 * aus.
 5 */
 6public class HelloWorld {
 7    /**
 8     * Hauptprogramm.
 9     * 
10     * @param args nicht genutzt
11     */
12    public static void main(String[] args) {
13        System.out.println("Hello, World!");  // Ausgabe
14    }
15}

Listing 2.1: HelloWorld.java

Wenn wir dieses Programm dem Java-Compiler zur Übersetzung geben, liest dieser den Code von oben nach unten. In /** und */ eingeschlossene Passagen des Codes sind Dokumentationskommentare. Sie dienen dem Java-Werkzeug javadoc, um aus diesen Kommentaren eine Dokumentation im HTML-Format zu generieren. Einzeilige Kommentare beginnen mit // und reichen bis ans Ende der jeweiligen Zeile. Ein einfacher mehrzeiliger Kommentar beginnt mit /* und endet mit */. Er sieht dem Dokumentaionskommentar sehr ähnlich. Der Java-Compiler ignoriert Kommentare. Er stellt dann das Folgende fest:

  • Das Programm besteht aus einer Klasse namens HelloWorld.

  • Innerhalb der Klasse gibt es eine Methode namens main().

  • Innerhalb der Methode steht eine Anweisung.

Die geschweiften Klammern bilden Blöcke. Das sind der Rumpf der Klasse und der Rumpf der main()-Methode. Anweisungen werden in Java mit einem Semikolon abgeschlossen.

In einem Terminalfenster übersetzen Sie das Programm mittels:

$ javac HelloWorld.java
$ _

Wenn das Programm fehlerfrei übersetzt werden kann, erhalten Sie kein Feedback. Erhalten Sie Warn- oder Fehlermeldungen, müssen Sie Ihren Programmcode entsprechend korrigieren. Prüfen Sie dann, ob Sie den Code exakt so, wie hier gezeigt, eingegeben haben.

Ist das Programm fehlerfrei übersetzt, hat der Java-Compiler eine Datei HelloWorld.class erstellt. Diese können Sie jetzt ausführen. Dazu rufen Sie die virtuelle Java-Maschine (JVM) auf und übergeben Ihr den Namen der auszuführenden Klasse.

$ java HelloWorld
Hello, World!
$ _

Das Programm wird ausgeführt und gibt „Hello, World!" im Terminalfenster aus.

Nutzen Sie IntelliJ, können Sie Übersetzung und Ausführung in einem Schritt per Mausklick initiieren. Im Editorfenster klicken Sie die rechte Maustaste. Im erscheinenden Kontextmenü wählen Sie „Run". IntelliJ öffnet dann ein eigenes Konsolenfenster und führt das Programm darin aus.

Hinweis: In IntelliJ erhalten Sie, wenn Sie Java 25 verwenden, einen Hinweis darauf, die mit Java 25 eingeführte kompakte Klassendefinition nutzen zu können. Sie können IntelliJ die Konvertierung durchführen lassen. Alternativ schalten Sie die entsprechenden Inspektionen ab. Sie können die Warnungen aber auch für den Moment ignorieren und später entscheiden, ob Sie die neuen kompakten Klassen nutzen möchten oder nicht. Wenn Sie später entscheiden wollen, lassen Sie die Inspektionen vorerst eingeschaltet, ansonsten schalten Sie sie gegebenenfalls ab. In jedem Fall ist die gezeigte Notation zulässig und bezüglich Java-Versionen vor Java 25 auch die einzig mögliche, ein Java-Programm zu formulieren.

Vorab eine kurze Zusammenfassung dessen, was wir uns im Folgenden näher ansehen: Java-Programme bestehen im Kern aus Anweisungen, die nacheinander ausgeführt werden. Diese Anweisungen werden zu größeren Einheiten zusammengefasst, den Methoden, die wiederum (zuzüglich eventueller Datenfelder) Klassen bilden. Klassen selbst werden in Paketen gesammelt. Mehrere Pakete lassen sich zu Java-Archiven (JAR-Dateien) bündeln und verteilen.

Anweisungen

Java gehört zur Familie der imperativen Programmiersprachen. Das bedeutet, dass wir dem Computer schrittweise konkrete Anweisungen geben, wie ein Algorithmus auszuführen ist. Solche Anweisungen (engl. statements) können unter anderem sein:

  • Methodenaufrufe
  • Fallunterscheidungen, zum Beispiel if
  • Schleifen für Wiederholungen, etwa mit for oder while

Erfassen Sie das folgende Programm MoreStatements.java und übersetzen es. Die Schritte sind ganz analog zu HelloWorld.java.

 1/**
 2 * Implementierung einer Sequenz von Anweisungen
 3 * (hier Ausgabeanweisungen).
 4 */
 5public class MoreStatements {
 6    /**
 7     * Hauptprogramm.
 8     * 
 9     * @param args nicht genutzt
10     */
11    public static void main(String[] args) {
12        System.out.println("Erste Anweisung");
13        System.out.println("Zweite Anweisung");
14    }
15}

Listing 2.2: MoreStatements.java

Achten Sie auf die Ausgabe, wenn Sie das Programm ausführen:

$ java MoreStatements
Erste Anweisung
Zweite Anweisung
$ _

System.out.println() steht offenbar für je eine Anweisung. Anweisungen werden jeweils mit einem Semikolon abgeschlossen.

Probieren Sie aus, dem Programm eine weitere Anweisung hinzuzufügen, zum Beispiel die Ausgabe von „Dritte Anweisung". Was geschieht, wenn Sie die Reihenfolge der Anweisungen im Code ändern?

public class MoreStatements {
    public static void main(String[] args) {
        System.out.println("Erste Anweisung");
        System.out.println("Zweite Anweisung");
        System.out.println("Dritte Anweisung");
    }
}

Richtig - die dritte Anweisung wird ganz analog zu den beiden schon vorhandenen Anweisungen notiert. Es ändert sich nur der auszugebende Text. Sie beobachten, dass die Anweisungen in der Reihenfolge Ihrer Notation im Programmcode ausgeführt werden.

Datentypen, Typisierung, Variablen und Zuweisungen

Java nutzt, wie es für Programmiersprachen typisch ist, Variablen zum Ablegen von Daten. Eine Variable bezeichnet verbal einen reservierten Speicherbereich und belegt, abhängig vom Datentyp, eine feste Anzahl von Bytes.

Alle Variablen haben einen Typ, der in Java zur Übersetzungszeit bekannt ist. Der Typ wird auch Datentyp genannt, da er festlegt, welche Art von Daten eine Variable speichern kann. Beispiele für einfache Datentypen sind: Ganzzahlen, Gleitkommazahlen, Wahrheitswerte und Zeichen. Der Datentyp bestimmt auch die zulässigen Operationen, denn Wahrheitswerte lassen sich nicht addieren, Ganz- und Gleitkommazahlen dagegen schon. Grundsätzlich können Sie mit Zahlen arithmetische Operationen ausführen.

Java ist eine streng typisierte Programmiersprache. Das bedeutet, dass Java zum Übersetzungszeitpunkt eines Programms die notwendigen Informationen benötigt, welche Variable von welchem Datentyp ist. Da sich dieser im Programmablauf nicht verändern kann, kann der Compiler sehr präzise prüfen, ob die einer Variablen zugewiesenen Werte mit dem vereinbarten Datentypen kompatibel sind und die im Programm ausgeführten Operationen auf den jeweils beteiligten Datentypen zulässig sind.

Die strenge Typprüfung mag anfangs als Einschränkung wahrgenommen werden. Der Vorteil ist jedoch, eine frühzeitige Fehlererkennung noch vor Ausführung eines Programms. Datentypfehler sind, bei Programmiersprachen ohne strenge Typprüfung, eine oftmals schwer zu lokalisierende Fehlerquelle. Wenn Sie erst ein wenig Erfahrung mit Java gesammelt haben, werden Sie diese Eigenschaft zu schätzen wissen.

In Java unterscheiden wir verschiedene Gruppen von Datentypen. Das sind primitive Datentypen und Referenzdatentypen. Primitive Datentypen sind:

Tabelle 2.1: Überblick über die primitiven Java-Datentypen

Datentyp Bits Wertbereich
boolean 1 true (wahr) oder false (falsch)
char 16 16-Bit-Unicode-Zeichen (0x0000 …0xFFFF)
byte 8 \((-2)^7\) bis \(2^7-1\)
short 16 \((-2)^{15}\) bis \(2^{15}-1\)
int 32 \((-2)^{31}\) bis \(2^{31}-1\)
long 64 \((-2)^{63}\) bis \(2^{63}-1\)
float 32 \(\pm 3.4\text{e}38\)
double 64 \(\pm 1.7\text{e}308\)

Hinweis: Java selbst macht zum Datentyp boolean keine Angabe zu dessen Speicherbedarf. Um zwei unterschiedliche Werte zu codieren ist 1 Bit jedoch ausreichend. Im Gegensatz zu vielen anderen Programmiersprachen, bei denen Wahrheitswerte kompatibel zu ganzen Zahlen sind, ist das bei Java ganz ausdrücklich nicht so. Man kann einen Wahrheitswert keiner ganzzahligen Variablen zuweisen.

Wir werden diese Datentypen nicht alle sofort im Detail behandeln. Zunächst konzentrieren wir uns auf wenige, besonders wichtige Typen.

Die primitiven Datentypen sind in Java hardwarenah abgebildet. Die JVM stellt jedoch sicher, dass die Datentypen auf jeder Hardware unter jedem Betriebssystem, welches Java unterstützt, die gleiche Größe und den gleichen Wertebereich haben.

Warum gibt es so viele Datentypen für Zahlen? - Je nach Zweck, benötigen Sie mal kleinere oder größere Wertebereiche oder auch Genauigkeiten. Ganze Zahlen haben keine Nachkommastellen. Je größer der Wertebereich und/oder die Genauigkeit, umso mehr Speicher benötigt ein Datentyp.

Ganze Zahlen stellen in Java, je nach Typ, eine begrenzte Teilmenge der ganzen Zahlen dar, die Sie aus dem Mathematikunterricht als Zahlenmenge \(\mathbb{Z}\) kennen. Javas Gleitkommazahlen stellen eine eingeschränkte Annäherung an die Zahlenmenge \(\mathbb{R}\) dar. Welche Einschränkungen das jeweils sind, sehen wir uns an, wenn wir die einzelnen Datentypen im Detail besprechen.

Zur Motivation und um Ihnen das Arbeiten mit Variablen und Datentypen näher zu bringen, betrachten wir das folgende Programm zur Berechnung des BMI einer Person. BMI steht für Body-Mass-Index. Die Formel zu seiner Berechnung lautet:

\[\text{BMI} = \frac{\text{Körpergewicht [kg]}}{\text{Körpergröße [m]}^2}\]

BMI.java berechnet den Body-Mass-Index einer Person. Das Programm entspricht dem bekannten Aufbau. Wir haben jetzt jedoch ein paar mehr Anweisungen. Das sind Variablenvereinbarungen, Zuweisungen und eine Ausgabe.

 1/**
 2 * BMI-Berechnung (Body Mass Index) für Werte, die fix im
 3 * Code codiert sind.
 4 */
 5public class BMI {
 6    /**
 7     * Hauptprogramm.
 8     * 
 9     * Ausgabe eines Namens und eines berechneten BMI. Alle
10     * Informationen sind fix codiert.
11     * 
12     * @param args nicht genutzt
13     */
14    public static void main(String[] args) {
15        String firstname = "Karsten";      // Definition: Vorname
16        double weight = 82.5;              // Definition: Gewicht
17        double height = 1.80;              // Definition: Größe in Meter
18        double bmi;                        // Deklaration: BMI (ohne Wertzuweisung)
19
20        bmi = weight / (height * height);  // Zuweisung eines Ausdrucks
21
22        System.out.println("BMI von " + firstname + ": " + bmi);
23    }
24}

Listing 2.3: BMI.java

Variablen werden immer in der Form vereinbart, erst den Datentypen zu benennen, dem dann der Variablenname folgt. Das nennen wir eine Variablendeklaration. Optional können wir auch gleich einen Wert zuweisen. Das nennen wir das dann eine Variablendefinition.

Datentyp Variablenname;         // Deklaration
Datentyp Variablenname = Wert;  // Definition

Wird eine Variable lediglich deklariert, nicht definiert, so ist auch ihr Wert undefiniert. Wir dürfen nicht lesend auf sie zugreifen, um sie beispielsweise auszugeben. Das ist erst dann erlaubt, wenn der Variablem ein valider Wert zugewiesen wurde. Eine Zuweisung erfolgt mithilfe des Zuweisungsoperators, der in Java als einfaches Gleichheitszeichen notiert wird.

In Zeile 16 weisen wir der bis dahin lediglich deklarierten Variablen bmi einen Wert zu. Dieser resultiert aus dem arithmetischen Ausdruck weight / (height * height). Anweisungen, die zu einem Wert evaluierbar sind, sind Ausdrücke.

Am Ende, in Zeile 18, geben wir das Ergebnis aus. Hierbei setzen wir die Ausgabe aus konstanten Zeichenketten und Variablen zusammen. Der +-Operator ist in Java nicht nur ein arithmetischer Operator. Er dient auch zum Verknüpfen von Zeichenketten. Wird dabei eine Zeichenkette mit einer Variablen oder auch einem konstanten Wert verknüpft, der keine Zeichenkette ist, so wird diese Variable beziehungsweise der Wert, automatisch seitens Java in eine Zeichenkette umgewandelt, sodass eine Zeichenkettenverknüpfung durchgeführt werden kann.

Variablen

Im Programm BMI.java haben Sie bereits Variablen verwendet. Wir wollen Sie uns aber genauer ansehen. Variablen sind für quasi jedes Programm, welches etwas Sinnvolles tut, essentiell. Wir benötigen Variablen um

  • uns Werte im Programm merken,
  • zu verarbeitende Werte ändern und
  • so verarbeiten zu können.

Der Speicher unserer Computers besteht aus Speicherzellen, die durchnummeriert sind. Die Nummern sind die sogenannten Speicheradressen. Mit den Adressen lässt sich faktisch nicht arbeiten. Wir wissen beim Erfassen des Programmcodes nicht, an welcher Stelle im Hauptspeicher ein Wert abgelegt wird. Sich darum zu kümmern, ist Sache der JVM beziehungsweise des Betriebssystems. Deshalb verwenden wir Variablen. Das sind Bezeichner für Speicherstellen. Welche Speicherstellen das konkret sind, braucht uns nicht zu interessieren. Wir vergeben lediglich einen Variablennamen.

Bei der Vergabe von Namen sind wir relativ frei. Bezeichner bestehen in Java aus Buchstaben, Ziffern und Unterstrichen. Das erste Zeichen darf keine Ziffer sein. Groß-/Kleinschreibung ist in Java signifikant.

Der Variablenname allein ist in Java jedoch nicht hinreichend. Wir müssen Java mitteilen, wie viel Speicherplatz reserviert werden muss. Dazu dient die Angabe des Datentyps. Jeder Datentyp ist mit einer festen Anzahl von Bits, die er im Speicher belegt, verbunden (siehe Tabelle 2.1).

Daneben erfüllt der Datentyp weitere, zentrale Funktionen. Er legt

  • den Wertebereich fest,
  • definiert die zulässigen Operationen
  • und die Notation von Literalen.

Als Literal bezeichnen wir die Notation von Konstanten, konkreten Werten, im Programmcode.

Minimal besteht eine Variablenvereinbarung also aus dem Variablennamen und dem Datentyp, den die Variable repräsentieren soll.

Datentyp Variablenname [, Variablenname_2 [, ...]];

Wie Sie an der Syntax erkennen, wird der Datentyp vor dem Variablennamen notiert. Die eckigen Klammern zeigen an, optional mehrere Variablen gleichzeitig vereinbaren zu können. Sie werden dann als Komma-separierte Liste notiert. Eine Variablenvereinbarung ist eine Anweisung und daher mit einem Semikolon abzuschließen.

Eine Variablenvereinbarung ohne Wertzuweisung nennen wir Variablendeklaration. Wir kündigen lediglich an, irgendwann im Programmcode eine Variable des spezifizierten Namens und Datentyps verwenden zu wollen. Ein deutscher Satz, der dies beschreibt lautet:

Richte Dich darauf ein, dass ich demnächst eine Variable x vom Datentyp y benötige.

Irgendwann wird der Variablen dann ein Wert zugewiesen. Bis dahin ist sie undefiniert. Bei der Zuweisung wird dann auch erst der erforderliche Speicherplatz reserviert und zugewiesen. Der Wert wird im zugewiesenen Speicherplatz abgelegt.

Anders sieht es bei der Variablendefinition aus. Deklaration und Wertzuweisung erfolgen in einer Anweisung.

Datentyp Variablenname = Wert;

Hierbei wird direkt auch der Speicherplatz reserviert und zugewiesen. Der angegebene Wert wird im zugewiesenen Speicherplatz abgelegt.

Variablendeklarationen und Variablendefinitionen dürfen in einer Anweisung vermischt genutzt werden.

Haben wir auf diese Weise dafür gesorgt, uns Werte in unserem Programm merken zu können, können wir die Werte aus den Variablen auslesen, mit den Werten arbeiten, sie modifizieren. – Wir haben das in BMI.java bereits genutzt.

Hier noch ein weiteres Beispielprogramm, welches sich ohne die Nutzung von Variablen nicht realisieren lassen würde. Das Programm Ruler.java gibt die relativen Längen der Teilstriche eines Lineals aus.

Das Programm definiert fünf Variablen: ruler1 bis ruler5. Diese Variablen sind jeweils vom Datentyp String, repräsentieren also jeweils eine Zeichenkette. Die Variablen sind definiert, nicht nur deklariert, weil ihnen jeweils direkt ein Wert zugewiesen wird. Dabei nutzen wir ab ruler2 die Möglichkeit, nicht nur Literale zuweisen zu können. Wir weisen Ausdrücke, bestehend aus Literalen und Variablen, in Form von Zeichenkettenverknüpfungen zu.

 1/**
 2 * Statische Implementierung der Linealfunktion.
 3 * 
 4 * Die Klasse {@code Ruler} gibt die relativen Längen der
 5 * Teilstriche auf einem Lineal aus.
 6 */
 7public class Ruler {
 8    /**
 9     * Ausgabe von 5 Linealen.
10     * 
11     * @param args nicht genutzt
12     */
13    public static void main(String[] args) {
14        String ruler1 = "1";                      // Länge:  1
15        String ruler2 = ruler1 + " 2 " + ruler1;  // Länge:  3
16        String ruler3 = ruler2 + " 3 " + ruler2;  // Länge:  7
17        String ruler4 = ruler3 + " 4 " + ruler3;  // Länge: 15
18        String ruler5 = ruler4 + " 5 " + ruler4;  // Länge: 31
19
20        // Erkenntnis: n Ausgaben haben die Länge 2^n - 1
21
22        System.out.println(ruler1);
23        System.out.println(ruler2);
24        System.out.println(ruler3);
25        System.out.println(ruler4);
26        System.out.println(ruler5);
27    }
28}

Listing 2.4: Ruler.java

Interessant an diesem Programm ist, dass mit wenig Aufwand, sehr lange Zeichenketten generiert werden können. Probieren Sie aus, das Programm um weitere Linealabschnitte zu ergänzen. Sie werden staunen, wie schnell die Ausgabe länger und länger wird.

Bei \(n\) Zeilen ruler-Definition (Linealabschnitte) gibt die Ausgabe \(2^n-1\) Zahlen aus.

Wahrheitswerte

Der einfachste Datentyp in Java ist der Wahrheitswert. Er kennt nur zwei mögliche Ausprägungen. Das sind false (falsch) und true (wahr). Der Datentyp heißt in Java boolean.

Wahrheitswerte kommen im Grunde zwei Rollen bei der Programmierung zu. Zum Einen können wir direkt am Wert interessiert sein. Zum Anderen, das sehen Sie später, benötigen wir Wahrheitswerte, um den Programmablauf zu steuern.

Vorerst betrachten wir nur den Wahrheitswert als solchen. Zu logischen Operatoren und zur Programmablaufsteuerung kommen wir später.

Das folgende Codefragment zeigt, wie Sie beispielsweise im Programm einen Schalter setzen können, um einen Debug-Modus ein- oder auszuschalten:

boolean debugMode = false;  // true, um Debug-Modus einzuschalten

System.out.println("Debug-Modus aktiv: " + debugMode)

Alternativ stellen Sie sich ein Programm LeapYear.java vor. Es soll prüfen, ob ein bestimmtes Jahr ein Schaltjahr war oder ist. Wir werden das noch implementieren, wenn wir logische Verknüpfungen behandeln. Im Moment betrachten Sie nur den folgenden Programmausschnitt.

public class LeapYear {
    public static void main(String[] args) {
        int year = 1900;  // Jahreszahl als ganze Zahl vereinbaren
        boolean isLeap;

        // hier findet die Prüfung statt, ob year ein 
        // Schaltjahr repräsentiert.

        System.out.println(isLeap);
    }
}

Ganze Zahlen

Ganze Zahlen sind die Zahlen, die Sie aus der Mathematik als die Zahlenmenge \(\mathbb{Z}\) kennen. In der Mathematik gibt es unendlich viele ganze Zahlen. Bei Java ist das nicht so. Die ganzen Zahlen sind auf unterschiedliche Wertebereich begrenzt (siehe Tabelle 2.1).

Es stellt sich die Frage, warum es unterschiedliche Typen von ganzen Zahlen in Java gibt. Ein einziger Datentyp mit hinreichendem Wertebereich könnte doch ausreichend sein. Der Grund ist der, dass wir nicht immer große Wertebereiche benötigen. Wenn der Wertebereich \([-128, 127]\) ausreichend ist, verwenden wir den Datentyp byte. Der benötigt im Speicher nur ein Byte. Würden wir dagegen long als Datentyp nutzen, bräuchten wir für jeden zu speichernden Wert 8 Bytes. Das sind 7 Bytes mehr als tatsächlich benötigt. Das wäre Speicherplatzverschwendung. In unserem Desktop-PC oder Notebook mag reichlich Speicher installiert sein, sodass uns die Verschwendung nicht wehtut. Java wird jedoch auch in Mini-Devices, wie Armbanduhren, Kaffeemaschinen usw., eingesetzt. Dort ist regelmäßig nur wenig Speicherplatz vorhanden, sodass man mit dem Speicher haushalten muss. Verschiedene Datentypen sind also durchaus sinnvoll.

Java stellt fünf ganzzahlige Datentypen zur Verfügung, die alle hardwarenah implementiert sind: byte, short, int, long und char. Zeichen gehören in Java zu den ganzzahligen Datentypen.

Literale

Die Zahlen, mit Ausnahme von char, sind vorzeichenbehaftet. Die Literale ganzer Zahlen entsprechen weitestgehend der Notation, die Sie aus dem Alltag kennen. Ein unäres Minuszeichen kennzeichnet negative Zahlen. Standardmäßig werden ganzzahlige Literale als int betrachtet. Soll dem nicht so sein, sind die Literale durch einen angehängten Buchstaben (long) entsprechend zu kennzeichnen oder umzurechnen, wenn ein int einem kleineren Datentypen zugewiesen wird. Die explizite Konvertierung ist auch dann erforderlich, wenn der tatsächliche Wert im Wertebereich des Ziel-Datentyps liegt.

 1/**
 2 * Der Quellcode der Klasse {@code Ints} demonstriert die Verwendung
 3 * der verschiedenen Ganzzahldatentypen in Java.
 4 */
 5public class Ints {
 6    /***
 7     * Hauptprogramm.
 8     * 
 9     * Es werden fix 4 ganzzahlige Variablen definiert und ausgegeben.
10     * 
11     * @param args nicht genutzt
12     */
13    public static void main(String[] args) {
14        int   i = 100;                   // int ist Standard
15        byte  b = (byte)i;               // Byte muss konvertiert werden!      
16        short s = (short)i;              // dto.
17        long  l = 123_456_789_012_345L;  // größer als Integer.MAX_VALUE
18        // 123 Billionen 456 Milliarden 789 Millionen 12 Tausend 345
19
20        // Hinweis: max. Wert von int ist 
21
22        System.out.println("b = " + b);
23        System.out.println("s = " + s);
24        System.out.println("i = " + i);
25        System.out.println("l = " + l);
26    }
27}

Listing 2.5: Ints.java

In der Praxis wird fast immer int verwendet, sodass besondere Kennzeichnungen oder Konvertierungen nicht notwendig sind.

Große Zahlen sind schlecht zu lesen. Java gestattet es deshalb, sie mittels Unterstrichen zu strukturieren.

long l = 1_000_000_000;  // leichter zu lesen als 1000000000

Außerdem dürfen ganze Zahlen nicht nur dezimal, sondern auch in Binär-, Oktal- und Hexadezimalschreibweise notiert werden.

Binäre Literale werden mit vorangestelltem 0b, hexadezimale Literale mit vorangestelltem 0x notiert. Man wollte nun annehmen, oktale Literale würden analog geschrieben. Dem ist aber nicht so. Sie werden lediglich mit einer führenden Null notiert.

 1/** 
 2 * Die Klasse {@code IntBases} demonstriert im Quellcode die
 3 * Notation von ganzzahligen {@code int}-Literalen zu den
 4 * Basen 2, 8, 10 und 16.
 5*/
 6public class IntBases {
 7    /**
 8     * Hauptprogramm. Die dezimale Ganzzahl 42 wird zu verschiedenen
 9     * Basen notiert und dezimal ausgegeben.
10     * 
11     * @param args nicht genutzt
12     */
13    public static void main(String[] args) {
14        int bin = 0b101010;  // Binärzahl
15        int dec = 42;        // Dezimalzahl
16        int oct = 052;       // Oktalzahl
17        int hex = 0x2A;      // Hexadezimalzahl
18
19        System.out.println("bin = " + bin);
20        System.out.println("dec = " + dec);
21        System.out.println("oct = " + oct);
22        System.out.println("hex = " + hex);
23    }
24}

Listing 2.6: IntBases.java

Hier ist die Ausgabe des Programms:

i1 = 42
i2 = 42
i3 = 42
i4 = 42

In der Regel werden Sie die alternativen Literale nicht benötigen. Sie werden vornehmlich die dezimale Notation verwenden.

Operatoren

Die folgende Tabelle gibt einen unvollständigen Überblick über die in Java verfügbaren Operatoren für ganze Zahlen. Es fehlen die Bit-Operationen, die in diesem Seminar nicht behandelt werden und für den Einstieg auch nicht nicht erforderlich sind.

Tabelle 2.2: Operatoren für ganze Zahlen (ohne Bit-Operatoren)

Operator Beschreibung Assoziativität
() Klammer links-rechts
++ Post-Inkrement links-rechts
-- Post-Dekrement
+ unäres Plus rechts-links
- unäres Minus
++ Pre-Inkrement
-- Pre-Dekrement
* / % multiplikative Operatoren links-rechts
+ - additive Operatoren links-rechts
< <= > >= Relation links-rechts
!= == (Un)Gleichheit links-rechts
= += -= Zuweisung rechts-links
*= /= %=

Lassen Sie sich von der Vielzahl der Operatoren nicht erschlagen. Ich habe sie hier nur aufgelistet, um sie nicht häppchenweise einzuführen. Außerdem haben Sie so eine zentrale Stelle im Skript, wo Sie bei Bedarf nachschlagen können.

Das +- und das --Zeichen dienen zudem als unäre Operatoren, um anzuzeigen, ob eine Zahl positiv oder negativ ist. Auf das Pluszeichen kann in der Regel verzichtet werden. Ohne Vorzeichenangabe wird eine Zahl stets als positive Zahl betrachtet.

Für den Moment interessieren wir uns nur für die folgenden Operatoren: Addition (+), Subtraktion (-), Multiplikation (*), Division (/) und Modulo (%). Der Operator liefert den Rest einer Division. Das Programm IntOps.java demonstriert ihre Verwendung.

 1/**
 2 * {@code IntOps} demonstriert die Resultate ausgewählter Operatoren
 3 * für ganzen Zahlen.
 4 * 
 5 * Die Operationen sind fix im Quellcode codiert.
 6 */
 7public class IntOps {
 8    /**
 9     * Hauptprogramm.
10     * <p>
11     * Folgende Operationen werden ausgeführt:
12     * </p>
13     * <ul>
14     * <li><code>42 + 4</code></li>
15     * <li><code>42 - 4</code></li>
16     * <li><code>42 * 4</code></li>
17     * <li><code>42 / 4</code> (<strong>Beachte:</strong> Division durch Null verboten!)</li>
18     * <li><code>42 % 4</code> (<strong>Beachte:</strong> Division durch Null verboten!)</li>
19     * </ul>
20     * 
21     * @param args nicht genutzt
22     */
23    public static void main(String[] args) {
24        int a = 42;
25        int b =  4;
26
27        int sum = a + b;  // Addition (Summe)
28        int dif = a - b;  // Subtraktion (Differenz)
29        int pro = a * b;  // Multiplikation (Produkt)
30        int quo = a / b;  // ganzzahlige Division (Quotient)
31        int rem = a % b;  // Modulo (Restwert, Remainder)
32
33        System.out.println("a + b = " +  sum);
34        System.out.println("a - b = " +  dif);
35        System.out.println("a * b = " +  pro);
36        System.out.println("a / b = " +  quo);
37        System.out.println("a % b = " +  rem);
38    }
39}

Listing 2.7: IntOps.java

Achtung: Ganze Zahlen dürfen nicht durch Null dividiert werden. Wenn Sie es versuchen, bricht Ihr Programm mit einer entsprechenden Fehlermeldung ab.

Übung: Modifizieren sie IntOps.java. Lassen Sie beispielsweise 2 + 3 * 4 berechnen. Im Mathematikunterricht haben Sie die Regel „Punkt-vor-Strichrechnung" kennengelernt. Beachtet Java diese Regel? Probieren Sie auch (2 + 3) * 4. Sie werden bemerken, dass Java genauso rechnet, wie Sie das in der Schule gelernt haben.

Exkurs: Restwertdivision

Wenn Sie an der Hochschule ein sogenanntes MINT-Fach studieren, werden Sie die Ergebnisse, die Java bezüglich der ganzzahligen (Restwert-)Division liefert, verwirren, wenn Dividend und/oder Divisor negative Zahlen sind.

Die Division ganzer Zahlen mit Rest verdient deshalb eine besondere Betrachtung.

 1/**
 2 * Die Klasse {@code Modulo} zeigt die Restwertdivision in Java.
 3 * 
 4 * Die Werte, mit denen gerechnet wird, sind im Quellcode fix
 5 * codiert.
 6 */
 7public class Modulo {
 8    /**
 9     * Hauptprogramm. Die Restwertdivision wird anhand der ganzen Zahlen
10     * 7 und 3 (positiv/negativ) nach der Java-Konvention durchgeführt.
11     * 
12     * @param args nicht genutzt
13     */
14    public static void main(String[] args) {
15        int a = 7, b = 3;  // unsere Werte
16    
17        System.out.println("Java-Konvention:");
18        System.out.println(" " + a + " /  " +    b + " =  " + (   a /    b) + " Rest  " + (   a %    b));
19        System.out.println((-a) + " /  " +    b + " = " + ((-a) /    b) + " Rest " + ((-a) %    b));
20        System.out.println(" " + a + " / " + (-b) + " = " + (   a / (-b)) + " Rest  " + (   a % (-b)));
21        System.out.println((-a) + " / " + (-b) + " =  " + ((-a) / (-b)) + " Rest " + ((-a) % (-b)));
22    }
23}

Listing 2.8: Modulo.java

Führen wir das Programm aus, so erhalten wir die folgende Ausgabe:

$ java Modulo
 7/ 3 =  2 Rest  1
-7/ 3 = -2 Rest -1
 7/-3 = -2 Rest  1
-7/-3 =  2 Rest -1
$ _

Sofern Sie von der an Hochschulen gelehrten Mathematik unberührt sind, nehme ich an, entspricht das Resultat Ihren Erwartungen. Auf der Hochschule, jedenfalls in den MINT-Fächern, wird Restwertdivision anders gelehrt. Betrachten wir hierzu die folgenden Beispiele:

 7/3 =  1 Rest  4
 7/3 =  2 Rest  1
 7/3 =  3 Rest -2
-7/3 = -1 Rest -4
-7/3 = -2 Rest -1
-7/3 = -3 Rest  2

Rechnen Sie nach. Alle Ergebnisse sind im Grunde korrekt. Die Restwert-Division ist scheinbar eine Geschmacksfrage. In der Mathematik haben Geschmacksfragen jedoch nichts zu suchen. Deshalb wird in der Mathematik, zwecks Normierung, eine Konvention angewendet, die Vorzeichen der Reste aus dem Teiler abzuleiten.

 7/ 3 =  2 Rest  1
-7/ 3 = -3 Rest  2
 7/-3 = -3 Rest -2
-7/-3 =  2 Rest -1

Nun aber zurück zum Beispiel mit den negativen Zahlen. Um das obige Resultat mit Java zu erreichen, müssen wir uns Methoden aus der Bibliothek Math bedienen. Die definiert die Methoden floorDiv() und floorMod(), die sich an die mathematische Konvention zur Restwert-Division halten.

So berechnet, stimmen die Resultate mit denen, die uns die mathematische Konvention auferlegt, überein. In der Regel rechnen wir aber mit jeweils positiven Dividenden beziehungsweise Divisoren, sodass die Unterschiede in den Berechnungsmethoden egal sind.

 1/**
 2 * Die Klasse {@code MathModulo} zeigt die Restwertdivision in Java
 3 * gemäß der mathematischen Konvention.
 4 * 
 5 * Die Werte, mit denen gerechnet wird, sind im Quellcode fix
 6 * codiert.
 7 */
 8public class MathModulo {
 9    /**
10     * Hauptprogramm. Die Restwertdivision wird anhand der ganzen Zahlen
11     * 7 und 3 (positiv/negativ) nach der  mathematischen Konvention 
12     * durchgeführt.
13     * 
14     * @param args nicht genutzt
15     */
16    public static void main(String[] args) {
17        int a = 7, b = 3;  // unsere Werte
18
19        System.out.println("MINT-Konvention:");
20        System.out.println(" " + a + " /  " +    b + " =  " + Math.floorDiv(a, b) + " Rest  " + Math.floorMod(a, b));
21        System.out.println((-a) + " /  " +    b + " = " + Math.floorDiv(-a, b) + " Rest  " + Math.floorMod(-a, b));
22        System.out.println(" " + a + " / " + (-b) + " = " + Math.floorDiv(a, -b) + " Rest " + Math.floorMod(   a, -b));
23        System.out.println((-a) + " / " + (-b) + " =  " + Math.floorDiv(-a, -b) + " Rest " + Math.floorMod(-a, -b));
24    }
25}

Listing 2.9: MathModulo.java

Wenn Sie an einer Hochschule studieren, sollten Sie floorDiv() und floorMod() aus dem Modul Math kennen.

Fallstricke: Über- und Unterlauf

Unsere Computer, genauer gesagt deren Prozessoren, arbeiten mit Zahlen im Binärformat. Noch genauer arbeiten sie mit zwei Zuständen, die wir im Sprachgebrauch allgemein mit 0 und 1 bezeichnen. Mit einer Dezimalzahl, wie beispielsweise \(42\), kann der Computer nichts anfangen. Sie muss binär codiert werden, um damit rechnen zu können. Diese Aufgabe übernimmt zum Übersetzungszeitpunkt der Java-Compiler. Zur Laufzeit sorgen die JVM beziehungsweise entsprechende Routinen, die in Java integriert sind, für die entsprechende Codierung.

Je nach Datentyp stehen unterschiedliche Anzahlen von Bits (Nullen und Einsen) zur Codierung zur Verfügung. Bei ganzen Zahlen wird das ganz links stehende Bit genutzt, das Vorzeichen einer Zahl zu codieren (Vorzeichenbit). \(0\) steht für eine positive, \(1\) für eine negative Zahl. Die verbleibenden Bits codieren den absoluten Wert.

Betrachten wir als Beispiel den Datentyp byte. Für die anderen ganzzahligen Datentypen gilt das Folgende ganz analog.

Ein byte-Wert hat 1 Byte, das sind 8 Bits. Abzüglich des Vorzeichenbits bleiben demnach 7 Bits zur Codierung des Wertes. Das sind, ohne hier auf Details eingehen zu wollen, $2^7$ mögliche Zahlenwerte. Weil die Null kein Vorzeichen hat, können wir mit diesen 128 Bits die Zahlen von \(0\) bis \(127\) codieren. Im negativen Bereich haben wir ebenfalls 128 Zahlenwerte, \(-1\) bis \(-128\). Ganze Zahlen sind also asymmetrisch um die Null verteilt.

Wenn wir diesen Wertebereich, bei byte \(-128 \dots 127\), verlassen, haben wir einen Überlauf, wenn wir ihn nach oben überschreiten. Unterschreiten wir ihn im negativen Wertebereich, dann sprechen wir von einem Unterlauf. Dabei tritt ein interessanter Effekt auf. Das folgende Programm OverUnderflow.java demonstriert das.

 1/**
 2 * Die Klasse {@code OverUnderFlow} zeigt die Auswirkungen von
 3 * Über- und Unterläufen anhand des Datentyps {@code byte}.
 4 * OverUnderFlow
 5 */
 6public class OverUnderFlow {
 7    /**
 8     * Hauptprogramm.
 9     * 
10     * @param args nicht genutzt
11     */
12    public static void main(String[] args) {
13        byte a = (byte)-128;  // kleinster zulässiger Wert für byte
14        byte b = (byte) 127;  // größter zulässiger Wert für byte
15        byte underflow = (byte)(a - 1);
16        byte overflow  = (byte)(b + 1);
17
18        System.out.println("Underflow: " + a + " - 1 =  " + underflow);
19        System.out.println("Overflow :  " + b + " + 1 = " + overflow);
20    }
21}

Listing 2.10: OverUnderFlow.java

Die Ergebnisse mögen im ersten Moment verwirren. In der Schule haben wir gelernt \(-128 - 1\) ergibt \(-129\). \(127 + 1\) ergibt \(128\). Stellen Sie sich den Wertebereich wie das Zifferblatt einer Uhr vor. Die Uhr hat 12 Einteilungen, unser hier betrachteter Wertebereich 256.

Stellen Sie sich vor, ein Bekannter kommt Sie am Morgen um 11:00 Uhr besuchen. Er bleibt 3 Stunden und geht um 14:00 Uhr. Umgangssprachlich sagen wir: „Mein Freund ist um 02:00 Uhr (am Nachmittag) gegangen." – Gedanklich haben wir eine Restwertdivision durchgeführt. 14 % 12 ergibt 2. In unserem Programm haben wir den gleichen Effekt. Wir „überdrehen" das uns zur Verfügung stehende „Zifferblatt", welches uns der Wertebereich zur Verfügung stellt. Dieses Zifferblatt beginnt bei \(-128\) und endet bei \(127\). Wird dieser Wert überschritten, kommen wir wieder an den Anfang, \(-128\).

Bei größeren Datentypen ist das „Zifferblatt" größer. Der Effekt ist jedoch immer derselbe.

Vielleicht fragen Sie sich, warum Java uns nicht davor bewahrt, den Wertebereich eines Datentyps zu über- oder unterschreiten. Die Antwort ist einfach und pragmatisch: Geschwindigkeit. Derartige Prüfungen, die bei jeder Zuweisung, jeder Operation auszuführen wären, kosten Zeit. Java überlässt es daher der Verantwortung des Programmierers solche Gefahren zu erkennen und nötigenfalls geeignet zu behandeln. Die Sprache soll keine Geschwindigkeit durch Prüfungen einbüßen, die bei verantwortungsvoll gewählten Datentypen, nicht erforderlich sind.

Zeichen

Java unterscheidet einzelne Zeichen (char) und Zeichenketten (String).

Fallstrick: Zeichen werden als Literale in einfache Anführungszeichen eingeschlossen, Zeichenketten, das haben Sie schon gesehen, in doppelte Anführungszeichen.

String s = "A"; // das ist eine Zeichenkette mit einem Zeichen
char   c = 'A'; // dies ist ein einzelnes Zeichen

Zeichen gehören in Java zu den ganzen Zahlen. Das liegt jedoch nicht allein daran, dass Computer intern sowieso nur mit Zahlen arbeiten können. Wenn wir von Zeichen sprechen, dann meinen wir damit, in Bezug auf den Computer, zum Einen eine Codierung und zum Anderen eine Verknüpfung der Codierung mit einer Glyphe, dem Zeichen welches wir bei Ausgaben im Terminalfenster angezeigt bekommen. Daneben gibt es jedoch weitere Zeichen, die keine visuelle Darstellung haben. Das sind die Steuerzeichen, wie beispielsweise Backspace, Linefeed und andere.

Eine Codierung stellt eine Abbildung dar. Bei einer Zeichencodierung werden Zeichen mit einer ganzen Zahl assoziiert. Die wird intern von Java gespeichert. Anhand des vereinbarten Datentyps weiß Java aber auch, dass es bei der Ausgabe ein Zeichen und nicht dessen Codierung ausgeben muss.

Java verwendet zur Speicherung eines Zeichens 16-Bit (UTF-16). Damit ist Java in der Lage die BMP (Basic Multilingual Plane) der sogenannten UTF-8-Zeichencodierung direkt abzubilden. UTF-8 definiert Zeichencodierungen mittels sogenannter Code Points. Ich will hier nicht auf die technischen Hintergründe eingehen. Die sind für Sie im Moment nicht wesentlich. Folgendes sollten Sie über UTF-8 jedoch wissen:

  • UTF-8 wurde in den 1990er Jahren geschaffen.
  • Das Ziel war/ist es, alle Zeichen dieser Welt codieren zu können.

Bis dahin konnte eine Zeichencodierung, von denen es viele gibt, nur jeweils 256 verschiedene Zeichen codieren. Das war stets ein Problem. Wenn beispielsweise ein Deutscher einem Amerikaner einen Text mit deutschen Umlauten zukommen ließ, wurden diese beim Empfänger falsch dargestellt.

Moderne Betriebssystem unterstützen UTF-8. Die gesamte Kommunikation über das Internet, beispielsweise E-Mail oder Web, verwendet UTF-8. In der BMP (2 Bytes) lassen sich (theoretisch) 65.536 Zeichen codieren. UTF-8 verwendet jedoch bis zu vier Bytes. Asiatische Zeichen benötigen in der Regel drei und manchmal tatsächlich auch vier Bytes. Die Datentyp char deckt die BMP ab, was für die meisten Anwendungen absolut hinreichend ist.

Hinweis: Microsoft Windows unterstützt in seinen Terminalanwendungen, der Eingabeaufforderung und der PowerShell, jeweils nur 8-Bit-Zeichencodierungen. macOS und Linux arbeiten dagegen mit der UTF-8-Zeichencodierung.

Allen Zeichencodierungen ist gemeinsam, dass die Codierung der Zeichen eine Ordnung auf diesen etabliert. Codierung in diesem Sinne bedeutet, einem Zeichen wird eine ganze Zahl zugewiesen. Üblicherweise werden diese Zahlen dann hexadezimal notiert. Wir machen es uns hier aber einfacher. Wir notieren die Zeichencodes dezimal. Das entspricht auch der Art und Weise, wie wir Zeichencodes in Java üblicherweise behandeln.

Ich habe hier einmal eine sogenannte ASCII-Tabelle (American Standard Code for Information Interchange) zusammengestellt. Ich habe sie mit einem Java-Programm erstellt. Die Zeichen mit einer dezimalen Codierung kleiner als 32 sind Steuerzeichen.

               32:     33: !   34: "   35: #   36: $   37: %   38: &   39: '   
               40: (   41: )   42: *   43: +   44: ,   45: -   46: .   47: /   
               48: 0   49: 1   50: 2   51: 3   52: 4   53: 5   54: 6   55: 7   
               56: 8   57: 9   58: :   59: ;   60: <   61: =   62: >   63: ?   
               64: @   65: A   66: B   67: C   68: D   69: E   70: F   71: G   
               72: H   73: I   74: J   75: K   76: L   77: M   78: N   79: O   
               80: P   81: Q   82: R   83: S   84: T   85: U   86: V   87: W   
               88: X   89: Y   90: Z   91: [   92: \   93: ]   94: ^   95: _   
               96: `   97: a   98: b   99: c  100: d  101: e  102: f  103: g   
              104: h  105: i  106: j  107: k  108: l  109: m  110: n  111: o   
              112: p  113: q  114: r  115: s  116: t  117: u  118: v  119: w   
              120: x  121: y  122: z  123: {  124: |  125: }  126: ~  127: 

Das Zeichen mit der dezimalen Codierung 32 ist das Leerzeichen. 127 ist ein Backspace, weshalb Sie kein Zeichen erkennen können. Alle anderen Zeichen sind sichtbar.

Im Kern funktioniert das von mir verwendete Programm sehr einfach. Ich bin die Codierungen von 32 bis 127 durchgegangen und habe die jeweilige Codierung, eine positive ganze Zahl, in ein Zeichen umgerechnet.

1
char ch = (char)65; // liefert 'A'

Das Schöne am ASCII-Code ist, dass er kompatibel mit UTF-8 ist. Schauen Sie sich die Codierungen genau an. Sie stellen fest, dass die Großbuchstaben 'A' bis 'Z' alphabetisch geordnet sind. Selbiges gilt für die Kleinbuchstaben sowie die Ziffern '0' bis '9'. Diese Ordnungseigenschaft gilt in allen Zeichencodierungen dieser Welt, auch dann wenn die jeweilige Codierung einem Zeichen eine andere Nummer zuweist.

Wir können uns diese Ordnungseigenschaft in unseren Programmen auf verschiedene Weisen zunutze machen. Die Codierung bestimmt die Sortierung von Zeichen und auch Zeichenketten. Ein Zeichen mit einer größeren Codierung kommt beim aufsteigenden Sortieren nach einem Zeichen mit einer kleineren Codierung.

Außerdem, und jetzt kommen wir dazu mit Zeichen zu rechnen, können wir deren Codierung nutzen, um den „Abstand" zwischen zwei Zeichen zu ermitteln. Oben haben wir eine Codierung in das korrespondierende Zeichen umgerechnet. Mithilfe von (int) wandeln wir ein Zeichen in seine Codierung um. Das macht Java aber, wenn in einem arithmetischen Ausdruck erforderlich, automatisch.

 1/**
 2 * {@code Char} demonstriert, wie Zeichen als ganze Zahlen 
 3 * und umgekehrt interpretiert werden.
 4 */
 5public class Char {
 6    /**
 7     * Hauptprogramm.
 8     * 
 9     * Das Zeichen {@code 'A'} wird in eine ganze Zahl umgerechnet und umgekehrt.
10     * 
11     * @param args nicht genutzt
12     */
13    public static void main(String[] args) {
14       char ch   = (char)65;  // das ist ein 'A'
15       int  code = (int)'A';  // das ist 65 
16
17       System.out.println("Zeichen: " + ch + ", Code: " + code);
18
19       // Achtung: Klammern sind notwendig, das Java ansonsten versuchen würde 
20       //          eine Zeichenkettenverknüpfung durchzuführen, was mit Minus
21       //          nicht geht.
22       System.out.println("Z ist der " + ('Z' - 'A' + 1) + ". Buchstabe.");
23    }
24}

Listing 2.11: Char.java

Wir müssen 1 addieren, weil ('A' - 'A') den Wert 0 ergibt. Gedanklich steht das 'A' aber nicht an Position 0, es steht an Position 1.

Auf analoge Weise können Sie den Ziffernwert einer Ziffer ermitteln. Hier brauchen wir keine 1 zu addieren. ('7' - '0') ergibt den Wert 7.

Damit bildet char eine wichtige Brücke zwischen Zahlen und Text. Diese Eigenschaft wird später bei der Verarbeitung von Eingaben nochmals eine wichtige Rolle spielen.

Derartige Umrechnungen werden stets benötigt, wenn Sie in ein Programm Zahlen eingeben. Alle Eingaben werden von Java als eine Sequenz von Zeichen interpretiert. Das gilt auch dann, wenn Sie meinen eine Zahl einzugeben. Java sieht die Zahl als eine Sequenz von Ziffern. Folglich muss Java aus den Ziffern den Zahlenwert ermitteln, wenn Sie mit einer solchen Eingabe rechnen wollen.

Als konkretes Beispiel schauen Sie sich einfach unsere bisherigen Programm an. Die sind in Textdateien gespeichert. Der Java-Compiler muss, wenn wir mit Zahlen arbeiten, aus den Ziffernfolgen die Werte dieser Zahlen ermitteln. Dabei helfen ihm natürlich unsere Typangaben. Wenn wir eine Variable vom Typ int vereinbaren, dann weiß der Compiler, das eine zuzuweisende 42 eine Zahl und keine einfache Ziffernfolge ist. Dazu kommt im Programmcode das Literal. 9 ist eine ganze Zahl. '9' ist eine Ziffer und "9" eine Zeichenkette, die aus einer Ziffer besteht. Dadurch weiß der Compiler, ob und wenn ja, er eine Konvertierung vornehmen kann und darf.

Gleitkommazahlen

Gleitkommazahlen stellen eine Näherung an die Zahlenmenge \(\mathbb{R}\) dar. Obgleich sie in Java den umfangreichsten Wertebereich besitzen, unterliegen sie der Einschränkung, begrenzt zu sein. Das betrifft sowohl die absolute Größe der Zahlen als auch deren Genauigkeit.

Java offeriert für Gleitkommazahlen die Datentypen float und double, die sich hinsichtlich ihres Wertebereichs und ihrer Genauigkeit unterscheiden (siehe Tabelle 2.1). Standardmäßig nimmt Java einen double-Wert an, wenn Java auf ein Gleitkomma-Literal stößt. Um einen Wert vom Typ float zu vereinbaren, ist dem Literal ein F anzufügen.

1
float f = 1.234F;

Gleitkommaliterale werden in Java mit einem Dezimalpunkt notiert, nicht mit einem Komma, wie es im Deutschen üblich ist. Zusätzlich ist die wissenschaftliche Exponentialnotation erlaubt.

double d1 = 42.0;       
double d2 = 0.42e2;  // auch 42.0

Das e darf auch großgeschrieben werden. Es ist zu lesen als: „mal 10 hoch".

Gleitkommazahlen sind in Java gemäß dem IEEE-Standard 754 hardwarenah implementiert.

 1/**
 2 * Demonstration der Ungenauigkeiten von Gleitkommazahlen aufgrund
 3 * deren interner Implementierung nach den IEEE-754-Standard.
 4 */
 5public class InAccuracy {
 6    /**
 7     * Hauptprogramm. Es wird das Ergebnis von {@code 3.0 * 0.3}
 8     * ausgegeben.
 9     * 
10     * @param args nicht genutzt
11     */
12    public static void main(String[] args) {
13        double result;
14
15        result = 3.0 * 0.3;  // mathematisch: 0.9, real: 0.8999999999999999
16        System.out.println(result);
17    }
18}

Listing 2.12: InAccuracy.java

$ java InAccuracy
0.8999999999999999
$ _

Diese Ungenauigkeit resultiert aus der internen Codierung der Gleitkommazahlen. Es lassen sich nicht alle gebrochenen Dezimalzahlen exakt abbilden. \(0.3\) sieht dezimal betrachtet nach einer „handelsüblichen" Gleitkommazahl aus. In der Tat kann sie gemäß dem IEEE-754-Standard nicht exakt im Binärcode abgebildet werden.

Dementsprechend kommt es zu, wie gezeigt, zu Ungenauigkeiten, die in der Praxis oft unkritisch sind, bei Vergleichen jedoch beachtet werden müssen..

Für Gleitkommazahlen gelten gemäß IEEE-754 Rechenregeln, die von denen der Schulmathematik abweichen. So kann eine Gleitkommazahl, ohne einen Fehler zu erzeugen, durch Null geteilt werden. Dabei kommt für jede von Null verschiedene Zahl plus oder minus unendlich heraus. Teilt man jedoch die Null durch Null, ist das keine Zahl (NaN, Not a Number).

Diese Behandlung von Gleitkommazahlen ist in der Informatik keine Seltenheit, eher sogar üblich. Jedoch handhaben nicht alle Programmiersprachen Gleitkommazahlen gleich. Sie sollten sich also, wenn Sie die Programmiersprache wechseln, informieren und auf Unterschiede einstellen.

Über- und Unterläufe führen bei Gleitkommazahlen nicht zu Laufzeitfehlern, sondern zu den Werten \(\pm\) unendlich.

Hinweis: Math.pow() implementiert die Potenzfunktion. Das Vorzeichen spielt für den Wertebereich keine Rolle. Gleitkommazahlen sind , im Gegensatz zu den ganzen Zahlen, symmetrisch um die Null verteilt.

 1/**
 2 * Demonstration der Werte {@code NaN} und {@code  Inf}, die
 3 * bei Operationen mit Gleitkommazahlen auftreten können.
 4 */
 5public class NaNInf {
 6    /**
 7     * Hauptprogramm.
 8     * 
 9     * Wird der Wertebereich von Gleitkommazahlen verlassen, resultiert das
10     * in unendlich {@code Inf}, eine Division durch Null führt zu {@code NaN}.
11     * Für dieses Verhalten ist die interne Codierung der Gleitkommazahlen nach
12     * dem IEEE-754-Standard verantwortlich.
13     * 
14     * @param args nicht genutzt
15     */
16    public static void main(String[] args) {
17        // Infinity hat ein Vorzeichen
18        System.out.println(" 10 hoch 399 =  " + Math.pow( 10, 399));
19        System.out.println("-10 hoch 399 = " + Math.pow(-10, 399));
20        System.out.println("  1.0 /  0.0 =  " +  ( 1.0 /  0.0));
21        System.out.println(" -1.0 /  0.0 = " +  (-1.0 /  0.0));
22        System.out.println("  1.0 / -0.0 = " +  ( 1.0 / -0.0));
23        System.out.println(" -1.0 / -0.0 =  " +  (-1.0 / -0.0));
24
25        // NaN hat kein Vorzeichen
26        System.out.println("  0.0 /  0.0 = " +  ( 0.0 /  0.0));
27        System.out.println(" -0.0 /  0.0 = " +  (-0.0 /  0.0));
28    }
29}

Listing 2.13: NaNInf.java

Führen wir das obige Programm aus, erhalten wir die folgende Ausgabe:

$ java NaNInf
Infinity
-Infinity
Infinity
-Infinity
NaN
NaN
$ _

Zusammenfassend ist festzuhalten:

  • Gleitkommazahlen bilden nur eine Teilmenge der reellen Zahlen ab.
  • Die Abbildungs- und Rechengenauigkeit ist eingeschränkt.
  • Es gibt keine Über- oder Unterläufe.
  • Die Division durch Null ist kein Fehler.

Operatoren für Gleitkommazahlen

Die folgende Tabelle zeigt, analog zu den ganzen Zahlen, die in Java zulässigen Operatoren. Die Restwertdivision ist erlaubt, sollte jedoch gemieden werden.

Tabelle 2.3: Operatoren für Gleitkommazahlen

Operator Beschreibung Assoziativität
() Klammer links-rechts
++ Post-Inkrement links-rechts
-- Post-Dekrement
+ unäres Plus rechts-links
- unäres Minus
++ Pre-Inkrement
-- Pre-Dekrement
+ unäres Plus rechts-links
- unäres Minus
* / % & multiplikative Operatoren & links-rechts multiplikative Operatoren links-rechts
+ - additive Operatoren links-rechts
< <= > >= Relation links-rechts
!= == (Un)Gleichheit links-rechts
= += -= Zuweisung rechts-links
*= /=

Praxis: Vergleich von Gleitkommazahlen

Sie haben soeben erfahren, dass Gleitkommazahlen mit Ungenauigkeiten behaftet sind, die aus ihrer Codierung resultieren. Als Folge daraus, müssen wir bei Vergleichen von Gleitkommazahlen Obacht geben.

Das folgende Programm FloatCompare.java illustriert den Sachverhalt.

 1/**
 2 * Demonstration zum Vergleich auf Gleichheit von
 3 * Gleitkommazahlen.
 4 */
 5public class FloatCompare {
 6    /**
 7     * Hauptprogramm. Anhand vorgegebener Zahlen wird im 
 8     * Quellcode gezeigt, wie zuverlässig Vergleiche auf
 9     * Gleichheit bei Gleitkommazahlen gezeigt.
10     * 
11     * @param args nicht genutzt
12     */
13    public static void main(String[] args) {
14        double a = 0.1, b = 0.2, c = 0.3;
15
16        // bei der einfachen Ausgabe ist die Welt in Ordnung
17        System.out.println("a = " + a);
18        System.out.println("b = " + c);
19        System.out.println("b = " + c);
20
21        // ein direkter Vergleich kann scheitern (hier der Fall)
22        System.out.println("Direkter Gleichheitsvergleich:");
23        System.out.println("a + b     : " + (a + b));
24        System.out.println("a + b == c: " + (a + b == c));
25
26        // ein indirekter Vergleich ist sicherer
27        System.out.println("Indirekter Vergleich via Epsilon-Umgebung:");
28        double epsilon = 1.0e-15;                         // Epsilon-Umgebung
29        boolean comp = Math.abs(c - (a + b)) <= epsilon;  // -epsilon <= Ergebnis <= epsilon
30        System.out.println("a + b == c: " + comp);        // Gleichheit annehmen ja/nein
31    }
32}

Listing 2.14: FloatCompare.java

Im Gegensatz zur Multiplikation von 0.3 * 3.0 (siehe oben) ist das Ergebnis dieses Mal geringfügig größer als erwartet. D. h., die Ungenauigkeiten von Gleitkommazahlen erzeugen Ergebnisse, die minimal zu klein oder auch minimal zu groß sein können. Wir müssen daher, wenn wir Gleitkommazahlen vergleichen, ein entsprechendes Intervall um den erwarteten Wert prüfen. Liegt das berechnete Ergebnis in diesem Bereich, gehen wir davon aus, das erwartete Ergebnis erzielt zu haben.

In der Mathematik nennt man einen solchen Bereich Epsilon-Umgebung. Hierfür bietet sich die Genauigkeit der Gleitkommazahlen an, die bei double 15 bis 16 Stellen beträgt. Dazu bilden wir die Differenz \(x\) zwischen berechnetem Ergebnis und dem erwarteten Wert. Für das Beispiel ist das:

\[x = c - (a + b)\]

Dann muss \(x\) die folgende Bedingung erfüllen, um die Gleichheit von \(a+b=c\) anzunehmen:

\[-1.0\text{e}-15 \leq x \leq 1.0\text{e}-15\]

Die Bedingung können wir dahingehend vereinfachen, den Absolutwert (Betrag) von \(x\) mit dem Absolutwert der Epsilon-Umgebung zu vergleichen. Also:

\[|x| \leq 1.0\text{e}-15\]

In Java stellt uns Math.abs() die notwendige Methode, den Absolutwert zu berechnen, zur Verfügung. Wir ergänzen unser Programm.

comp erhält hier den Wahrheitswert true, weil die Differenz aus berechnetem Ergebnis und dem erwarteten Wert, unserem Vergleichswert, kleiner gleich unserem Epsilon ist.

Vergleiche zwischen Gleitkommazahlen sind also kein Hexenwerk. Statt des Vergleichsoperators == bietet es sich in der Regel an, den Vergleich, wie oben gezeigt umzustellen. Das vermeidet potenzielle Fehler. – Problem gelöst!

Zeichenketten

Der Datentyp für Zeichenketten heißt in Java String. Im Gegensatz zu den bisher besprochenen Datentypen, boolean, int, double etc., sind Strings nicht primitiv. Strings gehören zu den sogenannten Referenzdatentypen. Es handelt sich im Sinne der Objektorientierung um Objekte.

Ich möchte hier nicht der Objektorientierung vorgreifen, die wir später noch detailliert besprechen werden. Strings werden in der Praxis jedoch so häufig benötigt, dass ich Sie an dieser Stelle einführen möchte.

Für das Verständnis des Datentyps String, sollten Sie sich ein Denkmodell schaffen. Analoge Denkmodelle werden Ihnen später auch bei der Behandlung der Objektorientierung helfen.

Denken Sie sich einen String als eine Einheit, in der Daten und auf diesen Daten operierende Methoden gekapselt sind. Ein String kann nämlich deutlich mehr, als das wir ihm nur eine Zeichenkette zuweisen und diese wieder auslesen und ausgeben können. Ein String kann uns Informationen über seine Länge liefern, wir können auf einzelne Zeichen zugreifen und vieles andere mehr.

Schauen Sie in die Dokumentation: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/lang/String.html.

Vieles werden Sie noch nicht verstehen, was in der Dokumentation beschrieben und erläutert wird. Das ist aber im Moment auch nicht notwendig. Es ist vollkommen ausreichend, wenn Ihnen bewusst wird, dass in der Method Summary eine Vielzahl sogenannter Methoden aufgelistet ist, welche die Fähigkeiten des Datentyps String auflisten und beschreiben. Sie finden dort beispielsweise die Methode length(), die Auskunft über die Länge einer Zeichenkette gibt. Klicken Sie eine Methode in dieser Liste an, gelangen Sie jeweils zu einer detaillierten Beschreibung.

Oben rechts im Dokumentationsfenster gibt es auch ein Such-Widget. Sie können es nutzen, wenn Sie beispielsweise eine bestimmte Methode oder später auch andere Dinge in der Dokumentation suchen.

Tipp: Gewöhnen Sie sich bereits jetzt an, die Dinge, die wir hier behandeln, auch in der Java-Dokumentation nachzuschlagen. Das ermöglicht Ihnen, das hier Gelernte mit den Ausführungen der Dokumentation zu vergleichen und hilft Ihnen, diese nach und nach leichter zu verstehen.

Denkmodell für Strings

Am einfachsten stellen Sie sich einen String als ein Objekt Ihrer Anschauung vor, das aus einem Daten- und einem Methodenbereich besteht. Im Datenbereich wird die jeweilige Zeichenkette in Form einer Sequenz von Zeichen gespeichert. Die sind vom Datentyp char, den Sie bereits kennen. Entscheidend ist, das diese Zeichensequenz indiziert ist. Jedes Zeichen ist mit einer Nummer assoziiert, dem sogenannten Index. Er beginnt mit der Nummer \(0\). Das bedeutet: In einer Zeichenkette mit \(n\) Zeichen, hat das erste Zeichen den Indexwert \(0\) und das letzte Zeichen steht an Indexposition \(n-1\).

Daneben gibt es den Methodenbereich, dessen vielfältiges Angebot Sie in der Dokumentation nachgeschlagen haben.

Um die Methoden eines Objekts nutzen zu können, notieren Sie die aufzurufende Methode mit einem Punkt. Die Syntax lautet ganz allgemein: Objekt.Methodenaufruf.

Wir probieren das sofort aus.

 1/**
 2 * {@code StringIndex} demonstriert die Indizierung
 3 * von Zeichenketten {@link String}.
 4 */
 5public class StringIndex {
 6    /**
 7     * Hauptprogramm. Im Quellcode wird gezeigt, wie auf
 8     * einzelne Zeichen einer vorgegebenen Zeichenkette
 9     * zugegriffen werden kann.
10     * 
11     * @param args nicht genutzt
12     */
13    public static void main(String[] args) {
14        String firstname = "Karsten";  // Zeichenkette
15        int len = firstname.length();  // Anzahl der Zeichen (Länge)
16
17        System.out.println("Zeichenkette   : " + firstname);
18        System.out.println("Anzahl Zeichen : " + len);
19        // eine Zeichenkette mit n Zeichen hat die Indizes 0 bis n-1
20        System.out.println("Erstes Zeichen : " + firstname.charAt(0));
21        System.out.println("Letztes Zeichen: "  + firstname.charAt(len-1));
22    }
23}

Listing 2.15: StringIndex.java

Daten- und Methodenbereich bilden zusammen das jeweilige String-Objekt.

Strings sind unveränderlich

Die in einem String gespeicherte Zeichenkette kann nicht verändert werden. Wir können nicht auf ein einzelnes Zeichen zugreifen und ihm einen neuen Wert zuweisen.

1
2
3
4
String firstname = "Carsten"; 

// Wunsch: Der erste Buchstabe des Vornamens soll ein 'K' sein.
firstname.charAt(0) = 'K';  // VERBOTEN - kann nicht compiliert werden

Wenn wir einen String ändern wollen, können wir das also nicht direkt tun.

Die Lösung besteht darin, dass wir uns aus dem vorhandenen ein neues String-Objekt bauen. Dazu können wir die String-Methoden nutzen, um den Ziel-String zu beschreiben. Das Ergebnis unserer Bemühungen können wir dann wieder unserer ursprünglichen Zeichenkettenvariablen zuweisen. Wir machen uns dabei zunutze, dass bei einer Zuweisung immer erst die Teilanweisung ausgeführt wird, die rechts vom Zuweisungsoperator steht.

Im Endeffekt wirkt das, als hätten wir die Zeichenkette geändert.

 1/**
 2 * Das Programm zeigt, wie eine Modifikation von Zeichenketten
 3 * ({@link String}) emuliert werden kann.
 4 */
 5public class StringModify {
 6    /**
 7     * Hauptprogramm. Weil Zeichenketten in Java unveränderlich sind,
 8     * wird für Modifikationen eine neue Zeichenkette zu konstruiert.
 9     * 
10     * @param args nicht genutzt
11     */
12    public static void main(String[] args) {
13        String firstname = "Carsten";
14        String new_name;
15
16        new_name = 'K' + firstname.substring(1, 7);  // ersetze 'C' durch 'K'
17        System.out.println("Original: " + firstname);
18        System.out.println("Neu     : " + new_name);
19
20        // einfacher mit replace()
21        new_name = firstname.replace('C', 'K');
22        System.out.println("Original: " + firstname);
23        System.out.println("Neu     : " + new_name);
24    }
25}

Listing 2.16: StringModify.java

Auch replace() liefert einen neuen String und belässt den alten unverändert.

Escape-Sequenzen

Gelegentlich ist es erforderlich Zeichen in einer Zeichenkette aufzunehmen, die Sie über die Tastatur nicht einfach eingeben können. Diesbezüglich am markantesten dürften die doppelten Anführungszeichen sein. Sie dienen als Begrenzungszeichen der String-Literale, sodass Sie ein solches Zeichen nicht einfach innerhalb einer Zeichenkette verwenden können. Die Lösung stellen sogenannte Escape-Sequenzen dar.

Eine Escape-Sequenz besteht, wenn Sie über die Tastatur erfasst wird, aus scheinbar zwei Zeichen, einem Backslash und einem weiteren Zeichen. Der Backslash ist das sogenannte Fluchtsymbol (engl. escape character) oder Entwertungszeichen. Er beraubt das folgende Zeichen seiner üblichen Bedeutung und weist im eine Sonderbedeutung zu. Um in einer Zeichenkette ein doppeltes Anführungszeichen zu integrieren, notieren wir \". Das bedeutet: „Nimm dem doppelten Anführungszeichen seine Bedeutung als String-Begrenzungszeichen und integriere es stattdessen direkt in die Zeichenkette." – Obgleich zwei Tastenanschläge notwendig sind, um diese Escape-Sequenz anzugeben, stellt jede Escape-Sequenz lediglich ein einziges Zeichen dar.

Es gibt verschiedene Escape-Sequenzen. Die folgende Tabelle stellt die am häufigsten benötigten Escape-Sequenzen und ihre jeweilige Bedeutung dar.

Beachten Sie: Weil der Backslash in einer Escape-Sequenz die Bedeutung des Fluchtsymbols innehat, erfordert seine Notation ebenfalls eine entsprechende Escape-Sequenz. Er wird doppelt notiert.

Tabelle 2.4: Häufig benötigte Escape-Sequenzen in Java

Escape-Sequenz Bedeutung
\t horizontaler Tabulator
\n Zeilenumbruch (Linefeed)
\r Carriage Return
\" doppeltes Anführungszeichen
\' einfaches Anführungszeichen
\\ Backslash

Das folgende Programm zeigt Ihnen, wie Sie Escape-Sequenzen praktisch anwenden.

 1/**
 2 * Demonstration von Escape-Sequenzen.
 3 */
 4public class EscapeSeq {
 5    /**
 6     * Hauptprogramm. Ausgabe einer Zeichenkette mit inkludierten
 7     * Escape-Sequenzen.
 8     * 
 9     * @param args nicht genutzt
10     */
11    public static void main(String[] args) {
12        String s = "Karsten's Lieblingsserie ist \"Game of Thrones\".";
13        System.out.println(s);
14        System.out.println("Länge: " + s.length());
15    }
16}

Listing 2.17: EscapeSeq.java

Wenn Sie das Programm ausführen, werden Sie das Folgende feststellen:

  • \n bricht die Ausgabe der Zeichenkette hinter Serie: um.
  • \" bewirkt die Ausgabe eines doppelten Anführungszeichens.
  • Die Zeichenkette ist 47 Zeichen lang, Jede Escape-Sequenz stellt jeweils nur ein Zeichen dar.
$ java EscapeSeq                  
Karsten's Lieblingsserie ist "Game of Thrones".
Länge: 47
$ _

Exkurs: Primitive vs. Referenzdatentypen

Für die Vorstellung primitiver Datentypen bietet sich ein Box-Modell an. Stellen Sie sich eine Box vor. Auf Ihrem Deckel steht dann beispielsweise: „Ich bin die Variable x und habe den Datentyp int." Wenn Sie den Deckel der Box öffnen, was dem Auslesen des Variablenwertes entspricht, finden sie den Wert, welcher der Variablen zugewiesen wurde – vielleicht eine 42.

Bei Referenzdatentypen trifft das Box-Modell nicht zu. Eine Variable stellt vielmehr einen Verweis auf den Speicherbereich dar, in welchem das Objekt, beispielsweise ein String liegt. Um an dessen Werte zu gelangen, nutzen Sie in der Regel Methoden, welche Ihnen die Werte liefern.

Ihnen wird bei der Nutzung von Strings aufgefallen sein, dass sich ein String nicht ganz so verhält. Sie brauchen keinen expliziten Methodenaufruf, um den Inhalt eines Strings in Erfahrung zu bringen. Sie können den Wert eines Strings direkt auslesen. Auch sonst zeigen Strings in ihrem Verhalten sehr viel Ähnlichkeiten mit den primitiven Datentypen. Die Java-Entwickler haben beim Entwurf der Sprache die Wichtigkeit und häufige Nutzung der Strings bedacht. Sie haben ihre Handhabung daher so implementiert, dass Sie von der Handhabung üblicher Objekt in Java abweichen und entsprechend einfach und komfortabel genutzt werden können.

Ein- und Ausgabe

In der Regel bestehen Programme, die etwas Sinnvolle tun, aus drei Teilen: Eingabe, Verarbeitung und Ausgabe. Insbesondere die Eingabe werden Sie bislang vielleicht vermisst haben. Es gibt verschiedene Möglichkeiten, Eingaben in einem Programm zu realisieren. In diesem Abschnitt beschränken wir uns vorerst auf die interaktive Eingabe im Terminalfenster. Dazu nutzen wir die sogenannte Standardeingabe. Sie ist, sofern wir nichts anderes verlangen, mit der Tastatur unseres Computers verbunden. Die Ausgabe differenzieren wir in Standardausgabe und Standardfehlerausgabe. Beide sind üblicherweise mit dem Terminalfenster assoziiert. – Wir haben also drei sogenannte Kanäle, über welche wir Ein- und Ausgaben realisieren.

Moderne Betriebssysteme gestatten es uns, Ein- und Ausgabekanäle umzulenken, etwa um Eingaben aus Dateien zu lesen oder Ausgaben in Dateien zu schreiben. Damit Warn- oder Fehlermeldungen eines Programms dabei nicht unbemerkt verschwinden, ist es sinnvoll, zwischen Standardausgabe und Standardfehlerausgabe zu unterscheiden. Sie lernen, welche Abstraktionen Java anbietet, mit den genannten Ein-/Ausgabekanälen zu arbeiten. Ich spreche bewusst von Abstraktionen, weil wir uns nicht um die technischen Details kümmern müssen, wie diese realisiert sind.

Des Weiteren schauen wir uns an, wie wir unsere Ausgaben formatieren können. Eine adäquate Ausgabeformatierung ist entscheidend für die Lesbarkeit und die leichte Verständlichkeit der Ergebnisse eines Programms.

Interaktive Eingabe

Interaktive Eingabe bedeutet:

  1. Die Ausführung eines Programms hält an und wartet auf eine Eingabe seitens des Benutzers.
  2. Der Benutzer gibt Daten über die Tastatur ein und beendet die Eingabe mithilfe der Return-Taste.
  3. Das Programm liest die Eingabe.
  4. Die Programmausführung wird fortgesetzt.

Ein Programm wird als interaktiv bezeichnet, wenn es während seiner Laufzeit Eingaben vom Benutzer entgegennimmt und auf diese reagiert.

Eingaben gehören in Java nicht zum Kern der Sprache, ebenso wenig wie Ausgaben. Bei Letzteren ist es Ihnen vermutlich noch nicht explizit aufgefallen. – Wir haben System.out bemühen müssen, um dann via print() oder auch println() Ausgaben zu realisieren. Für Eingaben nutzen wir das Pendant zu System.out. Das heißt System.in.

Die Ein-/Ausgabe-Abstraktion ist recht komplex aufgebaut. Ohne die Verwendung objektorientierter Syntax gelingt es uns nicht, eine Eingabe zu implementieren. Betrachten Sie dies vorerst als „syntaktischen Zucker", der notwendig ist. Den objektorientierten Hintergrund dürfen Sie aktuell noch vernachlässigen.

Eine komfortable Möglichkeit, Eingaben zu realisieren, bietet die Klasse Scanner. Ein Objekt der Klasse Scanner ist, ganz analog zu einem String, eine Einheit, in der Daten und darauf operierende Methoden zusammengefasst sind. Im Gegensatz zu den Strings müssen wir einen Scanner jedoch explizit instanziieren.

Bei einem String nimmt uns Java die explizite Instanziierung ab. Bei allen anderen Objekten (Referenzdatentypen) sind wir verpflichtet, Java mitzuteilen, uns ein solches Objekt zur Verfügung zu stellen. Das nennen wir Instanziierung. Das Objekt, welches wir daraufhin erhalten, ist eine Instanz des angeforderten Datentyps.

 1import java.util.Scanner;
 2
 3/**
 4 * Interaktive Benutzereingabe mithilfe von {@link java.util.Scanner}.
 5 */
 6public class UseScanner {
 7    /**
 8     * Hauptprogramm. Es wird ein Name interaktiv eingelesen und in
 9     * Form eines Satzes wieder ausgegeben.
10     * 
11     * @param args nicht genutzt
12     */
13    public static void main(String[] args) {
14        String firstname;
15
16        // Scanner-Instanz mit Standardeingabe als Quelle
17        Scanner scanner = new Scanner(System.in);
18
19        System.out.print("Bitte geben Sie Ihren Vornamen ein: ");
20        firstname = scanner.nextLine();  // interaktive Eingabe
21        System.out.println("Dein Name ist " + firstname + ".");
22
23        // Scanner-Ressource zurückgeben
24        scanner.close();
25    }
26}

Listing 2.18: UseScanner.java

Der Code zeigt den grundsätzlichen Gebrauch eines Scanners. Dabei ist folgendes zu beachten:

  • Um Scanner nutzen zu können, müssen wir Scanner aus einem Java-Modul importieren (Zeile 1). Ein Scanner ist kein Objekt, welches zum Sprachumfang von Java gehört. Es ist ein Bibliotheksobjekt.
  • Um eine Scanner-Instanz zu erhalten, müssen wir eine solche anfordern. Das geschieht mithilfe des Schlüsselwortes new.
  • Ein Scanner-Objekt kann verschiedene Quellen nutzen, die gescannt werden sollen. Wir wollen den Scanner mit der Tastatur, der Standardeingabe, verbinden. Die wird durch System.in repräsentiert. Daher übergeben wir Scanner System.in als Argument, um die Standardeingabe als Eingabequelle zu definieren.
  • Ein Scanner bietet uns verschiedene Methoden, Eingaben zu lesen. Die einfachste ist nextLine(). Diese Methode liest eine komplette Eingabezeile und liefert einen String.
  • Ein Scanner belegt im System eine Ressource. Hier ist das die Standardeingabe. Belegte Ressourcen müssen, wenn sie nicht mehr benötigt werden, freigegeben werden. Das ist spätestens am Programmende der Fall.

Sie erkennen: Die Nutzung eines Scanners zur Eingabe, ist ein wenig komplexer als der Gebrauch eines String-Objekts. Die wesentlichen Unterschiede sind, Scanner via Import erst bekanntmachen und explizit instanziieren zu müssen.

Scanner.close() ist kein Spezifikum objektorientierter Programmierung. Hierbei handelt es sich schlicht um die Notwendigkeit, Ressourcen des Betriebssystems, die wir mit new Scanner(System.in) gebunden haben, wieder freizugeben.

Ein Scanner kann natürlich nicht nur Strings einlesen. Wir müssen das Einlesen jedoch differenziert betrachten. Alles, was wir über die Standardeingabe einlesen, ist erst mal eine Sequenz von Zeichen, eine Zeichenkette. Auch eine vermeintlich einfache Zahl, wie beispielsweise 1, ist keine Zahl. Es handelt sich um eine Ziffer und stellt für Java eine Zeichenkette mit lediglich einem Zeichen dar.

Schauen Sie sich die Dokumentation zu Scanner an (https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Scanner.html). Im Methoden-Abschnitt finden Sie verschiedene Metho­den, über die Sie Informationen eines Scanners abrufen können. Zum Beispiel kann Ihnen der Scanner mitteilen, ob noch Zeichen, Zeilen oder irgendwelche Zahlen in der Eingabe stehen, die noch nicht verarbeitet wurden. Sie erkennen auch, dass ein Scanner für das Einlesen unterschiedlicher Datentypen jeweils unterschiedlich Methoden anbietet. hasNextDouble() informiert darüber, ob das nächste Element der Eingabe eine Zeichensequenz ist, die sich als Gleitkommazahl interpretieren lässt. nextDouble() liest das nächste Element der Eingabe und versucht dieses als Gleitkommazahl zu interpretieren und an den Aufrufer zurückzugeben. Lässt sich das gelesene Element nicht als double interpretieren, wirft Java eine Ausnahme. Das Programm bricht ab und Sie erhalten eine Meldung, die Sie über eine InputMismatchException informiert.

Anmerkung: Oben spreche ich von „Elementen der Standardeingabe". Das ist nicht falsch. Es ist aber umständlich formuliert. In der Informatik sprechen wir kürzer von sogenannten Token. Token sind durch Leerzeichen, Tabulatoren oder auch Zeilenumbrüche (Whitespaces) voneinander getrennt.

Wir wollen jetzt ein erstes sinnvolles Programm schreiben, um die neu erworbene Fähigkeit, Eingaben zu realisieren, auszunutzen. Das Programm Annuity.java soll uns für ein Darlehen der Höhe \(K\) die jährliche Annuität \(A\) für \(n\) Jahre berechnen, wenn der Zinssatz \(i\) beträgt.

Die Formel zur Berechnung der Annuität lautet:

\[A = K \cdot \frac{i \cdot (1+i)^n}{(1+i)^n-1}\]

\(K\), \(n\) und \(i\) sind einzulesende Eingabegrößen. Sie sind jeweils von Datentyp double. Die benötigte Funktionalität, eine Potenz zu berechnen, liefert uns Math.pow(). Das haben wir schon genutzt.

 1import java.util.Scanner;
 2
 3/**
 4 * {@code Annuity} berechnet eine jährliche Annuität.
 5 */
 6public class Annuity {
 7    /**
 8     * Hauptprogramm. Berechnung einer jährlichen Annuität:
 9     * 
10     * \[
11     * A = K \cdot \frac{i \cdot (1+i)^n}{(1+i)^n - 1}
12     * \]
13     * 
14     * mit:<br>
15     * \(i\) = Zinssatz<br>
16     * \(K\) = Anfangskapital und<br>
17     * \(n\) = Laufzeit in Jahren
18     * 
19     * @param args nicht genutzt
20     */
21    public static void main(String[] args) {
22
23        Scanner scanner = new Scanner(System.in);
24
25        double K, i, n;  // Eingabeparameter
26        double A;        // zu berechnende Annuität
27        
28        // Eingabe
29        System.out.print("Kapital (K)...........: ");
30        K = scanner.nextDouble();
31        System.out.print("Zinssatz (i)..........: ");
32        i = scanner.nextDouble();
33        System.out.print("Laufzeit in Jahren (n): ");
34        n = scanner.nextDouble();
35        
36        // Verarbeitung
37        A = K * (i * Math.pow(1+i, n)) / (Math.pow(1+i, n) -1);
38
39        // Ausgabe
40        System.out.println("Jährliche Annuität....: " + A);
41
42        // aufräumen
43        scanner.close();
44    }
45}

Listing 2.19: Annuity.java

Anhand der eingefügten Kommentare erkennen Sie den klassischen Ablauf eines Programms: Eingabe, Verarbeitung, Ausgabe.

Beim Ausprobieren, werden Sie erkennen, wiederum mit den Ungenauigkeiten des Datentyps double konfrontiert zu sein.

Im Abschnitt „Ausgabeformatierung", Seite , werden wir uns dieser Problematik annehmen und die Ausgabe adäquat gestalten.

$ java Annuity
Kapital (K)...........: 1000
Zinssatz (i)..........: 0,1
Laufzeit in Jahren (n): 1
Jährliche Annuität....: 1099.999999999999
$ _

Achtung: Unter einem deutsch eingerichteten Betriebssystem müssen Sie Gleitkommazahlen, bei der Verwendung von Scanner mit einem Dezimalkomma eingeben. Die Ausgabe mittels print() oder println() erfolgt dann allerdings wiederum mit einem Dezimalpunkt. Das ist im Standard inkonsistent. - Wir kümmern uns später darum.

Standardausgabe und Standardfehlerausgabe

Standardausgabe und Standardfehlerausgabe sind, wenn wir mit Java arbeiten, ebensolche Abstraktionen wie die Standardeingabe. Wir sprechen von Abstraktionen, weil wir uns um die technischen Details nicht zu kümmern brauchen. Wir nutzen einen Set von Objekten und deren Methoden, die Java uns als Schnittstelle zur Verfügung stellt. Sie übernehmen die notwendige Kommunikation mit dem Betriebssystem.

Bereits eingangs dieses Abschnittes habe ich darauf hingewiesen, bei der Ausgabe zwei Kanäle, die Standardausgabe und die Standardfehlerausgabe, zu unterscheiden. Wieso das wichtig ist, möchte ich Ihnen demonstrieren.

Das Programm ErrorDemo.java spendet keinen Nutzen. Es dient lediglich dazu, Ihnen zu zeigen, was passiert, wenn wir Standardausgabe und Standardfehlerausgabe nicht differenzieren.

 1/**
 2 * Anhand von {@code ErrorDemo} wird gezeigt, wie fatal es sein kann,
 3 * wenn Fehlermeldungen auf der Standardausgabe erfolgen.
 4 */
 5public class ErrorDemo {
 6    /**
 7     * Hauptprogramm. Reguläre Ausgaben und Fehlermeldungen erfolgen auf
 8     * der Standardausgabe. Teste:
 9     * 
10     * <pre>
11     * $ java ErrorDemo
12     * </pre>
13     * und
14     * <pre>
15     * $ java ErrorDemo &gt; ausgabe.txt
16     * </pre>
17     * 
18     * @param args nicht genutzt
19     */
20    public static void main(String[] args) {
21        // Version 1: Reguläre Ausgaben und Fehler auf StdOut
22        System.out.println("Ausgabe eines korrekten Ergebnisses.");
23        System.out.println("Ausgabe einer Fehlermeldung");
24    }
25}

Listing 2.20: ErrorDemo.java (Version) 1

Stellen Sie sich vor, das Programm würde tatsächlich Daten verarbeiten. Führen Sie das Programm aus. Sie erhalten die folgende Ausgabe:

$ java ErrorDemo
Ausgabe eines korrekten Ergebnisses
Ausgabe einer Fehlermeldung
$ _

Das ist soweit in Ordnung. Nun stellen wir uns weiter vor, das Programm leite seine Ausgabe in eine Datei um, weil es vielleicht eine große Menge statistischer Daten berechnet. D. h., wir benutzen den Ausgabeumlenkungsoperator des Betriebssystems, um die Ausgabe in eine Datei umzuleiten. Wichtig hierbei: Das Programm weiß nichts davon, dass die Ausgabe nicht mehr im Terminalfenster erscheint. Das unterstreicht nochmals den Aspekt der Abstraktion. Wir produzieren eine Ausgabe auf der Standardausgabe. Wohin diese gelenkt wird, ist dem Programm nicht bekannt.

$ java ErrorDemo > statistics.txt
$ _

Wir sehen keine Fehlermeldung auf dem Bildschirm. Das ist schlecht. Wenn Fehler auftreten, sollten wir informiert werden. Die Fehlermeldung ist in der Datei statistics.txt verschwunden und wird dort, bei deren Weiterverarbeitung, mit Sicherheit weitere Fehler verursachen.

Wir können uns den Inhalt von statistics.txt ansehen. Windows-Anwender ersetzen das cat-Kommando durch TYPE.

$ cat statistics.txt
Ausgabe eines korrekten Ergebnisses
Ausgabe einer Fehlermeldung
$ _

System.out ist die Verbindung zum Standardausgabekanal. System.err ist mit der Standardfehlerausgabe verbunden.

 1/**
 2 * Anhand von {@code ErrorDemo} wird gezeigt, wie fatal es sein kann,
 3 * wenn Fehlermeldungen auf der Standardausgabe erfolgen.
 4 */
 5public class ErrorDemo {
 6    /**
 7     * Hauptprogramm. Reguläre Ausgaben und Fehlermeldungen erfolgen auf
 8     * der Standardausgabe. Teste:
 9     * 
10     * <pre>
11     * $ java ErrorDemo
12     * </pre>
13     * und
14     * <pre>
15     * $ java ErrorDemo &gt; ausgabe.txt
16     * </pre>
17     * 
18     * @param args nicht genutzt
19     */
20    public static void main(String[] args) {
21        // Version 2: Reguläre Ausgaben (StdOut), Fehler (StdErr)
22        System.out.println("Ausgabe eines korrekten Ergebnisses.");
23        System.err.println("Ausgabe einer Fehlermeldung");
24    }
25}

Listing 2.21: ErrorDemo.java (Version) 2

Sofern wir Ausgaben nicht umleiten, geben beide im Terminalfenster aus. Sie bemerken keinen Unterschied.

$ java ErrorDemo
Ausgabe eines korrekten Ergebnisses
Ausgabe einer Fehlermeldung
$ _

Der Unterschied zwischen System.out und System.err wird offenbar, wenn wir die Standardausgabe umlenken.

$ java ErrorDemo > statistics.txt
Ausgabe einer Fehlermeldung
$ _

Die Fehlermeldung wird jetzt im Terminal sichtbar und wir werden über den Fehler informiert. Die Standardfehlerausgabe ist, weil nicht umgeleitet, weiter mit der Konsole als Ausgabemedium verbunden. In statistics.txt stehen nur die über System.out ausgegebenen Daten.

$ cat statistics.txt             
Ausgabe eines korrekten Ergebnisses
$ _

Zusammenfassung: Das Betriebssystem bietet uns drei Standardkanäle für die Ein- und Ausgabe. Wie das Wort „Standard" in ihrem jeweiligen Namen andeutet, handelt es sich um eine Standardisierung. Die Standardeingabe, in Java durch System.in repräsentiert, ist mit der Tastatur verbunden. Standardausgabe (System.out) und Standardfehlerausgabe (System.err) geben im Terminal aus. Beides sind also Ausgaben. Diese Standard-Zuordnung können wir jedoch ändern. Seitens des Betriebssystems lassen sich die drei Ein-/Ausgabekanäle flexibel konfigurieren. Unsere Programme bemerken das nicht. Sie arbeiten mit den Abstraktionen von Ein- und Ausgabe, die Java für uns bereithält.

Ausgabeformatierung

Bisher haben wir für Ausgaben die Methoden print() und println() genutzt, die uns gleichermaßen von System.out und System.err bereitgestellt werden. Die Methoden geben, wie Sie erfahren haben, unformatiert aus. Auszugebende Daten werden 1:1 in die jeweilige Ausgabe gestellt.

In der Praxis wünschen wir uns in der Regel formatierte Ausgaben, wie rechtsbündig ausgerichtete Zahlen, linksbündigen Text, Tabellen mit einer sauberen Spaltenstruktur usw.

Für die formatierte Ausgabe gibt es die Methode printf(). Analog zu print() und println() wird sie sowohl von System.out als auch System.err zur Verfügung gestellt.

Definition und Aufrufkonvention

printf() existiert in zwei Varianten:

printf(String format, Object... args)
printf(Locale l, String format, Object... args)

Wir beschränken uns vorerst auf die erste Variante, ohne Locale. Mithilfe von Locales können wir nationale Eigenheiten der Formatierung nutzen, um beispielsweise gezielt eine US-amerikanische oder auch deutsche Formatierung von Gleitkommazahlen zu bewirken. – Diesen Aspekt lassen wir jetzt unberücksichtigt.

Wir konzentrieren uns auf die Parameter format und args. Die drei Punkte vor args heißen Ellipse und bedeuten, dass an dieser Stelle eine flexible Anzahl an Argumenten übergeben werden kann.

Im Einzelnen:

  • format : Ein String-Parameter, der mithilfe von Platzhaltern das gewünschte Ausgabeformat beschreibt.
  • args: Ein oder auch mehrere Argumente, die formatiert ausgegeben werden sollen. Mehrere Argumente sind durch Kommata separiert zu notieren.

Platzhalter

Die Platzhalter haben die Form

%[arg$][flags][width][.precision]conversion

Die Komponenten in [eckigen Klammern] sind optional.

Erläuterung der einzelnen Komponenten:

  • arg$: Optional. Eine Zahl gefolgt von einem $-Zeichen, das angibt, welches der zusätzlichen Argumente verwendet werden soll. Die Argumentnummern beginnen bei 1. Dies kann durch ein < ersetzt werden, das angibt, dass das Argument aus dem vorherigen Platzhalter verwendet werden soll.
  • flags: Optional. Eine Folge beliebiger der folgenden Zeichen:
    • -: Richtet die Ausgabe linksbündig aus, indem anstelle von links rechts Füllzeichen hinzugefügt werden.
    • +: Bewirkt, dass positiven Zahlen ein „+" vorangestellt wird.
    • : (Ein Leerzeichen) Dies fügt ein Leerzeichen vor positiven Zahlen ein, hauptsächlich damit die Ziffern mit den Ziffern der negativen Zahlen ausgerichtet werden können. 0: Füllt Zahlen links mit Nullen auf.
    • ,: Gruppiert Ziffern (z. B. nach Tausendern) und fügt Trennzeichen zwischen den Gruppen ein.
    • .precision: Optional. Ein . gefolgt von einer ganzen Zahl, die angibt, wie viele Dezimalstellen in den formatierten Daten angezeigt werden sollen.
  • width: Optional. Eine ganze Zahl, die die Mindestanzahl an Zeichen angibt, die die Ausgabe einnehmen soll. Bei Bedarf werden Leerzeichen links hinzugefügt, um diese Anzahl zu erreichen, oder rechts, wenn das Flag - verwendet wird.
  • conversion: Erforderlich. Ein Zeichen, das angibt, wie die Daten eines Arguments dargestellt werden sollen. Wenn das Zeichen groß geschrieben ist, werden die Daten nach Möglichkeit in Großbuchstaben formatiert. Die Liste der möglichen Zeichen ist in der folgenden Tabelle aufgeführt.

Die Formatbeschreibung, der Formatstring, ist ein ganz normaler String. Ich habe ihn Ihnen hier vollständig an die Hand gegeben. Die möglichen Konvertierungen stelle ich lediglich in einer stark reduzierten Form vor. Sie können Sie bei Bedarf unter der URL: https://docs.oracle.com/en/java/javase/25/docs/api/java.base/java/util/Formatter.html#syntax in der Java-Dokumentation nachschlagen.

Die Fülle der Optionen ist im ersten Moment geradezu erschlagend, weshalb auch erfahrene Programmierer oftmals gezwungen sind, nachzuschauen.

Die wichtigsten Konvertierungen sollten Sie sich jedoch merken. Sie werden sie stets und ständig nutzen wollen.

  • d: dezimale Ganzzahl (byte, short, int, long)
  • f: dezimale Gleitkommazahl (float, double)
  • c: Zeichen (char)
  • s: Zeichenkette (String)
  • b: Wahrheitswert (boolean)

Der Formatstring besteht nicht nur aus den oben beschriebenen Platzhaltern. Er beinhaltet ebenso die konstanten Teile Ihrer Ausgabe. Stellen Sie sich den Formatstring daher als Vorlage dessen vor, was Sie ausgeben wollen. Die darin enthaltenen Platzhalter werden dann für die tatsächliche Ausgabe, mit den als Argumenten übergebenen Werten ausgefüllt.

Wir betrachten ein paar Beispiele gängiger Formatierungen, die Ihnen den Gebrauch des Formatstrings mit dessen Platzhaltern, in Verbindung mit den auszugebenden Argumenten, demonstrieren.

Hinweis: printf() fügt der Ausgabe, analog zu print() keinen automatischen Zeilenumbruch hinzu. Sie können, um dies zu erreichen, die Escape-Sequenz \n verwenden.

 1/**
 2 * Demonstration grundlegender Formatierungen mittels {@link java.io.PrintStream#printf}.
 3 */
 4public class PrintF {
 5    /**
 6     * Hauptprogramm. Formatierte Ausgaben auf der Standardausgabe.
 7     * 
 8     * @param args nicht genutzt
 9     */
10    public static void main(String[] args) {
11        String s = "Hallo, Welt!";
12        int    i = 42;
13        double d = 123.45678;
14
15        // Ausgabe analog zu println()
16        System.out.printf("%s\n", s);
17        // rechtsbündige Ausgabe, mindestens 20 Zeichen breit
18        System.out.printf("|%20s|\n", s);
19        // linksbündige Ausgabe, mindestens 20 Zeichen breit
20        System.out.printf("|%-20s|\n", s);
21
22        // Ausgabe analog zu println()
23        System.out.printf("%d\n", i);
24        // rechtsbündige Ausgabe, mindestens 5 Zeichen breit
25        System.out.printf("|%5d|\n", i);
26        // linksbündige Ausgabe, mindestens 5 Zeichen breit
27        System.out.printf("|%-5d|\n", i);
28        // rechtsbündig Ausgabe, mindestens 5 Zeichen breit mit führenden Nullen
29        System.out.printf("|%05d|\n", i);
30
31        // Ausgabe analog zu println()
32        System.out.printf("%f\n", d);
33        // rechtsbündige Ausgabe, 8 Zeichen breit, 2 Nachkommastellen
34        System.out.printf("|%8.2f|\n", d);
35    }
36}

Listing 2.22: PrintF.java

Beachten Sie den Unterschied bei der Ausgabe von Gleitkommazahlen. Wenn Ihr Betriebssystem deutsch konfiguriert ist, verwendet printf() ein Dezimalkomma, während println() einen Dezimalpunkt ausgibt. Wir beobachten die gleichen Inkonsistenzen, die Sie bereits bei der interaktiven Eingabe erlebt haben. Das kann (unter anderem) mit dem Parameter Locale (siehe oben) gesteuert werden. Auf Unix-Systemen können Sie auch einen Alias definieren, der java stets so aufruft, amerikanische Konventionen zu beachten.

alias java="`which java` -Duser.language=en -Duser.country=US"

Aktuell belassen wir es aber so, wie es ist. – Ich werden Ihnen später eine Bibliothek an die Hand geben, die für konsistente Ein- und Ausgaben sorgt, ohne dass Sie selbst darauf achten müssen.

Experiment: Ändern Sie das Programm und übergeben Sie einer Formatangabe für eine Gleitkommazahl eine Zeichenkette. Was beobachten Sie? – Korrekt, Sie müssen die Kompatibilität zwischen Formatangabe und auszugebendem Datentyp beachten!

Exkurs: Zentrierte Ausgabe

Sie werden vielleicht eine vorgegebene Möglichkeit vermissen, eine Ausgabe zu zentrieren. Das müssen Sie in Java eigenständig lösen. Die gegebenen Formatierungsmöglichkeiten reichen aber aus.

Die Lösung erfolgt in drei Schritten:

  1. Ermitteln die die Längen der Teilstücke Ihrer Ausgabe: Länge des auszugebenden Wertes, füllende Leerzeichen links und rechts davon

  2. Konstruieren Sie den Formatstring

  3. Geben Sie die fertig konstruierte Ausgabe aus.

 1/**
 2 * Der Quellcode von {@code PrintFCentered} zeigt, wie in Java
 3 * zentrierte Ausgabe realisiert werden können.
 4 */
 5public class PrintFCentered {
 6    /**
 7     * Hauptprogramm. Zentrierte Ausgabe der Zeichenkette "Hallo, Welt!"
 8     * mit einer Ausgabebreite von 20 Zeichen.
 9     * 
10     * @param args nicht genutzt
11     */
12    public static void main(String[] args) {
13        String s = "Hallo, Welt!";
14        int width = 20;                                   // Ausgabebreite
15        int blanks_lft = (width - s.length()) / 2;        // Anzahl Leerzeichen links
16        int blanks_rgt = width - blanks_lft - s.length(); // Anzahl Leerzeichen rechts
17
18        // Formatstring konstruieren, Beispiel: "|%5%s%4s|\n"
19        String format = "|%" + blanks_lft + "s%s%" + blanks_rgt +"s|\n";
20
21        System.out.printf(format, " ", s, " ");
22    }
23}

Listing 2.23: PrintFCentered.java

Einfachere Ein-/Ausgabe mit Komfort

Im vorangegangenen Abschnitt haben Sie praktische Erfahrungen mit einigen der in Java integrierten Ein-/Ausgabemechanismen sammeln können. Diese sind extrem mächtig, viel mächtiger als das, was wir im Seminar benötigen. Diese Mächtigkeit hat jedoch ihren Preis. Um alle Features nutzen zu können, die Java uns anbietet, bedarf es fundierten Hintergrundwissens und Sie benötigen eine gewisse Einarbeitungszeit.

Mit der Datei PAToolsUdemy.jar stelle ich Ihnen im Downloadbereich zu diesem Seminar eine Bibliothek zur Verfügung, die Ein-/Ausgabe-Operationen weiter abstrahiert. Diese Bibliothek ist weder so mächtig noch so flexibel wie das, was Java Ihnen in der Standard-Bibliothek zur Verfügung stellt. Dafür sind die Module in PAToolsUdemy.jar deutlich einfacher zu nutzen. Sie decken alle Features ab, die wir jetzt und zukünftig in diesem Seminar benötigen. Falls noch nicht geschehen, installieren Sie die Bibliothek. Wie Sie das tun, habe ich Ihnen im Anhang des Skripts, ab Seite , ausführlich beschrieben. Führen Sie alle Installationsschritte entsprechend des von Ihnen verwendeten Betriebssystems gewissenhaft durch. Vergessen Sie nicht den Suchpfad für Java-Klassen, die Systemvariable CLASSPATH, entsprechend zu definieren. Korrekt konfiguriert findet Java dann ein Paket namens de.pakad. Ich nenne die Bibliothek daher kurz pakad-Bibliothek. Darin befinden sich die Module, die ich Ihnen im Folgenden vorstelle und die wir ab sofort nutzen wollen. Eventuell notwendige Konfigurationen habe ich für Microsoft Visual Studio Code und IntelliJ beschrieben. Verwenden Sie eine andere Entwicklungsumgebung, können Sie sich an meinen Konfigurationsbeschreibungen orientieren.

Wir konzentrieren uns vorerst auf die folgenden drei Klassen:

  • StdIn: Standardeingabe,
  • StdOut: Standardausgabe und
  • StdErr: Standardfehlerausgabe.

Ich werde Sie jetzt mit den Features dieser Klassen vertraut machen, die wir bislang mithilfe der Java-Standardbibliothek umgesetzt haben.

Grundsätzlich ist die pakad-Bibliothek so vorkonfiguriert, dass Sie als einheitliches Ein-/Ausgabeformat von Zahlen das amerikanische Format nutzt. Das deckt sich mit dem, was Sie in der allgemein verfügbaren Java-Literatur und auch in der Java-Dokumentation vorfinden. Des Weiteren wird von technischen Details, wie beispielsweise einer Ressourcenfreigabe nach Gebrauch eines Scanners, abstrahiert.

StdIn

Wir beginnen mit den readln*() Methoden, die StdIn anbietet. Allen Methoden mit dem Namensbestandteil ln lesen stets interaktiv eine vollständige Eingabezeile.

Weisen Sie innerhalb Ihres Programms nicht alle Teile der Eingabezeile zu, wird der nicht verwertete Teil verworfen.

Tabelle 2.5: readln*()-Methoden in StdIn

Methode Beschreibung Rückgabedatentyp
readlnDouble() Einlesen einer Gleitkommazahl double
readlnInt() Einlesen einer ganzen Zahl int
readlnLine() Einlesen einer kompletten Zeile String
readlnString() Einlesen des ersten Tokens String

Die Methoden zum Einlesen von Zahlen versuchen die Eingabe entsprechend ihres jeweiligen Rückgabetyps zu interpretieren. Schlägt das fehl, weil ein Wert nicht korrekt geschrieben wurde, wird eine Ausnahme geworfen. Das Programm bricht mit einer entsprechenden Fehlermeldung ab.

Zu allen Methoden existiert ein Pendant, welches einen String als Parameter akzeptiert. Der übergebene String wird als Eingabeaufforderung ausgegeben.

StdOut und StdErr

StdOut und StdErr offerieren jeweils die Methoden print(), println() und printf(). Sie verhalten sich exakt so, wie Sie das bereits kennen. Ausnahme: Länderspezifische Formatierungen werden konsistent, auch zur Notation im Code, im US-amerikanischen Format durchgeführt.

Beispiel zu StdIn, StdOut, StdErr

Ich greife hier ein bereits bekanntes Programm auf, die ich lediglich umformuliere, um die pakad-Bibliothek zu nutzen. Sie werden leicht die Analogien zu dem erkennen, was Sie bereits gelernt haben.

 1import de.pakad.udemy.StdOut;
 2import de.pakad.udemy.StdIn;
 3
 4/**
 5 * {@code Annuity} berechnet eine jährliche Annuität.
 6 */
 7public class Annuity {
 8    /**
 9     * Hauptprogramm. Berechnung einer jährlichen Annuität:
10     * 
11     * \[
12     * A = K \cdot \frac{i \cdot (1+i)^n}{(1+i)^n - 1}
13     * \]
14     * 
15     * mit:<br>
16     * \(i\) = Zinssatz<br>
17     * \(K\) = Anfangskapital und<br>
18     * \(n\) = Laufzeit in Jahren
19     * 
20     * @param args nicht genutzt
21     */
22    public static void main(String[] args) {
23        double A, K, i, n;
24
25        // Eingabe
26        K = StdIn.readlnDouble("Kapital (K)...........: ");
27        i = StdIn.readlnDouble("Zinssatz (i)..........: ");
28        n = StdIn.readlnDouble("Laufzeit in Jahren (n): ");
29
30        // Verarbeitung
31        A = K * (i * Math.pow(1+i, n)) / (Math.pow(1+i, n) - 1);
32
33        // Ausgabe
34        StdOut.printf("Annuität..............: %.2f", A);
35    }
36}

Listing 2.24: Annuity.java (Version 2)

Sie machen die Klassen StdIn und StdOut via import-Anweisung im Programm bekannt. Ist das geschehen, können Sie die beschriebenen Methoden nutzen.

Das Programm ist jetzt bedeutend kürzer als in der ursprünglichen Version. Gleitkommazahlen werden konsistent im US-amerikanischen Format ein- und ausgegeben. Sie brauchen sich nicht um die Freigabe reservierter Ressourcen kümmern.

Beispielsitzungen:

$ java Annuity      
Kapital (K)...........: 1000.0
Zinssatz (i)..........: 0.08
Laufzeit in Jahren (n): 10
Jährliche Annuität....: 149.03
$ java Annuity
Kapital (K)...........: 1000.0    BlaBlaBla
Zinssatz (i)..........: 0.08
Laufzeit in Jahren (n): 10
Jährliche Annuität....: 149.03
$ _

In der zweiten Sitzung erkennen Sie, dass die Methode readlnDouble() nur die Zahl 1000.0 liest und den Rest der Eingabezeile verwirft. - Dieses Verhalten erspart Ihnen, gerade zu Beginn Ihres Lernens, eine Menge Ärger und Frustration.

Programmablaufsteuerung

In unseren bisherigen Programmen wurden die jeweiligen Anweisungen nur je einmal und stets in derselben vorgegebenen Reihenfolge ausgeführt. Die Anweisungen dieser Programme bilden jeweils eine Anweisungssequenz.

In der Regel sind Programme jedoch komplizierter. Die Abfolge, in welcher die Anweisungen ausgeführt werden, wie oft eine Anweisung (oder Anweisungsfolge) hintereinander ausgeführt wird, kann variieren. Die damit verbundenen Techniken fassen wir unter dem Begriff Ablaufsteuerung (engl. control flow) zusammen.

Wir betrachten jetzt Java-Anweisungen, die Verzweigungen (Fallunterscheidungen) und Schleifen (Wiederholungen) ermöglichen. In Abhängigkeit von Bedingungen wird entschieden, ob eine oder mehrere Anweisungen ausgeführt werden oder nicht, beziehungsweise wie häufig sie ausgeführt werden.

Fallunterscheidungen/Verzweigungen

if-Anweisung

Viele Berechnungen erfordern, dass abhängig von der Eingabe unterschiedliche Verarbeitungsschritte durchgeführt werden. Die if-Anweisung stellt eine Möglichkeit hierzu dar.

if prüft eine Bedingung. Ist diese wahr, werden die Anweisungen des davon abhängigen Anweisungsblocks ausgeführt. Ist dies nur eine einzige Anweisung, kann auf die geschweiften Klammern verzichtet werden. Trifft die Bedingung nicht zu, wird die Programmausführung hinter der if-Anweisung fortgesetzt.

1
2
3
if (boolescher Ausdruck) {
    Anweisungen  // mehrere abhängige Anweisungen sind mit {} zu klammern 
}

Als erstes Anwendungsbeispiel nehmen wir mal an, dass Sie den Absolutwert (Betrag) eines int-Wertes x berechnen wollen.

1
if (x < 0) x = -x;

Als zweites Beispiel ordnen wir zwei Zahlen in eine aufsteigende Reihenfolge.

1
2
3
4
5
if (x < y) {
    int temp = x;
    x = y;
    y = temp;
}

Um die beiden Zahlen in die korrekte Reihenfolge zu bringen, werden die Inhalte ihrer Variablen nötigenfalls vertauscht. Das Prinzip des Tauschens erfordert eine temporäre Variable als Zwischenspeicher. Das ist ganz analog zum Vertauschen von Flüssigkeiten in zwei Gläsern. Dazu benötigen Sie ein drittes Glas. Sie können die Flüssigkeiten der beiden Gläser nicht direkt tauschen. Ein Glas muss zuerst in das dritte Glas, welches als Zwischenspeicher dient, gegossen werden.

Wir können die if-Anweisung um eine else-Klausel erweitern, um auszudrücken, das eine von zwei Alternativen ausgeführt werden soll, je nachdem, ob der boolesche Ausdruck zu true oder false evaluiert.

1
2
if (boolescher Ausdruck) { Anweisungen } // wenn wahr
else                     { Anweisungen } // sonst

Als Beispiel soll der größte Wert zweier Variablen, x und y, in der Variablen max gespeichert werden.

1
2
if (x > y) max = x;
else       max = y;

Das Programm CoinToss.java simuliert einen fairen Münzwurf. Dazu nutzen wir eine Pseudo-Zufallszahl, die wir mithilfe von random() aus der Klasse Math generieren. Die Methode random() gibt bei jedem Aufruf eine gleich verteilte Pseudo-Zufallszahl vom Typ double, aus dem Intervall $[0, 1)$, zurück.

 1import de.pakad.udemy.StdOut;
 2
 3/**
 4 * Simulation eines fairen Münzwurfs.
 5 */
 6public class CoinToss {
 7    /**
 8     * Hauptprogramm.
 9     * 
10     * @param args nicht genutzt
11     */
12    public static void main(String[] args) {
13        if (Math.random() < 0.5)
14            StdOut.println("Kopf");
15        else
16            StdOut.println("Zahl");
17    }
18}

Listing 2.25: CoinToss.java

Wir sprechen von Pseudo-Zufallszahlen, weil der Computer keine echten Zufallszahlen generieren kann. Er kann lediglich Zahlen generieren, die viele der Eigenschaften von Zufallszahlen erfüllen. – Dies angemerkt, werde ich in Zukunft dennoch vereinfachend von Zufallszahlen sprechen.

Als bereits komplexeres Beispiel implementieren wir Midnight.java, benannt nach der bekannten „Mitternachtsformel" zur Nullstellenbestimmung quadratischer Polynome der folgenden Gestalt:

$$f(x) = ax^2 + bx + c$$

Die Koeffizienten $a$, $b$ und $c$, jeweils vom Typ double, werden interaktiv eingelesen.

Wenn Ihnen die Mitternachtsformel nicht mehr aus der Schule gegenwärtig ist, drucke ich sie hier, als Erinnerungshilfe, nochmals ab:

\[x_1 = \frac{-b + \sqrt{b^2-4ac}}{2a} \text{ und } x_2 = \frac{-b - \sqrt{b^2-4ac}}{2a}\]
 1import de.pakad.udemy.StdIn;
 2import de.pakad.udemy.StdOut;
 3
 4/**
 5 * Nullstellenberechnung für quadratische Gleichungen der Form:
 6 * \(f(x) = ax^2 + bx +c\).
 7 * <p>
 8 * <strong>Hinweis</strong>: Es können nur reelle Lösungen berechnet
 9 * werden.
10 */
11public class Midnight {
12    /**
13     * Hauptprogramm. Die Koeffizienten {@code a}, {@code b} und {@code c}
14     * werden interaktiv eingelesen.
15     * 
16     * @param args nicht genutzt
17     */
18    public static void main(String[] args) {
19        double a = StdIn.readlnDouble("Koeffizient a: ");
20        double b = StdIn.readlnDouble("Koeffizient b: ");
21        double c = StdIn.readlnDouble("Koeffizient c: ");
22
23        double discrminant = (b*b - 4*a*c);
24
25        if (discrminant < 0)
26            StdOut.println("Keine reelle Lösung!");
27        else {
28            double d = Math.sqrt(discrminant);
29            StdOut.printf("x1 = %8.4f\n", (-b + d) / (2.0 * a));
30            StdOut.printf("x2 = %8.4f\n", (-b - d) / (2.0 * a));
31        }
32    }
33}

Listing 2.26: Midnight.java

Math.sqrt() berechnet die Quadratwurzel einer positiven reellen Zahl. Negative Zahlen, deren Wurzel eine komplexe Zahl ist, verarbeitet Math.sqrt() nicht. Daher prüfen wir, ob discriminant negativ ist. Wenn ja, geben wir eine entsprechende Meldung aus und fangen mögliche Fehler ab.

Im Dokumentationskommentar können Sie übrigens erkennen, in diesem sogar mit mathematischen Formelsatz integrieren zu können. Dazu muss dann aber JavaScript in die HTML-Dokumentation eingebunden werden. Hier der Ausschnitt aus dem Aufruf von javadoc, der das erledigt.

"<script type='text/javascript' id='MathJax-script' async src='https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js'></script>"

Beispielsitzungen:

$ java Midnight
Koeffizient a: 1.0
Koeffizient b: -3.0
Koeffizient c: 2.0
x1 =   2.0000
x2 =   1.0000
$ java Midnight
Koeffizient a: 1.0
Koeffizient b: 1.0
Koeffizient c: 1.0
Keine reelle Lösung gefunden.
$ _

Exkurs: Ternärer Operator

Der ternäre Operator ist eine Kurzschreibweise für eine mit einer Zuweisung kombinierten if-Anweisung. Beispiel:

1
2
if (x > y) max = x;
else       max = y;

Mithilfe des ternären Operators können wir dies kürzer ausdrücken.

1
max = x > y ? x : y;

Zu lesen: „Wenn x > y, dann gib x zurück, sonst y. Weise Ergebnis max zu."

Anmerkung: Der ternäre Operator unterstützt extrem kurze Ausdrucksweisen. Auf der anderen Seite leidet aber auch die Lesbarkeit des Programmcodes. Ich werde ihn daher weitestgehend meiden.

switch-case-Anweisung

Ein switch-Statement in Java prüft eine Variable auf Gleichheit mit mehreren Werten (case-Labels) und führt bei Übereinstimmung den entsprechenden Codeblock aus, was eine kompakte Alternative zu längeren und ineinander geschachtelten if-else-Ketten sein kann. default ist eine optionale Alternative für Nicht-Treffer.

Anmerkung: Die hier gezeigte Syntax wurde mit Java 14 eingeführt. Davor sah sie etwas anders aus und wahr weniger klar verständlich.

1
2
3
4
5
6
7
8
9
switch (variable) {
    case wert1, wert2 -> {             // Mehrere Werte gruppieren
        // Code für wert1 ODER wert2
    }
    case wert3 -> System.out.println("Nur für wert3"); // ein Wert
    default -> {
        // Code, wen keine Übereinstimmung
    }
}

Mehrere abhängige Anweisungen sind mittels geschweifter Klammern zu einem Anweisungsblock zusammenzufassen.

Vor Java 14 sah die Syntax wie folgt aus:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
switch (variable) {
    case wert1, wert2:              // Mehrere Werte gruppieren
        // Code für wert1 ODER wert2
        break;
    case wert3: 
        System.out.println("Nur für wert3"); 
        break;
    default: 
        // Code, wen keine Übereinstimmung
}

Zur Bildung von Anweisungsblöcken waren keine geschweiften Klammern erforderlich. Bei Zutreffen einer Bedingung wurden die dahinter notierten Anweisungen ausgeführt. Die Abarbeitung wurde gestoppt, wenn sie auf ein break traf. Fehlte ein solches break wurden die Anweisungen des folgenden Blocks ausgeführt – „Durchfallprinzip". Nur im default-Block konnte generell auf ein break verzichtet werden, da es den letzten Block bildete. Diese Syntax ist nach wie vor valide. Sie weicht jedoch von der üblichen Java-Syntax ab. Die alte switch-case-Syntax ist der Programmiersprache C entlehnt.

Als Beispiel, die neue Syntax verwendend, wollen wir die Jahreszeit anhand eines einzugebenden Monats ausgeben. Der Monat soll als ganze Zahl (\([1, 12]\)) interaktiv eingegeben werden.

 1import de.pakad.udemy.StdIn;
 2import de.pakad.udemy.StdOut;
 3
 4/** 
 5 * Zuordnung eines Monats zu einer Jahreszeit.
 6 * 
 7 * <ul>
 8 * <li>Frühling: März, April, Mai</li>
 9 * <li>Sommer: Juni, Juli, August</li>
10 * <li>Herbst: September, Oktober, November</li>
11 * <li>Winter: Dezember, Januar, Februar</li>
12 * </ul>
13*/
14public class Season {
15    /**
16     * Hauptprogramm. Ein Monat wird in Form einer ganzen Zahl
17     * interaktiv eingelesen und klassifiziert.
18     * 
19     * @param args nicht genutzt
20     */
21    public static void main(String[] args) {
22        int month = StdIn.readlnInt("Bitte eine Monatszahl: ");
23
24        switch (month) {
25            case 3, 4, 5   -> StdOut.println("Frühling");    
26            case 6, 7, 8   -> StdOut.println("Sommer");
27            case 9, 10, 11 -> StdOut.println("Herbst");
28            case 12, 1, 2  -> StdOut.println("Winter");
29            default -> StdOut.println("Unbekannte Jahreszeit");
30        }
31   } 
32}

Listing 2.27: Season.java

Da von den Fällen jeweils nur eine Anweisung abhängig ist, können wir auf Anweisungsblöcke verzichten.

Wiederholungen/Schleifen

Viele Berechnungen sind iterativ. Das grundlegende Konstrukt dafür ist eine Schleife. Mithilfe von Schleifen können wir, in Abhängigkeit einer Bedingung, eine oder auch mehrere Anweisungen wiederholt ausführen. Java unterstützt drei Arten von Schleifen. Je nach Kontext und Anforderung kann die Art der Schleife gewählt werden, die der Situation angemessen ist.

while-Schleife

Die while-Schleife ist die erste der drei Schleifen-Konstruktionen in Java, die wir behandeln wollen. Ihr Aufbau ähnelt der if-Anweisung, hat jedoch eine vollkommen andere Semantik.

1
2
3
while (boolescher Ausdruck) {  // Schleifenkopf
    Anweisungen                // Schleifenrumpf
}

Hier wird der Computer angewiesen, sich wie folgt zu verhalten.

  1. Wenn die Bedingung im Schleifenkopf falsch ist, mache nichts. Andernfalls betrete den Schleifenrumpf und führe die darin notierten Anweisungen aus.

  2. Sind alle Anweisungen des Schleifenrumpfes ausgeführt, wiederhole Schritt 1.

An irgendeinem Punkt muss der Code im Schleifenrumpf etwas am Programmstatus ändern, um die Schleifenbedingung falsch werden zu lassen. Vergisst man das, hat man eine sogenannte Endlosschleife, die nicht terminiert.

while-Schleifen sind immer dann günstig einzusetzen, wenn die Anzahl der Durchführungen der Anweisungen im Schleifenrumpf nicht oder nur schwer vorhersagbar ist. Eventuell sind sie gar nicht auszuführen.

Ein klassisches Beispiel für solch eine Situation bietet der Collatz-Algorithmus, benannt nach dem deutschen Mathematiker Lothar Collatz. Der entdeckte in der 1930er Jahren eine einfach zu bildende Zahlenfolge.

  1. Nimm eine positive ganze Zahl.
  2. Ist diese Zahl gerade, dann halbiere sie; ist sie ungerade, dann verdreifache die Zahl und addiere 1.
  3. Wiederhole Schritt 2 mit der fortgeschriebenen Zahl.

Egal mit welcher Zahl Collatz gestartet ist, die Zahlenfolge mündete immer im Zyklus \(\dots, 4, 2, 1, 4, 2, 1, \dots\). Collatz formulierte daraufhin die Vermutung, dies gelte für alle positiven Ganzzahlen. Beweisen konnte er diese Vermutung nicht. Auch heute steht der Beweis noch aus.

Im Abschnitt Ablaufprotokoll, Kapitel 1, haben den Collatz-Algorithmus bereits einmal manuell durchgeführt. Jetzt wollen wir ein Programm dafür schreiben, welches die Anzahl der Transformationen zählt, die benötigt werden, eine zu Beginn eingegebene Zahl auf den Wert \(1\) zu transformieren. Ist das gelungen, soll das Programm enden und die Anzahl der gezählten Transformationen ausgeben.

Um bei fehlerhaften Eingaben, die den zulässigen Wertebereich verletzen, nicht in die Schleife zu geraten, formulieren wir deren Bedingung so, dass sie bei Eingaben kleiner gleich \(1\) nicht ausgeführt wird.

 1import de.pakad.udemy.StdOut;
 2import de.pakad.udemy.StdIn;
 3
 4/**
 5 * Implementierung einer <i>Collatz</i>-Folge.
 6 * 
 7 * Die Anzahl der Transformationen, eine Eingabezahl auf 1
 8 * zu transformieren, wird gezählt.
 9 */
10public class Collatz {
11    /**
12     * Hauptprogramm. Die Startzahl für die <i>Collatz</i>-Folge
13     * wird interaktiv eingegeben.
14     * 
15     * @param args nicht genutzt
16     */
17    public static void main(String[] args) {
18        int num = StdIn.readlnInt("Bitte eine Startzahl >= 1: ");  // Zahl
19        int cnt = 0;                                               // Zähler
20
21        while (num > 1) {           // wenn Zahl > 1, dann ...
22            if (num % 2 == 0)       //    wenn Zahl gerade, dann ...
23                num = num / 2;      //       halbiere Zahl
24            else                    //    ansonsten ...
25                num = 3 * num + 1;  //       verdreifache Zahl und addiere 1
26            cnt++;                  // inkrementiere Zähler
27        }
28        StdOut.println(cnt);
29    }
30}

Listing 2.28: Collatz.java

Ob eine Zahl gerade oder ungerade ist, prüfen wir, indem wir sie ganzzahlig durch \(2\) teilen. Ist der Rest \(0\), so ist die Zahl gerade, andernfalls ist sie ungerade.

Beispielsitzungen:

$ java Collatz
Ganze Zahl >1: 3
7
$ java Collatz 
Ganze Zahl >1: 5
5
$ java Collatz 
Ganze Zahl >1: 123
46
$ java Collatz 
Ganze Zahl >1: 124
108
$ _

do-while-Schleife

Der wesentliche Unterschied zwischen der while- und der do-while-Schleife besteht darin, dass die Anweisungen im Schleifenrumpf der do-while-Schleife mindestens einmal durchlaufen werden. Die Schleifenbedingung wird nicht im Kopf, sondern im Fuß der Schleife geprüft. Außerdem sind immer geschweifte Klammern zu setzen, auch dann, wenn nur eine Anweisung im Schleifenkörper notiert ist.

1
2
3
do {
    Anweisungen                 // Schleifenrumpf
} while (boolescher Ausdruck);  // Schleifenfuss

Stellen Sie sich vor, Sie wollten zufällig eine Koordinate generieren, die in der Fläche des Einheitskreises liegt. Die folgende Grafik visualisiert die Aufgabenstellung.

Zufallskoordinaten inner- und außerhalb des Einheitskreises
Abbildung 2.1: Zufallskoordinaten inner- und außerhalb des Einheitskreises

Wir können Math.random() nutzen, um die Koordinaten zufällig zu erzeugen. Liegt eine Koordinate außerhalb des Einheitskreises, muss der Vorgang wiederholt werden, andernfalls ist die Aufgabe gelöst. Wir müssen also mindestens einmal, gegebenenfalls aber auch mehrere Male die Anweisungen zum Generieren der Koordinaten ausführen.

 1import de.pakad.udemy.StdOut;
 2
 3/**
 4 * Zufällige Generierung eines Koordinatenpaares im Einheitskreis.
 5 */
 6public class UnitCircle {
 7    /**
 8     * Hauptprogramm.
 9     * 
10     * @param args nicht genutzt
11     */
12    public static void main(String[] args) {
13        double x, y;  // x- und y-Zufallskoordinate
14
15        int cnt = 0;
16        do {
17            x = 2 * Math.random() - 1;         // Projektion in Einheitskreis
18            y = 2 * Math.random() - 1;         // dto.
19
20            cnt++;
21        } while (Math.sqrt(x*x + y*y) > 1.0);  // Pythagoras
22
23        StdOut.printf("(%.4f, %.4f)\n", x, y);
24        StdOut.println(cnt + " Versuch(e)");
25    }
26}

Listing 2.29: UnitCircle.java

for-Schleife

Die for-Schleife bietet keine grundsätzlichen Erweiterungen dessen, was wir nicht auch schon mit der while-Schleife ausdrücken können. Wie die do-while-Schleife, die auch mittels einer einfachen while-Schleife abbildbar wäre, bekommen wir eine alternative Schleifenkonstruktion angeboten, die je nach Situation intuitiver und bequemer einzusetzen ist.

1
2
3
for (Initialisierung; boolescher Ausdruck; Inkrement) { // Schleifenkopf
    Anweisungen                                         // Schleifenrumpf
}

Initialisierung und Inkrement dürfen jeweils auch eine Liste von Ausdrücken sein, die mit Kommata separiert werden.

Die for-Schleife ist immer dann gut einsetzbar, wenn die Anzahl der Wiederholungen vor der Ausführungen der Anweisungen im Schleifenrumpf bestimmbar ist.

Sie wollen den dezimalen Wert einer binären Zeichenkette als positive Ganzzahl interpretieren. Dann haben Sie den Stellenwert einer jeden Binärziffer zu bewerten. Es sind so viele Bewertungen auszuführen, wie die eingegebene Binärzahl Ziffern hat.

Der im folgenden beschriebene und implementierte Algorithmus repräsentiert das sogenannte Horner-Schema für binäre positive Ganzzahlen. Dabei wird das Stellenwertsystem, welches dem Aufbau einer Zahl zugrunde liegt, ausgenutzt.

Im Alltag verwenden wir, weil wir in der Regel ein dezimales Stellenwertsystem verwenden, ein Stellenwertsystem mit der Basis \(10\). Bei binären Zahlen haben wir ein Stellenwertsystem mit der Basis \(2\).

Das Vorgehen der Wertermittlung ist immer gleich, unabhängig von der konkret gewählten Basis.

Beispiel:

\[1111_2 = 1 \cdot 2^3 + 1 \cdot 2^2 + 1 \cdot 2^1 + 1 \cdot 2^0 = 15_{10}\]

Wenn wir nun auf der rechten Seite einer Binärzahl beginnen, erhalten wir im Beispiel zuerst den Wert \(1 \cdot 1 = 1\) . Dann kommen wir zur nächsten \(1\), der zweiten Ziffer von rechts. Bis dahin bewertet, ergibt sich \(2 \cdot 1\) plus die \(1\), die wir schon ermittelt haben, was \(3\) ist. Der nächste Schritt liefert dann \(2 \cdot 3 + 1\). Das ist \(7\). Es bleibt die Binärziffer ganz links: \(2 \cdot 7 + 1 = 15\).

Das Prinzip funktioniert natürlich auch dann, wenn Nullen im Binärstring stehen.

\[1001_2 = 1 \cdot 2^3 + 0 \cdot 2^2 + 0 \cdot 2^1 + 1 \cdot 2^0 = 9_{10}\]

Das Programm hierzu soll die zu evaluierende Binärzahl soll als String einlesen. Auf die einzelnen Zeichen eines Strings können wir mit charAt() zugreifen, indem wir in den Klammern den Index des Zeichens eintragen. Zur Wiederholung: Der Index einer Zeichenkette mit \(n\) Zeichen beginnt bei \(0\) und endet bei \(n-1\).

 1import de.pakad.udemy.StdIn;
 2import de.pakad.udemy.StdOut;
 3
 4/**
 5 * Umrechnung einer positiven Binärzahl in eine Dezimalzahl.
 6 * <p>
 7 * <strong>Achtung</strong>: Der implementierte Algorithmus berücksichtigt
 8 *                           nicht das 2er-Komplement für negative Zahlen.
 9 */
10public class Bin2Dec {
11    /**
12     * Hauptprogramm. Interaktive Eingabe eines Binärmusters (max. 31 Ziffern),
13     * welches in einen positiven {@code int}-Wert umgerechnet wird.
14     * <p>
15     * <strong>Achtung</strong>: Bei mehr als 31 Binärziffern tritt ein 
16     *                           Überlauf des Datentyps {@code int} ein.
17     * 
18     * @param args nicht genutzt
19     */
20    public static void main(String[] args) {
21        String bin = StdIn.readlnString("Binärzahl: ");
22        int    val = 0;                                // Dezimalwert
23
24        // Horner-Schema: Summe der Stellenwerte für jede 0 und 1
25        for (int i=0; i < bin.length(); i++)
26            val = 2 * val + (bin.charAt(i) - '0');
27
28        StdOut.println(val);
29    }
30}

Listing 2.30: Bin2Dec.java

Hinweis: Der gezeigte Algorithmus ist nicht geeignet, um negative Zahlen zu bewerten. Die werden im sogenannten 2er-Komplement codiert, welches wir hier nicht behandelt haben.

Beispielsitzungen:

$ java Bin2Dec 1111  
15
$ java Bin2Dec 1001  
9
$ java Bin2Dec 101010
42 
$ _

Abbrechen von Schleifen

break-Anweisung

Manchmal kann es erforderlich sein, eine Schleife direkt zu verlassen, ohne sie bis zum Ende auszuführen. Hierfür stellt Java die break-Anweisung zur Verfügung. Der folgende Code ist beispielsweise dazu geeignet, um zu prüfen, ob eine gegebene ganze Zahl \(N > 1\) eine Primzahl ist:

 1import de.pakad.udemy.StdIn;
 2import de.pakad.udemy.StdOut;
 3
 4/**
 5 * Primzahlenüberprüfung für {@code int}-Werte. 
 6 */
 7public class Prime {
 8    /**
 9     * Hauptprogramm. Eine ganze int-Zahl wird interaktiv eingelesen
10     * und Ihre Primzahleneigenschaft untersucht. 
11     * 
12     * @param args nicht genutzt
13     */
14    public static void main(String[] args) {
15        int N = StdIn.readlnInt("Bitte eine positive int-Zahl: ");
16        int i;  // Laufvariable, Teiler
17
18        for (i=2; i<=N/i; i++)
19            if (N % i == 0) break;  // Teiler gefunden - NICHT prim
20
21        // Warum ist die Schleife abgebrochen?
22        if (i > N/i)                // Schleife ist bis zum Ende gelaufen
23            StdOut.println(N + " ist prim.");
24        else                        // Schleife wurde abgebrochen
25            StdOut.println(N + " ist NICHT prim.");
26    }
27}

Listing 2.31: Prime.java

Wir beginnen mit der kleinsten Primzahl, der \(2\) zu prüfen. Werte, die größer sind als N/i können keine Teiler von N mehr sein. Daraus ergeben sich die Grenzen der for-Schleife.

Es existieren zwei Möglichkeiten, wie diese Schleife verlassen werden kann. Entweder wird die break-Anweisung ausgeführt, weil N % i gleich Null ist, N also einen Teiler besitzt der ungleich N selbst ist. Dann ist N keine Primzahl und wir können uns weitere Prüfungen sparen. Alternativ ist die Schleife komplett durchlaufen worden. Nach dem letzten Schleifendurchlauf ist i, wenn N eine Primzahl ist, um \(1\) größer als N / i.

continue-Anweisung

Java bietet auch die Möglichkeit, zum nächsten Schleifendurchlauf zu springen. Das ist die continue-Anweisung. Alle nachfolgenden Anweisungen, die im Schleifenrumpf notiert sind, werden dann übersprungen und die Schleife wird vom Beginn des Schleifenrumpfes an wiederholt, so die Schleifenbedingung noch gilt.