Wyjątki w Javie dzielą się na checked exceptions oraz unchecked exceptions. Unchecked exception reprezentuje błąd w logice programu, który może wystąpić w dowolnym miejscu - przykładowo odwołanie się do nieistniejącego elementu tablicy spowoduje rzucenie wyjątku ArrayIndexOutOfBoundsException. Kompilator nie jest w stanie przewidzieć błędów logicznych, które pojawiają się dopiero w czasie wykonywania programu, dlatego nie może sprawdzać tego typu problemów w czasie kompilacji, co sprawia, że wyjątki te nie muszą być obsłużone przez programistę.
Przykład unchecked exception - dzielenie przez zero wyrzuci wyjątek ArithmeticException:
Checked exception to wyjątek reprezentujący przewidywalną, błędną sytuację, która może wystąpić nawet w przypadku poprawnej logiki programu - przykładowo próba otwarcia pliku, który nie istnieje, spowoduje rzucenie wyjątku FileNotFoundException. Wyjątki tego rodzaju są weryfikowane w czasie kompilacji, dlatego Java zmusza nas do ich obsługi - albo poprzez słowo kluczowe throws, albo poprzez złapanie wyjątku w bloku try-catch:
Przykład obsługi przez słowo kluczowe throws - przekazanie wyjątku w dół stosu wywołań:
Sneaky throws to koncepcja pozwalająca na rzucenie dowolnego checked exception bez jego jawnego definiowania w sygnaturze metody. Pozwala ona na ominięcie słowa kluczowego throws oraz imitowanie zachowania unchecked exception i jest możliwa, ponieważ obsługa wyjątków checked exception jest wymuszana tylko przez kompilator Javy. W kodzie bajtowym każdy wyjątek może zostać rzucony z dowolnego miejsca i jest traktowany przez JVM tak samo - zostaje on propagowany w dół stosu wywołań. Od Javy 8 każde użycie throws T, gdzie T jest typem generycznym rozszerzajacym Throwable, oznacza, że metoda może rzucić unchecked exception. Dzięki temu możemy stworzyć metodę pomocniczą, która będzie rzucała wyjątek typu checked, jednak kompilator nie będzie wymagał jego przechwycenia.
Metoda pomocnicza realizująca koncepcję sneaky throws:
Metodę tę możemy następnie dowolnie wykorzystać w naszym kodzie:
privateFilegetFile(StringfileName){returnnull;}privatevoiddeleteFile(StringfileName){Filefile=getFile(fileName);if(file==null){sneakyThrow(newFileNotFoundException("Nie znaleziono pliku"));}file.delete();}publicvoidtryToDeleteNotExistingFile(){try{deleteFile("Nieistniejacy_plik.txt");}catch(Exceptionexception){exception.printStackTrace();}}
Warto zauważyć, że w takiej sytuacji w metodzie tryToDeleteNotExistingFile() nie możemy złapać już wyjątku FileNotFoundException, ponieważ nie jest on zadeklarowany - możemy jedynie ratować się złapaniem bardziej ogólnego Exception.
Lombok - adnotacja @SneakyThrows
Biblioteka Lombok udostępnia adnotację @SneakyThrows, która wykorzystuje powyższą sztuczkę i dzięki oszukaniu kompilatora pozwala rzucać checked exception bez deklarowania tego w sygnaturze metody.
Przykład - użycie adnotacji @SneakyThrows:
privateFilegetFile(StringfileName){returnnull;}@SneakyThrows(FileNotFoundException.class)privatevoiddeleteFile(StringfileName){Filefile=getFile(fileName);if(file==null){thrownewFileNotFoundException("Nie znaleziono pliku");}file.delete();}publicvoidtryToDeleteNotExistingFile(){try{deleteFile("Nieistniejacy_plik.txt");}catch(Exceptionexception){exception.printStackTrace();}}
Do adnotacji @SneakyThrows można przekazać dowolną liczbę wyjątków. Jeśli nie podamy żadnego, to adnotacja ta uwzględni dowolny wyjątek. Należy pamiętać także o tym, że @SneakyThrows nie dziedziczy.
Kiedy używać?
Dokumentacja @SneakyThrows wspomina o dwóch częstych przypadkach użycia:
niepotrzebnie rygorystyczne interfejsy takie jak Runnable,
“niemożliwe” wyjątki, które nie powinny nigdy być rzucone np. ze względu na specyfikację JVM.
Inną sytuacją, w której można zastanowić się nad użyciem @SneakyThrows są wyrażenia lambda - użycie tej adnotacji pozwoli zwiększyć czytelność, ponieważ nie będziemy musieli przejmować się łapaniem wyjątków w blok try-catch. Przykład z użyciem @SneakyThrows: