Rozdział 11 - Wyjątki - Wyjątki Checked & Unchecked

Jak już kilka razy wspominałem, istnieją dwa typy wyjątków:

  • checked exceptions,
  • unchecked exceptions.

Główną różnicą pomiędzy tymi dwoma rodzajami wyjątków jest to, że gdy zamierzamy rzucić wyjątek tego pierwszego rodzaju (checked), to musimy go umieścić w klauzuli throws w sygnaturze naszej metody, natomiast w przypadku Unchecked Exceptions nie musimy tego robić (chociaż możemy).

To jest właśnie powód tego, że w niektórych przykładach w tym rozdziale korzystaliśmy z throws do zaznaczenia wyjątków rzucanych przez metodę, a w innych nie.

W przykładzie z dzieleniem liczb:

Nazwa pliku: ZwrocWynikDzielenia.java
public class ZwrocWynikDzielenia {
  public static void main(String[] args) {
    System.out.println(podziel(10, 0));
    System.out.println(podziel(25, 5));
  }

  public static int podziel(int x, int y) {
    return x / y;
  }
}

Pomimo, że wywołanie metody podziel może zakończyć się wyjątkiem ArithmeticException, nie korzystamy ani z throws do zaznaczenia tego faktu, ani nie musimy tego wyjątku obsługiwać. To dlatego, że ArithmeticException jest przykładem wyjątku Unchecked.

Z drugiej jednak strony, w przykładach w klasie Osoba, w której konstruktorze rzucaliśmy wyjątki NieprawidlowyWiekException i NieprawidlowaWartoscException, zaznaczyliśmy ten fakt za pomocą throws w sygnaturze konstruktora, a w metodzie main tej klasy, gdzie korzystaliśmy z tego konstruktora, stosowaliśmy try..catch:

fragment pliku Osoba.java
public Osoba(String imie, String nazwisko, int wiek)
    throws NieprawidlowaWartoscException, NieprawidlowyWiekException {
  if (imie == null) {
    throw new NieprawidlowaWartoscException(
        "Imie nie moze byc puste."
    );
  }

  // ... pozostaly fragment konstruktora został pominiety
}

public static void main(String[] args) {
  try {
    Osoba o = new Osoba("Jan", "Nowak", -1);
  } catch (NieprawidlowaWartoscException e) {
    System.out.println("Nieprawidlowa wartosc: " + e.getMessage());
  } catch (NieprawidlowyWiekException e) {
    System.out.println("Nieprawidlowy wiek!");
  }
}

Jak rozpoznać wyjątki Checked i Unchecked?

O tym, czy wyjątek to wyjątek Checked czy Unchecked decyduje tylko jedna właściwość – czy klasa wyjątku dziedziczy po klasie RuntimeException.

Wyjątki Unchecked nazywane są także runtime exceptions.

Co oznacza powyższe rozróżnienie?

Wyjątki to pochodne klasy Exceptionklasa RuntimeException także jest pochodną klasy Exception. Jeżeli wyjątek w swojej hierarchii dziedziczenia nie ma klasy RuntimeException, to jest wyjątkiem Checked i trzeba go definiować w klauzuli throws w sygnaturze metody i obsługiwać w try..catch. Jeżeli ma w hierarchii klasę RuntimeException, to jest rodzaju Unchecked. Spójrzmy na kilka przykładów zdefiniowania własnych wyjątków:

// checked exceptions
class MojWyjatek extends Exception {}
class MojKolejnyWyjatek extends MojWyjatek {}

// unchcked exceptions
class MojRuntimeWyjatek extends RuntimeException {}
class MojKolejnyRuntimeWyjatek extends MojRuntimeWyjatek {}

Pierwsze dwa wyjatki mają następującą hierarchię klas (począwszy od klasy Exception):

Exception
    MojWyjatek
        MojKolejnyWyjatek

MojKolejnyWyjatek jest wyjątkiem rodzaju Checked, ponieważ ma w swojej hierarchii typ Exception oraz nie ma typu RuntimeException (podobnie jak typ MojWyjatek).

Z kolei dwa ostatnie wyjątki mają następującą hierarchię klas (począwszy od klasy Exception):

Exception
    RuntimeException
        MojRuntimeWyjatek
            MojKolejnyRuntimeWyjatek

Wyjątki te mają w swojej hierarchii klasę RuntimeException, są więc wyjątkami rodzaju Unchecked i nie trzeba ich definiować w klauzuli throws.

Wyjątki Unchecked można łapać tak samo w try..catch jak wyjątki Checked – widzieliśmy już taki przykład podczas obsługi metody podziel, gdy występował potencjał dzielenia przez 0, oraz używając wyjątku IllegalArgumentException w pierwszej wersji klasy Osoba – ten wyjątek także jest wyjątkiem Unchecked.

