Rozdział 11 - Wyjątki - Podsumowanie

Podstawy wyjątków

  • Wyjątki (exceptions) to sytuacje, w których coś w programie poszło nie tak.
  • Gdy wystąpi wyjątek, mówimy, że został on rzucony.
  • Wyjątki to obiekty klas. Jak każda klasa mają one swoją nazwę, konstruktory, pola i metody.
  • Dzięki wyjątkom możemy obsłużyć dowolne sytuacje, które uznamy za nieprawidłowe, bez potrzeby stosowania rozwiązań z np. zwracaniem specjalnej wartości.
  • Spotkaliśmy już się z paroma wyjątkami, np. ArrayIndexOutOfBoundsException i NullPointerException.
  • Klasy wyjątków to klasy rozszerzające klasę Throwable (lub jej pochodną np. Exception).
  • Wyjątki mogą zawierać komunikat informujący o zaistniałym błędzie, który możemy pobrać za pomocą metody getMessage.
  • Stack trace to ścieżka wykonań metod, które doprowadziły do błędu. Kolejność metod w stack trace jest odwrotna do kolejności ich wykonywania. Metoda na dole została wykonana jako pierwsza, a ta na górze jako ostatnia i w niej rzucony został wyjątek:
Exception in thread "main" java.lang.ArithmeticException: / by zero at ZwrocWynikDzielenia.podziel(ZwrocWynikDzielenia.java:8) at ZwrocWynikDzielenia.main(ZwrocWynikDzielenia.java:3)

Łapanie wyjątków

  • Wyjątki łapiemy (obsługujemy) za pomocą mechanizmu try..catch..finally:
    try {
      // instrukcje ktore moga potencjalnie zakonczyc sie wyjatkiem
    } catch (TypWyjatku dowolnaNazwa) {
      // instrukcje, gdy zajdzie wyjątek TypWyjatku
    } catch (KolejnyTypWyjatku dowolnaNazwa2) {
      // instrukcje, gdy zajdzie wyjątek KolejnyTypWyjatku
    } finally {
      // instrukcje, ktore maja byc wykonane niezaleznie od tego,
      // czy wyjatek zostal zlapany, czy nie
    }
  • Używając try..catch spodziewamy się, że w instrukcjach objętych przez try coś może pójść nie tak (ale nie musi). To, co powinno się zadziać w przypadku napotkania konkretnego problemu (i tylko wtedy), umieszczamy w sekcji catch.
  • 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ć.
  • Gdy występuje wyjątek, jego typ dopasowywany jest do listy wyjątków z obecnych sekcji catch. Jeżeli wyjątek zostanie dopasowany, wykonywany jest kod powiązany z tą sekcją catch, która ten konkretny typ wyjątku obsługuje.
  • W opcjonalnym bloku finally możemy umieścić instrukcje, które mają zawsze się wykonać, niezależnie od tego, czy wyjątek zostanie złapany, czy nie. Blok finally zazwyczaj używany jest do czyszczenia zasobów, np. zamykania otwartych plików.
  • Gdy wystąpi wyjątek, aktualnie wykonywany blok kodu zostaje przerwany. Dalsze wykonanie programu kontynuowane jest w sekcji catch, jeżeli jest obecna i dopasowany zostanie do niej typ wyjątku do obsłużenia.
  • Silent catch to łapanie wyjątków bez ich obsługi – powinniśmy wystrzegać się takich sytuacji, ponieważ mogą prowadzić do trudnych do wykrycia i analizy błędów.
  • Wyjątki do złapania definiowane w catch muszą być pochodnymi klasy Throwable, lub, jak to zazwyczaj ma miejsce, którejś z jej klas pochodnych: Exception lub RuntimeException (pośrednio bądź bezpośrednio) – inaczej kod się nie skompiluje.
  • Zmienne definiowane wewnątrz bloku try po zakończeniu tego bloku przestają istnieć. Aby zmienna była dostępna poza try, należy ją zdefiniować i zainicjalizować przed try:
    int wynik = 0;
    
    try {
      wynik = podziel(10, 2);
    } catch (ArithmeticException e) {
      System.out.println("Blad dzielenia!");
    } finally {
      System.out.println("Sekcja finally: wynik wynosi " + wynik);
    }
    
    System.out.println("Po try wynik wynosi " + wynik);
  • Zamiast łapać kilka wyjątków, które może rzucić metoda, możemy złapać wyjątek nadrzędny dla tych wyjątków (którym zawsze jest wyjątek typu Exception) i w jednym miejscu obsłużyć wszystkie błędy:
    try {
      Osoba o = new Osoba("Joanna", "Strzelczyk", -1);
    } catch (Exception e) {
      System.out.println(
          "Wystapil blad! Komunikat bledu: " + e.getMessage()
      );
    }
  • W powyższym przykładzie złapaliśmy wszystkie wyjątki korzystając z klasy bazowej wyjątków – Exception. Klasa ta jest "bardziej ogólna" niż inne klasy wyjątków, ponieważ jest ich rodzicem (dziedziczenie). 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".
  • "Wyjątkiem ogólnym" powyżej nie musi być Exception, lecz dowolny typ wyjątku, który byłby w hierarchii dziedziczenia używanych przez nas klas wyjątków, które chcemy złapać.
  • Możemy także złapać kilka wykluczających się typów wyjątków za pomocą znaku | (pionowa kreska, ang. pipe):
    try {
      Osoba o = new Osoba("Adrian", "Sochacki", 30);
    } catch (NieprawidlowaWartoscException | NieprawidlowyWiekException e) {
      System.out.println("Nieprawidlowa wartosc: " + e.getMessage());
    }
  • 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:
    try {
      Osoba o = new Osoba(null, "Nowak", 30);
    } catch (Exception e) {
      System.out.println(
          "Wystapil blad! Komunikat bledu: " + e.getMessage()
      );
    } catch (NieprawidlowyWiekException e) { // blad kompilacji
      System.out.println("Nieprawidlowy wiek!");
    }
  • Ten fragment kodu powoduje błąd kompilacji:
    Error: java: exception NieprawidlowyWiekException has already been caught
  • Sekcje catch muszą zawierać najbardziej szczegółowe (najniżej w hierarchii dziedziczenia) wyjątki na początku, a najbardziej ogólne na końcu.
  • Od wersja Java 1.7 możemy korzystać z nowego mechanizmu try-with-resources, który ułatwia pracę z zasobami poprzez ich automatyczne zamykanie po zakończeniu bloku try. Klasy, które możemy używać w try-with-resources to te klasy, które implementują interfejs AutoCloseable lub Closeable. Przykład użycia:
