2.5 Bedingte Anweisungen oder Fallunterscheidungen
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.
Obwohl ein Sprung mit goto nicht möglich ist, besitzt Java eine spezielle Sprungvariante: continue und break mit definierten Sprungzielen.
2.5.1 Die if-Anweisung
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.
if ( Ausdruck )
Anweisung
Die Abarbeitung der Anweisung hängt nun vom Ausdruck ab. Ist das Ergebnis des Ausdrucks wahr (true), so wird die Anweisung ausgeführt. Ist das Ergebnis des Ausdrucks falsch (false), so wird mit der ersten Anweisung nach der if-Anweisung fortgefahren.
Beispiel Ein Relationenvergleich
if ( x < y )
System.out.println( "x ist kleiner als y" );
|
Im Gegensatz zu C(++) muss der Testausdruck in der if-Anweisung (übrigens auch in den folgenden Schleifen) vom Typ boolean sein. In C(++) wird ein numerischer Ausdruck als wahr bewertet, wenn das Ergebnis des Ausdrucks ungleich 0 ist.
Betrachten wir in einer if-Anweisung den Vergleich, ob ein Objekt existiert. Dann ist dies mit null zu vergleichen.1 Die Referenz auf das Objekt steht in der Variablen ref.
if ( ref != null )
...
if-Anfragen und Blöcke
Hinter dem if und der Bedingung erwartet der Compiler eine Anweisung. Wenn wir jedoch mehrere Anweisungen in Abhängigkeit von der Bedingung ausführen wollen, so müssen wir einen Block verwenden, denn andernfalls ordnet der Compiler nur die nächstfolgende Anweisung der Fallunterscheidung zu, auch wenn mehrere Anweisungen optisch abgesetzt sind.2 Dies ist eine große Gefahr für Programmierer, die optisch Zusammenhänge schaffen wollen, die in Wirklichkeit nicht existieren.
Beispiel Eine if-Anweisung soll testen, ob die Variable y den Wert 0 hat. In dem Fall soll sie die Variable x auf 0 setzen und zusätzlich auf dem Bildschirm »Null« anzeigen. Zunächst die semantisch falsche Variante:
if ( y == 0 )
x = 0;
System.out.println( "Null" );
|
Sie ist semantisch falsch, da unabhängig von y immer eine Ausgabe erscheint. Der Compiler interpretiert die Zeilen in folgendem Zusammenhang:
if ( y == 0 )
x = 0;
System.out.println( "Null" );
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.
Beispiel Ein korrekt geklammerter Ausdruck:
if ( y == 0 ) {
x = 0;
System.out.println( "Null" );
}
|
Zusammengesetzte Bedingungen
Unsere bisherigen Abfragen waren sehr einfach, jedoch kommen in der Praxis viel komplexere Bedingungen vor. Dafür werden häufig die logischen Operatoren &&, || beziehungsweise ! verwendet. Wenn wir etwa testen wollen, ob eine Zahl x entweder gleich 7 oder größer gleich 10 ist, schreiben wir die zusammengesetzte Bedingung
if ( x == 7 || x >= 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.
Hier klicken, um das Bild zu Vergrößern
if und (Strg)+(____) bietet an, eine if-Anweisung mit Block anzulegen.
Hier klicken, um das Bild zu Vergrößern
2.5.2 Die Alternative wählen mit einer if/else-Anweisung
Neben der einseitigen Alternative existiert die zweiseitige Alternative. Das optionale Schlüsselwort else veranlasst die Ausführung der alternativen Anweisung, wenn der Test falsch ist:
if ( Ausdruck )
Anweisung1
else
Anweisung2
Falls der Ausdruck wahr ist, wird die Anweisung 1 ausgeführt, andernfalls Anweisung 2. Somit ist sichergestellt, dass in jedem Fall eine Anweisung ausgeführt wird.
if ( x < y )
System.out.println( "x ist echt kleiner als y." );
else
System.out.println( "x ist größer oder gleich y." );
Dangling-Else-Problem
Bei Verzweigungen mit else gibt es ein bekanntes Problem, welches 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.
Beispiel Wenn das else immer zum innersten if gehört, und das ist nicht erwünscht, 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" );
|
|
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ührungsfolge. Ein Beispiel:
int alter = 29;
if ( alter < 0 ) ;
System.out.println( "Aha, noch im Mutterleib" );
if ( alter > 150 ) ;
System.out.println( "Aha, ein neuer Moses" );
Das Semikolon führt dazu, dass die leere Anweisung in Abhängigkeit von der Bedienung ausgeführt wird und unabhängig vom Inhalt der Variable alter immer die Ausgabe »Aha, noch im Mutterleib« erzeugt. Das ist sicherlich nicht beabsichtigt.
Folgen hinter einer if-Anweisung zwei Anweisungen, die nicht durch eine 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 alter = 29;
if ( alter < 0 ) ;
System.out.println( "Aha, noch im Mutterleib" );
else ( alter > 150 ) ;
System.out.println( "Aha, ein neuer Moses" );
Das führt zu der Fehlermeldung 'else' without 'if'.
Mehrfachverzweigung beziehungsweise geschachtelte Alternativen
if-Anweisungen zur Programmführung kommen sehr häufig in Programmen vor, und noch häufiger ist es, eine Variable auf einen bestimmten Wert zu prüfen. Dazu werden if- und if/else-Anweisung gerne geschachtelt. Wenn eine Variable einem Wert entspricht, dann wird eine Anweisung ausgeführt, sonst wird die Variable mit einem anderen Wert getestet und so weiter.
Dieser Ansatz ist sehr umständlich und kostet zudem noch Rechenzeit, da in jedem Fall drei Bedingungen geprüft werden. Wenn also x größer null ist, werden dennoch zwei Vergleiche gemacht. Wir schachteln daher in einer kleinen Programmverbesserung die Alternativen und arbeiten dann mit einer Abfolge von sequenziell abhängigen Alternativen:3
if ( x > 0 )
signum = 1;
else
if ( x < 0 )
signum = -1;
else
signum = 0;
Jetzt werden nur noch so viele Bedingungen geprüft wie zur Entscheidung notwendig sind. 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 gemacht sind.
Beispiel Kaskadierte if-Anweisungen:
if ( monat == 4 )
tage = 30;
else
if ( monat == 6 )
tage = 30;
else
if ( monat == 9 )
tage = 30;
else
if ( monat == 11 )
tage = 30;
|
else
if ( monat == 2 )
if ( schaltjahr )
tage = 29;
else
tage = 28;
else
tage = 31;
|
2.5.3 Die switch-Anweisung bietet die Alternative
In Java4 gibt es eine Kurzform für speziell gebaute, angehäufte if-Anweisungen - die Anweisung mit switch und case:
switch ( Ausdruck )
{
case Konstante:
Anweisungen
}
Die switch-Anweisung ist eine einfache Form der Mehrfachverzweigung. Sie vergleicht nacheinander den Ausdruck hinter dem switch (ein primitiver Typ wie byte, char, short oder int) mit jedem einzelnen Fallwert. Alle Fallwerte müssen unterschiedlich sein. Stimmt der Ausdruck mit der Konstanten überein, so wird die Anweisung beziehungsweise die Anweisungen hinter der Sprungmarke ausgeführt.
Hinweis Eine Einschränkung der switch-Anweisung besteht darin, dass die Tests und die Konstanten nur auf den primitiven Datentyp int beschränkt sind.
|
Es können also keine größeren Typen wie long oder Fließkommazahlen wie float beziehungsweise double oder gar Objekte benutzt werden. Als Alternative bleiben nur angehäufte if-Anweisungen. Dies ist auch der einzige Weg, um Bereiche abzudecken.
Alles andere abdecken mit default
Gibt es keine Übereinstimmung mit einer Konstanten, so lässt sich optional die Sprungmarke default einsetzen:
switch ( ausdruck )
{
case Konstante:
Anweisungen
...
default:
}
Ohne Übereinstimmung mit einem konkreten Ziel geht die Abarbeitung des Programmcodes hinter default weiter. default kann auch zwischen den Konstanten eingesetzt werden.
Beispiel Ein Taschenrechner mit Alternative
switch ( op )
{
case '+': // addiere
break;
case '-': // subtrahiere
break;
case '*': // multipliziere
break;
case '/': // dividiere
break;
default:
System.err.println( "Operand nicht definiert!" );
}
|
switch hat 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 herein 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.
Beispiel Über dieses Durchfallen ist es möglich, bei unterschiedlichen Werten immer die gleiche Anweisung ausführen zu lassen:
switch ( buchstabe )
{
case 'a': // Durchfallen
case 'e':
case 'i': case 'o': case 'u':
vokal = true;
break;
default:
vokal = false;
}
|
In dem Beispiel bestimmt eine case-Anweisung, ob die Variable buchstabe ein Vokal ist. Fünf case-Anweisungen decken jeweils einen Buchstaben ab. Stimmt die Variable mit einer Konstanten überein, so »fällt« der Interpreter in den Programmcode der Zuweisung. Dieses Durchfallen über die case-Zweige ist praktisch, so wie es unser Programmcode für das Ist-Vokal-Problem zeigt. Der erste case-Zweig setzt die boolesche Variable vokal bei einem Vokal auf wahr. Tritt die Bedingung nicht ein, so weist die Anweisung im default-Teil der Variablen vokal den Wert falsch zu.
Hinweis Obwohl ein fehlendes break zu lästigen Programmierfehlern führt, haben die Java-Entwickler dieses Verhalten vom syntaktischen Vorgänger C übernommen. Eine interessante Lösung wäre gewesen, das Verhalten genau umzudrehen und das Durchfallen explizit einzufordern, zum Beispiel mit einem Schlüsselwort.
|
switch und (Strg)+(_____) bietet an, ein Grundgerüst für eine switch Fallunterscheidung anzulegen.
Hier klicken, um das Bild zu Vergrößern
1 Und nicht einfach if (ref) wie in C(++).
2 In der Programmiersprache Python bestimmt die Einrückung die Zugehörigkeit.
3 In der Programmiersprache Euphoria (die Webseite>http://www.rapideuphoria.com/ wirbt mit safe und sexy - Huuh) heißt das einfach: return (x > 0) - (x < 0).
4 Und auch in C(++).
|