Dlaczego istnieją dwa rodzaje wyjątków?

Idea wyjątków Checked jest taka, że są to wyjątki, które zazwyczaj można obsłużyć, i pozwolić na dalsze wykonywanie programu, np. gdy użytkownik poda nieprawidłową nazwę pliku do otwarcia możemy mu pozwolić ponownie spróbować ją podać. Takie wyjątki chcemy obsługiwać i zaznaczamy potencjał ich rzucenia w sygnaturze metody za pomocą słowa kluczowego throws. Jest to informacja dla każdego użytkownika naszej metody:

"Jeśli będziesz korzystał z tej metody, to mogą się pojawić takie a takie wyjątki – powinieneś wziąć je pod uwagę i obsłużyć wedle własnego uznania."

Wyjątki Unchecked są często spowodowane błędnym stanem naszego programu i mogłoby być ciężko zareagować na nie w odpowiedni sposób – zamiast tego pozwalamy im zostać przetworzonymi przez Maszynę Wirtualną Java, nie podejmując sie sami próby ich obsługi. Takie wyjątki powinny zostać zauważne, a kod, które je powoduje, naprawiony, zamiast próbować obsłużyć je w kodzie za pomocą try..catch. Nie oznacza to jednak, że nie możemy obsługiwać wyjątków Unchecked w try..catch – możemy, jeżeli mamy taką potrzebę.

Sprawdzanie wyjątków rzucanych przez metodę

Korzystając z różnych bibliotek, w tym ze Standardowej Biblioteki Java, możemy czasem mieć potrzebę sprawdzić, jakie wyjątki dana metoda może rzucić – aby to zrobić, wystarczy zajrzeć do dokumentacji klasy, do której dana metoda należy.

Dla przykładu – używana już przez nas metoda charAt z klasy String może rzucić wyjątek IndexOutOfBoundsException. Dokładny opis można znaleźć w Java Doc. Z racji tego, że wyjątek ten jest rodzaju Unchecked, to korzystając z metody charAt nie musieliśmy stosować try..catch do łapania i obsługi tego wyjątku w poprzednich rozdziałach.

Jeżeli korzystamy z IntelliJ IDEA, możemy także sprawdzić wyjątki rzucane przez metodę poprzez naciśnięcie i przytrzymanie przycisku Ctrl na klawiaturze i kliknięcie lewym przyciskiem myszy nazwy metody – spowoduje to przejście do definicji tej metody, gdzie będziemy mogli spojrzeć na jej sygnaturę i sprawdzić, czy korzysta ze słowa kluczowego throws do zadeklarowania potencjału rzucenia pewnych wyjątków.

Wykorzystując powyższy sposób, spójrzmy na metodę charAt z klasy String:

/**
 * Returns the {@code char} value at the
 * specified index. An index ranges from {@code 0} to
 * {@code length() - 1}. The first {@code char} value of the sequence
 * is at index {@code 0}, the next at index {@code 1},
 * and so on, as for array indexing.
 *
 * <p>If the {@code char} value specified by the index is a
 * <a href="Character.html#unicode">surrogate</a>, the surrogate
 * value is returned.
 *
 * @param      index   the index of the {@code char} value.
 * @return     the {@code char} value at the specified index of this string.
 *             The first {@code char} value is at index {@code 0}.
 * @exception  IndexOutOfBoundsException  if the {@code index}
 *             argument is negative or not less than the length of this
 *             string.
 */
public char charAt(int index) {
    if (isLatin1()) {
        return StringLatin1.charAt(value, index);
    } else {
        return StringUTF16.charAt(value, index);
    }
}

Jest to fragment klasy String ze standardowej biblioteki Java. Jak widzimy, sygnatura metody charAt nie zawiera throws, czyli nie rzuca wyjątków rodzaju Checked – nie trzeba więc stosować try..catch, gdy z niej korzystamy.

Warto jednak zauważyć, iż autorzy języka Java zawarli w komentarzu dokumentacyjnym sekcję @exception, w której napisali, że metoda ta może rzucić wyjątek IndexOutOfBoundsException. Jest to co prawda wyjątek Unchecked i nie musimy go obsługiwać, jednak gdybyśmy chcieli to zrobić, to mamy tutaj informację, że taki wyjątek może zostać rzucony i jeżeli mamy taką potrzebę, to możemy go obsłużyć.

Jak widzisz, w kodzie tej metody nie ma bezpośredniego rzucania wyjątku IndexOutOfBoundsException – skąd więc taki komentarz twórców języka Java? Otóż wyjątek ten może rzucić jedna z metod wywoływanych przez tą metodę, a dokładniej StringLatin1.charAt.

Jak sprawdzić rodzaj wyjątku

