Iteracje


1. Pojęcie pętli iteracyjnej

Często instrukcję lub grupę instrukcji trzeba wykonać wielokrotnie, powtarzając je znowu i znowu, aż do chwili, gdy zostanie spełniony jakiś warunek (w szczególności np. dotyczący liczby powtórzeń). Każde powtórzenie instrukcji lub ich grupy nazywa się iteracją, a językowa konstrukcja pozwalająca na ich powtarzanie - pętlą iteracyjną.

Wyobraźmy sobie np., że trzeba policzyć sumę i iloczyn kolejnych dodatnich  liczb całkowitych. Nie znając pojęcia pętli iteracyjnej musielibyśmy zapisać to w następujący sposob:

int suma = 0;
int iloczyn = 1;
int i = 1;
suma += i;
iloczyn *= i;
i++;
suma += i;
iloczyn *= i;
i++;
suma += i;
iloczyn *=i;
...

Widzimy tu wielokrotnie powtarzające się fragmenty kodu. Oczywiście powinniśmy je umieścić w pętli.
Przy tym jednak istotne jest, aby dobrze sformułować warunki zakończenia działania pętli. Przecież nie możemy próbować sumować i mnożyć wszystkich liczb całkowitych, bo nasz program nie zakończyłby nigdy działania.
Ogólnie, warunki zakończenia działania pętli można podać w dwojaki sposób:
W instrukcjach sterujących, które wprowadzają pętle, ze względu na lepszą czytelność programu  podaje się warunki kontynuacji , a nie zakończenia pętli. Wykonanie pętli można jednak przerwać w jej środku - i wtedy warunek, który sprawdzimy, by przerwać pętlę - będzie warunkiem zakończenia działania pętli. Na to rozróżnienie warto początkowo zwracać baczną uwagę.

Na samym początku lepszemu zrozumieniu dzialania pętli sprzyja obrazowanie ich za pomocą schematów blokowych. Działanie bardziej skomplikowanych pętli dodatkowo należy prześledzić - iteracja po iteracji (oczywiście nie wszystkie, np. kilka poczatkowych iteracji i kilka końcowych).

Zobaczmy to na przykładzie  podnoszena do potęgi podanej liczby całkowitej.
Naszym pierwszym zadaniem jest policzenie n-tej potęgi (n>=0) podanej liczby całkowitej a.
Pętle o znanej liczbie iteracji zapisujemy zwykle za pomoca instrukcji for (...)
W naturalny sposób możemy zapisać to jako n-krotne mnożenie tej liczby przez samą siebie: a*a*a.... Będziemy mieli n iteracji. W każdej i-ej iteracji (1<= i <=n) zmienna wynik będzie miała wartość a w potędze i.
Sposób dzialania takiej pętli iteracyjnej ilustruje poniższy schemat blokowy.

r

Możemy sprawdzić dzialanie pętli "z kartką i olówkiem" dla jakiegoś zestawu danych wejściowych, np. a = 2 i n = 5.

Iteracja
Wartość
zmiennej i
Wartość warunku
kontynuacji pętli 
Wartość zmiennej wynik
1
1true2
2
2true4
3
3true8
4
4true16
5
5true32
-
6false (petla kończy działanie)32

Zwróćmy uwagę, że w tym algorytmie - po zakończeniu pętli - zmienna i będzie miała wartość o jeden większą od liczby wykonanych iteracji (chociaż traktowaliśmy ją jako licznik iteracji, to nie możemy powiedzieć, że pętla wykonała się i-razy).
Czy jesteśmy pewni poprawności tego algorytmu? Czy dobrze dobraliśmy wartość początkową i oraz rodzaj warunku kontynuacji?
Nie wystarczy rozpatrzeć tylko jednego, wybranego przebiegu obliczeń. Warto przyjrzeć się także  warunkom brzegowym zadania. Mamy podnosić dowolną liczbę całkowitą do potęgi całkowitej n >= 0. Co się zatem stanie, gdy n = 0? Pętla nie wykona się ani razu (bo i = 1 jest mniejsze od n = 0), ale wynik będzie poprawny (bo zmienną wynik zainicjowaliśmy wartością 1).
Ten sam efekt (poprawnego dzialania) można uzyskać gdyby zamiast i = 1 napisać i = 0, a zamiast i <= n napisać i < n. (oczywiście zwróćmy uwagę, że algorytm jest poprawny tylko dla n >= 0).
Częstym błędem przy konstrukcji pętli iteracyjnych jest wykonywanie iteracji o jeden raz za dużo lub jeden raz za mało
Ale nie możemy oczywiście całkiem dowolnie, na wyczucie stosować znaku < zamiast <= lub inicjowac zmiennej i zerem lub jedynką. Nie ma tu złotych reguł - dobór zależy od starannej analizy poprawności algorytmu. W przypadku tak prostego zadania o jakim była mowa przed chwilą nie ma tu większych problemów, ale w bardziej złożonych przypadkach właściwy dobór liczby iteracji może sprawiać kłopoty.

Rozważmy teraz inne zadanie. Mamy oto określić do jakiej parzystej potęgi n (n>=0) należy podnieść podaną liczbę całkowitą a (a > 1), aby osiągnąć wynik co najmniej równy  zadanej wielkości (którą określimy jako wartość całkowitą cel).

