Jako preview feature pojawiły się w JDK 14, by po dwóch odsłonach Javy wejść do standardu w wersji 16. Pół roku później została wydana Java 17, która będąc wydaniem Long Term Support prawdopodobnie będzie się cieszyła dużym zainteresowaniem.
Czas zatem przyjrzeć się, co w tych rekordach siedzi…
Krótkie wprowadzenie
Czym są rekordy w Javie? – to pytanie iście filozoficzne. Sami Twórcy musieli się nagłowić, co to adekwatnie jest i jak ten twór nazwać. Wszystkie te rozterki ładnie słychać w wywiadzie z Brianem Goetzem, który polecam. Dużo teorii o można wyczytać również ze źródłowego JEPa. Tam w podsumowaniu można między innymi przeczytać:
(…)records, which are classes that act as transparent carriers for immutable data. Records can be thought of as nominal tuples.”
JEP 395Czyli krótko: rekord to pojemnik na dane, przy czym dane są określone nazwą (w przeciwieństwie do Tuple).
Wielu mądrzejszych ode mnie ludzi pisało już o Rekordach, zatem nie będę pisał o rzeczach już opisanych, a podrzucę kilka linków (po angielsku i po polsku – pierwsze z brzegu znalezione na jvm-bloggers).
Implementacja Recordów
Warto zacząć od tego, rekordy są (nie)zwykłą klasą Javową. Zwięzły zapis:
public record Record(int i, String j)kompilowany jest do:
public final class Record extends java.lang.Record { private final int i; private final String j; public Record(int i, String j) { this.i = i; this.j = j; } public int i() { return i; } public String j() { return j; } public final java.lang.String toString() { // (implementacja) } public final int hashCode() { // (implementacja) } public final boolean equals(java.lang.Object) { // (implementacja) } }Jakby porównać te dwa zapisy, to ewidentnie łatwiej i szybciej utworzyć rekord niż tę drugą implementację.
Dodatkowo stworzone rekordy posiadają pewne feature’y, których normalnie zaimplementować się nie da. Przede wszystkim implementacje toString, hashCode i equals nie są generowane w czasie kompilacji do bytecode’u, a przy pierwszym użyciu danej metody. Podobny mechanizm można znaleźć w konkatenacji stringów lub w lambdach. Dodatkowo, do wygenerowania każdej z nich jest używana jedna metoda, chociaż w parametrach jest przekazywany pewien znacznik określający, którą metodę wygenerować. Wszystko to pozwala nieco odchudzić sam bytecode – nawet o 10%. Jednakże polemizowałbym, czy jest sensowne w czasach terabajtowych dysków twardych oszczędzać 100KB na bajtkodzie.
Mechanizm refleksji również został wzbogacony o informacje typowe dla rekordów. Można nie tylko wyciągnąć jakie pola istnieją w tym rekordzie (getRecordComponents), ale również jest informacja, jaka metoda pozwala na pobranie wartości tego pola. Warto tutaj nadmienić, iż rekordy nie spełniają standardu JavaBean – nie mają getterów. Jednak można to uznać za zaletę, gdyż nie musimy się zastanawiać, czy akcesor zaczyna się o „get” czy „is”.
Kolejność pól oraz argumentów konstruktora jest taka sama, da się ją pobrać refleksją i daje dobre podstawy do PatternMatchingu na rekordach. Istnieje na to już JEP i aktualnie jest w stanie „kandydat do JDK 18”.
Wsparcie dla serializacji jest również dostarczane automagicznie. Wystarczy dodać interfejs Serializable, a JVM zapewni, iż nikt niczego nikczemnego nie poczyni z obiektem. Dodatkowo deserializacja wywołuje domyślnie wygenerowany konstruktor, co jest niestandardowym zachowaniem w serializacji Javowej.
„Wyzwania”
Rekordy mają też swoje niedogodności.
Pierwszą z nich dodawanie adnotacji. jeżeli chcemy dodać adnotację to robimy to następująco public record Record(@SomeAnno int i, String j) . Niestety w ten sam sposób adnotujemy zarówno pole, jak i metodę dostępu do pola, ale również parametr konstruktora. Adnotacja zostanie dodana we wszystkich miejscach, do których pasuje – jeżeli adnotacja może być zastosowana do pola, wówczas pole zostanie zaadnotowane; jeżeli adnotacja przeznaczona jest do metod, wówczas akcesor zostanie zaadnotowany. Mimo wszystko może to tworzyć pewne konflikty.
Drugim problemem, na który się natknąłem, jest stawianie breakpointa przy debugowaniu. Aktualnie wszystkie akcesory, pola i konstruktor mają w bytecodzie przypisaną tę samą linię początku konstruktora. Zatem zastawienie się na „getterze”, a adekwatnie to akcesorze może być mało komfortowe.
Pewną niedogodnością może być brak prostego stworzenia nowego rekordu na podstawie starego bez przepisywania wszystkich wartości. Jednak tutaj z pomocą przychodzi Lombok, który aktualnie wspiera rekordy zarówno @Builderem, jaki i @Withem.
Wsparcie bibliotek
Rekordy są aktualnie wspierane przez Jacksona (od wersji 2.12) w serializacji i deserializacji bez dodatkowych akcji.
Hibernate również wspiera rekordy, ale jedynie w odczycie tj. rezultat zapytania można zmapować do rekordu/ów. O ile nie mogą one pełnić funkcji encji, o tyle można je wykorzystać jako obiekt wartości i w tej roli wykorzystać je w polach encji.
W Springu konfigurację można zmapować do rekordów. Można je również wykorzystać jako DTO do przekazywania wartości do silników template’ów takich jak Thymeleaf.
Garść linków
Wpisów o rekordach w internecie jest wiele, postaram się zalinkować najciekawsze:
- Na początek tekst o rekordach + porównanie z case klasami ze Scali i data klasami z Kotlina – link,
- Użycie rekordów w JPA/Hibernate – link oraz link,
- Nieco teorii, czyli JEP-395,
- Zapiski Briana Goetza o rekordach – link,
- Gdzie można zastosować dokładniej rekordy w Springu – link,
- Obszerne omówienie rekordów (po polsku!) w serii wpisów Piotra Przybył – intro, możliwości rekordów, refleksja, lombok; wpisów jest więcej, ale po angielsku
Pax!