Kihagyás

Kivételkezelés

Az előadás videója elérhető a itt.

A Java alapfilozófiája az, hogy rosszul formált kód nem fog jól futni. Ideális esetben jó lenne, ha minden hiba már fordítási időben kiderülne. Persze a valóságban ez nem megvalósítható, de mégis jó lenne, ha a hibákat már ott kezelni lehetne, ahol azok jelentkeztek.

Korábbi nyelvekben, mint amilyen a C is, inkább csak konvenciók voltak a hibák kezelésére. Azaz a legtöbb esetben egy-egy függvény csak beállított egy hibakódot, egy flaget, egy visszatérési értéket, amit a hívó félnek kell ellenőrizni, lekezelni, ami persze az esetek többségében elmarad, hiszen a hívó fél nem is tud arról, hogy az adott művelet hogyan is kell viselkedjen. Legjobb példa erre a printf függvény, ami egy intet ad vissza. Valószínű, hogy a legtöbb programozó nem is tudja, hogy ez az érték nem más, mint a kiírt karakterek száma, ami ha túl kevés adott esetben, lehetne következtetni arra, hogy a printf nem azt csinálta, ami a programozó szándéka volt.

Ahhoz, hogy ne maradjanak a hibák kezeletlenül, érdemes nyelvi szinten megkövetelni a hibakezelést. Persze ez önmagában nem új találmány, mert már az 1960-as években ismerték ezt operációs rendszer szinten. Illetve a Javaban megjelenő kivételkezelés is a C++ (esetleg az Object Pascal) kivételkezelésén alapszik, a C++-é pedig az Ada-n, tehát több nyelv tapasztalatait építették be a Java megoldásba.

A lényeg, hogy amikor egy hiba megjelenik a programban, azaz egy kivételes esemény történik, a program normális végrehajtása megáll, és átadódik a vezérlés a kivételkezelő mechanizmusnak. Mivel az alapfilozófia úgyis az, hogy a hibás kód nem tud jól működni, amíg a hiba nincs kezelve, nem érdemes folytatni a program végrehajtását. Azzal, hogy a kivételt lekezelő kód elkülönül a normálisan futó kódtól, az az előnyünk is megadatik, hogy maga a kód megtisztul és áttekinthetőbb lesz, nem kell mindenféle ellenőrzésekkel megszakítani a kód futását csak azért, hogy ellenőrizzük, nincs e benne hiba.

Kivételek

A kivételes feltétel egy olyan probléma, amely meggátolja egy aktuális metódus, vagy szkóp kódjának futtatását. Fontos, hogy a kivételes feltételt megkülönböztessük egy normál problémától, amelyben az adott szituációban elegendő információ van a probléma legyőzéséhez. A kivételes feltétel jelentkezésekor a program végrehajtását normálisan nem tudjuk folytatni, mert nincs elegendő információ arra vonatkozóan, hogyan tudnánk a problémát orvosolni. Amit tehetünk, hogy az adott kontextusból kiugrunk, és rábízzuk a probléma megoldását egy tágabb környezetre. Pont ez történik akkor, amikor egy kivételt dobunk.

Például ilyen eset lehet az osztás. Mi van, ha épp 0-val próbálnánk meg osztani? Elképzelhető, hogy adott probléma esetében tudjuk, hogyan kezeljük le a 0-val való osztást, de ha ez egy nem várt érték az adott szituációban, akkor lehet, hogy érdemesebb inkább dobni egy kivételt ahelyett, hogy folytatnánk a program végrehajtását az adott végrehajtási úton.

if (p < 0)
    throw new IllegalArgumentException();

