SchBedingte Anweisungen und Schleifen

Inhalt

 

 

if ...

 

while ...

 

do ... while

 

for

 

break, continue

 

switch ...

 

Beispiele

 

Übungsaufgaben

if ...

Bisher haben wir nur Programme betrachtet, bei denen alle Anweisungen nacheinander ausgeführt werden. Solche Programme sind aber in der Funktionalität stark eingeschränkt. Die wahre Kraft erreicht ein Programm erst dadurch, dass Anweisungen mehrfach, oder nur unter gewissen Bedingungen ausgeführt werden. Nur dadurch kann es nicht-triviale Aufgaben erledigen.

Will man eine Anweisung nur unter einer Bedingung ausführen lassen, so verwendet man if. Ein Beispiel wäre etwa

if (x < 0) x = -x;

Die Anweisung x = -x wird also nur ausgeführt, wenn x negativ ist. Dies entspricht der Berechnung des Absolutbetrags von x.

Allgemein lautet die Syntax von if

if (bedingung) anweisungsblock

Man beachte, dass die Bedingung in runden Klammern stehen muss. und anweisungsblock entweder eine einzelne Anweisung (mit ;) oder ein Block von Anweisungen, der durch {...} geklammert ist.

Beispiel:

if (x < 0)
{   
    System.out.println("x war negativ");
    x = -x;
}

Man kann auch die Alternative festlegen.

if (x < 0)
{   
    System.out.println("x war negativ");
    x = -x;
}
else
{   
    System.out.println("x war positiv");
}

Der else-Teil wird nur ausgeführt, wenn die Bedingung falsch ist. In diesen Beispiel kann man sich die Klammern nach else sparen, weil der Block ja nur eine Anweisung enthält. Allgemein lautet die Syntax also

if (bedingung) anweisungsblock
else anweisungsblock

Wie vorher, kann anweisungsblock auch eine einzelne Anweisung mit ; sein. Falls mehrere if geschachtelt werden, so bezieht sich else auf das vorhergehende if, unabhängig von der Einrückung.

Regel: else bezieht sich immer auf das vorangegangene if.

Beispiel:

if (needpositive)
    if (x < 0) x = -x;
    else System.out.println("x war schon positiv");

Dabei ist needpositiv eine Variable vom Typ boolean, die ein Flag enthalten soll, ob der Test durchzuführen ist oder nicht. Die Einrückung von else ist unwichtig, es bezieht sich in jedem Fall auf

if (x < 0) ...

Vermutlich ist es bei geschachtelten if besser, die Anweisungen mit {...} zu klammern.

Man sollte immer auf einwandfreie Lesbarkeit seiner Programme achten.

Programmierer verwenden gerne den folgenden Programmierstil mit geschachtelten Bedingungen.

if (x < 0) System.out.println("x ist negativ.");
else if (x == 0) System.out.println("x ist 0."); 
else if (x > 0) System.out.println("x ist positiv.");

Zur Warnung möchte ich noch darauf hinweisen, dass hinter Blöcken {...} kein ; zu stehen braucht. Dies würde nur eine leere Anweisung erzeugen. Wenn nach dem Block ein else folgt, funktioniert er mit ; sogar nicht mehr.

while ...

Diese Schleifen dienen dazu, Programmteile mehrfach mit veränderten Werten ausführen zu lassen. Schleifen werden abgebrochen, wenn eine vorgegene Bedingung erfüllt ist, bzw. nicht mehr erfüllt ist.

Die einfachste Schleife ist die while-Schleife. Ein Block von Anweisungen wird so lange ausgeführt, wie die Abbruch-Bedingung wahr ist. Das folgende Beispiel zählt zum Beispiel bis 10.

int i = 1;
while (i <= 10)
{   
    System.out.println(i);
    i++;
}

Diese Schleife kann man so formulieren:

  1. Setze den Zähler i auf 1.
  2. Falls i nicht kleiner oder gleich 10 ist, springe nach 6.
  3. Gib i aus.
  4. Erhöhe i um 1.
  5. Springe nach 2.
  6. Beende die Schleife.

Allgemein lautet die Syntax der while-Schleife

while (bedingung) anweisungsblock

Wie bei if muss die Bedingung in runden Klammern stehen.

