Doposud jsme tiše předpokládali, že v našich programech vše půjde dle plánu a nikdy nedojde k nějaké nepředvídané situaci. Pod takovouto situací si můžeme představit chyby programu způsobené programátorem – dělení nulou, sáhnutí mimo rozsah pole, pokus o volání metody na nullové referenci a podobně. Druhou skupinou pak mohou být neplatné uživatelské vstupy – pokud uživatel předá do kolonky věk řetězec, pokusí se uložit soubor někam, kam nemá přístup a jiné. Poslední skupinou jsou chyby mimo kontrolu a moc programátora – vyčerpání paměti, zásah operačního systému atd.
Při všech těchto situacích (a mnohých dalších) dojde k vyvolání výjimky, která způsobí okamžité přerušení vykonávání kódu a přechod vlákna do místa, kde je daná situace ošetřena. Pokud takové místo neexistuje, tak je toto vlákno ukončeno.
Fail-fast
V našich programech máme prozatím pouze jedno vlákno. To znamená, že neošetření výjimky vyústí v ukončení celého programu. Sice se může zdát, že bychom se proto měli snažit program za každých okolností zachránit, ale není tomu tak. Obvykle je nejlepší nechat program ihned spadnout, dokud nedošlo k nenávratným škodám na datach, nežli později opravovat běsnění programu, který se dostal do stavu, se kterým jsme nepočítali.
Typy výjimek
Výjimky samotné jsou objekty, které dědí ze speciální hierarchie, jejímž kořenem je třída Throwable (dokumentace). Throwable dále rozšiřují třídy Error a Exception.
Error
Výjimky typu Error (dokumentace) bychom neměli v žádném případě ošetřovat – značí totiž kritickou chybu (nedostatech zdrojů pro práci virtuálního stroje, přetečení zásobníku, nenalezení potřebné třídy při classloadingu atp.) – a většině případů je ani ošetřit nemůžeme.
Runtime exception
Mezi výjimky dědící z Exception patří také podtřída RuntimeException (dokumentace). Runtime exception jsou výjimky, které sice nejsou kritické z hlediska samotné možnosti pokračování aplikace (na rozdíl od Error), ale přesto se velmi často neošetřují. Značí totiž obvykle chyby, které způsobil sám programátor (neplatný index pole, volání nad nullovým ukazatelem...).
Zvláštní vlastností těchto výjimek je, že nemusíme deklarovat v hlavičce metody možnost jejich vyvolání (klíčové slovo throws). Tím je usnadněno vybublání chyby skrz program a jeho případné ukončení.
Exception
Zbylé výjimky dědící přímo z Exception (dokumentace) značí ty situace, které by se sice neměly stávat, ale na které jsme schopni adekvátně zareagovat. Pokud uživatel zadá neplatný adresář, tak mu vyhubujeme a řekneme, že to má zkusit ještě jednou. Obdobně jsme schopni vyřešit situaci, kdy uživatelské rozhraní nenalezne knihovny potřebné pro uživatelský motiv (look and feel) na daném systému. V tomto případě můžeme přejít k výchozímu nastavení uživatelského rozhraní.
Protože se jedná, jak jsme si již řekli, o chyby, ze kterých se program může snadno zotavit, tak je vždy musíme uvést v hlavičce metody, jež je může vyvolat. To učiníme pomocí klíčového slova throws a seznamu jmen tříd vyvolávaných výjimek. Ve volající metodě se pak musíme rozhodnout, zda-li výjimky ošetříme, nebo necháme vybublat dál (opět je uvedeme v hlavičce).
Tyto výjimky označujeme jako kontrolované (checked exceptions).
Vyvolávání výjimek
Výjimku můžeme vyvolat v našem kódu prostřenictvím příkazu throw následovaným objektem výjimky.
01.
package
jpz14;
02.
/**
03.
* Demonstace vyjimek
04.
* @author Pavel Micka
05.
*/
06.
public
class
Main {
07.
/**
08.
* @param args the command line arguments
09.
*
10.
* Jelikoz metoda throwException vyvolava kontrolovanou vyjimku a my
11.
* ji opet nezpracovavame, tak ji musime opet deklarovat,
12.
* aby probublala ven (a ukoncila vlakno programu)
13.
*/
14.
public
static
void
main(String[] args)
throws
Exception {
15.
throwException();
16.
}
17.
18.
/**
19.
* Metoda, ktera pouze vyvola vyjimku typu Exception
20.
* Jelikoz se nejedna o runtime Exception a zaroven ji
21.
* okamzite nezpracovavame, tak ji musime deklarovat v hlavicce
22.
* metody za klicovym slovem throws
23.
*/
24.
public
static
void
throwException()
throws
Exception{
25.
throw
new
Exception(
"Ja jsem zprava vyjimky"
);
26.
}
27.
}
1.
Exception in thread "main" java.lang.Exception: Ja jsem zprava vyjimky
2.
at jpz14.Main.throwException(Main.java:25)
3.
at jpz14.Main.main(Main.java:15)
Na výstupu tohoto příkladu neodchycené výjimky vidíme tzv. stack trace. Jedná se o výpis volání metod na systémovém zásobníku v okamžiku, kdy došlo k výjimce. Tento výstup nám bude významně pomáhat při odstraňování chyb v aplikaci.
Ve vývojových prostředích můžeme obvykle na jednotlivé záznamy kliknout a dostat se tak přímo na jednotlivé řádky kódu, které byly volány v okamžiku vzniku výjimky.
Try-catch-finally
Každou výjimku bez ohledu na její typ můžeme zpracovat. K tomu používáme bloky try, catch a finally. V bloku try uvedeme sekci, která je krirická a vyvolává výjimku. Až v mnoha blocích catch můžeme postupně zpracovávat případné výjimky dle jejich typu. Volitelný blok finally obsahuje kód, který se vyvolá vždy, bez ohledu na to, k čemu dojde v bloku try (dokonce se vyvolá i případě, že dojde k opuštění metody prostřednictvím příkazu return).
01.
/**
02.
* Metoda demonstrujici odchytavani vyjimek
03.
*/
04.
public
static
void
catchException() {
05.
try
{
06.
int
a =
10
/
0
;
//vyvola java.lang.ArithmeticException (Runtime vyjimka)
07.
}
catch
(RuntimeException e) {
08.
e.printStackTrace();
//vypiseme zasobnik na error vystup
09.
System.out.println(
"Runtime exception odchycena."
);
10.
}
catch
(Exception e) {
11.
e.printStackTrace();
//vypiseme zasobnik na error vystup
12.
System.out.println(
"Odchycena vyjimka tridy Exception"
);
13.
System.out.println(
"Tento blok se vypise, pokud nedojde k odchyceni"
14.
+
"vyjimky v nejakem z predchozich bloku."
);
15.
}
finally
{
16.
System.out.println(
"Obsah bloku finally bude zpracovan vzdy."
);
17.
}
18.
System.out.println(
"Vyjimka byla zpracovana, zde muzeme pokracovat"
);
19.
}
1.
Runtime exception odchycena.
2.
java.lang.ArithmeticException: / by zero
3.
Obsah bloku finally bude zpracovan vzdy.
4.
Vyjimka byla zpracovana, zde muzeme pokracovat
5.
at jpz14.Main.catchException(Main.java:26)
6.
at jpz14.Main.main(Main.java:11)
Ve výstupu si všimněme, že jsou promíchány jednotlivé výpisy. Nejedná se však o chybu. Pouze o dva různé výstupy (standardního a chybového) vypsané do jedné konzole.
Vytvoření vlastních výjimek
Výjimky jsou až na své specifické vlastnosti (jdou vyvolat prostřednictvím throw) třídy jako každé jiné. Z tohoto pohledu k nim také můžeme přistupovat a dědit je, abychom si zajistili vlastní chování.
S děděním výjimek to není třeba příliš přehánět, jelikož na většinu standardních situací již existují předpřipravené implementace (které budou uživatelé našich knihoven spíše očekávat).
01.
/**
02.
* Priklad vlasni vyjimky
03.
* @author Pavel Micka
04.
*/
05.
public
class
MyException
extends
Exception{
06.
public
MyException(Throwable cause) {
07.
super
(
"Instance MyException"
, cause);
08.
}
09.
public
void
myMethod(){
10.
System.out.println(
"Tato vyjimka ma navic tuto metodu"
);
11.
}
12.
}
Obvyklé zpracování výjimek
Nyní si představíme několik modelových situací zpracování výjimek, při kterých děláme více než pouhé ošetření pomocí try-catch-finally.
Neošetření
Základní strategií pro výjimky typu Error a velkou část runtime výjimek je jejich neošetření. K chybám (Error) by nemělo docházet nikdy, jsou to kritické situace z hlediska aplikace a jakákoliv snaha o záchranu je marná a navíc by mohla způsobit více škody než užitku (můžeme se leda pokusit chybu zalogovat, případně zkusit uložit stav aplikace, ale nikdy bychom neměli pokračovat dále).
V případě RuntimeException není situace kritická z hlediska aplikace (pouze z hlediska programu). V řadě případů můžeme aplikaci zachránit odchycením výjimky (ale v tom případě bychom měli spíše použít nějakou kontrolovanou výjimku). Druhým aspektem je, že se jedná vesměs o chyby programátora, které by měly být do značné míry eliminovány při testování aplikace a zároveň nebývá jejich náprava možná.
Přebalení na kontrolovanou výjimku
Často se stává, že vyvolaný typ výjimky neodpovídá situaci, ve které se program nachází. Příkladem může být uživatelské rozhraní a pole určené pro číslo. V naší aplikaci pak zadaný řetězec chceme převést na číslo pomocí volání metody parseInt třídy Integer. Pokud ovšem uživatel zadá řetězec, který číslu neodpovídá, tak metoda vyhodí výjimku NumberFormatException (dokumentace), která dědí od RuntimeException.
Příklad
Nyní jsme v situaci, kdy je vhodné tuto běhovou výjimku odchytit a vytvořit novou kontrolovanou výjimku odpovídajícího typu. Při konstrukci kontrolované výjimky nesmíme zapomenout předat konstruktoru původní běhovou výjimku. Novou obalující výjimku nakonec vyvoláme pomocí klíčového slova throws.
Tímto postupem zajistíme potřebnou konverzi typu výjimky, aniž bychom ztratili informaci předávanou původní výjimkou (obalující výjimka vypíše vždy i výjimku obsaženou). Zároveň přinutíme volající metodu nějakým způsobem nastalou situaci vyřešit (ta může vyčistit špatně vyplněné pole a upozornit uživatele, že má zadat číselnou hodnotu).
Přebalení výjimky na RuntimeException
Obdobně se také můžeme dostat do situace, kdy metoda sice vyvolá kontrolovanou výjimku, ale již nemůžeme udělat nic pro napravení dané situace, případně ji chceme nechat prubublat až do nějaké vyšší vrstvy aplikace, ve které řešíme výjimky bez ohledu na jejich typ. V tento okamžik je vhodné výjimku přebalit do RuntimeException (nebo její libovolné podtřídy) a tuto poté vyvolat.
Příklad
01.
/**
02.
* Demonstace vyjimek
03.
* @author Pavel Micka
04.
*/
05.
public
class
Main {
06.
07.
/**
08.
* @param args the command line arguments
09.
*/
10.
public
static
void
main(String[] args) {
11.
int
i =
0
;
12.
boolean
valid =
false
;
13.
do
{
14.
System.out.println(
"Zadejte cislo (v rozsahu int):"
);
15.
try
{
16.
i = readNumber();
17.
valid =
true
;
//cislo je v poradku
18.
}
catch
(InvalidInputException ex) {
19.
System.out.println(
"Zadal jste neplatne cislo!"
);
20.
}
21.
}
while
(!valid);
22.
System.out.println(
"Zadal jste cislo: "
+ i);
23.
}
24.
25.
/**
26.
* Precte cislo ze standardniho vstupu (konzole)
27.
* @return Ciselna hodnota vstupu uzivatele
28.
* @throws InvalidInputException Pokud uzivatel zada neplatny vstup (ktery neni cislo)
29.
*/
30.
private
static
int
readNumber()
throws
InvalidInputException {
31.
Scanner s =
new
Scanner(System.in);
32.
try
{
33.
return
s.nextInt();
34.
}
catch
(InputMismatchException e) {
//Runtime vyjimka, kterou scanner reaguje na neplatny vstup
35.
throw
new
InvalidInputException(e);
//prebalime ji na checked exception
36.
}
37.
}
38.
}
39.
40.
/**
41.
* Kontrolovana vyjimka vlastniho typu oznacujici, ze v nasem priklade doslo k chybe
42.
* na vstupu
43.
* @author Pavel Micka
44.
*/
45.
class
InvalidInputException
extends
Exception {
46.
47.
public
InvalidInputException(Throwable cause) {
48.
super
(cause);
49.
}
50.
}
Scanner
V tomto příkladu používáme třídu Scanner (dokumentace), která za nás provede čtení ze standardního vstupu (System.in) a rovnou také konverzi na číslo (integer). V případě, že se čtení z libovolného důvodu nepodaří, tak metoda nextInt() vyvolá runtime výjimku InputMismatchException, kterou v duchu předhozích odstavců překonvertujeme.
Zadávání vstupu v IDE
V Netbeans IDE můžeme po vyzvání programem (Zadejte cislo (v rozsahu int)) psát vstup rovnou do konzole.