Decyzje i wyjątki


1. Przegląd instrukcji sterujących

W anglojęzycznej literaturze informatycznej pojęcie instrukcji określa się slowem statement
Zanim zaczniemy omawiać instrukcje sterujące, warto wyraźnie powiedzieć co w ogóle znaczy pojęcie instrukcja, tym bardziej, że w większości instrukcji sterujących występują inne instrukcje "do wykonania".

Np. z opisu instrukcji if :

if (war) ins;

dowiemy się, że zostanie wykonana instrukcja ins, jeśli warunek war jest prawdziwy.
Czym zatem może być ins ?


W różnych językach programowania jako instrukcje traktowane są różne konstrukcje gramatyczne - różne składniowo i semantycznie zapisy w programie
Dotąd używaliśmy tego terminu nieco intuicyjnie (traktując instrukcje jako "coś", co w trakcie działania programu "jest wykonywane"). Instrukcje stanowią "motor" działania programu. Ich wykonanie powoduje odpowiednie przetwarzanie danych.
Zamiast kusić się o bardziej precyzyjną definicję powiemy raczej co w Javie jest instrukcją, a co nie.

W Javie instrukcjami są:

Rodzaj instrukcji
Instrukcja
Przykład
Instrukcja pusta
pojedynczy średnik
;
Instrukcje wyrażeniowa
uwaga: instrukcją
wyrażeniową jest jedno
z wymienionych obok wyrażeń
zakończone średnikiem






przypisanie;
a=b;
preinkrementacja;
++a;
postinkremetacja;
a++;
predekrementacja;
--a;
postdekrementacja;
a--;
wywołanie metody;
x.met();
met();
wyrażenie new;
new Para();
Instrukcja grupująca
uwaga:
po zamykającym nawiasie nie stawiamy średnika
dowolne instrukcje (i deklaracje zmiennych) ujęte w nawiasy klamrowe
{
  a=b;
  c=b;
}
Instrukcja etykietowana
etykieta: dowolna instrukcja

Konkretne
formy składniowe
i przykłady
poznamy zaraz.







Instrukcje sterujące







if
if .. else
switch
while
do ... while
for
break
continue
return

Instrukcja throw
Zob. punkt na temat wyjątków
Instrukcja synchronized
Wsparcie dla programowania współbieżnego 


Żadna inna konstrukcja składniowa nie jest w Javie instrukcją.


Szczególnym rodzajem instrukcji są instrukcje sterujące.

Instrukcje sterujące odgrywają w programowaniu bardzo istotną rolę, bowiem pozwalają na zmianę sekwencji (kolejności) wykonania instrukcji programu.


Poniżej przedstawiono szybki przegląd instrukcji sterujących. Dalej omówimy je dokładnie.



Instrukcje sterujące

if (wyr) ins

Jeżeli wyrażenie wyr daje w wyniku true, to wykonywana jest instrukcja ins

if (wyr) ins1
else ins2

Jeżeli wyrażenie wyr daje w wyniku true, to wykonywana jest instrukcja ins1, w przeciwnym razie wykonywana jest instrukcja ins2

switch (wyr) {
  case ws1 : ins1 [ break;]
  case ws2 : ins2 [ break;]
  ....
  case wsN : insN [ break;]
  default : insDef [ break;]
}

Instrukcja przeznaczona do wyboru z wielu wariantów.

Wyrażenie wyr musi być wyrażeniem całkowitym lub wyliczeniowym. Jego wynik jest porównywany po kolei z  wyrażeniami stałymi (np. literałami lub stałymi), w przypadku zgodności wykonywana jest odpowiednia instrukcja po dwukropku. Jeśli żadne ze stałych wyrażeń nie pasuje do wyr wykonywana jest instrukcja w klauzuli default.

while (wyr) ins

Instrukcja ins wykonywana jest w pętli dopóki wartość wyrażenia wyr jest true

do ins while (wyr);

Wykonywana jest instrukcja ins, następnie wyliczane wyrażenie wyr; jeśli daje wartość true - cały proces zaczyna się od nowa.

1 forma
for
(init; wyr; upd) ins

2 forma
for (Typ zm : zestaw) ins

Dokonywana jest inicjacja init (może to być deklaracja zmiennej lub opracowanie listy wyrażeń, rozdzielonych przecinkami ); następnie w pętli wykonywane są następujące czynności: wyliczane jest wyrażenie wyr i jeśli jego wartość jest true wykonywana jest instrukcja ins, po czym wykonywana jest modyfikacja mod (opracowanie wyrażenia lub listy wyrażeń rozdzielonych przecinkami).
Elementy init, wyr, upd są opcjonalne.

2 forma - "for-each" - wykonuje instrukcję ins dla każdego elementu zestawu zestaw kolejno przypisanego na zmienną zm. Zastosowanie poznamy w części o tablicach i kolekcjach.

continue [ etykieta ];

Instrukcja ta przerywa wykonanie bieżącego kroku pętli for, while lub do.. while i rozpoczyna wykonanie kroku następnego, lub też przekazuje sterowanie do instrukcji opatrzonej etykietą (jesli etykieta występuje)

break [ etykieta ];

Instrukcja powoduje przerwanie wykonania pętli, oprócz tego stosowana jest w ramach instrukcji switch

return wyr;

Instrukcja powoduje powrót z metody, zwracając wartość wynikającą z wyliczenia wyrażenia wyr; wyrażenie wyr może być opuszczone jeśli metoda nie zwraca żadnych wartości


Instrukcje (i deklaracje) mogą być grupowane w bloki poprzez ujęcie  w nawiasy klamrowe { i }. Jest to tak zwana instrukcja grupująca. W każdym miejscu programu, gdzie może wystąpić pojedyncza instrukcja, może również wystąpić blok - instrukcja grupująca (np. we wszystkich miejscach instrukcji sterujących, gdzie przewiduje się użycie instrukcji). Jest ona wtedy traktowany jak pojedyncza instrukcja. Przykładowo możemy grupować instrukcje, które mają być wszystkie wykonane w przypadku wystąpienia jakiegoś warunku:

if (a > b) {
    c = a + b;
    d = a - b;
    e = a*b;
}

Jak widać, po nawiasie klamrowym, zamykającym grupowanie instrukcji nie potrzebny jest średnik.

2. Operatory i wyrażenia porównania

W wielu instrukcjach sterujących będą występować warunki podane jako wyrażenie relacyjne, konstruowane z zastosowaniem operatorów relacyjnych oraz operatorów równości - nierówności.
Np. w nawiasach instrukcji if występuje warunek, który może być wyrażeniem relacyjnym:

if (a > b) ...

Po części już omawialiśmy te operatory. Warto jednak usystematyzować podane wcześniej informacje oraz uzupelnić je o nowe fakty.

Operatory relacyjne (<, <=, >, >=):
Operatory relacyjne
Wyrażenie
(a i b - dowolne wyrażenia,
ale koniecznie
typu numerycznego)
Wynik
a > b
true jeśli wartość a jest większa od b
false w przeciwnym razie
a >= b
true jeśli wartość a jest większa lub równa b
false w przeciwnym razie
a < b
true jeśli wartość a jest mniejsza od b
false w przeciwnym razie
a <= b
true jeśli wartość a jest mniejsza lub równa b
false w przeciwnym razie


