« poprzedni punkt  następny punkt »

4. Podejmowanie decyzji: instrukcje if oraz if-else

Znana nam już instrukcja if ma postać:



if (war) ins

gdzie:
  • wyr - warunek - dowolne wyrażenie, mające typ wyniku boolean
  • ins - dowolna instrukcja (w tym grupująca)
Działanie: instrukcja ins jest wykonywana wtedy i tylko wtedy, gdy wartością wyrażenia war jest true

Instrukcja if-else rozszerza działanie instrukcji if. Ma ona postać.



if (war) ins1
elseins2

gdzie:
  • wyr - warunek - dowolne wyrażenie, mające typ wyniku boolean
  • ins1, ins2 - dowolne instrukcje (w tym grupujące)
Działanie: instrukcja ins1 jest wykonywana wtedy i tylko wtedy, gdy wartością wyrażenia war jest true. Jeżeli wartością wyrażenia war jest false - wykonywana jest instrukcja ins2

Różnica pomiędzy zastosowaniem instrukcji if oraz if-else można zobaczyć wyraźnie na
poniższych fragmentach kodu:

if (a == b) c = d;
c = e;
System.out.println( a + " " + b + " " + c + " " + d);

oraz

if (a == b) c = d;
else c = e;
System.out.println( a + " " + b + " " + c + " " + d);

W pierwszym fragmencie instrukcja c = d; wykonana zostanie tylko wtedy, gdy wartość zmiennej a będzie równa wartości zmiennej b. Niezależnie jednak od tego, czy warunek ten zajdzie czy nie - bezpośrednio po wykonaniu instrukcji if zmiennej c zostanie przypisana wartość zmiennej e (if jest więc tu bez sensu!). Następnie wyniki zostaną wyprowadzone na standardowe wyjście.
W drugim fragmencie zmienna c będzie miała rzeczywiście wartość zależną od tego czy a == b czy też nie (wykonane zostanie przypisanie albo c = d albo c = e). Potem wyniki powędrują na wyjście.
Przykład ten nie znaczy oczywiście, że zawsze trzeba stosować instrukcję if-else, a samo if ma mniejsze znaczenie.

Warto zauważyć, że instrukcjami ins1 i ins2 (w syntaktycznym opisie instrukcji if-else) mogą być również instrukcje if. Pozwala to sprawdzać rozgałęzione warunki np.

char op;
double a, b, r;
...
if (op == '+') r = a + b;
else if (op == '-') r = a - b;
     else if (op == '*') r = a*b;
          else if (op == '/') r = a/b;
               else System.out.println("Błędny kod operacji");

Przy takich okazjach powstaje kwestia: które else odpowiada któremu if ? Zasada jest prosta: danemu else odpowiada pierwsze poprzedzające go i znajdujące się w tym samym bloku if nie mające jeszcze swojej "pary" w postaci else. Wcięcia - poprawiające czytelność - programu w żaden sposób nie decydują o odpowiedniości if i else.
Jeśli ktoś na przykład napisze:

if (a >= 0) if (a <= 100) System.out.println( "a w przedziale od 0 do 100");
else System.out.println("a mniejsze od 0");

to będzie to oczywisty błąd.

Taką konstrukcję można i należy oczywiście zaprogramować inaczej ( if (a >= 0 && a <= 100) ... ), ale gdyby się ktoś uparł przy zastosowaniu podwójnego if, to należałoby to zapisać tak

if (a >= 0) {
if (a <= 100) System.out.println("a w przedziale od 0 do 100");
}
else System.out.println("a mniejsze od 0");

Zastosowanie nawiasów klamrowych (uczynienie bloku z drugiej instrukcji if) rozwiązuje problem, bowiem dopasowanie if i else odbywa się zawsze tylko w ramach tego samego bloku.

Częste błędy przy stosowaniu instrukcji if oraz if -else, na które trzeba zwracać szczególną uwagę.

A. Złe dopasowanie if i else w przypadku kilku instrukcji if (omówione przed chwilą)

B. Stawianie średnika po nawiasie zamykającym warunek instrukcji if np. :

if (a > b);
System.out.println("a > b"); // niezależnie od tego czy a>b !

C. Zapomnienie nawiasu zamykającego instrukcję grupującą np.

