Spis treści

Java 13 i 14: Bloki tekstowe i rekordy

Java 13 i 14 - bloki tekstowe i rekordy, grafika

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żenia switch) służącej do zwrócenia wartości z bloku case (wyrażenie switch 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:

1
2
3
4
5
6
7
String html = """
<html>
<body>
<p>Dziś oglądałam "Dzień świra". </p>
</body>
</html>
""";
  • 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:

  1. Końce wierszy są zamieniane na LF (\u000A)
  2. Usuwane są “przypadkowe spacje”, które zostały dodane do literału po to, by tekst lepiej pasował do wcięć kodu źródłowego.
  3. 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:

1
2
3
4
if (obj instanceof String) {
  String s = (String) obj;    // <- o, tutaj
...
}

a chcemy tego:

1
2
3
4
if (obj instanceof String s) {
  // tutaj obj został zrzutowany już na s i używamy s jak Stringa
...
}

Zakresem leksykalnym zmiennej s jest całość - być może złożonego - warunku (nawias z “ifem”) oraz następujący po nim blok:

1
2
3
4
if (obj instanceof String s && s.length() > 5) {
  flag = s.contains("jdk");
}

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ę:

1
2
3
if (obj instanceof String s || s.length() > 5) {    // Błąd!
  ...
}

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.

1
record Person(String name, int age){}

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.