Multi catch – czyli łapanie wielu wyjątków jednocześnie

Wiesz już czym są wyjątki w Java, jak je rzucać oraz łapać, znasz składnie try-with-resources. Z tą wiedzą można stworzyć już sporą aplikację w Java. Jednak czasem kod tej aplikacji może wyglądać następująco:

Mimo, że pokazany wyżej kod działa poprawnie oraz spełnia założenia aplikacji (przynajmniej tak teraz zakładamy teraz). To nie jest to kod najlepszej jakości. Przyjrzyjmy się jak można go przepisać oraz przy okazji wykorzystać tzw. multi catch z Java 7.

Zacznijmy najpierw od tego jak takie problemy rozwiązywało się przed Java 7. Jakie problemy? możesz zapytać. Chodzi o niepotrzebną duplikację kodu, wszystkie bloki catch robią to samo (wypisują identyczny komunikat). Lepszym rozwiązaniem jest stworzenie prywatnej metody którą będziemy wołać z każdego bloku catch. W takim przypadku jeżeli przyjdzie nam zmienić komunikat, będziemy musieli zrobić to tylko w jednym miejscu, a nie jak to jest teraz w pięciu. Oto powyższy kod po modyfikacjach:

Innym możliwym rozwiązaniem jest po prostu złapanie wszystkich wyjątków typu Exception

tj.:

Chociaż wygląda to jak najlepsze rozwiązanie z punktu widzenia ilości kodu, to wcale takie nie jest. Głównym powodem jest to, że możemy przez nieuwagę złapać wyjątek który powinien zostać obsłużony w inny sposób. Powód ten bazuje na założeniu, że aplikacja jest ciągle rozwijana. Przez to właśnie mogą dojść nowe wyjątki wymagające innej obsługi. W takiej sytuacji poprzednie rozwiązanie (z kilkoma blokami catch) spowoduje błąd kompilacji (wynikający z nie obsłużenia zdeklarowanego wyjątku), a tym samy wymusi zastanowienie się nad tym co z tym wyjątkiem w tym miejscu należy zrobić.

W Java 7 oprócz try-with-resources pojawił się również multi catch. Pozwala on złapać kilka różnych, niezwiązanych ze sobą wyjątków w jednym bloku catch. W tym celu należy wymienić typy łapanych wyjątków po znaku “|” (ang. pipe). Z wykorzystaniem tej konstrukcji przykładowy kod będzie wyglądał następująco:

Nie musimy tworzyć prywatnej metody, nie trzeba też łapać wyjątków klasy Exception, wystarczy tylko wymienić te które chcemy obsłużyć w tym pojedynczym bloku catch. Dzięki temu rozwiązaniu również w sytuacji kiedy dojdzie nowy wyjątek w bloku try, kompilator poinformuje nas o konieczności jego obsłużenia.

Obsługa wyjątków w Java

Skoro już wiesz czym są wyjątki w Java, najwyższy czas nauczyć się je obsługiwać (przez pojęcie “obsługi wyjątku” rozumiemy w Java reakcję na zaistnienie sytuacji wyjątkowej/nie spodziewanej).

Java wymusza na nas programistach obsługę wyjątków! Jeżeli metoda deklaruje, że rzuca jakiś wyjątek (używając słowa kluczowego: throws) musimy ten wyjątek obsłużyć. Jeżeli tego nie zrobimy nasz kod się nie skompiluje.

Do obsługi wyjątków w Java używana jest konstrukcja trycatch. Składa się ona z bloku try (“spróbuj”) oraz bloków catch (“złap”) dla dowolnej ilości wyjątków które chcemy obsłużyć. Blok trycatch może wyglądać następująco:

Omówmy najpierw pierwszy blok kodu tj. try { … }. W tym miejscu znajduje się nasz “główny” kod aplikacji który może rzucić jeden lub więcej wyjątków. Dla przykładu załóżmy, że chcemy zapisać fragment tekstu do pliku. W tym celu musimy utworzyć obiekt pliku, następnie utworzyć obiekt typu java.io.PrintWriter, który umożliwi nam zapis do pliku. Później zapisać tekst do pliku oraz na końcu zwolnić zasoby zamykając obiekt PrintWriter. Przykładowy fragment kodu znajduje się poniżej:

