Už dlouho jsem nikoho nepoučoval a trochu mi to chybí, takže dneska budu psát o kontrolovaných alias synchronních alias čtverečkovaných alias checked výjimkách. Ano o těch výjimkách, které jste překladačem nuceni odchytit a zpracovat.
Teď pracuji s pěkně starým kódem. Jeho autoři si mysleli, že jsou kontrolované výjimky dobrý nápad, takže je hojně využívali. Já se jim nedivím, na první pohled se líbily i třeba Bruce Eckelovi. A nejen jemu, spousta důležitých knihoven v Javě je také hodně používá. Jenže já je nemám rád. Ukažme si to na příkladu ze života.
Představme si že píšeme finanční aplikaci která má být hyperrobustní. Vytovříme si třídu Money, do které budeme ukládat peníze. A to nejen množství, ale i měnu. Už při vytváření instance může nastat chyba. Filuta uživatel mé skvělé peněžní knihovny se mi může snažit podstrčit neexistující měnu. Jenže já nejsem žádný jakýpak copak, já mu na to přijdu a vyhodím mu CurrencyException
. A abych mu ukázal, že se to může stát, tak udělám CurrencyException
kontrolovanou (checked). Takže on musí počítat s tím, že mu ji vyhodím a správně ji zpracovat. Skvělé. Dokonce mohu i napsat metodu add, která si bude kontrolovat, jestli sčítám stejné měny a když ne, vyhodí mojí oblíbenou CurrencyException
.
public Money(float value, String currency) throws CurrencyException {
//value should be BigDecimal, float is used for simplicity
validateCurrency(currency);
this.value = value;
this.currency = currency;
}
public Money add(Money other) throws CurrencyException
{
if (!other.getCurrency().equals(getCurrency()))
{
throw new CurrencyException("Currencies do not match.");
}
return new Money(getValue() + other.getValue(),getCurrency());
}
Takže mám super kód, který nutí ostatní programátory zabývat se tím, co se stane, když se pokusí míchat americké dolary a polské zloté. Ať žijí kontrolované výjimky! To si ovšem budeme říkat jen do doby než začneme tuto třídu používat. Nebo než uvidíme kód který ji používá. Setkáme se s podobnými perlami:
Money money1 = null;
try {
money1 = new Money(123, "EUR");
} catch (CurrencyException e) {
//can not happen
}
Money money2 = null;
try {
money2 = new Money(121, "EUR");
} catch (CurrencyException e) {
log.log(Level.SEVERE, e.getMessage());
}
Money money3;
try {
money3 = money1.add(money2);
} catch (CurrencyException e) {
throw new IllegalStateException(e);
}
Při vytváření instance money1 si programátor zjevně myslel, že výjimka nemůže nastat. Vždyť tam má to euro natvrdo. Tak ji ignoruje. Tento případ je ze všech nejhorší. O trochu lepší případ nastane, když výjimku zapíše do logu jak to udělal v případě money2. Ale i tak, pokud při vytváření money2 dojde k výjimce, program bude pokračovat jen proto, aby o kousek dál skončil s NullPointerException
. To je ta lepší varianta. V horším případě k NPE dojde o velký kus dál a my si budeme marně lámat hlavu, jak se nám tam dostal null. Asi nejlepší ze špatných řešení je použito u money3. Ali i to je špatně čitelné a ošklivé. Už chápete proč nemám rád kontrolované výjimky? Pokud by CurrencyException
byla potomkem třeba IllegalArgumentException, kód by vypadal takto:
Money2 money1 = new Money2(123, "EUR");
Money2 money2 = new Money2(121, "EUR");
Money2 money3 = money1.add(money2);
Nejen že to je lépe čitelné, ono je to i mnohem bezpečnější. Pokud dojde k té nepravděpodobné události a něco se pokazí, dozvím se o tom ihned. A pokud výjimku neodchytím někde o kus dál, tak se o tom dozví bohužel i uživatel. Ale i to je mnohem lepší, než aby se aplikace tvářila jako že funguje.
Pokud vám uvedený příklad připadá umělý, podívejte se na konstruktor java.net.URL nebo na jakýkoliv JDBC kód. Nebo klasický příklad s kódováním.
Writer writer = null;
try {
writer = new OutputStreamWriter(out, "UTF-8");
} catch (UnsupportedEncodingException e) {
//can not happen
}
Co se stane když vaše JVM nebude znát UTF-8? Správně, dostanete NullPointerException
. S trochou štěstí po pár minutách debugování zjistíte co se děje. Když štěstí mít nebudete, tak vám to může zabrat i den.
Takže abych to shrnul. Kontrolované výjimky se mohou používat v jediném případě. Pokud jsem si jist, že programátor který ji odchytí bude vždy vědět co s ní má dělat. Což je asi důvod proč v C# kontrolované výjimky vůbec nejsou. Na celé záležitosti je navíc matoucí to, že ve skutečnosti máme opravdu dva druhy výjimek.
Jeden druh je obvykle způsoben špatnými vstupními daty. V takovém případě má smysl vyhazovat kontrolovanou výjimku a donutit programátora ji nějak zpracovat.
Jenže pak tu máme chyby způsobené chybou v kódu, konfigurací, selháním sítě, disku a podobně. V takovém případě nemá cenu výjimku obvykle chytat, protože beztak nevíme co s ní.
To by napovídalo, že pro chyby prvního typu bychom měli používat kontrolované výjimky, pro chyby druhého typu běhové (runtime) výjimky. EJB specifikace tak dokonce funguje. Podle typu výjimky dělá nebo nedělá rollback transakce (pokud ji neřekneme jinak).
Finta je v tom, že dopředu nikdy nevíme, ke kterému druhu chyby může dojít. Vraťme se k příkladu s penězi. Může se stát, že nám špatná měna přijde z jiného systému v XML. Pak má smysl vrátit specifickou chybovou hlášku a používat kontrolovanou vyjímku. Stejně tak může být měna načtena z konfiguračního souboru. Pak obvykle nemá smysl se výjimku nějak speciálně zabývat, aplikace je prostě nefunkční. Hláška v logu je naprosto dostačující.
Takže opakuji, nikdy dopředu nevíme jestli bude moci programátor používající náš kód umět výjimku zpracovat. Pravděpodobnější je, že to nedovede a provede s ní něco ošklivého. Proto se přikláním k názoru, že by kontrolované výjimky měly být používány zřídka, nejlépe vůbec. Běhové výjimky jsou bezpečnější a kód je s nimi mnohem čitelnější. Nicméně pokud má někdo nějaký příklad, kde si je jist, že by měly být za všech okolností použity kontrolované výjimky, sem s ním, rád se poučím.
Zdroje (aneb důkaz, že nosím dříví do lesa):
Does Java need Checked Exceptions?
Java theory and practice: The exceptions debate