Rozpatrujemy parzyste potęgi n liczby a. Niech w oznacza wynik potęgowania.
Dla n = 2 w = a*a.
Dla n = 4 w = a*a*a*a.
Dla n = 6 w = a*a*a*a*a*a
Itd.
A zatem jesli  na początku w = 1, to następnie w pętli możemy uzyskiwać kolejne potęgi za pomocą wyrażenia:
w = w *a * a

Nie wiemy jednak z góry ile razy powinna wykonać się pętla. Naszym celem jest przecież określenie liczby n, a nie podniesienie a do znanej potęgi n.
Warunkiem kontynuacji pętli powinno być porównanie uzyskiwanych wartości w z zadaną liczbą cel.
Powiedziano: "osiągnąć wynik co najmniej równy  zadanej wielkości cel". A to oznacza, że pętla ma być kontynuowana dopóki w jest mniejsze od cel. Proszę zwrócić uwagę: nie równe lub mniejsze, ale mniejsze!
W samej pętli musimy nie tylko wyliczać bieżący wynik potęgowania, ale również zwiększać n (i to zawsze o 2, bo potęgi mają być parzyste).
A jaką początkową wartość powinno mieć n?
Oczywiście 0, bowiem takie są warunki zadania (cel - jest dowolną liczbą całkowitą,  n zaś może być >=0). Zatem np. dla cel <= 1, właściwą odpowiedzią jest n = 0 (bo zmienna w początkowo rowna jest 1, co jest większe lub równe wartości zmiennej cel,  pętla nie wykona się ani razu, ale dowolne a podniesione do potęgi 0 jest równe 1).

Pokazuje to następujący schemat blokowy.

r

Mamy ty zatem pętlę, która kończy działanie nie na skutek wyczerpania limitu iteracji, ale ze względu na niespełnienie warunku, który z liczbą iteracji nie ma nic wspólnego. Takie pętle możemy nazwać warunkowymi pętlami iteracyjnymi.

Bardzo ważną sprawą przy programowaniu pętli jest zagwarantowanie zakończenia ich działania. Często - przy niezbyt starannej analizie warunków zadania, przy jakichś konkretnych wartościach danych, możemy uzyskać pętle działające w nieskończoność.

Np. prawidłowe dzialanie obu omówionych wyżej algorytmów zależy od konkretnej reprezentacji liczb w języku. Jeżeli wyniki potęgowania będziemy przedstawiać jako liczby typu int, to pierwszy algorytm (przy dużych a i n ) nie da prawidłowych wyników, a drugi może się zapętlić (tzn, petla będzie wykonywana w nieskończoność). Wynika to z tego, że w którymś momecie (przy dużych a i n) wynik potęgowania nie będzie mieścił się w obszarze pamięci przeznaczonym do przechowywania liczb typu int.


2. Warunkowe pętle iteracyjne: instrukcje while i do..while


Instrukcja while ma następującą postać:

        while (wyr) ins

gdzie:

Działanie pętli while jest następujące. Wyrażenie wyr reprezentuje podobny "warunek" jak w instrukcji if : jego wartość jest wyliczana i jeśli jest równa true to wykonywana jest instrukcja ins, w przeciwnym razie sterowanie przekazywane jest do pierwszej instrukcji po pętli while.
Po wykonaniu instrukcji ins ponownie "sprawdzany jest warunek" zawarty w wyrażeniu wyr i jeśli jest prawdziwy całą operacja znów jest powtarzana. Ta pętla kończy działanie gdy wyrażenie wyr równe jest false lub gdy w instrukcji ins zawarto instrukcję break, lub return, które wyprowadzają sterowanie poza pętlę.

Przypomnienie: znaki można traktować jak liczby, bo są w komputerze reprezentowane za pomocą kodów liczbowych
Na przykład poniższy fragment wyprowadza wszystkie małe litery alfabetu angielskiego (będzie on działał prawidłowo dla wszystkich tablic kodowych, w których małe litery alfabetu angielskiego zajmują ciągłą przestrzeń tzn. po a następuje b, po b c - itd; "następowanie po" oznacza, że kod następnego znaku jest o 1 większy od kodu znaku poprzedniego).

        char c = 'a';              
        while (c <= 'z') System.out.print(c++);

Jak widać dla prawidłowego zakończenia pętli ważne jest aby w jej wnętrzu następowały zmiany (modyfikacje wartości zmiennych) wpływające na "warunek" reprezentowany przez wyrażenie w nawiasach while.
Warunki mogą być oczywiście złożone. Aby wyprowadzić "od końca" m ostatnich liter możemy napisać
        int n = 0;
        char c = 'z';
        while(c >= 'a' && n < m)  {
          System.out.println(c--);
          n++;
        }

Dajemy tu podwójny warunek, nie wiadomo bowiem czy m nie przekroczy liczby małych liter w tablicy kodowej.
Jeszcze inaczej to samo można napisać w następujący sposób:

int m = ...;
char c = 'z';
while(m-- > 0) {
    if (c  <  'a') break;
    System.out.println(c--);
}

Przed niedopuszczalnymi wartościami m zabezpieczamy się wewnątrz pętli; gdy kolejny kod znaku przekroczy zakres kodów dla małych liter (będzie mniejszy od kodu litery a) instrukcja break spowoduje przerwanie pętli.

Baczny Czytelnik zauważył zapewne tu dwa ciekawe zjawiska.
Po pierwsze. w pętli while możemy stosować licznik iteracji i wykonywać jakąś zadaną ich liczbę (tę rolę pełniły w powyższych przykładach najpiewr zmienna n, a później m).
Po drugie, pętla może nie wykonać "pełnej liczby" iteracji.  Gdy m ma wartość większą od liczby małych liter w alfabecie angielskim ostatnia iteracja wykonywana jest "w połowie": z dwóch instrukcji zapisanych w pętli wykonana zostanie tylko pierwsza (if...) i na skutek break sterowanie opuści pętlę.
"Pętla i pół" (po angielsku "loop and half") jest typowym przypadkiem przy wprowadzaniu jakichś danych i wykonywaniu na nich operacji w pętli,

Np. w poniższym programie w pętli wprowadzamy i sumujemy liczby całkowite dopóki ich suma nie osiagnie lub nie przekroczy podanego limitu. Dodatkowo w każdej chwili użytkownik może zakończyć sumowanie (przed osiagnięciem limitu), jesli tylko zrezygnuje z wprowadzenia danych w dialogu przez wybór Cancel.
import javax.swing.*;

public class Sumowanie {

  public static void main(String[] args) {

    final int LIMIT = 200;
    int sum = 0;

    while(sum < LIMIT)  {
      String data = JOptionPane.showInputDialog("Podaj liczbę całkowitą:");
      if (data == null) break;
      sum += Integer.parseInt(data);
    }
    System.out.println("Suma: " + sum);
    System.exit(0);
  }
 }
Komentarze - przypomnienia:
Gdybyśmy w powyższym sumowaniu  zmienili działanie programu w taki sposób, by suma nigdy nie mogła  przekroczy podanego limitu (może być równa), to otzrymalibysmy następujący kod:

import javax.swing.*;

public class Sumowanie1 {

  public static void main(String[] args) {

    final int LIMIT = 200;
    int sum = 0;
    String in;
    while((in = JOptionPane.showInputDialog("Podaj liczbę :")) != null)  {
      int a = Integer.parseInt(in);
      if (sum + a > LIMIT ) break;
      sum += a;
    }
    System.out.println("Suma: " + sum);
    System.exit(0);
  }

}

Nieco inne podejście do sterowania wykonaniem pętli while można zrealizować za pomocą tzw. zmiennej sterującej:

boolean again = true;
while(again) {
  .....
  if (/*warunek zakończenia pętli*/) again = false;
}
Ten sam przykład można zapisać za pomoca pętli nieskończonej i z użyciem break:

while (true) {
    ...
    if (/*warunek zakończenia petli*/) break;
}


Inną instrukcją sterującą jest instrukcja do...while.

Instrukcja do ... while jest bardzo podobna do while.
Ma ona postać:

                do ins while (wyr)


Znaczenie ins i wyr jest takie samo jak w przypadku instrukcji while. Jedyna (zaznaczana zresztą przez zapis) różnica w stosunku do instrukcji while polega na tym, że warunek określony przez wyrażenie wyr jest sprawdzany po każdym wykonaniu pętli (instrukcji ins), a nie przed (jak to jest w przypadku while).

Pętla while może więc nie wykonać się ani razu, natomiast pętla do ... while zawsze wykona się przynajmniej raz.


Podsumujmy. Pętle while lub do..while stosujemy zwykle wtedy, gdy kontynuacja działania pętli zależy od jakieś warunku, a liczba iteracji nie jest z góry znana lub łatwa do określenia.

Zobaczmy teraz bardziej praktyczny przykład zastosowania instrukcji while.
Będziemy symulować zmiany stanu konta bankowego.
Warunki są takie:
Należy stworzyć klasę Konto (Account) o podanych charakterystykach i dostarczyć w niej metody np. o nazwie getMonthsToBalance, która (za pomocą symulacji miesięcznych zmian konta) pozwala odpowiedzieć po ilu miesiącach suma na koncie osiągnie podaną jako argument docelową wielkośc

Testowanie klasy Account może wyglądac tak:

// Tworzymy obiekt konto ze stanem 2000, wpłatami 2400, wypłatami 1800 i oprocentowaniem 10% w skali roku

Account ac = new Account(2000, 2400, 1800, 10);

// ile miesięcy zajmie uzyskanie na koncie  sumy co najmniej 10000

int lMies =   ac.getMonthsToBalance(10000);


Jedno z możliwych rozwiązań jest następujące:
public class Account {

  private double balance;          // stan konta
  private double monthIncome;      // stałe miesięczne wpływy (dochód)
  private double monthExpend;      // stałe miesięczne wydatki
  private double interest;         // stopa oprocentowania (roczna)

  // Konstruktor

  public Account(double s, double wpl, double wypl, double p) {
    balance = s;
    monthIncome = wpl;
    monthExpend = wypl;
    interest = p;
  }

  // Metoda - zwraca aktualny stan konta

  public double getBalance() {
    return balance;
  }

