Kihagyás

9. gyakorlat

A gyakorlat anyaga

Kivételek - hibakezelés

A kivételek valamiféle kivételes eseményt jelölnek, a Java nyelvben ezek segítségével van megoldva a hibakezelés.

Emlékezz vissza, mi történik, ha egy tömböt túl- vagy alulindexelsz! Ha nem emlékszel, próbáld ki!

Ha valamit rosszul írunk a kódban, például lemarad egy pontosvessző, vagy kapcsos zárójel, akkor fordításidejű hibát kapunk, ha megpróbáljuk fordítani a kódot. Azonban nem minden hiba derül ki fordításidőben, ilyenek például az io műveletek, felhasználói interakció, hálózati kommunikáció. Ezeket a hibákat Javaban kivételekkel oldjuk meg, egészen pontosan kivétel objektumokkal, amelyeket hiba bekövetkezésekor "eldobunk", a hívás helyén pedig elkapjuk, és lekezeljük, vagy továbbdobjuk. Ennek segítségével gyakorlatilag "megpróbáljuk menteni a menthetőt", azaz, ha kivételes esemény történik, de azért a program működése szempontjából nem végzetes, akkor valahogy lekezeljük a hibát, hogy a program zavartalanul működhessen tovább (például egy számológép alkalmazásban megpróbálunk nullával osztani, kivételes esemény, de egy hibaüzenetet követően használhatjuk tovább a kalkulátort).

Amikor a program futása során egy metódusban hiba keletkezik (tehát egy kivételes esemény, ami a program normál működése során nem fordul elő), a metóduson belül egy kivétel objektum jön létre (vagy kapjuk egy, általunk meghívott metódustól) a memóriában, amit a futtatókörnyezetnek átadunk, a metódus végrehajtása megáll, és a hívó fél számára továbbküldjük a létrejött kivételobjektumot, aki lekezelheti, vagy továbbdobhatja azt.

Ha a hívási helyen le van kezelve a hiba (alapesetben ezt biztosítani kell), akkor lekezeljük. Ha több hibakezelő kódunk is van, akkor az első megfelelőt fogja használni, majd a program futása folytatódik normál módon.

Tehát eddig összegezve tudjuk, hogy a kivételek speciális objektumok, amelyek valamilyen hiba esetében jönnek létre, és le tudjuk őket kezelni. De hogy kell ezt elképzelni Javaban?

throw

Ha van egy metódusunk, ahol bizonyos feltételek fennállnak, és szeretnénk, hogy hiba dobódna (például ha egy metódusban egy 0 számot kapunk, és azzal osztanánk), akkor ott létre kell hoznunk egy kivétel objektumot, melyet eltárolhatunk referenciában, ha szeretnénk (általában nem szeretnénk); ezt követően pedig ezt a kivételt el kell dobnunk, amire a throw kulcsszó fog szolgálni.

public int osztas(int a, int b) {
    if(b == 0) {
        // Ebben az esetben szeretnénk hibát dobni
        // throw-olni szeretnénk, de mit?
    }
    return a/b;
}

Egy másik példa a korábbról már megismert állatos példa, ahol mindenki házi feladata volt, hogy megvalósítsa, hogy csak szárazföldi, vagy csak vízi állatok lehessenek egy csordában, hiszen egy vegyes csorda nem igazán működőképes. A módosított Csorda osztály:

import allatok.*;

public class Csorda {
    private Allat[] tagok;
    private int maximum = 0;
    private int jelenlegi;

    private boolean szarazfoldiAllatok;

    public Csorda(int num) {
        this.tagok = new Allat[num];
        this.maximum = num;
        this.jelenlegi = 0;
    }

    public boolean csordabaFogad(Allat kit) {
        if (jelenlegi == 0) {
            if (kit instanceof SzarazfoldiAllat) {
                szarazfoldiAllatok = true;
            }
        }
        if (jelenlegi < maximum) {
            if ( (szarazfoldiAllatok && kit instanceof ViziAllat) ||
                 (!szarazfoldiAllatok && kit instanceof SzarazfoldiAllat) ) {
                    // Itt kellene hibát dobni
                }

                tagok[jelenlegi] = kit;
            jelenlegi++;
            return true;
        }
        return false;
    }

    public String toString() {
        String returnValue = "Allatok: ";
        for (int i = 0; i < jelenlegi; ++i) {
            returnValue += this.tagok[i].getNev();
            returnValue += ", ";
        }
        return returnValue;
    }
}

Exception

Már csak egy kivétel objektumra van szükségünk. Ez lehet saját kivétel osztály példánya, vagy egy a beépített Java kivételek közül. A kivételek ősosztálya Javaban az Exception. Ez egy általános kivétel osztály, minden kivétel osztály ebből származik. Néhány további kivételosztály:

  • ArithmeticException (pl.: nullával való osztás)
  • ArrayIndexOutOfBoundsException (tömbindexelés)
  • IllegalArgumentException
  • IOException (IO műveletekkel kapcsolatos)
  • SQLException
  • NullPointerException
  • ClassNotFoundException
RuntimeException

Futásidejű kivételek őse. Ezek elkapása nem kötelező, mivel a program normál futása során nem dobódnak ilyen kivételek, ilyen például a NullPointerException. Ezeket is lekezelhetjük, ha szeretnénk, de nem biztos, hogy jó ötlet, mert ezen kivételek érkezése esetén általában jobban szeretnénk, ha a program futása megszakadjon és leálljon teljesen.

Throwable

A kivételek (Exception osztály és gyermekei) és Error osztályok őse. Az Error típus komolyabb problémára utal, ezekkel általában nem foglalkozunk (ilyen például a virtuális gép meghibásodása, vagy ha kifogyunk a memóriából). Az osztályhierarchia látható az alábbi ábrán:

Exception hierarchia

Ha szeretnénk egy saját kivétel osztályt csinálni, akkor annyi a dolgunk, hogy örököltetjük az Exception nevű ősosztályból. Erre egyszerű példa az InkompatibilisAllatok osztály, amely az inkompatibilis állatok esetén fogunk dobni.

package allatok.kivetel;

public class InkompatibilisAllatok extends Exception {

    public InkompatibilisAllatok() {
        super();
    }

    public InkompatibilisAllatok(String message) {
        super(message);
    }

}

Ahol ezt el szeretnénk dobni, ott csak példányosítanunk kell, majd átadni a létrehozott objektumot a throw-nak. A kiegészített csordabaFogad metódus:

    public boolean csordabaFogad(Allat kit) {
        if (jelenlegi == 0) {
            if (kit instanceof SzarazfoldiAllat) {
                szarazfoldiAllatok = true;
            }
        }
        if (jelenlegi < maximum) {
            if ( (szarazfoldiAllatok && kit instanceof ViziAllat) ||
                 (!szarazfoldiAllatok && kit instanceof SzarazfoldiAllat)) {
                    throw new InkompatibilisAllatok("Ez így nem fog menni!");
                }

                tagok[jelenlegi] = kit;
            jelenlegi++;
            return true;
        }
        return false;
    }

throws

Ez így még nem fog működni. Az olyan metódusoknál, ahol kivétel objektumok jöhetnek létre és dobódhatnak, ott ezt jelezni kell a metódus fejlécében is, hogy bizony ebben a metódusban történhetnek kivételes dolgok is, amelyek lekezelése a hívó fél feladata lesz.

Ezt a jelzést úgy tudjuk megtenni, hogy a metódus paraméterezésében, a paramétereket bezáró zárójel után, de a metódus blokkjának nyitása előtt a throws kulcsszó után vesszővel felsoroljuk, hogy milyen kivételobjektumok dobódhatnak a metódusban. Ha nem szeretnénk sokat foglalkozni vele, egyszerűen csak az ősosztályt írjuk ki, vagyis annyit, hogy Exception. Ebben az esetben nem kell mást felsorolni, mert az összes kivételosztály az Exception-ből származik, így az bármely gyerekosztályt is jelenthet.

    public boolean csordabaFogad(Allat kit) throws InkompatibilisAllatok {
        if (jelenlegi == 0) {
            if (kit instanceof SzarazfoldiAllat) {
                szarazfoldiAllatok = true;
            }
        }
        if (jelenlegi < maximum) {
            if ( (szarazfoldiAllatok && kit instanceof ViziAllat) ||
                 (!szarazfoldiAllatok && kit instanceof SzarazfoldiAllat)) {
                    throw new InkompatibilisAllatok("Ez így nem fog menni!");
                }

            tagok[jelenlegi] = kit;
            jelenlegi++;
            return true;
        }
        return false;
    }

Végre van kivétel objektumunk, amit el is tudunk dobni, és a kód is lefordulna. Már csak egy dolgunk maradt: a hívás helyén lekezelni az esetleges kivételt. Ehhez szükségünk lesz néhány új kulcsszóra:

try, catch, finally

try - ezzel a kulcsszóval kezdődik a védett régió, az a kódrészlet, amely esetlegesen kivételt dobhat, a try blokkon belül vannak azok a metódusok, amelyek kivételt dobhatnak. catch - a try blokk után következik; a kivételkezelő blokk(ok). finally - az a blokk, amely mindenképpen lefut, akár történt kivétel, akár nem. Általában itt zárjuk le a fájlokat, hálózati kapcsolatot.

Kezdjünk is bele: a Csorda osztály csordabaFogad metódusa már nem hívható csak úgy, hiszen kivételt dobhat, ha a körülmények éppen úgy állnak. Ezért ezt egy try blokkba kell tenni:

import allatok.*;

public class EgyszeruMain {

    public static void main(String[] args) {
        Allat elso = new Balna("Charlie");
        Allat masodik = new Csirke("Kotkoda");

        Csorda csorda = new Csorda(5);
        try {
            csorda.csordabaFogad(elso);
            csorda.csordabaFogad(masodik);
        } catch(InkompatibilisAllatok inkompatibilisAllatok){
            System.err.println("Inkompatibilis állatok! Szárazföldi és vízi állatok nem keveredhetnek!");
        }
    }
}

A lehetséges kivételt elkaphatjuk valamelyik catch ággal. Ebből lehet egy vagy több is. A catch kulcsszó után megadhatjuk, hogy milyen kivételtípust szeretnénk elkapni, és azt, hogy ezt milyen néven fogjuk használni a kivétel lekezeléséért felelő blokkban. A kivétel elkapásánál elkaphatunk speciális típust vagy egy általánosabb típust is. Ha nem szeretnénk sokat szöszölni vele, akkor elkaphatjuk a kivételek őstípusát, az Exception osztály egy példányát, hiszen az elkapja a gyerek típusokat is. Több kivételkezelő blokkot is írhatunk egymás után, ha az elkapott kivételeket típusoktól függően másképp szeretnénk lekezelni.

try {
    csorda.csordabaFogad(elso);
    csorda.csordabaFogad(masodik);
} catch(InkompatibilisAllatok inkompatibilisAllatok){
    System.err.println("Inkompatibilis állatok! Szárazföldi és vízi állatok nem keveredhetnek!");
} catch(Exception exc){
    System.err.println("Valami hiba van! :(");
}
Azonban, ha több, különböző kivételt is szeretnénk elkapni, de nem az ősi Exception nevű osztályt használni, akkor Java 7-től lehetőségünk van az alábbi szintaxis használatára is:

try {
    csorda.csordabaFogad(elso);
    csorda.csordabaFogad(masodik);
} catch(InkompatibilisAllatok|ArrayIndexOutOfBoundsException exc){
    System.err.println("Baj van :(");
}

Ez körülbelül azt jelenti, hogy kapjuk el vagy az InkompatibilisAllatok egy példányát, vagy pedig az ArrayIndexOutOfBoundsException egy példányát és ezeket ugyanúgy kezeljük.

Ha van olyan kódrészlet, amelyről biztosítani szeretnénk, hogy mindenképpen lefusson, akkor azt finally blokkba kell betennünk.

try {
    csorda.csordabaFogad(elso);
    csorda.csordabaFogad(masodik);
} catch(InkompatibilisAllatok|ArrayIndexOutOfBoundsException exc){
    System.err.println("Baj van :(");
} finally {
    System.out.println("Ez mindenképp lefut!");
}

Videók

Feladatok

  • Hozz létre egy fix méretű vermet egész számok tárolására (tömb segítségével) és valósítsd meg a push/pop műveleteket.
  • Írj egy futtatható osztályt, mely a Main metódusban "push" vagy "pop" utasításokat vár a konzolról. Ha pop utasítást kap, hajtsa végre azt, és írja ki a konzolra a kivett elemet. Push utasítás esetén egy egész számnak kell következnie, ezt tegye be a verembe.
  • Írj meg egy kivételosztályt, amit a fenti függvények (push/pop) akkor dobnak, ha a verem megtelt vagy üres.

Feladatok

Exceptions

Exception Summary

Java Exceptions

Nested Classes


Utolsó frissítés: 2024-04-11 07:54:27