Elementarz Java #8 – Obsługa wyjątków
Wstęp
Wyjątki, błędy i inne niepożądane sytuacje. Dzisiaj w ramach serii #ElementarzJava, artykuł na temat obsługi wyjątków. Jak łapać wyjątki? Czy wszystkie błędy należy obsługiwać? Co oznacza słówko throw
i throws
? To tylko niektóre z pytań, na które znajdziesz odpowiedź w niniejszym materiale. Mam nadzieję, że wpis będzie dla Ciebie pomocny i jak zwykle dowiesz się czegoś nowego. Zapraszam do lektury!
Seria #ElementarzJava składa się z następujących artykułów:
- Podstawy języka Java (kompilacja, zmienne, struktura klasy, pakiety),
- Typy danych w języku Java (deklaracja i inicjalizacja zmiennych, różnica między typami, garbage collection, typy opakowujące),
- Operatory i konstrukcje warunkowe w Java (użycie operatorów, porównywanie obiektów, instrukcje:
if
,if/else
,switch
), - Tablice (charakterystyka tablic, tablice jedno i wielowymiarowe),
- Pętle (tworzenie pętli, instrukcje
break
icontinue
, etykiety), - Metody oraz hermetyzacja (metody statyczne, przeciążanie metod, konstruktory, modyfikatory widoczności, hermetyzacja elementów klasy, parametry),
- Dziedziczenie (implementacja, przysłanianie metod, polimorfizm, rzutowanie),
- Obsługa wyjątków (kategorie wyjątków, łapanie wyjątków, klasy wyjątków),
- API Java (
String
,StringBuilder
, data i czas, kolekcje, wyrażenia lambda).
Jesteś tu pierwszy raz? Polecam rozpocząć lekturę od materiału: Elementarz Java #0 – wprowadzenie.
Kategorie wyjątków w języku Java
W Javie wyróżniamy dwa rodzaje wyjątków: checked exceptions
– wyjątki, które muszą być łapane przez programistę oraz unchecked exceptions
– wyjątki, które z definicji nie powinny być łapane przez programistę (aplikacja w takiej sytuacji powinna się „wysypać”).
Dodatkowo, wyjątki dzielimy również na te, które rzucane są przez JVM (nie są rzucane programistycznie). Przykładowo, zaliczają się do nich: ArrayIndexOutOfBoundsException
, ExceptionInInitializerError
, NullPointerException
.
Użycie konstrukcji try/catch
Do “łapania” wyjątków w Javie, używamy konstrukcji try/catch
. Jej standardowa wersja wygląda tak.
try { // … } catch(Exception e) { // … } finally { // … }
W bloku try
umieszczamy kod, który w wyniki swojego działania może rzucić wyjątek. Wyjątek ten „łapiemy” w bloku catch
, a następnie dowolnie go obsługujemy. W bloku finally
, umieszczamy instrukcje, które mają się wykonać niezależnie od tego, czy wyjątek został złapany czy też nie. Jeśli pominiemy blok catch
, wtedy blok finally
jest wymagany (w przeciwnym przypadku jest on opcjonalny).
try { //… } finally { //… }
Powyższy kod zostanie więc skompilowany poprawnie.
try { int result = 5; } catch(RuntimeException e) { //… } catch(ArithmeticException e) { //… }
Taka konstrukcja, jak ta pokazana wyżej jest niedozwolona. Dlaczego? Mamy tutaj klasyczny błąd typu unreachable code
czyli kod nieosiągalny. Jeśli w pierwszej kolejności łapiemy wyjątki typu RuntimeException
, a następnie ArithmeticException
to już w momencie kompilacji, kompilator zdaje sobie sprawę, że tak naprawdę wszystkie wyjątki ArithmeticException
zostaną złapane wcześniej, a kod ten nie będzie wywoływany. Wyjątki ArithmeticException
dziedziczą po RuntimeException
(patrz, diagram na początku artykułu).
Czy kod znajdujący się w bloku finally
jest „zawsze” wywoływany. Jest to prawdą?
try { System.out.println("Hello!"); throw new Excetpion(); } catch(Exception e) { System.exit(0); } finally { System.out.print("Ups..."); }
Ten przykład pokazuje, że nie. Po wywołaniu instrukcji System.exit(0)
aplikacja zostanie zamknięta, bez wywołania kodu z bloku finally
(jest jeszcze kilka takich „kruczków”, które umożliwiają niewywoływania instrukcji z bloku finally
). Warto również dodać, że w momencie wystąpienia wyjątku, dalsze instrukcje z bloku try
, nie są uruchamiane.
try { int result = 5 / 0; System.out.print("Hello"); } catch(ArithmeticException e) { System.out.println(" World!"); }
Rezultatem uruchomienia powyższego kodu, będzie wyświetlenie napisu „ World!”. Napis „Hello” nie będzie wypisany.
try { int result = 5 / 0; } catch(NullPointerException e) { System.out.print("Ups..."); }
Blok catch
nie zawsze łapie wszystkie wyjątki. W tym przypadku aplikacja się wysypie.
try { String name = null; name.toString(); } catch(NullPointerException e) { System.out.println("Hello"); throw e; }
Oczywiście wszystkie instrukcje jakie znajdują się przed linijką powodującą błąd, wykonywane są poprawnie. Tak samo jak instrukcje znajdujące się w bloku catch
, które uruchamiane są po złapaniu wyjątku. W bloku tym możemy również „rzucić” inny wyjątek. Co widać na przykładzie powyżej. Kod ten wypisze „Hello” i adnotację na temat błędu „NullPointerException
”. Ciekawym przykładem jest łapanie wyjątków typu checked exceptions. Jeśli w bloku try
, nie ma instrukcji, która taki błąd mogła by wygenerować, a jest on łapany w bloku catch
, to już na etapie kompilacji dostaniemy błąd.
try { System.out.println("Hello world!"); } catch(IOException e) { System.out.println("Life is brutal!"); }
Ten kod nie zadziała. W bloku try
nie deklarujemy metody, która rzucałaby błąd IOException
. Kompilator „wie”, że blok catch
będzie nieosiągalny (ang. unreachable code).
try { System.out.println("A"); throw new NullPointerException(); } catch (NullPointerException e) { System.out.println("B"); throw new RuntimeException(); } catch (RuntimeException e) { System.out.println("C"); } finally { System.out.println("D"); throw new RuntimeException("1"); }
Jaki będzie efekt wywołania powyższych instrukcji? Na ekranie komputera otrzymamy następującą sekwencję: A B D 1
. Na początku wypiszemy „A
” i wyrzucimy wyjątek, który jest „łapany”, wypisujemy „B
” i wyrzucamy kolejny wyjątek, ale tym razem przechodzimy od razu do bloku finally
, gdzie wypisujemy „D
” i wyrzucamy wyjątek z 1
. Po pierwszym bloku catch
przechodzimy od razu do finally
. Tam wyrzucamy wyjątek, który nie jest obsługiwany, więc kończymy działanie aplikacji. Gdyby wyjątek ten nie był „rzucany”, został by w jego miejsce „rzucony” wyjątek RuntimeException()
z pierwszego bloku catch
, ale stało by się to „na końcu” po wypisaniu „D
”. Jeśli nie rzucalibyśmy w tych dwóch miejscach, wyjątków, to wykonały by się wszystkie instrukcje z pierwszego bloku catch
i przeszlibyśmy do finally
, gdzie wykonały by się kolejne instrukcje i aplikacja działała by dalej.
Przeznaczenie klas wyjątków
Każdy wyjątek zawiera swego rodzaju „informację” o błędzie. Jeżeli otrzymujemy wyjątek typu ClassCastException
to „wiemy”, że poszło coś nie tak z rzutowaniem klas.
Object obj = new Integer(1); String obj2 = (String) obj;
Powyższy kod „wyrzuci” wyjątek typu ClassCastException
. Obiekt String
nie dziedziczy po Integer
. W przypadku działań arytmetycznych sytuacja jest analogiczna.
System.out.print(1 / 0);
Przy próbie dzielenia przez zero, otrzymamy ArithmeticExcetpion
. We wszystkich innych sytuacjach, mamy analogiczne zachowanie.
Metody wyrzucające wyjątki
Wyjątki mogą zostać również „rzucane” przez metody. Taka konstrukcja wymaga użycia słówka kluczowego throws
, które umieszczamy po nazwie metody. Następnie podajemy typ wyjątku jaki będziemy rzucać, przy użyciu instrukcji throw
. Popatrzmy na przykład.
public void method throws Exception { throw new Exception(); }
W powyższym kodzie zdefiniowaliśmy “rzucany” typ wyjątku na Exception
. Nie oznacza to, że w metodzie, możemy używać tylko tego wyjątku. Dozwolone są wszystkie wyjątki, które „zawężają” Exception
(patrz, diagram na początku artykułu). Przykładowo może to być RuntimeException
. Możemy umieścić też dowolny wyjątek z grupy unchecked exceptions.
public void method() throws IOException { System.out.println("It's ok"); throw new Throwable(); // niedozwolone – wyjątek nie jest łapany throw new Exception(); // niedozwolone – wyjątek nie jest łapany throw new AWTException(""); // niedozwolone – wyjątek nie jest łapany throw new RuntimeException(); throw new IllegalArgumentException(); throw new java.io.IOException(); throw new EOFException(); throw new Error(); }
Mamy w interfejsie zdefiniowaną metodę, która „rzuca” wyjątek typu Exception
.
interface Document { void getType() throws Exception; } public class Passport implements Document { public void getType() ... { } }
Co możemy wstawić w miejsce „…”?
- nic – metoda, nie musi „rzucać” żadnych wyjątków,
- wszystkie wyjątki typu unchecked exception,
- wszystkie wyjątki „zawężające”
Exception
, - wyjątek
Exception
.
Gdybyśmy w miejscu Exception
mieli np. IOException
to moglibyśmy wstawić wszystkie wyjątki typu unchecked exception ale już np. AWTException
nie mógłby być (ten sam poziom w hierarchii).
Jeśli już jesteśmy przy temacie interfejsów w kontekście wyjątków, to nie może również zabraknąć przysłaniania metod.
class Animal { public void hasMammals() { } } class Dog extends Animal { public void hasMammals() throws RuntimeException() { } }
Metoda hasMammals
znajdująca się w klasie Dog
może „rzucać” dowolny wyjątek typu unchecked excpetions. Gdybyśmy chcieli rzucać w tym miejscu inny wyjątek typu checked exceptions, metoda przysłaniana (z klasy bazowej) musiała by mieć zdefiniowany taki sam wyjątek jaki chcemy „rzucić” lub jego „szerszą” wersję.
class Test { public static Exception method() { return new Exception(); } }
Wyjątki, jak każdy normalny obiekt mogą być zwracane przez metody. Co widać na powyższym przykładzie. Popatrzmy na jeszcze jeden przykład.
class FirstException extends Exception { } class SecondException extends FirstException { } public class Main { public static void main(String[] args) { try { test(); } catch(SecondException ex) { //… } catch(Exception ex) { //… } } private static void test() throws SecondException { throw new SecondException(); } }
Do drugiego bloku catch
można wstawić np. RuntimeException
ponieważ nie jest łapany wcześniej. Dopuszczalne są wszystkie wyjątki „szersze” od SecondException
. W tym przypadku będzie to FirstException
oraz Exception
. Tutaj jedna uwaga – FirstException
jako wyjątek, który dziedziczy po Exception
jest wyjątkiem typu Checked Exception
. Kompilator wie, że żadna instrukcja z bloku try
, takiego wyjątku nie „rzuci”. Drugi blok catch
będzie oznaczony jako nieosiągalny, ale nie spowoduje to błędu kompilacji. Jeśli FirstException
dziedziczył by po jakimś wyjątku unchecked exceptions np. Error
to wtedy żadnych komunikatów nie będzie. Kompilator nie będzie wiedział, że wyjątek taki nie wystąpi. Dodatkowo możemy też wstawić dowolny wyjątek typu unchecked exceptions. Pamiętaj, że zamiana kolejności tzn. w pierwszym bloku wstawienie FirstExcetpion
, a w drugim SecondException
jest niedopuszczalne – mamy wtedy unreachable code.
Świetny artykuł! Najlepszy opis wyjątków na jaki trafiłam :)
Brak wyjaśnień opisywanych mechanizmów czy odpowiedzi na zadawane w artykule pytania.
Hej em,
Czy możesz wskazać konkretny fragment?
Pozdrawiam,
Łukasz Dudziński, autor bloga StrefaKodera.pl
Hej! Dzieki za ta grafike z hierarchia wyjatkow! Ksiazki, e-learning nie pomgaly mi tej hierarchi sobie zobrazowac a tutaj jeden artykul i dobry rysunek pomogl mi sobie to wszytsko ulozyc to w glowie!:)
Hej Dominika,
Czasami jeden obrazek jest warty więcej niż tysiąc słów :) Powodzenia w dalszej nauce!
Łukasz Dudziński