Krzysztof Olszewski
Dyrektor Technologii i Architektury Oprogramowania
Krzysztof Olszewski
Dyrektor Technologii i Architektury Oprogramowania
Po latach opierania się i negowania słuszności samego pomysłu, w wersji ósmej JAVA’y, już 2 lata temu (jak ten czas leci), otrzymaliśmy kilka nowych, długo wyczekiwanych elementów języka. Jednym z nich są lambdy. W wielkim uproszczeniu, zamiast pisać tak:
możemy napisać tak:
Różnica jest ogromna, jest „dużo mniej kodu”. Co ważne w pierwszym przykładzie na etapie kompilacji powstaje klasa anonimowa z magicznym znakiem „$” w nazwie, klasa ta jest pełnoprawną klasą, jest ładowana przez ClassLoader’a, potem instancjonowana, nie ma tu żadnej magii. Można by się spodziewać, że w drugim przypadku dzieje się tak samo, przecież składnia lambd jest pewnie i tak „tłumaczona” na taka samą klasę anonimową. Jest jednak całkiem inaczej. Po użyciu lambdy i kompilacji nijak nie możemy znaleźć klasy ze znakiem „$” w katalogu wynikowym, zatem jak działają lambdy? Odpowiedź znajdziemy w bajtkodzie, widzimy, że nasz kod z lambdą powoduje coś na kształt:
Interesująca jest instrukcja „invokedynamic” oraz fakt powstania metody „lambda$0”, której nie tworzyliśmy przecież. Wniosek jest prosty, w trakcie kompilacji lambda przekształca się na dodatkową metodę w naszej klasie, nie powstaje klasa anonimowa, nie ponosimy kosztów jej tworzenia. Grejt! Coś tu jednak nie pasuje. Przecież JAVA jako język silnie typowany nie pozwoli do metody, której interface jest taki:
(co oznacza, że do tego konstruktora musimy przekazać instancję, która implementuje interfejs Runnable) przekazać czegoś, co tego interfejsu nie spełnia. A może JAVA 8 znosi taki obowiązek? Otóż nie znosi, a cała „tajemnica” jest w instrukcji „invokedynamic”, za którą stoi mechanizm, który pozwala (w tym przypadku) na dynamiczne, w czasie działania aplikacji tworzenie implementacji klas i ich instancji. Wniosek jest prosty, jakaś klasa jednak powstaje, klasa ta implementuje wymagany interfejs, co oznacza, że JAVA nie stała się mniej statycznie typowana, i wszystko dalej „gra”. Sprawdźmy zatem i podglądnijmy to coś, co powstaje, jak tworzymy lambdy, możemy użyć np. tego kodu:
Wynik jest taki:
Czyli jak się było można spodziewać, powstaje implementacja interfejsu java.lang.Runnable, o dziwnej nazwie, która w jedynej wymaganej przez interfejs metodzie ma pewnie wywołanie magicznie powstałej metody lambda$0. Wiemy już dużo, choć wynik działania poniższego kodu chwilowo zaskakuje:
To znaczy, że za każdym razem powstaje nowa instancja dynamicznej klasy (o!), oraz że za każdym razem powstaje nowa klasa dynamiczna (o!). Nie da się ukryć, że to może budzić kontrowersje, szczególnie że w wielu innych językach wynik porównania tych dwóch lambd pokazałby, że są one tożsame.
Kolejne pytanie, które się nasuwa, czy lambdy w tej implementacji są optymalne ze względu na zasoby i wydajność. Podobno tak, są dostępne wyniki różnych testów, które pokazują, że z użyciem lambd nie jest wolniej, a nawet szybciej. Za taką implementacją przemawiał także argument, że nie zamyka ona przyszłych działań optymalizacyjnych (super). Chciałoby się jeszcze na koniec zobaczyć bajtkod tej tajemniczej dynamicznej klasy, która powstaje, mi się nie udało, może ktoś umie to zrobić?