Jeżeli spróbujemy skompilować powyższy kod kompilator poinformuje nas o tym, że użyty konstruktor obiektu PrintWriter rzuca wyjątek typu java.io.FileNotFoundException wymiuszając w ten sposób na nas użycie bloku try { … } catch w tym miejscu.

W sekcji try { … }, musimy umieścić wszystkie odwołania do zmiennej writer gdyż blok try { … } wyznacza “blok kodu” (więcej informacji o blokach kodu). Dlatego finalna wersja naszej mikro aplikacji będzie wyglądała następująco:

W sekcji catch (linie 7 do 9) łapiemy wyjątek FileNotFoundException i w przypadku jego wystąpienia wypiszemy na konsoli komunikat: Wskazany plik nie istnieje.

Program napisany w ten sposób, zawsze będzie wypisywał komunikat z sekcji catch { … }, gdyż w nazwie pliku podajemy niepoprawną ścieżkę. Usunięcie znaku “/” w pierzej linii listingu automatycznie spowoduje, że aplikacja zacznie działać poprawnie tj. utworzy wskazany plik i zapisze w nim “Hello world”.

Warto również zauważyć, że program ten może zostać napisany znacznie lepiej korzystając z instrukcji try-with-resources dostępniej od Java7. W takim przypadku nie musimy sami “ręcznie” zamykać obiektu PrintWriter, zrobi to za nas JVM. Poniżej znajduje się optymalny kod aplikacji:

Try with resources – prawidłowa obsługa zasobów

Zanim przejdziemy do głównego tematu tego wpisu, warto zapoznać się trochę z historią, gdyż try-with-resources nie zawsze był dostępny w Java.

Jeżeli kiedyś zdarzy się Tobie przeglądać kod starszych projektów (takich sprzed 2012 roku) bardzo możliwe że trafisz na taki oto kod:

Powyższy fragment pseudo-kodu odpowiedzialny jest z otwarcie połączenia, zapisanie “czegoś” do niego oraz za zamknięcie tegoż połączenia.

Zacznijmy od rzeczy podstawowej, wszystkie otwarte połączenia oraz pliki należy zamykać! Dlaczego? W uproszczeniu mówiąc system operacyjny musi śledzić wszystkie otwarte zasoby, żeby to robić rezerwuje sobie pewien obszar w pamięci komputera. Jak wiemy pamięć może się skończyć. Podobnie do wycieków pamięci, w programach mogą wystąpić “wycieki zasobów”. W systemach typu Linux objawem “wycieku pamięci” jest komunikat: too many open files. Występuje on wtedy kiedy system nie może już otworzyć kolejnego pliku bo wszystkie “sloty” na otwarte pliki są już zajęte. W systemach Linux można zwiększyć limit otartych plików, ale zanim to zrobimy warto się zastanowić czy przypadkiem błąd nie leży w naszym kodzie. Jeżeli jest on “po naszej stronie” to prawdopodobnie pojawi się on ponowie za jakiś czas.

W Java zazwyczaj do zamykania (czasem również mówi się “zwalniania”) zasobów służy metoda close(). Prawdopodobnie w 99% przypadków metoda ta rzuca wyjątek typu IOException. Wymusza to na nas obsługę tego wyjątku.

Żeby mieć pewność, że metoda close() zostanie zawsze wykonana trzeba umieścić ją w bloku finally. Wiedząc już, że może ona nam rzucić wyjątkiem musimy jej wywołanie “włożyć” do bloku trycatch. W ten sposób otrzymujemy nie za ładną konstrukcję finallytrycatch.

Dodatkowo nie możemy za wiele zrobić w momencie wystąpienia wyjątku w czasie wykonania metody close(). Dlatego też zazwyczaj się taki wyjątek ignoruje. W najlepszym wypadku jedyne co możemy zrobić to zalogować jego wystąpienie.

Ale to nie wszystko… żeby zamknąć połączenie oczywiście musimy mieć dostęp do obiektu tego połączenia. Niestety jeżeli taki obiekt będzie zdeklarowany wewnątrz pierwszego bloku try to nie będziemy mieli do niego dostępu w bloku finally. Dlatego właśnie powyższy przykład zaczyna się od deklaracji zmiennej connection przed pierwszym blokiem try. W konsekwencji musimy mieć również warunek connection != null w bloku fianally

.

Przed Java 7 żeby poprawnie zamknąć dany zasób potrzebowaliśmy waśnie takiej ogromnej ilości kodu… deklarację obiektu przed blokiem try, blok finally, z instrukcją if oraz kolejnym blokiem try… tylko po to żeby zawołać jedną małą metodę close().

Na szczęście już jest lepiej! Teraz powyższy kod wygląda następująco:

Dużo mniej kodu! Odpadł nam cały blok finally! Wszystko dzięki konstrukcji try-with-resources dodanej w Java 7.

Więc o co tyle krzyku? Otóż instrukcja try może otrzymać opcjonalny blok z argumentami. W bloku tym możemy utworzyć obiekty które implementują interfejs AutoClosable. Takich argumentów (obiektów) może być kilka oddzielonych średnikiem jak na poniższym przykładzie:

Przy zastosowaniu try-with-resources to JVM odpowiedzialny jest za poprawne zamknięcie za nas pliku lub połączenia. Nie musimy się już gimnastykować z dodatkowym blokiem finally tylko po to żeby zamknąć zasób. Mniej kodu do napisania, mniej kodu do martwienia się, mniej kodu w którym mogą wystąpić błędy… same zalety 😉

Wyjątki w Java – wstęp

W programowaniu, jak w życiu codziennym zdarzają się sytuacje wyjątkowe, ot chociażby takie jak brak wody pod prysznicem kiedy to właśnie zacząłeś myć twarz… lub brak miejsca na dysku gdy program właśnie chciał na nim zapisać wynik kilkugodzinnych obliczeń.

Z jednej strony jako programiści jesteśmy niepoprawnymi optymistami, zakładamy że nigdy się nam nie skończy miejsce na dysku, że nie zabraknie nam pamięci RAM oraz, że zawsze będziemy mieli dostęp do internetu.

Z drugiej jednak strony sytuacje wyjątkowe zdarzają się… i to zdarzają się zawsze! Więc musimy być na nie przygotowani.

Jeżeli przed każdym zapisem danych na dysk mielibyśmy sprawdzać czy jest na nim odpowiednia ilość wolnego miejsca lub przed każdą alokacją zmiennej mielibyśmy sprawdzać czy jest na nią wystarczająca ilość pamięci. Kod stał by się bardzo nie czytelny, więcej było w nim “zapytań” o aktualny stan “świata zewnętrznego” niż właściwego kodu. Co więcej zamiast rozwiązywać nasze zadanie, zajmowalibyśmy się ciągłym sprawdzaniem czy możemy daną operację wykonać. Dodatkowo, ułamek sekundy po sprawdzeniu danego stanu (czy to wolnego miejsca na dysku czy wolnej pamięci) może okazać się, że nie jest on już aktualny gdyż inny program zajął “nasze” miejsce w pamięci bądź na dysku.

W Java sytuacje które zazwyczaj się nie zdarzają, bądź ich możliwość wystąpienia jest niewielka, są reprezentowane za pomocą wyjątków (ang. exception). Przykładami takich sytuacji są:

  • brak wolnej pamięci.
  • brak wolnego miejsca na dysku,
  • zły format danych,
  • brak referencji do obiektu,
  • a nawet brak połączenia z internetem.

Zacznijmy od teorii związanej z wyjątkami w Java.

“Operacje” na wyjątkach

W Java wyjątki się:

  • deklaruje możliwość wystąpienia (ang. throws) – konstruktory oraz metody w Java mogą “powiedzieć” (zdeklarować), że mogą rzucić dany typ wyjątku, osoba używająca takiej metody lub konstruktora jest zmuszona do obsługi tego wyjątku,
  • rzuca (ang. throw) – kiedy zachodzi sytuacja wyjątkowa, po prostu “rzucamy wyjątkiem w ciemno”, w ten sposób przenosimy odpowiedzialność za zachowanie się aplikacji w sytuacji wyjątkowej na osobę która będzie korzystała z naszego kodu,
  • łapie (ang. catch) – “żadna” sytuacja wyjątkowa w Java nie może zostać pominięta, jesteśmy wręcz zmuszeni to “łapania” zdeklarowanych wyjątków oraz ich obsłużenia.

