Dodatkowo o wyjątkach

(Zob. "Java od podstaw do technologii", t.I,, część C rozdz. 1)

Wyjątek - to sygnał o błędzie w trakcie wykonania programu

Wyjątek powstaje na skutek jakiegoś nieoczekiwanego błędu.
Wyjątek jest zgłaszany (lub mówiąc inaczej - sygnalizowany).
Wyjątek jest (może lub musi być) obsługiwany.

Klasy wyjątków


 catch (NumberFormatException exc) ...

Wyjątki są obiektami klas wyjątków.

Zatem nazwy NumberFormatException, ArithmeticException itd. sa nazwami klas, a zmienna exc we wczesniejszych przykładach jest faktycznie zmienną - zawiera referencję do obiektu odpowiedniej klasy wyjątku.

Wobec takiej zmiennej możemy użyć rozlicznych metod, które dostarczą nam informacji o przyczynie powstania wyjątku. Oto niektóre z nich.
 ThrowablegetCause()
          Zwraca wyjątek niższego poziomu, który spowodował powstanie tego wyjątku  albo null jeśli takiego wyjątku niższego poziomu nie było  lub nie jest zidentyfikowany (o wyjątkach niższego poziomu zobacz dalej).
 StringgetMessage()
         Zwraca napis, zawierający informację o wyjątku (np. błędne dane lub indeks).
 StringgetLocalizedMessage()
          Zwraca zlokalizowany  (dla danego języka) napis,  zawierający informacje o wyjątku .
 voidprintStackTrace()
          Wypisuje na konsoli informacje o wyjątku oraz  sekwencje wywołań metod, która doprowadziła do powstania wyjątku (stos wywołań). Wersje tej metody pozwalają te informacje zapisywac do plików (logów)
 StackTraceElement[] getStackTrace()
          Zwraca tablicę, której elementy stanowią opis kolejności wywołań metod, które spowodowały wyjątek
 StringtoString()
          Zwraca  informację o wyjątku (zazwyczaj nazwę klasy wyjątku oraz dodatkową informację uzyskiwaną przez getMessage())

Zobaczmy na przykładzie jakie informacje możemy uzyskać o wyjątku:
import java.util.*;

class ReportExc {

  public ReportExc() {
    wykonaj();
  }

  public void wykonaj() {
    try {
      int num = Integer.parseInt("1aaa");
    } catch (NumberFormatException exc) {
        System.out.println("Co podaje getMessage()");
        System.out.println( exc.getMessage());
        System.out.println("Co podaje toString()");
        System.out.println(exc);
        System.out.println("Wydruk śladu stosu (kolejność wywołań metod)");
        exc.printStackTrace();
        System.exit(1);
    }
  }

  public static void main(String[] args) {
    new ReportExc();
  }

}
Program ten wyprowadzi:
Co podaje getMessage()
1aaa
Co podaje toString()
java.lang.NumberFormatException: 1aaa
Wydruk śladu stosu (kolejność wywołań metod)
java.lang.NumberFormatException: 1aaa
        at java.lang.Integer.parseInt(Integer.java:435)
        at java.lang.Integer.parseInt(Integer.java:476)
        at ReportExc.wykonaj(Report.java:11)
        at ReportExc.<init>(Report.java:6)
        at ReportExc.main(Report.java:24)



r



Wszystkie wyjątki pochodzą od klasy Throwable. Mamy następnie dwie wyróżnione klasy Error i Exception. Od klasy Exception pochodzi klasa RunTimeException oraz wiele innych. Wyróżnione klasy spełniają ważne, niejako wyspecjalizowane role.

Wyjątki kontrolowane i niekontrolowane

Istnieją dwa rodzaje wyjątków: kontrolowane i niekontrolowane

Wyjątki pochodne od klas RuntimeException i Error są niekontrolowane, co oznacza, że:
Pozostałe wyjątki są kontrolowane, co oznacza, że:
Wiele razy natkniemy się na sytuację, w której musimy obslugiwać wyjątki, które mogą powstać przy wywołaniu jakichś metod ze standardowych klas Javy. Jeśli tego nie zrobimy, kompilator wykaże błąd w programie. Sytuacja taka dotyczy, na przykład, metod ze standardowego pakietu  java.io, zawierającego klasy do operowania na strumieniach danych (m.in. plikach).

