14. hét Kivételkezelés
Tartalom:
14.1. A Java kivételkezelése
14.2. A
try
blokk
14.3. A catch
blokk
14.4. A finally
blokk
14.5. A throws
használata
14.6. Saját kivételek létrehozása a
throw
utasítással
14.7. Tesztkérdések
14.8. Feladatok
A Java kivételkezelésének célja a programfutás során keletkezett hibák kiszűrése és megfelelő kezelése. Az ilyen hibákat a Java platformon Exception-nek (kivételnek) nevezik. Két fő csoportjuk van: a futási időben és a nem futási időben keletkezett kivételek. Futásidejű kivételek az aritmetikai (pl. nullával való osztás), az indexeléssel kapcsolatos (pl. tömb nem létező eleméhez való hozzáférés), és a referenciával kapcsolatos (pl. objektumokra való hivatkozás) kivételek. Ezeket a kivételeket nem kötelező implementálni, de erősen ajánlott. Nem futásidejű kivételek a Java rendszerén kívül keletkeznek. Ilyenek az I/O műveletek során keletkező hibák (pl. a fájl nem található). Utóbbiakat kötelező lekezelni.
A felmerülő hibák sokféleségének kezelését a Java az objektum-orientált paradigma lehetőségeinek felhasználásával oldja meg: a kivételeket osztályok (ill. objektumaik) reprezentálják. Minden beépített - vagy általunk létrehozott - kivétel közös ősosztálya a Throwable osztály.
Amikor egy metódus futása során valamilyen hiba lép fel (pl. nullával való osztás, veremtúlcsordulás, indexhatár túllépése, vagy a háttértároló megtelik, stb.), akkor egy kivételobjektum (egy kivételosztály példánya) jön létre. Ez az objektum olyan információkat tartalmaz a kivétel fajtájáról és a program aktuális állapotáról, amelyeket a kivétel lekezelésekor felhasználhatunk. A kivételobjektum létrehozását és a futtatórendszer által történő lekezelését kivételdobásnak hívjuk. Ezeket ellenőrzött kivételeknek is nevezzük. Az ellenőrzött kivételeket kötelező lekezelni, amit a fordító már fordítási időben ellenőriz is.
Nézzünk egy egyszerű példát a nullával való osztás kivételkezelésére! Az alábbi program alaphelyzetben semmiféle ellenőrzést nem végez az osztó értékére vonatkozóan:
A
program futtatása ennek megfelelően 0 osztónál hibát jelez:
Hagyományos megoldásként az osztás művelete előtt ellenőrizzük az osztó értékét!
Ez a megoldás bonyolultabb
ellenőrzések esetén az if
feltételek garmadáját vonhatja magával, ami nagymértékben rontja a kód
olvashatóságát és az esetleges programjavítás lehetőségét.
Kivétel mindig egy metódus törzsében keletkezhet. Ha ezeket a hibákat mind ugyanitt kezelnénk le, a programkód áttekinthetősége jelentősen romlana, és nem is lehet minden hibára előre felkészülni. Ezt szem előtt tartva olyan megoldás született, amelyben a hibás, kivételes programállapotot eredményezhető sorokat összefogva, s hozzá egy úgynevezett kivételkezelőt megadva a probléma elegánsan megoldható. Ekkor az adott blokkban fellépő kivételeket egységesen, egy helyen kezelhetjük, jól elválasztva egymástól a program fő feladatát végző, illetve a hibák lekezeléséért felelős kódrészt. Ennek megvalósítására a try - catch - finally blokkszerkezet használható. Sorrendjük szigorúan kötött!
A try blokk utasításokat zár közre, amelyeket futási időben végig felügyelete alatt tart. Lehetőleg minél kevesebb utasítást tegyünk egy ilyen blokkba, mert kivétel keletkezése esetén csak a kivétel keletkezési helyéig hajtódnak végre a blokkbeli utasítások.
A try - catch - finally kivételkezelő kódblokk felépítése:
try
{ |
Néhány fontosabb kivételosztály:
Exception (általános hiba, ez az osztály minden más kivételosztályt magába foglal)
IOException (általános I/O hiba)
FileNotFoundException (a fájl nem található)
EOFException (olvasás a fájl vége után)
IndexOutOfBoundsException (indextúlcsordulás)
NumberFormatException (számformátum hiba)
ArithmeticException (aritmetikai hiba)
IllegalArgumentException (érvénytelen argumentum)
InputMismatchException (az input adat nem megfelelő típusú)
A catch blokkok a try blokkban keletkező - típusuknak megfelelő - kivételeket kezelik le. Minden try blokkhoz megadható tetszőleges számú (ha nincs finally blokk, akkor legalább egy) catch ág, amelyek az esetlegesen fellépő hibák feldolgozását végzik. Semmilyen programkód nem lehet a két blokk között! A catch ágak sorrendje sem mindegy, mert az első olyan catch blokk elkapja a kivételt, amelynek típusa megegyezik a kiváltott kivétellel, vagy őse annak. Ezért érdemes a specifikus kivételtípusoktól az általánosabb típusok felé haladva felépíteni a catch blokkokat. A hierarchia tetején álló Exception osztály nem előzhet meg más (leszármazott) kivételosztályokat, mert azok sosem hajtódnak végre. A fordító ilyen sorrend esetén hibát jelez.
Ha valahol kivétel keletkezik, akkor a futtató rendszer megpróbál olyan catch ágat találni, amely képes annak kezelésére. Az az ág képes erre, amelynek paramétere megegyező típusú a kiváltott kivétellel (vagy annak ősével), valamint amelynek a hatáskörében a kivétel keletkezett.
A catch ág egy szigorúan egyparaméteres metódusként fogható fel, amely paraméterként megkapja a fellépő kivételt, és ezt tetszőlegesen felhasználhatja a hibakezelés során. A kivételobjektumot nem kötelező felhasználni, mert sokszor a típusa is elég ahhoz, hogy a programot a hibás állapotból működőképes mederbe tereljük.
Ha a catch ágak egyike sem tudja elkapni a kivételt, akkor a beágyazó kivételkezelő blokkban folytatódik a keresés. Ha egyáltalán nincs megfelelő catch blokk, a program befejeződik.
A finally ág akkor is lefut, ha volt kivétel, akkor is, ha nem. Ebben a tetszőlegesen felhasználható blokkban kezdeményezhetjük pl. a nyitott fájlok bezárását, amit - függetlenül attól, hogy a megelőző try blokkban volt-e kivétel, vagy sem - mindig illik megtennünk. Ezt az ágat nem kötelező létrehozni, kivéve, ha nincs egyetlen catch ág sem. A kivétel lekezelése után a program végrehajtása a kivételkezelő kódblokk utáni utasításon folytatódik.
A kivételkezelő blokkok megismerése után alakítsuk át példaprogramunkat! Az osztás művelete kerüljön egy try blokkba, a hibát egy catch blokkban aritmetikai hibaként kezeljük le, valamint a finally blokkban jelezzük az osztás sikerességét!
A futás eredménye nulla osztó esetén:
A vörös színben megjelenő "/ by zero" üzenet a kivételkezelő osztály
hibaüzenete. Ebben a példában a finally
ágra nincs is igazán szükség, így ezt elhagyva és a hibaüzenetet átírva
egyszerűsíthető a program.
A kimenet is barátságosabb:
A fenti program forráskódja: Osztás.java.
.
Ha egy metódus végrehajtása közben kivétel keletkezik, és ezt nem akarjuk, vagy nem tudjuk helyben - az adott metóduson belül - lekezelni, akkor tovább kell küldenünk egy magasabb szintre, az adott metódust hívó metódus felé. Ez mindaddig folytatódhat, amíg el nem érjük azt a szintet (metódust), amely már elegendő információval rendelkezik a megfelelő intézkedések elvégzéséhez. Ezt a metódus fejlécében a throws kulcsszóval tehetjük meg, utána felsorolva azokat a kivételosztályokat (vagy egy ősüket), amelyeket nem kívánunk (vagy nem tudunk) helyben lekezelni. Legkésőbb a program main metódusában az ilyen kivételeket le kell kezelni.
14.6. Saját kivételek létrehozása a throw utasítással
A Java kivételkezelése nyitott, ami azt jelenti, hogy bárki létrehozhat saját névvel és funkcionalitással ellátott kivételosztályokat, amelyeket célszerű az Exception osztályból származtatni. Az új kivételosztályok nevében célszerű az "Exception" szót is szerepeltetni, hogy utaljon annak szerepére.
Saját kivételobjektum létrehozása: throw new <Saját_Kivételosztály_név> ("Saját hibaüzenet"); |
Az alábbi program egy 3 db egész szám tárolására szolgáló
verem adatszerkezetet modellez. Megvalósítjuk a verembe helyezést és a veremből
való kivételt úgy, hogy ezen műveletek hibájának lekezelésére saját
kivételosztályt használunk, és a hiba okát is megjelenítjük.
Első lépésként a saját kivételosztályunkat definiáljuk, amely hibaüzenet átvételére is alkalmas.
Majd következik a verem implementálása. A verem fontos
jellemzője a mérete és a veremmutató. A
betesz()
metódus a paraméterként megadott számot a verem - mutató által jelzett -
tetejére helyezi, a kivesz()
- paraméter nélküli - metódus pedig a verem tetején levő számot "emeli ki". A
szám helyének "kinullázása" nem kötelező, mert a verem telítettségét a mutató
állása jelzi, nem a tartalma.
Próbáljunk háromnál több elemet elhelyezni a veremben, majd
ezután a megtelt veremből háromnál többet kivenni!
Az eredmény kiírása kissé rapszodikus sorrendben történik,
mivel a kivételek kezelése külön programszálon fut, így nem a várt
időpillanatban írják ki a hibaüzenetüket. Ezért a hibát kiváltó művelet előtt
alkalmazzunk egy 2 mp-es késleltetést, amelyet a
vár() nevű
saját készítésű metódussal állítunk elő. Az
msec
osztályváltozó tárolja a késleltetés idejét milliszekundumban.
Az eredmény:
Jól látható, hogy a 4. szám elhelyezése és az üres veremből
való 4. kivétel is hibát okozott. Figyeljük meg a verem működését is! Az
utoljára bekerült elem elsőként lett kivéve (LIFO adatszerkezet: Last In - First
Out, utoljára be - elsőként ki).
A fenti program forráskódja: Verem.rar.
Térjünk vissza egy rövid
kiegészítés erejéig a 9. heti tananyag I/O műveleteire!
Az ott alkalmazott forráskódok a jobb áttekinthetőség miatt nem tartalmazták a
fájl bezárását megvalósító close()
metódus biztonságos kivételkezelését, pedig ez a művelet is okozhat kivételt
(pl. időközben megszűnt a kapcsolat a fájllal), így ezt az utasítást is érdemes
try-catch
szerkezetbe foglalni a következők szerint:
Figyeljük meg, hogy a
close()
utasítást "védő" try-catch
szerkezet a fájlkezelő műveletek
finally ágában helyezkedik el, tehát minden
körülmények között lefut. Viszont a 9. fejezetben szereplő példák mindegyikében
hiába volt a close()
utasítás a try
blokk által védett, ha előtte bekövetkezett egy kivétel (pl. a fájlba írás
során), akkor soha nem került rá a vezérlés, tehát a fájl nyitva maradhatott.
14.7. Tesztkérdések
14.8.1. Olvassunk be egy számot a billentyűzetről és írjuk ki a négyzetgyökét! A kritikus műveleteket tegyük try - catch blokkokba, és negatív szám bevitele esetén "Negatív számból nem lehet négyzetgyököt vonni!" hibaüzenet jelenjen meg! A helyes eredmény 3 tizedes pontosságú legyen!
14.8.2. Egészítsük ki a 9. fejezet io_token.java programját úgy, hogy hibás inputadatok bevitele esetén is működjön, és csak az egész számokat adja össze! A hibás adatok NumberFormatException kivételt váltanak ki. Elválasztójel a szóköz legyen! Pl. a 23 44,4 12 k 10 bemenő adatokból kiszűrhető egész számok összege 23+12+10=45 jelenjen meg a képernyőn! A végeredmény mellett a hibás adatok is kerüljenek kiírásra!
14.8.3. Készítsünk programot, amely a vissza.txt szövegfájlból beolvassa a leghosszabb magyar szót, majd mind előre, mind hátrafelé olvasva kiírja képernyőre! A fájlműveleteket lássuk el megfelelő kivételkezeléssel (FileNotFoundException, IOException)!
14.8.4. Írjunk programot egy 3 db egész szám tárolására szolgáló sor adatszerkezet modellezésére. Típusa "fix kezdetű" legyen, azaz a sor első eleme mindig a sor első tárhelyén helyezkedjen el! Implementáljuk a sorba való behelyezést és a sorból való kivételt úgy, hogy ezen műveletek hibájának lekezelésére saját kivételosztályt használjunk, és a hiba okát is jelenítsük meg!