Przykłady:

int a =1, b = 3;
if (a > b) System.out.println("Większe");
else System.out.println("Mniejsze");
Wyprowadzi napis:
Mniejsze
int a =1, b = 3;
if (a + 1 > b/3) System.out.println("Większe");
else System.out.println("Mniejsze");

uwaga: priorytet operatorów relacyjnych jest niższy od priorytetu operatorów arytmetycznych - dzięki temu nie musimy pisać:
if ( (a + 1) > (b/3) ) ...
Wyprowadzi napis:
Większe
int a = 1, b = 3;
boolean wynik1 = a <= b;
boolean wynik2 = 1 >= a;
boolean wynik3 = a < 1;

uwaga: priorytet operatorów relacyjnych jest wyższy od priorytetu operatora przypisania - dzięki temu nie musimy pisać:
boolean wynik = (a <= b);
Zmienne będą miały następujące wartości:
wynik1 == true
wynik2 == true
wynik3 == false

 
Częstym błędem jest stosowanie zamiast operatora równości == znaku =. Należy się go wystrzegać, choć zwykle kompilator wykryje ten błąd i zwróci nam na niego uwagę.
Od grupy operatorów relacyjnych odróżnia się dwuargumentowe operatory równości (==) i nierówności (!=) - z dwóch powodów:
Konstruowane za pomocą operatorów równości-nierówności wyrażenia mają zawsze wynik typu boolean.


Operatory równości - nierówności
Wyrażenia
a == b
a != b
a i b dowolnego typu numerycznego
true jesli wartość a jest równa wartości b
false w przeciwnym razie
true jeśl i wartość a nie jest równa wartości b
false w przeciwnym razie
a i b typu boolean
true jeśli a i b oba mają wartość true lub wartość false
false w przeciwnym razie
true jeśli a jest true, b false lub odwrotnie
false, jeśli a i b są oba true lub oba false
a i b typu referencyjnego
true jeśli obie referencje wskazują ten sam obiekt lub jeśli obie mają wartość null
false jeśli referencje odnoszą się do różnych obiektów, lub jedna z nich ma wartość null, a druga - nie
true jeśli obie referencje odnoszą się do innych obiektów lub jeśli jedna z nich jest null, a druga nie
false jeśli obie referencje wskazują ten sam obiekt lub jeśli obie mają wartość null


Przykład:

int a = 2, b = a + 1;
if ( a == b) System.out.println("tak");
else System.out.println("nie");

if ( a != --b) System.out.println("tak");   // 2
else System.out.println("nie");

int c = 4;
if ( a < b + 1 == b  < c) System.out.println("tak");  //3
else System.out.println("tak");

nie
nie
tak
Ten fragment kodu wypisze na konsoli dane podane obok.
Uwagi:
  1. przedrostkowa forma operatora -- zapewnia, że wartość b w przykładzie 2 będzie zmieniona przed użyciem w wyrażeniu porownania na nierówność;
  2. w przykladzie 3 korzystamy z tego, że priorytet operatora == jest niższy od priorytetu operatorów relacyjnych, a te z kolei są niższe od priorytetów operatorów arytmetycznych (+); na równość porównywane są zatem dwie wartości typu boolean, a ponieważ obie są true, to wynik porównania też jest true

Jednak - w zasadzie - złożonych wyrażeń nie należy zapisywać w taki sposób jak w przykładzie oznaczonym // 3 . Raczej warto użyć nawiasów, które zwiększą czytelność kodu.
Zresztą porównywanie wartości boolowskich jest mało użyteczne, bo - w większości przypadków - zamiast niego należy stosować logiczne operatory koniunkcji czy alternatywy.

Przykład porównywania referencji:

// Klasa Para - to znana nam już klasa par liczb całkowitych
Para p1 = new Para(1,1);
Para p2 = new Para(1,1);
Para p3 = p1;

p1 == p2 // false, mimo że skladniki par są takie same
p1 == p3 // true, bo wskazują na ten sam obiekt

String s1 = "1,1";
p1 == s1 // bląd w kompilacji, bo p1 jest typu Para, a s1 typu String

String s2 = "1," + 1;
s1 == s2       // false, bo są to referencje do różnych obiektów
s1 == null      // false, bo s1 wskazuje na jakiś obiekt
s1.equals(s2) // true, bo zawartość łańcuchów znakowych jest taka sama.


Przypomnienie: porównywanie zawartości obiektów (na równość - nierówność) odbywa się zawsze za pomocą metody equals, zdefiniowanej w klasie obiektów.


Na koniec omawiania porównań warto zwrócić uwagę na liczby rzeczywiste. Otóż wszystkie porównania są zawsze dokładne. Ale reprezentacja liczb rzeczywistych w komputerze nie jest dokładna . Dlatego przy porównaniu liczb rzeczywistych powinniśmy sprawdzać nie tyle czy są one dokładnie równe, ale czy ich wartości są dostatecznie sobie bliskie.

3. Operatory i wyrażenia logiczne

Do zapisywania złożonych warunków służą warunkowe operatory logiczne:
Przypuśćmy, że chcemy sprawdzić czy zachodzi warunek: wielkość n znajduje się w przedziale [1, 10] - to znaczy, czy n jest większe lub równe 1 i jednocześnie mniejsze lub równe 10.
Jak już wiemy, zapis: if (1 <= n <= 10) będzie błędny (bo operatory relacyjne są stosowane tylko wobec typów numerycznych, a wyrażenie 1 <= n ma wynik typu boolean).
Nie znając operatorów logicznych i wiedząc, że w instrukcji if możemy umieścić inną instrukcję if, warunek moglibyśmy sprawdzić za pomocą takiego zapisu:

if (n >= 1)
    if (n <= 10) ... ;

Nie jest to jednak ani elegancka, ani zbyt czytelna forma.
Na szczęście mamy operator koniunkcji logicznej,
Chodzi nam o to czy równocześnie spelnione są dwa warunki

n >= 1 I n <= 10.

W Javie zapisujemy to jako:

n >= 1 && n <= 10

i stosujemy np. w instrukcji if:

if (n >= 1 && n <= 10) ...;


Podobnie możemy chcieć sprawdzić czy wartość n znajduje się poza przedziałem [1, 10]. Aby tak było n musi być mniejsze od 1 lub większe od 10.
Do zapisu takiego warunku stosuje się operator logicznej alternatywy ||:

if (n < 1 || n > 10) ..

Trzeci operator logiczny - negacji - służy do zaprzeczania warunkom.
Na przykład, jeżeli chcemy podjąć jakieś działania, jeżeli łańcuch znakowy txt nie jest napisem "Ala", to piszemy:

if (!txt.equals("Ala")) ... ;

Argumentami operatorów logicznych mogą być tylko wyrażenia typu boolean, zaś rezultatem jest wartość typu boolean, która może być równa true lub false, zgodnie z następującą tablicą:

