Rozdział 11 - Wyjątki - Definiowanie i rzucanie wyjątków

W poprzednich przykładach, wyjątek ArithmeticException rzucany był przez Maszynę Wirtualną Java, jednak nie jest to jedyna możliwość rzucania wyjątków – my, jako programiści, możemy sami rzucać wyjątki z naszych metod.

Rzucanie wyjątków odbywa się poprzez użycie słowa kluczowego throw, po którym następuje tworzenie obiektu wyjątku takiego typu, jaki chcemy rzucić. Spójrzmy na przykład obsługi sytuacji, gdy ktoś poda ujemny wiek podczas tworzenia obiektu typu Osoba:

Nazwa pliku: Osoba.java
public class Osoba {
  private String imie;
  private String nazwisko;
  private int wiek;

  public Osoba(String imie, String nazwisko, int wiek) {
    this.imie = imie;
    this.nazwisko = nazwisko;

    if (wiek <= 0) {
      throw new IllegalArgumentException("Wiek nie moze byc ujemny."); // 1
    }

    this.wiek = wiek;
  }

  public static void main(String[] args) {
    try {
      Osoba o = new Osoba("Jan", "Nowak", -1); // 2
    } catch (IllegalArgumentException e) { // 3
      System.out.println("Wystapil blad! " + e.getMessage());
    }
  }
}

Wynik działania tego programu to:

Wystapil blad! Wiek nie moze byc ujemny.

W konstruktorze klasy Osoba sprawdzamy, czy wiek jest niepoprawny – jeżeli tak (1), to rzucamy wyjątek za pomocą składni:

throw new IllegalArgumentException("Wiek nie moze byc ujemny.");

Zauważmy, że po słowie kluczowym throw umieszczamy wyjątek, jaki ma zostać rzucony – w tym przypadku tworzymy nowy wyjątek typu IllegalArgumentException, zdefiniowany w bibliotece standardowej Java. Jako parametr konstruktora możemy podać opcjonalny komunikat błędu.

Rzucać możemy wyjątki dowolnego typu – zarówno te zdefiniowane już w bibliotece standardowej Java jak i zdefiniowane przez nas. Wyjątek IllegalArgumentException to często stosowany wyjątek mający na celu wskazanie, że pewne dane są nieprawidłowe, tak jak w powyższym przypadku. Wiek nie może być ujemny, a taką wartość przekazujemy do konstruktora klasy Osoba (2). Wyjątek łapiemy w sekcji catch (3) i obsługujemy, wypisując na ekran komunikat.

Wyjątek to nic innego jak obiekt konkretnej klasy – tej, której wyjątek chcemy zasygnalizować. Tworzymy go tak jak wszystkie obiekty do tej pory – za pomocą słowa kluczowego new:

throw new IllegalArgumentException("Dzielnik nie moze byc rowny 0.");

Równie dobrze moglibyśmy powyższy kod zapisać jako:

IllegalArgumentException exc =
    new IllegalArgumentException("Dzielnik nie moze byc rowny 0.");

throw exc;

Stosujemy jednak to pierwsze podejście, ponieważ jest krótsze.

Definiowanie własnych wyjątków

Możemy zdefiniować na własne potrzeby nowe typy wyjątków, ale najpierw wyjaśnijmy, czym w ogóle są wyjątki?

Wyjątki to pochodne klasy Throwable lub jednej z jej klas pochodnych, np. Exception lub RuntimeException. Klasy te są zdefiniowane w bibliotece standardowej Java. To właśnie jedna bądź druga z tych klas pochodnych jest używana jako klasa bazowa dla wyjątków definiowanych przez programistów. Kiedy stosować każdą z nich zobaczymy w jednym z kolejnych rozdziałów, a w tym skupimy się na dziedziczeniu po klasie Exception.

Gdy definiujemy w blokach catch wyjątki do obsłużenia, muszą być one pochodnymi klasy Exception lub RuntimeException (pośrednio bądź bezpośrednio) – inaczej kod się sie skompiluje. Spójrzmy na najprostszy przykład zdefiniowania własnego wyjątku:

Nazwa pliku: NieprawidlowyWiekException.java
class NieprawidlowyWiekException extends Exception {

}

