Łapanie wyjątku zamiast wysypania programu
Wiesz już, że gdy coś pójdzie nie tak, Java rzuca wyjątek, a nieobsłużony wyjątek zatrzymuje program ze stack trace'em. Blok try-catch pozwala przejąć kontrolę: ryzykowny kod opakowujesz w try, a jeśli rzuci wyjątek, Java przeskakuje do pasującego bloku catch, zamiast przerwać działanie.
W chwili, gdy 10 / 0 rzuca wyjątek, reszta bloku try zostaje pominięta, a sterowanie przechodzi do catch. Po zakończeniu catch wykonanie toczy się normalnie dalej: program nie umiera.
Zmienna w catch przechowuje wyjątek
e w catch (ArithmeticException e) to prawdziwy obiekt. Niesie informacje o tym, co poszło nie tak, a najprzydatniejszy jest komunikat:
e.getMessage() zwraca krótki opis. Podczas debugowania e.printStackTrace() wypisuje pełny ślad stosu, który pokazuje dokładnie, skąd wziął się wyjątek. Sięgnij po niego, gdy sam komunikat nie wystarcza, żeby znaleźć przyczynę.
Łap konkretny typ, a nie wszystko
Blok catch łapie tylko wyjątki pasujące do zadeklarowanego typu (albo jego podklasy). Największy błąd początkujących to łapanie Exception, żeby problem "zniknął":
// Nie rób tak: to ukrywa prawdziwe bugi
try {
doWork();
} catch (Exception e) {
// połyka NullPointerException, literówki, błędy logiczne... wszystko
}
Łap najwęższy typ, który naprawdę potrafisz obsłużyć. Jeśli spodziewasz się błędnej liczby na wejściu, łap NumberFormatException. Wszystko, czego nie przewidziano, powinno móc się propagować, żeby faktycznie się o tym dowiedzieć, zamiast po cichu działać dalej w zepsutym stanie.
Obsługa kilku typów wyjątków
Możesz ułożyć kilka bloków catch jeden pod drugim. Java sprawdza je od góry do dołu i wykonuje pierwszy pasujący, więc układaj je od najbardziej konkretnego do najbardziej ogólnego:
Gdy dwa typy obsługujesz tak samo, użyj jednego multi-catch z |, zamiast powielać blok:
Pułapka: typ ogólniejszy musi stać po konkretnych. Jeśli catch (Exception e) postawisz na początku, późniejsze, bardziej konkretne bloki stają się nieosiągalne i kompilator odrzuca taki kod.
finally wykonuje się zawsze
Blok finally wykonuje się po try i ewentualnym catch, bez względu na to, co się stało: sukces, złapany wyjątek, a nawet wczesny return. To właściwe miejsce na sprzątanie, które zawsze musi się odbyć.
"Closing resource" zostaje wypisane niezależnie od tego, czy wyjątek wystąpił. Unikaj jednak return wewnątrz finally: może po cichu odrzucić wyjątek albo nadpisać wartość zwróconą z bloku try.
try-with-resources zamyka zasoby za ciebie
Gdy pracujesz z czymś, co trzeba zamknąć (plikiem, połączeniem sieciowym, zapytaniem do bazy danych), zadeklarowanie tego wewnątrz try (...) zamyka zasób automatycznie po zakończeniu bloku, nawet jeśli poleci wyjątek. Działa to z każdym typem, który implementuje AutoCloseable.
To zastępuje stary wzorzec otwierania w try i zamykania w finally, a przy tym trudniej o błąd, bo nie da się zapomnieć o zamknięciu. Wybieraj ten zapis zawsze, gdy masz do czynienia z zasobem, który trzeba zamknąć.
Nie używaj wyjątków do zwykłego sterowania przepływem
try-catch służy do sytuacji wyjątkowych, a nie zwyczajnych warunków. Złapanie wyjątku kosztuje więcej niż zwykłe sprawdzenie i utrudnia czytanie kodu. Jeśli możesz najpierw sprawdzić warunek, zrób to:
// Unikaj: używania catch do sprawdzania brakującego klucza
try {
process(map.get(key).trim());
} catch (NullPointerException e) {
// obsługa brakującego klucza
}
// Lepiej: sprawdź wprost
String value = map.get(key);
if (value != null) {
process(value.trim());
}
Zostaw catch dla rzeczy naprawdę poza twoją kontrolą: błędnych danych od użytkownika, brakujących plików, awarii sieci.
Dalej: NullPointerException
Najczęstszy wyjątek, który będziesz łapać (i powodować) w Javie, to NullPointerException: pojawia się w chwili, gdy wywołujesz metodę na czymś, co okazało się null. Dalej przyjrzymy się dokładnie temu, co go wywołuje, jak czytać jego stack trace i jakie nawyki sprawiają, że w ogóle się nie pojawia.
Najczęściej zadawane pytania
Jak używać try-catch w Javie?
Umieść kod, który może rzucić wyjątek, w bloku try, a potem dodaj blok catch z typem wyjątku, który chcesz obsłużyć: try { risky(); } catch (IOException e) { ... }. Jeśli kod w try rzuci pasujący wyjątek, Java przeskakuje od razu do bloku catch, zamiast przerwać program. Zmienna (e) przechowuje obiekt wyjątku, więc jego komunikat odczytasz przez e.getMessage().
Do czego służy blok finally w Javie?
Blok finally wykonuje się po try/catch bez względu na wszystko: gdy kod zadziałał, gdy rzucił wyjątek, a nawet gdy wcześniej wykonał return. To miejsce na sprzątanie, które zawsze musi się odbyć, np. zamknięcie pliku albo zwolnienie blokady. Do zamykania zasobów zwykle lepiej nadaje się try-with-resources, bo zamyka je automatycznie.
Łapać Exception czy konkretny typ wyjątku?
Łap najbardziej konkretny typ, który naprawdę potrafisz obsłużyć. Łapanie Exception (albo, co gorsza, Throwable) połyka każdy problem, łącznie z bugami takimi jak NullPointerException, i ukrywa prawdziwą przyczynę. Łap NumberFormatException, jeśli tego się spodziewasz, a nieoczekiwanym wyjątkom pozwól się propagować, żeby się o nich dowiedzieć.