Warunkowe operatory logiczne
a
b
a && b
a || b
!a
true
true
true
true
false
false
true
false
true
true
true
false
false
true

false
false
false
false


Operator && daje wartość true tylko wtedy, gdy oba jego argumenty są wartościami true, a wartość false zawsze, gdy którykolwiek z argumentów jest false.
Operator || daje wartość true zawsze, gdy którykolwiek z jego argumentów jest true i wartość false tylko wtedy, gdy oba argumenty są false

Operatory logiczne mają niższy priorytet od operatorów relacyjnych i równości-nierówności, zatem bez użycia dodatkowych nawiasów możemy pisać np:

if ( a == 1 && b > 3) c = a + b;

uzyskując efekt, o który nam chodzi (tu: wyliczenie c, gdy a równe 1 i b > 3).


Uwaga: operator negacji logicznej ma wyższy priorytet niż inne operatory logiczne, wobec tego należy używać nawiasów dla zapewnienia właściwej kolejności obliczania wartości wyrażeń


Sprawne posługiwanie się warunkami logicznymi wymaga podstawowej wiedzy z zakresu logiki matematycznej. Warto więc sięgnąć po jakiś prosty, początkowy opis rachunku zdań logicznych i przeczytać z niego kilka pierwszych stron.
Nota bene, zadziwiająco często mylone są w programowaniu operacje koniunkcji i alternatywy logicznej. Zawsze więc - wybierając operację do zastosowania - zastanówmy się troszkę dłużej nad tym czy ma to być koniukcja czy alernatywa
Np. jeżeli chcemy wykonać jakieś czynności jeżeli nie zachodzi warunek: "łańcuch znakowy nie jest napisem "Ala" i - jednocześnie - zmienna lata ma wartość większą od 1", to nie wolno nam napisać:

if (! txt.equals("Ala") && lata > 1) ...

bo to oznaczałoby konieczność równoczesnego spełnienia dwóch warunków:
- napis txt nie jest ala,
- i lata są większe od 1
(a przecież nie o to nam chodziło)

Piszemy za to:

if (!( txt.equals("Ala") && lata > 1)) ...

Użycie dodatkowych nawiasów zmienia kolejność opracowania wyrażenia i uzyskujemy pożądany efekt

Omawiane dotąd operatory nazywają się warunkowymi operatorami logicznymi, bowiem
obliczanie wartości wyrażeń logicznych, konstruowanych za ich pomocą ma bardzo ważną właściwość: obliczane są one od lewej do prawej (wiązania operatorów logicznych - oprócz operatora negacji - są lewostronne), ale proces obliczeń kończy się już w momencie gdy tylko wiadomo jaki będzie rezultat całego wyrażenia.
Oznacza to, że wyrażenie nie musi być wcale opracowywane do końca; wystarczy, że jego część jednoznacznie wskazuje wynik. Na przykład w wyrażeniu:

a && (b > 0 || c == 1)

jeśli a jest równe false, to wyrażenie w nawiasach nie będzie obliczone, bowiem wynik całego wyrażenia nie zależy już od tego co jest w nawiasach (przy a = false zawsze będzie false).
Ma to ogromnie ważne konsekwencje, bowiem jeśli oprzemy logikę programu na efektach ubocznych generowanych przy obliczaniu wyrażeń, które - właśnie ze względu na tę właściwość operatorów logicznych - mogą w ogóle nie podlegać opracowaniu, to może nas spotkać bardzo przykra niespodzianka.

Zobaczmy to na przykładzie poniższego programu. Programista, który go napisał chciał osiągnąć następujący efekt: zapytać w dialogach wejściowych o nazwisko i imię użytkownika, jeśli podano obie informacje - wyprowadzić połączone nazwisko i imię, jeśli zaś zabrakło którejś z nich - wyprowadzić napis "Niepełna informacja"; następnie pokazać dokładnie co jest nazwiskiem (może być null) a co imieniem (też może być null, jeśli nie wprowadzone). Pokusa zwięzłego napisania kodu sprawiła, że nasz programista sięgnął po połączenie warunków za pomocą operatora &&: jeżeli wprowadzono nazwisko i wprowadzono imię to txt = nazwisko + imie.
import javax.swing.*;

public class EfUb {

  public static void main(String[] args) {

    String nazwisko;
    String imie = null;
    String txt;

    if ((nazwisko = JOptionPane.showInputDialog("Podaj nazwisko")) != null
         && (imie = JOptionPane.showInputDialog("Podaj imie")) != null
       )
       txt = nazwisko + " " + imie;
    else txt = "Niepelna informacja";
    System.out.println(txt);
    System.out.println("Imie :" + imie);
    System.out.println("Nazwisko :" + nazwisko);
    System.exit(0);
  }

}
Jednak raczej wbrew intencjom programisty w tym programie może nie dojść do zapytania o imię (bowiem wartość pierwszego składnika wyrażenia połączonego koniunkcją && może być false - jeśli przy pytaniu o nazwisko użytkownik anulował dialog).

W Javie są również bezwarunkowe operatory logiczne (zwane czasem po prostu operatorami logicznymi):
Bezwarunkowe operatory logicznej koniunkcji (&) i alternatywy ( |) mają te same właściwości co operatory && i || z jednym wyjątkiem: oba wyrażenia-argumenty operatorów są zawsze opracowywane (dlatego mówimy, że operatory są bezwarunkowe).

Dodatkowo, wśród logicznych operatorów bezwarunkowych, zdefiniowano operator "wykluczające albo" (znak ^), który daje wynik true gdy wartości obu argumentów operatora są różne (czyli true i false lub false i true) oraz false w każdym innym przypadku.


Operatory logiczne (bezwarunkowe) są stosowane wyłącznie wobec argumentów typu boolean.
Niestety, te same symbole operatorów ( &, |, ^, ~) sa również używane w operatorach bitowych, stosowanych wobec liczb całkowitych (i umożliwiających operowanie na bitach liczb całkowitych), co powoduje trochę zamieszania.
Problemem są także błędy: np. często początkujący (i nie tylko) programiści zamiast && piszą &, a zamiast || - |. Kompilator nie wykryje takich błędów, a efekt opracowywania takich wyrażeń logicznych może być inny od spodziewanego przez programistę.

Generalnie więc:
nie należy mieszać w wyrażeniach użycia operatorów warunkowych i bezwarunkowych (ze względu na inny sposób dzialania oraz inne priorytety (por. tablicę priorytetów operatorów)).

Dobrą regułą "na początek" jest stosowanie wyłącznie operatorów warunkowych.




4. Podejmowanie decyzji: instrukcje if oraz if-else

Znana nam już instrukcja if ma postać:



if (wyr) ins

gdzie:
Działanie: instrukcja ins jest wykonywana wtedy i tylko wtedy, gdy wartością wyrażenia war jest true



Instrukcja if -else rozszerza dzialanie instrukcji if. Ma ona postać.



if (war) ins1
else  ins2

gdzie:
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 opisie składni 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ą konstrukcje można i należy oczywiście oprogramować 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"); // niezaleznie 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ęstymi błędami przy sprawdzaniu wielu warunków (które mogą się na siebie "nakładać" lub wykluczać) są
Błędy te są szczególnie niebezpieczne, gdyż dotyczą logiki programu i nie mogą być wykryte przez kompilator


Np. jeśli ktoś chciałby powiedzieć coś o liczbie a (czy jest duża, średnia, mala) 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")
co jest oczywistym błędem, bo jesli a = 1000, ten fragment wyprowadzi na konsolę wzajemnie wykluczającą się informację:
Duża liczba
Średnia liczba
Mała liczba

Ach, potrzebne jest else, ale uwaga - kolejność sprawdzania warunków jest ważna.
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 jesli 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 należy do złego stylu i powoduje niepotrzebne sprawdzanie warunków, o których już wiadomo, że są fałszywe.

Na przykład jeśli txt 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ń").
Należało napisać:

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


Istnieją przypadki, gdy niestosowanie else przy sprawdzaniu sekwencji wykluczających się warunków jest całkowicie zasadne, a nawet należy do dobrego stylu programowania. Jest tak zawsze wtedy, gdy przy prawdziwości któregoś z warunków, kolejny warunek w sekwencji nie będzie już sprawdzany.
Np.

int podajKod() {
  if (txt.equals("Ala")) return 1;
  if (txt.equals("kot")) return 2;
  if (txt.equals("koń")) return 3;
  return -1;
}


Praktycznych przykładów zastosowania instrukcji if oraz if-else dostarcza niemal każdy program, nie będziemy więc tu się nad tym dalej rozwodzić.


5. Wielowariantowe wybory za pomocą instrukcji switch

Zamiast wielokrotnych if - else if w niektórych przypadkach można zastosować instrukcję wyboru wielowariantowego switch. Ma ona następującą ogólną postać


switch (wyr) blok_switch

gdzie:


Wyrażenie w nawiasach switch jest wyliczane, a jego wartość porównywana z wartościami wyrażeń stałych (zawartych w częściach oznaczanych przez etykiety case). Sterowanie jest przekazywane do tej instrukcji, którą poprzedza etykieta case z wyrażeniem stałym równym co do wartości wyrażeniu w nawiasach switch.
Od tego miejsca wykonanie programu przebiega dalej sekwencyjnie (po to by nie wykonywały się inne instrukcje oznaczone przez inne etykiety case, następujące po wybranej w rezultacie porównania, trzeba zastosować instrukcję break lub return).
Jeśli nie znajdzie się żadna etykieta "pasująca" do wartości wyrażenie w nawiasach switch, to sterowanie jest przekazywane do części oznaczanej przez etykietę default, a jeśli jej nie ma, to - do instrukcji następującej w programie po switch.

Rozważmy przykład klasy, która udostępnia metodę makeOp wykonania operacji arytmetycznych na elementach swoich obiektów - dwóch liczbach rzeczywistych.
public class SimpleCalc {

  private double a;
  private double b;

  public SimpleCalc(double x, double y) {
    a = x;
    b = y;
  }

  public double makeOp(char op) {

    double r = 0;

    switch(op) {
       case '+' : r = a + b; break;
       case '-' : r = a - b; break;
       case '*' : r = a * b; break;
       case '/' : r = a / b; break;
       default  : System.out.println("Nieznany kod operacji");
    }

    return r;
  }

}

class SimplecalcTest {

  public static void main(String[] args) {

    SimpleCalc sc = new SimpleCalc(1.2, 3.7);
    System.out.println( sc.makeOp('+'));
    System.out.println( sc.makeOp('-'));
    System.out.println( sc.makeOp('*'));
    System.out.println( sc.makeOp('/'));

  }
}
W metodzie makeOp wartość zmiennej op (typu char) steruje obliczeniami (np. jeśli zawiera znak '+' to sterowanie jest przekazywane do miejsca oznaczonego etykietą case '+', wartość r jest wyliczana jako suma a i b, a instrukcja break pozwala opuścić dalsze instrukcje zawarte w bloku switch).

Taki sam program, stosując typ wyliczeniowy, można zapisać tak:

public class SimpleCalc {
  
  private static enum Op { PLUS, MINUS, MULT, DIV };

  private double a;
  private double b;

  public SimpleCalc(double x, double y) {
    a = x;
    b = y;
  }

  public double makeOp(Op op) {

    double r = 0;

    switch(op) {
       case PLUS : r = a + b; break;
       case MINUS : r = a - b; break;
       case MULT : r = a * b; break;
       case DIV : r = a / b; break;
       default  : System.out.println("Nieznany kod operacji");
    }

    return r;
  }
  
  public static void main(String[] args) {

    SimpleCalc sc = new SimpleCalc(1.2, 3.7);
    System.out.println( sc.makeOp(Op.PLUS));
    System.out.println( sc.makeOp(Op.MINUS));
    System.out.println( sc.makeOp(Op.MULT));
    System.out.println( sc.makeOp(Op.DIV));
  }

}

Warto szczególnie podkreślić, że etykiety case są tworzone przez wyrażenia stałe. Wyrażenie stałe to takie, którego wartość jest znana w fazie kompilacji i nie może być zmieniona w fazie wykonania programu. Będzie to np. literał (taki jak '+'. '*' czy 1), nazwa stałej (zadeklarowanej jako final), wyrażenie składające się z literałów i nazw stałych połączonych operatorami języka (np. LMAX + 3/LMIN, gdzie LMAX i LMIN - nazwy stałych) lub wartość stałej wyliczenia (z bloku enum). Wartość wyrażenia stałego musi być typu całkowitego i dać się przekształcić do typu wyrażenia w nawiasach switch, lub też typu wyliczeniowego i tego samego co typ wyrażenia w nawiasach switch.


Nie należy tez zapominać o umieszczeniu instrukcji we właściwym momencie przerywających sekwencje operacji zawartych w bloku switch.
Np. poniższa sekwencja:

double a = 0.1;
switch(n) {
    case 1 : a = a + 1;
    case 2 : a = a + 2;
    case 3 : a = a + 3;
}
System.out.prinln(a);

wypisze: 6.1 (dla n = 1), 5.1 (dla n = 2) i 3.1 (dla n = 3), zaś dla innych wartości n inicjalną wartość zmiennej a (0.1). Jeśli chcemy uzyskać 1.1 dla n =1, 2.1 dla n=2 i 3.1 dla n=3 to po każdym wyliczeniu a należy wstawić instrukcję break, przerywającą sekwencyjne wykonywanie instrukcji zawartych w bloku i przekazującą sterowanie do pierwszej instrukcji poza switch.

Etykiety case i default mogą występować w dowolnej kolejności, ale w ramach jednego switch nie może być dwóch takich samych etykiet (nie może być dwóch wyrażeń stałych, które dają w rezultacie tę samą wartość np. 3 i 1 + 2 lub dwóch etykiet default).

Co jednak, jeśli instrukcje switch są zagnieżdżone ? Okazuje się, że etykiety case i default są kojarzone zawsze ze "swoim" switch, możemy więc pisać np. tak:

import javax.swing.*;

public class Test {

