PO Wyjątki: Różnice pomiędzy wersjami
Linia 118: | Linia 118: | ||
* ''IndexOutOfBoundsException'', która oznacza odwołanie się do indeksu z poza zakresu oraz | * ''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'''. | * ''NullPointerException'', która oznacza że zamiast referencji wskazującej na obiekt pojawiła się wartość '''null'''. | ||
Mimo, że takie wyjątki też można obsługiwać, zdecydowanie lepiej jest zadbać żeby się nie pojawiały. Zgłaszając wyjątki jednej z istniejących podklas ''RuntimeException'' pamiętaj, że wskazują błędy w programie, a nie nietypowe sytuacje. 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'' == | == Rozszerzanie klasy ''Exception'' == |
Wersja z 21:23, 26 gru 2006
<<< Powrót do przedmiotu Programowanie obiektowe
Obsługa sytuacji wyjątkowych
Nawet w poprawnie napisanych programach należy się liczyć z możliwością wystąpienia sytuacji wyjątkowych. Nie można przykładowo przewidzieć, że podczas wpisywania do pliku skończy się miejsce na dysku (bo zajął je inny proces), lub że podczas przesyłania danych zerwie się połączenie. Nie da się również zagwarantować, że inni będą poprawnie używali naszych bibliotek. Nie uchroni przed tym nawet wzorowa dokumentacja techniczna, gdyż nie wszyscy do niej zaglądają. 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 sytuacji wyjątkowych. Zazwyczaj jednak nie da się podjąć żadnych działań naprawczych w miejscu ich wykrycia. 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 daną metodę 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, skutkowałaby wielokrotnym napęcznieniem 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 niektó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 jest 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. Użycie tej klauzuli 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. Jeżeli wymienianych jest kilka klas, ich nazwy oddziela się przecinkiem.
W klauzli throws można również wymienić wyjątki, które w aktualnej wersji metody nie mogą wystąpić. W ten sposób można za wczasu wymusić na innych programistach, aby ich kod używający naszej metody był na te wyjątki przygotowany. Gdy w przyszłej wersji będą już się mogły pojawić, nie spowoduje to żadnych problemów.
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 bloków catch, które przypominają deklaracje metod. Każda z nich obsługuje wyjątki określonego typu. Typ wyjątku wraz z nazwą zmiennej, na którą zostanie przypisany złapany egzemplarz, podaje się między nawiasami bezpośrednio po słowie kluczowym catch. Do tej zmiennej można się odwoływać w kodzie obsługi wyjątku. Do obsługi wyjątku wybierany jest zawsze pierwszy pasujący blok 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 bloki catch są pomijane, nawet jeżeli pasowały. Jeżeli wyjątek nie pasuje do żadnego bloku 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
Rozwijanie stosu
Poniższa prezentacja pokazuje jak rozwijany jest stos podczas zgłaszania wyjątku.
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żywa się wyjątków będących egzemplarzami różnych klas i stosuje dla każdej z nich oddzielny blok catch. W Javie zdefiniowanych jest wiele typowych wyjątków (najważniejsze są w pakiecie java.lang). Są używane przez klasy ze standardowych bibliotek. Różnią się jedynie nazwą, która 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órych egzemplarze 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. Wystąpienie takiego wyjątku 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 te wyjątki 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 okazuje się po wyliczeniu nieprawdziwy.
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ładowe 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.
Mimo, że takie wyjątki też można obsługiwać, zdecydowanie lepiej jest zadbać żeby się nie pojawiały. Zgłaszając wyjątki jednej z istniejących podklas RuntimeException pamiętaj, że wskazują błędy w programie, a nie nietypowe sytuacje. 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
Exception rozszerza się tak jak każdą inną klasę. W większości przypadków nowe wyjątki nie zawierają jednak definicji żadnych atrybutów ani metod. Już sama nazwa klasy wystarcza, aby się zorientować, co jest nie tak i jakie działania naprawcze podjąć.
class ZaMałoŚrodkówNaKoncie extends Exception {} public class NowyWyjątek1 { public static void main(String[] args) { try { throw new ZaMałoŚrodkówNaKoncie(); } catch (ZaMałoŚrodkówNaKoncie w) { System.out.println("Wiwat twórca wyjątków!"); } } }
Jeżeli jednak wyjątek przenosi już jakieś dane, to najczęściej są to napisy. Gdy na przykład nie daje się odnaleźć lub otworzyć wskazanego pliku, to warto do wyjątku dołączyć jego nazwę. Kod obsługi wyjątku może ją wyświetlić użytkownikowi, który być może pomylił się podczas wprowadzania. Dla wygody większość wyjątków ze standardowych bibliotek Javy (w tym Exception) posiada 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 w swoich wyjątkach móc korzystać z tego mechanizmu, trzeba dodać konstruktor przyjmujący napis i przekazać parametr do konstruktora nadklasy.
class PlikNieIstnieje extends Exception { PlikNieIstnieje(String s) { super(s); } //ponieważ definiujemy jakiś inny konstruktor, //kompilator sam nie doda bezparametrowego PlikNieIstnieje() {} } public class NowyWyjątek2 { public static void main(String[] args) { try { //napis przyda się podczas obsługi wyjątku throw new PlikNieIstnieje("alamakota.txt"); } catch ( PlikNieIstnieje w) { System.out.println(w.getMessage()); } } }
Zgłaszanie wyjątków w bloku catch
W reakcji na obsługiwany wyjątek, w bloku catch można zgłosić nowy wyjątek. Wyjątki zgłoszone w bloku 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, gdyż w tym kontekście nie wiadomo jak go obsłużyć.
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. Odpowiada za to konstruktor 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. Służy do tego metoda 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 i 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. Java idzie pod tym względem bardzo daleko. 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. Oprócz tego kompilator nie pozwoli umieścić nieosiągalnego bloku catch. Blok catch jest nieosiągalna, jeżeli wszystkie potencjalne wyjątki do niego pasujące są obsługiwane przez wcześniejsze bloki
public class NieosiągalnyBlokCatch { public static void main(String[] args) { try { System.out.println("Kompilator na ratunek gapom."); } catch (Exception e) { System.out.println("Ten catch nic nie przepuszcza dla pozostałych!"); } //catch (NullPointerException e) { //System.out.println("Ten kod jest tu niepotrzebny!"); //} } }
Kompilator zaprotestuje również wtedy, gdy blok catch jest po prostu niepotrzebny. Jest tak, jeżeli nigdzie w bloku try nie jest zgłaszany wyjątek do niego 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ągalnego lub niepotrzebnego bloku catch pozwala sądzić, ż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ł!");