Java idzie na rekord
Java 14 przyniosła nam rekordy (ang. records) jako preview feature. Było (jest?) przy tym sporo zamieszania, deklaracji i ciężkich obelg miotanych w kierunku wrażych bibliotek i wtyczek do IDE. Pozwólcie mi dorzucić moje trzy grosze.
Przede wszystkim można chyba powiedzieć: wreszcie. Po case classes w Scali i data classes w Kotlinie, “w końcu robią to w Javie”. Fakt, tylko do pełnego obrazu tej układanki trzeba dołożyć jeszcze kilka puzzli:
- Java stara się utrzymywać zgodność wsteczną, jak tylko się da (czy to dobrze, czy źle, temat na osobną dyskusję),
- Java w maju 2020 obchodzi 25 urodziny i jest w pełnym rozkwicie (nie jest nowym językiem bez ekosystemu), więc trzeba zmiany jakoś sensownie wpasować, jak w każdy dojrzały system/technologię,
- trzeba jakoś przygotować rzeszę programistów do nadchodzących zmian (raczej kapać nowościami niż urządzać topienie).
Jeśli masz ochotę zacząć od czegoś formalnego, to polecam przeczytać i zrozumieć JEP 359.
Moje postrzeganie rekordów
Tłumaczenie komuś czym są rekordy zacząłbym chyba od tego, żeby zapomnieć wszystko, co się wie o Javie do tej pory. I skupiłbym się na zdaniu z JEPa Records can be considered a nominal form of tuples. Lub, jak zdaje się stwierdził p. Brajan Goetz, named tuples. Czyli iż chodzi o nazwane krotki albo “krotki, które mają nazwę”.
W porządku, co to te krotki? To skoczmy może na grunt zaprzyjaźnionego SQLa. Gdy napiszesz
to w wyniku otrzymasz zbiór krotek (ang. tuples), każda krotka ma dwa elementy. (Tak, pewnie w programie do podglądu danych z bazy pokazuje to w formie tabelarycznej, stąd krotki wyglądają jak wiersze.) W Javie można napisać metodę zwracającą masę i pojemność jednej paczki np. tak
Tylko żeby takie coś zadziałało, to trzeba stworzyć kolejną klasę GrossWeightAndVolume, najprawdopodobniej jako JavaBean, razem z konstruktorem, getterami, najprawdopodobniej nie zaszkodzi dodać też equals() i hashCode(), może choćby toString(), żeby ładnie w logach wypisywało. Ewentualnie można do pracy zaprządz Project Lombok i wykorzystać adnotację @Value. (Wykorzystanie Lomboka ma swoje plusy i minusy, o czym innym razem).
W tym momencie pojawiają się tzw. “cwane gapy”, które w imię “śrubowania performęsu” zamiast kolejnego typu (bo rozmiar JARów, classloader,…) namiętnie zalecają stosować typy podstawowe do obłędu i sygnaturę metody zapisują dokładnie tak, jak de facto wychodzi ona z zapytania SQL i stosują tablicę do reprezentowania krotek:
Kłopot w tym, iż mając takie zapytanie w SQLu, możemy też mieć kolejne (pomijając jego sens “biznesowy”):
Można znowu albo tworzyć JavaBean (i utrzymywać!), albo radośnie wykorzystywać double[].
Zaczyna się robić wesoło, gdy uświadomimy sobie, iż te krotki z SQLa i te z double[] trzeba trzymać twardą ręką i w żadnym wypadku nie wolno ich ze sobą nigdy pomieszać. Bo nagle może dojść do takich absurdów:
Tak samo można pomieszać dwie różne tablice double[]. Czy może być jeszcze weselej? Oczywiście! Nie po to olewamy typy i kompilator, żeby było sztampowo… A co jeżeli z takiej metody trzeba zwrócić równocześnie liczbę całkowitą i rzeczywistą? Wtedy trzeba zrobić tablicę Number[], pojawia się autoboxing,… A może dodajmy jeszcze napis… co wtedy? Serializable[]? “A wcale iż nie, możesz mieć klasę Pair albo Triple z generykami!” Fakt, tylko skąd wtedy wiadomo, iż jedna para Pair<Double, Double> nie może być mieszana z inną parą Pair<Double, Double>, która logicznie i semantycznie przechowuje coś zupełnie innego? Widząc Set<Pair<Double, Double>>, czego spodziewać się w środku? Rozmiarów paczek? Liczb zespolonych?
Za każdym razem, gdy w Javie pozbywasz się kontroli typów przez kompilator i zaczynasz traktować takie same typy inaczej w zależności od kontekstu, to do nazwy Java dopisujesz Script.
Z tych właśnie powodów, żeby nie mieszać śliwek z Camaro, Java posiada krotki nazwane, które nazywamy rekordami.
Jak stworzyć rekord?
Bardzo łatwo. JEP określa rekordy jako “ograniczoną formą klasy, podobnie jak enum”. jeżeli masz ochotę zdefiniować np. rekord dla liczb zespolonych, można zapisać to tak:
A jeżeli masz potrzebę przechowywać masę i rozmiar paczki, można zdefiniować inny rekord:
Co to daje? Kompilator od razu stworzy na dla nas:
- konstruktor kanoniczny, czyli możemy napisać: Complex aComplex = new Complex(3.0, 1.7);
- akcesory dla wszystkich komponentu, czyli możemy pobrać część rzeczywistą: double x = aComplex.real();
- metody equals() i hashCode() wykorzystujące wszystkie komponenty rekordu,
- metodę toString(), która wypisze nam nazwę klasy rekordu oraz wartości wszystkich komponentów, np. Complex[real=3.0, imaginary=1.7]
Z rzeczy dostępnych na dzień dobry to w zasadzie tyle. Tylko boli mnie trochę, iż zapominamy o rzeczach nie mniej ważnych: rekord daje nam nazwę i typ! Czyli mając:
nie można do tego zbioru dodać innego rekordu (nawet jeżeli ma dokładnie takie same komponenty):
To pomaga pamiętać, iż kompilator to nie stary zrzędliwy architekt, tylko sympatyczny kolo, który zawsze programuje z Tobą w parze.
Tylko do odczytu
Jeśli Twym oczom przypadkiem umknęło, to warto podkreślić, iż rekordy nie mają niczego na kształt “setterów”. dla wszystkich komponentu jest tworzona tylko metoda dostępu, nie ma żadnej metody zmiany. W oryginalne figurują one jako accessors, stąd przyjąłem tłumaczenie (może nie najlepsze): akcesory. Oczywiście pod spodem każdy komponent trafia do pola, które jest private final, dlatego oczywiste jest, iż nie można referencji podmienić czy nadpisać. Co można zrobić, to jako komponent podać coś, czego wartość teoretycznie można zmieniać, np. ArrayList lub AtomicInteger. Jednakże z wielu powodów nie wydaje się to rozsądne.
W kolejnym wpisie jest o tym, co wolno zrobić z rekordami, a czego nie.