Amikor egy kivételt dobunk, számos dolog történik. Először is létrehozunk egy kivétel objektumot ugyanúgy, mint bármely más objektumot, a new-val, amely ezek után ugyanúgy, mint bármely objektum, a heapen jön létre. A program végrehajtása (ami ugyebár nem tud most folytatódni a kivétel jelentkezése miatt) megáll, és a kivétel objektumra mutató referencia kilép a jelenlegi végrehajtási kontextusból a throw utasítás által. Ezen a ponton a kivétel kezelő mechanizmus átveszi a végrehajtást, és megpróbálja megkeresni azt a helyet, ahol folytatni lehet a program futását. Ez a megfelelő hely a kivétel kezelő, amely az adott kivételt okozó problémát lekezeli és akkor a program ott folytatódhat tovább, vagy épp generál egy újabb kivételt.

Kivételek argumentumai

Minden beépített kivételnek két konstruktora van: egy default és egy olyan, ami egy Stringet vár a paraméterében (az a String a kivétel message adattagját fogja inicializálni, amit később akár fel is használhatunk.)

if (p < 0) {
    throw new IllegalArgumentException("A p nem lehet negatív!");
}

A throw kulcsszó a létrehozott kivételt tovább dobja. Ha az adott metódusban nincs lekezelve a kivétel, akkor olyan, mintha a throw egy alternatív return utasítása lenne a metódusnak, aminek a visszatérési értéke persze nem olyan típusú, mint amit a metódus deklarációja eredetileg ígér. Az is lehet, hogy még a hívó metódus sem kezeli le az adott kivételt, ilyenkor az egészen addig adódik át a hívási verem metódusain a dobott kívétel, amíg el nem jut a megfelelő kivétel kezelőig, vagy el nem ér a hívási verem aljáig. Általában azért arra számíthatunk, hogyha valahol dobódik egy kivétel, akkor lesz olyan hely, ahol azt valaki lekezeli (ha nem így írjuk a programjainkat, akkor annak használhatósága egész hamar meg fog kérdőjeleződni.)

Kivétel elkapása

A program azon részeit, ahol a kivételek keletkezhetnek, és amiket utána kivétel kezelő részek követnek, amelyek a jelentkező kivételeket lekezelik majd, a program védett régióinak nevezzük.

try {
    // " normál kód, amiben kivétel keletkezhet
    // (védett régió)
} catch (ExceptionType1 e1) {
    // hibakezelő kód az e1 kivételre
} catch (ExceptionType2 e2) {
    // hibakezelő kód az e2 kivételre
    throw e2; // tovább is lehet dobni
} catch (Exception e) {
    // hibakezelő kód az összes, megmaradt kivételre
} finally {
    // végül ( mindig lefut )
}

Kivétel dobásakor tetszőleges kivételt dobhatunk, amely a Throwable (az ős kivétel osztály) származhat. A hibakezelő blokkok nem feltétlenül kezelik le az összes kivétel típust. Az információ a hibáról az általában benne van a kivétel objektumban, implicitien már magában a kivétel osztály nevében, így egy tágabb kontextusában a hibának el lehet dönteni, hogy adott ponton mely kivételek kezelése oldható meg.

Saját kivételek és kivétel specifikáció

Ha akarunk, akár saját kivételeket is hozhatunk létre bármely kivétel osztály specializálásával. Általában azért a legjobb az Exception osztályból származtatni.

class SajatException extends Exception {
  public SajatException(String s) {super(s);}
} 
public class KivetelPelda {
  public void f() throws SajatException {
    System.out.println("Dobunk egy SajatException-t f()-bol");
    throw new SajatException("f()-bol dobtak");
  }
  public static void main(String[] args) {
    KivetelPelda kp = new KivetelPelda();
    try {
      // ...
      kp.f();
      // ...
    } catch(SajatException e) {
      System.err.println("Elkaptuk!");
      System.err.println(e);
    }
  }
}

Itt a SajatException egy egyszerű kivétel lesz, ami semmi újdonságot nem tesz hozzá az Exception osztályhoz, az egyedüli jellegzetessége a konkrét típus név lesz. A konstruktorában sem csinálunk semmit, csupán inicializáljuk a megfelelő adattagokat a super konstruktor hívás által.

A KivetelPelda f() metódusa egy olyan metódus lesz, amiben létrehozunk és eldobunk egy SajatException objektumot. Mivel ezt a kivételt nem kezeljük le a metóduson belül, így a metódus specifikációjában a throws kulcsszó után meg kell adjuk, hogy a metódus milyen kivételeket dobhat. Erre azért van szükség, hogy a metódust hívó más metódusok számíthassanak a kivételre, hogy adott esetben le tudják azokat kezelni. Ha egy metódus hív egy olyan metódust, ami kivételt dobhat, amit azonban a hívó nem fog lekezelni, akkor a hívó metódus specifikációjában is meg kell adnunk, hogy a metódus hívás által az adott kivétel dobódhat.

A példában a main metódusban elkapjuk a dobott kivételt, és le is kezeljük. Amikor a System.out.println-nal kiírjuk a kivétel objektumot, akkor a toString metódusa által az a szöveg fog gyakorlatilag kiíródni, amivel a kivétel konstruktorát hívtuk. Így a program kimenete a következő lesz:

Kimenet

Dobunk egy SajatException-t f()-bol

Elkaptuk!

SajatException: f()-bol dobtak

Java standard kivételek

Ahogy az már említésre került, az ős kivétel osztály a Throwable osztály. Ennek két közvetlen leszármazottja van. Az Error osztály az egyik, amivel általában nem kell foglalkozni, mert fordítási időben megjelenő, illetve rendszerhibákat képvisel. Az Exception osztály az, ami gyakorlatilag az ősosztálya a programokban használt kivételeknek. Az, hogy milyen kivételek használatosak a Java függvénykönyvtárakon belül, arról a megfelelő Java verzió specifikációjában olvashatunk. Pl.: https://docs.oracle.com/javase/8/docs/api/java/lang/Exception.html

Runtime exception

A runtime kivételek olyan speciális Java standard kivételek, amelyeket nem kell külön megadni a kivétel specifikációban. Azon kivételek őse lesz, amit a virtuális gép dobhat normális működés közben. Például ezek a NullPointerException, ClassCastException, IndexOutOfBoundsException. Az, hogy ezeket a kivételeket nem kell a metódusok specifikációjában feltüntetni azért van, mert szinte minden metódusról feltételezhetőek, hogy dobhatnak ilyen típusú kivételeket.

try-with-resources

Sok esetben a kivétel olyan helyzetekben kerül dobásra, amikor épp valamilyen erőforrást használunk, mint például egy fájl, és például az abban való olvasás ütközik hibába. Maga a fájl feltehetőleg meg van nyitva, de amikor a kivétel eldobódik, az úgy is marad, nincs ami bezárja, és felszabadítsa ezeket az erőforrásokat. Ahhoz, hogy ez ne így legyen, az erőforrások kezelésére oda kell figyelni nagyon (ami különösen egy programozó számára igen megterhelő feladat):

static String beolvas(String path) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        if (br != null) br.close();
    }
}

Java 7 előtt az ilyen eseteket úgy lehetett kezelni, hogy az erőforrást megnyitottuk, még a try blokk előtt (így tudunk majd rá hivatkozni a finally blokkban), majd elkezdtük azt használni a try blokkban. Amennyiben hiba dobódott itt, akkor a try blokkhoz kapcsolt finally blokkban zárni kellett az erőforrást, így ha a kivétel kezelése tovább is haladt volna a hívó metódus felé, az erőforrás felszabadítása nem maradt el.

Ezen a megoldáson egyszerűsített a Java 7 azzal, hogy minden olyan objektum inicializálása, amely osztálya megvalósítja a java.lang.AutoCloseable interface-t megtörténhet a try utasításban is, és ekkor az objektum lezárása meg fog történni minden esetben, ha volt hiba, ha nem, még a catch, illetve finally blokkok feldolgozása előtt:

static String beolvas(String path) throws IOException {
    try (BufferedReader br = new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

Utolsó frissítés: 2024-03-27 14:54:36