Zdefiniowaliśmy tutaj nową klasę wyjątków – NieprawidlowyWiekException – od teraz możemy łapać wyjątki tego typu w blokach catch. Aby wskazać, że nasz wyjątek dziedziczy po (rozszerza) klasę Exception, użyliśmy słowa kluczowego extends, poznanego w poprzednim rozdziale.

Zgodnie z konwencją nazewniczą klas wyjątków, na końcu nazwy wyjątku dodaliśmy słowo Exception.

Możemy także w prosty sposób umożliwić zapisywanie w wyjątku naszego typu komunikatu błędu. Utwórzmy jeszcze jedną klasę wyjątków:

Nazwa pliku: NieprawidlowaWartoscException.java
public class NieprawidlowaWartoscException extends Exception {
  public NieprawidlowaWartoscException(String message) {
    // wywolaj konstruktor z klasy bazowej (czyli z Exception)
    super(message);
  }
}

W tej klasie zdefiniowaliśmy konstruktor, który przyjmuje jako argument komunikat błędu – przesyłamy go do konstruktora klasy bazowej za pomocą słowa kluczowego super z poprzedniego rozdziału. Konstruktor z klasy bazowej, Exception, zapisze komunikat w polu, które będzie dostępne za pomocą metody getMessage. getMessage to metoda, którą nasza klasa wyjątku dziedziczy po klasie bazowej.

Spróbujmy dodać do konstruktora klasy Osoba walidację pól imie oraz wiek – sprawdzimy, czy mają wartość null – jeżeli tak, to rzucimy wyjątek NieprawidlowaWartoscException z odpowiednim komunikatem. Użyjemy także wyjątku NieprawidlowyWiekException:

Nazwa pliku: Osoba.java
public class Osoba {
  private String imie;
  private String nazwisko;
  private int wiek;

