PO Wyjątki
Obsługa sytuacji wyjątkowych
Nawet w proprawnie napisanych programach należy się liczyć z możliwością wystąpienia sytuacji wyjątkowych. Najczęstrzym rodzajem błedów których nie można uniknąć jest niedostępność różnego rodzaju zasobów, np. niemożliwość zapisu do pliku dyskowego czy problemy z komunikacją przez sięć. Wiele problemów może się pojawić gdy nieumiejętnie korzystamy z kodu napisanego przez kogoś innego lub gdy inni nieumiejętnie korzystają z kodu napisanego przez nas. Wiąże się to zazwyczaj z niezaglądaniem do dokumentacji technicznej lub z jej słabą jakością. Typowym przykładem takiej sytuacji jest przekazanie metodzie niepoprawnych danych, np. wartości null gdy spodziewała się referencji do obiektu lub krótszej niż oczekiwała tablicy.
Dobrze napisany kod powinien zakładać możliwość wystąpienia takich wyjątkowych sytuacji. Zazwyczaj jednak nie da się podjąć żadnych działań naprawczych w miejscu wykrycia błędu, trzeba przerwać normalny tok działania programu i przekazać informacje o błędzie "na zewnętrz" do szerszego kontekstu (do metody, która wywołała metodę w której występiła sytuacja wyjątkowa lub jeszcze dalej), gdzie mogą być podjęte działania napracze. Starsze języki programowania jak C nie zawierały żadnych specjalnych mechanizmów ułatwiających radzenie sobie w takich sytuacjach. Istniało natomiast kilka ogólnie przyjętych schematów postępowania. Można zwracać specjalną wartość oznaczającą błąd lub ustawiać glabalnie dostępną flagę, którą następnie trzeba skontrolować. Z czasem okazało się jednak, że programiści nie byli konsekwentni w przestrzeganiu tych konwencji. Zakładali, że błędy zdarzają się jedynie w kodzie innych, a w ich nie. Często byli też po prostu leniwi, gdyż obsługa wszystkich nietypowych sytuacji, np. sprawdzanie czy błąd nie wystąpił po wywołaniu każdej kolejnej metody, powodowałaby wielokrotne napęcznienie kodu oraz drastycznie utrudniła jego stworzenie i zrozumienie. W efekcie sytuacje wyjątkowe nie były prawidłowo rozpoznawane i program wykonywał się dalej powodując wystąpienie jeszcze większych problemów. Brak specjanlnych mechanizmów wymuszających obsługę sytuacji wyjątkowych był ważnym ograniczeniem przy tworzeniu dużych, solidnych i łatwych w pielęgnowaniu programów.
Mechanizm obsługi wyjątków
Nowoczesne obiektowe języki programowania mają wbudowany specjalny mechanizm ułatwiający i upraszczający radzenie sobie z obsługą systuacji wyjątkowych. W chwili wykrycia takiej sytuacji można stworzyć specjalny obiekt nazywany wyjątkiem (ang. exception), zawrzeć w nim wszystkie informacje na temat tego co się stało i przy pomocy specjalnej instrukcji throw (w niktórych językach raise) zgłosić ten wyjątek do obsłużenia. Zgłoszenie wyjątku wymusza przerwanie normalnego trybu wykonywania programu i rozwinięcie stosu wywołań aż do napotkania kontekstu zawierającego kod obsługi dla wyjątków tego rodzaju. Jeżeli cały stos zostanie rozwinięty, a wyjątku nie obsłużono, program zostaje przerywany.
Nieobsłużone wyjątki
Metoda głębiej() w pokazanej poniżej klasie NieobsługiwanyWyjątek zgłasza wyjątek klasy Exception (podstawowa klasa reprezentująca wyjątki w Javie), gdy przekazany jej parametr ma wartość null.
public class NieobsługiwanyWyjątek { void głębiej(String s) throws Exception { System.out.println("początek głębiej"); if (s == null) throw new Exception(); System.out.println("koniec głębiej"); } void głęboko(String s) throws Exception { System.out.println("początek głęboko"); głębiej(s); System.out.println("koniec głęboko"); } public static void main(String[] args) throws Exception { NieobsługiwanyWyjątek ue = new NieobsługiwanyWyjątek(); System.out.println("przed głęboko"); ue.głęboko(null); System.out.println("po głęboko"); } }
Z powodu zgłoszenia wyjątku stos wywołań jest rozwijany, a ponieważ żadna z kolejno znajdujących się tam metod głębiej(), głęboko() i main() nie zawiera kodu obsługi wyjątku, program zostaje przerwany. W takiej sytuacji Java wypisuje na standardowe wyjście błędu informacje o wyjątku, który spowodował przerwanie programu. Ich poziom szczegółowości zależy od ustawień kompilatora, ale przy ustawieniach domyślnych podawany jest ślad stosu wywołań z chwili wystąpienia wyjątku oraz numery linii w kodzie źródłowym odpowiadające kolejnym jego pozycjom.
przed głęboko początek głęboko początek głębiej Exception in thread "main" java.lang.Exception at test.NieobsługiwanyWyjątek.głębiej(NieobsługiwanyWyjątek.java:4) at test.NieobsługiwanyWyjątek.głęboko(NieobsługiwanyWyjątek.java:10) at test.NieobsługiwanyWyjątek.main(NieobsługiwanyWyjątek.java:17)
Nadzorowanie obsługi wyjątków przez kompilator
Warto zwrócić uwagę, że w deklaracjach wszystkich metod klasy NieobsługiwanyWyjątek występuje klauzula throws, która informuje jakich wyjątków można się spodziewać w wyniku wywołania danej metody. Ta klauzula jest wymuszane przez kompilator, jeżeli z metody mogą wydostać sie jakieś nieobsłużone wyjątki. Dzięki temu programista nie ma możliwości przeoczyć żadnego wyjątku i jeżeli nie chce wszystkich obsłużyć musi świadomie wymienić je (bądź ich nadklasy) w deklaracji metody.
Obsługa wyjątków
try { //kod który może zgłosić wyjątki } catch (Typ1 w) { //obsługa wyjątków Typ1 } catch (Typ2 w) { //obsługa wyjątków Typ2 } catch (Typ3 w) { //obsługa wyjątków Typ3 }
Do obsługi wyjątków służy instrukcja try-catch. Po try podaje się blok instrukcji, których wyjątki chcemy obsługiwać. Następnie podawana jest lista przypominających deklaracje metod klauzul catch, z których każda obsługuje wyjątki określonego typu. Typ wyjątku podaje się między nawiasami bezpośrednio po słowie kluczowym catch wraz z nazwą zmiennej, na którą zostanie przypisany złapany egzemplarz wyjątku. Do tej zmiennej można się odwoływać w kodzie obsługi wyjątku. Do obsługi wyjątku Wybierana jest zawsze pierwsza pasująca klauzula catch. Ponieważ wyjątki są obiektami "pasowanie" oznacza tu po prostu możliwość przypisania wyjątku na zmienną zadeklarowaną po słowie kluczowym catch. Wszystkie dalsze klauzule catch są pomijane, nawet jeżeli pasowały. Jeżeli wyjątek nie pasuje do żadnej klauzuli catch, nie zostaje obsłużony i stos wywołań jest nadal rozwijany. W pokazanej poniżej klasie ObsługiwanyWyjątek, która jest modyfikacją poprzedniego przykładu, wyjątek Exception jest już obsługiwany i nie powoduje przerwania programu.
public class ObsługiwanyWyjątek { void głębiej(String s) throws Exception { System.out.println("początek głębiej"); if (s == null) throw new Exception(); System.out.println("koniec głębiej"); } void głęboko(String s) { try { System.out.println("początek głęboko"); głębiej(s); System.out.println("koniec głęboko"); } catch (Exception e) { System.out.println("obsługa wyjątku"); e.printStackTrace(System.out); } System.out.println("po obsłużeniu wyjątku"); } public static void main(String[] args) { ObsługiwanyWyjątek ep = new ObsługiwanyWyjątek(); System.out.println("przed głęboko"); ep.głęboko(null); System.out.println("po głęboko"); } }
Warto zwrócić uwagę, że metody głęboko() i main() nie muszą już posiadać klauzuli throws.
Na konsolę nadal wypisywane są informacje o wyjątku, ale jest to spowodowane użyciem w kodzie obsługi wyjątku metody printStackTrace() z parametrem System.out. Metoda ta jest odziedziczona z klasy Throwable, po której muszą dziedziczyć wszystkie wyjątki.
przed głęboko początek głęboko początek głębiej obsługa wyjątku java.lang.Exception at test.ObsługiwanyWyjątek.głębiej(ObsługiwanyWyjątek.java:4) at test.ObsługiwanyWyjątek.głęboko(ObsługiwanyWyjątek.java:11) at test.ObsługiwanyWyjątek.main(ObsługiwanyWyjątek.java:23) po obsłużeniu wyjątku po głęboko
Hierarchia wyjątków w Javie
W trakcie wykonywania instrukcji występujących po słowie kluczowym try mogą zajść różne sytuacje wyjątkowe. Być może na każdą z nich należy zareagować w inny sposób. W obiekcie zgłaszanym jako wyjątek można zawrzeć dodatkowe informacje pozwalające zdecydować jakie działania naprawcze podjąć. Zazwyczaj jednak używana się wyjątków będących egzemplarzami różnych klas i stosuje dla każdej z nich oddzielną klauzulę catch. W Javie zdefiniowanych jest wiele typowych wyjątków, najważniejsze w pakiecie java.lang. Są one używane przez klasy ze standardowych bibliotek. Różnią się jedynie nazwą, która zazwyczaj pozwala zorientować się co dany wyjątek oznacza. W swoich aplikacjach można korzystać z tych klas lub zdefiniować nowe, pasujące do konkretnego zastosowania. Wszystkie klasy, które mogą być zgłoszone jako wyjątek muszą dziedziczyć po klasie Throwable. Tej klasy jednak bezpośrednio się nie rozszerza. Hierarchia wyjątków w Javie jest pokazana na diagramie poniżej.

Klasa Error
Jedyne dwie bezpośrednie podklasy Throwable to Error i Exception. Wyjątki dziedziczące po Error reprezentują poważne problemy, których aplikacja nie będzie w stanie rozwiązać. Przykładową podklasą jest VirtualMachineError, która oznacza, że maszyna wirtualna nie może dalej kontynuować pracy, np. z powodu wyczerpania się zasobów systemowych. Wyjątków rozszerzających Error nie należy przechwytywać, gdyż nie ma możliwości zaradzenia sytuacjom wyjątkowym, które je spowodowały. Z założenia mogą wystąpić praktycznie w każdej instrukcji kodu i nie muszą być wymieniane w klauzulach throws. Inną ciekawą podklasą Error jest AssertionError. Wyjątki tego typu są zgłaszane, jeżeli niezmiennik – w Javie określany jako asercja – umieszczony przez programistę w kodzie staje się nieprawdziwy. Asercje zostaną wyjaśnione na następnym wykładzie.
Klasa Exception
Wyjątki dziedziczące po Exception reprezentują sytuacje, na które dobrze napisana aplikacja powinna być przygotowana. To właśnie tą klasę rozszerza się tworząc własne rodzaje wyjątków. Jej przykłade podklasy to:
- IOException, która reprezentuje sytuacje wyjątkowe związane z wejściem/wyjściem,
- ClassNotFoundException, która wskazuje, że maszyna wirtualna nie odnalazła klasy o nazwie podanej jako napis,
- SQLException, która reprezentuje sytuacje wyjątkowe związane z dostępem do bazy danych oraz
- SAXParseException, która wskazuje, że podczas parsowania dokumentu XML wystąpił błąd.
Klasa RuntimeException
Bardzo ciekawą podklasą Exception jest RuntimeException, która sama posiada wiele podklas. Wyjątki rozszerzające RuntimeException mogą wystąpić podczas typowych operacji jak rzutowanie zmiennej, odwołanie się do elementu tablicy lub odwołanie się do składowej obiektu. Ich wystąpienie zazwyczaj oznacza, że programista popełnił błąd w swoim kodzie lub nieumiejętnie korzystał z kodu napisanego przez innych. Maszyna wirtualna wykrywa wystąpienie takich błędów w trakcie działania programu i informuje o tym zgłaszając odpowiedni wyjątek. Przykładowymi podklasami RuntimeException są:
- ClassCastException, która oznacza próbę rzutowania zmiennej na niepasujący typ,
- IndexOutOfBoundsException, która oznacza odwołanie się do indeksu z poza zakresu oraz
- NullPointerException, która oznacza że zamiast referencji wskazującej na obiekt pojawiła się wartość null.
Wyjątki te można obsługiwać, chociaż zdecydowanie lepiej jest zadbać żeby się nie pojawiały. Zgłaszając ten typ wyjątków lub tworząc nowe ich rodzaje pamiętaj, że wskazują błędy w programie, a nie nietypowe sytuację. Ze względu na swoją wszechobecność wymienianie ich w klauzulach throws praktycznie wszystkich metod mijałoby się się z celem, więc tak samo jak w przypadku wyjątków rozszerzających Error nie ma takiego obowiązku.
Rozszerzanie klasy Exception
Klasę Exception rozszerza się tak jak każdą inną klasę. Jak już zostało powiedziane rozróżnianie typu wyjątku wystarcza w większości zastosowań, dlatego zazwyczaj nie definiuje się ani nie przedefiniowuje się żadnych metod.
class MójNowyWyjątek1 extends Exception {} public class NowyWyjątek1 { public static void main(String[] args) { try { System.out.println("Rozpoczynamy test nowego wyjątku."); throw new MójNowyWyjątek1(); } catch (MójNowyWyjątek1 w) { System.out.println("Wiwat twórca wyjątków!"); } } }
Są jednak sytuacje, gdy warto w wyjątku przekazać jakieś dodatkowe informacje. Czasami wyjątki są zgłaszane, aby przenieść tekstowy opis nietypowej sytuacji, aby mógł być przedstawiony użytkownikowi przez kod obsługi wyjątku. Przykładem takiej sytuacji jest niemożliwość odnalezienia lub otwarcia pliku, którego nazwę wskazał użytkownik. Dlatego wszystkie wyjątki z pakietu java.lang (w tym Exception) posiadają dodatkowy konstruktor przyjmujący jako parametr napis przenoszony przez wyjątek. Oprócz tego, nadklasa wszystkich wyjątków Throwable, definiuje metodę getMessage() przy pomocy której można ten napis z wyjątku wydobyć. Żeby móc korzystać z tego mechanizmu należy do swoich wyjątków dodać konstruktory przyjmujący napis.
class MójNowyWyjątek2 extends Exception { MójNowyWyjątek2(String s) { super(s); } //ponieważ definiujemy jakiś inny konstruktor, //kompilator sam nie doda bezparametrowego MójNowyWyjątek2() {} } public class NowyWyjątek2 { public static void main(String[] args) { try { throw new MójNowyWyjątek2("Ten napis przyda się podczas obsługi wyjątku."); } catch (MójNowyWyjątek2 w) { System.out.println(w.getMessage()); } } }
Zgłaszanie wyjątków w klauzuli catch
Wyjątki zgłoszone w klauzuli catch nie są obsługiwane przez tą samą instrukcję try-catch.
Ponowne zgłaszanie obsługiwanego wyjątku
Czasami obsługiwany wyjątek jest zgłaszany ponownie.
void głębiej() throws Exception { throw new Exception(); } void głęboko() throws Exception { try { głębiej(); } catch (Exception e) { throw e; } }
Ponowne zgłoszenie nie zmienia zapamiętanego śladu wywołań. Nie jest on wypełniany w chwili zgłoszenia wyjątku, ale w chwili jego utworzenia. Dzieje się tak za sprawą konstruktora z nadklasy Throwable, który wywołuje metodę fillInStackTrace(). Przy pomocy tej metody można samemu podmienić na aktualny ślad stosu wywołań w ponownie zgłaszanym wyjątku.
void głębiej() throws Exception { throw new Exception(); } void głęboko() throws Exception { try { głębiej(); } catch (Exception e) { e.fillInStackTrace(); throw e; } }
Wyjątek powodujący zgłoszenie innego wyjątku
Od Javy 1.4 do zgłaszanego wyjątku można dołączyć wyjątek, który spowodował jego wystąpienie. Używa się do tego metody initCause(), która jako parametr przyjmuje dołączany wyjątek. Dla wygody do klas Throwable, Exception i RuntimeException dodano również po dwa nowe konstruktory. Pierwszy przyjmujący jako jedyny parametr wyjątek do dołączenia oraz drugi przyjmujący jako pierwszy parametr napis przenoszony przez wyjątek oraz jako drugi parametr wyjątek do dołączenia. Jeżeli mamy zamiar w ten sposób łączyć wyjątki w ciągi, warto dodać takie konstruktory do nowo definiowanych wyjątków.
class MójNowyWyjątek extends Exception { public MójNowyWyjątek() { super(); } public MójNowyWyjątek(String message) { super(message); } public MójNowyWyjątek(Throwable cause) { super(cause); } public MójNowyWyjątek(String message, Throwable cause) { super(message, cause); } } public class WyjątekSpowodowanyWyjątkiem { public static void main(String[] args) throws MójNowyWyjątek { try { throw new Exception("To ja jestem wszystkiemu winien."); } catch (Exception e) { throw new MójNowyWyjątek("To nie moja wina.", e); } } }
Informacje o dołączonym wyjątku można uzyskać przy pomocy metody getCause(). Informacje te są również wypisywane na standardowe wyjście błędu, jeżeli wykonanie programu zostało przerwane przez nieobsłużony wyjątek.
Exception in thread "main" test.MójNowyWyjątek: To nie moja wina. at test.WyjątekSpowodowanyWyjątkiem.main(WyjątekSpowodowanyWyjątkiem.java:24) Caused by: java.lang.Exception: To ja jestem wszystkiemu winien. at test.WyjątekSpowodowanyWyjątkiem.main(WyjątekSpowodowanyWyjątkiem.java:22)
Wsparcie ze strony kompilatora
Wiele błędów w programie związanych z nieprawidłową obsługą wyjątków można wykryć już w chwili kompilacji. Jak wyjaśniliśmy wcześniej kompilator pilnuje, aby wszystkie potencjalne wyjątki zostały obsłużone lub jawnie wymienione w nagłówku metody. Co więcej kompilator nie pozwoli umieścić nieosiągalnej klauzuli catch. Klauzula jest nieosiągalna, jeżeli wszystkie potencjalne wyjątki do niej pasujące są obsługiwane przez wcześniejsze klauzule
public class NieosiągalnaKlauzulaCatch { public static void main(String[] args) { try { System.out.println("Kompilator na ratunek gapom."); } catch (Exception e) { System.out.println("Ta klauzula nic nie przepuszcza dla pozostałych."); } //catch (NullPointerException e) { //System.out.println("Ten kod jest tu niepotrzebny."); //} } }
Oprócz tego dana klauzula catch może być po prostu niepotrzebna, jeżeli nigdzie w bloku try nie jest zgłaszany wyjątek do niej pasujący ani nie jest używana żadna metoda, która by taki wyjątek miała wymieniony w swojej klauzuli throws. Wystąpienie nieosiągalnej lub niepotrzebnej klauzuli catch oznacza, że programista się pomylił i chciał zrobić coś innego, dlatego kompilator zwróci na to uwagę.
Kolejnym rodzajem pomyłek przed, którymi kompilator chroni nieuważnych programistów jest nieosiągalny fragment kodu znajdujący się po zgłaszanym wyjątku.
throw new Exception(); //System.out.println("Ten kod nigdy by się nie wykonał.");