Gyakorlati alapok II.
A try-catch-finally blokkok
A bevezető fejezet már megadta a romantikus lepkevadász-hangulatot: az éterben lebegő kis programozási hibákat, bugokat el kell kapnunk, hogy ne fertőzze meg és döntse össze az éppen futó Java-programunkat. A lepkeháló ebben az esetben a try-catch-finally blokkok masszív szerkezete lesz, amelyet legelső példánkban a nullával való osztás hibájának lekezelésére használunk fel.
Először ismételjük meg ezt a durva programozási hibát:
public class Main {
public static void main(String[] args) {
System.out.println(4 / 0);
}
}
Végeredmény (hibaüzenet):
Exception in thread "main" java.lang.ArithmeticException:
/ by zero
at Main.main(Main.java:152)
Az olvashatóság és a továbbiak érdekében egy kicsit manikűrözzük ki a kódot, bár funkcionálisan ugyanaz lesz:
public class Main {
public static void main(String[] args) {
int a = 4;
int b = 0;
double c = a / b;
System.out.println(c);
}
}
Végeredmény (hibaüzenet):
Exception in thread "main" java.lang.ArithmeticException:
/ by zero
at Main.main(Main.java:152)
Most tegyük be a try-catch blokkokat a kódba:
-
a try blokkba tesszük a problémásnak ítélt műveletet (try = megpróbálom),
-
a catch blokkba tesszük az alapszintű hibakezelést (catch = elkapom), amely ebben az esetben egy egyszerű hibaüzenet lesz (System.out.println("Hiba: nullával való osztás!");). Egyúttal paramétereként definiáljuk a lehetséges kivételfajtát is (itt: ArithmeticException ae). Ha a catch paramétereként rossz kivételfajtát definiálunk (mondjuk itt ArithmeticException ae helyett FileNotFoundException fnfe), akkor szintén fordítási hibaüzenetet kapunk.
Most nézzük meg a futtatható Java-kódot!
public class Main {
public static void main(String[] args) {
int a = 4;
int b = 0;
double c = 0;
try{
c = a / b;
System.out.println(c);
}
catch(ArithmeticException ae){
System.out.println("Hiba: nullával
való osztás!");
}
}
}
Végeredmény:
Hiba: nullával való osztás!
A változás első pillantásra, hogy piros hibaüzenet helyett feketét kaptunk, azonban ez nagy változás, ugyanis azt jelzi: a hibát elkaptuk a catch blokkban, nem dőlt össze a program. Azaz a program képes továbbfutni, bár a kérdéses művelet végeredményben nem került végrehajtásra.
Voltaképpen még a catch hibaüzenete is elhagyható, ekkor látszólag nem történik semmi:
public class Main {
public static void main(String[] args) {
int a = 4;
int b = 0;
double c = 0;
try{
c = a / b;
System.out.println(c);
}
catch(ArithmeticException ae){}
}
}
Végeredmény:
semmi
A semmi azért mégis valami, hiszen legalább nem dőlt össze az alkalmazás, ám az igaz, hogy valójában csak a hibás művelet következményeit akadályoztuk meg, más egyebet még nem tettünk. Mivel ebben a kódban a kért művelet (azaz a nullával való osztás) semmiféleképpen sem végezhető el (ezt a JVM mindig le fogja csapni), ezért mást most nem tudunk tenni, mint a felhasználót tájékoztatjuk erről és esetleg bezárjuk a futó alkalmazást (System.exit(0);).
Ezt vagy ehhez hasonló lezáró-korrigáló műveleteket a finally blokkban tehetjük meg (finally = legvégül, ezt hajtsd végre), amely tehát a hibás művelet következményeinek végső elsimítására való, ilyen lehet például a lefoglalt erőforrások felszabadítása.
public class Main {
public static void main(String[] args) {
int a = 4;
int b = 0;
double c = 0;
try{
c = a / b;
System.out.println(c);
}
catch(ArithmeticException ae){
System.out.println("Hiba: nullával
való osztás!");
}
finally{
System.out.println("A kért művelet
nem végezhető el!");
System.exit(0);
}
}
}
Végeredmény:
Hiba: nullával való osztás!
A kért művelet nem végezhető el!
Természetesen a hibák döntő többsége a programozók rutinja és az ellenőrzött kivételkezelés miatt legtöbbször rejtettebben, alattomosabban jelentkezik, ekkor a programozónak első körben fogalma sincs, hogy mitől állt le a program. A Java-nyelv azonban rendkívül magas szinten támogatja a hibadetektálást és egyéb kisegítő műveleteit. Például sok programozó automatizálja a hibaüzenetek elkapását: a kötelező hibalekezelésen felül a rendszer által generált hibaüzeneteket külön log-fájlba irányítja (Apache Log4j), ahol további szűrési és irányítási lehetőségekre van lehetőség. De mivel kivételkezeléskor egy önálló hibadetektáló objektum jön létre, lényegében azt teszünk vele, amit csak akarunk. (Illetve a következő fejezetben fogjuk megemlíteni a throw utasítás gyakorlati hasznát is.)
Például nézzük meg a hibaértesítés alapszintű implementálását a printStackTrace() metódus segítségével. Itt csak kiírjuk a konzolra, de eredményeit valójában bárhová elküldhetjük, eltárolhatjuk.
public class Main {
public static void main(String[] args) {
int a = 4;
int b = 0;
double c = 0;
try{
c = a / b;
System.out.println(c);
}
catch(ArithmeticException ae){
System.out.println("Hiba: nullával
való osztás!");
ae.printStackTrace();
}
finally{
System.out.println("A kért művelet
nem végezhető el!");
System.exit(0);
}
}
}
Végeredmény:
Hiba: nullával való osztás!
java.lang.ArithmeticException: / by zero
at Main.main(Main.java:224)
A kért művelet nem végezhető el!
Most pedig nézzünk meg egy kissé összetettebb problémát, amelyben nem is 1, hanem 2 catch ág szerepel. Azonban jeleznem kell, hogy ez most kissé nehezebb példa lesz, hiszen a honlapon belül még nem foglalkoztunk fájlkezeléssel.
Egy erőforrás körüli műveletsorozat többféle hibát is produkálhat. Adott például egy távoli szerveren lévő .txt kiterjesztésű szövegfájl, amit a Java-alkalmazásnak el kell érnie, majd feldolgozás céljából meg kell nyitnia. Itt már kétféle kivétel keletkezhet:
-
nincsen vagy megszakad a hálózati adatkapcsolat a szerverrel,
-
valamilyen ok miatt sikertelen a fájl megnyitása.
A felmerült problémát a következő módon modellezzük le:
-
hozzunk létre egy adott szövegű és című, .txt kiterjesztésű állományt valamelyik tetszőleges meghajtónkon.
-
A .txt kiterjesztésű állomány teljes elérési útját illesszük be az állományolvasó objektum paramétereként (itt: reader = new FileReader("D:\\Kertész leszek.txt");). Ügyeljünk a kettős backslash-re \\ és a pontos címmegadásra!
Ha a fentieket korrekten adtuk meg, akkor az alábbi futtatható Java-kód képes lesz megjeleníteni az állomány szövegtartalmát, József Attila Kertész leszek című versét:
import java.io.*;
public class Main {
public static void main(String[] args) {
FileReader reader = null;
try {
reader = new FileReader("D:\\Kertész leszek.txt");
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
while(line != null) {
System.out.println(line);
line = br.readLine();
}
} catch (FileNotFoundException fnfe) {
System.out.println("A fájl nem található!");
fnfe.printStackTrace();
} catch (IOException ioe) {
ioe.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
}
Végeredmény:
Kertész leszek, fát nevelek,
kelő nappal én is kelek,
nem törődök semmi mással,
csak a beojtott virággal.
Minden beojtott virágom
kedvesem lesz virágáron,
ha csalán lesz, azt se bánom,
igaz lesz majd a virágom.
Tejet iszok és pipázok,
jóhíremre jól vigyázok,
nem ér engem veszedelem,
magamat is elültetem.
Kell ez nagyon, igen nagyon,
napkeleten, napnyugaton -
ha már elpusztul a világ,
legyen a sírjára virág.
Bizonyos fajta kivételeket a rendszeren belül viszonylag könnyű produkálnunk:
-
ha töröljük,
-
ha átnevezzük,
-
ha megnyithatatlanná-olvashatatlanná tesszük a fájlt,
...akkor máris kiváltottunk egy ízléses FileNotFoundException kivételt.
Ezzel párhuzamosan ebben a "zárt" rendszerben viszonylag nehéz kiváltanunk egy IOException jellegű kivételt, bár javaslatom, hogy tesztelés céljából azért a fájl olvasása közben ne csapjuk le azt a meghajtót, amelyen a fájlt éppen tároljuk!
De ha már itt tartunk, ízelítőül nézzük meg, hogy például IOException kivételt milyen problémák okozhatnak:
-
hálózaton elérhető fájl olvasása-írása alatt megszakad a hálózati kapcsolat,
-
helyi fájl olvasása-írása közben az hirtelen elérhetetlenné válik (fenti példa),
-
a fájlolvasó-író adatfolyamot (stream) egy másik folyamat (process) bezárja,
-
nincs engedély a fájlolvasáshoz-íráshoz,
-
stb.
(A fenti ügy kissé bonyolultabb, hiszen FileNotFoundException az IOException leszármazottja, amint ez majd alább, illetve a Kivételfajták című fejezetben részletezésre kerül.)
A fentiekből már kiderült, hogy a try-catch, mint hibaelkapó szubrutinok, valamint a végső megoldás finally blokkja rendkívül szoros mértékben tartoznak össze. Deklarálásukkor sokféle hibát véthetünk, aminek következménye természetesen mindenféle fordítási hibaüzenet lesz, ezért érdemes őket a fenti viszonyaikban megjegyezni és szerkezetileg mindig ugyanúgy deklarálni.
A fentieket összefoglalva:
-
try blokkot követhet 1 vagy több catch blokk és 1 finally blokk,
-
try blokkot követhet vagy catch blokk vagy finally blokk,
-
ha try blokk ellenőrzött kivételt tartalmaz (checked exception), kötelező catch blokkban lekezelni vagy továbbdobását deklarálni (throw), másként fordítási hibát kapunk,
-
több catch blokk esetén hibakezelés szempontjából a speciálistól az egyre általánosabb megoldás felé kell haladnunk (még nem tanultuk, de ekkor a kivételosztályok hierarchiájában az utódosztálytól kell haladnunk az ősosztály felé),
-
nem léteznek try-catch-finally blokkok egymástól függetlenül,
-
finally blokk nem lehet catch blokk előtt,
-
finally blokk mindig lefut, kivéve:
-
ha a try vagy catch blokk System.exit() metódust tartalmaz, mert ekkor a metódus bezárja a futó alkalmazást,
-
ha a finally blokk lefutása előtt összeomlik a JVM vagy az operációs rendszer.
-
A hibák osztályhierarchiába való szervezésének egyéb következményei is vannak. A fenti, fájlkezeléses kód 2 kivételt tartalmaz:
-
FileNotFoundException
-
IOException
Az Exception ág osztályhierarchiájába kissé beleturkálva azonban a következőt vehetjük észre:
java.lang.Throwable
↓
java.lang.Exception
↓
java.io.IOException
↓
java.io.FileNotFoundException
A FileNotFoundException tehát az IOException "gyermeke", leszármazottja (derived class) és fordítva: az IOException a "szülője", az alaposztálya (base class). Mivel az IOException tartalmazza a FileNotFoundException rutinjait is (ám ez fordítva már nem igaz!), ezért többszörös catch blokk esetén, ha az IOException kerül az 1. catch blokkba, akkor a 2. catch blokk FileNotFoundException rutinja soha nem fog lefutni:
public class Main {
public static void main(String[] args) {
FileReader reader = null;
try {
reader = new FileReader("D:\\Kertész leszek.txt");
BufferedReader br = new BufferedReader(reader);
String line = br.readLine();
while(line != null) {
System.out.println(line);
line = br.readLine();
}
} catch (IOException ioe) {
System.out.println("A fájl nem található!");
fnfe.printStackTrace();
} catch (FileNotFoundException fnfe) {
ioe.printStackTrace();
} finally {
try {
if (reader != null) {
reader.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
}
Voltaképpen a problémát a JVM is érzékeli és a következő fordítási hibaüzenetet dobja nekünk...
Exception in thread "main" java.lang.Error: Unresolved
compilation problem:
Unreachable catch block for FileNotFoundException. It is already handled by
the catch block for IOException at Main.main(Main.java:18)
...ami azt jelenti, hogy az osztályhierarchiába szervezett kivételkezelés belső szabályai miatt a FileNotFoundException elérhetetlen, mert IOException képes lekezelni az előbbi kivételeit is, amit kivétel esetén meg is tesz, következésképpen az előbbi felesleges deklaráció.
Hasonlatos ez ahhoz a szituációhoz, amikor csőszerelőt hív a család. Meg is érkezik, otthon pedig apa és kiskorú fia fogadja. Ha nem lenne otthon az apa, a szerelőnek be kéne érnie a fiúval. Ha azonban az apa is otthon tartózkodik, akkor már nem számít a fiú jelenléte, mert a szerelő csakis az apa utasításait fogja követni.