Część 3.5. Obsługa wyjątków
Wyjątek w programie Javy to sytuacja, gdy wykonywany program powoduje powstanie błędu, np: dzielenie przez
zero, próba otwarcia nieistniejącego pliku, itp. Obsługa wyjątków pozwala programiście zachować kontrolę
nad poprawnościś wykonania metody czy pojedynczej instrukcji.
W Javie istnieje bardzo rozbudowana hierarchia predefiniowanych klas wyjątków, których klasą bazową jest klasa Throwable, a główne podklasy to: Error i Exception. Niektóre wyjątki należą do grupy tzw. wyjątków weryfikowalnych: kompilator sprawdza, czy program zawiera procedury obsługi dla każdego wyjątku z tej grupy, a brak takiej obsługi sygnalizowany jest komunikatem błędu podczas kompilacji. Wyjątki klasy Error i jej klas pochodnych są nieweryfikowalne, ponieważ mogą one wystąpić w wielu punktach programu i powrót z nich jest bardzo skomplikowany lub niemożliwy. Do wyjątków nieweryfikowalnych należą także wyjątki klasy RuntimeException (podklasa Exception), gdyż zadeklarowanie takich wyjątków nie może być kontrolowane przez kompilator: powstają podczas pracy programu ze względu na nieprzewidziany układ danych. Do obsługi wyjątków weryfikowalnych służą słowa kluczowe: throw, throws, try, catch i finally. Do obsługi wyjątków najczęściej korzystamy z instrukcji try-catch-finally. Blok try zawiera fragment kodu (całą metodę, jej fragment lub pojedynczą instrukcję), w którym może pojawić się błąd. Definiując ten blok nie precyzujemy o obsługę jakich błędów chodzi. Określamy nim jedynie obszar, gdzie może wystąpić błąd, które chcemy obsłużyć. Jeżeli w trakcie działania programu wystąpi błąd, działanie procedur bloku try zostanie zakończone, a uruchomione zostaną procedury w odpowiadającym mu bloku catch.Właściwa obsługa wyjątku (błędu) wykonywana jest więc przez odpowiednie instrukcje w występujących dalej blokach catch. Każdy z nich odpowiedzialny jest za obsługę dokładnie jednego rodzaju wyjątków. Blok finally wykonywany jest zawsze: po obsłużeniu któregokolwiek z powstałych wyjątków, a także wtedy, gdy żaden wyjątek nie wystąpił. Jest on wykonywany także wtedy, gdy blok catch będzie przerywał dalsze wykonywanie bieżącej metody. Pełna struktura instrukcji try ... catch wygląda następująco: void Przyklad { try { // blok instrukcji, które mogą spowodować wystąpienie wyjątku } catch (ObiektImplementujacyInterfejsThrowable zmienna) { // blok obsługujący wystąpienie wyjątku; // wykonywany tylko, gdy wystąpi wyjątek typu takiego jak // typ parametrem bloku catch } catch (ObiektImplementujacyInterfejsThrowable zmienna) { // ..... } // ..... finally // opcjonalnie { // zakończenie działania metody; // instrukcje tu zapisane będą wykonane zawsze: // nawet po instrukcji return lub wystąpieniu wyjątku } } Żeby lepiej zrozumieć obsługę wyjątków w Javie napiszemy program, który będzie generował sytuacje błędne. Program będzie tylko wyświetlał komunikaty o wystąpieniu lub nie wystąpieniu błędu. W przypadku wystąpienia błędu będziemy wyświetlać dodatkowo komunikat o typie błędu i dodatkowe informacje generowane przez maszynę wirtualną Javy. public class Przyklad { public static void main (String[] args) // pięciokrotne wykonanie pętli { for (int i = 0; i <= 4; i++) { // tutaj zaczyna się blok try, który może wygenerować błąd try { wykonaj(i); // następna instrukcja wykona się tylko, jeżeli błąd nie wystąpi System.out.println("Wyjatek nr " + i + " nie zostal wygenerowany"); } // koniec bloku try catch (Exception e) // blok wykonywany, gdy wystąpi błąd { System.out.println("Operacja nr " + i + ": wyjatek: " + e.getClass() + "\n z informacja: " + e.getMessage()); } } } static int wykonaj(int i) { // tutaj zaczyna się blok try, który może wygenerować błąd try { if (i == 1) { int x = 0; return x / x; // dzielenie przez 0 } if (i == 2) { String s = null; return s.length(); // długość pustej zmiennej } if (i ==3 ) { int t[] = new int[10]; return t[10]; // modyfikator poza tablicą } return 0; } // koniec bloku try // blok finally, który będzie wykonany zawsze !!! finally { System.out.println("\n[generowanie wyjatku nr " + i +" zakonczone]"); } } } Po uruchomieniu programu powinieneś otrzymać taki efekt: Jak działa ten program? Z metody main wywoływana jest pięciokrotnie metoda wykonaj, za każdym razem z inna wartością parametru i. Zwracana z metody wykonaj wartość zależy od podanego parametru. Prześledźmy więc kolejno co będzie się działo w programie dla użytych wartości parametru i:
W poprzednim przykładzie wystąpił tylko jeden blok catch. Wykonywany był on dla każdego typu błędu. Nie zawsze taka sytuacja jest wygodna. Nieraz możemy chcieć wykonać różne działania, zależne od typu występującego wyjątku. W tym celu możemy stworzyć wiele bloków catch. Skorzystamy z tego, że po wystąpieniu wyjątku w bloku try, Java porównuje wyjątek, który wystąpił z parametrami poszczególnych bloków catchi wykonuje tylko ten blok, którego parametr jest zgodny z typem występującego wyjątku. W ten sposób możemy obsłużyć wystąpienie wyjątków różnego rodzaju. Spróbujmy skorzystać z tej właściwości, wykorzystując ją w naszym poprzednim programie. W tym celu przerobimy go, aby wyglądał tak: public class Przyklad { public static void main (String[] args) { String s = null; int t[] = new int[10]; for (int i = 0; i <= 4; i++) { // tutaj zaczyna się blok try, który może wygenerować błąd try { if (i == 1) System.out.println("0 / 0 = " + (0 / 0)); if (i == 2) System.out.println("Dlugosc napisu = " + s.length()); if (i == 3) System.out.println("10 element tablicy = " + t[10]); // następna instrukcja wykona się tylko, jeżeli błąd nie wystapi System.out.println("i == " + i + ": wyjątek nie zostal wygenerowany"); } // ten blok catch wykona się tylko dla błędów arytmetycznych catch (ArithmeticException e) { System.out.println("Wystapil wyjatek: dzielenie przez zero"); } // ten blok catch wykona się tylko dla wskaźnika null catch (NullPointerException e) { System.out.println("Wystapil wyjatek: operacja na wartosci null"); } // ten blok catch wykona się tylko dla błędów dostępu do tablicy catch (ArrayIndexOutOfBoundsException e) { System.out.println("Wystapil wyjatek: przekroczony zakrees tablicy"); } } } } Po uruchomieniu programu powinieneś otrzymać taki efekt: Analiza tego programu jest podobna do poprzedniego. a spróbuj przeprowadzić ją samodzielnie. Powróćmy do naszego pierwszego przykładu. Powstanie wyjątku w metodzie wykonaj powodowało powrót do metody main i wykonanie zapisanego tam bloku catch. Taki sam efekt możemy osiągnąć w inny jeszcze sposób: korzystając z instrukcji throws.Instrukcja throws przekazuje - po wystąpieniu wyjątku - sterowanie do skojarzonego z nim bloku catch. Jeśli blok catch nie występuje w bieżącej metodzie, to sterowanie natychmiastowo, bez zwracania wartości, przekazywane jest do metody, która wywołała bieżącą funkcję. W tej metodzie szukany jest blok catch. Jeśli blok catch nie zostanie znaleziony, to sterowanie przechodzi do metody, która wywołała tę metodę... Sterowanie przekazywane jest zatem zgodnie z łańcuchem wywołań metod, aż do momentu znalezienia bloku catch odpowiedzialnego za obsługę wyjątku. Z wykorzystaniem instrukcji throws nasz program wygląda tak: public class Przyklad { public static void main (String[] args) { for (int i = 0; i <= 4; i++) { // tutaj zaczyna się blok try, który może wygenerować błąd try { wykonaj(i); System.out.println("Wyjatek nr " + i + " nie zostal wygenerowany"); } // koniec bloku try catch (Exception e) // blok wykonywany, gdy wystąpi błąd { System.out.println("Operacja nr " + i + ": wyjatek: " + e.getClass() + "\n z informacja: " + e.getMessage()); } } } static int wykonaj(int i) throws Exception { if (i == 1) { int x = 0; return x / x; // dzielenie przez 0 } if (i == 2) { String s = null; return s.length(); // długość pustej zmiennej } if (i ==3 ) { int t[] = new int[10]; return t[10]; // modyfikator poza tablicą } return 0; } } W tak napisanym programie każde wystąpienie błędu w metodzie wykonaj spowoduje przejście do bloku catch w metodzie main. Komunikaty wyświetlone przez nasz program powinny wyglądać tak: Musisz pamiętać, że wyjątki w Javie stanowią pewną hierarchię: od najbardziej ogólnych do bardzo szczegółowych. W sytuacji, gdy wyjatek wystapi wykonywany jest tylko jeden blok catch: pierwszy, którego typ parametru jest zgodny z typem wyjątku. Dlatego też należy pamiętać, aby szczegółowe bloki catch umieszczać na początku, a bardziej ogólne - dalej.Najbardziej typowe błędy kolejności bloków obsługi wyjątków sygnalizowane są przez kompilator jako błędy. Gdybyśmy w jednym z poprzednich programów umieścili bloki obsługi wyjątków w takiej kolejności: try { ..... } catch (Exception e) { ..... } catch (ArithmeticException e) { ..... } catch (NullPointerException e) { ..... } catch (ArrayIndexOutOfBoundsException e) { ..... } } to praktycznie zawsze wykonywany byłby tylko pierwszy blok catch, gdyż klasa Exception jest najogólniejszą klasą wyjątków. Jednak kompilator nie dopuści do tego, sygnalizując błędy: exception java.lang.ArithmeticException has already been caught exception java.lang.NullPointerException has already been caught exception java.lang.ArrayIndexOutOfBoundsException has already been caught
|