Przejdź do głównej zawartości

Sneaky throws, czyli checked i unchecked exception w jednym!

Często podczas pisania kodu Java korzystam z wyrażeń lambda wprowadzonych wraz z Java 8. Często prowadzi to też do pewnych nowych problemów i zmusza do szukania rozwiązań. Przyjrzyjmy się jednemu z nich.

Checked exception w wyrażeniu lambda

Często zdarza się, że metoda wywoływana wewnątrz lambdy rzuca wyjątek. Nie ma sprawy gdy wyjątek jest obiektem klasy będącej podklasą RuntimeException. Wtedy po prostu się nim nie przejmujemy. Wyjątek jest przekazywany w górę stosu wywołań. Problem zaczyna się gdy kompilator zmusza nas do obsługi wyjątku. Jak zwykle mamy 2 wyjścia: obsłużyć wyjątek za pomocą bloku try-catch, bądź zadeklarować przekazanie wyjątku dalej za pomocą słowa kluczowego throws. O ile wiemy co zrobić po złapaniu wyjątku to wszystko gra. Co natomiast gdy chcemy wybrać drugą opcję? Większość interfejsów stosowanych jako typy parametrów, często przekazywanych jako lambdy jak np: Function, Consumer czy Supplier nie deklarują, że mogą rzucić wyjątek. W takim wypadku druga opcja zupełnie nam odpada.

Rozwiązanie

Rozwiązania w zasadzie są dwa:

Opakowanie wyjątku w RuntimeException

Weźmy sobie metodę, która rzuca wyjatkiem IOException:

public void throwsIOException() throws IOException {
    throw new IOException();
}
Teraz spróbujmy wywołać ją w wyrażeniu lambda:

public void throwInLambda() {
    Collections.singletonList(1).forEach(x -> throwsIOException());
}
Cóż... Kompilator nam na to nie pozwala. Opakujmy wyjątek w RuntimeException:

public void throwInLambdaWithRuntime() {
    Collections.singletonList(1).forEach(x -> {
        try {
            throwsIOException();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    });
}
Działa? Działa. Wyjątek jest przekazywany w górę do naszej metody throwInLambdaWithRuntime. Stąd można go obsłużyć lub przekazać wyżej. Problem w tym, że to tak naprawdę nie ten obiekt wyjątku o który nam chodziło. Docelowy obiekt znajduje się w cause. Traktuje to rozwiązanie jako połowiczne.

"Sneaky throws"

W Java 8 została wprowadzona nowa reguła wnioskowania, według której każde użycie throws T, gdzie T jest typem generycznym oznacza, że metoda może rzucić wyjątek typu RuntimeException. Dlaczego więc z tego nie skorzystać?
Przygotujmy sobie metodę sneakyThrow:

private static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
    throw (T) t;
}
Do powyższej metody przekazujemy obiekt wyjątku, który chcemy rzucić. Zostanie on wtedy rzucony tak jak z użyciem słowa kluczowego throws ale kompilator nie będzie wymagał już jego przechwycenia. Weźmy sobie metodę rzucająca wyjątek z użyciem metody sneakyThrow:

public void throwsSneakyIOException() {
    sneakyThrow(new IOException("sneaky"));
}
I spróbujmy ją wykorzystać w wyrażeniu lambda:

public void sneakyThrowInLambda() {
    Collections.singletonList(1).forEach(x -> throwsSneakyIOException());
}
Kompilator nie zgłasza już żadnych problemów.
Rozwiązanie to ma jednak jedną złośliwość. No bo jeśli teraz zechcielibyśmy złapać wyjątek to nie mamy takiej możliwości bo nie jest on zadeklarowany. Zawsze możemy jednak wydzielić sobie fragment kodu do metody deklarującej rzucenie danego wyjątku i wtedy można go już złapać i obsłużyć.

O sneaky throws dowiedziałem się trochę przypadkiem przeglądając kod, korzystający z adnotacji biblioteki Lombok, którą bardzo polecam.

Dla mnie to odkrycie nieco zmniejsza ból głowy regularnie powodowany przez wszechobecne wszędzie wyjątki :)

Komentarze

Popularne posty z tego bloga

Wzorzec Open Session in View w Spring Boot

Dzisiejszy post będzie z cyklu: "Wtf? Dlaczego to działa?". A dotyczy on pewnego wzorca, którego implementacja jak się okazuje jest we frameworku Spring Boot domyślnie włączona, czego nie wszyscy programiści mogą się spodziewać. Geneza Zaczęło się od tego, że pisałem kolejny test integracyjny do kolejnego kontrolera. Test napisany, uruchamiam, zielono. No i przeglądam sobie jeszcze raz kod zanim wypchnę zmiany do review. I rzuciło mi się w oczy, że zapomniałem dodać adnotacji @Transactional (do tej pory uważałem, że to jest zawsze wymagane). I wtedy zadałem sobie przytoczone na wstępie pytanie. Dlaczego test przeszedł skoro mój serwis dociąga sobie obiekt oznaczony jako LAZY ? Dużo się nagimnastykowałem by wpisać odpowiednie query do Google'a i znaleźć odpowiedź: Open Session in View. Ale o tym za chwilę. Jak działa Hibernate Żeby lepiej zrozumieć opisywane zagadnienie warto przypomnieć jakie kroki w uproszczeniu musi wykonać Hibernate podczas komunikacji z bazą d

Pułapki logowania w Javie

Czy spotkałeś kiedyś się z kodem takim jak poniższy? if (logger.isDebugEnabled()) { logger.debug("Request: " + requestObject); } Ja tak. Jakiś czas temu napotkałem na mnóstwo takich fragmentów w kodzie, który analizowałem. Zastanowiło mnie po co została wykorzystana tutaj instrukcja warunkowa - a jako, że if-y uwazam za zło konieczne postanowiłem to zbadać. Czy nie do tego właśnie służą biblioteki do logowania i udostępniane przez nie metody jak debug żeby właśnie sterowane konfiguracją decydowały co zalogować a co nie? Co więcej, okazało się, że instrukcję warunkową wprowadził ktoś w ramach większej akcji. I jak się okazuje, prawdopodobnie znacznie zwiększył dzieki temu wydajność aplikacji. W dzisiejszym wpisie postaram się wyjaśnić jak korzystać z loggera aby nie zaszkodziło to wydajności. Po co if-y? Na początek wyjaśnić trzeba czemu ten if tak na prawdę służy. Korzystamy z biblioteki do logowania i spodziewamy się, że wykorzystujac wybraną metodę, zostanie