Die Abbruchsbedingung wird jeweils überprüft, bevor der Anweisungsblock durchlaufen wird. Falls sie etwa von vornherein false ist, so wird die Schleife nie durchlaufen.

Man muss sicher stellen, dass die Abbruchsbedingung auch irgendwann erfüllt wird. Sonst entsteht eine Dauerschleife, die nur durch einen Programmabbruch beendet werden kann. Bei Java kann man dazu in der Kommandozeile Strg-C drücken. Eine Dauerschleife ist etwa

while (true)
{   
    System.out.println("Bitte Strg-C drücken");
}

Diese Schleife druckt andauernd den gleichen Text. Das Programm muss vom Benutzer abgebrochen werden.

do ... while

Man kann die Bedingung auch ans Ende stellen.

do anweisungsblock while (bedingung);

Hier wird die Schleife zuerst durchlaufen und dann die Bedingung getestet. Falls sie false ist, bricht die Schleife ab. Die Schleife wird also in jedem Fall mindestens einmal durchlaufen. Ansonsten unterscheidet sie sich nicht von der while-Schleife.

Unser Beispiel, das von 1 bis 10 zählt, sieht dann so aus.

int i = 1;
do
{   
    System.out.println(i);
    i++;
}
while (i<=10);

Der Unterschied zur while Schleife wird sichtbar, wenn man anfänglich i = 10 setzt. In diesem Fall wird nur mit do ... while die Zahl 10 ausgegeben. Noch drastischer sieht man das, wenn man i = 11 setzt. Es wird auch 11 ausgegeben.

for ...

Eine andere, kompaktere Form der Schleife ist die for-Schleife, die hauptsächlich eine Vereinfachung für einfache Zählschleifen ist. Zuerst ein Beispiel, das wieder von 1 bis 10 zählt.

int i;
for (i = 0; i <= 10; i++) System.out.println(i);

Allgemein lautet die Syntax

for (anfangs-anweisung; bedingung; schleifen-anweisung)
   anweisungsblock

anfangs-anweisung

Wird vor Beginn des ersten Schleifendurchlaufs ausgeführt. Im obigen Beispiel wird i auf den Wert 0 gesetzt.

schleifen-anweisung

Diese Anweisung wird am Ende jedes Schleifendurchlaufs ausgeführt. Im obigen Beispiel wird i um 1 erhöht.

bedingung

