Výnimky (vyhadzovanie, vlastné výnimky)

<< Java Collections Framework | Obsah | Statické metódy a premenné, konštanty >>

Už vieme, že pri volaní nejakej metódy môže nastať za istých okolností nejaká výnimka. Keď ju neodchytíme, program nám skončí a vypíše sa stack trace.

Tiež vieme, že výnimky, ktorým vieme zabrániť overovaním vhodných podmienok pred spustením kritickej operácie neriešime odchytávaním, ale práve overovaním podmienok vopred.

Výnimky, ktorým nevieme predchádzať overovaním vstupných podmienok môžeme odchytávať v try-catch-finally blokoch.

try {

  // blok príkazov z ktorého odchytávame výnimky

} catch (TypVýnimky1 e) {
  // vysporiadanie sa s daným typom výnimky
} catch (TypVýnimky2 e) {
  // vysporiadanie sa s daným typom výnimky
} finally {
  // príkazy, ktoré sa vykonajú bez ohľadu na to, či bola vyhodená výnimka alebo nie
  // a bez ohladu na to či sa výnimka vzniknutá v bloku try odchytila alebo nie
}

Vyhadzujeme výnimky

Výnimky sa používajú pri výnimočných stavoch, s ktorými si daná metóda nevie poradiť. Ak si metóda vie poradiť sama, nemá zmysel výnimku vyhadzovať. Metóda vyhadzuje výnimku s tým, že je šanca, že sa s tým kus kódu, ktorý danú metódu volal, vie vysporiadať. Dôvodom na vyhodenie výnimky je väčšinou neschopnosť vykonať požadovanú akciu a vrátiť požadovanú hodnotu. Príčinou môže byť:

  • Volajúci kód porušil kontrakt - v dokumentácii sme uviedli, za akých podmienok vie metóda vykonať očakávanú činnosť, ale tieto podmienky neboli dodržané
    • metóde na výpočet logaritmu podhodíme záporné číslo
    • metóde na nastavovanie rodného čísla pošleme reťazec "Michael Jackson is not dead!"
  • Nie sú potrebné podmienky na úspešné vykonanie kódu
    • nemáme internet
    • padol server, s ktorým sa potrebujem rozprávať
    • súbor sa nenašiel
    • zaplnil sa disk
    • nemám dáta o ktoré žiadaš

Doteraz, sme pracovali iba s výnimkami, ktoré hádzali cudzie metódy. Výnimky môžeme vyhadzovať aj zo svojich metód. Môžeme vyhadzovať už známe výnimky (java.lang.NumberFormatException, java.lang.IllegalArgumentException, ...) alebo si môžeme vyrobiť aj vlastné výnimky. Výnimky sú obyčajné triedy, ktoré dedia od triedy java.lang.Exception. Môžu teda mať vlastné inštančné premenné, metódy a konštruktory. Dokážeme tak odoslať veľmi komplexnú informáciu o vzniknutej sitácii, a nezriedka aj návrh na riešenie. Vyrobme si vlastnú výnimku ZápornýVstupException.

public class ZápornýVstupException extends Exception {
}

Vyrobme si príklad, ktorý počíta faktoriál čísla n. Ak príde n záporné, vyhodíme výnimku ZápornýVstupException.

public class Pocitadlo {
        public int faktorial(int n) throws ZápornýVstupException {
                if (n < 0)
                   throw new ZápornýVstupException(); //vyhadzujeme výnimku
                int faktorial = 1;
                for (int i = 1; i < n; i++) {
                        faktorial = faktorial * i;
                }
                return faktorial;
        }
}

Všimnite si, že výnimka je obyčajný objekt, vytvorený cez new. Pomocou kľúčového slova throw okamžite ukončíme beh metódy a vyhodíme výnimku. Výnimky, ktoré môžeme vyhadzovať, uvádzame v hlavičke za kľúčovým slovom throws. Ak môžeme vyhadzovať viac druhov výnimiek, oddeľujeme ich čiarkami.

Keď teraz budeme volať túto metódu, Java od nás bude požadovať ošetrenie výnimky. Zabalíme teda toto volanie do try-catch blokov.