  // Metoda - zwraca liczbę miesięcy potrzebnych
  // by stan konta osiągnął wartość targetBalance

   public int getMonthsToBalance(double targetBalance) {

    int n = 0;                                // miesiące
    double diff = targetBalance - balance;    // różnica między aktualnym
                                              // i docelowym stanem
    while (diff > 0) {         // dopóki jest TA różnica -
                               // symulujemy upływ miesięcy i zmiany konta
      n++;
      balance *= (1 + (interest/100)/12);     // doliczenie odsetek
      balance += monthIncome - monthExpend;   // dochody, wydatki
      double prevDiff = diff;                 // poprzednia różnica
      diff = targetBalance - balance;         // bieżąca różnica
      if (prevDiff <= diff) return -1;        // jeżeli różnica się
    }                                         // nie zmniejsza - nie ma szans
    return n;                                 // osiagnięcia docelowego stanu
  }


}

// Klasa testująca konto

class TestKonta {

  public static void main(String[] args) {

    Account ac = new Account(2000, 2400, 1800, 10);
    double cel = 10000;
    int m =  ac.getMonthsToBalance(cel);
    System.out.println("Miesiace do osiagniecia co najmniej " + cel + ":");
    System.out.println(m + " --- stan konta " + ac.getBalance());
  }


}

Wynik dzialania programu:
Miesiace do osiagniecia co najmniej 10000.0:
13 --- stan konta 10430.006715578125


Klasa jest suto komentowana, zwróćmy więc uwagę tylko na to, że staramy się tu zabezpieczyć przed nieosiągalnymi docelowymi stanami konta (np. kiedy wydatki są większe od dochodów, a - w którymś momencie - oprocentowanie nie pokryje tej różnicy, to docelowy stan, który jest większy od aktualnego nigdy nie zostanie osiągnięty). Innymi słowy staramy się zapewnić  zakończenie pętli while. W tym programie robimy to  sprawdzając w pętli - czy z każdą  iteracją różnica pomiędzy stanem docelowym i aktualnym zmniejsza się. Jeśli nie, to nie ma szansy na osiągnięcie stanu docelowego i metoda zwraca -1 jako liczbę miesięcy.  



3. Pętle iteracyjne o danej liczbie powtórzeń: instrukcja for

Instrukcja for ma następującą postać.


            for (init; wyr; upd) ins
gdzie:
 


Instrukcja for tworzy pętlę, która działa w następujący sposób:
  1. Opracowywana jest deklaracja lub lista wyrażeń w części init  (jeśli występuje).
  2. Wyliczane jest wyrażenie wyr. Jeżeli jego wartość równa jest false, to instrukcja for kończy działanie, w przeciwnym razie (lub gdy wyr jest pominięte) działanie instrukcji for jest kontynuowane (krok 3).
  3. Wykonywana jest instrukcja ins (jeśli jest to instrukcja grupująca i w jej "środku" znajduje się instrukcja break lub return  przekazująca sterowanie poza blok tej instrukcji grupującej, to instrukcja for kończy działanie)
  4. Obliczane są  wyrażenia w części upd (jeśli występuje). Działanie jest wznawiane od kroku 2.
 
Porównując instrukcję for z instrukcją while można wskazać, że taki sam efekt jak za pomocą instrukcji:

                        for (init; wyr; upd) 

możemy uzyskać zapisując następujący fragment za pomocą instrukcji while:
init;
while (wyr) {
  ins;
  upd;
}

Dobry styl programowania nakazuje używać instrukcji for wyłącznie w tym celu.
Najprostsze zastosowanie ma instrukcja for przy organizacji pętli iteracyjnych ze znanym zakresem iteracji. W tym przypadku częśc init inicjuje licznik, wyrażenie wyr kontroluje granice licznika, część upd zmienia bieżącą wartość licznika.


Krótkie przykłady:


int n = ...;
int sum = 0;
for (int i = 1; i <= n; i++) sum += i;

(sumuje n pierwszych dodatnich liczb całkowitych)

for (int i = 2; i <= n; i+=2) sum += i;

(sumuje n pierwszych dodatnich liczb parzystych)

for (int i = 1000; i  >= 990; i--) System.out.println(i);

(wyprowadza na konsolę kolejne liczby od 1000 do 990)

for (char c = 'a';  c <= 'z';  c++) System.out.print(c);

(wyprowadza na konsolę kolejne małe litery od a do z)


W części init może wystąpić deklaracja kilku zmiennych (ale jedna deklaracja, zatem muszą być to zmienne tego samego typu). Np.

for (int i =1, j = 2; i <=n && j <= m; i++, j++)  System.out.println(i+j);


Zasięg zmiennych deklarowanych w części inicjacyjnej  instrukcji for zaczyna się od miejsca deklaracji i kończy wraz z końcem instrukcji for


Obrazuje to poniższy rysunek:

Rys

Na przyklad:
int a =0, n = 10;
for (int i = 0; i < n; i++) a += i;
for (int j = i; j < n*2; j++) a+=j; <--- błąd! zmienna i jest nieznana

System.out.println(i);  <--- błąd! zmienna i jest nieznana

for (int i = n; i< n*2; i++) a+=i;  <--- OK, nowa zmienna i

