(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.
Throwable | getCause()
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). |
String | getMessage()
Zwraca napis, zawierający informację o wyjątku (np. błędne dane lub indeks). |
String | get Localized Message()
Zwraca zlokalizowany (dla danego języka) napis, zawierający informacje o wyjątku . |
void | printStackTrace()
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
|
String | toString()
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)
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:
-
sygnalizują one błędy fazy wykonania (mniej poważne i poważne),
-
mogą wystąpić w dowolnym miejscu kodu.
Pozostałe wyjątki są kontrolowane, co oznacza, że:
-
metody zgłaszające te wyjątki wymieniają je jawnie w swojej deklaracji
w klauzuli throws,
-
metody te mogą zgłaszać tylko wymienione w klauzuli throws
wyjątki lub wyjątki ich podklas,
-
odwołania do tych metod wymagają jawnej obsługi ew. zgłaszanych wyjątków:
-
poprzez konstrukcje try - catch,
-
poprzez wymienienie wyjątku w klauzuli throws naszej metody (tej która
odwołuje się do metody, która może zgłosić wyjątek) i "przesunięcie" obsługę
wyjątku do miesca wywołania naszej metody.
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
-
Wykonywane są kolejne instrukcje bloku try.
-
Jeśli w którejś instrukcji wystąpi błąd (na skutek czego powstanie wyjątek), wykonanie bloku try jest przerywane w miejscu wystąpienia błędu.
-
Sterowanie przekazywane jest do pierwszej w kolejności klauzuli
catch, w której podana w nawiasach okrągłych po słowie catch klasa wyjątku pasuje
do typu powstalego wyjątku. Słowko "pasuje" oznacza tu, że podana w klauzuli
klasa wyjatku jest taka sama jak klasa powstałego wyjątku lub jest jej dowolną
nadklasą.
-
Stąd ważny wniosek: najpierw podawać BARDZIEJ SZCZEGÓŁOWE TYPY WYJĄTKÓW np. najpierw FileNotFoundException, a później IOException, bo klasa FileNotFoundException jest pochodna od IOException
-
Inne klauzule catch nie są wykonywane.
-
Obsługująca wyjątek klauzula catch może zrobić wiele rzeczy: m.in. zmienić
sekwencję sterowania (np. poprzez return lub zgłoszenie nowego wyjątku
za pomocą instrukcji throw). Jeśli nie zmienia sekwencji sterowania
to wykonanie programu jest kontynuowane od następnej instrukcji po bloku try.
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ątku | Znaczenie
|
---|
IllegalArgumentException | Przekazany metodzie lub konstruktorowi argument jest niepoprawny,
|
IllegalStateException | Stan obiektu jest wadliwy w kontekście wywołania danej metody |
NullPointerException | Referencja ma wartość null w kontekście, który tego zabrania.
|
IndexOutOfBoundsException | Indeks wykracza poza dopuszczalne zakresy |
ConcurrentModificationException | Modyfikacja obiektu jest zabroniona |
UnsupportedOperationException | Operacja (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:
- dostarczenie dodatkowych informacji o wyjątku,
- zgłoszenie właściwego dla logicznego poziomu naszej aplikacji typu
wyjątku, który może powstać na skutek róznych błedów (różnych "niskopoziomowych"
wyjątków); np. wadliwym argumentem może być null lub zly zakres indeksu lub
wadliwy format napisowej reprezentacji liczby. To będą różne wyjątki - możemy
chcieć je uogólnić i powiedzieć, że wszystkie związane są z wadliwym argumentem
wywołania metody,
Zobaczmy to na przykładzie metody set z klasy Words, która ustala i-te słowo na podany napis. Za wadliwe argumenty uznajemy:
- podany napis null lub pusty,
- błędny - wykraczający poza dopuszaczalny zakres - indeks zmienianego słowa.
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:
- gdy jakaś nasza metoda może zgłosić wyjątek klasy NaszWyj -- musi podać
w deklaracji, w klauzuli throws klasę zglaszanego wyjątku lub dowolną jej nadklasę
- void naszaMetoda() throws NaszWyj
-
nasza metoda sprawdza warunki powstania błędu
-
jeśli jest błąd - tworzy wyjątek (new NaszWyj(...)) i sygnalizuje go za
pomocą instrukcji throw :
- throw new NaszWyj(ew_param_konstruktora_z_info_o_błędzie)
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ć:
- zapakowany do wyjątku wyższego poziomu za pomocą konstruktora z jednym
z argumentów typu Throwable albo za pomocą metody initCause(Throwable) wywolanej
na rzecz stworzonego wyjątku wyższego poziomu.
- uzyskany przy obsłudze wyjątku wyższego poziomu za pomoca metody Throwable getCause().
Ł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