if (a > b) {
c = a + b;
d = e + f;
else c = d + f;


Szczególnie częstym błędem przy sprawdzaniu wielu warunków (które mogą się na siebie "nakładać") jest stosowanie instrukcji if bez else, nie uwzględnienie wszystkich możliwości lub nieprawidłowa kolejność sprawdzania warunków.
Błędy te są szczególnie niebezpieczne, gdyż dotyczą logiki programu i nie mogą być wykryte przez kompilator

Np. jeśli ktoś chce powiedzieć coś o liczbie a (czy jest duża, średnia, mała) to mógłby zapisać to w ten sposób:

     if (a >= 1000) System.out.println("Duża liczba")
     if (a >= 100) System.out.println("Średnia liczba")
     if (a >= 10) System.out.println("Mała liczba")
Duża liczba
Średnia liczba
Mała liczba

co jest oczywistym błędem, bo jeśli a = 1000, ten fragment wyprowadzi na konsolę wzajemnie wykluczającą się informację.

Ach, potrzebne jest else, ale uwaga - kolejność sprawdzania warunków jest istotna.
Gdyby ktoś nie przywiązywał do niej istotnej wagi, mógłby zapisać:

     if (a >= 10) System.out.println("Mała liczba");
     else if (a >= 100) System.out.println("Średnia liczba");
          else  if (a >= 1000) System.out.println("Duża liczba");

co znowu daje całkiem niepoprawny wynik dla a = 1000: napis "Mała liczba"
Dopiero zastosowanie else przy właściwej kolejności warunków da (przy a = 1000) właściwy wynik "Duża liczba".

     if (a >= 1000) System.out.println("Duża liczba");
     else if (a >= 100) System.out.println("Średnia liczba");
          else if (a >= 10) System.out.println("Mała liczba");

Zauważmy jednak, że temu fragmentowi brakuje "zupełności": co się stanie jeśli a równa się np. 1? Nie dostaniemy żadnej informacji! Potrzebne jest zatem jeszcze jedno ("zamykające") else, które będzie obsługiwać wszystkie nie uwzględnione warunki.
Na przykład:

     if (a >= 1000) System.out.println("Duża liczba");
     else if (a >= 100) System.out.println("Średnia liczba");
          else if (a >= 10) System.out.println("Mała liczba");
               else System.out.println("Liczba mikra, bo mniejsza od 10")

Czasem nie stosowanie else przy wykluczających się warunkach nie prowadzi do błędów w programie, ale - na pewno nalezy do złego stylu i powoduje niepotrzebne sprawdzanie warunków, o których już wiadomo, że są fałszywe.

Na przykład jeśli tex jest typu String, to poniższy fragment programu:

if (txt.equals("Ala")) a = 1;
if (txt.equals("kot")) a = 2;
if (txt.equals("koń")) a = 3;

wykona się bezbłędnie i da prawidlowe wyniki, ale jeśli txt jest "Ala", to niepotrzebnie sprawdzane są pozostałe warunki ("kot" i "koń").

Rozważmy teraz praktyczny przykład zastosowania instrukcji if oraz if-else.
Spróbujemy zbudowac prostą klasę symulującą działanie bankomatu.
Bankomat:

  • pyta o PIN
  • sprawdza czy podany PIN jest właściwy
  • jeśli tak - pyta o kwotę do wypłaty; jeśli nie - przerywa transakcję
  • sprawdza, czy kwota mieści się w limicie dziennym
  • jeśli tak - dokonuje wypłaty; jeśli nie - przerywa transakcję.

Z punktu widzenia użytkownika bankomatu ważne jest tylko to, że bankomat pyta o PIN a następnie o kwotę do wypłaty i (ewentualnie) ją wypłaca.
Zatem klasa Bankomat pwoinna dostarczyć jej użytkownikom dwóch odpowiadających tym działaniom metod nazwijmy je: askPin (zapytaj o pin) i askAmmountAndWithdraw (zapytaj o kwotę i wypłać).

Wszelkie sprawdzenia poprawności wprowadzonych danych (oraz akceptacja kwoty do wypłaty) powinny się odbywać wewnętrz klasy Bankomat i nie obarczać użytkowników tej klasy. Ci powinni tylko móc użyć w/w metod i ew. móc sprawdzić, czy metody te zakończyły swoje działanie pomyślnie czy też nie (na skutek braku akceptacji PINu czy kwoty). Z tego sprawdzenia na razie nie będziemy korzystać, zatem program główny symulujący działanie bankomatu i korzystający z gotowej klasy Bankomat mógłby wyglądać tak:

class BankomatTest {

  public static void main(String[] args) {

    Bankomat b = new Bankomat();

    b.askPin();
    b.askAmmountAndWithdraw();

    System.exit(0);

  }
}

A jego działanie pokazuje następująca sekwencja dialogów (generowanych przez klasę Bankomat):

Rys

wprowadzanie numeru PIN:


Rys

wprowadzenie kwoty do wypłaty:


Rys

po jej zakceptowaniu - wypłata:


Gdyby numer PIN okazał się nieprawidłowy - to klasa Bankomat powinna o tym poinformować w wywołaniu metody askPin (i oczywiście nie dopuścić do pytania o kwotę), a gdyby żądana kwota przekraczała limit - również poinformować i nie dopuścić do wypłaty:

Rys Rys

Uwaga. Nie możemy teraz sprawdzać, czy podana kwota do wypłaty jest liczbą całkowitą, bo jeszcze nie omówiliśmy zgadnień związanych z wyjątkami. Uzupełnimy to później
 

Istotną rolę będzie w niej odgrywać sprawdzanie poprawności wprowadzanych danych, które zapiszemy właśnie za pomocą instrukcji if-else.


Zobaczmy teraz jak wygląda klasa Bankomat.

Oczywiście, bankomat - sprawdzając dane - komunikuje się z jakimś "kartowym" czy bankowym, centrum. W naszym programie będzie go (w uproszczony sposób) reprezentować klasa CardIdent (na razie potraktujmy ją jako daną, bo jej konstrukcja wykracza poza omówiony dotąd materiał).
Dla naszej klasy Bankomat ważne jest, by rozpoczynając działanie nawiązała łacznośc z klasą CardIdent, co symulujemy w konstruktorze klasy Bankomat poprzez wywołanie statycznej metody klasy CardIdent - init().

W metodzie askPin() - po pobraniu w dialogu wejściowym pin-u - bankomat zwraca się do klasy CardIdent o potwierdzenie numeru pin i o kwotę limitu związaną z daną kartą (skorzystamy tu ze statycznej metody CardInit.getLimit(), która zwraca aktualny limit lub -1, gdy numer pin jest wadliwy):

limit = CardInit.getInit()

Jeżeli pin jest Ok, to zapisujemy dane: pin oraz bieżący limit w prywatnych polach klasy Bankomat (currentPin i currentLimit ). Zwróćmy uwagę: informacje te są niedostępne dla żadnej innej klasy oprócz Bankomatu. Metoda askPin() zwraca wartośc true, jeśli wszystko się powiodło i false - jeśli klient zrezygnowal z transakcji lub wprowadził wadliwy pin.

Przypomnijmy, że uzyskana w dialogu kwota do wypłaty jest napisem. Musimy go przekształcić na liczbę za pomocą metody Integer.parseInt()

Metoda askAmmountAndWithdraw pyta o kwotę do wyplaty - będzie ją przechowywac w zmiennej ammount (ale tylko w przypadku, gdy jest już ustalony pin; jeśli pin był wadliwy, to pole currentPin zawiera null i metoda konczy działanie z wynikem false - nie będzie wyplaty; to zapewniamy w pierwszym wierszu metody, pisząc if (currentPin == null) return false;).

Jeśli kwota do wypłaty przekracza limit - to do wypłaty też nie dochodzi i pojawia się odpowiedni komunikat. W przeciwnym razie - kwota jest wypłacana, do centrum kartowego posyłany jest komunikat o wypłaconej kwocie po to, by ew. zmniejszyć dzienny limit ( CardIdent.changeLimit(currentPin, ammount); ), a wartości pól currentPin i currentLimit są zerowane w przygotowaniu do następnej transakcji.

import javax.swing.*;

public class Bankomat {

  private static String CANCEL_MSG = "Zrezygnowano z transakcji. Do widzenia";
  private String currentPin;
  private int  currentLimit;

  public Bankomat() {
    CardIdent.init();
  }

  public boolean askPin() {
    String pin = ask("Wprowadz numer PIN:");
    boolean pinOk = false;
    int limit = 0;
    if (pin == null)  say(CANCEL_MSG);
    else {
      limit =  CardIdent.getLimit(pin);
      if (limit == -1) say("Wadliwy PIN");
      else  {
        pinOk = true;
        currentPin = pin;
        currentLimit = limit;
      }
    }
    return pinOk;
  }

  public boolean askAmmountAndWithdraw() {
    if (currentPin == null) return false;
    boolean withdrawAccepted = false;
    String request = ask("Podaj kwote do wyplaty:");
    if (request == null) say(CANCEL_MSG);
    else {
      int ammount = Integer.parseInt(request);
      if (ammount > currentLimit) say("Limit przekroczony.");
      else {
        CardIdent.changeLimit(currentPin, ammount);
        say("Wyplacam kwote : " + ammount + " zl");
        withdrawAccepted = true;
        }
    }
    currentPin = null;
    return withdrawAccepted;
  }

  private String ask(String txt) {
    return JOptionPane.showInputDialog(txt);
  }

  private void say(String txt) {
    JOptionPane.showMessageDialog(null, txt);
  }

Zwróćmy też uwagę na dwa elementy:

  • dla ułatwienia sobie życia w klasie Bankomat zawarliśmy dwie metody dialogowe - ask i say; uczyniliśmy je jednak prywatnymi, bowiem mają znaczenie tylko dla klasy Bankomat i nie powinny być udostępnione użytkownikom tej klasy
  • komunikat o rezygnacji z transakcji (który pojawia się gdy użytkownika zamyka okno dialogowe np. pzrycsikiem Cancel) uczyniliśmy stałą statyczną w klasie; stałą - ponieważ jest on wykorzystywany w dwóch miejscach (osczędność pisania i łatwość zmiany) i ponieważ nie ma sensu; statyczną - ponieważ nie ma sensu by był on zawarty w każdym obiekcie klasy Bankomat.

W katalogu samples\bankomat9 znajdują się wszystkie omawiane klasy.
Informacje niezbędne do testowania programu:

Poprawne PINy
Limit
1234
200
2345
300
3456
400
4567
500
5678
500
6789
500


« poprzedni punkt  następny punkt »