Výnimky (odchytávanie)

<< Dvojrozmerné polia | Obsah | Práca so súbormi a adresármi operačného systému >>

Výnimky sú riadiaci mechanizmus, ktorý sa používa, keď nejaká operácia nedokáže prebehnúť štandardným spôsobom alebo nedokáže vrátiť očakávanú hodnotu. Uveďme si pár príkladov:

  • Metóda, ktorá počíta priemer prvkov číselného poľa dostane na vstup pole dĺžky nula. Keďže vzorec na priemer je podiel súčtu prvkov a počtu prvkov, pokúsi sa deliť nulou, čím nastane nečakaná situácia alebo výnimočný stav.
  • Vytvárate nové pole Turtle[] korytnacky = new Turtle[-5]; a program sa dozvie, že vytvárate pole dĺžky -5, ale také pole vytvoriť nevie.
  • Idete zapísať hodnotu na 15-ty prvok 10-prvkového poľa.
  • Chcete pridať hodnotu do poľa, ktoré ste nevytvorili cez new

V starších programovacích jazykoch vám takáto situácia spôsobí okamžité ukončenie alebo zamrznutie celej aplikácie, čo je veľmi nepríjemné. Často program iba vypíše ničnehovoriacu hlášku ako "segmentation fault" alebo "fatal error", ktorá mu nepovie ani to, kde a ani aká výnimočná situácia nastala.

Aké výhody oproti tomuto má výnimka? Za prvé, výnimka má svoj názov, ktorý nám (po preklade z angličtiny) naznačí, aká udalosť sa stala.

Akých výnimiek by sme sa dočkali v prípade predchádzajúcich príkladov?

  • java.lang.ArithmeticException: / by zero - výnimka pri aritmerickej operácii: delenie nulou
  • java.lang.NegativeArraySizeException - výnimka zápornej veľkosti poľa
  • java.lang.ArrayIndexOutOfBoundsException: 15 - výnimka indexu poľa mimo hranice poľa: 15-ty prvok
  • java.lang.NullPointerException - výnimka ktorá sa vyhodí ak namiesto objektu máme v premennej null a chceme cez túto premennú volať metódu.

Vytvorme si príklad metódy korytnačky, ktorá vráti true ak priemer prvých k prvkov poľa na vstupe je väčší ako 0, inak vráti false. Chceme aby táto metóda vždy vrátila nejakú hodnotu, to znamená, že by mala byť odolná voči všetkým zlým vstupom. Napíšme si najprv metódu, ktorá verí, že všetky vstupy budú pekné a nespôsobia výnimočný stav:

public boolean kladnyPriemer(int[] pole, int k) {
  int priemer = 0;
  int sucet = 0;
  for (int i = 0; i < k; i++) {
    sucet += pole[i];
  }
  priemer = sucet / k;
  if (priemer > 0) {
    return true;
  }
  else {
    return false;
  }
}

Čo by sa stalo v prípade, ak v premennej pole bude null? Telo for cyklu vyhodí výnimku NullPointerException, lebo chceme pristupovať do poľa, ktoré neexistuje. Vyrobíme si podmienku, ktoré nám zabezpečí, že ak nastane tento prípad, tak sa v tomto prípade korektne vráti ako výstupná hodnota metódy false. (to znamená, že priemer prvých k prvkov nie je väčší ako nula).

public boolean kladnyPriemer(int[] pole, int k) {
  if (pole == null)
     return false;
  int priemer = 0;
  int sucet = 0;
  try {
    for (int i = 0; i < k; i++) {
      sucet += pole[i];
    }
    priemer = sucet / k;
    if (priemer > 0) {
      return true;
    }
    else {
      return false;
    }
  } catch (NullPointerException e) {
    return false; // priemer nie je vacsi ako nula lebo priemer neexistuje
  }
}

null v premennej pole však nie je jediné, čo môže postihnúť bezchybné fungovanie metódy.

Premenná k môže mať hodnotu nula. V tom prípade sa for cyklus nevykoná ani raz (lebo 0 < 0 je false) a program sa bude snažiť deliť nulou teda vyhodí ArithmeticException. Dodáme teda ďalšiu podmienku.

Ďalšia chyba vstupu môže byť, že pole môže mať dĺžku nula. Zasa pridáme podmienku.

Další problém môže byť, keď k je väčšie ako dĺžka poľa. V tomto prípade program vyhodí výnimku ArrayIndexOutOfBoundsException pri snahe pripočítať prvok poľa ktorý je prvý za posledným prvkom poľa. Zasa pridáme ďalšiu podmienku ktorá nám opraví k na dĺžku poľa.

Výsledný kód vyzerá nasledovne:

public boolean kladnyPriemer(int[] pole, int k) {
  if (pole == null || pole.length==0 || k<=0)
     return false;
  if (k > pole.length)
     k = pole.length;
  int priemer = 0;
  int sucet = 0;
  for (int i = 0; i < k; i++) {
    sucet += pole[i];
  }
  priemer = sucet / k;
  if (priemer > 0) {
    return true;
  }
  else {
    return false;
  }
}