Jeżeli metoda lub konstruktor deklaruje wyjątek jesteś jako programista zmuszony go obsłużyć, inaczej kompilator nie zaakceptuje twojego kodu (czyt. będziesz miał błędy kompilacji). Zanim przejdziemy do tego jak się deklaruje oraz obsługuje wyjątki w Java warto poznać ich strukturę oraz typy.

Struktura wyjątków w Java

Klasą bazową dla wszystkich rodzajów “sytuacji wyjątkowych” w Java jest klasa Throwable (“rzutliwy”, nie wiem jak to lepiej przetłumaczyć). Klasa ta nigdy nie powinna znaleść się w Twoim kodzie, nie powinieneś z niej dziedziczyć,  jej”rzucać” ani “łapać”. Z racji tego, że Java jest językiem obiektowym jest potrzebna jedna klasa z której różne rodzaje sytuacji wyjątkowych będą dziedziczyć.

Z klasy Throwable dziedziczą dwie inne klasy:

  • Error – “amba fatima”, wyjątki tego typu oznaczają nieodwracalny błąd, zazwyczaj związany z błędem maszyny wirtualnej (JVM) lub systemu operacyjnego. Dwoma najczęściej spotykanymi błędami są OutOfMemoryError oraz StackOverflowError,
  • Exception – błąd z którego (teoretycznie) da się wyjść. Zazwyczaj są to błędy programistyczne (NullPointerException), wejścia/wyjścia (IOException) oraz inne (w tym RuntimeExcpetion).

Tak samo jak w przypadku klasy Throwable, z klasy Error również nie dziedziczymy. Można za to łapać zarówno sam Error jak i klasy z niej dziedziczące w celu poprawnej obsługi zdarzeń krytycznych JVM. W tym wypadku będzie to “ratowanie się” w sytuacji kiedy to niebo wali nam się na głowę. Takie ratownie może polegać na zamknięciu otwartych połączeń sieciowych (np. baz danych) czy też zapisaniu (częściowych) wyników obliczeń itp.

To co nas jako programistów najbardziej interesuje to wszystkie klasy dziedziczące z Exception. Dlaczego? Gdyż głównie z takimi wyjątkami będziemy mieli doczynienia. Właśnie tego typu wyjątki są rzucane przez konstruktory i metody. Z klasy Exception (oraz klas pochodnych) możemy śmiało dziedziczyć żeby tworzyć swoje własne typy wyjątków.

Tyle kwestią w stępu teoretycznego do wyjątków w Java. W następnym wpise omówimy instrukcję throws, throw, try, catch i finally; czyli deklaracje, rzucenie wyjątkiem oraz jego złapanie.

Liczby (pseudo)losowe w Java

W trakcie nauki każdego języka programowania przychodzi taki etap, że trzeba w jakimś ćwiczeniowym programie wylosować liczbę. Dla nas ludzi zadanie “wymyślenia” jakieś losowej liczby jest banalnie proste, ot tak każdy może sobie strzelić: 1342.

Ponoć ktoś kiedyś próbował zaimplementować generowanie liczb losowych w taki sposób:

Generator liczb pseudolosowych

Źródło: http://goo.gl/VgBo0b

Oczywiście jest to błędna implementacja 😉

W świecie komputerów nie jest tak łatwo z liczbami losowymi. Komputer nie może sobie ot tak “wymyślić” czy “strzelić” jakąś liczbą. Jedyne co może on zrobić to taką liczbę wyliczyć… dokładnie tak, komputery nie “zgadują” liczb losowych tylko je wyliczają. Dlatego właśnie w informatyce mówimy o liczbach “pseudolosowych”.

Z liczbami pseudolosowymi jest trochę jak z powietrzem, nie widzimy go nie “czujemy”, a jednak jest ono wokół nas i nie możemy bez niego żyć. Nikt z nas nie “widzi” liczb pseudolosowych na codzień, co więcej nawet pisząc programy zazwyczaj się ich nie używa bezpośrednio. Jednak bez nich nie istniały by gry, symulatory czy też bezpieczny internet; gdyż to właśnie  w tych dziedzinach są one najczęściej wykorzystywane. Bez nich nie istniała by bankowość elektroniczna czy też zakupy przez internet; tak samo mogli byśmy zapomnieć o grach!

Przejdźmy teraz do części praktycznej. Jak wylosować liczbę w Java?

Do losowanie liczb służy klasa java.util.Random. Posiada ona dwa publiczne konstruktory:

  • bezparametrowy – do używania w “prawdziwym życiu” czyli tzw. produkcji,
  • jednoparametrowy przyjmujący argument typu long – do używania w testach.

Zastanawiasz się może po co jest ten drugi konstruktor “do używania w testach”. Otóż wartość jaką przekazujesz w parametrze jest tak zwanym “ziarnem” (ang. seed) służącym do inicjalizacji algorytmu stosowanego do generowania liczb pseudolosowych. Dwa obiekty klasy Random zainicjalizowane tą samą wartością parametru seed będą zwracały te same wartości jako liczby losowe… zawsze! Może zastanawiasz się po co komu taki generator liczb losowych który zawsze zwraca te same wartości… otóż jest to niezmiernie przydatne w testach jednostkowych. Porównujesz tam znaną wartość z oczekiwaną wartością na wyjściu Twojego algorytmu… jeżeli algorytm używa “prawdziwych” liczb losowych nie będziesz w stanie przewidzieć jego wyniku… chyba, że generator liczb losowych będzie zwracał zawsze te same wartości (czyli de facto nie będzie on taki losowy ;)). Właśnie po to jest ten jednoparametrowy konstruktor. Nigdy go nie używaj po za testami (chyba, że na prawdę… na prawdę wiesz co robisz!).

Dobrze, czyli już wiesz, że zawsze masz używać bezparametrowego konstruktora, czyli:

Co on właściwie robi? W dużym uproszczeniu mówiąc, wylicza pewną stałą, potem wykonuje na niej operację XOR z aktualnym czasem w nanosekundach, po czym wywołuje jednoparametrowy konstruktor ;). W ten sposób biblioteka standardowa Java zapewnia nam “losowe” ziarno dla generatora liczb pseudolosowych 😉

Skoro mamy już instancję naszego generatora liczb pseudolosowych i będzie on nam zwracał “unikalne” wartości (bo został zainicjalizowany “unikalnym” ziarnem) to warto było by go użyć.

Jak dostać losową liczbę z klasy java.util.Random?

Wszystko zależy od tego jaki rodzaj liczby chcesz dostać. Jeżeli chcesz uzyskać dodatnią liczbę całkowitą (int) z przedziału od 0 do 2.147.483.647 (czyli maksymalnej wartości jaką można przechowywać w zmiennej typu int) musisz wywołać metodę nextInt()

Jeżeli chcesz wylosować całkowitą liczbę dodatnią od 0 do pewnej wartości możesz wywołać metodę nextInt(int bound), gdzie parametr bound określa górną granicę:

Powyższy fragment kodu wylosuje nam liczbę z przedziału od 0 do 50.

Dobrze a co jeżeli chcemy wylosować liczbę całkowitą z przedziału od 10 do 60? Przyjrzyj się następującemu przykładowi:

Wystarczy w parametrze metody nextInt() podać wartość górnej granicy pomniejszoną o wartość dolnej granicy (60 – 10 = 50), a następnie dodać wartość dolnej granicy.

Co w przypadku innych typów podstawowych?

Każdy typ podstawowy (oprócz typu short) ma w klasie Random metodę która pozwala zwrócić dla niego wartosć losową:

  • boolean -> nextBoolean(),
  • int -> nextInt(),
  • long -> nextLong(),
  • float -> nextFloat(),
  • dobule -> nextDouble().

Dodatkowo można również otrzymać tablicę losowych wartości byte:

Reasumując, w Java do wygenerowania liczby losowej używamy klasy java.util.Random, którą tworzymy używając bezparametrowego konstruktora. Liczy losowe z obiektu klasy Random uzyskujemy wywołując jedną z metod nextInt(), nextLong(), nextFloat() i tak dalej.

Jeżeli masz jakieś pytania związane z tematem liczb pseudolosowych zapraszam do komentarzy!