Rheinwerk Computing < openbook >


 
Inhaltsverzeichnis
Materialien
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Arrays und ihre Anwendungen
5 Der Umgang mit Zeichenketten
6 Eigene Klassen schreiben
7 Objektorientierte Beziehungsfragen
8 Ausnahmen müssen sein
9 Geschachtelte Typen
10 Besondere Typen der Java SE
11 Generics<T>
12 Lambda-Ausdrücke und funktionale Programmierung
13 Architektur, Design und angewandte Objektorientierung
14 Java Platform Module System
15 Die Klassenbibliothek
16 Einführung in die nebenläufige Programmierung
17 Einführung in Datenstrukturen und Algorithmen
18 Einführung in grafische Oberflächen
19 Einführung in Dateien und Datenströme
20 Einführung ins Datenbankmanagement mit JDBC
21 Bits und Bytes, Mathematisches und Geld
22 Testen mit JUnit
23 Die Werkzeuge des JDK
A Java SE-Module und Paketübersicht
Stichwortverzeichnis


Download:

- Listings, ca. 2,7 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 2 Imperative Sprachkonzepte
Pfeil 2.1 Elemente der Programmiersprache Java
Pfeil 2.1.1 Token
Pfeil 2.1.2 Textkodierung durch Unicode-Zeichen
Pfeil 2.1.3 Bezeichner
Pfeil 2.1.4 Literale
Pfeil 2.1.5 (Reservierte) Schlüsselwörter
Pfeil 2.1.6 Zusammenfassung der lexikalischen Analyse
Pfeil 2.1.7 Kommentare
Pfeil 2.2 Von der Klasse zur Anweisung
Pfeil 2.2.1 Was sind Anweisungen?
Pfeil 2.2.2 Klassendeklaration
Pfeil 2.2.3 Die Reise beginnt am main(String[])
Pfeil 2.2.4 Der erste Methodenaufruf: println(…)
Pfeil 2.2.5 Atomare Anweisungen und Anweisungssequenzen
Pfeil 2.2.6 Mehr zu print(…), println(…) und printf(…) für Bildschirmausgaben
Pfeil 2.2.7 Die API-Dokumentation
Pfeil 2.2.8 Ausdrücke
Pfeil 2.2.9 Ausdrucksanweisung
Pfeil 2.2.10 Erster Einblick in die Objektorientierung
Pfeil 2.2.11 Modifizierer
Pfeil 2.2.12 Gruppieren von Anweisungen mit Blöcken
Pfeil 2.3 Datentypen, Typisierung, Variablen und Zuweisungen
Pfeil 2.3.1 Primitive Datentypen im Überblick
Pfeil 2.3.2 Variablendeklarationen
Pfeil 2.3.3 Automatisches Feststellen der Typen mit var
Pfeil 2.3.4 Finale Variablen und der Modifizierer final
Pfeil 2.3.5 Konsoleneingaben
Pfeil 2.3.6 Fließkommazahlen mit den Datentypen float und double
Pfeil 2.3.7 Ganzzahlige Datentypen
Pfeil 2.3.8 Wahrheitswerte
Pfeil 2.3.9 Unterstriche in Zahlen
Pfeil 2.3.10 Alphanumerische Zeichen
Pfeil 2.3.11 Gute Namen, schlechte Namen
Pfeil 2.3.12 Keine automatische Initialisierung von lokalen Variablen
Pfeil 2.4 Ausdrücke, Operanden und Operatoren
Pfeil 2.4.1 Zuweisungsoperator
Pfeil 2.4.2 Arithmetische Operatoren
Pfeil 2.4.3 Unäres Minus und Plus
Pfeil 2.4.4 Präfix- oder Postfix-Inkrement und -Dekrement
Pfeil 2.4.5 Zuweisung mit Operation (Verbundoperator)
Pfeil 2.4.6 Die relationalen Operatoren und die Gleichheitsoperatoren
Pfeil 2.4.7 Logische Operatoren: Nicht, Und, Oder, XOR
Pfeil 2.4.8 Kurzschluss-Operatoren
Pfeil 2.4.9 Der Rang der Operatoren in der Auswertungsreihenfolge
Pfeil 2.4.10 Die Typumwandlung (das Casting)
Pfeil 2.4.11 Überladenes Plus für Strings
Pfeil 2.4.12 Operator vermisst *
Pfeil 2.5 Bedingte Anweisungen oder Fallunterscheidungen
Pfeil 2.5.1 Verzweigung mit der if-Anweisung
Pfeil 2.5.2 Die Alternative mit einer if-else-Anweisung wählen
Pfeil 2.5.3 Der Bedingungsoperator
Pfeil 2.5.4 Die switch-Anweisung bietet die Alternative
Pfeil 2.5.5 Switch-Ausdrücke
Pfeil 2.6 Immer das Gleiche mit den Schleifen
Pfeil 2.6.1 Die while-Schleife
Pfeil 2.6.2 Die do-while-Schleife
Pfeil 2.6.3 Die for-Schleife
Pfeil 2.6.4 Schleifenbedingungen und Vergleiche mit == *
Pfeil 2.6.5 Schleifenabbruch mit break und zurück zum Test mit continue
Pfeil 2.6.6 break und continue mit Marken *
Pfeil 2.7 Methoden einer Klasse
Pfeil 2.7.1 Bestandteile einer Methode
Pfeil 2.7.2 Signatur-Beschreibung in der Java-API
Pfeil 2.7.3 Aufruf einer Methode
Pfeil 2.7.4 Methoden ohne Parameter deklarieren
Pfeil 2.7.5 Statische Methoden (Klassenmethoden)
Pfeil 2.7.6 Parameter, Argument und Wertübergabe
Pfeil 2.7.7 Methoden vorzeitig mit return beenden
Pfeil 2.7.8 Nicht erreichbarer Quellcode bei Methoden *
Pfeil 2.7.9 Methoden mit Rückgaben
Pfeil 2.7.10 Methoden überladen
Pfeil 2.7.11 Gültigkeitsbereich
Pfeil 2.7.12 Vorgegebener Wert für nicht aufgeführte Argumente *
Pfeil 2.7.13 Rekursive Methoden *
Pfeil 2.7.14 Die Türme von Hanoi *
Pfeil 2.8 Zum Weiterlesen
 

