Java (14) - Výjimky

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).

Základní struktura výjimek v Javě
Základní struktura výjimek v Javě

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.

package jpz14;
/**
 * Demonstace vyjimek
 * @author Pavel Micka
 */
public class Main {
    /**
     * @param args the command line arguments
     *
     * Jelikoz metoda throwException vyvolava kontrolovanou vyjimku a my
     * ji opet nezpracovavame, tak ji musime opet deklarovat,
     * aby probublala ven (a ukoncila vlakno programu)
     */
    public static void main(String[] args) throws Exception {
        throwException();
    }

    /**
     * Metoda, ktera pouze vyvola vyjimku typu Exception
     * Jelikoz se nejedna o runtime Exception a zaroven ji
     * okamzite nezpracovavame, tak ji musime deklarovat v hlavicce
     * metody za klicovym slovem throws
     */
    public static void throwException() throws Exception{
        throw new Exception("Ja jsem zprava vyjimky");
    }
}
Exception in thread "main" java.lang.Exception: Ja jsem zprava vyjimky
        at jpz14.Main.throwException(Main.java:25)
        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).


    /**
     * Metoda demonstrujici odchytavani vyjimek
     */
    public static void catchException() {
        try {
            int a = 10 / 0; //vyvola java.lang.ArithmeticException (Runtime vyjimka)
        } catch (RuntimeException e) {
            e.printStackTrace(); //vypiseme zasobnik na error vystup
            System.out.println("Runtime exception odchycena.");
        } catch (Exception e) {
            e.printStackTrace(); //vypiseme zasobnik na error vystup
            System.out.println("Odchycena vyjimka tridy Exception");
            System.out.println("Tento blok se vypise, pokud nedojde k odchyceni"
                    + "vyjimky v nejakem z predchozich bloku.");
        } finally {
            System.out.println("Obsah bloku finally bude zpracovan vzdy.");
        }
        System.out.println("Vyjimka byla zpracovana, zde muzeme pokracovat");
    }


Runtime exception odchycena.
java.lang.ArithmeticException: / by zero
Obsah bloku finally bude zpracovan vzdy.
Vyjimka byla zpracovana, zde muzeme pokracovat
        at jpz14.Main.catchException(Main.java:26)
        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).

/**
 * Priklad vlasni vyjimky
 * @author Pavel Micka
 */
public class MyException extends Exception{
    public MyException(Throwable cause) {
        super("Instance MyException", cause);
    }
    public void myMethod(){
        System.out.println("Tato vyjimka ma navic tuto metodu");
    }
}

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


/**
 * Demonstace vyjimek
 * @author Pavel Micka
 */
public class Main {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        int i = 0;
        boolean valid = false;
        do {
            System.out.println("Zadejte cislo (v rozsahu int):");
            try {
                i = readNumber();
                valid = true; //cislo je v poradku
            } catch (InvalidInputException ex) {
                System.out.println("Zadal jste neplatne cislo!");
            }
        } while (!valid);
        System.out.println("Zadal jste cislo: " + i);
    }

    /**
     * Precte cislo ze standardniho vstupu (konzole)
     * @return Ciselna hodnota vstupu uzivatele
     * @throws InvalidInputException Pokud uzivatel zada neplatny vstup (ktery neni cislo)
     */
    private static int readNumber() throws InvalidInputException {
        Scanner s = new Scanner(System.in);
        try {
            return s.nextInt();
        } catch (InputMismatchException e) { //Runtime vyjimka, kterou scanner reaguje na neplatny vstup
            throw new InvalidInputException(e); //prebalime ji na checked exception
        }
    }
}

/**
 * Kontrolovana vyjimka vlastniho typu oznacujici, ze v nasem priklade doslo k chybe
 * na vstupu
 * @author Pavel Micka
 */
class InvalidInputException extends Exception {

    public InvalidInputException(Throwable cause) {
        super(cause);
    }
}

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.








Doporučujeme

Internet pro vaši firmu na míru