Rozdział 11 - Wyjątki - Łapanie wyjątków

W poprzednim rozdziale rzucaliśmy wyjątki za pomocą słowa kluczowego throw, po którym następował wyjątek. Wyjątek to po prostu obiekt konkretnej klasy wyjątku. Gdy łapiemy wyjątek w sekcji catch, podajemy typ wyjątku, jaki chcemy obsłużyć, oraz nazwę zmiennej, za pomocą której będziemy się do tego obiektu-wyjątku odnosić.

We wszystkich przykładach do tej pory nazywaliśmy tą zmienną po prostu e, np.:

fragment metody main z pliku Osoba.java
try {
  Osoba o = new Osoba("Jan", "Nowak", -1);
} catch (NieprawidlowaWartoscException e) { // 1
  System.out.println("Nieprawidlowa wartosc: " + e.getMessage());
} catch (NieprawidlowyWiekException e) { // 2
  System.out.println("Nieprawidlowy wiek!");
}

W tym przykładzie spodziewamy się dwóch różnych wyjątków, które chcemy obsłużyć. Do każdego z nich w odpowiedniej sekcji catch będziemy mogli odnieść się za pomocą zmiennej o nazwie e (1) (2). Obiekt, na który ta zmienna będzie wskazywać w każdym z tych przypadków, to obiekt klasy wyjątku utworzonego w konstruktorze klasy Osoba:

fragment pliku Osoba.java – konstruktora klasy Osoba
public Osoba(String imie, String nazwisko, int wiek)
    throws NieprawidlowaWartoscException, NieprawidlowyWiekException {
  if (imie == null) {
    throw new NieprawidlowaWartoscException( // 3
        "Imie nie moze byc puste."
    );
  }
  if (nazwisko == null) {
    throw new NieprawidlowaWartoscException( // 4
        "Nazwisko nie moze byc puste."
    );
  }
  if (wiek <= 0) {
    throw new NieprawidlowyWiekException(); // 5
  }

  this.imie = imie;
  this.nazwisko = nazwisko;
  this.wiek = wiek;
}

Gdy przekazane imię lub nazwisko będzie nullem (3) (4), to zmienna e z pierwszej sekcji catch (1) będzie wskazywała na obiekt typu NieprawidlowaWartoscException utworzony w (3) bądź (4). Analogicznie, jeżeli wiek będzie nieprawidłowy, to zmienna e w drugiej sekcji catch (2) będzie odnosiła się do obiektu typu NieprawidlowyWiekException tworzonego i rzucanego w (5).

W sekcji catch zmienna wyjątku może mieć dowolną nazwę, niekoniecznie musi to być e. Taka nazwa jest jednak używana dla wygody, tym bardziej, że nie ma ona specjalnie znaczenia – typ i treść wyjątku ma większe znaczenie, niż nazwa zmiennej, której chwilowo używamy do odnoszenia się do niego.

Łapanie wyjątków za pomocą klasy bazowej

Załóżmy, że mamy metodę, która może rzucić wiele wyjątków, ale my, jako osoby używające tej metody, nie chcemy obsługiwać osobno każdego z nich. Zamiast wypisywać poszczególne wyjątki, możemy zamiast tego złapać wyjątek nadrzędny dla naszych wyjątków (którym zawsze jest wyjątek typu Exception) i w jednym miejscu obsłużyć wszystkie błędy. Spójrzmy jak by to wyglądało w klasie Osoba:

fragment metody main z pliku Osoba.java
try {
  Osoba o = new Osoba("Joanna", "Strzelczyk", -1);
} catch (Exception e) {
  System.out.println("Wystapil blad! Komunikat bledu: " + e.getMessage());
}

try {
  Osoba o = new Osoba(null, "Strzelczyk", 30);
} catch (Exception e) {
  System.out.println("Wystapil blad! Komunikat bledu: " + e.getMessage());
}

Zamiast łapać konkretne wyjątki, złapaliśmy po prostu wszystkie wyjątki korzystając z klasy bazowej wyjątków – klasy Exception. Kod działa, ponieważ zarówno wyjątek NieprawidlowaWartoscException, jak i NieprawidlowyWiekException, bazują na typie Exception, więc kwalifikują się do złapania. Widzimy tutaj dziedziczenie i polimorfizm w akcji – korzystamy z klasy bazowej, a potencjalnie działamy na obiektach klas pochodnych. Klasa Exception jest "bardziej ogólna" niż dwa pozostałe wyjątki.

Zapisując kod w ten sposób mówimy kompilatorowi:

"Nieważne czy to będzie NieprawidlowaWartoscException czy NieprawidlowyWiekException, każdy z nich to Exception i chcę je obsłużyć w tej jednej sekcji catch".

W wyniku działania tego fragmentu kodu zobaczymy na ekranie:

Wystapil blad! Komunikat bledu: null Wystapil blad! Komunikat bledu: Imie nie moze byc puste.

W pierwszej linii widzimy, że komunikat błędu jest nullem – wynika to z faktu, że tworząc wyjątek typu NieprawidlowyWiekException w konstruktorze klasy Osoba nie podajemy żadnego komunikatu błędu.