for (int i = 0; i < n; i++)  {         <--- OK, nowa zmienna i
      a += i;                                       używana w bloku
      System.out.println(i + " " + a)
}

for (int i = 0; i < n; i++)  {         <--- OK, nowa zmienna i
      int i = 1;                             <--- błąd!  redeklaracja zmiennej i
      ...
}
Terminem redeklaracja określamy ponowną deklarację zmiennej lokalnej w jej zasięgu, tzn. w bloku. Przypomnijmy: każda zmienne lokalna może być deklarowana tylko raz w bloku i nie jest dopuszczalne przesłanianie nazw zmiennych lokalnych w blokach wewnętrznych. Nazwy zmiennych, opisujących pola klasy mogą być w blokach metod przesłaniane.


Oczywiście, wcale nie musimy deklarować zmiennych w części inicjacyjnej instrukcji for, ale - pamiętajmy, że zgodnie z ogólną zasadą - każda zmienna musi być przed użyciem zadeklarowana:

int a =0, n = 10, i;
for (i=0; i < n; i ++) a+=i;        // Ok
for (i=n; i < n*2; i++) a+=i;      // Ok

Ale nie wolno pisać tak:

int i = 0;
for (int i = 0; i< 10; i++) ... //  błąd! redeklaracja i


Przejdżmy teraz do praktycznych zastosowań isntrukcji for.
Pierwsze zadanie: za pomocą instrukcji for policzyć n-tą potęgę (n>=0) podanej liczby całkowitej a.

Możliwe rozwiązanie - metoda pow w klasie Liczba, która może być użyta na rzecz dowolnej liczby całkowitej:

public class Liczba {

  int a;

  public Liczba(int liczba) {  // konstruktor
    a = liczba;
  }

//...

  public double pow(int n) {

    if (n < 0) {        // warunek konieczny: n >=0
       System.out.println("Niedopuszczalna wartość wykładnika");
       return -0.1;
    }
    double wynik = 1;
    for (int i = 1; i <= n; i++) wynik *= a;
    return wynik;
  }

Zauważmy, że w pętli for zastosowaliśmy zmiany licznika od 1 do n (włącznie). Ten sam efekt moglibyśmy osiągnąć pisząc: for (i=0; i<n; i++).

Instrukcje w następujących pętlach for zostaną wykonane n - razy

        for (int i = 1; i <= n; i++) ....

        for (int i = 0; i < n; i++) ....

i pod względem liczby "obrotów" pętli obie instrukcje for są równoważne


Drugi praktyczny przykład zastosowania instrukcji for polega na rozwiązaniu następującego zadania.
W zbudowanej i omówionej wcześniej  klasie Account dostarczyć metody:
double getBalanceAfter(int n), która zwraca stan konta po n miesiącach.


Możliwe rozwiązanie:
  public double getBalanceAfter(int n) {
     double wspOds = (interest/100)/12;
     for (int i = 1; i <= n; i++)
       balance += wspOds*balance +  monthIncome - monthExpend;
     return balance;
  }


Ogólnie, instrukcja for jest bardzo elastyczna. Spełniając reguły składniowe możemy pisać dosyć dziwne programy (np. w calości złożone z jednej tylko intsrukcji for). Choć można tak programować, to jest to raczej niewskazane, gdyż program jest mało czytelny i narażony na błędy.
Podkreślić też warto, że części (init, wyr,  upd) instrukjci for są nieobowiązkowe (każda z nich może być pominięta). Przy tym jednak nie wolno pomijać średników rozdzielających te częsci.
W szczególności, za pomocą for w następujący sposób można zapisać odpowiednik pętli nieskończonej while(true):

for(;;) {
...
}

Oczywiście, wewnętrz takich nieskończonych pętli nalezy przewidzieć i umieścić warunki ich zakończenia za pomocą instrukcji break lub return.

Ogólnie jednak: używajmy instrukcji for wyłącznie do konstruowania pętli ze znaną liczbą lub zakresem iteracji, czyli w postaci:

        for ( i = ?; i <?; i = i ± ?) ...

wszystkie inne przypadki programując za pomoca instrukcji while lub do..while


Na koniec warto zwrócić uwagę na to, że pętle iteracyjne mogą być dowolnie zagnieżdżane.
Przecież ins w instrukcjach while, do..while i for - to dowolne instrukcje, w tym instrukcje sterujące.

Nic nie stoi na przeszkodzie, by np. zapisać taki fragment:

public class Nested {

  public static void main(String[] args) {
     String out = null;
     char c = 'a';
     while (c <= 'd') {
       for (int i=1; i<=2; i++) {
         out = "Dla " + c + " " + i + " mamy j =";
         for (int j = i; j <= i + 3; j++) out += " " + j;
         System.out.println(out);
       }
       c++;
     }
   }

}
i uzyskać wydruk:
Dla a 1 mamy j = 1 2 3 4
Dla a 2 mamy j = 2 3 4 5
Dla b 1 mamy j = 1 2 3 4
Dla b 2 mamy j = 2 3 4 5
Dla c 1 mamy j = 1 2 3 4
Dla c 2 mamy j = 2 3 4 5
Dla d 1 mamy j = 1 2 3 4
Dla d 2 mamy j = 2 3 4 5



4. Przerywanie i kontynuowanie pętli

Widzieliśmy już w działaniu instrukcję break. Powoduje ona:
Dzięki zastosowaniu etykiety możliwe jest wychodzenie z głęboko zagnieżdżonych pętli.
Zobaczmy to na przykładzie:

Poniższy program
public class Break {

