Gyakorlati alapok II.
Nyugodtan hozzám vághatod a kivételedet! (throw)
A try-catch-finally blokkok és a Kivételfajták című fejezetekben ismertetett kivételkezelések egyik fontos közös tulajdonsága volt eddig, hogy a kivételkezelés "helyben", azaz vagy a main() függvényben, vagy abban a metódusban került lekezelésre, ahol maga a kivétel keletkezett. Ezt mindezidáig nem bonyolítottuk túl, azaz (minimálisan) a try-catch blokkpár, mondhatjuk úgy is, hogy lokálisan került implementálásra. Magasabb szempontú kódrendszerezéskor azonban ennek lehetnek hátrányai, hiszen a kivételt lokálisan kezeltük le, így megtörténhet, hogy ez a lokális kivételkezelés megtöri a magasabb szempontú rendezettséget.
Minél nagyobb, minél bonyolultabb és minél több rétegű egy projekt, annál inkább kényszerít minket, hogy a kódokat magasabb rendű szempontok alapján rendszerezzük, hiszen -emlékezzünk vissza-, talán a legfontosabb követelmény, hogy a kódban egy funkció csak egyszer fordulhat elő. Ez persze egy tapasztalt programozó számára magától értetődő folyamat (és voltaképpen minőségi elvárás), hiszen folyamatosan ilyen jellegű kódszerkezetekkel dolgozunk, csak gondoljunk például az API-ra, a Java előre megírt, logikus szempontok alapján rendszerezett, "belső" osztálykönyvtárára.
A problémát összefoglalva: a kivételt sokszor nem kezelhetjük le lokálisan, azaz keletkezési helyén. Mi hát akkor a megoldás?
Az ötlet már a C++ programozási nyelvben, mint a Java egyik közvetlen elődjében is megvalósításra került. A nyelvalkotók nagyon viccesen a következőt találták ki: a kivételt tovább kell dobni egy szinttel feljebb (throw = dob). Ekkor a felmerült problémát nem helyben kezeljük le, hanem átdobjuk egy másik metódusnak, lényegében valaki másnak, hogy legalább ne nekünk kelljen bajlódni vele.
A probléma szemléltetéséhez először ismételjük meg a "klasszikus", 0-val való osztás már orvosilag lekezelt kódját:
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!
Ezután ezt a defektes, de a hiba szempontjából lekezelt műveletet mindenestől tegyük bele egy külön metódusba:
public class Main {
static void nullavalValoOsztas(){
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!");
}
}
public static void main(String[] args) {
nullavalValoOsztas();
}
}
Végeredmény:
Hiba: nullával való osztás!
Láthatóan a végeredmény ugyanaz. Ha a kódból kiemeljük a hibakezelést, természetesen ki fog akadni, illetve amint már megállapítottuk, a hibát valójában a JVM fogja elkapni:
public class Main {
static void nullavalValoOsztas(){
int a = 4;
int b = 0;
double c = a / b;
}
public static void main(String[] args) {
nullavalValoOsztas();
}
}
Végeredmény:
Exception in thread "main" java.lang.ArithmeticException:
/ by zero
at Main.nullavalValoOsztas(Main.java:5)
at Main.main(Main.java:9)
Most pedig alkossunk egy olyan Java-kódot, amelyben a metódusok láncszerűen hívják meg egymást:
nullavalValoOsztas()
↓
lustaDisznoMetodus1()
↓
lustaDisznoMetodus2()
↓
main()
Mivel a nullavalValoOsztas() metódus lesz a legelső, de hibás kód, a hibakezelésnek garantáltan végig kell futnia a metódusláncon.
Nézzük meg a futtatható Java-kódot:
public class Main {
static void nullavalValoOsztas(int a, int b){
double c = a / b;
System.out.println(c);
}
static void lustaDisznoMetodus1(){
nullavalValoOsztas(4, 0);
}
static void lustaDisznoMetodus2(){
lustaDisznoMetodus1();
}
public static void main(String[] args) {
try{
lustaDisznoMetodus2();
}
catch(ArithmeticException ae){
System.out.println("Hiba: nullával
való osztás!");
}
}
}
Végeredmény:
Hiba: nullával való osztás!
No de ácsi Karcsi bácsi! Hol van itt kivételdobás?
Sehol, mert a szabály a következő:
Kivétel továbbdobása (throw) csakis ellenőrzött kivételeknél (checked exceptions) lehetséges, futásidejű (runtime, unchecked exceptions) nem.
Ezen okból ismételjük meg a legfontosabb ellenőrzött és nem ellenőrzött kivételeket!
Gyakori ellenőrzött kivételek:
IOException
FileNotFoundException
ParseException
ClassNotFoundException
CloneNotSupportedException
InstantiationException
InterruptedException
NoSuchMethodException
NoSuchFieldException
Gyakori nem ellenőrzött (futásidejű) kivételek:
-
ArrayIndexOutOfBoundsException
-
IndexOutOfBoundsException
-
ClassCastException
-
IllegalArgumentException
-
IllegalStateException
-
NullPointerException
-
NumberFormatException
-
ArithmeticException
A fenti példa -mivel az ArithmeticException nem ellenőrzött (futásidejű) kivétel-, nem igényel throw deklarációt.
A throw szemléltetésére ismételjük meg láncolt metódushívásainkat, ám immár egy ellenőrzött kivétellel. A kivételt először lokálisan, magában a kiváltó metódusban kezeljük le. A 2 db lustaDisznoMetodus() nem csinál semmit, csak meghívja a lánc előző tagját:
fajlOlvasas()
↓
lustaDisznoMetodus1()
↓
lustaDisznoMetodus2()
↓
main()
import java.io.*;
public class Main {
static void fajlOlvasas(){
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ó!");
} catch (IOException ioe) {}
finally {
try {
if (reader !=
null) {
reader.close();
}
} catch (IOException ioe) {
ioe.printStackTrace();
}
}
}
static void lustaDisznoMetodus1(){
fajlOlvasas();
}
static void lustaDisznoMetodus2(){
lustaDisznoMetodus1();
}
public static void main(String[] args) {
lustaDisznoMetodus2();
}
}
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.
Ha a fájl valamilyen ok miatt nem található, akkor a kód végeredménye a következő lesz:
Végeredmény:
A fájl nem található!
Ezután megpróbáljuk a hibát továbbdobni a throw utasítással, azaz nem lokálisan lekezelni.
Ennek során a hibakiváltó metódus fejléce mellett definiálnunk kell a throws utasítást, valamint a kivétel fajtáját is, például:
static void fajlOlvasas() throws IOException{
...
}
Illetve ugyanilyen módon kell definiálnunk minden metódus fejléce mellett, amelyik részt vesz az esetleges továbbítási láncban, azaz továbbdobja a kivételt, például:
static void lustaDisznoMetodus2()throws IOException{
lustaDisznoMetodus1();
}
Nézzük meg a futtatható Java-kódot:
import java.io.*;
public class Main {
static void fajlOlvasas() throws IOException{
FileReader reader = null;
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();
}
}
static void lustaDisznoMetodus1() throws IOException{
fajlOlvasas();
}
static void lustaDisznoMetodus2() throws IOException{
lustaDisznoMetodus1();
}
public static void main(String[] args) {
try{
lustaDisznoMetodus2();
}
catch (IOException ioe) {
System.out.println("A fájl nem
található!");
}
}
}
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.
Ha a fájl valamilyen ok miatt nem található, akkor a kód végeredménye a következő lesz:
Végeredmény:
A fájl nem található!
A throws definícióknál számos hibát véthetünk, ezt legtöbbször a JVM rendkívül magas szintű ellenőrzési programmechanizmusai fordítási hiba formájában jelzik (azaz a kód nem futtatható):
-
nem kezeljük le a kivételt ellenőrzött kivételeknél,
-
rossz kivételfajtát definiálunk,
-
rossz a try-catch blokkpár definiálása,
-
valamelyik továbbdobó metódusfejléc mellett elfelejtjük a kivétel definiálását (azaz megszakad a továbbdobási lánc),
-
elfelejtjük importálni az IO rutinokat,
-
stb.
Például érdekes fordítási hibaértesítés a következő:
a BufferedReader osztály használata mindenképpen ellenőrzött kivételkezelést követel meg (BufferedReader br = new BufferedReader(reader);). Ehhez azonban mégsem elég egy FileNotFoundException kivétel-definiálás, hiszen egy fájl beolvasásakor számos egyéb hiba is felmerülhet, nemcsak FileNotFoundException. A megoldás, hogy a JVM már csak IOException kivételkezelést fogad el, sőt erről hibaüzenet formájában értesít is minket: Unhandled exception type IOException. Ekkor tulajdonképpen 1 szinttel feljebb léptünk a kivétel-hierarchiában (Exception hierarchy), hiszen FileNotFoundException az IOException leszármazottja:
java.lang.Throwable
↓
java.lang.Exception
↓
java.io.IOException
↓
java.io.FileNotFoundException
A kivétel tehát valamilyen módon, lokálisan vagy lusta módon 1 vagy több szinttel továbbdobva, de lekezelésre került. A throw használatával kivételkezelés problémája átadható egy másik metódusnak, tágabb értelemben egy egészen máshol lévő, talán funkcionálisan is különböző osztálynak. Ebben a pillanatban már bármi lehetségessé válik: például lehetőségünk nyílik külön kivételkezelő osztály megalkotására, ahová folyamatosan érkeznek a kivételek, mint fontos hibaértesítő üzenetek.
Mert ha jobban belegondolunk, akkor a kivétel továbbdobása voltaképpen irányított hibaüzenet-továbbítás a kijelölt objektumok felé.