Zum Seitenanfang

2.5    Bedingte Anweisungen oder Fallunterscheidungen Zur vorigen ÜberschriftZur nächsten Überschrift

Kontrollstrukturen dienen in einer Programmiersprache dazu, Programmteile unter bestimmten Bedingungen auszuführen. Java bietet zum Ausführen verschiedener Programmteile eine if- und if-else-Anweisung sowie die switch-Anweisung. Neben der Verzweigung dienen Schleifen dazu, Programmteile mehrmals auszuführen. Bedeutend im Wort »Kontrollstrukturen« ist der Teil »Struktur«, denn die Struktur zeigt sich schon durch das bloße Hinsehen. Als es noch keine Schleifen und »hochwertigen« Kontrollstrukturen gab, sondern nur ein Wenn/Dann und einen Sprung, war die Logik des Programms nicht offensichtlich; das Resultat nannte sich Spaghetti-Code. Obwohl ein allgemeiner Sprung in Java mit goto nicht möglich ist, besitzt die Sprache dennoch eine spezielle Sprungvariante. In Schleifen erlauben continue und break definierte Sprungziele.

 

Zum Seitenanfang

2.5.1    Verzweigung mit der if-Anweisung Zur vorigen ÜberschriftZur nächsten Überschrift

Die if-Anweisung besteht aus dem Schlüsselwort if, dem zwingend ein Ausdruck mit dem Typ boolean in Klammern folgt. Es folgt eine Anweisung, die oft eine Blockanweisung ist.

Ein Beispiel: Mit der if-Anweisung wollen wir testen, ob der Anwender eine Zufallszahl richtig geraten hat:

Listing 2.18    src/main/java/WhatsYourNumber.java

public class WhatsYourNumber {



public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);



System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();



if ( number == guess ) {

System.out.println( "Super getippt!" );

}



System.out.println( "Starte das Programm noch einmal und rate erneut!" );

}

}

Die Abarbeitung der Ausgabeanweisungen hängt vom Ausdruck im if ab.

  • Ist das Ergebnis des Ausdrucks wahr (number == guess wird zu true ausgewertet), wird die folgende Anweisung, also die Konsolenausgabe mit "Super getippt!", ausgeführt.

  • Ist das Ergebnis des Ausdrucks falsch (number == guess wird zu false ausgewertet), so wird die Anweisung übersprungen, und es wird mit der ersten Anweisung nach der if-Anweisung fortgefahren.

[»]  Programmiersprachenvergleich

In Java muss der Testausdruck für die Bedingung der if-Anweisung immer vom Typ boolean sein – für Schleifenbedingungen gilt das Gleiche. Andere Programmiersprachen wie JavaScript, C(++) oder PHP bewerten einen numerischen Ausdruck als wahr, wenn das Ergebnis des Ausdrucks ungleich 0 ist. So ist in ihnen auch if (10) gültig, was in Java einem if (true) entspräche. In PHP oder JavaScript sind auch der leere String oder ein leeres Array »falsy«.

if-Abfragen und Blöcke

Hinter dem if und der Bedingung erwartet der Compiler eine Anweisung. Ein {}-Block ist eine besondere Anweisung, und die nutzt das Beispiel. Ist nur eine Anweisung in Abhängigkeit von der Bedingung auszuführen, kann die Anweisung direkt ohne Block gesetzt werden. Folgende Varianten sind also identisch:

Explizite Klammerung der Anweisung

Ohne Klammerung der Anweisung

if ( number == guess ) {

System.out.println( "Super!" );

}
if ( number == guess )

System.out.println( "Super!" );

Ein Programmfehler entsteht, wenn der {}-Block fehlt, aber mehrere Anweisungen in Abhängigkeit von der Bedingung ausgewertet werden sollen. Dazu ein Beispiel: Eine if-Anweisung soll testen, ob die geratene Zahl gleich der Zufallszahl war, und dann, wenn das der Fall war, die Variable number mit einer neuen Zufallszahl belegen und eine Ausgabe liefern. Zunächst betrachten wir die semantisch falsche Variante:

if ( number == guess )

number = (int)(Math.random()*5 + 1); System.out.println( "Super getippt!" );

Die Implementierung ist semantisch falsch, da unabhängig vom Test immer die Ausgabe erscheint. Der Compiler ordnet nur die nächstfolgende Anweisung der Fallunterscheidung zu, auch wenn die Optik etwas anderes suggeriert.[ 88 ](In der Programmiersprache Python bestimmt die Einrückung die Zugehörigkeit. ) Dies ist eine große Gefahr für Programmierer, die optisch Zusammenhänge schaffen wollen, die in Wirklichkeit nicht existieren. Der Compiler interpretiert die Anweisungen in folgendem Zusammenhang, wobei die Einrückung die tatsächliche Ausführung widerspiegelt:

if ( number == guess )

number = (int) (Math.random() * 5 + 1);

System.out.println( "Super getippt!" );

Für unsere gewünschte Logik, beide Anweisungen zusammen in Abhängigkeit von der Bedingung auszuführen, heißt es, sie in einen Block zu setzen:

if ( number == guess ) {

number = (int) (Math.random() * 5 + 1);

System.out.println( "Super getippt!" );

}

Grundsätzlich Anweisungen in Blöcke zu setzen – auch wenn nur eine Anweisung im Block steht – ist also nicht verkehrt.

[+]  Tipp

Einrückungen ändern nicht die Semantik des Programms! Einschübe können das Verständnis nur empfindlich stören. Damit das Programm korrekt wird, müssen wir einen Block verwenden und die Anweisungen zusammensetzen. Entwickler sollten Einrückungen konsistent zur Verdeutlichung von Abhängigkeiten nutzen. Es sollte zudem immer nur eine Anweisung in einer Zeile stehen.

Zusammengesetzte Bedingungen

Die bisherigen Abfragen waren sehr einfach, doch kommen in der Praxis viel komplexere Bedingungen vor. Oft im Einsatz sind die logischen Operatoren && (Und), || (Oder), ! (Nicht).

Wenn wir etwa testen wollen, ob

  • eine geratene Zahl number entweder gleich der Zufallszahl guess ist oder

  • eine gewisse Anzahl von Versuchen schon überschritten ist (trials größer 10),

dann schreiben wir die zusammengesetzte Bedingung so:

if ( number == guess || trials > 10 )

Sind die logisch verknüpften Ausdrücke komplexer, so sollten zur Unterstützung der Lesbarkeit die einzelnen Bedingungen in Klammern gesetzt werden, da nicht jeder sofort die Tabelle mit den Vorrangregeln für die Operatoren im Kopf hat.

 

Zum Seitenanfang

2.5.2    Die Alternative mit einer if-else-Anweisung wählen Zur vorigen ÜberschriftZur nächsten Überschrift

Ist die Bedingung erfüllt, wird der if-Zweig durchlaufen. Doch wie lässt es sich programmieren, dass im umgekehrten Fall – die Bedingung ist nicht wahr – auch Programmcode ausgeführt wird?

Eine Lösung ist, eine zweite if-Anweisung mit negierter Bedingung zu schreiben:

if ( number == guess )

System.out.println( "Super getippt!" );

if ( number != guess )

System.out.printf( "Tja, stimmt nicht, habe mir %s gedacht!", number );

Allerdings gibt es eine bessere Lösung: Neben der einseitigen Alternative existiert die zweiseitige Alternative. Das optionale Schlüsselwort else mit angehängter Anweisung veranlasst die Ausführung einer Alternative, wenn der if-Test falsch ist.

Rät der Benutzer aus unserem kleinen Spiel die Zahl nicht, wollen wir ihm die Zufallszahl präsentieren:

Listing 2.19    src/main/java/GuessTheNumber.java

public class GuessTheNumber {



public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);



System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();



if ( number == guess )

System.out.println( "Super getippt!" );

else

System.out.printf( "Tja, stimmt nicht, habe mir %s gedacht!", number );

}

}

Falls der Ausdruck number == guess wahr ist, wird die erste Anweisung ausgeführt, andernfalls die zweite Anweisung. Somit ist sichergestellt, dass in jedem Fall eine Anweisung ausgeführt wird.

Das Dangling-else-Problem

Bei Verzweigungen mit else gibt es ein bekanntes Problem, das Dangling-else-Problem genannt wird. Zu welcher Anweisung gehört das folgende else?

if ( Ausdruck1 )

if ( Ausdruck2 )

Anweisung1;

else

Anweisung2;

Die Einrückung suggeriert, dass das else die Alternative zur ersten if-Anweisung ist. Dies ist aber nicht richtig. Die Semantik von Java (und auch fast aller anderen Programmiersprachen) ist so definiert, dass das else zum innersten if gehört. Daher lässt sich nur der Programmiertipp geben, die if-Anweisungen zu klammern:

if ( Ausdruck1 ) {

if ( Ausdruck2 ) {

Anweisung1;

}

}

else {

Anweisung2;

}

So kann eine Verwechslung gar nicht erst aufkommen. Wenn das else immer zum innersten if gehört und das nicht erwünscht ist, können wir, wie gerade gezeigt, mit geschweiften Klammern arbeiten oder auch eine leere Anweisung im else-Zweig hinzufügen:

if ( x >= 0 )

if ( x != 0 )

System.out.println( "x echt größer null" );

else

; // x ist gleich null

else

System.out.println( "x echt kleiner null" );
Die Tastenkombination (Strg) + Leertaste hinter dem if bietet an, eine if-Anweisung mit Block anzulegen.

Abbildung 2.6    Die Tastenkombination (Strg) + Leertaste hinter dem if bietet an, eine if-Anweisung mit Block anzulegen.

Das böse Semikolon

An dieser Stelle ist ein Hinweis angebracht: Ein Programmieranfänger schreibt gerne hinter die schließende Klammer der if-Anweisung ein Semikolon. Das führt zu einer ganz anderen Ausführungsreihenfolge. Ein Beispiel:

