Podczas działania programu mogą wystąpić różne sytuacje specjalne, do których należą m.in. wystąpienia błędu polegającego na próbie otwarcia pliku, który nie istnieje. Java posiada zapożyczony z języka Ada mechanizm informowania o błędach: wyjątki (ang. exceptions). Mechanizm obsługi wyjątków w Javie umożliwia zaprogramowanie "wyjścia" z takich sytuacji krytycznych, dzięki czemu program nie zawiesi się po wystąpieniu błędu wykonując ciąg operacji obsługujących wyjątek. Generowanie i obsługę sytuacji wyjątkowych w Javie zrealizowano przy wykorzystaniu paradygmatu programowania zorientowanego obiektowo.
Wystąpienie sytuacji wyjątkowej przerywa "normalny" tok wykonywania programu. W Javie sytuacja wyjątkowa występuje wtedy, gdy program wykona instrukcję throw. Wyrażenie throw przekazuje sterowanie do skojarzonego z nim bloku catch (łap, blok obsługujący wystąpienie sytuacji wyjątkowej). Jeśli nie ma bloku catch w bieżącej metodzie, 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, przekazuje sterowanie 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.
Wszystkie wyjątki, jakie mogą wystąpić w programie muszą być podklasą klasy Throwable. Poniższy rysunek pokazuje hierarchię dziedziczenia klasy Throwable i jej najważniejszych podklas.
Rysunek 2-4 Hierarchia dziedziczenia klas wyjątków
Wyjątki typu Error występują wtedy, gdy wystąpi sytuacja specjalna w maszynie wirtualnej (np. błąd podczas dynamicznego łączenia). Wyjątki tego typu nie powinny być obsługiwane w "zwykłych" programach Javy. Jest także mało prawdopodobne, że typowy program Javy spowoduje wystąpienie wyjątku tego typu.
W większości programów generowane są i obsługiwane obiekty, które dziedziczą z klasy Exception. Wyjątek tego typu oznacza, że w programie wystąpił błąd, lecz nie jest to poważny błąd systemowy.
Szczególną podklasę klasy Exception stanowią wyjątki, które występują podczas wykonywania programu, są to wyjątki typu RunTimeExceptions (i jej podklas np.: NullPointerException, ClassCastException, IllegalThreadStateException i ArrayOutOfBoundsException) i występują np.: wtedy, gdy zostaną wyczerpane zasoby systemowe, nastąpi odwołanie do nie istniejącego elementu tablicy i inne. Gdy wyjątek taki nie jest obsłużony, program zostaje zatrzymany, a na ekranie pojawia się nazwa wyjątku oraz klasa i metoda, w której wystąpił. Dzięki temu wiemy, w którym miejscu kodu wystąpił błąd i można go szybko poprawić.
Zobaczmy na przykładzie, jak wygląda wywołanie wyjątku w programie.
Przykład 2.20 Generacja sytuacji wyjątkowych
public class WywolajWyjatek { static public void main(String args[]) throws Exception { Liczba liczba = new Liczba(); liczba.dziel(1); } } class Liczba { int m_i = 10; int dziel(float i) throws Exception { if (i/2 != 0) throw new Exception("Liczba nieparzysta!"); if (i == 0) throw new Exception("Dzielenie przez zero!"); return (int)(m_i/i); } }
W metodzie Liczba.dziel() klasy WywolajWyjatek, za pomocą frazy throw new Exception("..."), generujemy wyjątek poprzez utworzenie obiektu typu Exception i przerywamy wykonanie metody. Jak już wspomniano obiekt ten musi być typu będącego podklasą klasy Throwable. W programach zawsze generujemy wyjątki typu Exceptions lub dowolnej podklasy Exceptions. W ten sposób można w Javie wywoływać wyjątki, dla sytuacji, które uważamy za nieprawidłowe. W powyższym przykładzie założono, że nieprawidłowa jest sytuacja gdy zmienna 'i' jest liczba nieparzystą lub jest równa zero. W obu przypadkach generowany jest wyjątek, choć z innym komentarzem. W definicji metody dziel() użyto frazy throws Exception, jej użycie informuje maszynę wirtualną Javy i metodę wołającą, że metoda może generować wyjątek typu Exception. Użycie tej frazy jest obowiązkowe, jeśli nasza metoda może generować wyjątek. Każda metoda, która woła metodę dziel() musi albo mieć blok (catch) obsługujący wyjątek albo informację throws Exception, że może być źródłem wyjątku pochodzącego z metody, którą woła w swoim ciele. Przykładem tego jest metoda main(), która nie ma obsługi wyjątku a tylko frazę throws Exception. W naszej aplikacji wygenerowany błąd nie zostaje nigdzie obsłużony, więc program kończy działanie, a na ekranie widzimy:
Ilustracja 2-4 Rezultat wykonania aplikacji WywolajWyjatek.
Wiadomo, że wyjątek może wystąpić w programie właściwie w każdym momencie jego wykonania. Nie jest wymagane użycie frazy throws NazwaKlasyWyjątku w nagłówku deklaracji metody dla błędów klasy RunTimeException lub jej podklas. Umieszczenie frazy throws dla tych przypadków jest jednak dobrym pomysłem, szczególnie wtedy, gdy sami generujemy jeden z powyższych wyjątków w swojej metodzie.
Blok instrukcji:
try { //blok instrukcji gdzie może wystąpić wyjątek } catch (ObiektImplementujacyInterfejsThrowable nazwaZmiennej) { //blok instrukcji obsługujących wystąpienia sytuacji wyjątkowej //jest wykonywany tylko, gdy wystąpi wyjątek typu takiego jak // typ zmiennej będącej parametrem bloku catch } catch (ObiektImplementujacyInterfejsThrowable nazwaZmiennej) { . . . } catch (ObiektImplementujacyInterfejsThrowable nazwaZmiennej) { . . . } finally //opcjonalnie { // ten blok instrukcji jest wykonywany przed opuszczeniem // sterowania, nawet jeśli blok try zawiera instrukcję // return lub spowodował wystąpienie wyjątku }
przeznaczony jest do obsługi wystąpienia sytuacji wyjątkowych. Blok catch jest fragmentem programu wykonywanym w przypadku wystąpienia wyjątku w bloku try. Blok catch musi znajdować się zaraz za blokiem try lub następnym blokiem catch. Użycie wielu bloków catch pozwala obsłużyć wystąpienie wyjątków różnych typów.
Przykładem użycia bloku catch niech będzie niewiele zmieniona metoda main() z klasy WywolajWyjatek:
static public void main(String args[]) throws Exception { Liczba liczba = new Liczba(); try { liczba.dziel(1); } catch (Exception e) { e.printStackTrace(); } pauza(); }
Dodano tu obsługę wystąpienia wyjątku typu Exception w metodzie liczba.dziel(). Wyjątek w bloku catch może zostać obsłużony na wiele sposobów. W naszym przypadku, obsługa wystąpienia wyjątku polega na wydrukowaniu na ekranie (w tym celu użyto standardowej metody printStackTrace() z klasy Exception) ścieżki wywołań do metody, w której wystąpił wyjątek. Informacje te są może niezbyt ważne dla użytkownika ale mają ogromne znaczenie dla programisty w procesie pisania i testowania kodu.
Ilustracja 2-5 Rezultat wykonania aplikacji WywolajWyjatek po zmodyfikowaniu metody main().
Ewentualne wystąpienie wyjątku w metodzie dziel() dzięki zastosowanie bloku catch w metodzie main() zostanie obsłużone. Gdyby nie to, że także metoda pomocnicza pauza() w metodzie main() może generować wyjątek, użycie frazy throws Exception byłoby w metodzie main() nadmiarowe, choć nie byłoby błędem, ponieważ wyjątek może wystąpić w innym miejscu programu.
Sterowanie opuszcza blok try w przypadku wystąpienia instrukcji return lub sytuacji wyjątkowej. Java pozwala jednak zdefiniować blok instrukcji, które będą wykonane zanim sterowanie opuści metodę niezależnie od tego, czym jest to spowodowane. Jest to blok finalny (ang. finally block), nazywany tak od słowa kluczowego finally. W języku C++ nie ma odpowiednika bloku finalnego z Javy.
Poniżej prezentujemy przykład programu, który wyświetla na ekranie zawartość pliku: tekst.txt i próbuje zrobić to samo dla nieistniejącego pliku nieistniejacy.txt.
Przykład 2.21 Obsługa wyjątków przy operacji czytania z pliku
import java.io.*; class ReadFile { public static void main(String[] args) throws Exception { //Proba wyswietlenia na ekranie pliku tekst.txt PokazPlik(new File("tekst.txt")); //Proba wyswietlenia na ekranie zawartości // nieistniejacego pliku PokazPlik(new File("nieistniejacy.txt")); //Zatrzymanie wyniku dzialania programu na ekranie pauza("Koniec programu"); } static void PokazPlik(File plik) throws Exception { try { FileInputStream in = new FileInputStream(plik); //Klasa BufferedInputStream umożliwia czytanie wiekszych //ilości danych z pliku BufferedInputStream bin = new BufferedInputStream(in); try { byte bTablica[] = new byte[10]; int nPrzeczytanychBajtow; System.out.println("Dane z pliku "+plik.getName()); while(bin.available()>0) { //czyanie danych z pliku nPrzeczytanychBajtow = bin.read(bTablica); //wyprowadzenie danych na ekran System.out.write(bTablica); } } //przechwycenie wyjątków podczas czytania z pliku catch (IOException ioe) { System.out.println(ioe.toString()); } finally { //zamkniecie pliku in.close(); System.out.println("\nPlik "+plik.getName()+" zamkniety"); } } // przechwycenie wyjątków podczas otwierania pliku catch (IOException ioe) { System.out.println("Blad przy otwarciu pliku " + plik.getName()); ioe.printStackTrace(); } finally { pauza("Koniec czytania"); } } static void pauza(String s) throws Exception { System.out.print(s+" Nacisnij Enter.....\n"); System.in.read(); } }
Ilustracja 2-6 Wynik działania aplikacji ReadFile
Zastosowanie bloku finally pozwala uniknąć dublowania kodu, który musiałby być napisany zarówno dla przypadku, gdy wystąpi wyjątek, jak i dla normalnego toku wykonania programu. Blok finalny jest odpowiednim miejscem do zwolnienia zasobów zarezerwowanych przez metodę, ponieważ zasoby te powinny być zwolnione niezależnie od tego, czy wykonanie programu przebiegło w sposób zaplanowany, czy też wystąpił wyjątek.
W Javie umożliwiono definiowanie klasy wyjątków, które będą obsługiwały sytuacje, uznane przez programistę za wyjątkowe. Zaprezentujemy przykład, w którym zdefiniowano klasę wyjątków NaszWyjatek, która jest podklasą klasy Exception.
Przykład 2.22 Definicja własnej klasy wyjątków
class NaszWyjatek extends Exception { NaszWyjatek() { this(""); } NaszWyjatek(String s) { super("\n***\n\tNic sie nie stalo to tylko: " + "NaszWyjatek\n***\n\t"+s); } }
Zdefiniujmy teraz klasę Wyjatek definiującą wyjątki: NaszWyjatek, operację dzielenia przez zero, odwołania do nieistniejącego obiektu, odwołania do elementu tablicy poza jej zakresem.
public class Wyjatek { static void pauza() throws Exception { ... } public static void main(String[] args) throws Exception { String wyjatki[] ={"dzielenie","null","test","tablica"}; for (int i = 0; i < 4; i++) { try { wygeneruj(wyjatki[i]); System.out.println("Wyjatek przy operacji typu:\"" + wyjatki[i] + "\" nie zostal wygenerowany"); } catch (Exception e) { System.out.println("Przy operacji typu \"" + wyjatki[i] + "\" wystapil wyjatek: \n" + e.getClass() + "\n Z nastepujaca informacja: " + e.getMessage()); } } pauza(); } static int wygeneruj(String s) throws NaszWyjatek { try { if (s.equals("dzielenie")) { int i = 0; return i/i; } if (s.equals("null")) { s = null; return s.length(); } if (s.equals("test")) { throw new NaszWyjatek("Test sie powiodl"); } if (s.equals("tablica")) { int t[] =new int[5] ; return t[6]; } return 0; } finally { System.out.println("\n[wygeneruj(\"" + s +"\") zakonczone]"); } } }
Jak widać na ilustracji 2-7 aplikacja w Javie po wystąpieniu wyjątków tego rodzaju nie zawiesza się ale może je obsłużyć. W przykładzie obsługa sytuacji wyjątkowej sprowadza się do wydrukowania na ekranie informacji o wystąpieniu wyjątku i dodatkowego tekstu komentarza.
Ilustracja 2-7 Wynik wykonania aplikacji Wyjatek.
Pojedyncza metoda może spowodować wystąpienie wyjątków różnego rodzaju. Aby przedstawić sposób obsługi wielu wyjątków napiszmy szkielet aplikacji przeznaczonej do rezerwacji miejsc na loty do różnych miast.
Przykład 2.23 Obsługa wyjątków różnego typu
Na początku zdefiniujmy klasę Lot opisującą pojedynczy rejs samolotu.
class Lot { int m_nIloscMiejsc, m_nWolneMiejsca, m_nZarezerwowane; // Tablica miejsca[] zawiera informacje o pasażerach, // którzy zarezerwowali poszczególne miejsca w samolocie. Pasazer miejsca[]; String KodRejsu; //... definicje innych pol danych Lot(int iloscMiejsc, String kod) // Konstruktor klasy Lot { m_nIloscMiejsc = iloscMiejsc; // utworzenie tablicy wskaźników na obiekty typu Pasazer miejsca = new Pasazer[iloscMiejsc]; m_nWolneMiejsca = iloscMiejsc; // na początku nie ma żadnego zarezerwowanego miejsca m_nZarezerwowane = 0; KodRejsu = kod; } // Metoda SprawdzWolneMiejsca() sprawdza czy są jeszcze // wolne miejsca na bieżący lot a w razie ich braku // powoduje wystąpienie wyjątku typu BrakWolnychMiejsc int SprawdzWolneMiejsca() throws BrakWolnychMiejsc { if (m_nWolneMiejsca == 0) { throw new BrakWolnychMiejsc(this); } return m_nWolneMiejsca; } //... definicje innych metod klasy Lot }
Zdefiniujmy też klasę wyjątku BrakWolnychMiejsc, występującą wtedy, gdy nie ma już wolnych miejsc na dany lot:
class BrakWolnychMiejsc extends Exception { BrakWolnychMiejsc(Lot l, String info) { // Wywołanie konstruktora nadklasy: Exception(String) super("\n"+info+l.KodRejsu+"\n"); } BrakWolnychMiejsc(Lot l) { // Wywołanie pierwszego konstruktora tej klasy this(l,"Brak wolnych miejsc na lot :"); } }
Zdefiniujmy także klasę BrakRezerwacji jako podklasę klasy BrakWolnychMiejsc. Widać, że definiowane przez nas klasy wyjątku mogą w dowolny sposób obsługiwać wystąpienie wyjątku. (W naszym przykładzie klasy wyjątków ograniczają się do przygotowania odpowiednich komunikatów dla użytkownika.)
class BrakRezerwacji extends BrakWolnychMiejsc { BrakRezerwacji(Lot l, Pasazer p) { // Wywołanie konstruktora nadklasy: BrakWolnychMiejsc(Lot, String) super(l,"Nie bylo rezerwacji na nazwisko " + p.Nazwisko + "\nna lot "); } }
Klasa Pasazer opisuje pasażera i takie jego właściwości jak: imię i nazwisko (pole Nazwisko), rezerwację (pole Rezerwacja czyli referencja na obiekt typu Lot - opisujący lot na jaki pasażer zarezerwował miejsce).
class Pasazer { String Nazwisko; // dzięki deklaracji private informacja o rezerwacji dostepna jest // tylko poprzez metody tej klasy private Lot Rezerwacja; //... definicje innych pol danych Pasazer(String Nazwisko, Lot lot) throws BrakWolnychMiejsc { //Sprawdzamy czy na lot sa wolne miejsca if ((lot != null) && (lot.m_nWolneMiejsca == 0)) throw new BrakWolnychMiejsc(lot); this.Nazwisko = Nazwisko; Rezerwacja = lot; System.out.println(this.Nazwisko+ " rezerwacja na lot"+lot.KodRejsu); } // metoda ta sprawdza czy pasażer ma rezerwację na lot l // gdy takiej rezerwacji nie posiada generowany jest // wyjątek BrakRezerwacji boolean SprawdzRezerwacje(Lot l) throws BrakRezerwacji { if (Rezerwacja != l) throw new BrakRezerwacji(l,this); return true; } //... definicje innych metod }
W celu sprawdzenia działania wszystkich powyżej zadeklarowanych klas stworzono klasę Rezerwacja, w której w ciele metody main() wywołana jest metoda test().W metodzie test() w bloku try tworzymy kolejne obiekty typu Pasazer (patrz linia /*14*/). Ponieważ dla obiektu lot[0] reprezentującego lot do Londynu liczba wolnych miejsc ustawiona została na zero (/*10*/), podczas próby utworzenia tego obiektu wygenerowany zostanie wyjątek BrakWolnychMiejsc.
public class Rezerwacja { public static void main(String args[]) throws Exception { test(); } static void test() throws Exception { int iloscLotow = 3; // deklaracja i inicjalizacja tablicy pas[] // zawierającej informacje o nazwisku i imieniu // pasażera, dane te zostaną użyte przy inicjalizacji // tablicy pasażer[] String pas[] = {"Kowalski Artur","Nowak Olga","Egg Jan"}; // deklaracja i utowrzenie tablicy pasazer[] referencji do // obiektów typu Pasażer (bez inicjalizacji) Pasazer pasazer[] = new Pasazer[pas.length]; Lot lot[] = new Lot[iloscLotow] ;
// inicjalizacja tablicy lot[] lot[0] = new Lot(250,"Londyn 0566-45g BA"); lot[1] = new Lot(150,"Los Angelse 0235-45g A&A"); lot[2] = new Lot(250,"New York 0345-65 Lot");
// ustawienie ilości wolnych miejsc na 0 dla lotu do Londynu // robimy to aby wymusić wystąpienie wyjątku // BrakWolnychMiejsc dla próby rezerwacji miejsc na ten lot /*10*/ lot[0].m_nWolneMiejsca = 0; for (int i=0;i<iloscLotow;i++) try { //Próba rezerwacji dla pasażera pas[i] na lot lot[i] /*14*/ pasazer[i] = new Pasazer(pas[i],lot[i]);
// Tu sprawdzamy, czy pasazer[1] ma rezerwację // na lot lot[0], a ponieważ nie ma tej rezerwacji // wygenerowany zostanie wyjątek BrakRezerwacji if (i==2) pasazer[1].SprawdzRezerwacje(lot[0]); } /*18*/ catch (BrakRezerwacji br) { System.out.println("\n***********"); br.printStackTrace(); } /*23*/ catch (BrakWolnychMiejsc bwm) { bwm.printStackTrace(); } catch (Exception e) { e.printStackTrace(); } finally { System.out.println("====================\n"); } /*35*/ pauza(); }
static void pauza() throws Exception { /* ... Zdefiniowana już wcześniej w tej pracy */ } }
Ewentualne wystąpienie wyjątków typu BrakWolnychMiejsc i BrakRezerwacji jest obsłużone w metodzie test(), nie ma potrzeby informowania o ich wystąpieniu metod wołających metodę test() (fraza throws). W nagłówku metody test() mamy jednak frazę: throws, informuje ona metody wołające ją, że w metodzie tej może wystąpić wyjątek typu Exception pochodzący z metody pauza()/*35*/.
Po wystąpieniu wyjątku w bloku try, Java porównuje wyjątek, który wystąpił z parametrami poszczególnych bloków catch. Przypuśćmy, że wystąpił wyjątek typu BrakWolnychMiejsc, pierwszy blok catch /*18*/ nie obsługuje tego wyjątku, więc sterowanie przekazywane jest do drugiego bloku /*23*/, który obsłuży wystąpienie tego wyjątku. W ten sposób możemy obsłużyć wystąpienie wyjątków różnego rodzaju.
Ilustracja 2-8 Wynik wykonania aplikacji Rezerwacja.
Należy jednak pamiętać, że wyjątek może być obsłużony nie tylko wtedy, gdy parametrem bloku catch będzie zmienna typu takiego, jak typ wyjątku, który wystąpił. Wyjątek będzie obsłużony także w przypadku, gdy parametrem bloku catch będzie zmienna typu, z którego dziedziczy typ wyjątku. Dlatego, gdyby dla naszego przykładu kolejność obsługi wyjątków była następująca:
try { ... } catch (Exception e) { ... } catch (BrakRezerwacji br) { ... } catch (BrakWolnychMiejsc bwm) { ... }
wyjątki typu BrakRezerwacji i BrakWolnychMiejsc nigdy nie zostałyby obsłużone w bloku catch do tego przeznaczonym ale zawsze w bloku catch (Exception e). Większość kompilatorów Javy dla takiego przypadku generuje błąd kompilacji, np. Microsoft Visual J++ po kompilacji wyświetli komunikat:
error J0102: Handler for 'BrakRezerwacji' hidden by earlier handler for 'Exception' error J0102: Handler for 'BrakWolnychMiejsc' hidden by earlier handler for 'Exception'
Należy więc pamiętać aby bardziej ogólne
bloki catch obsługi wyjątków umieszczać dalej.