Rekordy w Javie - co na to Lombok?
Aktualizacja kwiecień 2021: Proszę mieć na względzie, że poniższy wpis dotyczy Javy 15, gdzie rekordy były preview feature. Od tego czasu parę rzeczy się zmieniło, aktualizacja jest w innym wpisie.
Rekordy: konfrontacja
W czasie moich aktywności online dotyczących tłumaczenia rekordów parę osób rzuciło podkręconą piłkę “no a Lombok?”
W szczególności pytanie przybiera formę “skoro rekordy są immutable, to czym się różnią od @Value
z Lomboka?”
Przede wszystkim trzeba pamiętać, że “rekordy nie są Java Beans”, @Value
generuje nam niezmienialny (“niemutowalny”) JavaBean.
Częściowo efekty zastosowania record
i @Value
pokrywają się.
- Pojawią się metody
equals()
,hashCode()
,toString()
. - Wszystkie pola zostaną oznaczone jako
private final
. - Pojawią się “konstruktory do wszystkich pól” (jeśli w ziarenku od
@Value
któreś z pól jest zainicjalizowane przy deklaracji, to oczywiście zostanie pominięte). record
i@Value
tworzą klasy, które sąfinal
, więc nie można po nich dziedziczyć.
Dwie fundamentalne różnice są takie:
- W rekordach deklarujemy składowe w nagłówku rekordu (pięknie opisuje to gramatyka z JEPa 384) i na tej podstawie generowane są pola i parametry konstruktora kanonicznego, zaś do JavaBean poddanych
@Value
trzeba wpisać wszystkie pola i to na ich podstawie jest tworzony konstruktor “kanoniczny”. - Rekordy posiadają “akcesory”,
@Value
da nam “gettery”.
Różnic oczywiście jest więcej. Jeśli chcecie je dokładnie poznać, to sposobem totalnym można porównywać kod bajtowy (bytecode), lub chociaż dokonać “delombokizacji” @Value
w IDE. Przykładowo, jeśli do konfiguracji Lomboka w lombok.config
dopisać lombok.anyConstructor.addConstructorProperties=true
, to konstruktor “kanoniczny” dzięki @Value
posiada także adnotację @java.beans.ConstructorProperties
. “E tam, jakiś detal”. Tak, ale ten detal powoduje, że elegancko można zdeserializować takim konstruktorem JSONa wykorzystując Jacksona. Jako “elegancko” rozumiem bez pisania @JsonCreator
przed konstruktorem i @JsonProperty("name)
przed każdym parametrem konstruktora. (Pamiętajmy, że “normalna deserializacja”, czyli z formatu binarnego, w rekordach też jest robiona przez konstruktor, więc koncepcja deserializacji konstruktorem zyskuje na popularności na wielu frontach. Wreszcie.)
W przyszłości tych różnic może być jeszcze więcej. Jednym ze sposobów na wykorzystanie rekordów ma być dopasowanie wzorców (pattern matching) z wyłuskiwaniem danych. A takie wyłuskiwanie wymaga dekonstrukcji obiektu (jak unapply
w Scali). Kiedyś może się okazać, że record
może tworzyć takie dekonstruktory dla rekordów. Na razie brak mi wiedzy, czy i jak będzie można rozkładać obiekty samemu i/lub czy Lombok jakoś to umożliwi.
A teraz: pocałujcie się!
@Value
(bo czytasz dokumentację, prawda?) zauważy, że w jakimś sensie @Value
jest skrótem kilku innych adnotacji. Lista wygląda tak: final @ToString @EqualsAndHashCode @AllArgsConstructor @FieldDefaults(makeFinal = true, level = AccessLevel.PRIVATE) @Getter
Rekordy mają toString()
, to nie potrzebują @ToString
itd. W zasadzie poza @Getter
to mają wszystko.
Co się stanie, jeśli do definicji rekordu dopisać @Getter
@Getter
public record RecordWithGetters(int f1, String f2) {}
.class
, który po dekompilacji wygląda mniej więcej tak:
1 // IntelliJ API Decompiler stub source generated from a class file
2 // Implementation of methods is not available
3
4public final class RecordWithGetters extends java.lang.Record {
5 private final int f1;
6 private final java.lang.String f2;
7
8 public RecordWithGetters(int f1, java.lang.String f2) { /* compiled code */ }
9
10 public int getF1() { /* compiled code */ }
11
12 public java.lang.String getF2() { /* compiled code */ }
13
14 public java.lang.String toString() { /* compiled code */ }
15
16 public final int hashCode() { /* compiled code */ }
17
18 public final boolean equals(java.lang.Object o) { /* compiled code */ }
19
20 public int f1() { /* compiled code */ }
21
22 public java.lang.String f2() { /* compiled code */ }
23}
Czyli poza akcesorami do składowych, w rekordzie pojawiły mi się również gettery. Nie wiem, czy mi się to podoba. Ale wiem, że dzięki temu pojawia się kolejna opcja, którą można dołączyć do listy z wpisu o refleksji rekordów i mieć gettery wygenerowane, zamiast dopisywać je ręcznie, bo “niestety, ta biblioteka wymaga getterów”.
No ale coś tam pisałem o działaniu @AllArgsConstructor
… Jak pomęczyć rekordy jeszcze bardziej i nad klasą dodać tę właśnie adnotację (i gdy mamy odpowiedni wpis w json.config
):
@AllArgsConstructor
public record RecordWithAllArgsConstructor(int f1, String f2) {}
to po całej kompilacji konstruktor wygląda następująco:
@java.beans.ConstructorProperties({"f1", "f2"})
public RecordWithAllArgsConstructor(int f1, java.lang.String f2) { /* compiled code */ }
Co nam to daje? “Kompatybilność” z nieszczęsnym Jacksonem przy przerabianiu JSONa na rekordy (czyli deserializacji).
Podsumowując: dopisanie do definicji rekordu dwóch adnotacji @Getter @AllArgsConstructor
przy posiadaniu odpowiednio skonfigurowanego Lomboka w projekcie, już teraz umożliwi nam wykorzystanie rekordów w dość popularnym obszarze, czyli przerabianiu obiektów/rekordów Javy na odpowiedzi RESTowe i wczytywanie tych obiektów/rekordów z żądań RESTowych.
Czy warto?
Szczerze? Nie wiem.
Z jednej strony umożliwi to już teraz (jeśli ktoś się nie boi --enable-preview
i lubi życie na krawędzi) wykorzystanie rekordów w bardzo popularnym zakresie. A gdy już wszystkie biblioteki wykorzystywane przez nas będą w stanie obsługiwać rekordy, to będzie można z tych adnotacji zrezygnować.
Z drugiej strony Lombok nie jest za darmo. Ciężko się żyje bez wtyczki do IDE, wtyczka nie zawsze jest aktualna, Lombok (z racji bycia “wtyczką do kompilatora”) nie zadziała np. przy mieszanej kompilacji ze Scalą itd. Na duży minus zasługuje to, że w moich obecnych ustawieniach (Intellij IDEA 2020.1.1, wtyczka Lombokowa do IDEI: 0.30-2020.1) nie da się wywołać konstruktora rekordu, jeśli ten konstruktor jest z Lomboka ;-) Kompilowanie i uruchamianie Mavenem na OpenJDK Runtime Environment (build 15-ea+23-1098)
jak najbardziej działa.
Czyli móc, to można. Ale czy warto? Każdy niech odpowie sobie sam.
A co z @Value
“No dobrze, ale skoro @Getter @AllArgsConstructor
są w jakimś sensie podzbiorem @Value
, to może korzystać z @Value
przy rekordach?”
Tego bym raczej nie robił w żadnych okolicznościach.
- Wykorzystanie
@Value
generuje w IDE ostrzeżenie, że metodyequals()
ihashCode()
nie uwzględniają klas, z których nasza klasa dziedziczy. Na pierwszy rzut oka@Value record ARecord(){}
z niczego nie dziedziczy, ale trzeba pamiętać ojava.lang.Record
. Lombok, zdaje się, ma tu (jeszcze?) problemy. - Równoczesne wykorzystanie Lomboka i rekordów powoduje, że jeśli jakaś metoda jest generowana przez Lombok i przez rekord “z definicji”, to w pliku
.class
znajdzie się metoda wygenerowana przez Lombok. A tak sobie myślę, że chyba lepiej zostawić by było tę “oryginalną od rekordu”. Kwestia smaku i kompatybilności w przód.
Lombok jest potężnym narzędziem. I jak każde potężne narzędzie wymaga zrozumienia i odpowiedzialności za każde (złe) użycie.
Jeśli świerzbią kogoś palce, żeby własnoręcznie popastwić się nieco nad rekordami, to kod leży sobie na Githubie.