int age = 29;

if ( age < 0 ) ; // inline image logischer Fehler

System.out.println( "Aha, noch im Mutterleib" );

if ( age > 150 ) ; // inline image logischer Fehler

System.out.println( "Aha, ein neuer Moses" );

Das Semikolon führt dazu, dass die leere Anweisung in Abhängigkeit von der Bedingung ausgeführt wird und unabhängig vom Inhalt der Variablen age immer die Ausgabe »Aha, noch im Mutterleib« und »Aha, ein neuer Moses« erzeugt. Das ist sicherlich nicht beabsichtigt. Das Beispiel soll ein warnender Hinweis sein, in jeder Zeile nur eine Anweisung zu schreiben – und die leere Anweisung durch das Semikolon ist eine Anweisung.

Folgen hinter einer if-Anweisung zwei Anweisungen, die durch keine Blockanweisung zusammengefasst sind, dann wird die eine folgende else-Anweisung als Fehler bemängelt, da der zugehörige if-Zweig fehlt. Der Grund ist, dass der if-Zweig nach der ersten Anweisung ohne else zu Ende ist:

int age = 29;

if ( age < 0 )

;

System.out.println( "Aha, noch im Mutterleib" );

else if ( age > 150 ) ; // inline image Compiler-Fehlermeldung: 'else' without 'if'

System.out.println( "Aha, ein neuer Moses" );

Mehrfachverzweigung bzw. geschachtelte Alternativen

if-Anweisungen zur Programmführung kommen sehr häufig in Programmen vor, und noch häufiger werden sie genutzt, um eine Variable auf einen bestimmten Wert zu prüfen. Dazu werden if- und if-else-Anweisungen gerne verschachtelt (kaskadiert). Wenn eine Variable einem Wert entspricht, dann wird eine Anweisung ausgeführt, sonst wird die Variable mit einem anderen Wert getestet usw.

[zB]  Beispiel

Kaskadierte if-Anweisungen sollen uns helfen, die Variable days passend zu dem Monat (vorbelegte Variable month) und der Information, ob das Jahr ein Schaltjahr ist (vorbelegte boolean-Variable isLeapYear), zu belegen:

int month = 2; boolean isLeapYear = false; int days;

if ( month == 4 )

days = 30;

else if ( month == 6 )

days = 30;

else if ( month == 9 )

days = 30;

else if ( month == 11 )

days = 30;

else if ( month == 2 )

if ( isLeapYear ) // Sonderbehandlung im Fall eines Schaltjahrs

days = 29;

else

days = 28;

else

days = 31;

In dem kleinen Programm ist semantisch eingerückt, eigentlich würden die Anweisungen bei jedem else immer weiter nach rechts wandern.

Die eingerückten Verzweigungen nennen sich auch angehäufte if-Anweisungen oder if-Kaskade, da jede else-Anweisung ihrerseits weitere if-Anweisungen enthält, bis alle Abfragen gestellt sind.

Angewendet auf unser Zahlenratespiel, wollen wir dem Benutzer einen Tipp geben, ob seine eingegebene Zahl kleiner oder größer als die zu ratende Zahl war:

Listing 2.20    src/main/java/GuessTheNumber2.java

public class GuessTheNumber2 {



public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);



System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();



if ( number == guess )

System.out.println( "Super getippt!" );

else if ( number > guess )

System.out.println( "Nee, meine Zahl ist größer als deine!" );

else // number < guess

System.out.println( "Nee, meine Zahl ist kleiner als deine!" );

}

}
 

Zum Seitenanfang

2.5.3    Der Bedingungsoperator Zur vorigen ÜberschriftZur nächsten Überschrift

In Java gibt es einen einzigen Operator, der drei Operanden benutzt: den Bedingungsoperator, auch Konditionaloperator genannt. Er erlaubt es, den Wert eines Ausdrucks von einer Bedingung abhängig zu machen, ohne dass dazu eine if-Anweisung verwendet werden muss. Die Operanden sind durch ? und : voneinander getrennt. Die allgemeine Syntax ist:

Bedingung ? Ausdruck, wenn Bedingung wahr : Ausdruck, wenn Bedingung falsch
[zB]  Beispiel

Die Bestimmung des Maximums ist eine schöne Anwendung des Bedingungsoperators:

max = ( a > b ) ? a : b;

Der Wert der Variablen max wird in Abhängigkeit von der Bedingung a > b gesetzt. Der Ausdruck entspricht folgender if-Anweisung:

if ( a > b ) max = a; else max = b;

Drei Ausdrücke kommen im Bedingungsoperator vor, was ihn zu einem ternären/trinären Operator – vom lateinischen ternarius (»aus drei bestehend«) – macht.[ 89 ](Der Bedingungsoperator ist zwar ein ternärer/trinärer Operator, doch könnte es im Prinzip mehrere Operatoren mit drei Operanden geben, weshalb der Umkehrschluss nicht gilt, automatisch beim ternären/trinären Operator an den Bedingungsoperator zu denken. Da allerdings in Java (und so ziemlich allen anderen Sprachen auch) bisher wirklich nur ein ternärer/trinärer Operator existiert, ist es in Ordnung, statt vom Bedingungsoperator vom ternären/trinären Operator zu sprechen. ) Der erste Ausdruck (in unserem Fall der Vergleich a > b) muss vom Typ boolean sein. Ist die Bedingung erfüllt, dann erhält die Variable den Wert des zweiten Ausdrucks, andernfalls wird der Variablen max der Wert des dritten Ausdrucks zugewiesen.

