Wydana we wrześniu 2021 Java 17 to następna, po Java 11, wersja z długoterminowym wsparciem (LTS). Czy migracja z Java 11 na 17 się opłaca? Jakie nowe funkcjonalności uzyskasz po migracji na Java 17? Wybrałem pięć najciekawszych nowości dla programisty(-ki), obok których nie sposób przejść obojętnie.
Wraz z wydaniem Java 9 we wrześniu 2017 roku, Java przeszła na nowy model wydawniczy. w tej chwili co pół roku możemy liczyć na nową wersję Javy, a co trzy lata na wersję z długoterminowym wsparciem (LTS). We wrześniu 2021 odbyła się premiera Java 17, która jest następną po Java 11 wersją LTS. Oto, co się zmieniło.
Records – modeluj dane jako dane
Java bywa krytykowana za nadmiarowość kodu, który trzeba napisać choćby dla prostych konstrukcji. Przykładowo, tworząc niemutowalny obiekt w Javie (na przykład DTO), oprócz napisania konstruktora i getterów dla wszystkich pola, trzeba pamiętać też o poprawnej implementacji metod hashCode i equals, a najlepiej także i toString. Dla klasy z dwoma polami jest to ponad czterdzieści linii kodu. Dotychczasowe sposoby na radzenie sobie z tym problemem to generowanie kodu albo na poziomie IDE, albo w trakcie kompilowania do bytecode’u (Lombok). Oba rozwiązania jedynie maskują problem, ale go nie rozwiązują. Dodatkowo, programiści i programistki, idąc czasem „na skróty”, pomijają implementowanie hashCode/equals albo używają innych struktur danych (na przykład mapy), aby uniknąć tworzenia klas.
W Java 14 po raz pierwszy wprowadzono nowy typ klasy o nazwie record, który w ostatecznej wersji został zaimplementowany w Java 16. Głównym celem dodania rekordów nie było jednak wcale usunięcie wspomnianej wcześniej nadmiarowości, a raczej chęć udostępnienia programist(k)om czytelnego sposobu reprezentacji danych. Brian Goetz – jeden z architektów Java – proponuje myśleć o rekordach jako o „nominal tuples”, czyli jak o nazwanych krotkach. Dzięki rekordom programist(k)a może w łatwy i spójny sposób zadeklarować klasę służącą jako nośnik niezmiennych danych.
Przykładowa deklaracja rekordów opisujących osobę oraz adres:
record Person(String firstName, String lastName, Address address) {} record Address(String street, String houseNo, String city, String zipCode) {}Rekord można utworzyć także lokalnie, wewnątrz metody. Przydaje się to szczególnie w przypadku Streams API, ponieważ możemy zgrupować i nazwać zmienne, reprezentujące pośrednie wyniki przetwarzania.
Migracja z Java 11 na 17: Switch Expressions
Kolejną nowością, pozytywnie wpływającą na czytelność kodu, są zmiany związane z instrukcją warunkową switch-case. Dotychczasowa implementacja naśladowała składnie z języków C czy C++, co sprawdzało się przy implementacji niskopoziomowych rozwiązań. Jednak w przypadku aplikacji biznesowych, obecna składnia prowadziła do powstawania nieczytelnego i podatnego na błędy kodu. Wpływ na to miała między innymi specyfika kaskadowego wykonywania kolejnych wariantów (fallthrough). jeżeli dany wariant (case) nie był poprawnie zakończony słowem kluczowym break lub return, wykonywane były kolejne instrukcje, często niezgodnie z intencją programisty(-ki). Zaczynając od Java 14, instrukcję switch-case można używać nie tylko w postaci deklaracji (statement), ale również w postaci wyrażenia (expression).
Przykładowo, poszczególne sekcje case mogą być zapisane w istniejącej dotychczas postaci deklaracji (statement) z fallthrough:
String getCardSuitColor(CardSuits cardSuit) { switch (cardSuit) { case HEARTS: case DIAMONDS: return "Red"; case SPADES: case CLUBS: return "Black"; default: throw new IllegalCardSuitException(cardSuit); } }Albo korzystając z nowej składni jako wyrażenia (expression), bez fallthrough:
String getCardSuitColor(CardSuits cardSuit) { return switch (cardSuit) { case HEARTS, DIAMONDS -> "Red"; case SPADES, CLUBS -> "Black"; }; }Warto zauważyć, iż w przypadku typu wyliczeniowego (enum) CardSuits, kompilator „zna” wszystkie wartości i jest w stanie sprawdzić, czy instrukcja switch-case obejmuje wszystkie przypadki. Pozwala to uniknąć sytuacji, w której programist(k)a pomija jedną z wartości, a tym samym sekcja default staje się zbędna.
Sealed Classes – hierarchia typów pod kontrolą
Wraz z wydaniem JDK 17 w Javie pojawiły się tzw. sealed classes, co na język polski można by przetłumaczyć jako typy zapieczętowane. W skrócie, sealed classes umożliwiają programiście(-stce) kontrolę nad hierarchią typów i jawne zadeklarowanie listy podtypów, jakie może mieć dany interfejs czy klasa. Zamknięta lista podtypów to bardzo istotna informacja nie tylko dla osoby czytającej kod, ale również dla kompilatora. W ten sposób już na etapie kompilacji dostaniemy informację na przykład o pominięciu jednego z możliwych przypadków dla wyrażenia switch-case. Tak jak w przypadku typów wyliczeniowych (enum) definiujemy zestaw dopuszczalnych wartości, tak korzystając z sealed classes, możemy zdefiniować zestaw dopuszczalnych typów wartości (klas). Może to być szczególnie użyteczne przy modelowaniu domeny lub gdy tworzymy bibliotekę, którą chcemy później udostępnić użytkownikom.
Typ zapieczętowany może być rozszerzony tylko przez jawnie wskazaną klasę lub interfejs. Służy do tego słowo najważniejsze „permits”. Dodatkowo, do obsługi sealed classes dodane zostały dwa słowa kluczowe: sealed oraz non-sealed, które zamykają lub otwierają hierarchię typów na dalsze rozszerzanie.
public sealed interface ShoppingBasket permits StandardBasket, DiscountBasket {...} public final class StandardBasket implements ShoppingBasket {...} public non-sealed class DiscountBasket implements ShoppingBasket {...} public class LimitedDiscountBasket extends DiscountBasket {}Powyższy przykład „zapieczętowanej” hierarchii typów dla koszyka sklepowego, dopuszcza dwa podtypy koszyka: standardowy (StandardBasket) oraz ze zniżką (DiscountBasket). Koszyk standardowy nie może być już rozszerzony, ponieważ klasa została zadeklarowana jako final. Natomiast dla koszyka ze zniżką hierarchia jest otwarta na dalsze rozszerzanie poprzez użycie słowa non-sealed.
Dopasowanie wzorców w instanceof
Drobnym, ale użytecznym rozszerzeniem, które pojawiło się w JDK 16, jest pattern matching dla operatora instanceof. Co prawda, w językach obiektowych używanie instanceof traktowane jest jako code smell. jeżeli istnieje potrzeba zróżnicowania przetwarzania w zależności od typu obiektu, wykorzystuje się polimorfizm lub na przykład wzorzec Visitor. Czasami jednak nie mamy wpływu na hierarchię typów, nie możemy jej rozszerzyć lub zmodyfikować, bo na przykład została nam dostarczona w postaci zewnętrznej biblioteki.
Dotychczas typowy przykład użycia operatora instanceof wyglądał następująco:
if (obj instanceof String) { String s = (String) obj; System.out.println(s); ... }Po zmianach wprowadzonych w JDK 16, możemy teraz również napisać:
if (obj instanceof String s) { System.out.println(s); ... }Jak możecie zauważyć, Java w coraz szerszym zakresie stosuje pattern matching. Zmiana w powyższym wywołaniu instanceof polega na możliwości podania wzorca typu (String s) zamiast samego typu (String). Wzorzec typu (type pattern) w tym przypadku to kombinacja predykatu określającego typ (String) oraz pojedynczej zmiennej (s).
Migracja z Java 11 na 17: bloki tekstowe
Ostatnią, ale równie użyteczną nowością, jest wprowadzenie obsługi bloków tekstowych. Blok tekstowy pozwala użyć wielolinijkowego napisu bez konieczności unikania lub dodawania specjalnej obsługi znaków specjalnych. Przykładowo przypisanie bloku zawierającego HTML, XML, JSON lub SQL do zmiennej wymagało dotychczas mozolnego wstawania ukośników, znaków końca linii i innych znaków specjalnych, a efekt końcowy był niezbyt czytelny. Co prawda, edytowanie można było sobie ułatwić, korzystając z funkcjonalności dostarczanej przez niektóre środowiska programistyczne (IDE), ale niestety nie wpływało to na czytelność.
Oto przykład zapisu JSONa bez wykorzystania bloku tekstowego:
var json = "{\n" + " \"order\": {\n" + " \"product\": \"ABC\",\n" + " \"quantity\": \"10\",\n" + " \"price\": \"25.0\"\n" + " }\n" + "}";Oraz jako blok tekstowy:
var jsonMultiline = """ { "order": { "product": "ABC", "quantity": "10", "price": "25.0" } }""";Oprócz powyższych zmian, przez trzy lata wprowadzono wiele innych usprawnień nie tylko w samym języku, ale także w maszynie wirtualnej Hotspot VM, bibliotekach czy narzędziach z pakietu JDK. Migracja z wersji 11 na 17 może więc nie tylko pozytywnie wpłynąć na kod aplikacji, ale również na jej wydajność. A Tobie które nowinki najbardziej przypadły do gustu?