Vyrobme si teraz nový program, ktorý bude vracať súčet čísiel, ktoré sa nachádzajú v reťazci. Budeme potrebovať reťazec rozdeliť na slová a každé slovo sa pokúsiť pretransformovať na int. Na to použijeme funkciu Integer.parseInt(...).

String retazec = "235";
int cislo = new Integer.parseInt(reťazec); //do premennej cislo sa pridaí číslo 235

Samotný kód metódy vyzerá nasledovne.

public int sucetCisiel(String vstup) {
  int sucet = 0;
  vstup = vstup + " ";                         // aby sme zachytili aj posledné slovo
  String cislo = "";
  for(int i = 0; i < vstup.length(); i++) {
     if(vstup.charAt(i)==' ') {                // prišli sam na koniec slova
        int hodnota = Integer.parseInt(cislo);
        sucet = sucet + hodnota;
        cislo = "";                            // pripravíme si reťazec pre nové číslo
     } else {                                  // sme na cifre, pridáme ju na koniec čísla
        cislo = cislo + vstup.charAt(i);
     }
  }
  return sucet;
}

Tento program však môže pri zlom vstupe tiež vyhodiť výnimku, konkrétne NumberFormatException, a to vtedy, ak budeme mať viac medier medzi číslami, alebo ak použijeme vo vstupnom reťazci aj iné znaky ako cifry. V takomto prípade si už jednou podmienkou nepomôžeme a museli by sme robiť kadejaké testy, prechádzať reťazec a podobne.

Veľká výhoda výnimky je, že ju môžeme odchytiť, a ak je to možné, vysporiadať sa s týmto stavom v programe. Napríklad môžeme poprosiť používateľa nech zadá iný vstup alebo mu minimálne namiesto výnimky vypísať nejakú hlášku v ľudskej reči.

Odchytávanie výnimky robíme tak, že najprv si ohraničíme oblasť, kde očakávame, že by mohla nastať výnimka s ktorou sa chceme vysporiadať do bloku try, a za ním napíšeme blok catch ktorý sa aktivuje, iba ak nastala výnimka. Ak výnimka nenastane prebehnú všetky príkazy v bloku try akoby tam blok try ani nebol. Blokov catch môže byť jeden alebo viac - pre každý typ výnimky jeden. Syntax je teda nasledovná:

try {
  //príkazy, ktoré ak vyhodia výnimku skočíme na blok catch
} catch (typ_výnimky1 e) {
  vysporiadanie sa s prvým typom výnimky
} catch (typ_výnimky2 e) {
  vysporiadanie sa s druhým typom výnimky
}

Čo teda urobíme v našom príklade? Ak sa nám nepodarí stransformovať číslo v reťazci na číslo typu int, telo for cyklu vyhodí výnimku NumberFormatException. Vyrobíme si teda bloky try a catch, ktoré nám zabezpečia kód tým, že sa takáto výnimka odchytí, dané slovo budeme ignorovať a používateľovi vypíšeme hlášku, že mal nejaké chyby vo vstupe.

public int sucetCisiel(String vstup) {
  int sucet = 0;
  vstup = vstup + " ";
  String cislo = "";
  for(int i = 0; i < vstup.length(); i++) {
     if(vstup.charAt(i)==' ') {
        try {
            int hodnota = Integer.parseInt(cislo);
            sucet = sucet + hodnota;
        } catch (NumberFormatException e) {
            System.err.println("Slovo \"" + cislo + "\" neviem transformovať! Ignorujem ho.");
        }
        cislo = "";
     } else {
        cislo = cislo + vstup.charAt(i);
     }
  }
  return sucet;
}

Ak nastane výnimka a nemáme príslušný catch blok, ktorý by odchytával správnu výnimku, program skončí s chybou. Aby to nebolo také jednoduché, tak za blokmi catch môže nasledovať ešte jeden blok finally. Podľa definície je to blok, ktorý sa vykoná vždy bez ohľadu na to, či výnimka vyhodená vo vnútri try bloku bola odchytená alebo nie. Používa sa obvykle na korektné ukončenie sieťového spojenia, na korektné zatvorenie súboru alebo odhlásenie z databázy. Ak výnimka nebola odchytená niektorým catch blokom, tak sa ešte vykoná blok finally a až potom program skončí s chybou.

try {
  //príkazy, ktoré ak vyhodia výnimku skočíme na blok catch
} catch (typ_výnimky1 e) {
  vysporiadanie sa s prvým typom výnimky
} catch (typ_výnimky2 e) {
  vysporiadanie sa s druhým typom výnimky
} finally {
  // príkazy ktoré sa vykonajú bez ohľadu na to, či bola výnimka odchytená niektorým catch blokom
}

K výnimkám je ešte potrebné spomenúť, že Java pozná dva typy výnimiek:

  • RuntimeException alebo behové výnimky, ktoré sa obvykle dajú opraviť zmenou programu. Doteraz spomínané výnimky sú všetky tohto typu.
  • Exception alebo bežné výnimky, ktoré sú obvykle spôsobené chybou používateľa a zmenou programu sa im nevieme vyhnúť. Tieto výnimky sa musia odchytávať. Eclipse na to upozorňuje. S jednou takouto výnimkou, FileNotFoundException, sa stretneme pri otváraní súborov.