Sprawdzanie rodzaju wyjątku sprowadza się do analizy jego hierarchii dziedziczenia – jeżeli jest w niej zawarta klasa RuntimeException, oznacza to, że jest to wyjątek Unchecked i nie trzeba go obsługiwać w try..catch.

W przeciwnym razie wyjątek jest rodzaju Checked i jeżeli metoda, z której korzystamy, rzuca go, to musimy go prędzej czy później obsłużyć. Jak zobaczymy w jednym z kolejnych rozdziałów nie zawsze musimy koniecznie od razu obsługiwać wyjątki rodzaju Checked.

Jak jednak sprawdzić hierarchię dziedziczenia wyjątku? Najłatwiej sprawdzić to w dokumentacji – jeżeli korzystamy z klasy ze Standardowej Biblioteki Java, to informację o klasach znajdziemy w Java Doc. Dla przykładu, dla wyjątku IllegalArgumentException hierarchia dziedziczenia wygląda następująco:

java.lang.Object
    java.lang.Throwable
        java.lang.Exception
            java.lang.RuntimeException
                java.lang.IllegalArgumentException

źródło: oficjalna dokumentacja Java Doc – klasa Illegal ArgumentException

Widzimy w tej hierarchii klasę RuntimeException, więc klasa IllegalArgumentException jest przedstawicielką wyjątków rodzaju Unchecked.

Jeżeli korzystamy z wyjątków z innej biblioteki, to musimy sprawdzić to w dokumentacji tej biblioteki w internecie.

Błędy kompilacji podczas braku obsługi wyjątków Checked

Jeżeli skorzystamy z metody, która rzuca wyjątki rodzaju Checked, a nie obsłużymy ich w try..catch, to nasza klasa się nie skompiluje – będzie to dla nas informacja, że brakuje obsługi wyjątków – widzieliśmy taki przypadek w klasie Osoba w tym rozdziale. Definiując, że konstruktor klasy Osoba może rzucić dwa wyjątki rodzaju Checked:

public Osoba(String imie, String nazwisko, int wiek)
    throws NieprawidlowaWartoscException, NieprawidlowyWiekException {

i korzystając z poniższego fragmentu kodu:

Osoba o = new Osoba("Jan", "Nowak", -1);

spowodujemy, że próba kompilacji zakończy się następującym błędem:

Osoba.java:31: error: unreported exception NieprawidlowaWartoscException; must be caught or declared to be thrown Osoba o = new Osoba("Jan", "Nowak", -1); ^ 1 error

Gdy korzystamy z IntelliJ IDEA to łatwo wykryć sytuację, gdy korzystamy z kodu, który powinien zostać opakowany w blok try..catch jeszcze zanim spróbujemy skompilować nasz kod. IntelliJ IDEA jako mądre narzędzie analizuje nasz kod na bieżąco i wykrywa, że zapomnieliśmy o złapaniu wyjątków – podkreśli wtedy fragment kodu, który może rzucić nieobsłużone wyjątki, dając nam znać, że powinniśmy to zrobić:

IntelliJ IDEA informuje o braku zlapania wyjatkow

Błąd Error

Istnieje jeszcze trzeci rodzaj wyjątków, które są pochodnymi klasy Error. Podobnie jak klasa Exception, dziedziczy ona bezpośrednio po Throwable, czyli klasie, która jest klasa nadrzędną wszystkich wyjątków.

Chociaż Throwable to ta właściwa klasa nadrzędna dla wszystkich wyjątków, to w praktyce nigdy z niej bezpośrednio nie korzystamy – zamiast tego jako klas bazowych dla naszych wyjątków używamy klasy Exception bądź jej pochodnych.

Wyjątki dziedziczące po Error to błędy krytyczne, których za bardzo nie da się obsłużyć i nie powinniśmy próbować tego robić. Sami też takich wyjątków nie będziemy nigdy rzucać. Taki błąd to np. OutOfMemoryError, który występuje gdy skończy się pamieć komputera przeznaczona dla naszego programu. My, jako autorzy programu, nie możemy nic na wystąpienie takiego wyjątku poradzić. To, co możemy i powinniśmy zrobić, to przeanalizować dlaczego pamięć się skończyła:

  • Czy po prostu za mało pamięci zostało przeznaczone na działanie naszego programu?
  • Czy nasz program nie jest optymalnie napisany?
  • Czy w programie występuje błąd, który powoduje niemożliwość zwalniania pamięci przez Garbage Collector (mechanizm odpowiedzialny za zwalnianie nieużywanej pamięci w naszych programach)?

Występienie wyjątku typu Error to poważniejszy problem, który trzeba przeanalizować – jego obsługa w kodzie albo nie ma sensu, albo wręcz nie jest możliwa.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Nie musisz podawać swojego imienia, e-mailu, ani strony www, aby opublikować komentarz. Komentarze muszą zostać zatwierdzone, aby były widoczne na stronie.