File f = new File("C:/programowanie/powitanie.txt");

try (FileReader fileReader = new FileReader(f)) {
  int odczytanyZnak;

  while ((odczytanyZnak = fileReader.read()) != -1) {
    System.out.print((char) odczytanyZnak);
  }
} catch (IOException e) {
  System.out.println(e.getMessage());
}

Rodzaje wyjątków

  • Istnieją dwa typy wyjątków: Checked exceptions oraz Unchecked exceptions.
  • Różnica pomiędzy tymi rodzajami wyjątków jest taka, że potencjał rzucenia przez metodę wyjątku typu Checked musi być umieszczony w klauzuli throws w sygnaturze metody. W przypadku wyjątków Unchecked nie musimy tego robić.
  • O tym, czy wyjątek to jest rodzaju Checked czy Unchecked decyduje to, czy klasa wyjątku dziedziczy po klasie RuntimeException. RuntimeException to klasa pochodna od Exception.
  • Jeżeli klasa wyjątku ma w hierarchii dziedziczenia klasę RuntimeException, to jest wyjątkiem typu Unchecked.
  • Przykłady klas wyjątków rodzaju Checked:
    class MojWyjatek extends Exception {}
    class MojKolejnyWyjatek extends MojWyjatek {}
  • Wyjątki te mają następującą hierarchię dziedziczenia (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 wyjątek MojWyjatek).
  • Przykład wyjątków Unchecked:
    class MojRuntimeWyjatek extends RuntimeException {}
    class MojKolejnyRuntimeWyjatek extends MojRuntimeWyjatek {}
  • Te klasy wyjątków mają następującą hierarchię dziedziczenia (począwszy od Exception):
    Exception
        RuntimeException
            MojRuntimeWyjatek
                MojKolejnyRuntimeWyjatek
  • Klasy te mają w swojej hierarchii klasę RuntimeException, są więc wyjątkami rodzaju Unchecked.
  • Wyjątki Checked zazwyczaj chcemy obsłużyć, a potencjał ich rzucenia przez metodę zaznaczamy w sygnaturze metody za pomocą słowa kluczowego throws. Jest to informacja dla każdego użytkownika tej 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.
  • Istnieje jeszcze trzeci rodzaj wyjątków, które są pochodnymi klasy Error. Wyjątki te to błędy krytyczne, których za bardzo nie da się obsłużyć, np. OutOfMemoryError.

Definiowanie i rzucanie wyjątków

  • Jeżeli nasza metoda może rzucić wyjątki rodzaju Checked, to musimy zaznaczyć to w sygnaturze tej metody za pomocą słowa kluczowego throws:
    public Osoba(String imie, String nazwisko, int wiek)
        throws NieprawidlowaWartoscException, NieprawidlowyWiekException {
  • Powyższy konstruktor klasy Osoba może rzucić wyjątek NieprawidlowyWiekException lub NieprawidlowaWartoscException.
  • Korzystając z metody, która sygnalizuje, że może rzucić wyjątek, musimy:
    • umieścić wykonanie takiej metody w bloku try..catch i obsłużyć rzucane wyjątki lub
    • do sygnatury metody, która korzysta z metody, która rzuca wyjątki, także dodać throws, oddelegowując w ten sposób obsługę wyjątków do kolejnej metody, która będzie z tej metody korzystać.
  • 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ć:
    throw new IllegalArgumentException("Wiek nie moze byc ujemny.");
    
  • throw i throws to dwa różne słowa kluczowe – pierwsze stosujemy do rzucania wyjątków, a drugie to sygnalizowania w sygnaturze metody, że może ona rzucić pewne wyjątki.
  • W momencie rzucenia wyjątku przerywany jest aktualnie wykonywany blok kodu.
  • Metoda nie musi zwrócić wartości za pomocą return, jeżeli rzuci wyjątek.
  • Wyjątki możemy rzucić ponownie za pomocą throw (exception rethrow):
    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;
    }
  • Wyjątki to obiekty klasy, więc mogą mieć własne konstruktory, metody, i pola.
  • Cechą specjalną wyjątków jest jest to, że rozszerzają (pośrednio bądź bezpośrednio) klasę Exception:
    class NieprawidlowyWiekException extends Exception {
    
    }
  • Zgodnie z konwencją nazewniczą klas wyjątków, na końcu nazwy takiej klasy dodajemy słowo Exception.
  • Możemy w prosty sposób umożliwić zapisywanie w wyjątku komunikatu błędu. Aby to osiągnąć, należy do klasy wyjątku dodać konstruktor, który przyjmie komunikat, a następnie przekaże go do konstruktora klasy nadrzędnej, gdzie zostanie zapamiętany w polu message. Komunikat wyjątku będzie później dostępny za pomocą metody getMessage.
public class NieprawidlowaWartoscException extends Exception {
  public NieprawidlowaWartoscException(String message) {
    // wywolaj konstruktor z klasy bazowej (czyli z Exception)
    super(message);
  }
}

Sprawdzanie rzucanych wyjątków i ich rodzaju

  • Aby sprawdzić, jakie wyjątki może rzucić metoda, należy zajrzeć do dokumentacji tej metody w Java Doc jeżeli jest to metoda należąca do Biblioteki Standardowej Java, lub do odpowiedniej dokumentacji biblioteki, z której ta metoda pochodzi.
  • W komentarzach dokumentacyjnych potencjał rzucenia przez metodę wyjątku opisywany jest za pomocą sekcji @exception.
  • 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.
  • Hierarchię dziedziczenia można sprawdzić w dokumentacji – jeżeli korzystamy z klasy ze Standardowej Biblioteki Java, to informację o klasach znajdziemy w Java Doc.
  • 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 IllegalArgumentException

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.