Der Bedingungsoperator kann eingesetzt werden, wenn der zweite und dritte Operand ein numerischer Typ, boolescher Typ oder Referenztyp ist. Der Aufruf von Methoden, die void zurückgeben, ist nicht gestattet. Es ist also keine kompakte Syntax, einfach zwei beliebige Methoden abhängig von einer Bedingung ohne if aufzurufen, denn es gibt beim Bedingungsoperator immer ein Ergebnis. Mit diesem können wir alles Mögliche machen, es etwa direkt ausgeben.

[zB]  Beispiel

Gib das Maximum von a und b direkt aus:

System.out.println( ( a > b ) ? a : b );

Das wäre mit if-else nur mit temporären Variablen möglich oder eben mit zwei println(…)-Anweisungen.

Beispiele

  • Das Maximum oder Minimum zweier Zahlen liefern die Ausdrücke a > b ? a : b bzw. a < b ? a : b.

  • Den Absolutwert einer Zahl liefert x >= 0 ? x : -x.[ 90 ](Wegen der Zweierkomplement-Arithmetik ist das nicht ganz unproblematisch, lässt sich aber nicht verhindern. Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, arbeitet das bei Math.abs(…) noch einmal auf. )

  • Ein Ausdruck soll eine Zahl n, die zwischen 0 und 15 liegt, in eine Hexadezimalzahl konvertieren: (char)((n < 10) ? ('0' + n) : ('a'10 + n )).

Geschachtelte Anwendung des Bedingungsoperators*

Die Anwendung des Bedingungsoperators führt schnell zu schlecht lesbaren Programmen. Er sollte daher vorsichtig eingesetzt werden. In C(++) führt die unbeabsichtigte Mehrfachauswertung in Makros zu schwer auffindbaren Fehlern. Gut, dass uns das in Java nicht passieren kann! Durch ausreichende Klammerung muss sichergestellt werden, dass die Ausdrücke auch in der beabsichtigten Reihenfolge ausgewertet werden. Im Gegensatz zu den meisten Operatoren ist der Bedingungsoperator rechtsassoziativ (die Zuweisung ist ebenfalls rechtsassoziativ).

Der Ausdruck

b1 ? a1 : b2 ? a2 : a3

ist demnach gleichbedeutend mit:

b1 ? a1 : ( b2 ? a2 : a3 )
[zB]  Beispiel

Wollen wir einen Ausdruck schreiben, der für eine Zahl n abhängig vom Vorzeichen –1, 0 oder 1 liefert, lösen wir das Problem mit einem geschachtelten Bedingungsoperator:

int sign = (n < 0) ? -1 : (n > 0) ? 1 : 0;

Ein Umbruch hinter dem Doppelpunkt bietet sich an und macht die geschachtelten Bedingungssausdrücke deutlicher:

int sign = (n < 0) ? -1 :

(n > 0) ? 1 :

0;

Der Bedingungsoperator ist kein L-Value *

Der Bedingungsoperator liefert als Ergebnis einen Ausdruck zurück, der auf der rechten Seite einer Zuweisung verwendet werden kann. Da er rechts vorkommt, nennt er sich auch R‐Value. Er lässt sich nicht derart auf der linken Seite einer Zuweisung einsetzen (L-Value), dass er eine Variable auswählt, der ein Wert zugewiesen wird.[ 91 ](In C(++) kann dies durch *((Bedingung) ? &a : &b) = Ausdruck; über Pointer gelöst werden. )

[zB]  Beispiel

Die folgende Anwendung des Bedingungsoperators ist in Java nicht möglich:

boolean up = false, down = false;

int direction = 12;

((direction >= 0) ? up : down) = true; // inline image Compilerfehler

Der Bedingungsoperator kann jedoch eine Referenz auswählen, und dann ist ein Methodenaufruf gültig.

 

Zum Seitenanfang

2.5.4    Die switch-Anweisung bietet die Alternative Zur vorigen ÜberschriftZur nächsten Überschrift

Eine Kurzform für speziell gebaute, angehäufte if-Anweisungen bietet switch. Im switch-Block gibt es eine Reihe von unterschiedlichen Sprungzielen, die mit case markiert sind. Die switch-Anweisung erlaubt die Auswahl von:

  • Ganzzahlen

  • Wrapper-Typen (Mehr dazu folgt in Kapitel 10, »Besondere Typen der Java SE«.)

  • Aufzählungen (enum)

  • Strings

[»]  Interna

Im Bytecode gibt es nur eine switch-Variante für Ganzzahlen. Bei Strings, Aufzählungen und Wrapper-Objekten wendet der Compiler Tricks an, um sie auf Ganzzahl-switch-Konstruktionen zu reduzieren.

switch bei Ganzzahlen (und somit auch chars)

Ein einfacher Taschenrechner für vier binäre Operatoren ist mit switch schnell implementiert (und wir nutzen die Methode charAt(0), mit der wir von der String-Eingabe auf das erste Zeichen zugreifen, um ein char zu bekommen):

Listing 2.21    src/main/java/Calculator.java

