12. fejezet

Mindenki a saját hibáinak kovácsa: személyre szabott hibajelzések a Jávában. Kivételek élete és halála: throw utasítás, a throws kulcsszó valamint a try-catch blokk.

Feladat

Írjunk programot, ami kiírja a bementi paraméter faktoriálisát. Ha emlékszel, n faktoriálisa az 1-tõl n-ig terjedõ egész számok szorzata, 0! = 1. Negatív számokra a faktoriális nincs értelmezve. A faktoriális értékek rendkívül gyorsan nõnek, így 20!-nál nagyobb értékek a long típusból (az int 64 bites változatából) is kilógnak. Írj egy függvényt, amelyiket meg lehet hívni egy egész számmal és visszaadja a faktoriális értéket! A függvény adjon vissza hibajelzést, ha érvénytelen bemeneti értéket kapott. A main függvény hívja meg ezt a faktoriálisszámító függvényt az args[0]-ban kapott bemeneti paraméterrel (Integer.parseInt ...) majd írja ki a hibaüzenetet, ha a faktoriálisszámító függvény hibajelzést adott vissza, különben írja ki az eredményt. A feladat nem okozhat gondot, íme a megoldás.

A programban valóban nincs semmi újdonság. Ugye, a 9. lecke után nem lep meg, hogy a fakt függvénynek szintén statikusnak kell lennie? (mivel a statikus main-ból hívjuk) A fakt függvény önmaga nem írhatja ki a hibajelzést (hiszen a függvényben nem tudhatjuk, egyáltalán szöveges hibajelzést kell-e adnunk, így szólt a feladat kitûzése), ezért negatív értékkel jelez hibát. Mivel a faktoriális nem lehet negatív szám, ezt észreveheti a hívó függvény és hibát jelezhet.

Nem túl ronda ez? De igen. Összekeveredik itt két dolog, a függvény visszatérési értéke és a hibakód. A mostani helyzet viszonylag egyszerû, mivel itt egy hibaeset van és a függvény bizonyos számokat nem adhat vissza, nehezebb lenne azonban a dolgunk, ha nem lenne ilyen "üres tartomány" a visszatérési értékek között és több hibakódunk lenne. A hibák kultúrált jelzésére a Jáva az kivétel (exception) eszközét adja.

A kivételt úgy lehet felfogni, mint egy vészféket. Ha a program futása során kivétel keletkezik (akár a rendszer generál egyet, akár mi generáljuk) a futás megszakad és a Jáva virtuális gép a kivétel feldolgozásával folytatja a tevékenységét. Hogy ez pontosan hogyan zajlik, arról egy kicsit késõbb. Most nézzük meg, hogyan generálhatunk kivételt. Ez roppant egyszerû.

throw new IllegalArgumentException();

A throw utasítás paramétere egy kivételobjektum. A kivételobjektum pont olyan mezei objektum, mint a többi; egyetlen speciális tulajdonsága van: a java.lang.Exception könyvtári objektum leszármazottjának kell lennie. A példánkban szereplõ java.lang.IllegalArgumentException is ilyen és õ is része az alap objektumkönyvtárnak, tehát bátran használhatjuk minden további nélkül. Ugye, emlékszel, hogy a minden Jáva program elejére egy import java.lang.*; utasítást képzel oda a fordító, barátainkat tehát nyugodtan hívhatjuk Exception-nek és IllegalArgumentException-nek.

Kivételekbõl lehet de-luxe változatot is létrehozni, ha úgynevezett részletezõ üzenettel (detail message) hozzuk õket létre. Van ugyanis nekik egy olyan konstruktoruk, ami egy String-et fogad, itt passzolható át a részletezõ üzenet. Ennek a szerepe csupán csak annyi, hogy informaciót közvetíthet a kivételt megkapó számára, pontosan mi is volt a baj. Példa:

throw new IllegalArgumentException( "Az argumentum túl kicsi" );

A metódusban generált kivételeket mindig deklarálni kell a metódus fejlécében, erre való a throws klauza. Példa:

void func() throws IllegalArgumentException {
...
  throw new IllegalArgumentException();
...
}

A Jáva fordítónak mindig megvan a módja rá, hogy összehasonlítsa a metódusban dobott kivételeket a throws után felsoroltakkal (vesszõvel elválasztva tetszõleges számú kivételt felsorolhatunk) és ha eltérést lát, a maga egyenes, de kissé nyers módján hibaüzenetet küld. Próbáld ki!

Feladat

Írd át a faktoriálisszámító programot úgy, hogy a fakt metódus IllegalArgumentException-nel jelezze, ha a metódus hibás bemeneti paramétert kapott. Emitt a megoldas. Futtasd le a programot néhány hibás bemenõ értékkel és ellenõrizd az eredményt!

Ilyen választ fogsz kapni:

Exception in thread "main" java.lang.IllegalArgumentException: Tul kicsi
bemeneti parameter
        at Faktorialis2.fakt(Faktorialis2.java:5)
        at Faktorialis2.main(Faktorialis2.java:15)

Ez egy hívási verem lista. Azt mondja meg, hogy az elsõ sorban részletezett kivétel a Faktorialis2 osztály fakt metódusában keletkezett, majd továbbterjedt a main metódusba (methogy a fakt-ot innen hívták meg) és ezzel elérte a hívási verem tetejét, a program futását megszakította. Ez azért volt lehetséges, mert primkó programunkban nem okozott gondot, hogy a kivétel megszakítsa a program futását. Normális esetben azonban ennél kifinomultabb kivételkezelésre van szükség és erre való a try-catch blokk.