Sekwencja działania try-catch i klauzula finally

Szczególowy mechanizm działania bloku try-catch można opisac w następujący sposób

Klauzula finally służy do wykonania kodu niezależnie od tego czy wystąpił wyjątek czy nie.

boolean metoda(...) {
try {
       // instrukcje, które mogą spowodować wyjątek
}
catch(Exception e) {  return false; }
finally {
       // uporządkowanie, np. zamknięcie pliku
       }
return true;
}

Jeśli powstał wyjątek - wykonywana jest klauzula catch.
Mimo, iż zmienia ona sekwencję sterowania (zwraca false na znak, iż nastąpiło niepowodzenie), sterowanie przekazywane jest do klauzuli finally. I dopiero potem zwracany jest wynik - false.
Jeśli nie było wyjątku, po zakończeniu instrukcji w bloku try sterowanie od razu wchodzi do klauzuli finally, a po jej zakończeniu zwracany jest wynik true (wykonywana jest ostatnia instrukcja metody).
 

Zgłaszanie wyjątków

Do zgłaszania wyjątków służy instrukcja sterująca throw.


Instrukcja sterująca throw ma postać:

        throw excref;

gdzie:
        excref - referencja do obiektu klasy wyjątku.
Np. 
        throw new NumberFormatException("Wadliwy format liczby: " + liczba);


W istocie, instrukcja throw jest niczym innym jak sposobem specyficznego przekazywania sterowania do jakichś punktów programu (do miejsc obsługi wyjątku). Należy jednak korzystać z niej wyłącznie w celu sygnalizowania błędów.

Zwykle w naszym kodzie będziemy sprawdzać warunki powstania błędu i jeśli sa spełnione (wystąpił błąd) - zgłaszać wyjątek.
Jak zobaczymy w następnym punkcie możemy tworzyć własne klasy wyjątków i zgłaszać własne wyjatki. Nie należy jednak tego naduzywac, w istocie w Javie dostępna jest duża liczba gotowych, standardowo nazwanych, klas wyjątków i warto z nich własnie korzystać.

Typowe, gotowe do wykorzystania, klasy wyjątków opisujących częste rodzaje błędów fazy wykonania programu pokazuje tabela.

Klasa wyjątkuZnaczenie
IllegalArgumentExceptionPrzekazany metodzie lub konstruktorowi argument jest niepoprawny,
IllegalStateExceptionStan obiektu jest wadliwy w kontekście wywołania danej metody
NullPointerExceptionReferencja ma wartość null w kontekście, który tego zabrania.
IndexOutOfBoundsExceptionIndeks wykracza poza dopuszczalne zakresy
ConcurrentModificationExceptionModyfikacja obiektu jest zabroniona
UnsupportedOperationExceptionOperacja (na obiekcie) jest niedopuszczalna (obiekt nie udostępnia tej operacji).

Możemy także wykorzystywać inne klasy, takie jak NumberFormatException (błąd formatu liczby) czy NoSuchElementException (wyjątek sygnalizowany, gdy w kolekcjach danych staramy się sięgnąc do nieistniejącego elementu).

Zwrócmy uwagę, że wszystkie wymienione wyżej wyjątki są niekontrolowane, bowiem pochodzą od klasy RuntimeException, co ułatwia ich wykorzystanie, bowiem nie zmusza programisty do ich obsługi. Nie musimy też takich wyjątków podawać w klauzuli throws w deklaracji metody, które je zgłasza (ale możemy, co sprzyja lepszej dokumentacji kodu).

Ważne jest, by tworząc i zgłaszając wyjątek jakiejś standardowej klasy podać w konstruktorze informację o przyczynie wyjątku.

Dla przykładu, jeśli mamy klasę Words, której obiekty sa tablicami słów, to w konstruktorze i np. w metodzie find(...) wyszukującej słowo w tablicy możemy zabezpieczyć się przed wadliwym argumentem poprzez zgłoszenie wyjątku IllegalArgumentException:

class Words {

  private String[] words;

  // Tworzy obiekt- tablicę słów z podanego napisu
  public Words(String s) {
    if (s == null) throw new IllegalArgumentException(
                             "Wadliwy argument konstruktora klasy Words - null.");
    // ...
  }

