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.

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 😉