Gyakorlati alapok II.

A try-catch-finally blokkok

 

A bevezető fejezet már megadta a romantikus lepkevadász-hangulatot: az éterben lebegő kis programozási hibákat, bugokat el kell kapnunk, hogy ne fertőzze meg és döntse össze az éppen futó Java-programunkat. A lepkeháló ebben az esetben a try-catch-finally blokkok masszív szerkezete lesz, amelyet legelső példánkban a nullával való osztás hibájának lekezelésére használunk fel.

 

Először ismételjük meg ezt a durva programozási hibát:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

public class Main {
    public static void main(String[] args) {
    System.out.println(4 / 0);
    }
}

 

Végeredmény (hibaüzenet):

Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.main(Main.java:152)

 

Az olvashatóság és a továbbiak érdekében egy kicsit manikűrözzük ki a kódot, bár funkcionálisan ugyanaz lesz:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

public class Main {
    public static void main(String[] args) {
    int a = 4;
    int b = 0;
    double c = a / b;
    System.out.println(c);
    }
}

 

Végeredmény (hibaüzenet):

Exception in thread "main" java.lang.ArithmeticException: / by zero
at Main.main(Main.java:152)

 

www.informatika-programozas.hu - Ezt most meg kell tanulni!

 

Most tegyük be a try-catch blokkokat a kódba:

  1. a try blokkba tesszük a problémásnak ítélt műveletet (try = megpróbálom),

  2. a catch blokkba tesszük az alapszintű hibakezelést (catch = elkapom), amely ebben az esetben egy egyszerű hibaüzenet lesz (System.out.println("Hiba: nullával való osztás!");). Egyúttal paramétereként definiáljuk a lehetséges kivételfajtát is (itt: ArithmeticException ae). Ha a catch paramétereként rossz kivételfajtát definiálunk (mondjuk itt ArithmeticException ae helyett FileNotFoundException fnfe), akkor szintén fordítási hibaüzenetet kapunk.

Most nézzük meg a futtatható Java-kódot!

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

public class Main {
    public static void main(String[] args) {
    int a = 4;
    int b = 0;
    double c = 0;
    try{
        c = a / b;
        System.out.println(c);
    }
    catch(ArithmeticException ae){
        System.out.println("Hiba: nullával való osztás!");
        }
    }
}

 

Végeredmény:

Hiba: nullával való osztás!

 

www.informatika-programozas.hu - Ezt most meg kell tanulni!

 

A változás első pillantásra, hogy piros hibaüzenet helyett feketét kaptunk, azonban ez nagy változás, ugyanis azt jelzi: a hibát elkaptuk a catch blokkban, nem dőlt össze a program. Azaz a program képes továbbfutni, bár a kérdéses művelet végeredményben nem került végrehajtásra.

 

Voltaképpen még a catch hibaüzenete is elhagyható, ekkor látszólag nem történik semmi:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

public class Main {
    public static void main(String[] args) {
    int a = 4;
    int b = 0;
    double c = 0;
    try{
        c = a / b;
        System.out.println(c);
    }
    catch(ArithmeticException ae){}
    }
}

 

Végeredmény:

semmi

 

A semmi azért mégis valami, hiszen legalább nem dőlt össze az alkalmazás, ám az igaz, hogy valójában csak a hibás művelet következményeit akadályoztuk meg, más egyebet még nem tettünk. Mivel ebben a kódban a kért művelet (azaz a nullával való osztás) semmiféleképpen sem végezhető el (ezt a JVM mindig le fogja csapni), ezért mást most nem tudunk tenni, mint a felhasználót tájékoztatjuk erről és esetleg bezárjuk a futó alkalmazást (System.exit(0);).

 

www.informatika-programozas.hu - Ezt most meg kell tanulni!

 

Ezt vagy ehhez hasonló lezáró-korrigáló műveleteket a finally blokkban tehetjük meg (finally = legvégül, ezt hajtsd végre), amely tehát a hibás művelet következményeinek végső elsimítására való, ilyen lehet például a lefoglalt erőforrások felszabadítása.

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

public class Main {
    public static void main(String[] args) {
    int a = 4;
    int b = 0;
    double c = 0;
    try{
        c = a / b;
        System.out.println(c);
    }
    catch(ArithmeticException ae){
        System.out.println("Hiba: nullával való osztás!");
    }
    finally{
        System.out.println("A kért művelet nem végezhető el!");
        System.exit(0);
        }
    }
}

 

Végeredmény:

Hiba: nullával való osztás!

A kért művelet nem végezhető el!

 

Természetesen a hibák döntő többsége a programozók rutinja és az ellenőrzött kivételkezelés miatt legtöbbször rejtettebben, alattomosabban jelentkezik, ekkor a programozónak első körben fogalma sincs, hogy mitől állt le a program. A Java-nyelv azonban rendkívül magas szinten támogatja a hibadetektálást és egyéb kisegítő műveleteit. Például sok programozó automatizálja a hibaüzenetek elkapását: a kötelező hibalekezelésen felül a rendszer által generált hibaüzeneteket külön log-fájlba irányítja (Apache Log4j), ahol további szűrési és irányítási lehetőségekre van lehetőség. De mivel kivételkezeléskor egy önálló hibadetektáló objektum jön létre, lényegében azt teszünk vele, amit csak akarunk. (Illetve a következő fejezetben fogjuk megemlíteni a throw utasítás gyakorlati hasznát is.)

 

Például nézzük meg a hibaértesítés alapszintű implementálását a printStackTrace() metódus segítségével. Itt csak kiírjuk a konzolra, de eredményeit valójában bárhová elküldhetjük, eltárolhatjuk.

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

public class Main {
    public static void main(String[] args) {
    int a = 4;
    int b = 0;
    double c = 0;
    try{
        c = a / b;
        System.out.println(c);
    }
    catch(ArithmeticException ae){
        System.out.println("Hiba: nullával való osztás!");

        ae.printStackTrace();
    }
    finally{
        System.out.println("A kért művelet nem végezhető el!");
        System.exit(0);
        }
    }
}

 

Végeredmény:

Hiba: nullával való osztás!

java.lang.ArithmeticException: / by zero
    at Main.main(Main.java:224)

A kért művelet nem végezhető el!

 

Most pedig nézzünk meg egy kissé összetettebb problémát, amelyben nem is 1, hanem 2 catch ág szerepel. Azonban jeleznem kell, hogy ez most kissé nehezebb példa lesz, hiszen a honlapon belül még nem foglalkoztunk fájlkezeléssel.

www.informatika-programozas.hu - Ez a programozási feladat nehéz lesz!

 

Egy erőforrás körüli műveletsorozat többféle hibát is produkálhat. Adott például egy távoli szerveren lévő .txt kiterjesztésű szövegfájl, amit a Java-alkalmazásnak el kell érnie, majd feldolgozás céljából meg kell nyitnia. Itt már kétféle kivétel keletkezhet:

  1. nincsen vagy megszakad a hálózati adatkapcsolat a szerverrel,

  2. valamilyen ok miatt sikertelen a fájl megnyitása.

A felmerült problémát a következő módon modellezzük le:

  1. hozzunk létre egy adott szövegű és című, .txt kiterjesztésű állományt valamelyik tetszőleges meghajtónkon.

  2. A .txt kiterjesztésű állomány teljes elérési útját illesszük be az állományolvasó objektum paramétereként (itt: reader = new FileReader("D:\\Kertész leszek.txt");). Ügyeljünk a kettős backslash-re \\ és a pontos címmegadásra!

Ha a fentieket korrekten adtuk meg, akkor az alábbi futtatható Java-kód képes lesz megjeleníteni az állomány szövegtartalmát, József Attila Kertész leszek című versét:

 

www.informatika-programozas.hu - Futtatható Java-kód!

 

 

 

 

 

 

 

 

import java.io.*;

public class Main {
public static void main(String[] args) {
    FileReader reader = null;
    try {
        reader = new FileReader("D:\\Kertész leszek.txt");
        BufferedReader br = new BufferedReader(reader);
        String line = br.readLine();

        while(line != null) {
            System.out.println(line);
            line = br.readLine();
        }

    } catch (FileNotFoundException fnfe) {
        System.out.println("A fájl nem található!");
        fnfe.printStackTrace();
    } catch (IOException ioe) {
        ioe.printStackTrace();
    } finally {
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
            }
        }
    }
}

 

Végeredmény:

Kertész leszek, fát nevelek,
kelő nappal én is kelek,
nem törődök semmi mással,
csak a beojtott virággal.

Minden beojtott virágom
kedvesem lesz virágáron,
ha csalán lesz, azt se bánom,
igaz lesz majd a virágom.

Tejet iszok és pipázok,
jóhíremre jól vigyázok,
nem ér engem veszedelem,
magamat is elültetem.

Kell ez nagyon, igen nagyon,
napkeleten, napnyugaton -
ha már elpusztul a világ,
legyen a sírjára virág.

 

Bizonyos fajta kivételeket a rendszeren belül viszonylag könnyű produkálnunk:

...akkor máris kiváltottunk egy ízléses FileNotFoundException kivételt.

 

Ezzel párhuzamosan ebben a "zárt" rendszerben viszonylag nehéz kiváltanunk egy IOException jellegű kivételt, bár javaslatom, hogy tesztelés céljából azért a fájl olvasása közben ne csapjuk le azt a meghajtót, amelyen a fájlt éppen tároljuk!

De ha már itt tartunk, ízelítőül nézzük meg, hogy például IOException kivételt milyen problémák okozhatnak:

(A fenti ügy kissé bonyolultabb, hiszen FileNotFoundException az IOException leszármazottja, amint ez majd alább, illetve a Kivételfajták című fejezetben részletezésre kerül.)

 

www.informatika-programozas.hu - Ezt most meg kell tanulni!

 

A fentiekből már kiderült, hogy a try-catch, mint hibaelkapó szubrutinok, valamint a végső megoldás finally blokkja rendkívül szoros mértékben tartoznak össze. Deklarálásukkor sokféle hibát véthetünk, aminek következménye természetesen mindenféle fordítási hibaüzenet lesz, ezért érdemes őket a fenti viszonyaikban megjegyezni és szerkezetileg mindig ugyanúgy deklarálni.

 

A fentieket összefoglalva:

A hibák osztályhierarchiába való szervezésének egyéb következményei is vannak. A fenti, fájlkezeléses kód 2 kivételt tartalmaz:

Az Exception ág osztályhierarchiájába kissé beleturkálva azonban a következőt vehetjük észre:

 

java.lang.Throwable

java.lang.Exception

java.io.IOException

java.io.FileNotFoundException

 

A FileNotFoundException tehát az IOException "gyermeke", leszármazottja (derived class) és fordítva: az IOException a "szülője", az alaposztálya (base class). Mivel az IOException tartalmazza a FileNotFoundException rutinjait is (ám ez fordítva már nem igaz!), ezért többszörös catch blokk esetén, ha az IOException kerül az 1. catch blokkba, akkor a 2. catch blokk FileNotFoundException rutinja soha nem fog lefutni:

 

public class Main {
public static void main(String[] args) {
    FileReader reader = null;
    try {
        reader = new FileReader("D:\\Kertész leszek.txt");
        BufferedReader br = new BufferedReader(reader);
        String line = br.readLine();

        while(line != null) {
            System.out.println(line);
            line = br.readLine();
        }

    } catch (
IOException ioe) {
        System.out.println("A fájl nem található!");
        fnfe.printStackTrace();
    } catch (
FileNotFoundException fnfe) {
        ioe.printStackTrace();
    } finally {
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
            }
        }
    }
}

 

Voltaképpen a problémát a JVM is érzékeli és a következő fordítási hibaüzenetet dobja nekünk...

 

Exception in thread "main" java.lang.Error: Unresolved compilation problem:
Unreachable catch block for FileNotFoundException. It is already handled by the catch block for IOException at Main.main(Main.java:18)

 

...ami azt jelenti, hogy az osztályhierarchiába szervezett kivételkezelés belső szabályai miatt a FileNotFoundException elérhetetlen, mert IOException képes lekezelni az előbbi kivételeit is, amit kivétel esetén meg is tesz, következésképpen az előbbi felesleges deklaráció.

 

Hasonlatos ez ahhoz a szituációhoz, amikor csőszerelőt hív a család. Meg is érkezik, otthon pedig apa és kiskorú fia fogadja. Ha nem lenne otthon az apa, a szerelőnek be kéne érnie a fiúval. Ha azonban az apa is otthon tartózkodik, akkor már nem számít a fiú jelenléte, mert a szerelő csakis az apa utasításait fogja követni.