Elementarz Java #8 – Obsługa wyjątków

Wstęp

Wyjątki, błędy i inne niepożądane przez programistów i użytkowników sytuacje. Dzisiaj w ramach serii #ElementarzJava, artykuł na temat obsługi wyjątków w języku Java. 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!

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: ArrayIndexOutOfBoundsExceptionExceptionInInitializerError, NullPointerException.

Użycie konstrukcji try/catch

Do “łapania” wyjątków w Javie, używamy konstrukcji try/catch. Jej standardowa wersja wygląda tak.

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).

Powyższy kod zostanie więc skompilowany poprawnie.

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ą?

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.

Rezultatem uruchomienia powyższego kodu, będzie wyświetlenie napisu „ World!”. Napis „Hello” nie będzie wypisany.

Blok catch nie zawsze łapie wszystkie wyjątki. W tym przypadku aplikacja się wysypie. 

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.

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).

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.

Powyższy kod „wyrzuci” wyjątek typu ClassCastException. Obiekt String nie dziedziczy po Integer. W przypadku działań arytmetycznych sytuacja jest analogiczna.

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.

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.

Mamy w interfejsie zdefiniowaną metodę, która „rzuca” wyjątek typu Exception.

Co możemy wstawić w miejsce „…”?

  1. nic – metoda, nie musi „rzucać” żadnych wyjątków,
  2. wszystkie wyjątki typu unchecked exception,
  3. wszystkie wyjątki „zawężające” Exception,
  4. 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.

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ę.

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.

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.

Podsumowanie

Na dzisiaj to tyle. Mam nadzieję, że w przystępny sposób przedstawiłem ten trudny i wrażliwy temat. Jeśli masz jakieś pytania, coś nie jest jasne to zapraszam do sekcji komentarzy. Tradycyjnie też zachęcam do lektury pozostałych materiałów z serii #ElementarzJava.

Przeczytaj również

, , , , , , , , , , , , , , , , , ,

Dodaj komentarz

guest
0 komentarzy
Inline Feedbacks
View all comments

Pin It on Pinterest