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:
- jako liczbę iteracji do wykonania (liczbę powtórzeń instrukcji umieszczonych
w pętli) - np. zsumowac i pomnożyć 20 pierwszych dodatnich liczb całkowitych
- jako jakiś inny warunek - np. sumowac i mnożyć kolejne liczby aż do
chwili, gdy ich iloczyn stanie się równy lub większy od 100000.
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.
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
|
1 | true | 2 |
2
|
2 | true | 4 |
3
|
3 | true | 8 |
4
|
4 | true | 16 |
5
|
5 | true | 32 |
-
|
6 | false (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.
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:
- wyr - wyrażenie, dające w wyniku wartość typu boolean
- ins - dowolna instrukcja (w tym grupująca)
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:
- stałe deklarujemy jako final, używamy ich zamiast "ważnych"
literałów (takich jak np limit sumowania, liczba powtórzeń, etc)
- jeśli zrezygnowano z wprowadzania danych (np. poprzez wybranie Cancel
w okienku dialogowym), to metoda showInputDialog zwraca null
- instrukcja break służy do przerywania pętli
- liczby całkowite wprowadzone w dialogu są napisami - trzeba je przekształcić
na postać binarną i do tego służy statyczna metoda parseInt z klasy Integer
- moglibyśmy napisać int a = Integer.parseInt(data); sum = sum + a; jednak
krócej to samo zapisujemy jako: sum += Integer.parseInt(data);
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:
- konto ma jakiś dany stan (ilość pieniędzy na koncie)
- co miesiąc na konto wpływa zadana suma i wypłacana jest z niego inna zadana suma
- suma na koncie jest oprocentowana w skali rocznej wg podanej stopy procentowej
- odsetki są doliczane do tej sumy co miesiąc, na początku miesiąca, przed zaksięgowaniem miesięcznego wpływu i wydatku
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:
- część init - może nie występować albo może być deklaracją jednej lub
wielu zmiennych (tego samego typu) lub wyrażeniem lub listą wyrażeń rozdzielonych przecinkami.
- część wyr - jest wyrażeniem o wartości typu boolean (lub może nie występować),
- część upd - jest wyrażeniem lub listą wyrażeń rozdzielonych przecinkami (lub może nie występować).
- ins - jest instrukcją wykonywaną w pętli (w szczególności może to być instrukcja grupująca)
Instrukcja for tworzy pętlę, która działa w następujący sposób:
- Opracowywana jest deklaracja lub lista wyrażeń w części init (jeśli występuje).
- 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).
- 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)
- 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:
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:
- przerwanie bieżącej (najbardziej wewnętrznej, w której jest umieszczona) pętli
- lub - jeśli następuje po niej etykieta - wyjście sterowania z bloku oznaczanego tą etykietą
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:
- w
środowisku Eclipse przy uruchamianiu aplikacji biezacym katalogiem jest
katalog projektu; w nim nalezy umieścić przykładowe pliki,
- w przedstawionej
w przykładowych programach formie konstruktora Scanner(File), pliki
tekstowe są interpretowane zgodnie z domyślną stroną
kodową, ustawioną w systemie (więcej o kodowaniu plików dowiemy
się później)
- ogólnie Skaner, działający na plikach należy
zamykac po wykorzystaniu (scan.close()), co spowoduje zamknięcie
skanowanego pliku. W prostych przykładach czytania plików nie było to jednak niezbędne.
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: