#Niezbędnik Juniora: Wyjątki i ich obsługa

By 21 May 2016 May 22nd, 2016 Niezbędnik Juniora

W każdej aplikacji zdarza­ją się sytu­acje wyjątkowe, które nie mieszczą się w ‘stan­dar­d­owym’ dzi­ała­niu aplikacji. Na takie okaz­je pozosta­je nam obsłu­ga za pomocą wyjątków, dzię­ki którym może­my przy­wró­cić aplikację do ‘nor­mal­nej’ postaci.

Czym są wyjątki w Javie

Wyjąt­ki w Javie to spec­jalne obiek­ty, które poza stan­dar­d­owy­mi oper­ac­ja­mi na obiek­tach może­my także rzu­cać za pomocą słowa kluc­zowego throws, co powodu­je naty­ch­mi­as­towe prz­er­wanie dzi­ała­nia wątku (w najprost­szym przy­pad­ku — aplikacji) oraz prze­jś­cie do pier­wszego napotkanego miejs­ca, które ten wyjątek jest w stanie obsłużyć. Nieob­służony wyjątek uśmier­ca bieżą­cy wątek.

Aby wyjątek mógł być wyjątkiem, musi dziedz­iczyć po klasie Excep­tion (nie jest to do koń­ca praw­da, dokład­niejszy opis poniżej w sekcji o hier­ar­chii wyjątków). Więcej przykładów jak ich uży­wać, jakie są dostęp­ne w samej Javie oraz jak je obsługi­wać zna­jdziesz poniżej.

Hierarchia wyjątków

wyjatki w javie.001W języku Pol­skim to, co reprezen­tu­je powyższa grafi­ka nazy­wamy hier­ar­chią wyjątków, choć wyjąt­ki to tylko jej część. Niem­niej taka nazwa się przyjęła i funkcjonu­je, więc jeśli kiedykol­wiek zosta­niesz zapy­tana na roz­mowie o hier­ar­chię wyjątków — to właśnie powyższy dia­gram powinien pojaw­ić Ci się w głowie :)

Przed­staw­ia on pod­sta­wowe klasy, które dziedz­iczą po klasie Throw­able. Klasa ta sama w sobie nie ma dużego zas­tosowa­nia, ale jest ona znacznikiem — wszys­tkie klasy, które po niej dziedz­iczą moż­na ‘rzu­cać’ uży­wa­jąc kon­strukcji throw obiekt;

Bezpośred­nio po niej dziedz­iczą dwie klasy — Excep­tion oraz Error. Różni­ca pomiędzy nimi jest log­icz­na — klasy dziedz­iczące po Error z założe­nia oznacza­ją błąd, po którym aplikac­ja może nie dzi­ałać sta­bil­nie lub nie dzi­ałać wcale, są one rzu­cane przez maszynę wirtu­al­ną i aplikac­ja nie powin­na samodziel­nie ich rzu­cać. Wys­tępu­ją np. kiedy aplikacji zabraknie pamię­ci lub dojdzie do przepełnienia sto­su — w teorii w tej sytu­acji aplikac­ja powin­na zostać zamknię­ta, a błąd ten służy jedynie wyświ­etle­niu komu­nikatu. W prak­tyce w niek­tórych sytu­ac­jach moż­na obsługi­wać obiek­ty dziedz­iczące po Error (kil­ka przykładów zna­jdziesz np. na stack­over­flow), choć jest to prak­ty­ka ogól­nie nieza­le­cana. Klasy dziedz­iczące po Excep­tion syg­nal­izu­ją prob­le­my, z który­mi aplikac­ja może i powin­na sobie poradz­ić, przykład­owo prob­lem z odczytem pliku, brak połączenia z ser­w­erem za pośred­nictwem sieci itp. Jeśli Two­ja aplikac­ja generu­je określony rodzaj prob­le­mu w wyjątkowych sytu­ac­jach, możesz samodziel­nie zaim­ple­men­tować obiekt, który dziedz­iczy po Excep­tion (lub jed­nej z jej klas-dzieci) i prze­chowu­je ważne dla Ciebie infor­ma­c­je związane z prob­le­mem. Taki wyjątek możesz potem obsługi­wać tak jak każdy inny.

Checked vs unchecked exceptions

W hier­ar­chii wyjątków mamy wyróżnioną jeszcze jed­ną klasę — Run­time­Ex­cep­tion. Różni­ca pomiędzy Excep­tion a Run­time­Ex­cep­tion pole­ga na tym, że te pier­wsze musimy obsłużyć — tzn. jeśli meto­da może rzu­cić wyjątek tego typu, musi to zadeklarować (poprzez dodanie throws XYZ do syg­natu­ry), a meto­da ją wywołu­ją­ca musi taki wyjątek obsłużyć lub ‘świadomie’ przekazać dalej (wtedy kole­j­na meto­da musi obsłużyć lub przekazać wyjątek dalej itd). Wyni­ka to z fak­tu, że wyjąt­ki te są nat­u­ral­ną i ważną częś­cią danego mech­a­niz­mu (np. wyjątek wejścia/wyjścia — IOEx­cep­tion, który może zostać rzu­cony przy obsłudze plików). Wyjątków dziedz­iczą­cych po Run­time­Ex­cep­tion z kolei nie trze­ba deklarować i obsługi­wać. Ta gru­pa wyjątków to wyjąt­ki pow­sta­jące głównie ‘z niedopa­trzenia’ — np. Null­Point­erEx­cep­tion oznacza że próbu­je­my wywołać metodę na zmi­en­nej, która jest null’em; w gotowej aplikacji powin­niśmy mieć wery­fikację w odpowied­nich miejs­cach, a wyma­ganie ich obsłuże­nia (mogą pow­stać właś­ci­wie w każdej linii kodu) było­by bard­zo niewygodne i zaburza­jące czytel­ność kodu. Oczy­wiś­cie wyjąt­ki takie nadal może­my łapać i przetwarzać tak jak wszys­tkie inne, jeśli aplikac­ja tego wymaga.

Najpopularniejsze wyjątki i ich zastosowanie

Poniżej zna­jdziesz kil­ka najpop­u­larniejszych wyjątków oraz bard­zo krót­ki opis kiedy są używane.

Null­Point­erEx­cep­tion — rzu­cany kiedy próbu­jesz wywołać metodę na zmi­en­nej, której wartość to null
Ille­galArgu­mentEx­cep­tion — rzu­cany, kiedy przekazy­wany argu­ment jest z jakiegoś powodu niepraw­idłowy (wal­i­dac­ja wewnątrz metod)
IOEx­cep­tion (wyjąt­ki po nim dziedz­iczące) — rzu­cany w przy­pad­ku prob­lemów z sys­te­mem wejścia/wyjścia, czyli najogól­niej rzecz ujmu­jąc, kiedy wys­tąpi prob­lem przy pra­cy z plika­mi lub z trans­misją danych za pośred­nictwem internetu
Num­ber­For­ma­tEx­cep­tion — rzu­cany, kiedy próbu­je­my zamienić na liczbę np. obiekt typu String, który zaw­iera nie tylko cyfry
Index­Out­Of­Bound­Ex­cep­tion — rzu­cany, kiedy próbu­je­my się odwołać do nieist­niejącego ele­men­tu tabl­i­cy lub listy

To tylko kil­ka z najczęś­ciej spo­tykanych wyjątków. Bardziej wycz­er­pu­jące opra­cow­anie zna­jdziesz np. na stron­ie rymden.nu .

Korzystanie z wyjątków

Słowem wstępu — wyjąt­ki są kosz­towne dla kom­put­era! W prak­tyce zatrzy­mu­ją cały bieg pro­gra­mu, po czym ‘cofa­ją’ się na stosie szuka­jąc kodu pozwala­jącego je obsłużyć — to nie jest stan­dar­d­owa pro­ce­du­ra, przez co jest bard­zo nieefek­ty­w­na. Także dlat­ego powin­ny być uży­wane tylko do obsłu­gi sytu­acji naprawdę wyjątkowych i prob­lematy­cznych, wyma­ga­ją­cych spec­jal­nego podejścia.

Rzucanie wyjątków

Jeśli w Two­jej aplikacji potrze­bu­jesz zasyg­nal­i­zować prob­lem, warto wiedzieć jak należy rzu­cać wyjąt­ki. Jest to bard­zo proste i sprowadza się do trzech kroków:

1. Określ prawidłowy typ wyjątku

Wyjątek już jako klasa powinien opisy­wać rodzaj prob­le­mu. Być może będziesz chciała/potrzebowała stworzyć włas­ny wyjątek (o tym przeczy­tasz poniżej), ale możesz wybrać spośród tych już ist­nieją­cych. Najpraw­dopodob­niej będzie to Ille­galArgu­mentEx­cep­tion lub podob­ny wyjątek w przy­pad­ku wal­i­dacji, ale może też być któryś z dziedz­iczą­cych po IOEx­cep­tion. Ważne, aby nie rzu­cać ‘ogól­nych; wyjątków jak np. po pros­tu Excep­tion czy Run­time­Ex­cep­tion — może to znaczą­co utrud­nić ich sen­sowną obsługę w innych częś­ci­ach systemu.

2. Dodaj deklaracje throws (tylko, jeśli wybrany wyjątek nie dziedziczy po RuntimeException)

Jeśli wybrany przez Ciebie wyjątek nie dziedz­iczy po Run­time­Ex­cep­tion (w prze­ci­wnym razie możesz pom­inąć cały ten krok), to musisz go zadeklarować w syg­naturze metody. Jest to sposób żeby powiedzieć Javie (ale także innym pro­gramis­tom korzys­ta­ją­cym z Two­jego kodu), że w tej metodzie może pow­stać (lub zostać przekazany dalej) wyjątek danego typu. Robimy to dopisu­jąc do syg­natu­ry metody (czyli przed klam­rą otwier­a­jącą) słówko kluc­zowe throws oraz wymieni­amy możli­we wyjąt­ki po przecinku. Przykład­owo rzu­ca­jąc wyjątek typu IOEx­cep­tion meto­da, która wyglądała:

public void doSomething() {
    //...
}

będzie ter­az wyglą­dać następująco:

public void doSomething() throws IOException {
    //...
}

Ten krok nie jest konieczny dla wyjątków dziedz­iczą­cych po Run­time­Ex­cep­tion, ale moż­na aby pod­kreślić że konkret­ny wyjątek jest uży­wany do syg­nal­i­zowa­nia określonych prob­lemów (i uwzględ­nić go w JavaDocs tej metody).

Uwa­ga! Deklarac­ja throws musi być obec­na na poziomie inter­fe­j­su — to znaczy, że jeśli dana meto­da ‘pochodzi’ z inter­fe­j­su, to w tym inter­fe­jsie także musisz dodać stosowną deklarację. Podob­nie rzecz się ma z dziedz­icze­niem — jeśli meto­da ta przesła­nia jakąś metodę rodz­i­ca (lub imple­men­tu­je abstrak­cyjną metodę rodz­i­ca), to syg­natu­ra metody w rodz­icu musi uwzględ­ni­ać wyjątek, który chcesz rzu­cić! Inaczej mogło­by dojść do sytu­acji, w której wyjątek taki jest nieob­służony poprawnie.

3. Rzuć obiekt wyjątku

W kodzie metody najpierw stwórz obiekt wyjątku tak, jak­by był to nor­mal­ny obiekt, a następ­nie użyj słowa kluc­zowego throw, aby taki wyjątek rzucić:

IOException e = new IOException("Komunikacja intergalaktyczna z kocim centrum dowodzenia nie zadziałała");
throw e;

Tworząc nowy obiekt wyjątku zawsze musisz podać treść komu­nikatu o prob­lemie — wyni­ka to z tego, że pole to jest w samej klasie Excep­tion, przez co konieczne jest jego ustaw­ie­nie. Dru­gi, opcjon­al­ny, argu­ment jest typu Throw­able i pozwala przekazać wyjątek, który był ‘powo­dem’ rzu­canego wyjątku (jeśli taki wyjątek wyp­iszesz na kon­solę, to do infor­ma­cji o Twoim wyjątku będzie dodana linia ‘Caused by:’, po której opisany zostanie ten dru­gi wyjątek — ten przekazany jako argu­ment). Oczy­wiś­cie lista argu­men­tów poszczegól­nych wyjątków może się różnić, ale te dwie infor­ma­c­je są wspólne i wys­tępu­ją we wszys­t­kich stan­dar­d­owych wyjątkach.

Tworzenie własnych wyjątków

Aby stworzyć włas­ny wyjątek, wystar­czy dziedz­iczyć po klasie Excep­tion lub Run­time­Ex­cep­tion (lub jed­nej z klas po nich dziedz­iczą­cych). Ponieważ klasy te nie mają kon­struk­torów bezar­gu­men­towych, będziesz musi­ała także wywołać ich kon­struk­to­ry. Poniżej przykład­owa imple­men­tac­ja wyjątku o przekrocze­niu lim­i­tu kon­ta (bard­zo minimalistyczna):

public class LimitExceededException extends IOException {
    private final String limitName;
    private final Long limitValue;

    public LimitExceededException(String message, String limitName, Long limitValue) {
        super(message);
        this.limitName = limitName;
        this.limitValue = limitValue;
    }
    //...gettery, hashCode, equals itp...
}

Tworząc własne wyjąt­ki zas­tanów się, czy nie ist­nieje już jak­iś bardziej ‘ogól­ny’, który pasu­je do prob­le­mu, jaki chcesz zasyg­nal­i­zować. Częs­to wyjąt­ki dostęp­ne w języku Java są wystar­cza­jące i nawet ogromne sys­te­my deklaru­ją maksy­mal­nie po kil­ka specy­ficznych wyjątków. Tworząc nowy wyjątek pamię­taj o kilku zasadach:

  1. Wyjątek musi być względ­nie ogól­ny, czyli nada­ją­cy się do uży­cia w różnych częś­ci­ach sys­te­mu. Przykład dobrego ‘rozmi­aru’ wyjątku to AccountLim­i­tEx­ceed­edEx­cep­tion — wyjątek rzu­cany kiedy jak­iś z lim­itów kon­ta został przekroc­zony. Przykład źle dobranego rozmi­aru to AccountLim­itOfNum­berOfRepos­i­to­rie­sEx­ceed­ed — nie ma możli­woś­ci zas­tosowa­nia go ponownie
  2. Poza wiado­moś­cią wyjątek może ‘nieść’ ze sobą także dodatkowe infor­ma­c­je wspier­a­jące; staraj się jed­nak, aby były to proste ele­men­ty jak cią­gi znaków czy licz­by i raczej unikaj złożonych obiek­tów (np. obiekt typu ‘Użytkown­ik’) — może to stanow­ić zagroże­nie bez­pieczeńst­wa aplikacji w przyszłości
  3. Sprawdź raz jeszcze, czy nie ma już takiego wyjątku w jakiejś stan­dar­d­owej bib­liotece — Spring czy Hiber­nate ofer­u­ją dodatkowe wyjąt­ki, które mogą być tym, czego potrzebujesz
  4. Pomyśl o imple­men­tacji dodatkowych metod jak np. get­Lo­cal­izedMes­sage() oraz wspar­ciu dla różnych kon­struk­torów — poz­woli to uniknąć prob­lemów z wyko­rzys­taniem wyjątku w innym miejs­cu aplikacji

Wyjąt­ki twor­zone przez Ciebie dzi­ała­ją dokład­nie tak samo, jak wyjąt­ki stan­dar­d­owe — zasady, który­mi się rządzą, a także sposób ich uży­cia i obsłu­gi są identyczne :)

Obsługa wyjątków

Pod­stawą obsłu­gi wyjątków w Javie jest kon­strukc­ja try-catch-final­ly. Przykład­owo może ona wyglą­dać następująco:

try {
    //... jakiś kod, który może rzucić wyjątek
} catch (NullPointerException e) {
    //... kod, który zostanie wykonany, jeśli wystąpi wyjątek
} finally {
    //kod, który zostanie wykonany zawsze (nawet jeśli wewnątrz 'try' nastąpi zwrócenie jakiejś wartości z metody)
}

Pier­wsza sekc­ja — try — jest obow­iązkowa. W niej należy umieś­cić kod, który chce­my ‘zabez­pieczyć’ na wypadek wys­tąpi­enia wyjątku. Może to być jed­na lin­ij­ka lub wiele, ale warto starać się ograniczyć ilość linii wewnątrz tylko do tych najbardziej potrzeb­nych, inaczej kod stanie się mniej czytel­ny i nie będzie jasne, która jego część jest ‘niebez­piecz­na’.

Kole­jne sekc­je — catch oraz final­ly mogą wys­tąpić razem, ale wyma­gana jest tylko jed­na z nich (dowol­na). Może­my mieć więc tylko sekc­je final­ly, tylko sekc­je catch, a także sekcję catch i final­ly razem (wtedy wyma­gane jest, żeby sekc­ja catch była przed sekcją final­ly). Co więcej — może­my mieć kil­ka sekcji catch, aby inaczej obsługi­wać różne wyjąt­ki (ale o tym za chwilę).

Pod­czas dzi­ała­nia aplikacji kod z sekcji try jest wykony­wany nor­mal­nie, do cza­su wys­tąpi­enia wyjątku. Jeśli wyjątek nie zostanie rzu­cony pod­czas jej wykony­wa­nia, sekc­ja catch jest ‘pomi­jana’. Jeśli jed­nak pojawi się wyjątek obsługi­wany przez jed­ną z sekcji catch, aktu­al­nie wykony­wany kod z sekcji ‘try’ zostanie prz­er­wany i rozpocznie się wykony­wanie kodu w odpowied­niej sekcji ‘catch’. Po jej zakończe­niu maszy­na wirtu­al­na prze­jdzie od razu do sekcji ‘final­ly’ (jeśli ist­nieje), a następ­nie będzie wykony­wała kod, który zna­j­du­je się za całym blok­iem try-catch.

Bard­zo istot­na jest także sekc­ja final­ly — jeśli jest obec­na, wykona się ona zawsze, nieza­leżnie od tego czy w sekcji try pojawi się wyjątek czy nie. Co więcej, wykona się ona nawet wtedy, kiedy w sekcji try lub catch zwrócimy wartość z metody! Jedyną sytu­acją, kiedy ta część kon­strukcji się nie wykona jest zamknię­cie maszyny wirtu­al­nej (np. poprzez wywołanie System.exit(0) ).

Wiele sekcji catch w jednym bloku (obsługa różnych wyjątków w różny sposób)

Jak wspom­i­nal­iśmy powyżej, sekcji catch może być więcej — mogą one obsługi­wać różne wyjąt­ki na różne sposo­by. Przykład­owo konstrukcja:

try {
    //... kod, który może generować wyjątki ...
} catch (IllegalArgumentException e) {
    System.out.println("Pierwsza sekcja catch");
} catch (IOException | IllegalArgumentException e) {
    System.out.println("Druga sekcja catch dla 2 wyjątków");
} catch (RuntimeException e) {
    System.out.println("Trzecia sekcja catch");
}

w zależnoś­ci od rodza­ju wyjątku wykona inny kod. Zwróć uwagę także na drugą sekcję catch — ‘łapie’ ona kil­ka różnych wyjątków jed­nocześnie, dzię­ki czemu może­my uniknąć dup­likacji kodu. Ważne jest, że w przy­pad­ku wys­tąpi­enia wyjątku sekc­je te są sprawdzane od góry do dołu, w kole­jnoś­ci w jakiej są umieszc­zone w kodzie, i wyko­nana zostanie wyłącznie pier­wsza z nich, która odpowia­da dane­mu wyjątkowi! Nie moż­na zatem jako pier­wszej umieś­cić sekcji obsługu­jącej wyjątek bardziej ‘ogól­ny’ (np. Run­time­Ex­cep­tion) niż późniejszy wyjątek (np. NullPointerException).

Antywzorce — czego z wyjątkami NIE robić

Wyjąt­ki to mech­a­nizm do sytu­acji wyjątkowych, przez co bard­zo łat­wo go nadużyć — poniżej zna­jdziesz kil­ka rzeczy, których należy unikać, aby aplikac­ja była wyda­j­na i możli­wa do roz­wo­ju w przyszłości:

  1. Logi­ka biz­ne­sowa poprzez wyjąt­ki — wyjąt­ki nie służą do ‘zwraca­nia’ wartoś­ci z metody i przekazy­wa­nia infor­ma­cji pomiędzy warst­wa­mi! Ich zas­tosowanie to sytu­acje wyjątkowe i prob­le­my, a nie oczeki­wane (i częs­to wys­tępu­jące) zagad­nienia jak np. wal­i­dac­ja wejś­cia od użytkown­i­ka (czym innym jest wal­i­dac­ja danych w jakiejś wewnętrznej metodzie — aplikac­ja może zakładać, że w tym miejs­cu dane są już zwery­fikowane na wcześniejszym etapie, a więc wyjątek jest uza­sad­niony) czy pus­ta odpowiedź (np. w odpowiedzi na RESTowe zapy­tanie o nieist­nieją­cy obiekt)
  2. Zbyt specy­ficzne wyjąt­ki — uży­wanie wyjątków, które mają zas­tosowanie tylko w jed­nym miejs­cu prowadzi do sytu­acji, w której aplikac­ja ma więcej wyjątków niż jakichkol­wiek innych klas
  3. Zbyt ogólne wyjąt­ki — rzu­canie po pros­tu ‘Excep­tion’ czy ‘Run­time­Ex­cep­tion’ utrud­nia praw­idłową reakcję (wewnątrz aplikacji) na pow­stały prob­lem i może uniemożli­wić automaty­czne zaradze­nie mu.
  4. Ignorowanie wyjątków — czego prze­jawem jest np. pusty blok catch lub sam log — cza­sem fak­ty­cznie nie moż­na zro­bić nic więcej niż zal­o­gować, ale w więk­szoś­ci sytu­acji aplikac­ja powin­na pod­jąć jakieś dzi­ałanie, kiedy napot­ka na wyjątek
  5. Prze­puszczanie wyjątków ‘dalej’ — np. kiedy każ­da meto­da od kon­trol­era przez ser­wis po DAO rzu­ca ten sam wyjątek, w efek­cie nie obsługu­jąc go nigdzie, powodu­je to, że wyjąt­ki tracą swo­ją rolę (umożli­wienia aplikacji powrót do sta­bil­nego dzi­ała­nia po napotka­niu problemu)

Więk­szość z tych zasad jest log­icz­na i jas­na, ale pod presją cza­su cza­sem mamy ochotę iść ‘na skró­ty’ — pamię­taj, że zemś­ci się to na Tobie kilkukrot­nie już chwilę później!

Podsumowanie

Wyjąt­ki i praw­idłowe obchodze­nie się z nimi to jeden z ważniejszych aspek­tów języ­ka. Warto się nad nim pochylić nieco bardziej nie tylko z uwa­gi na przy­go­towanie do rekru­tacji, ale także aby kole­j­na aplikac­ja, którą będziesz tworzyć, była jeszcze lep­sza i sprawniejsza!

Więcej o wyjątkach w Javie możesz przeczy­tać w tuto­ri­alu na stron­ie Oracle.com poświę­conym temu zagad­nie­niu.