public class Calculator {



public static void main( String[] args ) {

double x = new java.util.Scanner( System.in ).nextDouble();

char operator = new java.util.Scanner( System.in ).nextLine().charAt( 0 );

double y = new java.util.Scanner( System.in ).nextDouble();



switch ( operator ) {

case '+':

System.out.println( x + y );

break;

case '-':

System.out.println( x - y );

break;

case '*':

System.out.println( x * y );

break;

case '/':

System.out.println( x / y );

break;

}

}

}

Die Laufzeitumgebung sucht eine bei case genannte Sprungmarke (auch Sprungziel genannt) – eine Konstante –, die mit dem in switch angegebenen Ausdruck übereinstimmt. Gibt es einen Treffer, so werden alle auf das case folgenden Anweisungen ausgeführt, bis ein (optionales) break die Abarbeitung beendet. (Ohne break geht die Ausführung im nächsten case-Block automatisch weiter; mehr zu diesem »It’s not a bug, it’s a feature!« folgt im übernächsten Abschnitt »switch hat ohne Unterbrechung Durchfall«.) Stimmt keine Konstante eines case-Blocks mit dem switch-Ausdruck überein, werden erst einmal keine Anweisungen im switch-Block ausgeführt. Die case-Konstanten müssen unterschiedlich sein, andernfalls gibt es einen Compilerfehler.

Die switch-Anweisung hat einige Einschränkungen:

  • Die JVM kann switch nur auf Ausdrücken vom Datentyp int ausführen. Elemente der Datentypen byte, char und short sind somit erlaubt, da der Compiler den Typ automatisch an int anpasst. Ebenso sind die Aufzählungen und die Wrapper-Objekte Character, Byte, Short, Integer möglich, da Java automatisch die Werte entnimmt – mehr dazu folgt in Abschnitt 10.7, »Die Spezial-Oberklasse Enum«. Die Datentypen boolean, long, float und double können nicht benutzt werden. Zwar sind auch Aufzählungen und Strings als switch-Ausdruckstypen möglich, doch intern werden sie auf Ganzzahlen abgebildet. Allgemeine Objekte sind sonst nicht erlaubt.

  • Die hinter case aufgeführten Werte müssen konstant sein. Dynamische Ausdrücke, etwa Rückgaben aus Methodenaufrufen, sind nicht möglich.

  • Es sind keine Bereichsangaben möglich. Das wäre etwa bei Altersangaben nützlich, um zum Beispiel die Bereiche 0–18, 19–60, 61–99 zu definieren. Als Lösung bleiben nur angehäufte if-Anweisungen.

[»]  Hinweis *

Die Angabe bei case muss konstant sein, aber kann durchaus aus einer Konstanten (finalen Variablen) kommen:

final char PLUS = '+';

switch ( operator ) {

case PLUS:



}

Alles andere mit default abdecken

Soll ein Programmteil in genau dem Fall abgearbeitet werden, in dem es keine Übereinstimmung mit irgendeiner case-Konstanten gibt, so lässt sich die besondere Sprungmarke default einsetzen. Soll zum Beispiel im Fall eines unbekannten Operators das Programm eine Fehlermeldung ausgeben, so schreiben wir:

switch ( operator ) {

case '+':

System.out.println( x + y );

break;

case '-':

System.out.println( x - y );

break;

case '*':

System.out.println( x * y );

break;

case '/':

System.out.println( x / y );

break;

default:

System.err.println( "Unbekannter Operator " + operator );

}

Der Nutzen von default besteht darin, falsch eingegebene Operatoren zu erkennen, denn die Anweisungen hinter default werden immer dann ausgeführt, wenn keine case-Konstante gleich dem switch-Ausdruck war. default kann auch zwischen den case-Blöcken auftauchen, doch das ist wenig übersichtlich und nicht für allgemeine Anwendungen zu empfehlen. Somit würde der default-Programmteil auch dann abgearbeitet, wenn ein dem default vorangehender case-Teil kein break hat. Nur ein default ist erlaubt.

[»]  Hinweis

Der Compiler kann natürlich nicht wissen, ob alle von uns eingesetzten Werte mit einem case-Block abgedeckt sind. Ein default kann helfen, Fehler schneller ausfindig zu machen, wenn bei switch eine Zahl ankommt, für die das Programm keine Operationen hinterlegt. Falls wir kein default einsetzen, bleibt ein unbehandelter Wert sonst ohne Folgen.

Bei Aufzählungen sieht das schon wieder anders aus, denn hier ist die Menge abzählbar. Eine Codeanalyse kann herausfinden, ob es genauso viele case-Blöcke wie Konstanten gibt. Eclipse kann das zum Beispiel melden.

switch hat ohne Unterbrechung Durchfall

Bisher haben wir in die letzte Zeile eine break-Anweisung gesetzt. Ohne ein break würden nach einer Übereinstimmung alle nachfolgenden Anweisungen ausgeführt. Sie laufen somit in einen neuen Abschnitt hinein, bis ein break oder das Ende von switch erreicht ist. Da dies vergleichbar mit einem Spielzeug ist, bei dem Kugeln von oben nach unten durchfallen, nennt sich dieses auch Fall-through. Ein häufiger Programmierfehler ist, das break zu vergessen, und daher sollte ein beabsichtigter Fall-through immer als Kommentar angegeben werden.

Über dieses Durchfallen ist es möglich, bei unterschiedlichen Werten immer die gleiche Anweisung ausführen zu lassen. Das nutzt auch das nächste Beispiel, das einen kleinen Parser für einfache Datumswerte realisiert. Der Parser soll mit drei unterschiedlichen Datumsangaben umgehen können; hier folgt je ein Beispiel:

  • "18 12": Jahr in Kurzform, Monat

  • "2018 12": Jahr, Monat

  • "12": Nur Monat; das aktuelle Jahr soll implizit gelten.