  public static void main(String[] args) {
    int i = Integer.parseInt(JOptionPane.showInputDialog("Liczba"));
    switch(i)    {
      case 1 : switch(testNum(i)) {
                    case 1 : System.out.println("same one"); break;
                    case 2 : System.out.println("double one"); break;
                    default: System.out.println("other one"); break;
                }
                break;
      case 2 :  System.out.println("two"); break;
      default:  System.out.println("other two"); break;
    }
    System.out.println("Koniec");
  }

 static int testNum(int n) {
   return n*2;
 }

}
(w tym fragmencie np. wprowadzenie 1 da na wyjściu napis "double one", zaś 2 - napis "two")

Zwykła instrukcja break przerywa wykonanie tego bloku switch, w którym jest umieszczona.
Etykieta w programie jest identyfikatorem z następującym po nim znakiem :. Nie nalezy mylić ogólnego pojęcia etykiety ze specjalnym przypadkiem "etykiety" case
Jeśli natomiast w instrukcji break podamy etykietę, to możemy przerwać  blok oznaczony tą etykietą, nawet jeśli break znajduje się w bloku zagnieżdżonym.  W ten sposób za pomocą break możemy wychodzić z głęboko zagnieżdżonych bloków switch.
Np.  gdyby w poprzednim przykładzie pominąć break po pierwszym case 1:
    switch(i)    {
      case 1 : switch(testNum(i)) {
                 case 1 : System.out.println("same one"); break;
                 case 2 : System.out.println("double one"); break;
                 default: System.out.println("other one"); break;
               }
              // pominięte break
      case 2 :  System.out.println("two"); break;
      default:  System.out.println("other two"); break;
    }
    System.out.println("Koniec");


to po wprowadzeniu 1 uzyskalibyśmy wynik:
double one
two
Koniec

Natomiast wprowadzenie etykiety i użycie jej w instrukcji break w następujący sposób:
    outerSwitch:
    switch(i)    {
      case 1 : switch(testNum(i)) {
                 case 1 : System.out.println("same one"); break;
                 case 2 : System.out.println("double one");
                          break outerSwitch;
                 default: System.out.println("other one"); break;
               }

      case 2 :  System.out.println("two"); break;
      default:  System.out.println("other two"); break;
    }
    System.out.println("Koniec");
umożliwi "wyjście" z zewnętrznego bloku switch za pomocą break użytego w bloku wewnętrznym i da wynik (przy 1 na wejściu):
double one
Koniec

Uwaga: stosowanie etykiet w instrukcji break dla wychodzenia z zagnieżdżonych bloków dotyczy rózwnież instrukcji sterujących for, while i do...while. (o czym w następnym rozdziale). Stosowanie etykiet podlega pewnym ograniczeniom - zob. opis języka na stronie sun.java,com.

6. Operator warunkowy ?:

Operator warunkowy ?: ma trzy argumenty - wyrażenia i stosowany jest do konstrukcji wyrażenia warunkowego w następujący sposób:

e1 ? e2 : e3

Typ wyrażenia e1 musi być boolean. Typy pozostałych wyrażeń (e2 i e3) muszą być takie same lub dopuszczające przekształcenie do tego samego typu.

Zastosowania operatora ?: tworzy wyrażenie warunkowe.
Wyrażenie warunkowe obliczane jest w następujący sposób. Obliczane jest wyrażenie e1. Jeśli jego wartością jest true to obliczane jest wyrażenie e2 i jego wartość staje się wartością całego wyrażenia warunkowego, natomiast wyrażenie e3 jest ignorowane. W przeciwnym razie ignorowane jest wyrażenie e2, a wartością wyrażenia warunkowgo staje się - po obliczeniu - wartość wyrażenia e3.

Priorytet operatora ?: jest niski (wyższy tylko od priorytetów operatorów przypisań i połączenia), wiązanie zaś - prawostronne. Niski priorytet operatora warunku pozwala unikać nawiasów, choć często warto je zastosować dla większej czytelności programu.

Wyrażenia warunkowe mogą zastępować proste instrukcje if .. else np.

  W kontekście:
      int a = 1, d;
      double b = 0.7, c;
      .....
      int func1() {...}
      int func2(int) { ...}
      void func4() { ... }

Instrukcja if
Odpowiednik - wyrażenie warunkowe
if (a > b) c = a + 2;
else c = b;
c = a > b ? a + 2 : b;
if (a < b) c = a + 2;
else d = b + 2;
a < b ? c = a + 2 : d = b + 2;
if (b>0) d= func2(a) + 2;
else b *= 3.3;
b > 0 ? d = func2(a) + 2 : b *= 3.3;
if (b > 0) d = func1();
else a++;
b > 0 ? d = func1() : a++;
if (b > 0) func4();
else d = a + 7;
Nie da się tego zapisać w postaci wyrażenia warunkowego. gdyż operand e2 miałby typ void, a operand e3 int.


Jak widać zastępowanie instrukcji if operatorem warunkowym nie zawsze jest możliwe.

Jednak nie na zastępowaniu instrukcji if polega wartość operatora warunkowego.

Podstawową jego zaletą jest to, że może on być stosowany w wyrażeniach i w takich sytuacjach, w których if nie może być zastosowane, lub jego zastosowanie wymaga - w sumie - trochę sztucznych zabiegów.

Dotyczy to np.  inicjacji zmiennnych np.:

            int x = (a > b ? 1 : 0);

7. Wyjątki

7.1. Obsługa wyjątków

Swoistym rodzajem podejmowania decyzji w programie jest obsługa i zgłaszanie wyjatków.

Wyjątek - to sygnał o błędzie w trakcie wykonania programu

Wyjątek powstaje na skutek jakiegoś nieoczekiwanego błędu.
Wyjątek jest zgłaszany (lub mówiąc inaczej - sygnalizowany).
Wyjątek jest (może lub musi być) obsługiwany.
Prosty schemat obslugi wyjątków

    try {
        // ... w bloku try ujmujemy instrukcje, które mogą spowodować wyjątek
    } catch(TypWyjątku exc)  {
        // ... w klauzuli catch umieszczamy obsługę wyjątku
    }
     
Gdy w wyniku wykonania instrukcji w bloku try powstanie wyjątek typu TypWyjatku  to sterowanie zostanie przekazane do kodu umieszczonego w w/w klauzuli catch


Przykłady.

a) Brak jawnej obsługi wyjątku - powstały błąd (wyjątek) powoduje zakończenie programu, a JVM wypisuje komunikat o jego przyczynie.
public class NoCatch {

  public static void main(String[] args) {
    int a = 1, b = 0, c = 0;
    c = a/b;
    System.out.println(c);
  }

}

Exception in thread "main" java.lang.ArithmeticException: / by zero
        at NoCatch.main(NoCatch.java:6)


b) Zabezpieczamy się przed możliwymi skutkami całkowitoliczbowego dzielenia przez zero, obsługując wyjątek ArithmeticException

public class Catch1 {