Dieser Ausdruck wird vor jedem Durchlauf (auch vor dem ersten) ausgewertet. Der Ausdruck muss vom Wert boolean sein. Fällt die Auswertung positiv aus, so wird ein neuer Durchlauf gestartet, ansonsten wird die Schleife beendet. Im obigen Beispiel wird die Schleife beendet, wenn i größer als 10 wird (d.h. sie wird ausgeführt, solange i<=10 wahr ist.

Das folgende Beispiel zählt rückwärts von 10 bis 1.

int i;
for (i = 10; i >= 1; i--) System.out.println(i);

Es ist günstig, die for-Schleife nur für einfache Zählschleifen einzusetzen, da sonst die Syntax verwirrend wird.

Man kann übrigens auch die Variable gleich in der Schleife deklarieren. Damit sieht das letzte Beispiel so aus:

for (int i = 10; i >= 1; i--) System.out.println(i);

Die Variable ist dann nur in der Schleife gültig. Man kann also den Endwert danach nicht mehr abrufen. Für die Deklaration der Schleifenvariablen außerhalb der Schleife ist das möglich, aber aus Klarheitsgründen nicht empfehlenswert.

Man kann auch mehrere einfache Befehle, durch Komma getrennt, als Anfangsanweisung verwenden. Beispiel

for (int i = 0, k = n; i < n; i++, k--) System.out.println(i + ", " + k);

Dies zählt i hoch und k herunter. Hier ist schon unklar, ob eine solche Kompaktheit nicht der Übersichtlichkeit schadet. Man kann den gleichen Effekt auch deutlicher erreichen.

k=n;
for (int i = 0; i < n; i++)
{    
     System.out.println(i + ", " + k);
     k--; 
}

Wichtig! Die folgende Schleife ist inkorrekt:

int i;
for (i = 0; i <= 10; i++);
{   
    System.out.println(i);
}

Das Semikolon vor dem Schleifenblock ist falsch. Es wird als Leerbefehl interpretiert, der dann 10-mal ausgeführt wird. Danach wird der Schleifenblock einmal ausgeführt mit dem Wert 10 für i. Dieser Fehler ist erstaunlich häufig und schwer zu finden.

Beispiele

Als erstes Beispiel erzeugen wir 100 Zufallszahlen zwischen 0 und 1. Die Routine Math.random() liefert eine (Pseudo-)Zufallszahl vom Type double zwischen 0 und 1. Die beiden Endwerte kommen nicht vor, und die Zahlen sind ansonsten gleichmäßig im Intervall verteilt. Unser Ziel ist, die größte der erzeugten Zahlen auszugeben. Dazu verwenden wir folgenden Algorithmus:

  1. Erzeuge die erste Zufallszahl und merke diese Zahl als Maximum.
  2. Erzeuge die restlichen Zufallszahlen und teste, ob eine von ihnen größer als das Maximum ist. Wenn ja, aktualisiere das Maximum.
  3. Gib das Maximum aus.

Das Programm dazu sieht folgendermaßen aus:

public class Test
{   
    public static void main (String args[])
    {   
        int i;
        double max;
        max=Math.random(); // Erste Zufallszahl
        for (i=2; i<=100; i++)
        {   
            double x=Math.random(); // nächste Zahl
            if (x>max) max=x; // Aktualisiere Maximum
        }
        System.out.println("Maximum :"+max);
    }
}

Als zweites Beispiel schachteln wir zwei Schleifen ineinander.

for (i=1; i<10; i++)
{   
    for (j=1; j<10; j++) System.out.print(i*j+" ");
        // gib eine Zeile des Einmaleins aus
    System.out.println(""); // neue Zeile anfangen.
}

Das Programm druckt das kleine Einmaleins.

1 2 3 4 5 6 7 8 9
2 4 6 8 10 12 14 16 18
3 6 9 12 15 18 21 24 27
4 8 12 16 20 24 28 32 36
5 10 15 20 25 30 35 40 45
6 12 18 24 30 36 42 48 54
7 14 21 28 35 42 49 56 63
8 16 24 32 40 48 56 64 72
9 18 27 36 45 54 63 72 81

Das Unterprogramm System.out.print druckt ohne Zeilenumbruch. Dadurch lassen sich alle 10 Werte in einer Zeile ausgeben, bevor ein Zeilenumbruch die nächste Zeile beginnt.

Die Formatierung lässt noch zu wünschen übrig, da die Zahlen verschieden breit ausgegeben werden. Im Vorgriff auf spätere Kapitel formatieren wir die Zahlen mit String.format(). In diesem Fall verwenden wir das Format "%2d", das eine int-Zahl dezimal auf zwei Stellen ausgibt.

public class Test 
{
    public static void main(String args[]) {
        int i, j;
        for (i = 1; i < 10; i++) 
        {
            for (j = 1; j < 10; j++) 
            {
                int p = i * j;
                System.out.print(String.format("%2d ", p));
            }
            System.out.println("");
        }
    }
}

Man beachte die Deklaration einer lokalen Variablen im Schleifenblock. Diese Variable gilt nur innerhalb der for-Schleife. Die Ausgabe ist nun wesentlich schöner.

  1  2  3  4  5  6  7  8  9 
 2  4  6  8 10 12 14 16 18 
 3  6  9 12 15 18 21 24 27 
 4  8 12 16 20 24 28 32 36 
 5 10 15 20 25 30 35 40 45 
 6 12 18 24 30 36 42 48 54 
 7 14 21 28 35 42 49 56 63 
 8 16 24 32 40 48 56 64 72 
 9 18 27 36 45 54 63 72 81 

break, continue

Eine Schleife kann auch unterbrochen werden. Dazu dient die break-Anweisung. Normalerweise wird sie in einer Bedingung aufgerufen, das heißt innerhalb einer if-Anweisung. Z.B. zählt die folgende Schleife nur bis 5, weil sie an diesem Punkt durch break unterbrochen wird.

for (int i=1; i<=10; i++)
{   
    System.out.println(i);
    if (i==5) break;
}

Es kann natürlich mehrere break-Anweisungen in einer Schleife geben.

Analog funktioniert der Befehl continue. Er bricht die Schleife allerdings nicht ab, sondern überspringt nur den Rest dieses Schleifendurchlaufs. Die Abbruchbedingung der Schleife wird danach überprüft, und bei for-Schleifen wird vorher noch die Schleifen-Anweisung ausgeführt.

Bei geschachtelten Schleifen kann auch zu einer äußeren Schleife gesprungen werden. Dazu dienen Labels.

loop: while (bedingung)
{
    ...
    break loop;
    ...
}

In diesem Beispiel springt der break-Befehl aus der angegeben while-Schleife, auch wenn er in einer inneren Schleife enthalten ist. Dadurch lässt sich der Effekt eines absoluten Sprunges erreichen.

loop: while (true)
{
    ...
    if (error) break loop;
    ...
    break;
}

Dies ist eigentlich gar keine Schleife, da am Ende ja ein break steht. Sinn macht das nur, wenn vorher irgendwo ein continue auftaucht. Solche Konstrukte sollte man nur verwenden, wenn sie den logischen Ablaufe widerspiegeln.

Merke: break und continue, besonders mit Labels, sollen nur verwendet werden, wenn dadurch die Programmlogik nicht verschleiert wird.

switch ...

Diese Anweisung dient dazu, Fallunterscheidungen durchzuführen. Sie testet einen int-Ausdruck darauf, ob er gleich gewissen Konstanten ist. Die allgemeine Syntax lautet

switch (ausdruck)
{   
    case konstante1 : anweisungen
    case konstante2 : anweisungen
    ...
    default : anweisungen
}

Dabei bedeutet anweisungen eine Reihe von Anweisungen, jede gefolgt von einem ;, oder einen mit {...} geklammerten Anweisungsblock.

Merke:  Die Reihe von Anweisungen muss jeweils mit einer break-Anweisung beendet werden, sonst werden die Anweisungen des nächsten case auch noch ausgeführt.

Beispiel:

switch (n)
{   
    case 0 : System.out.println("zero"); break;
    case 1 : System.out.println("one"); break;
    case 2 : System.out.println("two"); break;
    case 3 : System.out.println("three"); break;
    default : System.out.println("Cannot count that much!");
}

Die Konstanten müssen echte Konstanten sein, die schon der Compiler auswerten kann. Der Ausdruck hinter switch wird dann mit den einzelnen Konstanten verglichen und zu den Anweisungen nach dem entsprechenden case gesprungen.

Aufzählungen

Mit Aufzählungen lassen sich vordefinierte Mengen von Werten einstellen. Die Alternative ist, eine Reihe von int-Variablen mit vordefinierten Werten zu definieren. Diese Methode ist jedoch unterlegen, weil die Elemente einer Aufzählung mit int-Werten eigentlich nichts zu tun haben und die feste Bindung zu unflexibel und unklar ist.

enum Farbe {Rot,Grün,Blau};

public class Test
{   
    static public void main (String args[])
    {   
        Farbe f;
        f=Farbe.Rot;
        System.out.println(f);
        switch (f)
        {   
            case Rot : System.out.println("OK, Rot!"); break;
            default : System.out.println("Nicht rot!");
        }
    }
}

Die Deklaration erfolgt einfach wie die einer Klasse, kann aber auch innerhalb einer Klasse in einer Unterklasse erfolgen. Aufzählungen verhalten sich auch in vielen anderen Bereichen wie Klassen. Die switch-Funktion arbeitet, wie man seiht sehr gut mit Aufzählungen zusammen.

Beispiele

Kleinster Teiler und Primzahlen

Das folgende kompliziertere Beispiel findet den kleinsten Teiler einer natürlichen Zahl, oder gibt aus, dass die Zahl prim ist.

public class Test
{   
    public static void main (String args[])
    {   
        int n=1019,i;

        // Teste n auf Teiler:
        if (n%2 == 0) System.out.println("2 teilt "+n); // 2 teilt n
        else // 2 teilt n nicht!
        {   
            i=3; // teste 3,5,7,9,...
            double ns=Math.sqrt(n); // nur bis zur Wurzel nötig

            while (i <= ns)
            {   
                if (n%i == 0) // Teiler gefunden!
                {   
                    System.out.println(i + " teilt " + n);
                    break; // Schleife wird abgebrochen
                }
                i+=2;
            }

            if (i>ns) System.out.println(n + " ist prim");
                // In diesem Fall wurde kein Teiler gefunden.
        }
    }
}

Beachten Sie, dass man nur die Zahlen 2,3,5,7,... als Teiler überprüfen muss. Außerdem muss immer ein Teiler kleiner als die Quadratwurzel von n existieren, wenn n nicht prim ist.

Da dieses Programm sehr mathematiklastig ist und schon recht komplex ist, beschreiben wir hier seine Logik einmal verbal.

  1. Teste, ob die Zahl 2 die gegebene Zahl teilt. Falls ja, gib diesen Sachverhalt aus und beende das Programm.
  2. Andernfalls: Teste nacheinander die Zahlen 3,5,7,..., ob eine von ihnen die Zahl teilt. Tue dies bis zur Quadratwurzel der Zahl. Falls ein Teiler gefunden wurde, so gib ihn aus und beende das Programm.
  3. Andernfalls: Gib aus, das kein Teiler gefunden wurde.

Ausgabe eines Tilgungsplans

Angenommen, eine Schuld von 100000 wird in Jahresraten von von 8000 zurückgezahlt bei einem Zinssatz von 7%. Wann ist die Schuld bezahlt?

public class Test
{
    public static void main(String args[])
    {
        double K = 100000, R = 8000, P = 0.07;
        int i = 0;
        while (K > 0)
        // Abbruch, wenn das Kapital zurückgezahlt ist
        {
            K = K + K * 0.07 - R; // Kapital+Zinsen-Rate
            i++;
            // Ausgabe der Restschuld :
            System.out.println(String.format("%2d", i) + ": " 
                  + String.format("%10.2f", K));
        }
    }
}

Zur Formatierung der Ausgabe des Kapitals auf zwei Stellen nach dem Komma und insgesamt 10 Stellen (rechtsbündig) verwenden wir das Format "%10.2f".

Die Ausgabe ist

  1:   99000,00
 2:   97930,00
 3:   96785,10
 4:   95560,06
 5:   94249,26
 6:   92846,71
 7:   91345,98
      ...
25:   36750,96
26:   31323,53
27:   25516,18
28:   19302,31
29:   12653,47
30:    5539,21
31:   -2073,04

Also ist nach 31 Jahren die Schuld getilgt.

Ausgabe des Zeichensatzes

Wir wollen zunächst einfach den ASCII-Zeichensatz ausgeben. Dies sind die char-Werte von 32 bis 127. Die Werte von 0 bis 31 sind Sonderzeichen, und ab 128 finden wir nationale Zeichen.

public class Test
{   
    public static void main (String args[])
    {   
        int i,j;
        for (i=2; i<16; i++) // 14 Zeilen
        {   
            for (j=0; j<16; j++) System.out.print((char)(16*i+j));
                // Jeweils 16 Zeichen pro Zeile
            System.out.println(" "+i*16+" - "+(i*16+15));
                // Am Schluss der Bereich A-B
                // der Zeichen in dieser Zeile.
        }
    }
}

Die Ausgabe ist

 !"#$%&'()*+,-./  32 - 47
0123456789:;<=>? 48 - 63
@ABCDEFGHIJKLMNO 64 - 79
PQRSTUVWXYZ[\]^_ 80 - 95
`abcdefghijklmno 96 - 111
pqrstuvwxyz{|}~  112 - 127
...

Dies entspricht den ersten Unicode-Zeichen. Danach folgen Zeichen, die in einem HTML-File wie diesem nur schwer dargestellt werden können. Man stößt hier auf das Problem der Unicode-Kodierung, auf das wir später zurückkommen.

Iteration

Nun wollen wir die Iterationsvorschrift

x(n+1) = (x(n)+2/x(n))/2;

mit dem Startwert

x(0) = 2;

berechnen. Diese Folge konvergiert gegen Wurzel von 2. Bei der Implementation dieser Rechenvorschrift speichern wir die Variablen x(n) alle in derselben Variablen x. Der Ablauf ist also

  1. Setze x auf 2.
  2. Berechne (x+2/x)/2 und speichere diesen Wert wieder auf x ab. Prüfe, ob x*x nahe genug bei 2 liegt. Wenn ja, brich das Programm ab, und gib x aus.
public class Test
{   
    public static void main (String args[])
    {   
        double x=2; // Anfangswert
        while (Math.abs(x*x-2)>1e-12) // Abbruchsbedingung
            x=(x+2/x)/2; // Iteration
        System.out.println(x); // Ausgabe des Endwertes
    }
}

Der Wert von x wird also immer durch den neuen Wert ersetzt. Als Abbruchbedingung wählen wir, dass x^2 nahe genug bei 2 liegt.

Bisektionsverfahren

Als zweites Beispiel wird das Bisektionsverfahren zur Bestimmung der Lösung der Gleichung

e^x=4x

zwischen 0 und 1 programmiert. Am linken Ende des Intervalls ist e^0>0 und am rechten Ende gilt e^1<4. Wir berechnen eine Folge von Intervallen, die den Wert beliebig genau annähern. Alle diese Intervalle haben die Eigenschaft, dass der Funktionswert von e^x-4x links kleiner 0 und rechts größer 0 ist.

public class Test
{   
    public static void main (String args[])
    {   
        double a=0,b=1,m,y;
        while (b-a>1e-10) // Abbruch, wenn Intervall klein genug
        {   
            m=(a+b)/2; // Berechne Mitte
            y=Math.exp(m)-4*m; // und Wert in der Mitte
            if (y>0) a=m; // Nimm rechtes Halb-Intervall
            else b=m; // Nimm linkes Halb-Intervall
        }
        System.out.println(a+","+b); // Ergebnis ausdrucken
    }
}

Das Ergebnis ist

0.3574029561714269,0.3574029562296346

Übungsaufgaben

  1. Schreiben Sie eine Schleife, die die ersten 100 Quadratzahlen zusammenzählt.
  2. Berechnen Sie die sogenannte Kollatz-Folge für den Startwert 27, bis 1 erreicht wird. Diese Folge ist definiert durch n -> n/2, falls n gerade ist, n -> 3n+1, falls n ungerade ist. Die ersten Glieder der Folge sind also 27, 82, 41, 124, 62, 31, ...
  3. Berechnen Sie einige Fibonacci-Zahlen. Diese Zahlfolge ist die Folge, bei der jedes Folgenglied Summe der beiden vorhergehenden ist, also 1,1,2,3,5,8,13,...
  4. Was passiert, wenn man auf einem Taschenrechner im Radians-Modus die Kosinustaste immer wieder drückt? Schreiben Sie eine Dauerschleife, die sich nur mit CRTL-C abbrechen lässt, und eine Schleife die sich selbst unterbricht, wenn die Iteration bei einer Zahl stehen bleibt.
  5. Wiederholen Sie die Aufgabe 4 mit der Sinustaste und dem Startwert 1.

Lösungen.

Aufgaben ohne Lösung

  1. Was ergibt (1/11)*11-1, wenn man es mit double-Werten auswertet? Bestimmen Sie die erste Zahl, bei der dieser Ausdruck nicht 0 ergibt.
  2. Nehmen Sie eine beliebige double-Variable zwischen 0 und 1 und multiplizieren Sie die Zahl mit 2. Wenn Sie größer als 1 wird, ziehen Sie 1 ab. Führen Sie diesen Prozess in einer Schleife fort, bis die Zahl 0 ist. Warum endet die Schleife immer?
  3. Verbinden Sie 2. mit einer Ausgabe der Zahl im Dualsystem. Dazu geben Sie 0. aus und dann 0, wenn die Zahl nicht größer als 1 wurde, und 1 andernfalls.
  4. Versuchen Sie, die Zeichen über 256 im Zeichensatz auszugeben.
  5. Nehmen Sie eine größere Primzahl p und berechnen Sie 2^n modulo p, bis dieser Ausdruck 1 wird. Nach wievielen Schritten passiert das? Man kann dabei einfach immer mit 2 multiplizieren und das Ergebnis modulo p nehmen.
  6. Berechnen Sie e mit Hilfe der Exponentialsumme 1+1/2+1/3!+... auf 14 Stellen. Vergleichen Sie mit Math.E.

Zurück zum Java-Kurs