Gyakorlati alapok II.
Kivételfajták
Amint az az egyik előző, a Try-catch-finally blokkok című fejezetből kiderült, a kivétel (exception) egy olyan, sokféle formában megmutatkozó problémacsomag, amely valamilyen módon megakaszthatja a Java-program futását. A Java-nyelv lehetőséget ad arra, hogy védekezzünk ellenük, sőt ha előre látjuk a probléma felmerültét, akkor kísérletet tehetünk megszüntetésére is.
A kivételek 3 nagy csoportba osztályozhatók:
Az ellenőrzött kivételek attól ellenőrzöttek, hogy előre láthatjuk a felmerülő problémát és kísérletet tehetünk megszüntetésükre. Tipikusan ilyen kivétel a Try-catch-finally blokkok című fejezet egyik példakódjában felhasznált FileNotFoundException kivétel. Egy fájl közeli vagy távoli elérése és megnyitása ugyanis nagyon sokféle hardver-, és szoftverkomponens egyidejű sikerességén múlik, más szóval igen kényes művelet. Sokkal problémásabb, mint mondjuk 2 db integer összeadása. A Java alkotói úgy döntöttek: a nagy hibalehetőségű műveleteknél a legkényesebbeket egyenesen kötelező lekezelni, azaz kötelező try-catch blokk közé illeszteni, nélküle fordítási hibát kapunk. Ez a szigor persze a programfutás előnyére válik, hiszen a kötelező hibakezeléssel jóval biztonságosabban futó kód állítható elő.
Gyakori ellenőrzött kivételek:
IOException
FileNotFoundException
ParseException
ClassNotFoundException
CloneNotSupportedException
InstantiationException
InterruptedException
NoSuchMethodException
NoSuchFieldException
Sajnos nem tudunk minden problémát előre látni, sok magán a programozó tudásán, figyelmén múlik. Minden programozó találkozott már NullPointerException vagy ArrayIndexOutOfBoundsException nevű kivétellel; a magam részéről az utóbbit tanulmányaim során sokszor elkövettem. A programozási hiba legtöbbször futási idő alatt derül ki és sok esetben nem is számítunk rá, ezért a nem ellenőrzött kivételeket nevezik futásidejű kivételeknek is (runtime exceptions). Az alábbi Java-kódban egy ízléses ArrayIndexOutOfBoundsException kivételt generálunk, hiszen a for ciklus során szándékosan túllépjük a fix méretű tömb határát:
public class Main {
public static void main(String[] args) {
int[] tomb = new int[5];
for(int i = 0; i < 6; i++){
System.out.print(tomb[i] + " ");
}
}
}
Végeredmény:
0 0 0 0 0 Exception in
thread "main" java.lang.ArrayIndexOutOfBoundsException: 5
at Main.main(Main.java:5)
Gyakori nem ellenőrzött (futásidejű) kivételek:
-
ArrayIndexOutOfBoundsException
-
IndexOutOfBoundsException
-
ClassCastException
-
IllegalArgumentException
-
IllegalStateException
-
NullPointerException
-
NumberFormatException
-
ArithmeticException
Az error mindig komolyabb, általában vagy hardver, vagy mélyebb, rendszerszintű (minimum a JVM-től induló) hibát jelent.
A hibajellegek szétválasztása osztályszinten is megtörténik: csak elég egy gyors pillantást vetnünk az Exception ősosztálya, a java.lang.Throwable (nem teljesen mutatott) osztályhierarchiájára...
Forrás - Source: proofbyexample.com
...amelyben az Error és Exception külön ágakat képvisel.
Az alábbi "futtatható" Java-kód verem-túlcsordulást fog okozni (StackOverflowError). A verem (stack) egy speciális, LIFO-működésű (Last In First Out) memóriaterület, amelyben a regiszterek tartalmát utolsó-betesz-első-kivesz elven ideiglenesen tárolhatjuk. A hiba produkálása úgy történik, hogy egy saját metódust (metodus()) önmagában, azaz saját metódustörzsén belül folyamatosan meghívunk (úgy mondjuk: rekurzív módon. Ez tehát tipikusan a saját farkába harapó kígyó, az Uroboros példája.) A keletkezett hiba már nem is exception, hanem error jellegű, azaz már komolyabb hardverhiba, hiszen teljesen feltöltöttük, ezáltal pedig használhatatlanná tettük a verem-memóriaterületet.
public class Main {
public static void metodus() {
System.out.println("Metódus");
metodus();
}
public static void main(String args[]) {
metodus();
}
}
Végeredmény:
Metódus
Metódus
Metódus
...
at Main.metodus(Main.java:5)
at Main.metodus(Main.java:5)
at Main.metodus(Main.java:5)
...
A fenti osztályhierarchiából kiderül, hogy az error hibát nem tudjuk elkapni az Exception e használatával (azaz a catch ág soha nem fut le):
public class Main {
public static void metodus() {
System.out.println("Metódus");
metodus();
}
public static void main(String args[]) {
try{
metodus();
}
catch (Exception e){
System.out.println("\nVerem-túlcsordulás!");
}
}
Végeredmény:
Metódus
Metódus
Metódus
...
at Main.metodus(Main.java:5)
at Main.metodus(Main.java:5)
at Main.metodus(Main.java:5)
...
Ellenben Error e (vagy StackOverflowError sofe) definiálásával elkaphatjuk a keletkezett hardverhibát:
public class Main {
public static void metodus() {
System.out.println("Metódus");
metodus();
}
public static void main(String args[]) {
try{
metodus();
}
catch (Error e){
System.out.println("\nVerem-túlcsordulás!");
}
}
Végeredmény:
Metódus
Metódus
Metódus
...
Verem-túlcsordulás!
Végezetül vizsgáljuk meg a fenti kód apró módosítását, amely során a finally blokkot is implementáljuk. A kódból kivesszük a rekurziót, helyette a különálló metódusban szabályozott for ciklust használunk a "Metódus" szöveg kiírására:
public class Main {
public static void metodus() {
for(int i = 0; i <= 5; i++){
System.out.println("Metódus");
}
}
public static void main(String args[]) {
try{
metodus();
}
catch (StackOverflowError sofe){
System.out.println("\nVerem-túlcsordulás!");
System.exit(0);
}
finally{
System.out.println("\nNem keletkezett
verem-túlcsordulás!");
}
}
}
Végeredmény:
Metódus
Metódus
Metódus
Metódus
Metódus
Metódus
Nem keletkezett verem-túlcsordulás!
Bár a fenti, error jellegű kivételt lekezeltük, sok programozó mégis azt javasolja, hogy az ilyen jellegű hibákat inkább ne kezeljük le, hanem hagyjuk a JVM-re. Érvelésük meggyőző, hiszen például egy OutOfMemoryError hiba azt jelzi, hogy elfogyott a memória, ami elég durva hardver-probléma és amelyet úgysem lehet megoldani alkalmazásszintű programozáskor.
Gyakori error-jellegű hibák:
ExceptionInInitializerError
StackOverflowError
NoClassDefFoundError
OutOfMemoryError