  public static void main(String[] args) {
    int a = 1, b = 0, c = 0;
    String wynik;
    try {
      c = a/b;
      wynik = "" + c;
    } catch (ArithmeticException exc) {
        wynik = "***";
    }
    System.out.println(wynik);
  }

}
W tym przypadku, wykonanie instrukcji c = a/b; spowoduje powstanie wyjątku (dzielenie przez zero), a ponieważ instrukcja ta znajduje się w bloku try, do którego "podczepiona" jest klauzula catch z odpowiednim typem wyjątku, to sterowanie zostanie przekazane do kodu w catch, zmienna wynik uzyska wartość "***", i wynik ten zostanie wyprowadzony na konsolę. Gdyby zmienna b nie miała wartości zero, wyjątek by nie powstał, kod w klauzuli catch nie został by wykonany i na konsolę wyprowadzony by został wynik dzielenia a/b.

Mechanizm obsługi wyjątków może być wykorzystywany w bardzo różny i elastyczny sposób.
Typowym przykładem jest weryfikacja wprowadzanych przez użytkownika danych.
Wielokrotnie w dotąd omawianych przykładowych programach żądaliśmy od użytkownika wprowadzania liczb całkowitych, a następnie za pomocą metody parseInt przekształcaliśmy ich znakową reprezentację na binarną. Jak wiemy, jeśli przy tym wprowadzony napis nie reprezentuje liczby całkowitej, to powstaje wyjątek NumberFormatException. Powinniśmy go zawsze obsługiwać.
Pokazuje to poniższy program.
  public static void main(String[] args) {

    String s1 = JOptionPane.showInputDialog("Podaj pierwszą liczbę");
    if (s1 != null) {
      String s2 = JOptionPane.showInputDialog("Podaj drugą liczbę");
      if (s2 != null) {
        int n1;
        int n2;
        try {
          n1 = Integer.parseInt(s1);
          n2 = Integer.parseInt(s2);
        } catch (NumberFormatException exc) {
            System.out.println("Błędne dane - kończę działanie");
            return;
        }
        JOptionPane.showMessageDialog(null, "Suma: " + (n1 + n2));
      }
    }
  }
Zobacz demo programu.


Tutaj obsługa wyjątku polega na wypisaniu komunikatu i zakończeniu dzialania programu. Program jest niedoskonały, choćby dlatego, że nie pozwla na poprawienie błędów, tylko kończy dizałanie.

W przytoczonych przykładach na pewno dość tajemniczo wyglądają napisy NumberFormatException czy ArithmeticException.
I  co oznacza użycie w nawiasach klauzuli catch tych nazw z dodatkiem czegoś co wygląda jak zmienna np.

 catch (NumberFormatException exc) ...

Otóż wyjątki są obiektami klas wyjątków.
Zatem nazwy NumberFormatException, ArithmeticException itd. sa nazwami klas, a zmienna exc we wcześniejszych przykładach jest faktycznie zmienną - zawiera referencję do obiektu odpowiedniej klasy wyjątku.

Wobec takiej zmiennej możemy użyć rozlicznych metod, które dostarczą nam informacji o przyczynie powstania wyjątku. Oto niektóre z nich.
 StringgetMessage()
         Zwraca napis, zawierający informację o wyjątku (np. błędne dane lub indeks).
 voidprintStackTrace()
          Wypisuje na konsoli informacje o wyjątku oraz  sekwencje wywołań metod, która doprowadziła do powstania wyjątku (stos wywołań). Wersje tej metody pozwalają te informacje zapisywac do plików (logów)
 StringtoString()
          Zwraca  informację o wyjątku (zazwyczaj nazwę klasy wyjątku oraz dodatkową informację uzyskiwaną przez getMessage())
ThrowablegetCause()
          Zwraca wyjątek niższego poziomu, który spowodował powstanie tego wyjątku  albo null jeśli takiego wyjątku niższego poziomu nie było  lub nie jest zidentyfikowany (zastosowaniem tej metody nie będziemy się teraz zajmować - przydaje się ona np. przy obsłudze wyjątków SQL, o czym w przyszłym semestrze).

Zobaczmy na przykładzie jakie informacje możemy uzyskać o wyjątku:
import java.util.*;

class ReportExc {

  public ReportExc() {
    wykonaj();
  }

  public void wykonaj() {
    try {
      int num = Integer.parseInt("1aaa");
    } catch (NumberFormatException exc) {
        System.out.println("Co podaje getMessage()");
        System.out.println( exc.getMessage());
        System.out.println("Co podaje toString()");
        System.out.println(exc);
        System.out.println("Wydruk śladu stosu (kolejność wywołań metod)");
        exc.printStackTrace();
        System.exit(1);
    }
  }

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

}
Program ten wyprowadzi:
Co podaje getMessage()
For input string: "1aaa"
Co podaje toString()
java.lang.NumberFormatException: For input string: "1aaa"
Wydruk śladu stosu (kolejność wywołań metod)
java.lang.NumberFormatException: For input string: "1aaa"
    at java.lang.NumberFormatException.forInputString(Unknown Source)
    at java.lang.Integer.parseInt(Unknown Source)
    at java.lang.Integer.parseInt(Unknown Source)
    at ReportExc.wykonaj(ReportExc.java:11)
    at ReportExc.<init>(ReportExc.java:6)
    at ReportExc.main(ReportExc.java:24)


Warto zwrócić uwagę, że wydruk "śladu" stosu (stack trace) odtwarza całą sekwencję wywołań metod, ktora doprowadziła do powstania wyjątku. Czytamy od góry:  wyjątek został zgłoszony przez instrukcję w wierszu 435 pliku z definicją klasy Integer w metodzie parseInt() klasy java.lang.Integer, do którego to miejsca sterowanie przyszło z wiersza 476. Ta informacja dotyczy klas standardowych pakietów Javy, a nie naszego kodu, ale może okazać się czasem pomocna. Metoda parseInt została wywołana przez metodę wykonaj() z klasy ReportExc w wierszu 11  pliku źródłowego (tu po raz piewrszy widzimy nasz plik źródłowy na wydruku - zatem tu, w linii 11, tkwi przyczyna błędu). Jaki to błąd ? Błędny format danych. Jakich? 1aaa. To wszystko mamy podane.
Zauważmy jeszcze, że słowo <init> oznacza konstruktor, że metoda wykonaj() została wywołana z tego konstruktora (w wierszu 6) klasy ReportExc, a konstruktor był wywołany z metody main w wierszu 24  pliku źródłolwego.

Klasy wyjątków tworzą dość rozbudowaną  hierarchię dziedzczenia (zob. rysunek, na którym pokazano jej fragmenty).
r

Wszystkie klasy wyjątków pochodzą od klasy Throwable. Mamy następnie dwie wyróżnione klasy Error i Exception. Od klasy Exception pochodzi klasa RuntimeException oraz wiele innych.
Podstawowa różnica pomiędzy tymi klasami polega na tym, że wyjątki klas pochodnych od Exception. ale nie RuntimeException (klasy zaznaczone na rysunku na czerwono) są tzw. wyjątkami kontrolowanymi, co oznacza, że musimy zapewnić ich obsługę (kompilator zgłosi błąd, jeśli tego nie zrobimy). Pozostałych wyjątków obsługiwać nie musimy (ale możemy) - co już widzieliśmy na przykładzie NumberFormatException czy ArithmeticException.

