JE_39: Používejte výjimky pouze pro výjimečné podmínky
Když máte nějaký den smůlu, můžete se setkat s kódem, který se podobá tomuto:
// Úděsné zneužívání výjimek. Tohle nikdy nedělejte! try { int i = 0; while (true) a[i++].f(); } catch (ArrayIndexOutOfBoundsException e) { }
Co tento kód dělá? Není to patrné z prvního pohledu už to je důvod, proč jej nepoužívat. Ukazuje se, že jde o děsivý idiom procházení smyčkou prvky nějakého pole. Nekonečná smyčka skončí vyvoláním, zachycením a ignorováním výjimky ArrayIndexOutOfBoundsException, když se pokusí přstoupit k prvnímu prvku mimo hranoce pole. Má snad odpovídat standardnímu idiomu procházení polem ve smyčce, který okamžitě pozná každý programátor znalý Javy:
for (int i = 0; i < a.length; i++) a[i].f));
Proč by někdo používal idiom pracující s výjimkami namísto toho vyzkoušeného a ověřeného? Jedná se o naprost zcestný pokus o zlepšení výkonnosti, který vyplývá z nepsrávného uvažování spočívajícího v tom, že jelikož VM kontroluje hranice všech přístupů k poli, je normální test ukončení smyčky (i < a.length) nadbytečný a je zapotřeb se jej zbavit. V tomto uvažování jsou nesprávné tři věci:
-
Protože výjimky se mají používat pouze za výjiečných podmínek, jen málo implementací JVM (pokud vůbec nějaké) se snaží o optimalizaci jejich výkonu. Obecně je náročné vytvořit, vyvolat a zachytit nějakou výjimku.
-
Umístění kódu dovnitř bloku try - catch může zabránit určitým optimalizacím, které mohou jinak moderní implementace JVM vykonat.
-
Stndardní idiom procházení polem ve smyčce nemusí nutně znamenat nadbytečné kontroly; některé moderní implementace JVM je zoptimalizují.
Ve skutečnosti je idiom pracující s výjimkami mnohem pomalejší než ten standardní na téměř všech současných implementacích JVM. Na mém počítači běží idiom používající výjimky sedmdesátkrát pomaleji než ten standardní při procházení smyčkou od 0 do 99.
Nejenže princip smyčky s výjimkou zatem�uje účel kódu a snižuje jeho výkonnost, ale ani nemusí fungovat. Vyskytne-li se nějaká jiá chyba, pak celý idiom potichu selže a chybu zamaskuje, čímž výrazně zkomplikuje proces ladění. Představme si, že výpočet v těle smyčky obshuje nějakou chybu, jejímž důsledkem je přístup mimo hranice nějakého jiného pole. Pokud by byl použit rozumný idiom smyčky, pak by tato chyba vygenerovala nezachycenou výjimku, čímž b došlo k okamžitému ukončení vlákna s příslušným chybovým hlášením. jesltiže použijeme špatný idiom smyčky s výjimkou, pak dojde k zachycení výjimky související s výše zmíněnou chybou, která se pak mylně interpretuje jako normální ukončení smyčky.
Poučení je jednoduché: výjimky je zapotřebí používat, jak už jejich náezv naznačuje, pouze za výjimečných podmínek; nikdy je nepoužívejte pro obyčejné řízení toku. Obecněji platí, že byste měli používat standardní, snadno rozpoznatelné idiomy nmísto těch příliš chytrých, které by snad měly nabízet vyšší výkonnost. I když skutečně dojde k nárůstu výkonu, nemusí se již projevit v dalších neustále se zlepšujících implementacích JVM. Skryté chyby a problémy se správou přechtralých idiomů však nikdy nikam nemizejí.
Tento princip má také dopady na návrh API. Dobře navržené API nesmí nutit své klienty používat výjimky k normálnímu řízení toku. Třída s nějakou "stavově závislou" metodou, kterou lze volat pouze za určitých nepředvídatelných podmínek, by obecně měla mít nějakou samostatnou metodu "testování stavu", indikující, zda je možné zavolat tu první metodu. Například třída Iterator má stavově závislou metodu next, která vrací následující prvek v iteraci, a odpovídající metodu testování stavu hasNext. Tak je zajištěna podpora standardního idiomu iterování kolekcí:
for (Iterator i = collection.ierator(); i.hasNext();) {
Foo foo = (Foo) i.next();
. . .
}
Pokud by třída Iterator neměla metodu hasNext, pak by byl klient přinucen postupovat takto:
try { Iterator i = collection.iterator(); while (true) { Foo foo = (Foo) i.next(); . . . } } catch (NoSuchElementException e) { }
Tento kód vám jistě bude povědomý, protože jste viděli příkad, kterým začínala tato rada. Nejenže je tento idiom upovídaný a matoucí, ale také se bude pravděpodobně vykonávat výrazně hůře než ten standardní a může zamaskovat chyby v nesouvisejících součástech systému.
Alternativou k poskytnutí samostatné metody testující stav je to, aby metoda závislá na stavu vracela nějakou nerozpoznatelnou metodu, například null je-li vyvolána v okamžiku kdy se objekt nenachází ve vhodném stavu. Tato technika by ale nevyhovovala pro třídu Iterator, protože null je legitimní návratová hodnota netody next.
Zde jsou zásady, které vám pomohou rozhodnout semezi metodou testování stavu a rozpoznatelnou návratovou hodnotou. Pokud se bude k njakému objektu přistupovat souběžně bez externí synchronizace nebo je vystaven externě vyvolávaným změná stavu, pak může být důležité použít nějakou výraznou návratovou hodnotu, protože stav objektu se může změnit v intervalu mezi voláním metody testující stav a jí odpovídající stavově závislé metody. Výkonnostní důsledky mohou vyžadovat použití rozpoznatelné návratové hodnoty, pokud by musela metoda testování stavu duplikovat práci metody závislé na stavu. Jestliže jsou si však všechny jiné ohledy rovné, pak je metoda testování stavu trochu výhodnější než rozpoznatelná návratová hodnota. Poskytuje trochu lepší čitelnost a nesprávné použití se bude asi lépe zjišťovat a opravovat.