public class Spustac {
        public static void main(String[] args) {
                try {
                        Pocitadlo p = new Pocitadlo();
                        System.out.println(p.faktorial(-10));
                } catch (ZápornýVstupException e) {
                        e.printStackTrace();
                }
        }
}

Aby mohli bez ujmy na zdraví používať vaše programy aj iní ľudia (resp. po dlhšom čase aj vy sami), nikdy nevyhadzujte všeobecné výnimky, napr:

public int faktorial(int n) throws Exception {
       ...
}

Ak nastane všeobecná výnimka, používateľ vášho kódu nemusí pochopiť, a obvykle ani nepochopí, čo sa stalo a čo bolo príčinou. Na druhej strane, keď dostane výnimku s rozumným menom, napr. ZápornýVstupException, už z jej názvu vie veľmi rýchlo pochopiť, že problém zrejme spočíva v záporných vstupoch.

(Ne)ošetrujeme výnimky

Ak výnimku spôsobilo volanie inej metódy, máme dve možnosti:

  • výnimku odchytíme v catch bloku a ošetríme ju
  • pošleme výnimku na spracovanie metóde, ktorá našu metódu volala - výnimka vybuble vyššie

V druhom prípade neošetrenú výnimku uvedieme v throws a necháme vyššie metódy nech to riešia. Skôr či neskôr sa však toto prehadzovanie zodpovednosti za ošetrenie vzniknutého stavu musí niekde zastaviť -- v opačnom prípade výnimka vybuble von z metódy main(), čo vyústi v násilné ukončenie programu. Napríklad, keby sme pri čítaní súboru neodchytili FileNotFoundException mohli by sme ju proste nechať vybublať vyššie:

public class Pocitadlo {
        public List<Integer> nacitajCisla(File f) throws FileNotFoundException {
                List<Integer> cisla = new ArrayList<Integer>();
                Scanner citac = new Scanner(f); //neodchytená výnimka FileNotFoundException
                while(citac.hasNextInt()) {
                        cisla.add(citac.nextInt());
                }
                citac.close();
                return cisla;
        }
}

Obveľa zaujímavejšou možnosťou je prebaliť výnimku do novej, popisnejšej. Vytvoríme si novú výnimku PocitadloException, ktorej cez konštruktor vložíme textový popis a aj pôvodnú výnimku FileNotFoundException, ktorú prebaľujeme. Ten, čo odchytí výnimku má tak lepšiu informáciu o tom, čo sa stalo, pretože má vlastne popisnú výnimku aj pôvodnú výnimku v jednom. Konštruktory triedy PocitadloException si vygenerujeme cez Source > Generate Constructors From Superclass.

public class Pocitadlo {
          public List<Integer> nacitajCisla(File f) throws PocitadloException {
                List<Integer> cisla = new ArrayList<Integer>();
                Scanner citac = null;
                try      {     
                        citac = new Scanner(f);
                        while(citac.hasNextInt()) {
                                cisla.add(citac.nextInt());
                        }
                } catch (FileNotFoundException e) {
                        throw new PocitadloException("Čísla nenačítateľné", e);
                } finally {
                        if (citac!=null) citac.close();
                }
                return cisla;
          }
        }

Prebaľovanie výnimiek má zmysel aj preto, že používateľ cudzieho kódu môže mať s nízkoúrovňovými výnimkami, vybublanými z vnútra programu, problém. Má pochopiť, čo sa stalo, a hlavne čo bolo príčinou a ako to treba riešiť. Predstavte si, že vám cudzí modul vyhodí napr. výnimku NumberFormatException a vy ste pritom volali metódu na poslanie matice na server a nevidíte súvis.

Príklad správneho prebaľovania výnimiek bublacúcich z vnútra nejakého projektu:

  • VSúboreChýbaPremennáException: početBalíčkov
  • KonfiguráciaNemáPremennúException: početBalíčkov
  • ModulKonfiguráciaOutOfDateException
  • StarrýJarSúborException: konfiguracia.jar

Keby sa k vám dostala prvá výnimka, neviete čo s tým. Ale na základe poslednej už viete, že potrebujete zohnať novší modul konfigurácie.

Čo všetko môžeme vyhadzovať

Už vieme, že v Jave sú dva druhy výnimiek - kontrolované a nekontrolované. Okrem nich môžu metódy vyhadzovať aj chyby.

  • Kontrolované výnimky sa musia buď odchytiť v try-catch blokoch alebo nechať vybublať do volajúcej metódy. Ak sa kontrolované výnimky vyhadzujú, musia sa uviesť v hlavičke za throws.
  • Nekontrolované výnimky sú potomkovia triedy RuntimeException. Nemusia odchytiť a nemusia sa uvádzať v throws.
  • Chyby sú potomkovia triedy Error. Podobne ako nekontrolované výnimky sa nemusia odchytávať a uvádzať v throws.

Chyby (Errors) predstavujú taký stav systému, z ktorého sa aplikácia nemá šancu zotaviť. Väčšinou ju vyhadzujú nízkoúrovňové metódy z vnútra Javy (bežný programátor ich nevyhadzuje), a predstavujú fatálne chyby, z ktorých je nemožné sa zotaviť. Príkladmi sú OutOfMemoryError -- indikácia vyčerpanej pamäte alebo VirtualMachineError, keď virtuálny stroj nevie púšťať Java programy.

Otázka, kedy použiť kontrolovanú a kedy nekontrolovanú výnimku, nie je jednoduchá. Sú vývojári, ktorí nepoužívajú kontrolované výnimky vôbec (iné OOP jazyky kontrolované výnimky nemajú). Nekontrolované výnimky majú tú nevýhodu, že zvádzajú k lenivosti. Dobrý programátor, keď hádže nekontrolované výnimky, uvádza ich, napriek tomu, že to nie je nutné, aj do dokumentácie aj do throws.

Výhodou nekontrolovaných výnimiek je, že ak sme si istí, že sme dodržali kontrakt a nenastanú výnimky identifikujúce nedodržanie kontraktu, nemusíme ich odchytávať v try-catch blokoch. Ak sa naviac vyhadzované nekontrolované výnimky uvedú do dokumentácie a do throws, nenastane situácia, že nejaká neočakávaná (nezdokumentovaná) výnimka vám vybuble z vnútra cudzieho kódu. Takéto situácie sú veľmi nepríjemné a spôsobujú často mnohé hodiny tápania a hľadania príčiny. Uvedenie v throws je to najmenej, čo môžeme urobiť, aj keby sme dokumentáciu ešte nemali dotiahnutú. Je to aspoň informácia o možnom probléme. Naviac vďaka throws vám Eclipse vie sám generovať try-catch bloky.

Výnimky pri prekrývaní metód

Pri kombinácii výnimiek a dedičnosti si treba dávať pozor. Platí totiž, že prekrývajúce metódy môžu vyhadzovať iba také výnimky ako ich náprotivky v rodičovských triedach. Inými slovami, ak chceme v oddedenej triede v nejakej metóde vyhadzovať viac výnimiek ako vyhadzuje metóda ktorú prekrývame, musíme rozšíriť zoznam výnimiek v throws pôvodnej rodičovskej metódy.

Napríklad ak metóda dajUmiestnenie() triedy Film vyhadzuje výnimku FilmException:

public class Film {
   public void dajUmiestnenie() throws FilmException {
      ...
   }
}

tak v metóde dajUmiestnenie() triedy FilmNaDvd, ktorá dedí od triedy Film môžeme vyhadzovať iba FilmException, jej potomkov alebo nič. Napríklad nasledujúci kód by nefungoval:

public class FilmNaDvd extends Film {
   public void dajUmiestnenie() throws FilmException, DvdException { // Exception DvdException is not compatible with throws clause in Film.dajUmiestnenie()
      ...
   }
}

Možné opravy:

public class Film {
   public void dajUmiestnenie() throws FilmException,DvdException {
      ...
   }
}

Táto oprava je však pomerne nešťastná. Všeobecný Film by totiž nemal mať do činenia s výnimkami, ktoré zväzujú triedu Filmu s jej potomkom DVD.

public class Film {
   public void dajUmiestnenie() throws Exception { //alebo ina spolocna nadtrieda tried FilmException a DvdException
      ...
   }
}

I v tomto prípade je oprava netradičná. Hádzanie všeobecnej výnimky Exception sme totiž vyššie v texte vyhlásili za nevhodné.