  // ...
  // Zwraca indeks słowa, które jest takie samo jak podane
  // lub -1 jesli takiego słowa nie ma
  public int find(String word) {
    int foundIndex = -1;
    if (word == null || word.equals(""))
      throw new IllegalArgumentException("Wadliwy argument metody find");
    // ..
    return foundIndex;
  }
  // ...

}

Sposoby oprogramowania sygnalizacji wyjątków mogą być bardzo różne. Często wykorzystuje się tu mechanizm tzw. ponownego zgłaszania wyjątku (rethrowing ), polegający na tym, że po przechwyceniu wyjątku zgłaszamy go ponownie (albo wyjątek innej klasy). Jest to mechanizm, który pozwala na:
Zobaczmy to na przykładzie metody set z klasy Words, która ustala i-te słowo na podany napis. Za wadliwe argumenty uznajemy:
Moglibyśmy więc napisać tak:
// Ustala wartość i-go słowa na w
  public void set(int i, String w) {
    String errMsg = "Metoda Words.set(int, String). Wadliwy argument.";
    if (w == null)
      throw new IllegalArgumentException(errMsg + "\nString == null");
    if (i < 0 || i >= words.length)
       throw new IllegalArgumentException(errMsg + "\nIndeks: " + i);
    words[i] = w;;
  }
Ale jest też prostszy i może bardziej elegancki sposób, polegający właśnie na ponownym zgłoszeniu wyjątku:
  public void set(int i, String w) {
    try {
      if (w.equals("")) throw new IllegalArgumentException("Pusty String");
      words[i] = w;
    } catch (Exception exc) {
         throw new IllegalArgumentException(
            "Metoda Words.set(int, String). Wadliwy argument:\n" + exc);
    }
  }
Tutaj pozostawiamy sprawdzenie zakresu indeksów oraz niedopuszczalnej wartości null samej Javie. W przypadku błędnych argumentów powstaną wyjątki NullPointerException lub ArrayIndexOutOfBoundsException. Ze swojeje strony sprawdzamy tylko, czy napis nie jest przypadkiem pusty (jesli tak - zgłosimy wyjątek). Wszystkie te wyjątki będziemy obsługiwac w bloku try-catch, a obsługa polegać będzie na tym, że zgłosimy nowy wyjątek IllegalArgumentException, który będzie uogólniał wszystkie specyficzne błędy (jako błąd argumentu) i podawał także informację o konkretnej przyczynie błędu. Np.

java.lang.IllegalArgumentException: Metoda Words.set(int, String). Wadliwy argument:
java.lang.ArrayIndexOutOfBoundsException: -1
    at Words.set(Throwing.java:41)
    at Throwing.main(Throwing.java:67)


Należy podkreślić, że często "przetłumaczenie" powstającego wyjątku na wyższy poziom abstrakcji jest istotne. Oto np. metody next() i previous() zwykle zwracają następny i poprzedni element jakiejś struktury danych. W przypadku naszej tablicy słów w klasie Words będzie to następne lub poprzednie słowo.
Całkiem szybko moglibyśmy napisac tak:

class Words {

  private String[] words;
  private int currIndex = 0;

  // ...

  public String next() {
    return words[currIndex++];
  }

  public String previous() {
    return words[--currIndex];
  }
Wtedy w metodach next() i previus() mogą wystąpić wyjątki ArrayIndexOutOfBoundsException. Ale to, że słowa przechowujemy w  tablicy (a nie w jakiejś onnej strukturze danych) jest tylko właściwością tej konkretnej implementacji. Ogólniejszy kontrakt dla metod next lub previous powinien od implementacji abstrahować - niemożność uzyskania następnego lub poprzedniego elementu-słowa powinna raczej być zgłaszana jako wyjątek typu NoSuchElementException.
Zatem powinniśmy napisać raczej tak:

  public String next() {
    String word = null;
    try {
     word = words[currIndex++];
    } catch (ArrayIndexOutOfBoundsException exc) {
      throw new NoSuchElementException(
        "Brak elementu na pozycji " + exc.getMessage()
        );
    }
    return word;
  }
I podobnie zmodyfikować metodę previous().

Bardzo ważne jest też, by przy zgłaszaniu wyjątku zachować dopuszczalny stan obiektu, tak by ewentualna obsługa wyjątku mogła naprawić błąd.
W pokazanych wyżej kodach metod next i previous warunek ten nie jest spełniony.
Np. po wprowadzeniu dwóch słów "a" i "b" poniższy fragment:
     Words w = new Words("a b");

     try {
       System.out.println(w.next());
       System.out.println(w.next());
       System.out.println(w.next());
     } catch (Exception exc) {
      System.out.println(exc);
      System.out.println("Do tyłu jeden krok:");
      System.out.println("Previous daje: " + w.previous());
      System.out.println("I teraz next(): " + w.next());
     }
nie da spodziewanych wyników, ponieważ po powstaniu błędu w metodzie next stan obiektu (currIndex) jest wadliwy. Wynik będzie następujący:
a
b
java.util.NoSuchElementException: Brak elementu na pozycji 2
Do tyłu jeden krok:
java.util.NoSuchElementException: Brak elementu na pozycji 2


Przyczyna błędu leży w niedopuszczalnych zmianach bieżącego indeksu. Powinniśmy raczej napisac tak (zmieniając również kod metody previous()):

  public String next() {
    String word = null;
    try {
     word = words[currIndex];
     currIndex++;
    } catch (ArrayIndexOutOfBoundsException exc) {
      throw new NoSuchElementException(
        "Brak elementu na pozycji " + exc.getMessage()
        );
    }
    return word;
  }

  public String previous() {
    String word = null;
    try {
     word = words[--currIndex];
    } catch (ArrayIndexOutOfBoundsException exc) {
      currIndex++;
      throw new NoSuchElementException(
        "Brak elementu na pozycji " + exc.getMessage()
        );
    }
    return word;
  }
a wtedy taki fragment programu:
     Words w = new Words("a b");
     try {
       System.out.println(w.next());
       System.out.println(w.next());
       System.out.println(w.next());
     } catch (Exception exc) {
      System.out.println(exc);
      System.out.println("Do tyłu jeden krok:");
      System.out.println("Previous daje: " + w.previous());
      System.out.println("I teraz next(): " + w.next());
     }
     System.out.println("Odwrotnie");
     try {
       System.out.println(w.previous());
       System.out.println(w.previous());
       System.out.println(w.previous());
     } catch (Exception exc) {
      System.out.println(exc);
      System.out.println("Do przodu jeden krok:");
      System.out.println("Next daje: " + w.next());
      System.out.println("I teraz Previous(): " + w.previous());
     }

wyprowadziw właściwie wyniki:

a
b
java.util.NoSuchElementException: Brak elementu na pozycji 2
Do tyłu jeden krok:
Previous daje: b
I teraz next(): b
Odwrotnie
b
a
java.util.NoSuchElementException: Brak elementu na pozycji -1
Do przodu jeden krok:
Next daje: a
I teraz Previous(): a


Własne  wyjątki

Wyjątki są obiektami klas pochodnych od Throwable.
Nic nie stoi na przeszkodzie, by definiować własne klasy wyjątków i posługiwac się nimi w  naszych programach.

Żeby stworzyć własny wyjątek należy zdefiniować odpowiednią klasę.

Zgodnie z konwencją dziedziczymy podklasę Throwable - klasę Exception.

class NaszWyj extends Exception {
...
}

Zwykle w naszej klasie wystarczy umieścić dwa konstruktory: bezparametrowy oraz z jednym argumentem typu String (komunikat o przyczynie powstania wyjątku). W konstruktorach tych nalezy wywołać konstruktor nadklasy (za pomocą odwołania super(...), w drugim przypadku z argumentem String).

Obowiązują następujace zasady zgłaszania wyjątków: Poniższy przykład ilustruje wyżej powiedziane.
W klasie ZipAsk zdefiniowano metodę wprowadzania kodu pocztowego (getZip). W metodzie tej sprawdzana jest poprawność struktury wprowadzonego kodu (nn-nnn, gdzie n - cyfry). Jeżeli kod nie jest poprawny zgłaszany jest wyjątek własnej klasy NotValidZipException. Metoda main klasy ZipAskTest służy do przetestowania działania: obsługujemy w niej wyjątek NotValidZipException, zmuszając użytkownika programu do wprowadzenia trzech poprawnych kodów (lub ew. rezygnacji z działania poprzez wybór Cancel w dialogu).

 import javax.swing.*;

class NotValidZipException extends Exception {   // Klasa wyjątku

    NotValidZipException() {
      super();
    }

    NotValidZipException(String s)  {
      super(s+ "\nPoprawny kod ma postać: nn-nnn");
    }
}


public class ZipAsk {

 public ZipAsk() { }

 public String getZip() throws NotValidZipException {

    final int N = 6,        // długość kodu
              P = 2;        // pozycja na której występuje kreska

    String zip = JOptionPane.showInputDialog("Podaj kod pocztowy:");
    if (zip == null) return zip;

    boolean valid = true;   // czy kod poprawny?

    char[] c = zip.toCharArray(); // tablica znaków w podanym kodzie

    // jeżeli struktura wadliwa: nie ta długość, brak kreski
    if (c.length != N || c[P] != '-') valid = false;

    // czy w kodzie występują tylko cyfry?
    for (int i = 0; i<N && valid; i++) {
         if (i==P) continue;
         if (!Character.isDigit(c[i])) valid = false;
         }
    // w tej chwili wiemy już, czy kod jest poprawny
    // jeśli nie:
    // - tworzymy i zgłaszamy wyjątek
    if (!valid) throw new NotValidZipException("Wadliwy kod: " + zip);

    // w przeciwnym razie zwracamy kod
    return zip;
    }
}

class ZipAskTest {

  public static void main(String[] args) {

    JOptionPane.showMessageDialog(null, "Podaj trzy prawidłowe kody pocztowe");

    ZipAsk zask = new ZipAsk();
    String zip = null;
    int n = 3;

    while (n > 0) {
      try {
        zip = zask.getZip();
        if (zip == null) break;
        n--;
      } catch (NotValidZipException exc) {
          JOptionPane.showMessageDialog(null, exc.getMessage());
          continue;
      }
      System.out.println("Kod " + (3-n) + " : " + zip);
    }
    System.exit(0);

  }

} 

Oczywiście, nie musimy tworzyć wyłącznie kontrolowanych własnych wyjątków.
Możemy pzrecież dziedziczyć klasę RuntimeException lub nawet Error,
Podejmując decyzje w tym względzie warto jednak posługiwac się przyjętymi konwncjąmi: te wyjątki, które mogą powstawać w różnych miejscah kodu i wymuszanie obowiązkowej obsługi których byłoby dla użytkowników naszych klas bardzo uciążliwe - uczyńmy niekontrolowanymi, inne, szczególnie takie, z których aplikacja powinna prawie zawsze "się podnosić" (jak np. koniec pliku) - programujmy jako kontrolowane.

Niskopoziomowe przyczyny i łańcuchowanie wyjątków

W Javie w wersji 1.4 dodano nowy mechanizm łańcuchowania wyjątków.
Każdy wyjątek może zawierać w sobie odniesienie do obiektu klasy innego wyjątku (wyjątku niższego poziomu). Jest to zazwyczaj wyjątek, który spowodował błąd, ale został  przechwycony w  metodzie przy zgłaszaniu wyjątku logicznie wyższego poziomu. Ten "niskopoziomowy" wyjątek nazywa się przyczyną (cause) wyjątku wyższego poziomu  i może być:
Łańcuchowanie wyjątków ilustruje nowa wersja metody set z omawianej klasy Words oraz jej wykorzystanie:
class Words {
  // ...
  public void set(int i, String w) {
    try {
      if (w.equals("")) throw new IllegalArgumentException("Pusty String");
      words[i] = w;
    } catch (Throwable lowLevelException) {
        IllegalArgumentException highLevelExc = new IllegalArgumentException(
              "Metoda Words.set(int, String).Wadliwy argument"
            );
        highLevelExc.initCause(lowLevelException);
        throw highLevelExc;
    }
  }
  // ...
}

// gdzie indziej:

     Words w = new Words(data);
     try {
       w.set(10, "ala");
     } catch (IllegalArgumentException exc) {
         System.out.println(exc);
         System.out.println("Przyczyna - " + exc.getCause());
     }

Ten fragment programu może wyprowadzić następujące informacje:

java.lang.IllegalArgumentException: Metoda Words.set(int, String).Wadliwy argument
Przyczyna - java.lang.ArrayIndexOutOfBoundsException: 10