Java a výjimky – zpracování výjimek

9. února 2005

Známe již objektovou hierarchii výjimek a víme, z jakých základních pohledů nahlížet na vzniklou výjimku. V tomto článku se podíváme na špatné příklady práce s výjimkami, jako je například požírání výjimek, a vysvětlíme si, na jaké konstrukce dát pozor.

Než se zakousneme do výjimek pořádně, uděláme si čisto na stole a špatné návyky hodíme do koše. Následující případy demonstrují špatnou práci s výjimkami.

Požírání výjimek

Pohlcení výjimky bez jakéhokoli zpracování patří mezi základní chyby, kterých bychom se měli vyvarovat. Snad nejhorší možnost nastane v případě, kdy deklarujeme chycení výjimky na úrovni předka (Exception). Zvažme následující příklad:

public class Account{
    …
    …//business logika
    …
    public long getAccountBalance(){
        long balance = 0;
        try{
            .
            //operace pro zjištění zůstatku
            .
        }catch(Exception e){}
        return balance;
    }
}

Máme třídu Account, která představuje účet. Třída má metodu getAccountBalance(), která vrací zůstatek na účtu. Zjištění zůstatku, implementované v metodě getAccountBalance(), je uzavřeno v chráněném bloku try. K tomuto chráněnému bloku náleží blok zpracování – catch, kde jsou všechny výjimky odvozené od Exception pohlceny, a to právě díky prázdnému tělu tohoto bloku.

Představme si příklad, kdy se zjištění zůstatku provádí operacemi nad databází. V určitou chvíli se přeruší spojení s databází, které vyvolá vznik výjimky. Takto vzniklá výjimka je zachycena naším prázdným catch blokem a metoda je ukončena vrácením nulového zůstatku. Klient se pak může vzteky potrhat, neboť systém se navenek chová zcela korektně.

Další nepříjemnou vlastností výše uvedeného kódu je, že případná výjimka není nikde zaznamenána. Nevíme tak, jestli se jedná skutečně o výjimku signalizující chybu systému, nebo o jiný typ výjimky, například že účet je zablokován.

Základní pravidla pro práci s výjimkami

  1. Nikdy nepohlcujte výjimky.
  2. Porušíte-li pravidlo jedna, připojte vysvětlující komentář.

Pravidlo dva existuje pro výjimečné případy, které neznamenají chybu. Základní API Javy například definuje výjimku InterruptedException, která se pojí s vlákny a jejich přerušením. I v takovém případě je dobrým zvykem vložit do prázdného catch bloku vysvětlující komentář.

Ošetřování Exception

Ošetřování výjimek na úrovni předka (Exception) není závažné jako pohlcování výjimek, ale o to je zrádnější. Představme si kód, který je nucen zpracovat kontrolovanou výjimku. Tento případ ilustruje metoda fooBar, která volá metodu doSomethingFishy, jež deklaruje kontrolovanou výjimku:

public void doSomethingFishy() throws CheckedException{
    …
    …
}
    
public void fooBar(){
    doSomethingFishy();
    …
    …
}

Základní chybou je ošetřovat nebo propagovat výjimku CheckedException na úrovni předka, tedy Exception. Oba případy ilustrují následující ukázky.

Ošetření

public void fooBar(){
    try{
        doSomethingFishy();
    }catch(Exception e){
        //ošetření Exception
        …
        …
        …
    }
}

V prvním případě jsme se sice rozhodli kontrolovanou výjimku ošetřit, ale učinili jsme tak na úrovni předka Exception. Na první pohled se to nemusí jevit jako chybné, ale je potřeba si uvědomit, že ošetřením na úrovni Exception zachytíme i všechny nekontrolované výjimky, které mohou signalizovat vážné selhání systému, například nenalezení odpovídající třídy (ClassNotFound).

Další úskalí představuje otázka, jak s takto obecnou výjimkou naložit. Jakým způsobem ji ošetřit? Máme v daném kontextu dostatek prostředků pro její zpracování? Většinou se v takovýchto případech použije pouhý výpis výjimky. Prostým „chytáním“ výjimky na úrovni Exception připravujeme aktuální kontext o možnost zotavení, neboť na výjimku můžeme nahlížet pouze jako na objekt Exception a nikoli jako na její generalizaci poskytující specifické informace. V případě propagace výjimky stavíme vyšší vrstvy před ten samý problém s obecnou (nekonkrétní) výjimkou.