  public Osoba(String imie, String nazwisko, int wiek) {
    if (imie == null) { // 1
      throw new NieprawidlowaWartoscException(
          "Imie nie moze byc puste."
      );
    }

    if (nazwisko == null) { // 2
      throw new NieprawidlowaWartoscException(
          "Nazwisko nie moze byc puste."
      );
    }

    if (wiek <= 0) {
      throw new NieprawidlowyWiekException();
    }

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

  public static void main(String[] args) {
    try {
      Osoba o = new Osoba("Jan", "Nowak", -1);
    } catch (IllegalArgumentException e) {
      System.out.println("Wystapil blad! " + e.getMessage());
    }
  }
}

Dodaliśmy do konstruktora sprawdzanie wartości imie (1) oraz nazwisko (2). Jednakże podczas próby kompilacji klasy kompilator zaczyna zgłaszać problemy:

Osoba.java:8: error: unreported exception NieprawidlowaWartoscException; must be caught or declared to be thrown throw new NieprawidlowaWartoscException( ^ Osoba.java:14: error: unreported exception NieprawidlowaWartoscException; must be caught or declared to be thrown throw new NieprawidlowaWartoscException( ^ Osoba.java:20: error: unreported exception NieprawidlowyWiekException; must be caught or declared to be thrown throw new NieprawidlowyWiekException(); ^ 3 errors

Co się stało? Używaliśmy już wcześniej wyjątków i nie napotkaliśmy takiego błędu. Otóż w naszym kodzie brakuje jeszcze jednego elementu.

Wyjątki dzielą się na dwa rodzaje – Checked oraz Unchecked Exceptions. O różnicy opowiemy sobie w kolejnym rozdziale. To, co musimy teraz wiedzieć, to to, że w przeciwieństwie do wyjątków Unchecked, takich jak IllegalArgumentException łapanych wcześniej w tym rozdziale, potencjał rzucenia wyjątków rodzaju Checked musi zostać zdefiniowany w sygnaturze metody, która może go rzucić. Służy do tego słowo kluczowe throws.

W kolejnym rozdziale dokładnie sobie omówimy wyjątki Checked oraz Unchecked. Dla dociekliwych – wyjątki Unchecked to wyjątki, które dziedziczą po klasie Runtime Exception. Pozostałe wyjątki to wyjątki Checked.

Spójrzmy na sygnaturę poprawionego konstruktora:

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

Po nawiasie zamykającym definicję argumentów konstruktora, a przed klamrą { otwierająca ciało metody, napisaliśmy słowo kluczowe throws, po którym wypisaliśmy, rozdzielone przecinkami, nazwy typów wyjątków, które nasza metoda może rzucić. Jest to wymagane, gdy istnieje potencjał rzucenia wyjątku rodzaju Checked – kompilator Java nie pozwoli skompilować kodu, jeżeli zabraknie klauzuli throws, gdy kompilator zauważy, że metoda może rzucić wyjątek/wyjątki.

Kompilator wie, że wyjątki mogą być rzucone, bo analizując podczas kompilacji kod konstruktora klasy Osoba widzi użycie słowa kluczowego throw do rzucenia wyjątków.
Nie pomyl słów kluczowych throw i throws – pierwsze rzuca wyjątek, a drugie służy do zdefiniowania, jakie wyjątki metoda może rzucić.

Czy teraz kod klasy Osoba się skompiluje? Jeszcze nie – zobaczymy następujący komunikat kompilatora:

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

Tym razem kompilator wiedząc, że konstruktor klasy Osoba może rzucić wyjątki, oczekuje od nas, że korzystając z tego konstruktora weźmiemy te potencjalne wyjątki pod uwagę – innymi słowy, kompilator oczekuje użycia try..catch i obsługi wyjątków NieprawidlowaWartoscException oraz NieprawidlowyWiekException (chociaż w powyższym komunikacie widnieje nazwa tylko jednego z nich, to po dodaniu jego obsługi w catch kompilator przy kolejnej próbie kompilacji wskazałby, że wyjątek NieprawidlowyWiekException także należy obsłużyć).

Ponownie widzimy tutaj różnicę między wyjątkami Checked i Unchecked. Wcześniej w rozdziale, gdy metoda podziel mogła rzucić wyjątek, nie musieliśmy tego wyjątku obsługiwać, bo IllegalArgumentException to wyjątek rodzaju Unchecked. W konstruktorze klasy Osoba korzystamy natomiast z wyjątków rodzaju Checked, które muszą zostać obsłużone – stąd powyższy błąd kompilacji.

Spójrzmy na kompletny przykład wraz z dodaną obsługą wyjątków za pomocą try..catch:

Nazwa pliku: Osoba.java
public class Osoba {
  private String imie;
  private String nazwisko;
  private int wiek;

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

    if (nazwisko == null) {
      throw new NieprawidlowaWartoscException(
          "Nazwisko nie moze byc puste."
      );
    }

    if (wiek <= 0) {
      throw new NieprawidlowyWiekException();
    }

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

  public static void main(String[] args) {
    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!");
    }

    try {
      Osoba o = new Osoba(null, "Nowak", 30);
    } catch (NieprawidlowaWartoscException e) { // 1
      System.out.println("Nieprawidlowa wartosc: " + e.getMessage());
    } catch (NieprawidlowyWiekException e) { // 2
      System.out.println("Nieprawidlowy wiek!");
    }
  }
}

Powyższy program używa dwóch nowych typów wyjątków, które rzucane są w konstruktorze klasy Osoba. Wyjątki te obsługiwane są następnie w ciele metody main (1) i (2).

W przypadku obsługi wyjątku typu NieprawidlowaWartoscException, do wypisywanego na ekran komunikatu dodajemy treść błędu, która zawarta jest w wyjątku – umieściliśmy ją tam rzucając wyjątek w konstruktorze klasy Osoba. Wiadomość ta zawiera informację, która wartość jest nieprawidłowa. Ta wiadomość zwracana jest przez metodę getMessage.

Przerywanie wykonania bloku kodu przez wyjątki

Chociaż widzieliśmy w poprzednim rozdziale, jak rzucanie wyjątku wpływa na wykonanie bloku kodu, w którym wyjątek wystąpił, to warto jeszcze raz omówić to zagadnienie.

W momencie rzucenia wyjątku przerywany jest aktualnie wykonywany blok kodu. W przypadku konstruktora klasy Osoba, gdy okaże się, że przesłane imię jest nullem, wykonanie konstruktora klasy Osoba natychmiast się kończy – kolejne pola nie będą już sprawdzane – konstruktor kończy działanie. Nasz program kontynuuje wykonanie od sekcji catch, która odpowiedzialna jest za obsłużenie tego konkretnego wyjątku.

Oznacza to, że jeżeli przekazalibyśmy do konstruktora klasy Osoba wartość null dla imienia, null dla nazwiska, oraz -1 dla wieku, to rzucony zostałby tylko jeden wyjątek – ten, który zasygnalizowałby nieprawidłowe imię, ponieważ to to pole sprawdzamy jako pierwsze:

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

  if (nazwisko == null) {
    throw new NieprawidlowaWartoscException( // 2
        "Nazwisko nie moze byc puste."
    );
  }

  if (wiek <= 0) {
    throw new NieprawidlowyWiekException(); // 3
  }

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

Poniższe wywołanie konstruktora:

Osoba o = new Osoba(null, null, -1);

Spowoduje, że wykona się tylko fragment konstruktora do linii oznaczonej jako (1) – od tej linii wykonanie dalszej części ciała konstruktora zostanie przerwane, ponieważ rzucony zostanie wyjątek.

Następujące wywołanie:

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

Spowoduje, że wykona się tylko fragment konstruktora do linii oznaczonej jako (2).

Idąc dalej tym tropem:

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

W tym przypadku, konstruktor zostanie przerwany w linii (3).

W żadnym z powyższych przypadków nigdy nie dojdzie do wykonania kodu zaczynającego się od linii (4), a co ważniejsze, obiekt typu Osoba nie zostanie utworzony.

Dopiero poniższe wywołanie konstruktora, które zawiera poprawne wartości dla imienia, nazwiska, oraz wieku, spowoduje wykonanie całego ciała konstruktora klasy Osoba oraz utworzenie i zwrócenie nowego obiektu klasy Osoba:

Osoba o = new Osoba("Jan", "Nowak", 30);

Rzucanie wyjątków i nieosiągalny kod

W związku z tym, że rzucenie wyjątku przerywa aktualnie wykonywany blok kodu, kompilator jest w stanie wykryć sytuacje, w których pewien fragment kodu nigdy nie miałby szansy się wykonać z powodu rzucania wyjątku.

Poniższy przykład w ogóle się nie kompiluje – kompilator zgłasza błąd, ponieważ instrukcja System.out.println nie ma szansy się wykonać – jest przed nią rzucany wyjątek IllegalArgumentException:

Nazwa pliku: NieosiagalnyKodRzucanyWyjatek.java
public class NieosiagalnyKodRzucanyWyjatek {
  public static void main(String[] args) {
    throw new IllegalArgumentException("Fajrant!");

    System.out.println("Witaj Swiecie?");
  }
}

Komunikat błędu:

NieosiagalnyKodRzucanyWyjatek.java:5: error: unreachable statement System.out.println("Witaj Swiecie?"); ^

Rzucanie wyjątków a wartość zwracana z metody

Gdy poznawaliśmy metody, dowiedzieliśmy się, że metoda zawsze musi zwrócić wartość jeżeli definiuje typ zwracany inny niż void. W przeciwnym razie kompilacja naszego kodu zakończy się błędem:

Nazwa pliku: Rozdzial_07__Metody.BrakReturn.java
public class BrakReturn {
  public static void main(String[] args) {
    int liczbaDoKwadratu = kwadratLiczby(5);
  }

  public static int kwadratLiczby(int x) {
    int wynik = x * x;
    // ups! zapomnielismy zwrocic wynik!
  }
}

Ten kod kończy się błędem kompilacji missing return statement – zapomnieliśmy zwrócić wartość z metody kwadratLiczby, a musimy to zrobić – sygnatura metody wskazuje, że metoda ta zwraca wartość int.

W rozdziale o metodach wspomniałem o wyjątku od tej reguły – metoda nie musi zwrócić wartości, jeżeli rzuci wyjątek. Ma to taki sens, że skoro coś w metodzie poszło nie tak, skoro wystąpił jakiś błąd, to metoda zamiast zwrócić wartość może rzucić wyjątek:

Nazwa pliku: WyjatekZamiastReturn.java
public class WyjatekZamiastReturn {
  public static void main(String[] args) {
    try {
      System.out.println(podziel(10, 0));
    } catch (IllegalArgumentException e) {
      System.out.println("Wystpil wyjatek " + e.getMessage());
    }
  }

  public static int podziel(int x, int y) {
    if (y == 0) {
      throw new IllegalArgumentException("Dzielnik nie moze byc rowny 0.");
    }

    return x / y;
  }
}

Ten kod kompiluje się bez błędów pomimo, że istnieje taka ścieżka wykonania metody podziel, w której nie zwróci ona wartości – jeżeli y będzie równe zero, to rzucony zostanie wyjątek IllegalArgumentException. Rzucenie wyjątku powoduje natychmiastowe zakończenie działania metody podziel bez zwracania jakiejkolwiek wartości z metody. Wykonanie programu powraca do metody main, gdzie działanie kontynuowane jest w sekcji catch, w której obsługujemy złapany wyjątek. Metoda podziel nie zwraca co prawda wartości, ale rzuca wyjątek, co stanowi wyjątek od reguły, że metoda zawsze musi zwracać wartość, jeżeli definiuje zwracany typ inny niż void.

Zauważ, że wcześniej w tym rozdziale mówiłem, że jeżeli metoda rzuca wyjątek, to musimy go zdefiniować dodając po nazwie i argumentach metody słowo kluczowe throws oraz nazwy wyjątków, które metoda może rzucić. W powyższym przykładzie jednak nie ma throws, a kod działa – jak już wspomniałem w tym rozdziale, istnieją dwa rodzaje wyjątków – takie, które trzeba definiować za pomocą throws i te, których nie trzeba – porozmawiamy o tym w jednym z kolejnych rozdziałów.

Rzucanie wyjątków w try, catch, i finally

Wyjątki możemy rzucać w dowolnych blokach kodu – także w sekcji try, catch, a także finally.

Dla przykładu, moglibyśmy pobrać od użytkownika wiek osoby do utworzenia. Jeżeli ten wiek już w momencie pobrania od użytkownika jest nieprawidłowy (ujemny), to możemy rzucić w sekcji try wyjątek, by przejść natychmiast do sekcji catch obsługującej taką sytuację:

Nazwa pliku PobierzWiekOdUzytkownika.java
import java.util.Scanner;

public class PobierzWiekOdUzytkownika {
  public static void main(String[] args) {
    try {
      System.out.print("Podaj wiek osoby: ");
      int wiek = getInt(); // 1

      if (wiek <= 0) {
        throw new NieprawidlowyWiekException(); // 2
      }

      Osoba osoba = new Osoba("Jan", "Nowak", wiek); // 3

      System.out.println("Obiekt utworzony!");
    } catch (NieprawidlowaWartoscException e) {
      System.out.println("Nieprawidlowa wartosc: " + e.getMessage());
    } catch (NieprawidlowyWiekException e) { // 4
      System.out.println("Nieprawidlowy wiek!");
    }
  }

  public static int getInt() {
    return new Scanner(System.in).nextInt();
  }
}

W tym przykładzie korzystamy z metody getInt, której używamy już od kilku rozdziałów. W metodzie main pobieramy od użytkownika wiek osoby do utworzenia (1).

Zanim w ogóle przystąpimy do tworzenia obiektu typu Osoba w linii (3), najpierw sprawdzamy pobraną od użytkownika liczbę. Jeżeli jest nieprawidłowym wiekiem, to w ogóle nie będziemy próbowali utworzyć obiektu typu Osoba. Zamiast tego, od razu rzucimy wyjątek NieprawidlowyWiekException (2), a wykonanie programu przejdzie do sekcji catch, która ten wyjątek obsługuje (4).

Przykład wykonania z podaniem poprawnego i niepoprawnego wieku:

Podaj wiek osoby: 35 Obiekt utworzony!
Podaj wiek osoby: -5 Nieprawidlowy wiek!

Wyjątki mogą być też rzucane w sekcji catch. Czasem chcemy wykonać pewną akcję związaną z obsługą wyjątku, a potem rzucić go ponownie, co zobaczymy w kolejnym rozdziale.

Wyjątki mogą równie dobrze być rzucone w sekcji finally – jeżeli wykonujemy w nich kod bądź wywołujemy metody, które potencjalnie mogą zakończyć się wyjątkiem, to musimy mieć to na uwadze, opakowując taki kod ponownie w try..catch lub sygnalizując metodom, które z naszego kodu korzystają, że to one powinny się tymi potencjalnymi wyjątkami zająć – o tym zagadnieniu także porozmawiamy w jednym z kolejnych rozdziałów.

Ponowne rzucanie wyjątku

Złapanie wyjątku nie musi oznaczać zakończenia obsługi sytuacji wyjątkowych – złapany wyjątek można rzucić ponownie (exception rethrow), by został obsłużony przez metodę "wyżej" (tzn. jedną z wcześniejszych metod, które doprowadziły do wykonania metody, w której wyjątek był obsługiwany).

Jaki jest sens takiego działania? Możemy chcieć obsłużyć pewien wyjątek w dany sposób, a także dać szansę na jego obsłużenie w jednej z wcześniejszych metod.

Dla przykładu – wywołując metodę, która rzuca wyjątek, łapiemy go i zapisujemy do pliku logu informację, że wystąpił błąd. Moglibyśmy też umieścić w obiekcie wyjątku dodatkowe informacje o okolicznościach błędu. Następnie rzucamy ten wyjątek "dalej", by został obsłużony ponownie, potencjalnie już bez ponownego rzucania.

Do ponownego rzucania wyjątku stosuje się po prostu słowo kluczowe throw, po którym następuje wyjątek, który chcemy "rzucić dalej".

public static void glownaMetoda() {
  try {
    pewnaMetoda();
  } catch (PewienWyjatek e) {
    // ponowna obsluga wyjatku PewienWyjatek
  }
}

public static void pewnaMetoda() throws PewienWyjatek {
  try {
    // ...
    // instrukcje ktora moga spowodowac PewienWyjatek
    // ...
  } catch (PewienWyjatek e) {
    // zapisz informacje o bledzie do pliku logu 
    log.error("Wystapil blad " + e.getMessage());

    // rzuc wyjatek dalej 
    throw e;
  }
}

W tym przykładzie glownaMetoda wywołuje metodę pewnaMetoda i spodziewa się potencjalnego wyjątku PewienWyjatek, ponieważ pewnaMetoda deklaruje za pomocą throws, że taki wyjątek może rzucić. Dlatego też glownaMetoda zawiera try..catch i obsługę PewienWyjatek.

pewnaMetoda wykonuje pewne instrukcje, które mogą skutkować rzuceniem wyjątku PewienWyjatek. Obsługujemy go w sekcji catch, po czym rzucamy go "dalej" – teraz obsłuży go metoda glownaMetoda.

Treść wyjątku, stack trace i inne pola i metody

Wszystkie klasy w tym rozdziale znajdują się w jednym pliku: ZawartoscWyjatkowPrzyklady.java.

Wyjątki to klasy jak wszystkie inne – mogą mieć własne konstruktory, metody, i pola. Ich cechą specjalną jest to, że rozszerzają klasę Exception.

Najprostszym wyjątkiem jest klasa, która nie definiuje żadnych pól ani metod (pamiętajmy, że klasa ta otrzyma automatycznie domyślny konstruktor):

class WyjatekBezTresciException extends Exception {}

Ta klasa jest już gotowa do użycia – możemy rzucać wyjątki tego typu za pomocą throw i łapać za pomocą catch. Mogłoby się wydawać, że taka klasa nie jest specjalnie przydatna, ale jest całkiem odwrotnie – dobrze nazwana klasa wyjątku bez żadnej dodatkowej treści może tak samo spełniać swoje zadanie, jak klasy wyjątków ze szczegółowymi komunikatami o błędzie. W poprzednim rozdziale widzieliśmy taki właśnie przypadek – klasa NieprawidlowyWiekException była pusta – sama jej nazwa i rzucenie takiego wyjątku daje nam wystarczającą informację, co i dlaczego się stało.

Klasy wyjątków dziedziczą po klasie Exception kilka metod. Jedną z nich jest getMessage, która zwraca treść (komunikat) wyjątku. Ta treść może być podana podczas rzucania wyjątku – musimy wtedy udostępnić w klasie naszego wyjątku konstruktor, który przyjmie tą wiadomość i przekaże ją do konstruktora klasy bazowej. Spójrzmy na kolejny przykład wyjątku, który pozwala na zapisanie w wyjątku komunikatu:

class WyjatekZKomunikatemException extends Exception {
  public WyjatekZKomunikatemException(String message) {
    // przekaz tresc wyjatku do konstruktora klasy bazowej,
    // ktory umiesci ja w polu message, ktore bedzie dostepne
    // za pomoca metody getMessage
    super(message);
  }
}

Przykładowe utworzenie wyjątku z komunikatem:

try {
  throw new WyjatekZKomunikatemException("Co tu sie wyprawia?!");
} catch (WyjatekZKomunikatemException e) {
  System.out.println("Wyjatek zawiera komunikat: " + e.getMessage());
}

Wynik:

Wyjatek zawiera komunikat: Co tu sie wyprawia?!

Jeżeli utworzymy wyjątek bez komunikatu, to getMessage zwróci null – zobaczymy to na przykładzie wyjątku WyjatekBezTresciException:

try {
  throw new WyjatekBezTresciException();
} catch (WyjatekBezTresciException e) {
  System.out.println("Wyjatek zawiera komunikat: " + e.getMessage());
}

Wynik:

Wyjatek zawiera komunikat: null

Czasem tworząc nowy typ wyjątku chcemy w nim mieć możliwość zapisać dodatkowe informacje, którą mogą pomóc podczas próby jego obsługi, lub by móc zapisać je do pliku logu w celu późniejszej analizy, co poszło nie tak. Nic nie stoi na przeszkodzie, aby klasa wyjątku definiowała nowe pola, które takie dane będą przechowywać:

class WyjatekZDodatkowymiDanymiException extends Exception {
  private int pewnaWartosc;
  private String innaWartosc;

  public WyjatekZDodatkowymiDanymiException(
      int pewnaWartosc, String innaWartosc) {
    this.pewnaWartosc = pewnaWartosc;
    this.innaWartosc = innaWartosc;
  }

  public String getMessage() {
    return "Wartosci zapisane w tym wyjatku: " +
        pewnaWartosc + " " + innaWartosc;
  }
}

Ta klasa wyjątku pozwala na skojarzenie z nim wartości int oraz String poprzez przekazanie ich do konstruktora. Te wartości możemy potem zobaczyć korzystając z przeładowanej metody getMessage (o przeładowaniu metod mówiliśmy w rozdziale o dziedziczeniu). Przykładowe użycie mogłoby wyglądać następująco:

try {
  int pewnaWartosc = 10;
  String innaWartosc = "test";

  throw new WyjatekZDodatkowymiDanymiException(pewnaWartosc, innaWartosc);
} catch (WyjatekZDodatkowymiDanymiException e) {
  System.out.println("Wyjatek zawiera komunikat: " + e.getMessage());
}

Wynik:

Wyjatek zawiera komunikat: Wartosci zapisane w tym wyjatku: 10 test

Moglibyśmy też dodać do powyższej klasy gettery, które zwracałyby wartość pola liczbowego i typu String, jeżeli potrzebowalibyśmy mieć możliwość bezpośredniego odniesienia się do nich.

Inną metodą, którą wyjątki dziedziczą z klasy bazowej, jest printStackTrace. Metoda ta wypisuje na standardowe wyjście hierarchię wywołań metod w programie do momentu wystąpienia wyjątku:

try {
  throw new WyjatekZKomunikatemException("Co tu sie wyprawia?!");
} catch (WyjatekZKomunikatemException e) {
  System.out.println("Wyjatek zawiera komunikat: " + e.getMessage());
  e.printStackTrace();
}

Wywołanie e.printStackTrace spowoduje pojawienie się na standardowym wyjściu następujących komunikatów:

WyjatekZKomunikatemException: Co tu sie wyprawia?! at ZawartoscWyjatkowPrzyklady.main(ZawartoscWyjatkowPrzyklady.java:32)

Stack trace opisywałem na początku rozdziału o wyjątkach. W pierwszej linii znajduje się nazwa klasy wyjątku, po której następuje komunikat wyjątku. Następnie podane są metody i numery linii, który były po kolei wywoływane do momentu, w którym wystąpił wyjątek (te metody posortowane są, patrząc od góry, od ostatniej wywołanej do pierwszej). Umożliwia to prześledzenia wykonania programu aż do zaistnienia błędu i ułatwia analizę okoliczności, w jakich napotkany został problem.

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.