Zapewnienie obsługi wyjątku oznacza:
Zobaczmy przykład. W poniższym programie chcemy wypisać jakiś tekst, wstrzymać wykonanie programu na 2 sekundy i następnie wypisać inny tekst. W celu uśpienia programu zastosujemy statyczną metodę sleep z  klasy Thread z argumentem, równym liczbie milisekund, którą program ma odczekać przed podjęciem dalszego wykonania.
public class ControlledExc {
  
  public void printWaitAndPrint() {
    System.out.println("Za 2 sekundy wypiszę następny tekst");
    Thread.sleep(2000);
    System.out.println("Następny tekst");
  }
  
  public static void main(String[] args) {
    new ControlledExc().printWaitAndPrint();
  }

}
Niestety, ten kod się nie skompiluje poprawnie, ponieważ metoda sleep może zgłosić kontrolowany wyjątek klasy InterruptedException (przerwanie stanu oczekiwania).
Rysunek pokazuje błąd kompilacji sygnalizowany w Eclipse.

r

Po kliknięciu w ikonkę błędu Eclipse zaproponuje dwa możliwe rozwiązania problemu:

r

Wybranie opcji "Surround .." spowoduje dopisanie bloku try-catch wokół wywołania metody sleep() (propozycja widoczna na żółtym tle).
Wybranie opcji "Add throws declaration" spowoduje natomiast dopisanie deklaracji throws InterruptedException do nagłówka metody  printWaitAndPrint() i oznacza, że przy powstaniu tego wyjątku zamiast jego obsługi  w kodzie metody  printWaitAndPrint() zostanie on przez nią zgłoszony. To z kolei przeniesie obowiązek obsługi  do miejsca, które wywołuje metodę printWaitAndPrint().

Deklaracja throws uzywana jest w nagłówku definicji metody (lub konstruktora) i ma postać:

throws TypWyj1, TypWyj2, ... , TypWyjN

Oznacza to, że dana metoda może zgłaszać wyjątki podanych typów.
Np.

public void metoda() throws InterruptedException {
  // ... ciało metody
  // ... może tu powstać wyjątek InterruptedException
}

Po wyborze opcji "Add throws declaration" metoda printWaitAndPrint będzie miała postać:

  public void printWaitAndPrint() throws InterruptedException {
    System.out.println("Za 2 sekundy wypiszę następny tekst");
    Thread.sleep(2000);
    System.out.println("Następny tekst");
  }

i kompilator nie będzie się czepiał jej kodu. Ale ponieważ metodę tę wywołujemy z main:

  public static void main(String[] args) {
    new ControlledExc().printWaitAndPrint();
  }
to problem przeniesie się w miejsce wywolania (wyjątki kontrolowane - tak czy inaczej, tu czy gdzie indziej muszą być obsługiwane):

r

Poprawny kod  będzie zatem wyglądał tak:
public class ControlledExc {
  
  public void printWaitAndPrint() throws InterruptedException {
    System.out.println("Za 2 sekundy wypiszę następny tekst");
    Thread.sleep(2000);
    System.out.println("Następny tekst");
  }
  
  public static void main(String[] args) {
    try {
      new ControlledExc().printWaitAndPrint();
    } catch (InterruptedException exc) {
        exc.printStackTrace();
    }
  }

}

Naturalnie, moglibyśmy też obsłuzyć ten wyjątek w samej metodzie printWaitAndPrint()  i wtedy niepotrzebna by była  deklaracja throws, ani też obsługa w main(..).

Z obsługą wyjatków wiążą się jeszcze dwie ważne kwestie.

Po pierwsze, w bloku try { ... } może powstać (być zgłoszonych) wiele wyjątków różnych typów. Dlatego możemy mieć wiele klauzul catch. A dodatkowo możemy (ale nie musimy) zapisać klauzulę finally, w której  zawrzemy jakiś kod, który ma się wykonać niezależnie od tego czy przy wykonaniu bloku try jakiś wyjątek powstał czy nie. Kod w klauzuli finally służy zwykle do porządkowania zasobów (np. zamykania połączeń bazodanowych czy sieciowych). Zatem ogólna postać bloku try { .. } jest następująca:
  try {
    // instrukcje wykonywane w bloku try
  } catch (TypWyj1 exc) {
    // obsluga wyjątku typu TypWyj1
  } catch (TypWyj2 exc) {
    // obsluga wyjątku typu TypWyj2
  }
  // ...
  } catch (TypWyjN exc) {
    // obsluga wyjatku typu TypWyjN
  } [ finally {                       
    // kod wykonywany niezaleznie do tego czy wyjatek powstał czy nie
  } ]

Uwaga: nawiasy kwadratowe wskazuję na opcjonalność klauzuli finally

Sekwencja dzialania jest następująca.


I druga wazna kwestia:

Jeśli obsługujemy wyjątek za pomocą try-catch,  to albo w klauzuli catch powinniśmy poprawić błąd, albo wypisać informację o błędzie. Nigdy nie pozostawiajmy catch pustym!

Zobaczmy przykład programu, który na podstawie dwóch wprowadzonych w dialogach wejściowych napisów, reprezentujących liczby całkowitych podaje sumę tych liczb oraz sumę długości wprowadzonych napisów (długość napisu s uzyskamy za pomocą odwołania s.length()).
Sumowanie realizuje metoda sumStrings, której przekazujemy wprowadzone napisy. Metoda zwraca tekst opisujący wynik.
Może się okazać, że metodzie przekazano wartość null - wtedy w odwołaniu s.length() wystąpi wyjątek NullPointerExceptiom. Może się też zdarzyć, że napis nie daje się zinterpretować jako znakowa reprezentacja liczby (wtedy wystąpi NumberFormatException). Obsługujemy oba te wyjątki, zwracając informację o zaistniałym błędzie. Na końcu dodaliśmy obsługę dowolnego wyjątku pochodnego od klasy Exception ("jakiś błąd") oraz klauzulę finally.
import javax.swing.*;

public class StringCalc {
  
  public String sum(String s1, String s2) {
    int sum = 0,
        sumLen = 0;
    try {
      sum = Integer.parseInt(s1) + Integer.parseInt(s2);
      sumLen = s1.length() + s2.length();
    } catch(NullPointerException exc) {
        return "Któryś z napisów jest null";
    } catch (NumberFormatException exc) {
        return "Któryś z napisów nie jest liczbą";
    } catch (Exception exc) {
        return "Wystąpił jakis błąd";
    } finally {
        JOptionPane.showMessageDialog(null, "Jestem finally!");
    }
    return "Suma: " + sum + '\n' + "Suma długości: " + sumLen;
  }

  public static void main(String[] args) {
    String s1 = JOptionPane.showInputDialog("Podaj pierwszą liczbę");
    String s2 = JOptionPane.showInputDialog("Podaj drugą liczbę");
    StringCalc calc = new StringCalc();
    JOptionPane.showMessageDialog(null, calc.sum(s1, s2));
  }
  
}