Listing 2.22    src/main/java/SimpleYearMonthParser.java

public class SimpleYearMonthParser {



@SuppressWarnings( "resource" )

public static void main( String[] args ) {



String date = "17 12";



int month = 0, year = 0;

java.util.Scanner scanner = new java.util.Scanner( date );



switch ( date.length() ) {

case 5: // YY MM

year = 2000;

// Fall-through

case 7: // YYYY MM

year += scanner.nextInt();

// Fall-through

case 2: // MM

month = scanner.nextInt();

if ( year == 0 )

year = java.time.Year.now().getValue();

break;

default :

System.err.println( "Falsches Format" );

}



System.out.println( "Monat=" + month + ", Jahr=" + year );

}

}

In dem Beispiel bestimmt eine case-Anweisung über die Länge, wie der Aufbau ist. Ist die Länge 5, so ist die Angabe des Jahres verkürzt, und wir initialisieren das Jahr mit 2000, um im folgenden Schritt mithilfe vom Scanner das Jahr einzulesen. Zu diesem Schritt wären wir auch direkt gekommen, wenn die Länge der Eingabe 7 gewesen wäre, also das Jahr vierstellig gewesen wäre. Damit ist der Jahresanteil geklärt, es bleibt, die Monate zu parsen. Kommen wir direkt über einen String der Länge 2, ist vorher kein Jahr gesetzt, wir bekommen über java.time.Year.now().getValue() das aktuelle Jahr, andernfalls überschreiben wir die Variable nicht.

Was sollte der Leser von diesem Beispiel mitnehmen? Eigentlich nur Kopfschütteln für eine schwer zu verstehende Lösung. Das Durchfallen ist eigentlich nur zur Zusammenfassung mehrerer case-Blöcke sinnvoll zu verwenden.

[»]  Sprachvergleich *

Obwohl ein fehlendes break zu lästigen Programmierfehlern führt, haben die Entwickler von Java dieses Verhalten vom syntaktischen Vorgänger C übernommen. Eine interessante andere Lösung wäre gewesen, das Verhalten genau umzudrehen und das Durchfallen explizit einzufordern, zum Beispiel mit einem Schlüsselwort. Dazu gibt es eine interessante Entwicklung: Java übernimmt diese Eigenschaft von C(++) und C(++) wiederum erbt den Durchfall von der Programmiersprache B. Einer der »Erfinder« von B ist Ken Thompson, der heute bei Google arbeitet und an der neuen Programmiersprache Go beteiligt ist. In Go müssen Entwickler ausdrücklich die fallthrough-Anweisung verwenden, wenn ein case-Block zum nächsten weiterleiten soll. Das Gleiche gilt für die neue Programmiersprache Swift; auch hier gibt es die Anweisung fallthrough. Selbst in C++ gibt es seit dem Standard C++17 das Standardattribut [[fallthrough]][ 92 ](http://en.cppreference.com/w/cpp/language/attributes), das den Compiler anweist, bei einem Durchfallen keine Warnung anzuzeigen.

Stack-Case-Labels

Stehen mehrere case-Blöcke untereinander, um damit Bereiche abzubilden, nennt sich das auch Stack-Case-Labels. Nehmen wir an, eine Variable hour steht für eine Stunde am Tag, und wir wollen herausfinden, ob Mittagsruhe, Nachtruhe oder Arbeitszeit ist:

Listing 2.23    src/main/java/RestOrWork.java, Ausschnitt

int hour = 12;



switch ( hour ) {

// Nachtruhe von 22 Uhr bis 6 Uhr

case 22:

case 23:

case 24: case 0:

case 1:

case 2:

case 3:

case 4:

case 5:

System.out.println( "Nachtruhe" );

break;



// Mittagsruhe von 13 bis 15 Uhr

case 13:

case 14:

System.out.println( "Mittagsruhe" );

break;



default :

System.out.println( "Arbeiten" );

break;

}
Die Tastenkombination (Strg) + Leertaste nach dem switch bietet an, ein Grundgerüst für eine switch-Fallunterscheidung anzulegen.

Abbildung 2.7    Die Tastenkombination (Strg) + Leertaste nach dem switch bietet an, ein Grundgerüst für eine switch-Fallunterscheidung anzulegen.

switch auf Strings

Neben der Möglichkeit, ein switch bei Ganzzahlen zu verwenden, ist eine switch-Anweisung auf String-Objekten möglich:

Listing 2.24    src/main/java/SweetsLover.java, main()

String input = javax.swing.JOptionPane.showInputDialog( "Eingabe" );



switch ( input.toLowerCase() ) {

case "kekse":

System.out.println( "Ich mag Keeeekse" );

break;

case "kuchen":

System.out.println( "Ich mag Kuchen" );

break;

case "schokolade": // Fällt durch

case "lakritze":

System.out.println( "Hm. Lecker" );

break;

default:

System.out.printf( "Kann man %s essen?%n", input );

}

Obwohl direkte Zeichenkettenvergleiche möglich sind, fallen Überprüfungen auf reguläre Ausdrücke leider heraus, die insbesondere Skriptsprachen wie Ruby oder Perl anbieten.

Wie auch beim switch mit Ganzzahlen können die Zeichenketten beim String-case-Zweig aus finalen (also nicht änderbaren) Variablen stammen. Ist etwa String KEKSE = "kekse"; vordefiniert, ist case KEKSE erlaubt.

 

Zum Seitenanfang

2.5.5    Switch-Ausdrücke Zur vorigen ÜberschriftZur nächsten Überschrift

Java hat seit Version 1.0 eine switch-Anweisung zum Kontrollfluss. Im Wesentlichen basiert die Syntax auf der Programmiersprache C, die aus den 1970er-Jahre stammt. In Java 12 wurde eine neue Syntax probeweise eingeführt, in Java 13 wurde sie verändert und in Java 14 endgültig integriert.

Insgesamt kann switch in vier Formen auftauchen:

Anweisung/Ausdruck

Ab Java-Version

Syntax

Durchfall

vollständige Abdeckung

Anweisung

1.0

:

Ja

Nein

Anweisung

14

->

Nein

Nein

Ausdruck

14

:

Ja

Ja

Ausdruck

14

->

Nein

Ja

Tabelle 2.13    Vier Typen von »switch«

Den ersten Typ haben wir schon ausgiebig betrachtet, schauen wir uns die weiteren Varianten an.

Vereinfachte Switch-Anweisung, kein Durchfall, keine vollständige Abdeckung

Bei der vereinfachten switch-Anweisung steht hinter dem Label bzw. default kein Doppelpunkt, sondern ein ->. Dieser hat nichts mit Lambda-Ausdrücken zu tun, auch wenn die Symbole gleich sind. Hinter dem Pfeil steht entweder ein Ausdruck, ein Block in geschweiften Klammern oder eine throw-Anweisung, die eine Ausnahme auslöst. Implizit beendet ein break jeden Zweig, es gibt also kein Durchfallen mehr.

[zB]  Beispiel
String operator = "+";

switch ( operator ) {

case "+" -> System.out.println( "Plus" );

case "-" -> { String minus = "Minus"; System.out.println( minus ); }

}

Dadurch, dass bei mehreren Anweisungen immer Blöcke gesetzt werden müssen, tritt eine lokale Variable auch nicht aus dem Bereich aus.

Ein default kann gesetzt werden, muss aber nicht. Das switch muss nicht jede Möglichkeit abdecken, was bei Zahlen und Strings eh nicht funktioniert.

[zB]  Beispiel
String operator = "+";

switch ( operator ) {

case "+" -> System.out.println( "Plus" );

case "-" -> System.out.println( "Minus" );

default -> System.out.println( "Unbekannter Operator" );

}

Bei vereinfachten switch-Anweisungen sind mehrere Labels möglich, die die gleiche Behandlung haben. Kommas trennen die Labels.

[zB]  Beispiel
String operator = "+";

switch ( operator ) {

case "+" -> System.out.println( "Plus" );

case "*", "×" -> System.out.println( "Mal" );

}

Switch-Ausdrücke, kein Durchfall, vollständige Abdeckung

Traditionell finden sich die Fallunterscheidungen mit switch als Anweisung, und Anweisungen geben nichts zurück. In Java 14 ist es möglich, switch als Ausdruck mit Ergebnis zu nutzen.

[zB]  Beispiel
String operator = "+";

String writtenOperator = (switch ( operator ) {

case "+" -> "Plus";

case "-" -> "Minus";

default -> "Unbekannter Operator";

} ).toUpperCase();

System.out.println( writtenOperator );

Ausdrücke müssen immer Ergebnisse liefern, und folglich muss switch immer einen Pfad auf einen Wert nehmen. Der übliche Fall ist default wie gezeigt, es gibt allerdings Sonderfälle, wie bei Aufzählungen, wo der Compiler prüfen kann, dass alle Möglichkeiten abgedeckt sind.

[zB]  Beispiel
DayOfWeek today = LocalDate.now().getDayOfWeek();

System.out.println( switch ( today ) {

case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Arbeit";

case SATURDAY, SUNDAY -> "Party";

} );

Falls rechts neben dem Pfeil kein einfacher Ausdruck steht, sondern ein Block, muss auch dieser Block ein Ergebnis zurückgeben. Dafür wird das neue Schlüsselwort yield eingesetzt, hinter dem ein Ausdruck kommt.

[zB]  Beispiel
String operator = "+";

System.out.println( switch ( operator ) {

case "+" -> "Plus";

case "-" -> { String minus = "Minus"; yield minus; }

default -> throw new IllegalArgumentException( "Unknown operator" );

} );

Ein Block muss ein yield besitzen oder eine ungeprüfte Ausnahme auslösen.

Switch-Expression mit :-Syntax, mit Durchfall, vollständige Abdeckung

Auch die Doppelpunkt-Syntax lässt sich als Ausdruck einsetzen. Mit ihr ist auch ein Durchfall wieder möglich; ein yield ist zwingend, oder eine ungeprüfte Ausnahme muss ausgelöst werden. Die Syntax birgt mit dem Durchfallen eine Fehlerquelle, sodass diese Variante vielleicht die schlechteste ist.

[zB]  Beispiel
String operator = "+";

System.out.println( switch ( operator ) {

case "+" : yield "Plus";

case "*" : System.out.println( "Sternchen" );

case "×" : yield "Mal";

default : throw new IllegalArgumentException( "Unknown operator" );

} );

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Objektorientierte Programmierung

Objektorientierte Programmierung




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2021

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.

Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de



Cookie-Einstellungen ändern