Możemy także złapać konkretne wyjątki, a na końcu podać Exception, by obsłużyć wyjątki pewnego rodzaju, a pozostałe obsłużyć w bloku obsługi ogólnego wyjątku Exception:

try {
  Osoba o = new Osoba("Jan", "Nowak", -1);
} catch (NieprawidlowyWiekException e) {
  System.out.println("Nieprawidlowy wiek!");
} catch (Exception e) {
  System.out.println("Wystapil blad! Komunikat bledu: " + e.getMessage());
}

try {
  Osoba o = new Osoba(null, "Nowak", 30);
} catch (NieprawidlowyWiekException e) {
  System.out.println("Nieprawidlowy wiek!");
} catch (Exception e) {
  System.out.println("Wystapil blad! Komunikat bledu: " + e.getMessage());
}

Ten fragment kodu spowodowałby wypisanie na ekran:

Nieprawidlowy wiek! Wystapil blad! Komunikat bledu: Imie nie moze byc puste.
Używanym tutaj "wyjątkiem ogólnym" nie musi być Exception, lecz dowolny typ wyjątku, który byłby w hierarchii dziedziczenia używanych przez nas klas wyjątków w klasie Osoba. Dla przykładu, jeżeli wyjątki NieprawidlowaWartoscException i NieprawidlowyWiekException dziedziczyłyby nie bezpośrednio po Exception, lecz po innym utworzonym przez nas typie wyjątków, np. BladWalidacjiDanychOsobyException, to moglibyśmy używać tego typu wyjątku w sekcji catch, aby złapać oba rodzaje wyjątków pochodnych od tego nowego typu wyjątku.

Łapanie kilku wyjątków za pomocą znaku |

Jest jeszcze inna składnia pozwalająca na łapanie wykluczających się wyjątków – możemy rozdzielić je w klauzuli catch za pomocą znaku | (pionowa kreska, ang. pipe):

fragment metody main z pliku Osoba.java
try {
  Osoba o = new Osoba("Adrian", "Sochacki", 30);
} catch (NieprawidlowaWartoscException | NieprawidlowyWiekException e) {
  System.out.println("Nieprawidlowa wartosc: " + e.getMessage());
}

W jednej sekcji catch łapiemy dwa wyjątki, rozdzielając nazwy ich klas znakiem |. Wyjątki te nie mogą dziedziczyć po sobie – jeżeli spróbowalibyśmy w ten sposób umieścić w catch np. wyjątki Exception | NieprawidlowaWartoscException, to kod nie skompilowałby się, ponieważ typ wyjątku Exception jest typem bazowym wyjątku NieprawidlowaWartoscException.

Typ Exception jest "bardziej ogólny", więc już samo jego umieszczenie w sekcji catch powoduje, że i wyjątek NieprawidlowyWiekException, który jest klasą pochodną od Exception, zostanie złapany. Kompilator wykryłby taką sytuację i zgłosiłby błąd.

Kolejność łapania wyjątków ma znaczenie

Kolejność obsługiwania wyjątków w blokach catch ma znaczenie – bardziej ogólne wyjątki muszą zawsze następować po mniej ogólnych. Najbardziej ogólnymi wyjątkami są wyjątki typu Exception (ponieważ wszystkie wyjątki bazują na tym typie), a mniej ogólne to te, które dziedziczą po klasie Exception. Możemy rozszerzać inne wyjątki zdefiniowane w bibliotece standardowej Java, a także nasze własne wyjątki, więc musimy pamiętać o hierarchii. Jeżeli kolejność będzie nieprawidłowa, kompilator zaprotestuje:

try {
  Osoba o = new Osoba(null, "Nowak", 30);
} catch (Exception e) {
  System.out.println("Wystapil blad! Komunikat bledu: " + e.getMessage());
} catch (NieprawidlowyWiekException e) {
  System.out.println("Nieprawidlowy wiek!");
}

Ten fragment kodu powoduje błąd kompilacji:

Error: java: exception NieprawidlowyWiekException has already been caught

Komunikat mówi o tym, że już obsłużyliśmy wyjątek typu NieprawidlowyWiekException – nastąpiło to w pierwszym bloku catch – wyjątek ten został dopasowany do pierwszego bloku catch, ponieważ wyjątek NieprawidlowyWiekException jest klasą pochodną klsay Exception.

Innymi słowy, sekcja catch z "bardziej ogólnym" wyjątkiem Exception obsługuje wszystkie wyjątki klasy Exception i jej klas pochodnych. W związku z tym, sekcja catch z wyjątkiem NieprawidlowyWiekException nigdy nie miałaby szansy zostać wykonana. Kompilator wykrywa ten problem i nie pozwala na skompilowanie takiego kodu.

Pamiętajmy, by sekcje catch zawsze zawierały najbardziej szczegółowe (najniżej w hierarchii dziedziczenia) wyjatki na początku, a najbardziej ogólne – na końcu. Obsługa wyjątków za pomocą typu Exception powinna zawsze być ostatnią sekcją catch, ponieważ załapią się do niej wszystkie rodzaje wyjątków w związku z tym, że klasa ta jest klasą bazową dla wszystkich wyjątków.

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.