Kategória: Standard Java.
Áttekintés
A kivételkezelés célja az, hogy ha egy metódus valamilyen hiba miatt nem tudja folytatni a futását, akkor ezt jelezni tudja a hívó metódusnak anélkül, hogy a visszatérési értéket fel kellene készíteni erre.
A Java nyelv tartalmaz egy igen jól felépített kivételkezelés rendszert. A metódusok fejlécébe, a név és a paraméterlista után throws kulcsszóval tudjuk felsorolni a vesszővel elválasztott kivételeket, amit az adott metódus dobni tud. A kivétel dobása a következőképpen történik: példányosítani kell a kivétel osztályt (ami normál esetben a beépített Exception vagy annak leszármazottja; erről még lesz szó bővebben), és a throw kulcsszóval tudjuk eldobni, pl. így: throw new Exception("Error");
Azok a metódusok, amelyek azt hívják, amely kivételt dobhat, dönthetnek: vagy tovább engedik, vagy elkapják. Az előző esetben a hívó függvénynek is fel kell sorolnia a fejlécben a lehetséges kivételeket a már bemutatott throws kulcsszó használatával. Az utóbbi estben a függvényhívást egy try … catch struktúrába kell helyezni, ahol a hívás a try utáni blokkba kerül, ami után közvetlenül következnek a lehetséges kivétel elkapások (catch).
Az egész struktúrát egy opcionális finally zárhatja le, ami mindenképpen lefut, akár történt kivétel, akár nem. A finally akkor is lefut, ha a try vagy valamelyik catch blokkon belül volt egy return utasítás, vagy egy újabb throw volt benne. A finally egyetlen esetben nem fut le: ha van egy System.exit(); hívás, ami a program fizikai végét jelenti. A finally blokk általában olyan utasításokat tartalmaz, amelyeket a sikerességtől függetlenül végre kell hajtani, pl. egy adatbázis olvasást követően a kapcsolat lezárása (amit akár sikerült az olvasás, akár nem, végre kell hajtani).
Ha kivétel történik a try blokkon belül, akkor az azt követő utasítások nem hajtódnak végre. Emiatt van egyébként értelme annak is, hogy egy olyan struktúrát hozzunk létre, hogy a try blokk után nincs catch, hanem egyből finally.
Példa
Lássunk egy példát!
class MyExceptionA extends Exception {} class MyExceptionB extends Exception {} public class ExceptionExample { public static double myFunction(int i) throws MyExceptionA, MyExceptionB { if (i < 0) {throw new MyExceptionA();} if (i == 0) {throw new MyExceptionB();} return i * i; } public static void main(String[] args) { for (int i = -1; i <= 1; i++) { try { System.out.println("Calling myFunction(" + i + ")"); System.out.println(ExceptionExample.myFunction(i)); System.out.println("Call myFunction(" + i + ") successfully finished"); } catch (MyExceptionA e) { System.out.println("Call myFunction(" + i + ") finished with exception A"); e.printStackTrace(); } catch (MyExceptionB e) { System.out.println("Call myFunction(" + i + ") finished with exception B"); e.printStackTrace(); } finally { System.out.println("Call finished with parameter " + i); } } } }
A példában létrehoztunk két kivételt: MyExceptionA és MyExceptionB. Figyeljük meg a két konstruktort. Az ExceptionExample osztályban található myFunctionB() dobhatja ezeket a kivételeket, ami a fejlécében található felsorolásból is látható, valamint a kódban a kivétel kiváltódása is. A myFunctionA() hívja a fenti függvényt. Abban nincs külön kivételkezelés, tovább dobja a kivételt. Így annak a fejlécében is fel kell sorolni a lehetséges kivételeket. A kivételkezelés a hívó oldalon, a main függvényben található. Ha kivétel váltódik ki a myFunctionA() függvény hívásakor, akkor a harmadik println() nem fut le, hanem a megfelelő catch ág, valamint a finally. A a.printStackTrace() hívás igen gyakori; ez kiírja a kivétel nevét, a paraméterül átadott hibaüzenetet, valamint a hívási láncot. Ez utóbbi megmutatja, hogy pontosan hol történt a kivétel, ami két szempontból is megkönnyíti a hibakeresést: egyrészt pontosan megmutatja, hogy ki hívott kit, másrészt ez általában igen hosszú, és látványosan "virít" a naplófájlban.
A fenti programnak a kimenete az alábbi:
Calling myFunction(-1)
Call myFunction(-1) finished with exception A
basics.MyExceptionA
Call finished with parameter -1
Calling myFunction(0)
Call myFunction(0) finished with exception B
at basics.ExceptionExample.myFunction(ExceptionExample.java:8)
at basics.ExceptionExample.main(ExceptionExample.java:17)
basics.MyExceptionB
at basics.ExceptionExample.myFunction(ExceptionExample.java:9)
at basics.ExceptionExample.main(ExceptionExample.java:17)
Call finished with parameter 0
Calling myFunction(1)
1.0
Call myFunction(1) successfully finished
Call finished with parameter 1
Részletek
A kivételkezelés idáig is elég bonyolultnak tűnik, de a Java nyelv alkotói még inkább tökélyre fejlesztették. A fenti példában a kivételek az Exception (beépített) osztályból származnak. Valójában a catch nemcsak az Exception osztály leszármazottjait kaphatja el, hanem a Throwable osztályét. A kivételkezelés osztálystruktúrája az alábbi:
Ha a kivételkezelést a fenti módon alkalmaznánk, akkor hamar rájönnénk arra, hogy túl sok lenne a kivétel, amit egész egyszerűen nem tudnánk kezelni, a teljes kód tele lenne lehetséges kivételekkel. Így megszületett az ellenőrzött (checked) és nem ellenőrzött (unchecked) kivétel fogalma. Ez utóbbi kategóriába tartozó kivételeket nem kell külön kezelni: az ha kiváltódik, végig fut a hívási láncon és hibát ír ki. Ezek tipikusan olyan hibák, amit kezelni nem lehet, hanem javítani kell. Ilyen pl. a nullával való osztás, amikor egy ArithmeticException váltódik ki. Ha nem lenne nem ellenőrzött kivétel, akkor pl. minden olyan eljárás fejlécébe, amelyben van osztás, ki kellene tenni a nullával való osztás kivételét, még akkor is, ha biztosak vagyunk abban, hogy ezt megfelelően kezeltük. Vagy pl. ha lenne benne tömb, akkor minden esetben foglalkoznunk kellene a tömb túlcsordulással. És mivel az eljárások öröklik a meghívott eljárások kivételeit, a legtöbb eljárás fejlécében tucatnyi kivétel lehetőséget kellene felsorolni. A nem ellenőrzött kivételeket nem kell felsorolni, csak az ellenőrzötteket.
A fenti ábrán látható komponensek a következők:
- Throwable: minden "dobható" komponens ősosztálya.
- Exception: az alapértelmezett kivételosztály; ha ellenőrzött kivételeket szeretnénk létrehozni, akkor ezt kell használni.
- RuntimeException: a nem ellenőrzött kivételek ősosztálya. Akkor kell alkalmaznunk, ha biztosan vagyunk abban, hogy nem lehet a problémát futásidőben lekezelni. A gyakorlatban ilyet ritkán hozunk létre.
- Error: ez sem ellenőrzött, viszont a catch Exception nem kapja el. Ezek általában Java virtuális gép szintű hibák, amelyeket döntően nem tudunk futási időben kezelni, pl. elfogyott a memória. A gyakorlatban ilyet szinte sohasem hozunk létre, és sohasem próbáljuk elkapni.
Néhány gyakori, a rendszer által definiált kivétel:
- Ellenőrzött:
- IOException: írás-olvasási hiba, pl. nem létező fájlból szeretnénk olvasni, vagy úgy szeretnénk elérni egy távoli számítógépet, hogy lejárt az internet előfizetés.
- ClassNotFoundException: ez egy futásidejű hiba, és a virtuális gép nem talál egy osztályt. A leggyakrabban induláskor lép fel, amikor rosszul adtuk meg a főosztályt, pl. elfelejtettük megadni a csomagot.
- Nem ellenőrzött:
- NullPointerException: ha egy objektum értéke null és megpróbálunk hivatkozni valamelyik attribútumára vagy metódusára.
- ArrayIndexOutOfBoundsException: ha egy tömb nem létező elemét címezzük meg (pl. a tömb 5 elemű, és mi a nyolcadiknak szeretnénk értéket adni).
- NumberFormatException: ha egy String-et számmá szeretnénk alakítani, de nem megfelelő a formátum, pl. "alma".
- ArithmeticException: nem értelmezett matematika művelet, pl. 0-val való osztás.
- ClassCastException: ha át szeretnénk alakítani egy osztályt egy másikká, de nem sikerül. Pl. vagy egy Állat ősosztályunk, abból származik a Kutya és a Macska, van egy Kutya objektumunk, amit Macskává szeretnénk konvertálni.
A kivételkezelés továbbfejlesztése
A Java haladó témák oldalon olvashatunk a kivételkezelés következő továbbfejlesztéseiről:
- try with resources: a Java 7-ben megjelent lehetőségként a try-nak átadhatunk egy Closable interfész megvalósítást, és az lekezeli a lezárást, nem kell a finally-ban nekünk megtennünk. Valójában enélkül igencsak elbonyolódik a kód.
- Több kivétel elkapása egyszerre: a catch (MyExceptionA | MyExceptionB e) szintaxis.