V tomto konkrétnom príklade je ideálne, keď DVDException oddedí od FilmException, a metóda dajUmiestnenie() bude môcť vyhodiť DVDException bez toho, aby narušila podmienku v rodičovi.

Odchytávanie viacerých výnimiek

Už vieme že odchytávanie výnimiek sa deje v catch blokoch. Tiež vieme, že catch blokov môže byť viac. Fungovanie viacerych blokov je založené na tom, že výnimky odchytí prvý catch blok, ktorý vie prijať vyhodenú výnimku. V každom catch bloku sa uvádza ako parameter nejaká premenná, ktorej typ určuje, či sa výnimka odchytí alebo nie.

Na to, aby sme vedeli, ktoré výnimky budú odchytené ktorým catch blokom, potrebujeme poznať hierarchiu dedenia vyhadzovateľných (Throwable) objektov.

Už vieme, že z premennej nadradenej triedy vieme referencovať ľubovoľný objekt potomka tejto triedy. Tento vzťah platí aj vo svete výnimiek. To znamená, že ak v prvom catch bloku odchytávame výnimku do premennej typu Exception tak odchytíme ľubovoľnú výnimku. Jediné čo by sa neodchytilo by boli chyby. Z toho vyplýva, že keby sme v ďalších catch blokoch odchytávali napríklad výnimku FileNotFoundException, ktorá je potomkom triedy Exception tak, by sme ju týmto catch blokom nikdy neodchytili, pretože by bolá odchytená prvým catch blokom s typom Exception. catch bloky teda uvádzame vždy od najkonkrétnejších po najvšeobecnejšie.

Príklad správneho poradia odchytávania:

try {   
        ...
} catch (FileNotFoundException e) {
        System.out.println("Nenašiel som súbor");
} catch (IOException e) {
        System.out.println("Vstupno-výstupná chyba");
} catch (Exception e) {
        System.out.println("Nastala nejaká výnimka, napríklad aj ľubovoľná RuntimeException");
}

Časté chyby pri používaní výnimiek

  • Chyba č.1 - Odchytíme výnimku a nespravíme nič.
try {   
        citac = new Scanner(f);
} catch (FileNotFoundException e) {}
  • Dôsledok chyby č.1 - Program obvykle spadne na úplne inom mieste a nik netuší prečo sa to stalo
  • Chyba č.2 - Banality riešime výnimkami. V prípade, že sa dá výnimkam predchádzať overovaním jednoduchých podmienok, je vhodnejšie výnimkam zabrániť, ako ich potom odchytávať. V nasledujúcom príklade je príklad na zlé použitie výnimiek, kde neprechádzame pole cez for cyklus ale počkáme si pokiaľ nezačneme čítať neexistujúci prvok poľa.
try {   
   int i = 0;
   while (true) {
      pole[i+1] = 2 * pole[i];
      i++;
   }
} catch (ArrayIndexOutOfBoundsException e) {}
  • Dôsledok chyby č.2 - kód je veľmi ťažko čitateľný
  • Chyba č.3 - nerobíme zmysluplné mená výnimiek, alebo proste vyhadzujeme rovno výnimku typu Exception.
void metóda() throws Exception {
   ...
}
  • Dôsledok chyby č.3 - Používateľ metódy netuší, čo sa môže stať a nevie sa na to pripraviť. Aby bol schopný sa pripraviť, musí čítať cudzie zdrojáky.
  • Chyba č.4 - Necháme vybublať výnimky príliš vysoko v zmysle hesla "nechce sa mi písať try-catch bloky, throws je rýchlejšie napísané"
void upečKoláč() throws IOException, SQLException {
   ...
}
  • Dôsledok chyby č.4 - Výnimky sa majú odchytiť na mieste, kde sa s tým vieme vysporiadať. Vo vysokoúrovňových metódach netušíme, čo sa mohlo na nižších úrovniach pokaziť a ani nie sme v stave to riešiť. Ak sa s tým nevedia vysporiadať nižšie vrstvy, treba takéto nízkoúrovňové výnimky prebaliť do zrozumiteľnejších, aby bolo jasné, ako to z vysokej úrovne vyriešiť.