  public static void main(String[] args) {
    for (int i=1; i < 10; i++) {
      System.out.println("Petla po i: i = " + i);
      for (int j=1; j < 10; j++) {
        if (i + j  > 5) break;
        System.out.println("Pętla po j: i = " + i + "  j = " + j);
      }
    }
  }

}
wyprowadzi na konsolę podaną sekwencję napisów.
Petla po i: i = 1
Pętla po j: i = 1  j = 1
Pętla po j: i = 1  j = 2
Pętla po j: i = 1  j = 3
Pętla po j: i = 1  j = 4
Petla po i: i = 2
Pętla po j: i = 2  j = 1
Pętla po j: i = 2  j = 2
Pętla po j: i = 2  j = 3
Petla po i: i = 3
Pętla po j: i = 3  j = 1
Pętla po j: i = 3  j = 2
Petla po i: i = 4
Pętla po j: i = 4  j = 1
Petla po i: i = 5
Petla po i: i = 6
Petla po i: i = 7
Petla po i: i = 8
Petla po i: i = 9

Instrukcja break przerywa tutaj wykonanie pętli w której jest umieszczona ("pętli po j"), nie przerywa natomiast wykonania tej pętli w której wewnętrzna pętla ("po j") jest zagnieżdżona. 

Gdybyśmy zastosowali etykietę, to moglibyśmy przerwać działanie pętli zewnętrznej ("po i"):

public class Break {

  public static void main(String[] args) {
    outerLoop: for (int i=1; i < 10; i++) {
      System.out.println("Petla po i: i = " + i);
      for (int j=1; j < 10; j++) {
        if (i + j  > 5) break outerLoop;
        System.out.println("Pętla po j: i = " + i + "  j = " + j);
      }
    }
  }

}
i wynik byłby całkiem inny:
Petla po i: i = 1
Pętla po j: i = 1  j = 1
Pętla po j: i = 1  j = 2
Pętla po j: i = 1  j = 3
Pętla po j: i = 1  j = 4



Instrukcja continue powoduje przerwanie bieżącej iteracji i przejście do następnej iteracji pętli. Podobnie jak w przypadku break działanie to dotyczy tylko tej (wewnętrznej) pętli, w której instrukcja ta jest umieszczona. Jeśli natomiast użyjemy etykiety, to inicjowana jest następna iteracja tej pętli, która daną etykietą została oznaczona.

Przykładowy program.

import javax.swing.*;
import java.io.*;

public class Continue {

  public static void main(String[] args) {
    new Continue();
  }

  public Continue() {

    String header = null;
    int i = 0;

    outerLoop:
      while ((header = ask("Nagłówek?")) != null) {
        i++;
        if (header.equals("")) continue;
        int j = 0;
        while (true) {
          String txt = ask("Tekst?");
          if (txt == null) break outerLoop;
          j++;
          if (txt.equals("")) continue;
          if (txt.equals("nh")) continue outerLoop;
          System.out.println(i + " " + header + " : " + j + " " + txt);
        }
      }

    System.out.println("Koniec");
    System.exit(0);
  }

  private String ask(String txt) {
    String s = JOptionPane.showInputDialog(txt);
    return s;
  }
}
Program zawiera dwie pętle. W zewnętrznej użytkownik pytany jest o napis traktowany jako "nagłówek". Jeżeli poda pusty napis, to pętla przechodzi do następnej iteracji. W przeciwym razie wprowadzana jest pętla wewnętrzna, w której użytkownik pytany jest o teksty, a na konsolę wyprowadzany jest numer iteracji pętli zewnętrznej, nagłówek, numer iteracji pętli wewnętrznej i tekst podany w pętli wewnętrznej. Jeżeli ten podany tekst jest pusty, to - zamiast wyprowadzenia napisu -  następuje przejście do następnej iteracji pętli wewnętrznej. Natomiast jeżeli podany tekst to "nh" (next header) - następuje przejście do następnej iteracji pętli zewnętrznej. Program kończy dzałanie gdy w którymkolwiek okienku dialogowym użytkownik wciśnie Cancel.

Przykładowo, jeśli w tym programie zostanie wprowadzona następująca informacja:
Nagłówek? --- A
Tekst? --- aaa
Tekst? --- bbb
Tekst? ---
Tekst? --- ccc
Tekst? --- nh
Nagłówek? ---
Nagłówek? --- B
Tekst? --- aaa
Tekst? --- bbb

to na konsoli zobaczymy:
1 A : 1 aaa
1 A : 2 bbb
1 A : 4 ccc
3 B : 1 aaa
3 B : 2 bbb
Koniec

Proszę zwrócić uwagę na zmiany numerów iteracji powodowane wprowadzeniem pustego tekstu oraz skutki wprowadzenia tekstu "nh".

5. Praktyczny przykład iteracji: wczytywanie danych z plików tekstowych za pomocą skanera

O operacjach wejście-wyjścia dowiemy się więcej nieco później. Teraz - w charakterze praktycznego przykładu łączącego iteracje, obsługę wyjątków oraz instrukcje decyzyjne - zobaczymy jak w łatwy sposób można wczytać dane z pliku tekstowego za pomocą skanera.
Widzieliśmy już skaner w zastosowaniu do standardowego wejścia oraz napisów reprezentowanych przez obiekty klasy String. Źródłem danych skanera może być również plik.

Obiekty plikowe są w Javie reprezentowane przez klasę File.
Możemy np. napisać:

    File f = new File("plik.txt");

i zmienna f będzie reprezentować plik o nazwie "tekst.txt" z bieżącego katalogu (istniejący lub taki, który dopiero będzie tworzony).

Utworzenie obiektu skanera do skanowania pliku "tekst.txt" wygląda więc tak:
Scanner scan = new Scanner(new File("plik.text"));
Teraz za pomocą metod:

String scan.next()
boolean scan.hasNext()
 możemy pobierac kolejne symbole z pliku (napisy rozdzielone "białymi znakami")
i sprawdzac czy jest jezcze jakiś niezeskanowany symbol
String scan.nextLine()
boolean scan.hasNextLine()
możemy pobierac kolejne wiersze pliku
i sprawdzać czy sa jeszcze nie przeczytane wiersze
int scan.nextInt()
boolean scan.hasNextInt()
możemy pobierać kolejne napisy reprezentujące liczby calkowite i uzyskiwac ich binarna reprezentację i asprawdzać czy są jeaszcze nie przeczytane liczby
double scan.nextDouble()

boolean  scan.hasNextDouble()
j.w. w odniesieniu do liczb rzeczywistych
inne metody next...()
i hasNext(...)
j.w. w odniesieniu do innych prostych typów danych

Przykładowy program zlicza wiersze pliku tekstowego i sumuje ich długości:
public class GetLines {

  public static void main(String[] args) {
    String fname = "tekst.txt";
    Scanner scan = null;
    try {
      scan = new Scanner(new File(fname));
    } catch (FileNotFoundException exc) {
       System.out.println("Plik " + fname + " nie istnieje");;
    }
    int lcount = 0;
    int strLength  = 0;
    while (scan.hasNextLine()) {
      String line = scan.nextLine();
      lcount++;
      strLength += line.length();
    }
    System.out.println("Liczba wierszy pliku: " + lcount + 
                       "\nIch łączna długość: " + strLength );
  }

}
Zwróćmy uwagę: konstruktor  Scanner(File) może zgłosic kontrolowany wyjątek FileNotFoundException (nie ma pliku o podanej nazwie). Musimy go obsługiwac.
Pokazana pętla while jest typową idiomatyczną kosntrukcją: dopóki są jeszcze wiersze wykonuj instruukcje w pętli, gdy nie ma wierszy (koniec pliku) zakończ działanie.
Dla pliku o zawartości:
ala
ma
kota
program wyprowadzi na konsolę:
Liczba wierszy pliku: 3
Ich łączna długość: 9

Inny przykład pokazuje z jaką łatwością za pomocą skanera można wczytywac liczby z plików tekstowych (a więc wczytywac napisy, któr przedstawiają liczby i od razu uzyskiwac ich binarną reprezentację, umożlwiająca wykonywanie obliczeń). W programi będziemy sumować liczby całkowite zapisane w pliku.

import java.io.*;
import java.util.*;

public class SumNumbers {

  public static void main(String[] args) {
    File f =  new File("nums.txt");
    long sum = 0;
    String msg;
    try {
      for (Scanner sc = new Scanner(f); sc.hasNextInt(); sum += sc.nextInt());
      msg = "Suma: " + sum;
    } catch (Exception exc) {
        msg = exc.toString();
    }
    System.out.println(msg);
  }

}
Tutaj ciekawostką jest instrukcja for: w jednej instrukcji zawarliśmy wczytywanie i sumowanie liczb!
W obsłudze wyjątków daliśmy ogólną klasę Exception, bowiem może powstać nie tylko wyątek FileNotFoundException, ale również wyjątek InputMismatchException przy próbie skanowania napisu, któr nie ma formatu liczby całkowitej.

Dla pliku o zawartości:
1 2 3
5 7 9
program wyprowadzi na konsolę:
Suma: 27



Dodatkowe uwagi:


6. Podsumowanie

W charakterze podsumowania przedstawiona zostanie program operujący na kontach bankowych z użyciem odpowiedniej wersji klasy Account, enumeracji określających typ konta i rodzaj operacji na koncie, własnej klasy wyjątku AccountException i różnych wariantów instrukcji sterujących.
Program będzie składał się z klasy Account, klasy wyjątku AccountException, klasy testującej Test i publicznych klas-enumeracji, umieszczonych w odrębnych plikach.
Po kolei,

enumeracje:
// typy kont
public  enum AccType { 
  NORMAL, SILVER, GOLD 
};
// typy operacji
public enum OpType {
  DEPOSIT, WITHDRAW, TRANSFER
}

własna klasa wyjątku:
public class AccountException extends Exception {

  public AccountException() {
  }

  public AccountException(String msg) {
    super(msg);
  }

}

klasa definiująca konto:
public class Account {
  
  private static double interestRate; // oprocentowanie
  
  private AccType at;                // typ konta 
  private String id;                 // numer konta  
  private double balance;            // stan konta

  private static double normalFee;          // opłaty za konto
  private static double silverFee;
  private static double goldFee;

  // Konstruktor
  // - tworzy nowe konto podaneego typu i o podanym numerze
  public Account(AccType at, String id) {
    this.id = id;
    this.at = at;
  }

  // Stan konta
  public double getBalance() {
    return balance;
  }

  // Wplata
  // jezeli < 0 zglaszamy wyjatek (bledny argument)
  // jesli wyjatek nie zostanie obsluzony - program zakonczy dzialania
  public void deposit(double d) throws AccountException {
    if (d < 0) throw new AccountException("Deposit should be >= 0");
    balance += d;
  }

  // Wyplata z konta
  // jeżeli < 0 zgłaszamy wyjątek (błędny argument)
  // jeżeli wyplata przekraca stan konta - wyjątek "Wypłata niedozwolona"
  public void withdraw(double d) throws AccountException  {
    if (d < 0) throw new AccountException("Withdrawal should be >= 0");
    if (balance - d < 0)
      throw new AccountException(
                "Withdrawal exceeding balance not allowed");
    balance -= d;
  }

  
  // Przelew z konta na konto
  // Parametry:
  // - account - konto na ktore przelewamy
  // - d - ile przelewamy
  public void transfer(Account account, double d) throws AccountException {
    withdraw(d);
    account.deposit(d);
  }

  // Kumulacja odsetek (jednorazowa, w wysokosci oprocentowania)
  public void addInterest() {
    balance *= (1 + interestRate / 100);
  }
  
  // Pobranie opłaty za prowadzenie konta
  public void fee() {
    switch (at) {
      case NORMAL : balance -= normalFee; break;
      case SILVER : balance -= silverFee; break;
      case GOLD : balance -= goldFee;
    }
  }

  // Metoda statyczna: ustala oprocentowanie dla wszystkich kont
  public static void setInterestRate(double d) throws AccountException {
    if (d < 0) throw new AccountException("Interest should be >= 0");
    Account.interestRate = d;
  }
  
  public static void setNormalFee(double normalFee) {
    Account.normalFee = normalFee;
  }
  
  public static void setSilverFee(double silverFee) {
    Account.silverFee = silverFee;
  }

  public static void setGoldFee(double goldFee) {
    Account.goldFee = goldFee;
  }

  // Informacja o koncie
  public String toString() {
    return id + " account type " + at + " balance " + balance;
  }

}
klasa testująca:
import java.util.*;
import javax.swing.*;


public class Test {
  
  public static void msg (String s) {
    JOptionPane.showMessageDialog(null, s);
  }
  
  public static String ask(String msg, Object data) {
    return JOptionPane.showInputDialog(msg, data);
  }

  public static void main(String[] args) {
    // Ustalenie parametrów kont
    try {
      Account.setInterestRate(5);
      Account.setNormalFee(10);
      Account.setSilverFee(20);
      Account.setGoldFee(40);
    } catch (AccountException exc) {
        msg("Invalid accounts param: " + exc.getMessage());
        return;
    }
    
    // Konta
    Account ac1 = new Account(AccType.GOLD, "A0000001"),
            ac2 = new Account(AccType.NORMAL, "A0000001");
    
    // Wplaty, wyplaty, transfery 
    String in = "";
    while ((in = ask("Enter: \n" +
                     "DEPOSIT ammount - for deposit to A1\n" + 
                     "WITHDRAW ammount - for withdrawal from A1\n" +
                     "TRANSFER ammount - for transfer from A1 to A2", in)) != null) {
      try {
        Scanner sc = new Scanner(in);
        OpType op = OpType.valueOf(sc.next());
        double d = sc.nextDouble();
        switch (op) {
          case DEPOSIT  : ac1.deposit(d); break;
          case WITHDRAW : ac1.withdraw(d); break;
          case TRANSFER : ac1. transfer(ac2, d); break;
          default: msg("No such operation"); break; 
        }
        msg("Accounts:\n" + ac1 + '\n' + ac2); 
      } catch(Exception exc) {
         msg("Invalid data\n" + exc.getMessage());
      }
    }
      
    // Symulacja stałych miesięcznych wpłat-wypłat - jaki stan konta po n miesiącah?
    in = "";
    
    int m = 0;
    double deposit = 0, withdraw = 0;
    while ((in = ask("Enter: number of months, month deposits, month withdrawals", in)) != null) {
      Scanner sc = new Scanner(in);
      try {
        m = sc.nextInt();
        deposit = sc.nextDouble();
        withdraw = sc.nextDouble();
        for (int i = 1; i <= m; i++) {
          ac1.deposit(deposit);
          ac1.withdraw(withdraw);
          ac1.addInterest();
          ac1.fee();
        }
        msg("After " + m + " months balance should be " + ac1.getBalance());
      } catch(Exception exc) {
          msg("Invalid data" + exc.getMessage());
      }
    }
    
  }

}

Zobacz działanie programu: