To jest jeden z artykułów w ramach darmowego kursu programowania w Javie. Proszę zapoznaj się z pozostałymi częściami, mogą one być pomocne w zrozumieniu materiału z tego artykułu.
Czym jest wyjątek?
Wyjątek (ang. exception) jest specjalną klasą. Jest ona specyficzna ponieważ w swoim łańcuchu dziedziczenia ma klasę java.lang.Throwable
. Instancje, które w swojej hierarchii dziedziczenia mają tę klasę mogą zostać „rzucone” (ang. throw) przerywając standardowe wykonanie programu.
Przykładem może być tutaj walidacja argumentów metody. Załóżmy, że nasza metoda jako argument przyjmuje liczbę godzin i zwraca liczbę sekund, odpowiadających przekazanemu argumentowi. Możemy założyć, że akceptujemy wyłącznie argumenty dodatnie lub 0.
Innymi słowy jeśli metoda zostanie wywołana z argumentem mniejszym od 0 możemy uznać to za nieprawidłowe wywołanie i zasygnalizować taką sytuację rzucając wyjątek.
public int getNumberOfSeconds(int hour) {
if (hour < 0) {
throw new IllegalArgumentException("Hour must be >= 0: " + hour);
}
return hour * 60 * 60;
}
W przykładzie powyżej użyliśmy wyjątku występującego w standardowej bibliotece języka Java: java.lang.IllegalArgumentException
. Do rzucania wyjątku używamy słowa kluczowego throw
.
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Co się dzieje po rzuceniu wyjątku?
Wyobraź sobie kilka metod, które wywołują siebie nawzajem. Te kilka wywołań nazywamy stosem wywołań. Proszę zwróć uwagę na przykład poniżej:
package pl.samouczekprogramisty.kursjava.exception;
public class StackTraceExample {
public static void main(String[] args) {
StackTraceExample example = new StackTraceExample();
example.method1();
}
public void method1() {
method2();
}
public void method2() {
method3();
}
public void method3() {
throw new RuntimeException("BUM! BUM! BUM!");
}
}
W naszym przykładzie w metodzie main
tworzymy instancję klasy StackTraceExample
i na instancji wywołujemy metodę method1
, metoda ta wywołuje z kolei metodę method2
. method2
wywołuje method3
, która rzuca wyjątek java.lang.RuntimeException
(kolejny wyjątek z biblioteki standardowej).
Tą listę metod wywołujących siebie nawzajem nazywamy stosem wywołań. W naszym przypadku stos wygląda następująco:
main
method1
method2
method3
A co stanie się po uruchomieniu tego programu? Oczywiście zostanie rzucony wyjątek, a programista zobaczy stos wywołań metod (ang. stacktrace), jak w przykładzie poniżej:
Exception in thread "main" java.lang.RuntimeException: BUM! BUM! BUM!
at pl.samouczekprogramisty.kursjava.exception.StackTraceExample.method3(StackTraceExample.java:18)
at pl.samouczekprogramisty.kursjava.exception.StackTraceExample.method2(StackTraceExample.java:14)
at pl.samouczekprogramisty.kursjava.exception.StackTraceExample.method1(StackTraceExample.java:10)
at pl.samouczekprogramisty.kursjava.exception.StackTraceExample.main(StackTraceExample.java:6)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:140)
W pracy programisty umiejętność czytania tego typu komunikatów jest bardzo istotna. Dzisiaj stacktrace widzisz pierwszy raz, zapewniam Cię, że zobaczysz go jeszcze dużo razy ;)
Zrozumieć stacktrace
Proszę zwróć uwagę na stos wywołań metod, który wspomniałem wyżej i porównaj go ze stacktrace’em. Widzisz pewną zależność? Dokładnie – stacktrace to nic innego jak odwrócony stos wywołań metod od rozpoczęcia programu do miejsca w którym został rzucony wyjątek.
Pierwsza linijka mówi o tym jaki wyjątek został rzucony, kolejne linijki to metody, które były wywoływane. Każda linia składa się z nazwy klasy wraz z pakietem, w nawiasach znajduje się nazwa pliku oddzielona dwukropkiem od numeru linii w tym pliku. W naszym przypadku wyjątek RuntimeException
został rzucony po wywołaniu metody pl.samouczekprogramisty.kursjava.exception.StackTraceExample.method3
, która znajduje się w 18 linijce pliku StackTraceExample.java
.
Ostatnie linijki także pokazują na kod programu, jednak ten nie jest już napisany prze mnie. Program powyżej uruchamiałem z IDE i to właśnie przez IntelliJ IDEA stacktrace zawiera te 5 dodatkowych linijek.
Obsługa wyjątków
Już wiesz jak można rzucić wyjątek. Najwyższy czas zacząć je obsługiwać :) Mówimy, że wyjątek jest obsługiwany, jeśli reagujemy na jego wystąpienie i próbujemy “naprawić” program w trakcie jego działania. Możemy też powiedzieć, że łapiemy wyjątek.
Do obsługi wyjątków służy blok try
/catch
. Proszę spójrz na przykład poniżej:
int hours = -3;
int numberOfSeconds = 0;
try {
numberOfSeconds = instance.getNumberOfSeconds(hours);
}
catch (IllegalArgumentException exception) {
numberOfSeconds = instance.getNumberOfSeconds(hours * -1);
}
System.out.println(numberOfSeconds);
Znasz już metodę getNumberOfSeconds
. Wiesz, że rzuca wyjątek IllegalArgumentException
jeśli argument jest mniejszy od 0. W przykładzie powyżej otaczamy wywołanie metody blokiem try {…} catch
. Jeśli kod wewnątrz nawiasów { }
rzuci wyjątek i blok catch
będzie obsługiwał ten typ wyjątku wówczas zostanie wywołany kod w bloku catch
i wyjątek nie przerwie działania programu.
W przykładzie powyżej pierwsze wywołanie metody rzuci wyjątek ponieważ przekazaliśmy -3
jako argument. Rzucony wyjątek jest obsługiwany przez klauzulę catch
(klasa wyjątku “pasuje”) więc zostaje wywołany kod wewnątrz bloku.
Pod blokiem try
może znajdować się wiele bloków catch
. Pierwszy pasujący zostanie wykonany. Rzucony wyjątek może być obsłużony przez dany blok catch
jeśli klasa wyjątku w ()
znajduje się w hierarchii dziedziczenia rzuconego wyjątku. Nie jest to skomplikowane, zdecydowanie łatwiej wygląda to na przykładzie
try {
throw new IllegalArgumentException();
}
catch (ArithmeticException exception) {
// 1
}
catch (RuntimeException exception) {
// 2
}
catch (Exception exception) {
// 3
}
Blok catch
1 nie zostanie wykonany bo ArithmeticException
nie znajduje się w hierarchii dziedziczenia wyjątku IllegalArgumentException
. Blok catch
2 zostanie wykonany bo IllegalArgumentException
dziedziczy po RuntimeException
. Następny blok nie zostanie wykonany ponieważ w przypadku obsługi wyjątku pierwszy pasujący blok catch
jest wykonywany jako jedyny.
Obsługa kilku rodzajów wyjątków w jednym bloku catch
Może zdarzyć się sytuacja, w której chciałbyś obsłużyć kilka wyjątków a nie mają one wspólnej klasy bazowej. Wówczas w nawiasach po catch
możesz oddzielić klasy wyjątków symbolem |
jak w przykładzie poniżej.
try {
someMagicMethod();
}
catch (ArithmeticException | IllegalArgumentException exception) {
// handle exception
}
Rodzaje wyjątków checked oraz unchecked
Każdy wyjątek w języku Java dziedziczy po klasie Throwable
. Wyróżniamy dwa rodzaje wyjątków, tak zwane “checked exceptions” oraz “unchecked exceptions”. Różnica między nimi sprowadza się do tego, że te pierwsze muszą być obsłużone przez programistę, wymaga tego kompilator. Przykładowym wyjątkiem typu unchecked jest IllegalArgumentException
, natomiast IOException
jest wyjątkiem typu checked.
Reguła podziału wyjątków na te dwa rodzaje jest prosta. Jeśli wyjątek w swojej hierarchii dziedziczenia ma Exception
i nie ma RuntimeException
jest wyjątkiem typu checked. W każdym innym przypadku jest to wyjątek typu unchecked.
Kiedy zatem stosować wyjątki typu checked? Zalecenie jest proste, za każdym razem kiedy program ma możliwość “naprawienia” zaistniałej sytuacji wyjątkowej powinniśmy rzucić wyjątek typu checked. Reguła ta jednak jest bardzo często łamana ponieważ obsługa tego typu wyjątków wymaga trochę więcej kodu ;)
Klauzula throws
Wyjątek można obsłużyć na dwa sposoby. Jeden już znasz, to otoczenie fragmentu kodu blokami try/catch
. Drugi sprowadza się do “zepchnięcia” odpowiedzialności obsłużenia wyjątku o poziom niżej, do metody wywołującej. Służy do tego klauzula throws
, którą dodajemy do deklaracji metody. Spójrz na przykład poniżej:
public class CheckedExceptions {
public static void main(String[] args) {
CheckedExceptions instance = new CheckedExceptions();
try {
instance.methodWithCheckedException();
}
catch (IOException e) {
e.printStackTrace();
}
}
private void methodWithCheckedException() throws IOException {
throw new IOException();
}
}
Metoda methodWithCheckedException
rzuca wyjątek IOException
, który jest typu checked. Nie obsługuje go jednak wewnątrz ale informuje o tym, że może rzucić taki wyjątek dzięki throws
. W metodzie main
mamy standardowy blok catch
, gdzie wyjątek jest obsłużony.
Klauzula finally
Blok finally
możemy umieścić po try
. Kod wewnątrz tego bloku zawsze zostanie wykonany1. W rzeczywistości blok try
nie musi mieć żadnej klauzuli catch
jeśli ma blok finally
.
try {
throw new RuntimeException();
}
finally {
System.out.println("Surprise!");
}
Może być także sytuacja w której mamy zarówno try
, catch
jak i finally
. Jeśli wewnątrz try zostanie rzucony wyjątek, który jest obsługiwany przez blok catch
to dodatkowo, jako ostatni, uruchomi się blok finally
.
Dobre praktyki przy używaniu wyjątków
Poniżej zebrałem dla Ciebie zestaw kilku dobrych praktyk przy pracy z wyjątkami:
- Pierwsza i najważniejsza zasada, blok
try
powinien być jak najmniejszy. Takie podejście bardzo ułatwia znajdowanie błędów w bardziej skomplikowanych programach. Dzięki małemu blokowitry
także możemy napisać lepszy kod do obsługi wyjątku – wiemy dokładnie z którego miejsca wyjątek może zostać rzucony więc wiemy także jak najlepiej na niego zareagować. - Blok
finally
bardzo często jest niezbędny. Szczególnie jeśli operujemy na instancjach, które wymagają “zamknięcia”. - Używaj klas wyjątków, które idealnie pasują do danej sytuacji. Jeśli nie ma takiego wyjątku w bibliotece standardowej utwórz własną klasę wyjątku.
- Tworząc instancję wyjątków podawaj możliwie najdokładniejszy opis w treści wyjątku. Pozwala to na dużo łatwiejsze znajdowanie błędów w programie jeśli komunikat wyjątku jest szczegółowy.
- Nie zapominaj o używaniu wyjątków typu checked. Chociaż wymagają trochę więcej kodu i generują często irytujące błędy kompilacji ich używanie jest czasami wskazane.
Zadanie
Napisz program, który pobierze od użytkownika liczbę i wyświetli jej pierwiastek. Do obliczenia pierwiastka możesz użyć istniejącej metody java.lang.Math.sqrt()
. Jeśli użytkownik poda liczbę ujemną rzuć wyjątek java.lang.IllegalArgumentException
. Obsłuż sytuację, w której użytkownik poda ciąg znaków, który nie jest liczbą.
Zachęcam do samodzielnego rozwiązania zadania, jeśli rozwiązujesz zadanie samodzielnie uczysz się najwięcej. Jeśli jednak chciałbyś zobaczyć przykładowe rozwiązanie, to umieściłem je na githubie.
Dodatkowe materiały do nauki
- http://docs.oracle.com/javase/tutorial/essential/exceptions/index.html
- https://docs.oracle.com/javase/specs/jls/se12/html/jls-11.html
- https://www.youtube.com/watch?v=Nl16BzP6Fao
- kod źródłowy przykładów użytych w artykule
Podsumowanie
Bardzo się cieszę, że dobrnąłeś tak daleko. Mam nadzieję, że artykuł przypadł Ci do gustu. Na koniec mam do Ciebie prośbę, podziel się artykułem ze swoimi znajomymi, którzy mogą być zainteresowani tym tematem. Niezmiennie zależy mi na tym, żeby dotrzeć do jak największej grupy czytelników :) Jeśli nie chcesz ominąć kolejnych artykułów polub nasz profil na Facebook’u i dopisz się do newslettera.
Do następnego razu i życzę Ci udanego dnia :)
-
Są pewne sytuacje kiedy to nie jest prawdą, np. jeśli wirtualna maszyna Javy zostanie wyłączona. ↩
Pobierz opracowania zadań z rozmów kwalifikacyjnych
Przygotowałem rozwiązania kilku zadań algorytmicznych z rozmów kwalifikacyjnych. Rozkładam je na czynniki pierwsze i pokazuję różne sposoby ich rozwiązania. Dołącz do grupy ponad 6147 Samouków, którzy jako pierwsi dowiadują się o nowych treściach na blogu, a prześlę je na Twój e-mail.
Zostaw komentarz