Ostatnio prowadzę sporą ilość rekrutacji do projektu, na którym jestem. Dzięki temu mam możliwość poznać ciekawych ludzi, od których jestem w stanie się czegoś nauczyć. Podczas rozmowy rekrutacyjnej proszę kandydatów o zrobienie code review kodu, który “napisałem”. W jednej z linijek tego kodu znajduje się tytułowa adnotacja Lomboka @SneakyThrows. To czy jej wykorzystanie jest antypatternem czy też nie, nie mi to osądzać. Natomiast na rozmowie rekrutacyjnej jeden z kandydatów powiedział, że wykorzystanie adnotacji @SneakyThrows jest w 100% złe, ponieważ miesza ona w strukturze bytecode aplikacji. Wydaje mi się, że wszystkie adnotacje Lomboka tak robią… Niemniej zaciekawiło mnie jak to faktycznie wygląda pod maską dla @SneakyThrows. I oto co znalazłem.

Wszystko jest fajnie opisane na blogu Dev Genius. Okazuje się, że twórcy Lomboka wykorzystali po prostu trick oszukujący kompilator. W jaki sposób? Sprawdźmy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SuppressWarnings("unchecked")
static <T extends Throwable> void sneakyThrow(Throwable t) throws T {
  throw (T) t;
}

static <T extends Throwable> void nonSneakyThrow(T t) throws T {
  throw t;
}

static void testSneaky() {
  final Exception e = new Exception();
  sneakyThrow(e);    // No Errors
  nonSneakyThrow(e); // Error: Unhandled exception: java.lang.Exception
}

Skopiuj to sobie do IDE i przetrzyj oczy ze zdumienia. Kompilator z jakiegoś powodu zaczyna wierzyć, że ma do czynienia z unchecked exception zamiast z checked exception. Magia? Nie, to po prostu specyfika języka Java. Zostało to w mniej lub bardziej zrozumiały sposób opisane w dokumentacji. Wszystko oczywiście ma związek z wielkią zmianą, która zaszła w Javie. Dokładniej mówiąc - dotyczącą typów generycznych i ich wnioskowania. W powyższej dokumentacji możemy przeczytać:

Otherwise, if the bound set contains throws αi, and the proper upper bounds of αi are, at most, Exception, Throwable, and Object, then Ti = RuntimeException.

Czyli kompilator zakłada, że jeśli dla danej klasy w hierarchii dziedziczenia są przynajmniej klasy Object, Throwable i Exception to jest to całkiem prawdopodobne, że typem tej klasy jest RuntimeException. I tak to właśnie wygląda w przypadku metody sneakyThrow. Natomiast w nonSneakyThrow nie mamy czego wnioskować, bo przekazany do niej obiekt jest przypisany do parametru typu T. Wtedy komplilator zakłada, że on już na pewno będzie po prostu dalej tym samym typem co wcześniej np. checked exception. Oto cała sztuczka! Czy wiedziałeś/wiedziałaś o tym?

Powyższy kod możesz znaleźć na GitHub projektu Lombok.