1. diel - Výnimky v Jave
V tejto sekcii sa budeme venovať práci so súbormi. Než však začneme zapisovať a čítať, mali by sme vyriešiť, ako ošetriť chybové stavy programu, ktoré pri práci so súbormi nastanú často.
V našom programe môže často dôjsť k chybe. Tým nemyslím chybe z dôvodu, že bol program funkčne zle napísaný, takýchto chýb sme schopní sa dobre vyvarovať. Všeobecne ide najmä o chyby, ktoré zapríčinili tzv. vstupno/výstupné operácie. V anglickej literatúre sa hovorí o input/output alebo skrátene o IO. Ide napr. o vstup užívateľa z konzoly, zo súboru, výstup do súboru, na tlačiareň a podobne. V zásade platí, že tu figuruje užívateľ, ktorý nám môže zadať nezmyselný vstup, neexistujúci alebo nevalidný súbor, odpojiť tlačiareň a podobne. My však nenecháme program spadnúť s chybou, naopak budeme zraniteľné miesta v programe ošetrovať a na danú skutočnosť používateľa upozorníme.
Aktívne ošetrenie chýb
Prvú možnosť ošetrenia chýb nazývame aktívna. V programe nájdeme
všetky zraniteľné miesta a ošetríme ich podmienkami. Ako učebnicový
príklad sa spravidla používa delenie nulou. Predstavme si program, ktorý
používa triedu Mathematics, ktorá má metódu
divide(). Trieda by mohla vyzerať napr. takto:
class Mathematics { public static int divide(int a, int b) { return a / b; } // ... }
Teraz triedu použijeme takýmto spôsobom:
System.out.println("Enter the dividend and the divisor to calculate the result:"); int a = Integer.parseInt(scanner.nextLine()); int b = Integer.parseInt(scanner.nextLine()); System.out.println(Mathematics.divide(a, b));
Pokiaľ teraz programu užívateľ zadá čísla 12 a
0, program spadne s chybou, pretože nulou nie je možné deliť.
Aktívne chybu ošetríme jednoduchou podmienkou v programe:
System.out.println("Enter the dividend and the divisor to calculate the result:"); int a = Integer.parseInt(scanner.nextLine()); int b = Integer.parseInt(scanner.nextLine()); if (b != 0) { System.out.println(Mathematics.divide(a, b)); } else { System.out.println("You cannot divide by zero."); }
Teraz si musíme pri každom použití metódy strážiť, či do druhého parametra nevkladáme nulu. Predstavte si, že by metóda brala parametrov 10 a používali sme ju v programe niekoľkokrát. Určite je veľmi zložité takto ošetrovať všetky vstupy v každom prípade.
Riešením by mohlo byť vložiť kontrolu priamo do metódy. Máme tu však
nový problém: Akú hodnotu vrátime, keď bude druhý parameter nulový?
Potrebujeme hodnotu, pomocou ktorej spoznáme, že výpočet neprebehol
správne. To je však problém, pretože keď zvolíme napr. nulu, nepoznáme,
či je 0/12 chybný výpočet alebo nie. Nevieme, či
0 predstavuje výsledok alebo chybu. Nepomôžeme si ani
zápornými číslami. Mohli by sme použiť nullovateľný typ
Integer, ale ide to aj jednoduchšie a správnejšie. Parsovanie
hodnôt je druhý klasický príklad zraniteľného vstupu od užívateľa.
Ďalšie sú súborové operácie, kde súbor nemusí existovať, nemusíme naň
mať práva, môže sa s ním práve pracovať a podobne.
Pasívne ošetrenie chýb
Keď je operácia zložitejšia a bolo by príliš náročné ošetrovať
všetky možné chybové stavy, nastupujú výnimky, tzv.
pasívne ošetrenie chýb. Nás totiž vôbec nemusí zaujímať
vnútorná logika v metóde, ktorú voláme. Pokúsime sa nebezpečnú časť
kódu spustiť v "chránenom režime". Tento režim je nepatrne
pomalší a líši sa tým, že pokiaľ dôjde k chybe, máme možnosť ju
odchytiť a zabrániť pádu programu. O chybe tu hovoríme ako o
výnimke. Využívame na to tzv.
try-catch bloky:
try { // code that can throw an exception } catch (Exception e) { // code that is executed when an exception is caught }
Do bloku try umiestnime nebezpečnú časť
kódu. Pokiaľ nastane v bloku try chyba, jeho
vykonávanie sa preruší a program prejde do bloku catch. Pokiaľ
všetko prebehne v poriadku, try sa vykoná celý a
catch sa preskočí. Vyskúšajme si situáciu na našom
predchádzajúcom príklade:
try { System.out.println(Mathematics.divide(a, b)); } catch (Exception e) { System.out.println("Error occurred."); }
Kód je jednoduchší v tom, že nemusíme ošetrovať všetky zraniteľné
miesta a premýšľať, čo všetko by sa mohlo pokaziť. Nebezpečný kód iba
obalíme blokom try a všetky chyby sa zachytia v
catch. Samozrejme do try-catch bloku
umiestnime len to nevyhnutné, nie celý program 
Teraz teda už vieme, ako ošetriť situácie, keď používateľ zadáva nejaký vstup, ktorý by mohol vyvolať chybu. Nemusí ísť len o súborové operácie, výnimky majú rozsiahle využitie. Dokážeme náš program napísať tak, aby sa nedal používateľom jednoducho zhodiť.
V minulosti sme už bloky try a catch použili
niekoľkokrát, bolo to najmä pri parsovaní dátumu a času.
Použitie výnimiek pri práci so súbormi
Ako už bolo povedané, súborové operácie môžu vyvolať mnoho výnimiek,
preto so súbormi vždy pracujeme v try-catch
bloku. Existuje aj niekoľko ďalších konštrukcií, ktoré pri
výnimkách môžeme využívať.
Finally
Do try-catch bloku môžeme pridať ešte 3. blok a
to finally. Ten sa spustí vždy, či už k výnimke došlo
alebo nie. Predstavte si nasledujúcu metódu na uloženie nastavení.
Metódy na obsluhu súboru budú vymyslené:
public void saveSettings() { try { openFile("file.dat"); writeToFile(settings); } catch (Exception e) { System.out.println("An error has occurred."); } if (fileIsOpened()) { closeFile(); } }
Metóda sa súbor pokúsi otvoriť a zapísať do neho objekt nastavenia. Pri
chybe vypíše hlášku do konzoly. Otvorený súbor musíme opäť uzavrieť.
Vypisovať chyby priamo v metóde je však nepekn a to ostatné už vieme.
Metódy a objekty by vo všeobecnosti mali vykonávať len logiku a komunikáciu
s užívateľom zabezpečuje ten, kto ich volá. Dajme teda metóde návratovú
hodnotu boolean a vracajme true/false
podľa toho, či sa operácia podarila alebo nie:
public boolean saveSettings() { try { openFile(); writeToFile(); return true; } catch (Exception e) { return false; } if (fileIsOpened()) { closeFile(); } }
Na prvý pohľad to vyzerá, že sa súbor vždy uzavrie. Celý kód je však
v nejakej metóde, v ktorej voláme return. Ako vieme,
return ukončí metódu a nič za ním sa už nevykoná.
Súbor by tu vždy zostal otvorený a uzavretie by sa už nevykonalo.
Následkom by bolo, že by bol súbor potom neprístupný. Pokiaľ
vložíme zatvorenie súboru do bloku finally, vykoná sa
vždy. Java si pamätá, že blok try-catch
obsahoval finally a po opustení sekcie catch zavolá
finally:
public boolean saveSettings() { try { openFile(); writeToFile(); return true; } catch (Exception e) { return false; } finally { if (fileIsOpened()) { closeFile(); } } }
Finally sa teda používa pri výnimkách na upratovacie
práce, dochádza tu k zatváraniu súborov, uvoľňovaniu pamäte a
podobne.
Pre každý typ súborov poskytuje Java triedu zapisovača a čítača (writer a reader). Metóda na uloženie napr. nastavenia by v Jave reálne vyzerala asi takto:
public boolean saveSettings() { FileWriter w = null; try { w = new FileWriter("file.dat"); w.write(object); return true; } catch (Exception e) { return false; } finally { if (w != null) { w.close(); } } }
Takto sa so súbormi naozaj pracuje, vymyslená je iba trieda. Do inštancie
zapisovača umiestnime najprv null. Potom, už v bloku
try, skúsime vytvoriť zapisovač na súbore file.dat
a zapísať nejaký objekt. Pokiaľ sa všetko podarí, vrátime
true (samozrejme sa potom ešte zavolá blok finally).
Operácia môže zlyhať z dvoch dôvodov. Buď sa do súboru nepodarí
zapísať alebo sa nám súbor pre zápis ani nepodarí otvoriť. Výnimku v
každom prípade zachytíme a vrátime false, z čoho potom vieme,
že sa metóda uloženia nepodarila. Blok finally zatvorí súbor,
ktorý zapisovač otvoril. Keďže sa ale otvorenie nemuselo podariť, musíme
sa najskôr pozrieť, či sa zapisovač vôbec vytvoril, aby sme mali čo
zatvárať. Metódu by sme volali napr. takto:
if (!saveSettings()) { System.out.println("Unable to save settings."); }
Blok catch môžeme vynechať a nechať metódu, aby výnimku
pokojne vyvolala. Budeme počítať s tým, že sa s výnimkou vysporiada ten,
kto metódu zavolal, nie metóda sama. Je to tak lepšie, ušetríme návratovú
hodnotu metódy (ktorú je potom možné použiť na niečo iné) a kód sa nám
zjednoduší:
public void saveSettings() throws Exception { FileWriter w = null; try { w = new FileWriter("file.dat"); w.write(object); } finally { if (w != null) { w.close(); } } }
Všimnite si, že sme k hlavičke metódy pridali
throws Exception. Tým Jave hovoríme, že vyvoláva výnimku a
vďaka tomu nám prestane nadávať za chýbajúci catch blok.
Metódu by sme teraz volali takto:
try { saveSettings(); } catch (Exception e) { System.out.println("Unable to save settings."); }
Teraz si ukážeme, ako celú situáciu ešte viac zjednodušiť. Použijeme
konštrukciu try-with-resources.
Try-with-resources
Java (od verzie 7) umožňuje značne zjednodušiť prácu s inštanciami
tried na čítanie a zápis do súborov. Vyššie uvedený blok môžeme
zapísať pomocou notácie try-with-resources,
ktorá nahrádza bloky try a finally.
Obrovskou výhodou je, že blok finally Java vygeneruje sama a sama
aj zaistí, aby daná inštancia readera alebo writera súbor uzavrela. Metóda
saveSettings() by teda vyzerala s pomocou
try-with-resources takto:
public void saveSettings() throws Exception try (FileWriter w = new FileWriter("file.dat")) { w.write(object); } }
Vidíme, že sa kód extrémne zjednodušil, aj keď robí v podstate to
isté. Pri volaní metódy opäť použijeme blok
try-catch. Nezabudnite, že
try-with-resources nahrádza iba
try-finally, nie catch!.
Metódu, v ktorej sa používa try-with-resources, musíme rovnako
volať v bloku try-catch.
Teraz sme sa dostali presne tam, kam som chcel. Na všetky manipulácie so
súbormi totiž budeme v nasledujúcich tutoriáloch používať konštrukciu
try-with-resources. Kód bude jednoduchší a nikdy sa nám
nestane, že by sme súbor zabudli zavrieť.
K výnimkám sa ešte raz vrátime a ukážeme si, ako odchytávať len
niektoré typy výnimiek, ktoré hotové triedy výnimiek môžeme v našich
programoch používať a tiež, ako vytvoriť vlastnú výnimku. Teraz som však
chcel len vysvetliť potrebné minimum na prácu so súbormi a nie vám
zbytočne pliesť hlavu zložitými konštrukciami 
V ďalšom tutoriáli Java, Úvod do práce so súbormi v Jave, si ukážeme, ako fungujú prístupové
práva v operačných systémoch a ako sa vytvárajú inštancie tried
Path a File.

David sa informačné technológie naučil na