Java 13 i 14: Bloki tekstowe i rekordy

Java 13 nie wprowadza zbyt wielu zmian do samego języka. Wyrażenia switch
wiciąż są w fazie preview; w tej wersji wylądowało wyrażenie yield
służące do zwracania wartości z gałęzi case
, a wcześniejsza propozycja używała słowa kluczowego break
(patrz: Java 12 - wyrażenie switch). Dopiero Java 14 wprowadza interesujące dodatki (obydwa w fazie preview): bloki tekstowe (JEP-355) i rekordy (JEP-359). Oraz kilka pomniejszych zmian.
Lista zmian w języku w wydaniach 13 i 14
Zmiany w Java 13
Zmiany w języku w wersji 13:
- dodanie instrukcji
yield
(poprawnej w kontekście wyrażeniaswitch
) służącej do zwrócenia wartości z blokucase
(wyrażenieswitch
wciąż preview) - wprowadzenie bloków tekstowych (JEP-355) (preview)
Zmiany w Java 14
Oto najciekawsze zmiany wprowadzone w Java 14:
- ustandaryzowane wyrażenie
switch
- bardziej pomocny opis wyjątku NullPointerException (JEP-358)
- pattern matching w
instancof
(preview) - rekordy (preview)
Najważniejsze zmiany
Bloki tekstowe
Bloki tekstowe zostały ustandaryzowane w Javie 15, a rekordy i pattern matching dla instanceof
- w Javie 16. Ponieważ jednak mam zainstalowaną Javę 14, to w tym wpisie opiszę dostępne w czternastce cechy języka.
Bloki tekstowe są wielowierszowymi literałami napisowymi, w których nie trzeba “eskejpować” cudzysłowów oraz które są automatycznie formatowane w przewidywalny dla programisty sposób. Dzięki ich użyciu:
- łatwiej pisać kod zawierający wielowierszowe napisy
- kod jest czytelniejszy, bo nie trzeba używać tylu ukośników do wprowadzenia np cudzysłowu (problem przy zapisywaniu literałów reprezentujących dane w formacie JSON)
Wciąż mamy do czynienia z typem java.lang.String
, nie jest dostępna (jeszcze!) interpolacja wartości (jak np. w JavaScript) ani też nie mamy możliwości zareprezentowania napisu “raw” (w którym wszystkie znaki poza znakiem końca są dosłowne). Jak zatem wyglądają?
Anatomia
Przypatrzmy się blokowi tekstowemu reprezentującemu fragment html-a:
|
|
- Blok tekstowy rozpoczyna się i kończy się trzema podwójnymi cudzysłowami (double quote).
- Można w nim używać znaków nowego wiersza wprost, bez konieczności używania znaków
\n
(można użyć\n
, ale nie jest to zalecane) - Można w nich używać cudzysłowów (podobnie tutaj, można użyć sekwencji unikowej
\"
, ale nie jest to zalecane)
Poprawne czy niepoprawne
Uwaga! Oto kilka niuansów, na które warto zwrócić uwagę.
- Aby uzyskać pusty napis, można zadeklarować blok tekstowy w taki sposób:
var a = """ """;
Wygląda to jednak dziwnie i zajmuje dwa wiersze kodu źródłowego.
- Nie będzie poprawna deklaracja bez znaku końca wiersza; poniższe dwie deklaracje są niepoprawne:
// nie skompiluje się var notGood = """"""; var alsoNotGood = """ """;
- Należy też użyć sekwencji unikowej do backslasha. Kolejna niepoprawna deklaracja:
// nie skompiluje się var iNeedEscape = """ one \ other """;
(Uwaga: parser Java kolorujący składnię jeszcze nie uwzględnia nowej składni Javy; do wyświetlenia powyższego kodu używam więc tagów pre.)
Powyższe sytuacje staną się bardziej zrozumiałe, gdy popatrzymy na sposób, w jaki kompilator przetwarza bloki tekstowe
Przetwarzanie bloków tekstowych w czasie kompilacji
Przetwarzanie jest trzyetapowe:
- Końce wierszy są zamieniane na LF (\u000A)
- Usuwane są “przypadkowe spacje”, które zostały dodane do literału po to, by tekst lepiej pasował do wcięć kodu źródłowego.
- Interpretowane są sekwencje unikowe
Zwykłe literały napisowe i bloki tekstowe nie są rozróżnialne w pliku .class - w obydwu przypadkach literał trafia do constant poola jako wpis CONSTANT_String_info. W czasie wykonania również nie ma roźnicy - literały i bloki są referencjami na typ String i jeśli mają taką samą treść, mogą wskazywać na ten sam obszar pamięci ze względu na interning
.
Pattern matching dla instanceof
Pattern matching oznacza w tym kontekście brak konieczności rzutowania na typ, który został sprawdzony operatorem instanceof
, ponieważ skoro jesteśmy w bloku if
, to mamy pewność że zmienna jest właściwego typu. A więc nie chcemy kodu:
|
|
a chcemy tego:
|
|
Zakresem leksykalnym zmiennej s jest całość - być może złożonego - warunku (nawias z “ifem”) oraz następujący po nim blok:
|
|
ale tylko wówczas, gdy kompilator ma pewność, że zmienna s została przypisana. W przypadku operatora logicznego || s może nie być przypisana (bo nie jest spełniona lewa strona operatora ||), więc s nie będzie dostępna po prawej stronie operatora (a prawa strona jest wartościowana niezależnie od wartościowania strony lewej).
Poniższy kod nie skompiluje się:
|
|
Rekordy
Rekordy, podobnie jak pattern matching dla instanceof
, weszły do standardu języka w wydaniu 16., ale ponieważ są dostęne jako preview już w 14, postanowiłam wspomnieć o nich już teraz.
Rekord jest specjalnym, dość ograniczonym rodzajem klasy. Ograniczonym z wielu stron:
- rekord nie może dziedziczyć po innej klasie
- rekord jest zawsze final i nie może być wobec tego zadeklarowany jako abstract
- nie może deklarować (nie-statycznych) pól innych niż określone w jego “strukturze” czy “reprezentacji” (pola rekordu formalnie określa się nazwą “komponenty”)
- rekord jest więc, wobec powyższych ograniczeń, immutable (niemodyfikowalny)
- nie może, tak jak klasa, elegancko oddzielić API od reprezentacji; jego reprezentacja określa jednocześnie jego API
Dzięki tym ograniczeniom kompilator ma pełną wiedzę o strukturze klasy i może się napwawdę wykazać.
Jak wygląda rekord?
Anatomia rekordu
Rekord posiada nazwę (podobnie jak każda nie anonimowa klasa) oraz opis swojego stanu. Opisem tym jest lista oddzielonych przecinkiem komponentów rekordu. Każdy z komponentów posiada nazwę i typ.
|
|
Ponieważ rekordy są prostymi agregatami danych, kompilator automatycznie generuje
- prywatne i finalne pola o nazwach i typach takich jak nazwy i typy komponentów wskazanych w deklaracji rekordu
- gettery do wygenerowanych w ten sposób pól o nazwach takich jak nazwy pól (nie zastosowano tu konwencji Java Beans)
- publiczny konstruktor z argumentami odpowiadającymi zadeklarowaym komponentom i przypisujący przekazane argumenty do odpowiednich pól
- implmentacje equals(…) (dwa rekordy są równe gdy są tego samego typu oraz mają takie same wartości komponentów) oraz hashCode()
- implementację toString(), która wyraża nazwę rekordu oraz nazwy i wartości wszystkich pól
Rozważane alternatywy
Jedną z rozważanych możliwości było zaimplementowanie heterogenicznych krotek, jednak nie miałyby one trzech ważnych cech:
- jej elementy nie posiadałyby nazw, a nazwa ma ogromne znaczenie dla rozumienia, stosowania, budowania modelu mentalnego kodu itd.
- klasy, w przeciwieństwie do krotek, mają możliwość weryfikowania danych wejściowych potrzebnych do inicjalizacji klasy
- przestrzenne (bo w tej samej jednostce kompilacji) związanie zachowania z klasą (metody) sprawia, że łatwiej odnaleźć kod opisujący zachowanie i łatwiej ogarnąć zakres opowiedzialności klasy
“It is better to have 100 functions operate on one data structure than 10 functions on 10 data structures.”
Alan Perlis
Ta często cytowana wypowiedź Alana Perilsa z artykułu “Epigrams on Programming” (1982) - dotycząca języka Lisp - bywa przytaczana czasami jako argument za tym, że tworzenie wielu małych klasek o tym samym kształcie (a tym w zasadzie są rekordy) to mnożenie bytów ponad potrzebę.
Kto bawił się językami funkcyjnymi takimi jak Lisp czy Clojure, ten zna przyjemność operowania na jednej strukturze danych (liście czy mapie). Jednak kto pisze duże, złożone systemy, ten docenia statyczną typizację i zdaje sobie sprawę z tego, jak bardzo warto wykorzystać system komputerowy do zapewnienia poprawności kodu wobec zawodnej pamięci/wiedzy/motywacji programistów.
Rekord/klasa reprezentuje wprost semantykę pewnego bytu i pomaga budować w głowie model (zwykle mocno dynamiczny) programowanego systemu. Nawet Python posiadający od dawna heterogeniczne krotki wprowadził (w 2.6 i 3.0) “named tuples”, i choć pod spodem są one wciąż krotkami, porządkują one myślenie o kodzie i ułatwiają jego zrozumienie.
Mam nadzieję, że rekordy - dzięki swojej lekkości - przekonają programistów niechętnych tworzeniu nowych klas i z zasady będących gorącymi zwolennikami używania do wszystkiego typu String, że warto wprowadzać rozróżnenie między napisami, jeśli używa się ich w innych kontekstach bądź jeśli reprezentują inne byty.
Reflection API
W klasie java.lang.Class zostały dodane dwie metody, które informują o tym, czy klasa jest rekordem oraz zwracają listę komponentów:
- RecordComponent[] getRecordComponents()
- boolean isRecord()
Klasa java.lang.reflect.RecordComponent
jest też nowa i można z niej uzyskać informacje o nazwie, typie (być może generycznym) czy adnotacjach.
Przykład
Poniżej przykład użycia rekordu oraz uzyskania w runtimie informacji o nim.
Ten wpis jest częścią serii java.
- 2021-09-12 - Java 18: co nowego? - przegląd JEP-ów
- 2021-21-09 - Java 17 - RandomGenerator i spółka
- 2021-15-09 - Java 17 - co nowego?
- 2021-04-03 - Java 15 - czym są sealed classes?
- 2021-26-02 - Java 13 i 14: Bloki tekstowe i rekordy
- 2021-24-02 - Java 12 - wyrażenie switch (preview feature)
- 2021-23-02 - Java 11 - HTTP Client i uruchamianie jednoplikowych programów
- 2021-18-02 - Java 9 - co to jest JShell i dlaczego warto używać REPL-a w Javie
- 2021-12-02 - Java 10 - var, nowe metody w Optional, kolekcje "unmodifiable"
- 2021-11-02 - Java 9 - nowości w bibliotece
- 2021-10-02 - Java 9: czy mogę stworzyć z mojej aplikacji binarkę?
- 2021-10-02 - Java 9: praktyczny przykład - trzy moduły
- 2021-08-02 - Java 8: praktyczny przykład - przewidywanie kolejnej daty w serii