Pokud je nezbytně nutné ošetřit výjimku na úrovni Exception, musíme oddělit výjimky kontrolované od nekontrolovatelných, tak jako v následující upravené ukázce:

public void fooBar(){
    try{
        doSomethingFishy();
    }catch(Exception e){
        //otestujeme jestli se jedná o běhovou výjimku    
        if(e instanceof RuntimeException){
            //Běhovou výjimku necháme pokračovat
            throw (RuntimeException) e;
        }
        //zpracujeme všechny kontrolované výjimky
        …
        …
        …
    }
}

Díky testu na běhovou výjimku jsme zajistili bezproblémový průchod těchto závažných výjimek. Na druhou stranu existují opodstatněné případy, kdy se na úrovni Exception výjimka chytá. Typickým příkladem je Controller webové aplikace, kdy je zapotřebí, nejlépe na centrálním místě, zpracovat všechny výjimky a nabídnout uživateli rozumnou formou informaci o vzniku výjimky a umožnit mu tak další práci.

Ošetření – na pořadí záleží

Výše uvedené řešení, s kontrolou na běhovou výjimku, lze přepsat pomocí deklarativního zpracování. Jedná se o to, že každý chráněný blok může mít několik bloků catch pro zpracování různých typů výjimek. V našem příkladu bychom upravili kód následujícím způsobem:

public void fooBar(){
    try{
        doSomethingFishy();
    }catch(RuntimeException re){
        throw re;//běhovou výjimku necháme pokračovat
    }catch(Exception e){
        //zpracujeme všechny kontrolované výjimky
        …
        …
        …
    }
}

Nejdříve jsme deklarovali zpracování RuntimeException a teprve potom zpracování Exception. Pořadí, v jakém jsme deklarovali jednotlivé typy výjimek, je důležité z hlediska zpracování. Z předchozího článku již víme, že RuntimeException je potomek Exception. Pokud bychom deklarovali pořadí výjimek opačně, tedy nejdříve zpracování Exception, nedošlo by nikdy k zpracování (v našem případě opětovného vyhození běhové výjimky) RuntimeException. RuntimeException by skončila v prvním bloku, který jí odpovídá, což je blok s Exception. Obecně musíme mít na paměti, že nejdříve se v rámci několika catch bloků deklarují specializované typy výjimek a teprve poté se deklarují výjimky obecnějšího charakteru

Propagace

public void fooBar() throws Exception{
    doSomethingFishy();
}

Druhý příklad, ve kterém jsme deklarovali vyhození výjimky Exception, je principielně špatný. Nutíme totiž všechny, jež volají metodu fooBar, aby výjimku zpracovali nebo propagovali. Odpovědnost za výjimku jsme alibisticky nechali na klientech využívajících API . Diky tomu, že vyhazujeme Exception, nutíme klienty vyhodit Exception a nebo ji ošetřit, aniž by k tomu měli prostředky.

Deklarace Exception se násobně pronáší do dalších API, neboť volající si není schopen s takto obecně definovanou výjimkou poradit. V konečném stadiu nastane veliký problém při práci s kontrolovanými výjimkami a klient bude nucen testovat, jestli je chycená výjimka určitého typu.

Deklarace několika kontrolovaných výjimek

Pokud jsme mluvili o deklarovaném vyhazování Exception, pak musíme zmínit druhý extrém, vyhazování několika výjimek.

public void fooBar() throws CheckedExceptionX, CheckedExceptionY, CheckedExceptionZ{
    doSomethingFishy();
}

Klientský programátor je nucen ošetřovat zbytečné množství výjimek. V takovýchto případech je vhodnější zvolit obecnější výjimku. Od této výjimky je pak možné vytvořit jednotlivé specializace na úrovní objektů a nebo výjimku zřetězit do obecnější výjimky. Díky tomu můžeme v budoucnu ošetřit další typ výjimky, aniž by se to dotklo klientského programátora.

public class ApplicationException extends Exception{
    …
    …
    …
}
public class ConfigApplicationException extends ApplicationException{
    …
    …
    …
}
public class DataApplicationException extends ApplicationException{
    …
    …
    …
}
public class Foo{
    private void configure() throws ConfigApplicationException{
        …
        …
        …
    }
    private void postData() throws DataApplicationException{
        …
        …
        …
    }
    public void fooBar() throws ApplicationException{
        configure();
        postData();
    }
}

Řetězení výjimek jakožto druhý způsob si s dalšími dobrými technikami necháme na příští díl našeho putování.

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Předchozí článek fo
Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *