Jelenlegi hely

18.1. Kivételek elkapása vagy továbbengedése

A Java futtató rendszer megköveteli, hogy a metódus elkapja, vagy felsorolja az összes ellenőrzött kivételt, melyet a metódus eldobhat (másként fogalmazva a kivételt továbbengedi). Először is tekintsünk át néhány kifejezést.

Elkapás (catch)

A metódus elkaphat egy kivételt, ha rendelkezik ilyen típusú kivételek kezelőjével.

Továbbengedés (throws záradék)

A metódus a deklarációja throws záradékában írja le, hogy milyen kivételeket dobhat.

Ellenőrzött kivételek

Kétfajta kivétel létezik: futási időben keletkezett kivétel és nem futási időben keletkezett kivétel. Futási idejű kivétel a Java futtatórendszerében keletkezik: aritmetikus kivételek (például nullával való osztás), referenciával kapcsolatos kivételek (mint például egy objektum tagjaihoz való hozzáférés null hivatkozással) és indexeléssel kapcsolatos kivételek (mint például egy tömb elemeihez való hozzáférés olyan indexszel, mely túl nagy vagy túl kicsi). Egy metódusnak nem kötelező előírnia futási idejű kivételeket, de ajánlott.

Nem futási időben keletkezett kivételek olyan kivételek, melyek a Java futási rendszeren kívül keletkeznek. Például: kivételek, melyek I/O során keletkeznek. A fordító biztosítja, hogy a nem futási időben keletkezett kivételeket elkapják, vagy továbbengedjék; ezért ezeket ellenőrzött kivételeknek is nevezzük.

Sok programozó inkább futási időben keletkezett kivételeket használ az ellenőrzött kivételekkel szemben, hogy ne keljen elkapniuk vagy továbbengedniük őket. Ez általában nem ajánlott.

Kivételek, melyeket a metódus eldobhat

  • Minden a metódus által közvetlenül eldobott kivétel
  • Minden közvetetten eldobott kivétel másik metódus hívásával, mely kivételt dob.

Kivételek elkapása és kezelése

Ez a fejezet bemutatja, hogyan kell használni a kivételkezelő három komponensét – a try, catch, és finally blokkokat – egy kivételkezelő megírásához. A fejezet utolsó része végigmegy egy példán, bemutatva, mi történik különböző esetekben.

A következő példa a ListOfNumbers osztályt definiálja és implementálja. A ListOfNumbers konstruktor egy Vector objektumot hoz létre, mely tíz Integer elemet tartalmaz a nullától kilencig terjedő index értékekhez. A ListOfNumbers osztály egy writeList metódust is definiál, mely a számok listáját kiírja egy szövegfájlba, melynek neve OutFile.txt. A példa program a java.io csomagban definiált kimeneti osztályokat használja, melyekről részletesebben később lesz szó.

import java.io.*;
import java.util.Vector;
public class ListOfNumbers {
    private Vector vector;
    private static final int SIZE = 10;
    public ListOfNumbers () {
        vector = new Vector(SIZE);
        for (int i = 0; i < SIZE; i++) {
            vector.addElement(new Integer(i));
        }
    }
    public void writeList() {
        PrintWriter out = new PrintWriter(
            new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++) {
            out.println("Value at: " + i + " = " +
                vector.elementAt(i));
        }
        out.close();
    }
}

Az első vastag betűs sor egy konstruktor hívása. A konstruktor egy kimeneti folyamot inicializál. Ha a fájlt nem lehet megnyitni, a konstruktor IOException-t dob. A második vastag betűs sor a Vector osztály elementAt metódusát hívja meg, mely egy ArrayIndexOutOfBoundsException-t dob, ha az paramétereinek értéke túl kicsi (kisebb, mint nulla), vagy túl nagy (nagyobb, mint a változók száma, melyeket a Vector tartalmaz). Ha megpróbáljuk lefordítani a ListOfNumbers osztályt, a fordító egy hibaüzenetet ír ki a FileWriter konstruktor által dobott kivételről, de nem fog hibaüzenetet megjeleníteni az elementAt által dobott hibáról. Ennek oka, hogy a konstruktor által dobott kivétel, IOException, ellenőrzött kivétel, míg a másik, ArrayIndexOutOfBundsException, egy futási időben keletkezett kivétel. A Java programozási nyelv csak az ellenőrzött kivételek kezelését követeli meg, tehát a felhasználó csak egy hibaüzenetet fog kapni.

Most, hogy megismerkedünk a ListOfNumbers osztállyal és az abban eldobott kivételekkel, készen állunk egy kivételkezelő megírására, mely ezeket a hibákat elkapja, és kezelni tudja.

A try blokk

Egy kivételkezelő elkészítésének első lépése, hogy elhatároljuk a kódot, ami hibát dobhat a try blokkban. A try blokk általában így néz ki:

try {
    code
}
catch and finally blocks ...

A példakódban található szegmens tartalmaz egy vagy több olyan sort, amely kivételt dobhat. (A catch és a finally blokkokról részletes magyarázatot a következő részben találhatunk.)

Ahhoz, hogy egy kivételkezelőt készítsünk a ListOfNumbers osztály writeList metódusához, a writeList metódus kivétel-dobó részeit el kell határolnunk a try blokkal. Ezt többféleképpen tehetjük meg. A programkód azon sorait, melyekről feltételezzük, hogy kivételt dobhatnak, try blokkba tesszük, és mindegyiknél gondoskodunk a kivételek kezelésről. Vagy pedig betehetjük az összes writeList kódot egy egyszerű try blokkba, és ehhez hozzákapcsolhatunk többféle kivételkezelőt. A következőkben láthatjuk, hogy az egész metódusra használjuk a try blokkot, mivel a szóban forgó kód nagyon rövid:

...
private Vector vector;
private static final int SIZE = 10;
...
PrintWriter out = null;
try {
    System.out.println("Entered try statement");
    out = new PrintWriter(new FileWriter("OutFile.txt"));
    for (int i = 0; i < SIZE; i++) {
        out.println("Value at: " + i + " = " +
            vector.elementAt(i));
    }
} /// catch and finally statements ...

Amennyiben a kivétel bekövetkezik a try blokkon belül, a kivétel lekezelésre kerül a kivételkezelő által. Ahhoz, hogy a kivételkezelést hozzá tudjuk kapcsolni a try blokkhoz, utólag használnunk kell catch blokkot is. A következő rész megmutatja, hogyan is kell ezt használnunk.

Megjegyzés: A kivételkezeléssel ismerkedők esetén gyakori hiba, hogy csak a kivételt eldobó függvényhívást teszik a try blokkba. Érdemes ezen a példán átgondolni, hogy ha nem sikerülne az állomány megnyitása, akkor semmi értelme nem lenne a for ciklus lefutásának. A fenti megoldásnál ez nem is fog bekövetkezni, hiszen a kivétel létrejöttekor a vezérlés a teljes try blokkból kilép.

A catch blokk

A try blokkhoz hozzáilleszthetjük a kivételkezelést, amennyiben egy vagy több catch blokkot használunk közvetlenül a try blokk után. Semmilyen programkód nem lehet a try blokk vége és az első catch blokk között!

try {
    ...
} catch (ExceptionType name) {
    ...
} catch (ExceptionType name) {
    ...
} ...

Minden catch blokk egy kivételkezelő, és azt a típusú kivételt kezeli, amilyet a paraméter tartalmaz. A paraméter típusa (ExceptionType) deklarálja a kivétel típusát, amit a kezelő lekezel.

A catch blokk tartalmazza azt a programkódot, amely végrehajtásra kerül, amikor a kivételkezelőt meghívjuk. A futtatórendszer meghívja azt a kivételkezelőt, amelyik esetén az ExceptionType megfelel a dobott kivétel típusának.

Itt látható két kivételkezelő a writeList metódushoz – az ellenőrzött kivételek két típusához, melyeket a try blokk dobhat:

try {
    ...
} catch (FileNotFoundException e) {
  System.err.println("FileNotFoundException: "+e.getMessage());
  throw new SampleException(e);
} catch (IOException e) {
  System.err.println("Caught IOException: "+e.getMessage());
}

Mindkét kezelő egy hibaüzenetet ír ki. A második kezelő semmi mást nem hajt végre. Bármilyen IOException (I/O kivétel) elkapása esetén (amit az első kezelő nem kapott el), megengedi a programnak, hogy folytassa a futtatást.

Az első kezelő a kiírandó szövegek összeillesztése során egy saját definiált kivételt dob. Ebben a példában a FileNotFoundException kivétel létrejöttekor egy saját definiált kivételt hoz létre, aminek SampleException a neve, és ezt dobja a program.

A kivételkezelők többet is tudnak, mint hogy hibaüzeneteket írnak ki, vagy pedig megállítják a program futását. Hibajavításra is képesek, amint a felhasználó hoz egy rossz döntést, vagy pedig a hibák túlnőnek a legmagasabb szintű kezelőn láncolt kivételeket használva.

A finally blokk

A kivételkezelő beállításának utolsó lépése, hogy rendet tegyünk magunk után, mielőtt átadjuk az irányítást a program különböző részeinek. Ezt a takarító programkódot a finally blokkba kell beírnunk. A finally blokk tetszőlegesen használható. Olyan mechanizmust nyújt, ami kiküszöböli azokat a figyelmetlenségeket, amik a try blokkban történtek. A finally blokkot például arra használhatjuk, hogy bezárjuk a fájlokat, amelyekre se a hiba nélküli futás, se a hiba dobása esetén nem szükségesek már.

A writeList metódus try blokkja (amivel eddig dolgoztunk) megnyitja a PrintWriter-t. A writeList metódusból való kilépés előtt programnak be kellene zárnia ezt a folyamatot. Ez felvet egy némiképp komplikált problémát, mivel writeList metódus try blokkja a következő három lehetőség közül csak egyféleképpen tud kilépni:

  • A new FileWriter hibát jelez és IOException-t dob.
  • A vector.elementAt(i) hibát jelez és ArrayIndexOutOfBoundsException-t dob.
  • Minden sikerül és a try blokk zökkenőmentesen kilép.

A futtatórendszer mindig végrehajtja azokat az utasításokat, amelyek a finally blokkban vannak, függetlenül attól, hogy történt-e kivétel dobása, vagy nem. Így ez a legmegfelelőbb hely, hogy kitakarítsunk magunk után.

A következő finally blokk a writeList metódust takarítja ki, és bezárja a PrintWriter-t.

finally {
    if (out != null) {
        System.out.println("Closing PrintWriter");
        out.close();
    } else {
        System.out.println("PrintWriter not open");
    }
}

A writeList példában, a finally blokkba való beavatkozás nélkül tudunk gondoskodni a kitakarításról. Például, a PrintWriter bezárását végző programkódot a try blokk végéhez tudjuk illeszteni, továbbá az ArrayIndexOutOfBoundsException kivétel-kezelőhöz:

try {
    ...
    out.close();
} catch (FileNotFoundException e) {
    out.close();
    System.err.println(
            "Caught: FileNotFoundException: "+e.getMessage());
    throw new RuntimeException(e);
} catch (IOException e) {
    System.err.println("Caught IOException: " +
                        e.getMessage());
}

Azonban ez mégis lemásolja a kódot, így amennyiben később módosítjuk a programkódot, nehéz lesz olvasni benne, és a hibák kiterjedhetnek. Például, ha a try blokkhoz egy olyan programkóddal bővítjük, ami egy új típusú kivételt dobhat, emlékeznünk kell arra, hogy bezárjuk a PrintWriter-t az új kivétel-kezelőben.

Az összeillesztés

Az előző részekben volt arról szó, hogyan építsünk fel a ListOfNumbers osztályban try, catch és finally blokkokat a writeList metódus számára. A következőkben a forráskódot nézzük meg, vizsgálva a végeredményt.

Mindent egybevéve, a writeList metódus a következőképp néz ki:

public void writeList() {
    PrintWriter out = null;
    try {
        System.out.println("Entering try statement");
        out =
            new PrintWriter(new FileWriter("OutFile.txt"));
        for (int i = 0; i < SIZE; i++)
            out.println("Value at: " + i + " = "
              + vector.elementAt(i));
    } catch (ArrayIndexOutOfBoundsException e) {
        System.err.println("Caught " +
                    "ArrayIndexOutOfBoundsException: " +
                      e.getMessage());
    } catch (IOException e) {
        System.err.println("Caught IOException: " +
                         e.getMessage());
    } finally {
        if (out != null) {
            System.out.println("Closing PrintWriter");
            out.close();
        } else {
            System.out.println("PrintWriter not open");
        }
    }
}

Ahogy azt előzőekben említettük, a metódusban a try szerkezetnek három lehetséges kimenete lehet:

  • A kódunk hibás és kivételt dob. Ez lehet egy IOException kivétel, amit a FileWriter okoz vagy egy ArrayIndexOutOfBoundsException kivétel egy rosszul megadott érték esetén a for ciklusban, esetleg egy RuntimeException kivétel bármilyen futási hiba esetén.
  • Minden rendben, a várt kimenetet kapjuk.

Nézzük meg, mi történik a writeList metódusban ezen esetekben:

Kivétel történik

Számos oka lehet annak, hogy a FileWriter hibás működést fog eredményezni.

Okozhatja a konstruktor (ha nem tudjuk létrehozni a fájlt, vagy nem tudunk beleírni).

Amikor a FileWriter az IOException kivételt dobja, a program azonnal leállítja a try blokk végrehajtását. Ezek után a rendszer a metódus elejétől indul, és meghívja a megfelelő kivételkezelőt. Habár a FileWriter konstruktornak nincs megfelelő kivételkezelője, a rendszer megnézi a writeList metódust.

A writeList metódusnak két kivételkezelője van: egyik az IOException, másik, pedig az ArrayIndexOutOfBoundsException.

A rendszer megnézi a writeList kivételkezelőit, de csak a try blokk után. Az első kivételkezelő nem foglalkozik a konkrét kivétel típussal, ezért a rendszer automatikusan a második kivételkezelőt használja (IOException). Ez be tudja azonosítani a típusát, ennek köszönhetően a rendszer már megtalálja a megfelelő kivételkezelőt, így a catch blokk végrehajtódik.

Miután a kivételkezelő lefutott, a rendszer a finally blokkra ugrik, melynek futása elindul a kivételeket figyelmen kívül hagyva. Miután a finally blokk lefutott a rendszer normális ütemben fut tovább.

Az IOException által dobott kimenet:

Entering try statement
Caught IOException: OutFile.txt
PrintWriter not open

A try blokk normális működése

Minden, ami a try blokkon belül van, sikeresen lefut és megfelelő kimenetet ad eredményként. Nem kapunk kivételt és a rendszer továbblép a finally blokkra. A sikeresség eredményeképpen a PrintWriter megnyílik, majd amikor a rendszer eléri a finally blokkot, bezárja. Miután a finally blokk lefutott, a rendszer normális ütemben fut tovább.

A kivételek nélküli kimenet:

Entering try statement
Closing PrintWriter

Metódusok által dobott kivételek

Az előzőekben láthattuk, hogyan írhatunk kivételkezelőt a ListOfNumbers osztályban a writeList metódusnak. Olykor ez megfelelő a működést illetően, de vannak esetek, amikor jobb a veremkezelőt használni. Például, ha a ListOfNumbers osztályt egy másik osztály csomagjaként hozunk létre, ebben az esetben jobban tesszük, ha más módon kezeljük a kivételt.

Bizonyos esetekben szükség lehet az eredeti writeList metódus módosítására, melynek hatására már a kívánt működést kaphatjuk. Az eredeti writeList metódus, ami nem fordul le:

public void writeList() {
    PrintWriter out =
               new PrintWriter(new FileWriter("OutFile.txt"));
    for (int i = 0; i < SIZE; i++) {
        out.println("Value at: " + i + " = " +
                   victor.elementAt(i));
    }
    out.close();
}

A writerList metódus deklarációjában a throws kulcsszó hozzáadásával két kivételt adunk meg. A throws kulcsszót egy vesszővel elválasztott lista követ a kivételosztályokkal.

A throws a metódus neve és a paraméter lista után áll, utána következik a kapcsos zárójelben a definíció:

public void writeList() throws IOException,
                               ArrayIndexOutOfBoundsException {

Az ArrayIndexOutOfBoundsException futásidejű kivétel, nem kötelező lekezelni, így elegendő az alábbi forma használata:

public void writeList() throws IOException {