Wyjątki w Javie =============== Wyjątki to specjalny mechanizm języka programowania, który służy do sygnalizowania w kodzie sytuacji nieoczekiwanych, błędnych, z którymi bieżący kod nie umie sobie poradzić. Po angielsku wyjątek to exception, skrót od exceptional event, tj. zdarzenie wyjątkowe. Przykłady sytuacji wyjątkowych w kodzie: - plik, do którego chcemy pisać nie istnieje, - nie mamy praw do pisania do pliku, - plik, z którego chcemy czytać jest w niewłaściwym formacie, - połączenie z bazą danych zostało zerwane, - próba zdjęcia elementu z pustego stosu. Podnoszenie wyjątków ==================== Wyjątek można podnieść (rzucić). Po angielsku mówimy "raise an exception" albo "throw an exception". Do podnoszenia wyjątków w Javie służy słowo kluczowe throw: ... throw wyjątek; ... Na przykład w konstruktorze klasy Ułamek możemy podnieść wyjątek, gdy mianownik jest zerem: ... if (mianownik == 0) { throw new DivisionByZeroException(); } this.mianownik = mianownik; this.licznik = licznik. ... Rzucenie wyjątków przerywa normalne wykonanie metody. Kolejne instrukcje nie są wykonywane. Metoda kończy się bez zwrócenia wyniku. W naszym przykładzie z Ułamkiem: jeśli mianownik jest zerem, to po rzuceniu wyjątku przypisanie na pola licznik i mianownik nie zostanie wykonane. Deklarowanie wyjątków w metodzie ================================ Jeśli metoda może rzucić wyjątek, trzeba to zadeklarować w nagłówku metody. Są od tej zasady wyjątki, o których będzie później. Wyjątek deklarujemy używając słowa kluczowego throws w nagłówku metody. W naszym przykładzie z Ułamkiem będzie to wyglądać tak: public Ulamek(int licznik, int mianownik) throws DivisionByZeroException { ... } Możemy też zadeklarować wyjątek w zwykłej metodzie: public Ułamek podziel(Ułamek drugi) throws DivisionByZeroException { ... } Obsługa wyjątków ================ Rzucony wyjątek przerywa wykonanie metody, w której został on rzucony. Sterowanie wraca do metody wywołującej, która również zostaje przerwana. Wyjątek przerywa po kolei wykonanie metod, które znajdują się na stosie wywołań, aż stos się skończy (zapewne w metodzie main) lub aż natrafi na obsługujący go blok try. Najprostsza składnia bloku try w Javie: try { ... // kod, w którym potencjalnie może zostać rzucony wyjątek } catch (TypWyjątku e) { // kod obsługujący wyjątek } Przykład: try { wczytajDanezPliku(nazwaPliku); wykonajDziałaniaNaDanych(); jeszczeInneDziałaniaNaDanych(); ... } catch(FileNotFoundException e) { System.err.println("Nie można otworzyć pliku " + nazwaPliku + ". Proszę podać inną nazwę pliku."); } Ten kod wykonuje jakieś operacje na pliku. Jeśli pliku nie znaleziono wypisuje odpowiedni komunikat na standardowe wejście. W bloku try-catch może być kilka bloków catch dla różnych typów wyjątków, z różną kodem obsługującym: try { ... // kod, w którym potencjalnie może zostać rzucony wyjątek } catch (Wyjątek1 e) { // kod obsługujący wyjątek 1 } catch (Wyjątek2 e) { // kod obsługujący wyjątek 2 } Jeżeli dwa typy wyjątku mają ten sam kod obsługujący, można umieścić je w tym samy bloku catch, rozdzielając typy wyjątku znakiem | try { ... // kod, w którym potencjalnie może zostać rzucony wyjątek } catch (Wyjątek1 | Wyjątek2 e) { // kod obsługujący wyjątek 1 i 2 } Bloki catch obsługiwane są w takiej kolejności, w jakiej zostały zadeklarowane. Jeśli wyjątek pasuje do kilku bloków catch, zostanie wykonany ten, który zadeklarowano najwcześniej. Blok finally ============ W bloku try-catch może także wystąpić blok finally: try { ... // kod, w którym potencjalnie może zostać rzucony wyjątek } catch (Wyjątek1 e) { // kod obsługujący wyjątek } finally { ... } Blok finally jest wykonywane zawsze, niezależnie od tego, czy wyjątek został rzucony czy nie. W bloku finally umieszczamy np. kod, który zwalnia przydzielone zasoby. try-with-resources ================== W Javie od wersji 7 istnieje nowa wersja bloku try, tzw. try-with-resources. Try with resources automatycznie zamyka zadeklarowane zasoby, bez użycia klauzuli finally: try (Zasób1 z1 = new Zasób1(); Zasób2 z2 = new Zasób2(0); Zasób3 z3 = new Zasób3()) { //tu jest niebezpieczny kod korzystający z z1, z2 i z3 //... } catch (IOException e) { //obsługa wyjątku IOException } catch (InnyMożliwyWyjątek w) { //obsługa wyjątku InnyMożliwyWyjątek } Zasoby tworzone w try-with-resources muszą implementować interfejs AutoCloseable. Tworzenie własnych wyjątków =========================== Wyjątki w Javie są zwykłymi obiektami, które mają swoją hierarchię klas. Wierzchołkiem hierarchii wyjątków w Javie jest klasa Throwable. Wszystkie obiekty klasy Throwable można rzucać (podnosić) i łapać (ale nie wszystkie należy!). Po Throwable dziedziczą klasy Error i Exception. Klasa Error służy do sygnalizowania poważnych awarii maszyny wirtualnej, np. brak pamięci to OutOfMemoryError. Błędów typu Error nie należy łapać, bo są to tak poważne awarie, że nie jesteśmy w stanie nic na nie poradzić. Klasa Exception to miejsce na zwykłe wyjątki definiowane przez programistę. Zwykłe wyjątki dziedziczą po klasie Exception. Jeśli chcemy zdefiniować wyjątek DivisionByZeroException, możemy to zrobić tak: public class DivisionByZeroException extends Exception { ... } Obiekty klasy DivisionByZeroException będą wyjątkami. Możemy je rzucać i łapać do woli. Własne wyjątki w Javie z reguły mają nazwy kończące się na Exception. Typowe konstruktory wyjątków: Exception() - konstruktor bezargumentowy Exception(String message) - konstruktor z komunikatem dla użytkownika Exception(Throwable throwable) - konstruktor z innym wyjątkiem, będącym pierwotną przyczyną danego wyjątku Exception(String m, Throwable throwable) - konstruktor z komunikatem i przyczyną Wyjątki typu Runtime ==================== Po klasie Exception dziedziczy klasa RuntimeException. Wyjątki, które dziedziczą po RuntimeException nazywane są wyjątkami niekontrolowanymi (unchecked exceptions). Wszystkie pozostałe wyjątki to wyjątki kontrolowane (checked exceptions). Wyjątki niekontrolowane nie muszą być deklarowane w nagłówkach metod, ani obsługiwane w bloku try catch. Większość znanych nam wyjątków, np. NullPointerException, IndexOutOfBoundsException, ArithmeticException to wyjątki typu Runtime, dlatego nie musieliśmy przejmować się ich obsługą. Jest tematem gorących dyskusji, czy lepiej używać wyjątków kontrolowanych czy niekontrolowanych. Uwaga: Nie używamy wyjątków do sterowania przebiegiem programu ============================================================== Tworzenie i przetwarzanie wyjątków jest dosyć kosztowne. Nie należy używać wyjątków do sterowania przebiegiem programu, zamiast zwykłych instrukcji warunkowych. Na przykład, jeśli chcemy wczytać licznik i mianownik, to nie należy wykorzystywać wyjątków do sprawdzenia, czy mianownik jest zerem. ŹLE: try { int licznik = wczytajLiczbe(); int mianownik = wczytajLiczbe(); Ułamek u = new Ułamek(licznik, mianownik); catch (DivisionByZeroException e) { System.out.println("Mianownik nie może być zerem."); } Lepiej jest po prostu sprawdzić, czy mianownik jest zerem i wykonać odpowiednią akcję. DOBRZE: int licznik = wczytajLiczbe(); int mianownik = wczytajLiczbe(); if (mianownik == 0) { System.out.println("Mianownik nie może być zerem."); return; } Ułamek u = new Ułamek(licznik, mianownik);