mainejug.org

Praktyczna wiedza na co dzień

mainejug.org

Praktyczna wiedza na co dzień

Java

Obsługa wyjątków w Javie: dobre praktyki, które oszczędzają godziny debugowania

Wyjątki to jeden z tematów, które w Javie na początku wydają się proste, a z czasem pokazują coraz więcej niuansów. Dobrze obsłużony błąd ułatwia znalezienie przyczyny problemu, a źle obsłużony potrafi ukryć go na wiele tygodni. Poniżej zebrano praktyki, które warto stosować w codziennej pracy, niezależnie od tego, czy piszesz małą aplikację konsolową, czy duży system serwerowy.

Rodzaje wyjątków: kontrolowane i niekontrolowane

W Javie wyjątki dzielą się na dwie grupy. Wyjątki kontrolowane (checked) dziedziczą po Exception, ale nie po RuntimeException, a kompilator wymaga ich obsłużenia albo zadeklarowania w sygnaturze metody. Typowym przykładem jest IOException. Wyjątki niekontrolowane (unchecked), czyli potomkowie RuntimeException, nie wymuszają takiej deklaracji; należą do nich NullPointerException czy IllegalArgumentException. Osobną gałąź stanowią błędy dziedziczące po Error, jak OutOfMemoryError, których zwykle nie próbuje się obsługiwać.

Przyjmuje się, że wyjątki kontrolowane stosujemy tam, gdzie wywołujący może sensownie zareagować na problem, a niekontrolowane dla błędów programistycznych lub naruszeń kontraktu metody.

Nie połykaj wyjątków

Najgorszym nawykiem jest pusty blok catch:

try {
    zapiszPlik(sciezka);
} catch (IOException e) {
    // nic
}

Program będzie działał dalej, jakby nic się nie stało, a ty nie dowiesz się, że zapis się nie powiódł. Jeśli naprawdę nie możesz zareagować, przynajmniej zapisz błąd w logu razem z całym śladem stosu. Najlepiej jednak przekaż go wyżej, tam gdzie jest wystarczająco dużo kontekstu, aby podjąć decyzję.

Podobnie nie łap zbyt ogólnych typów, takich jak Exception czy Throwable, jeśli nie masz do tego powodu. Szeroki catch może przechwycić błędy, których się nie spodziewałeś, i ukryć prawdziwy problem.

Try-with-resources zamiast ręcznego zamykania

Zasoby takie jak pliki, strumienie czy połączenia z bazą trzeba zawsze zamknąć. Dawniej robiło się to w bloku finally, co było rozwlekłe i podatne na błędy. Od Javy 7 istnieje konstrukcja try-with-resources, która zamyka zasoby automatycznie:

try (BufferedReader reader = Files.newBufferedReader(sciezka)) {
    String linia = reader.readLine();
    System.out.println(linia);
} catch (IOException e) {
    throw new UncheckedIOException(e);
}

Zasób musi implementować interfejs AutoCloseable. Jeśli podczas wykonywania bloku i zamykania zasobu wystąpią błędy, wyjątek z zamykania zostanie dołączony jako tzw. suppressed, a nie nadpisze pierwotnej przyczyny.

Twórz sensowne komunikaty i zachowuj przyczynę

Gdy łapiesz jeden wyjątek i rzucasz inny, zawsze przekaż oryginalny jako przyczynę. Dzięki temu cały łańcuch jest widoczny w logach:

try {
    return parser.parse(dane);
} catch (ParseException e) {
    throw new ConfigException("Nie udało się wczytać konfiguracji: " + plik, e);
}

Dobry komunikat odpowiada na pytania: co się nie powiodło, na jakich danych i dlaczego. Zamiast „Błąd” napisz „Nie znaleziono zamówienia o identyfikatorze 123”. Uważaj jednak, aby nie umieszczać w komunikatach danych wrażliwych, takich jak hasła, tokeny czy dane osobowe, bo trafią do logów.

Zasady, o których warto pamiętać

  1. Rzucaj wcześnie, łap późno. Walidacja argumentów powinna następować na początku metody, a obsługa błędu tam, gdzie można zdecydować o dalszym działaniu, na przykład na granicy warstwy.
  2. Nie używaj wyjątków do sterowania przepływem. Rzucanie ich w normalnych, spodziewanych sytuacjach utrudnia czytanie kodu i bywa wolne.
  3. Nie loguj i nie rzucaj ponownie tego samego błędu na każdym poziomie. Skutkuje to wielokrotnymi wpisami o tym samym problemie. Zaloguj w miejscu, gdzie błąd jest ostatecznie obsłużony.
  4. Dokumentuj wyjątki w Javadoc. Znacznik @throws informuje użytkowników metody o spodziewanych sytuacjach.
  5. Tworzenie własnych wyjątków. Rób to wtedy, gdy wnoszą informację biznesową, a nie dla samej zasady.

Własne wyjątki w praktyce

Własny wyjątek bywa przydatny, gdy chcesz odróżnić konkretną sytuację od innych błędów. Wystarczy prosta klasa:

public class ZamowienieNieIstniejeException extends RuntimeException {
    public ZamowienieNieIstniejeException(long id) {
        super("Nie znaleziono zamówienia: " + id);
    }
}

Pomyśl, czy wywołujący będzie w stanie coś z tym zrobić. Jeśli tak, rozważ wyjątek kontrolowany, a jeśli to raczej błąd użycia, zostań przy niekontrolowanym. W aplikacjach webowych często stosuje się jedną, centralną warstwę, która zamienia wyjątki na odpowiednie odpowiedzi dla użytkownika.

Podsumowanie

Dobra obsługa wyjątków polega na rzetelnej informacji o tym, co poszło nie tak, i o zachowaniu kontroli nad stanem programu. Nie ukrywaj błędów, zamykaj zasoby przez try-with-resources, pisz zrozumiałe komunikaty i zachowuj przyczynę. Przy następnym code review przejrzyj bloki catch w swoim projekcie: jeśli któryś jest pusty lub zbyt ogólny, to dobre miejsce na pierwszą poprawkę.

Warto jeszcze wspomnieć o testowaniu obsługi błędów. Napisz testy, które sprawdzają, że metoda rzuca oczekiwany wyjątek dla niepoprawnych danych, na przykład za pomocą assertThrows w JUnit 5. Dzięki temu zmiana zachowania nie przejdzie niezauważona, a dokumentacja w postaci testów pokazuje kolejnym osobom, jakich sytuacji można się spodziewać. Obsługa wyjątków, którą się sprawdza, jest zwykle znacznie bardziej niezawodna niż ta, którą się tylko zakłada.

https://wnetrzaikrajobraz.pl/ranking-abonamentow-telefonicznych-ktory-operator-wypada-najlepiej/ | https://imerp.pl/dla-kogo-jest-luxmed-pakiet-rodzinny-i-jak-go-wybrac/ | https://za10froszy.pl/luxmed-pakiet-pracowniczy-dla-firm-inwestycja-w-zdrowie-zespolu-i-bliskich/