Tak jak w naszym życiu, tak samo w kodzie programu zdarzają się sytuacje wyjątkowe, które trzeba jakoś rozwiązać.
Czasami przewidujemy, że mogą one wystąpić, a czasem po prostu stajemy przed faktem dokonanym i możemy się tylko zastanawiać, co poszło nie tak.
Doskonałym przykładem, w którym dochodzi do wystąpienia wyjątku, jest poniższy fragment kodu:
Wynikiem wykonania kodu będzie wydruk:
Wartość 3 wynika z dzielenia 6 i 2, a wartość 6 to efekt dzielenia 6 i 1. Kiedy dochodzi do dzielenia 6 i 0, program zwraca nam
wyjątek o swojsko brzmiącej nazwie
ArithmeticException. To, co widzimy dalej, to jest tak zwany
stacktrace wyjątku, czyli ścieżka wywołań kodu, która finalnie doprowadziła do wystąpienia wyjątku. Czytamy go od ostatniej linii
do pierwszej. Najpierw zauważamy, że problem dotyczy linii 10, gdzie wywoływana jest metoda
divide.
Następnie widzimy, że kolejnym etapem jest wywołanie linii 15 i to miejsce właśnie doprowadza do wystąpienia wyjątku.
ArithmeticException
W ten sposób dosyć dokładnie jesteśmy w stanie określić, co poszło nie tak. Wiadomo, że nie wolno jest dzielić przez 0, a
że Java jest przygotowana na taką sytuację, to otrzymujemy wyjątek. Wyjątek ten jest obiektem stworzonym na podstawie klasy
ArithmeticException z pakietu
java.lang. Jest to jeden z wielu wyjątków
stworzonych przez programistów Javy. Każdy z nich ma swoje konkretne przeznaczenie.
NullPointerException
Jednym z tak przygotowanych wyjątków, jest legendarny
NullPointerException. Kiedy on występuje?
Zobaczmy na przykładzie:
Item to prosta klasa z jednym polem i metodami ustawiającymi/pobierającymi wartość:
Wykonując kod, najpierw zobaczymy na wydruku liczbę 8, która jest długością ciągu tekstowego
"TestItem"
zwróconą w linii 7. Tutaj wszystko jest w porządku, ponieważ w obiekcie
item
ustawiliśmy wcześniej nazwę w postaci wspomnianego ciągu tekstowego.
Problemem jest linia 10, w której drukujemy długość nazwy pobranej z obiektu
item2.
W tym obiekcie nazwa nie została ustawiona, więc pole
name nie jest zainicjowane referencją do konkretnego obiektu tekstowego.
Pole posiada domyślnie postać
null. Skoro pole typu
String jest
nullem
(tak mówimy potocznie), to próba wywołania na nim metody
length kończy się wyrzuceniem przez program wyjątku
NullPointerException:
Checked vs unchecked
Przedstawione do tej pory wyjątki są wyjątkami typu
unchecked. Oznacza to, że nie spodziewamy się ich wystąpienia i szczerze mówiąc
ich pojawienie się, najczęściej świadczy o błędzie programisty. Powinniśmy tak dewelopować kod, by takie wyjątki się nie pojawiały (pomijamy tutaj celowe tworzenie własnych wyjątków - o tym kiedy indziej).
W przypadku
ArithmeticException możemy sprawdzić, czy wartość, przez którą dzielimy,
nie jest zerem i wtedy zwrócić komunikat o niepoprawnej wartości.
Jeśli chodzi o
NullPointerException, wystarczy upewnić się,
że nasze pola i zmienne posiadają referencje do obiektów, zanim dojdzie do wywoływania na nich konkretnych metod.
Wystąpienie tego wyjątku jest dużym błędem programisty.
Oprócz wyjątków typu
unchecked, w Javie występują również wyjątki typu
checked, które mogą, ale nie muszą wystąpić i co najważniejsze,
nie zawsze jesteśmy w stanie ich uniknąć. Przyjrzyjmy się teraz fragmentowi kodu:
Kompilator informuje nas o tym, że metoda, której chcemy użyć, może zakończyć się wyjątkiem typu
IOException, a my w żaden sposób go nie obsługujemy
(Unhandled exception).
Co w takiej sytuacji należy zrobić, aby program mógł się skompilować? Przede wszystkim należy się zastanowić, co oznacza ten wyjątek, a następnie podjąć odpowiednie kroki w celu jego obsłużenia.
To, co próbujemy osiągnąć w naszym przykładzie, to odczyt pliku, pobranie jego zawartości i wyświetlenie danych na konsoli.
Plik nazywa się
items.txt i powinien znajdować się w głównym katalogu projektu.
Wyjątek
IOException pojawia się wtedy, gdy wystąpił problem w komunikacji z zasobem systemu, w tym przypadku z naszym plikiem.
Tak więc, jeśli plik nie istnieje, albo program nie może go otworzyć, wówczas metoda
get rzuci wyjątkiem.
Należy pamiętać, że każde wystąpienie nieobsłużonego wyjątku
(checked albo
unchecked)
powoduje zatrzymanie programu, przez co nie wykona się już żadna instrukcja, która następuje po linii, z której wyszedł wyjątek.
W naszym przypadku nie wykona się linia 10. Nic nie zostanie tutaj wydrukowane na konsolę.
Try/catch i try/catch/finally
Przewidując czarny scenariusz (a przede wszystkim chcąc doprowadzić do kompilacji), przygotowujemy się do obsłużenia wyjątku. Wprowadzamy klauzulę
try/catch:
Zastosowanie tej klauzuli polega na wykonaniu bloku
try, a jeśli podczas wykonania tego kodu
wystąpi wyjątek
IOException, wówczas wykonany zostanie kod w
bloku catch.
Bloków typu
catch może być więcej i będą one sprawdzenie kolejno, co pozwala na obsługę różnych typów wyjątków i zareagowanie w określony
sposób na każdy z nich.
Teraz możemy zrobić coś jeszcze. Wprowadzamy dodatkową klauzulę
finally:
Klauzula reprezentuje blok kodu, który - w przeciwieństwie do
catch - zostanie wykonany zawsze (niezależnie od tego, czy wyjątek się pojawi, czy też nie).
Tak więc kod w naszym przykładzie zawsze pokaże na konsoli tekst
"Reading process completed".
Nawet wykonanie komendy
return nie pozwoli ominąć tego kawałka kodu. Na konsoli zobaczymy:
Throws
W Javie nie musimy obsługiwać wyjątku dokładnie w miejscu, w którym jest on rzucany. Możemy przekazać go wyżej, czyli do metody wywołującej.
W tym celu bieżącą metodę oznaczamy klauzulą
throws:
Na powyższym snippecie widzimy, że metoda
getContent została opatrzona klauzulą
throws z nazwą typu wyjątku, który może zostać wyrzucony z kodu metody. W ten sposób
przekazujemy potencjalny wyjątek do
miejsca, w którym metoda jest uruchomiona i tam możemy obsłużyć jego wystąpienie.
Możemy też w takiej metodzie (w tym przypadku
main) ponownie zastosować klauzule, aby przekazać wyjątek jeszcze wyżej.
Exception i RuntimeException
Na koniec zostawiliśmy bardzo ważny aspekt dotyczący używania wyjątków w Javie. Otóż to czy wyjątek jest oznaczony, czy nie (checked albo unchecked),
wynika bezpośrednio z tego, jakiego typu jest obiekt naszego wyjątku. W celu lepszego zrozumienia zerknijmy na fragment schematu klas wyjątków:
Każdy wyjątek, który dziedziczy z hierarchii klas wywodzącej się z
RuntimeException to wyjątek typu
unchecked, tak więc nie będzie on oznaczony jawnie w kodzie.
Natomiast wyjątki wywodzące się w hierarchii z klasy
Throwable i
Exception
są wyjątkami typu
checked i dlatego też będą wymagały zastosowania
try/catch lub
throws.
W przypadku klasy
Error też mówimy, że jest to byt nieoznaczony
unchecked, natomiast nie nazywamy go wyjątkiem.
Przykładem może być bardzo poważny błąd
OutOfMemoryError, oznaczający brak wolnej pamięci wymaganej do wykonywania programu.
Temat tego rozdziału jest bardzo rozbudowany, choćby z tego powodu, że możemy też tworzyć własne wyjątki, a także możemy wykorzystywać hierarchię wyjątków,
w celu ustalenia określonej kolejności ich obsługi. W przyszłości pewnie będziemy jeszcze wracać do tego obszaru, a na tę chwilę polecamy użyć kod z przykładów,
tak by oswoić się z opisanymi tutaj zagadnieniami.
Autor: Jarek Klimas
Data: 03 stycznia 2024
Labele: Backend, Podstawowy, Java
Czy informacje, które otrzymałeś, były pomocne?
Jeśli tak, zapraszam Cię do podarowania mi kawy.
Masz pytanie dotyczące prezentowanego materiału?
Coś jest dla Ciebie niejasne i Twoje wątpliwości przeszkadzają Ci w pełnym zrozumieniu treści?
Napisz do nas maila, a my chętnie znajdziemy odpowiednie rozwiązanie.
Najciekawsze pytania wraz z odpowiedziami będziemy publikować pod rozdziałem.
Nie czekaj. Naucz się programować jeszcze lepiej.
Dlaczego na wydruku z finally najpierw jest "Reading process completed", a później no content available, skoro
najpierw wykonane zostaje "No content available"?
Odpowiadamy:
"No content available" jest zwracane, ale jeszcze nie jest drukowane. Najpierw wykonuje się sekcja finally,
metoda kończy się, a dopiero wtedy następuje wydruk zwróconej zawartości,
za pomocą System.out.println(content);