Uruchomienie programu pokaże, że dostajemy właściwe wyniki, a  przy wadliwych danych (np. po Cancel w dialogu wejściowym lub podaniu napisu, nie dającego się potraktować jako liczba) odpowiednie informacje o błędach. Niezależnie od tego czy wystąpił błąd w danych czy nie, za każdym razem zadziała klauzula finally i to jeszcze przed zwróceniem sterowania.

Moglibyśmy jednak popełnić dwa błędy w kodowaniu.
Nie umieszczając kodu obsługi w klauzulach catch (pozostawiając je puste) nie dostalibyśmy żadnej informacji o błędzie, a jedynie nieprawidłowy wynik.
Z kolei przestawiając klauzulę catch(Exception exc) na początek nie uzyskalibyśmy informacji o przyczynach wadliwości danych (czy null czy nie-liczba), tylko enigmatyczny komunikat ("Wystąpił jakiś błąd"). Dzieje się tak dlatego, gdyż Exception jest nadklasą pozostałych obsługiwanych wyjątków i wobec tego "jego" catch pasuje do obu rodzajów błędów w danych i jako pierwszemu pasującemu  (w kolejności zapisów) jemu to właśnie zostanie przekazane sterowanie.

7.2. Zgłaszanie wyjątków

 
Do zgłaszania wyjątków służy instrukcja sterująca throw.


Instrukcja sterująca throw ma postać:

        throw excref;

gdzie:
        excref - referencja do obiektu klasy wyjątku.
Np. 
        throw new NumberFormatException("Wadliwy format liczby: " + liczba);


W istocie, instrukcja throw jest niczym innym jak sposobem specyficznego przekazywania sterowania do jakichś punktów programu (do miejsc obsługi wyjątku). Należy jednak korzystać z niej wyłącznie w celu sygnalizowania błędów.

Zwykle w naszym kodzie będziemy sprawdzać warunki powstania błędu i jeśli są spełnione (wystąpił błąd) - zgłaszać wyjątek.
Jak zobaczymy w następnym punkcie możemy tworzyć własne klasy wyjątków i zgłaszać własne wyjątki. Nie należy jednak tego nadużywać, w istocie w Javie dostępna jest duża liczba gotowych, standardowo nazwanych, klas wyjątków i warto z nich właśnie korzystać.

Typowe, gotowe do wykorzystania, klasy wyjątków opisujących częste rodzaje błędów fazy wykonania programu pokazuje tabela.

Klasa wyjątkuZnaczenie
IllegalArgumentExceptionPrzekazany metodzie lub konstruktorowi argument jest niepoprawny,
IllegalStateExceptionStan obiektu jest wadliwy w kontekście wywołania danej metody
NullPointerExceptionReferencja ma wartość null w kontekście, który tego zabrania.
IndexOutOfBoundsExceptionIndeks wykracza poza dopuszczalne zakresy
ConcurrentModificationExceptionModyfikacja obiektu jest zabroniona
UnsupportedOperationExceptionOperacja (na obiekcie) jest niedopuszczalna (obiekt nie udostępnia tej operacji).

Możemy także wykorzystywać inne klasy, takie jak NumberFormatException (błąd formatu liczby) czy NoSuchElementException (wyjątek sygnalizowany, gdy w kolekcjach danych staramy się sięgnąc do nieistniejącego elementu).

Zwróćmy uwagę, że wszystkie wymienione wyżej wyjątki są niekontrolowane, bowiem pochodzą od klasy RuntimeException, co ułatwia ich wykorzystanie, bowiem nie zmusza programisty do ich obsługi. Nie musimy też takich wyjątków podawać w klauzuli throws w deklaracji metody, które je zgłasza (ale możemy, co sprzyja lepszej dokumentacji kodu). Ważne jednak jest, by tworząc i zgłaszając wyjątek jakiejś standardowej klasy podać przy wywołaniu konstruktora informację o przyczynie wyjątku.

Przykładowo w klasie definiującej konta bankowe możemy mieć metodę withdraw(double d), za pomocą której jest dokonywana wypłata z konta w wielkości d.
Oczywiście nie można wypłacić sumy mniejszej lub równej 0. Załóżmy, że nie można też wypłacić więcej niż wynosi aktualny stan konta.
Oba błędy możemy sygnalizować za pomocą zgłoszenia  wyjątku IllegalArgumentException:
public class Account {
 // ...
 private double balance;
 // ... 
 
 public void withdraw(double d) {
    if (d <= 0) throw new IllegalArgumentException("Withdrawal should be > 0");
    if (balance - d < 0)
      throw new IllegalArgumentException("Withdrawal exceeding balance not allowed");
    balance -= d;
  }

Nic nie stoi na przeszkodzie, by definiować własne klasy wyjątków i posługiwać się nimi w  naszych programach.

Żeby stworzyć własny wyjątek należy zdefiniować odpowiednią klasę.

Zgodnie z konwencją dziedziczymy podklasę Throwable - klasę Exception.

class NaszWyj extends Exception {
...
}

Zwykle w naszej klasie wystarczy umieścić dwa konstruktory: bezparametrowy oraz z jednym argumentem typu String (komunikat o przyczynie powstania wyjątku). W konstruktorach tych należy wywołać konstruktor nadklasy (za pomocą odwołania super(...), w drugim przypadku z argumentem String).

Obowiązują następujące zasady zgłaszania wyjątków:
Oczywiście, nie musimy tworzyć wyłącznie kontrolowanych własnych wyjątków (a tak się dzieje, gdy odziedziczymy klasę Exception).
Możemy przecież dziedziczyć klasę RuntimeException.
Podejmując decyzje w tym względzie warto jednak posługiwać się przyjętymi konwencjami: te wyjątki, które mogą powstawać w różnych miejscach kodu i wymuszanie obowiązkowej obsługi których byłoby dla użytkowników naszych klas bardzo uciążliwe - uczyńmy niekontrolowanymi, inne, szczególnie takie, z których aplikacja powinna prawie zawsze "się podnosić" (jak np. koniec pliku) - programujmy jako kontrolowane.

Przykładowo możemy obok klasy Account dostarczyć własnej klasy kontrolowanego wyjątku AccountException:
public class AccountException extends Exception {

  public AccountException() {
  }

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

}
i zmienić metodę withdraw w następujący sposób:
  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;
  }
W takim przypadku (wyjatek niekontrolowany) użycie klauzuli throws jest opcjonalne, możemy ją pominąć, ale zazwyczaj umieszcza się ją ze względów informacyjnych i dokumentacyjnych.

Klasie Account przyjrzymy się dokładnie w następnym wykładzie.

8. Podsumowanie

Wykład poświęcony był przede wszystkim szczegółowemu omówieniu instrukcji i operatorów przeznaczonych do podejmowania decyzji w programie.
W tej chwili instrukcje if, if-else, switch oraz operator warunkowy nie mają przed nami tajemnic.
Omówiono też szczególny rodzaj podejmowania decyzji w programie jakim jest obsługę i zgłaszanie wyjątków .  
Prawidłowe użycie instrukcji sterujących, jak i konstrukcji związanych z obsługą i zgłaszaniem wyjątków jest ważnym czynnikiem sprawnego programowania.