A try-catch szintaktikája a következõképpen néz ki:

try {
   ... utasítások ...
} catch( Exception1 e1 ) {
  ... Exception1 típusú hibát lekezelõ utasítások ...
} catch( Exception2 e2 ) {
  ... Exception2 típusú hibát lekezelõ utasítások ...
} finally {
  ... Az egész blokk legvégén (hiba bekövetkezésétõl függetlenül) végrehajtódó   utasítások ...
}

A try után bekövetkezõ blokkban vannak az utasítások, amelyek Exception1 vagy Exception2 típusú hibát generálnak. A fordító ellenõrizni fogja, tényleg megvan-e erre a lehetõség (minthogy a meghívott metódusok throws klauzájából tudja, azok milyen kivételt dobhatnak), ha valamelyik kivétel nem következhet be, hibaüzenetet ad. Ha nem történik hiba, a try blokk rendben végrehajtódik és a finally mögött levõ blokkal folytatódik a program futása. Ezen blokk végrehajtása után a szerkezet végetér.

Ha azonban a try blokkban kivétel következik be, a blokk futása megszakad és a kivételobjektum (amit a throw-val generáltunk) beíródik a megfelelõ catch blokk referenciaváltozójába. Ha például Exception1 következett be, az e1 mutat a kivételobjektumra, amikor a catch blokkba kerülünk és a futás az Exception1 catch blokkjában folytatódik. Ezt elvégezve a finally blokkja kerül sorra, majd elhagyjuk a szerkezetet. Egynél több catch blokk és a finally blokk opcionális. Legegyszerûbb formájában a szerkezet így néz ki:

try {
  ... utasítások ...
} catch( Exception1 e1 } {
  ... Exception1 típusú hibát lekezelõ utasítások ...
}

Mostanra eljutottunk oda, hogy le tudjuk írni, pontosan mi történik egy kivétel keletkezésekor. Kivétel keletkezésekor a program futása megszakad és megszakad a hívóé is, ha nem definiált try-catch blokkot a kivételre, hanem a throws klauzával azt jelezte, hogy tovább akarja dobni. Így megy ez addig, amíg a hívási lánc tetejére nem érünk (mint elõzõ példánkban) vagy bele nem ütközünk egy aktív trí-catch blokkba. Ekkor a try blokk megszakad és a futás a megfelelõ catch blokkon folytatódik. A fentiekbõl következik, hogy ha egy metódust meghívunk, ami dobhat valami kivételt és ezt jelzi is a throws klauzájában, akkor két eset lehetséges: vagy elkapjuk try-catch-csel, vagy továbbdobjuk throws-szal. Harmadik eset nincs, az összes további próbálkozás szintaktikai hiba.

Egy teljesen logikus dolgot megemlítenék, amiba kezdõ, de még tapasztaltabb Jáva programozók is beleesnek: a try blokk minden utasítását úgy tekinti a fordító, hogy az esetleg nem hajtódik végre, ezért a try blokkban elkövetett változóinicializálásokat nem veszi figyelembe, amikor azt nézi, hogy inicializálták-e a változót. A következõ tehát szintaktikai hiba:

int i;
try {
  i = 1;
  ...
} catch( Exception1 ex ) { ... }
System.out.println( "i: "+i );

A System.out.println soránál a fordító figyelmeztetni fog, hogy az i változót nem inicializáltuk. Ezt a megnyugtatására meg kell tennünk, tehát int i = 0; szükséges.

Mielõtt leckénk zárófeladatához érkeznénk, még egy pár szót a nem jelzett kivételekrõl. Ezek valóban kivételesek, mert általában rendszerszintû programrészek generálják õket gépi kódú részekbõl. Sok esetben nincsenek throws-szal jelölve, hiszen nagyon sokféle utasítás végrehajtásakor keletkezhetnek. Ilyen pl. minden Jáva programozó legõszintébb barátja, a NullPointerException, ami akkor lesz, ha null értékû referenciát akarunk felhasználni egy objektumhivatkozásban. Ezt nem metódusok dobják, hanem ártalmatlan sorok pl. ref.valtozo formájú hivatkozás. Nem jelzett kivételeket is elkaphatunk, ha a problémás környékre egy try-catch blokkot telepítünk a catch ágban minden kivétel õsével, az Exception típussal. Példa:

Object o = null;
try {
  o.toString();
} catch( Exception ex ) {
   ... NullPointerException-t lekezelõ programrész ...
}

Feladat

Bõvítsd ki elõzõ programunkat úgy, hogy a keletkezõ kivételt a main-ben kapd el és írd ki valami kultúrált formában. Használd az Exception objektum getMessage() metódusát, amivel megszerezheted a részletezõ üzenetet a catch blokkban. Emitt a megoldás.

Ellenõrzõ kérdések

  1. Mi a kivétel?
  2. Hogyan lehet kivételeket létrehozni?
  3. Kell-e deklarálni a metódusban létrehozott kivételeket és ha igen, hogyan?
  4. Mire szolgál a try-catch?
  5. Hány catch ága lehet egy try-catch szerkezetnek?
  6. Hány finally ága lehet egy try-catch szerkezetnek?
  7. Mi a teendõ, ha metódusunk olyan másik metódust hív meg, ami kivételt generálhat?
  8. Mik a nem jelzett kivételek és hogyan lehet elkapni õket?

Tartalomjegyzek