Kivételkezelés Javában

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:

exceptionhierarchy.png

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.
Unless otherwise stated, the content of this page is licensed under Creative Commons Attribution-ShareAlike 3.0 License