Twitter: @grzegg
Kategoria: java, Tagi: - - .

Java [34] – Wyjątki, czyli jak nie wykrzaczyć programu

Pisząc program zakładamy, że po ukończeniu w zasadzie powinien działać prawidłowo. Błędy powinny pojawiać się wyjątkowo. Dlatego mówimy wtedy o wyjątkach. Na szczęście twórcy Javy stworzyli bardzo użyteczny system obsługi wyjątków, który pomaga sobie z nimi radzić.

Jak program się „wykrzacza”

Uruchom taki prosty programik:

Wynik działania programu będzie wyglądał tak:


Podzielę 10 przez 10
Wynik: 1
Podzielę 10 przez 9
Wynik: 1
Podzielę 10 przez 8
Wynik: 1
Podzielę 10 przez 7
Wynik: 1
Podzielę 10 przez 6
Wynik: 1
Podzielę 10 przez 5
Wynik: 2
Podzielę 10 przez 4
Wynik: 2
Podzielę 10 przez 3
Wynik: 3
Podzielę 10 przez 2
Wynik: 5
Podzielę 10 przez 1
Wynik: 10
Podzielę 10 przez 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
at wyjatki.Wyjatki.main(Wyjatki.java:10)

Jeśli dziwią Cię wyniki działania, przypomnij sobie jaki jest rezultat dzielenia liczb całkowitych. Program oczywiście w końcu przerwał działanie, ponieważ wystąpił błąd – jak wiadomo: „pamiętaj cholero nie dziel przez zero”. Wartość zmiennej mianownik zmniejszała się z 10 o 1 aż doszła do wartości 0 i wtedy doszło do błędu, w konsekwencji program się „wykrzaczył” czyli zakończył działanie. Zauważ jednak, że został także wyświetlony komunikat o miejscu i rodzaju błędu. Tu znajduje się na końcu wydruku, ale jeśli uruchomiłeś program w NetBeans linie mogą być trochę wymieszane.

Przyjrzyjmy się komunikatowi, zacznijmy od końca. Ostatnia linia wskazuje na miejsce wystąpienia błędu. Jest to 10 linia w klasie Wyjatki w metodzie main(). Oczywiście jest to linia kodu, w której wystąpiło dzielnie przez zero. Poprzednia linia komunikatu wskazuje na przyczynę błędu: / by zero ale także zwróć uwagę na ten fragment: java.lang.ArithmeticException. Po pierwsze wygląda to na nazwę klasy (ArithmeticException) w pakiecie java.lang. Po drugie sama nazwa klasy wskazuje, że ma związek z błędem arytmetycznym.

Kiedy doszło do błędu arytmetycznego został utworzony obiekt (wyjątek), który opisuje rodzaj błędu jaki wystąpił a następnie został rzucony (ang. thrown), dlatego mówi się czasem, że program „rzuca wyjątkami”. Wyjątki mogą być spowodowane działaniem programu, albo środowiskiem uruchomieniowym Javy (JRE – ang. Java Runtime Environment). W tym drugim przypadku niewiele możemy zrobić, ale przed zgubnymi skutkami błędów pochodzących z naszego kodu w dużej mierze się możemy zabezpieczyć.

W powyższym programie wystarczy zmienić warunek w pętli, aby do błędu nie dochodziło, ale liczba przez którą dzielimy może na przykład pochodzić od użytkownika albo być wynikiem innych procesów na które niekoniecznie mamy wpływ. Oczywiście w przypadku dzielenia można zastosować konstrukcję if, która dopuści do dzielenia tylko wtedy, gdy dzielnik nie jest zerem ale nie we wszystkich przypadkach jest to takie proste. Potrzebny jest zatem mechanizm, który pozwoliłby sobie radzić także w przypadkach, kiedy zabezpieczenia zawiodą i wyjątek jednak wystąpi. Program nie powinien wtedy się po prostu wyłączyć ale w sensowny sposób obsłużyć wyjątek.

Obsługa wyjątków to oczywiście szeroki temat, skupię się tu jedynie na podstawowych mechanizmach. Najpierw jednak szczypta teorii.

Klasy wyjątków

Java dostarcza wielu klas związanych z rodzajem wyjątków, które mogą wystąpić w programach. Z niektórymi już się spotkaliśmy na kursie jak np. ArrayIndexOutOfBoundsException czy powyższy ArithmeticException. Wszystkie one dziedziczą po klasie Throwable, która znajduje się na szczycie hierarchii klas wyjątków. Bezpośrednio po niej dziedziczą dwie klasy: Exception oraz Error. Drugą z nich nie będziemy się szerzej zajmować, ponieważ odpowiada ona za błędy, których źródłem jest Java Runtime Environment.

Po klasie Exception natomiast dziedziczy mnóstwo klas odpowiednich dla wyjątków, które mogą się wydarzyć w pisanych przez nas programach i które powinniśmy obsłużyć, czyli napisać kod, który w odpowiedni sposób zareaguje na tego typu sytuacje. Listę tych klas możesz znaleźć w dokumentacji klasy. Jedną z nich jest RuntimeException która jest klasą nadrzędną dla kolejnych klas, wśród których znajdziemy m. in. znajome ArithmeticException czy ArrayIndexOutOfBoundsException.

Czasem, jak w przypadku tego programu, obsługa błędu jest kwestią wyboru, ale w niektórych przypadkach, jak już się mieliśmy okazję przekonać jesteśmy do tego zmuszeni.

Poznajmy teraz niektóre z metod obsługi wyjątków.

Konstrukcja try ... catch ... finally

Schematycznie konstrukcja tego typu wygląda tak:

try {
   instrukcje, w których może wystąpić wyjątek;
}
catch (KlasaWyjątku1 nazwa) {
   kod, który się wykonuje, jeśli wystąpi wyjątek typu KlasaWyjatku1;
}
catch (KlasaWyjątku2 nazwa) {
   kod, który się wykonuje, jeśli wystąpi wyjątek typu KlasaWyjatku2;
}

finally {
   kod, który się wykonuje na końcu konstrukcji, niezależnie
   od tego, czy wystąpił wyjątek, czy nie.
}

Blok kodu odpowiadający try może zawierać także fragment kodu, który znajduje się przed i/lub po instrukcji, która może generować wyjątek, jednak ze względu na czytelność kodu należy z tej możliwości korzystać rozsądnie. Należy przy tym pamiętać, że nawet jeśli ten blok składa się z jednej linii kodu, musi być zamknięty w parze nawiasów klamrowych ({...}) w przeciwieństwie np. do konstrukcji if.

Spróbujmy zatem poprawić nasz prosty program tak, by obsłużyć wyjątek.

Teraz po uruchomieniu programu uzyskamy taki wynik:


Podzielę 10 przez 10
Wynik: 1
Podzielę 10 przez 9
Wynik: 1
Podzielę 10 przez 8
Wynik: 1
Podzielę 10 przez 7
Wynik: 1
Podzielę 10 przez 6
Wynik: 1
Podzielę 10 przez 5
Wynik: 2
Podzielę 10 przez 4
Wynik: 2
Podzielę 10 przez 3
Wynik: 3
Podzielę 10 przez 2
Wynik: 5
Podzielę 10 przez 1
Wynik: 10
Podzielę 10 przez 0
Pamiętaj cholero, nie dziel przez zero!
Mimo dzielenia przez zero, program się nie wykrzaczył!
Kończę działanie.

Jak widać, mimo wystąpienia błędu, program działał dalej. Jeśli chcesz, aby w przypadku wystąpienia wyjątku mimo wszystko drukował się komunikat o błędzie należy wywołać metodę printStackTrace() na obiekcie wyjątku. W powyższym przykładzie wystarczy usunąć komentarz w linii 15. Wynik będzie taki:


Podzielę 10 przez 10
Wynik: 1
Podzielę 10 przez 9
...
Podzielę 10 przez 1
Wynik: 10
Podzielę 10 przez 0
Pamiętaj cholero, nie dziel przez zero!
java.lang.ArithmeticException: / by zero
at wyjatki.Wyjatki.main(Wyjatki.java:10)
Mimo dzielenia przez zero, program się nie wykrzaczył!
Kończę działanie.

throws

Jeśli metoda może wygenerować wyjątek, ale nie chcemy go obsługiwać w tej metodzie, można użyć w jej nagłówku klauzuli throws po której następuje nazwa wyjątków. Jeśli jest ich więcej wymieniamy je oddzielając przecinkami. Przeróbmy nieco nasz program:

Po uruchomieniu programu, zobaczę na ekranie:


Podzielę 10 przez 10
Wynik: 1

Podzielę 10 przez 1
Wynik: 10
Podzielę 10 przez 0
Exception in thread "main" java.lang.ArithmeticException: / by zero
at wyjatki.Wyjatki.dziel(Wyjatki.java:14)
at wyjatki.Wyjatki.main(Wyjatki.java:6)

Jak widać na razie jeszcze wyjątek nie został należycie obsłużony, ale warto zauważyć, że komunikat o błędzie stał się nieco dłuższy, można prześledzić łańcuch wywołań, który doprowadził do krytycznej linii kodu, w której powstał błąd. Pojawił się on w metodzie dziel(), która została wywołana przez metodę main(). Metoda dziel() „rzuciła” wyjątek do metody main() a ta go już nie obsłużyła w związku z czym program przerwał działanie.

Jak widać samo zastosowanie klauzuli throws nie zabezpiecza ciągłości wykonywania programu ale jedynie przekazuje problem wyżej. Uzupełnijmy zatem kod:

Leave a Reply