Java 21: Nowości z przykładami

19 września 2023 roku została wydana Java 21, która jest kolejną wersją z długoterminowym wsparciem (LTS). W Javie 21 pojawiło się 15 nowości, a każdą z nich przedstawimy w tym artykule. Niemniej jednak skupię się na tych, które faktycznie będziemy mogli użyć w kodziku. Jeśli przykłady z artykułu są dla Ciebie niewystarczające, to na końcu wpisu odnajdziesz link do GitHuba, na którym zaimplementowane są nowości z Java 21. Miłej lektury!

  1. JEP 444: Virtual threads
  2. JEP 431: JEP 431: Sequenced Collections
  3. JEP 441: Pattern Matching dla switch
  4. JEP 440: Record Patterns
  5. JEP 430: String Templates (Wersja próbna)
  6. JEP 443: Unnamed Patterns and Variables (Wersja próbna)
  7. JEP 445: Unnamed Classes and Instance (Wersja próbna)
  8. JEP 446: Scoped Values (Wersja próbna)
  9. JEP 453: Structured Concurrency (Wersja próbna)
  10. Pozostałe zmiany
Jeśli wolisz formę wideo 😀

1. JEP 444: Virtual threads

Jedną z głównych nowości w Java 21 są wirtualne wątki – lekkie konstrukcje, mające na celu ułatwienie pisania, utrzymania oraz debugowania aplikacji wielowątkowych o wysokiej przepustowości. Wirtualne wątki są instancjami klasy java.lang.Thread, ale w przeciwieństwie do tradycyjnych wątków platformy, nie są one bezpośrednio powiązane z wątkami systemu operacyjnego. Oznacza to, że Java runtime może efektywnie zarządzać dużą liczbą wirtualnych wątków, mapując je na mniejszą liczbę wątków systemu operacyjnego. System operacyjny decyduje, kiedy uruchomić wątek platformy, natomiast Java runtime decyduje, kiedy uruchomić wątek wirtualny. Dzięki czemu możemy stworzyć miliony wątków wirtualnych w aplikacji, niezależnie od liczby wątków platformy. To umożliwia aplikacją serwerowym przetwarzanie znacznie większej liczby żądań jednocześnie, co prowadzi do wyższej przepustowości i mniejszego marnotrawstwa sprzętu.

Java 21 Wirtualne wątki
        //wykonaj w wątku tradycyjnym
        Thread.ofPlatform().start(runnable);

        //wykonaj w wątku wirtualnym
        Thread.ofVirtual().start(runnable);

        //wykonaj w wątku wirtualnym inny sposób
        Thread.startVirtualThread(runnable);

Korzystając z klasy Executors, nie musimy już jawnie deklarować, ile chcemy utworzyć naszych cennych wątków platformowych:

Executors.newFixedThreadPool(10);

Zamiast tego, możemy skorzystać z lekkich wirtualnych wątków, które są tworzone dla każdej operacji:

Executors.newVirtualThreadPerTaskExecutor();

Podsumowując wirtualne wątki sprawdzą się głównie w aplikacjach serwerowych dla zadań, które spędzają większość czasu na oczekiwaniu I/O, takie jak np. pobieranie zasobów. Podczas pobierania zasobów w klasycznym podejściu, wątek platformowy rezerwuje wątek systemowy i czeka, aż zasób się pobierze, marnując tym samym czas wątku systemowego. Natomiast, korzystając z wątków wirtualnych, w czasie tego oczekiwania można stworzyć inny wątek wirtualny, który będzie wykonywał inną operację, co zwiększa ilość wykonanych operacji w tym samym czasie przy użyciu tej samej liczby wątków systemowych. Warto dodać, że wirtualne wątki nie są szybsze niż wątki platformowe, chodzi o skalę, a nie prędkość.

2. JEP 431: Sequenced Collections

Od wersji Java 21 możemy korzystać z kolejnej nowości – sekwencyjnych kolekcji. Są to specjalne typy kolekcji, które ułatwiają dostęp do pierwszego i ostatniego elementu, a także pozwalają na przetwarzanie elementów w odwrotnej kolejności. Wprowadzenie tego typu kolekcji ma na celu ułatwienie pracy z danymi, które mają zdefiniowaną kolejność.

Java 21 Sequenced Collections
https://cr.openjdk.org/~smarks/collections/SequencedCollectionDiagram20220216.png
    @Test
    void sequencedCollectionsList() {
        //when
        List<String> list = List.of("A", "B", "C");
        //then
        Assertions.assertEquals("A", list.getFirst());
        Assertions.assertEquals("C", list.getLast());
    }

    @Test
    void sequencedCollectionsSet() {
        //when
        LinkedHashSet<String> set = new LinkedHashSet<>(List.of("A", "B", "C"));
        //then
        Assertions.assertEquals("A", set.getFirst());
        Assertions.assertEquals("C", set.getLast());
    }

    @Test
    void sequencedCollectionsMap() {
        //when
        LinkedHashMap<String, String> linkedHashMap = new LinkedHashMap<>();
        linkedHashMap.put("K1", "V1");
        linkedHashMap.put("K2", "V2");
        linkedHashMap.put("K3", "V3");

        //then
        Assertions.assertEquals("K1", linkedHashMap.firstEntry().getKey());
        Assertions.assertEquals("V3", linkedHashMap.lastEntry().getValue());
    }


Dostępne metody:

  1. SequencedCollection: Definiuje metody takie jak getFirst(), getLast(), addFirst(E), addLast(E), removeFirst(), removeLast() i reversed().
  2. SequencedSet: Rozszerza Set i SequencedCollection, dostarczając dodatkowe metody specyficzne dla zbiorów.
  3. SequencedMap: Nowy interfejs dla map, które mają zdefiniowaną kolejność elementów. Zawiera metody takie jak firstEntry(), lastEntry(), pollFirstEntry(), pollLastEntry(), putFirst(K, V), putLast(K, V) i reversed().
  4. Metody pomocnicze w klasie Collections: Takie jak unmodifiableSequencedCollection(SequencedCollection c), unmodifiableSequencedSet(SequencedSet s) i unmodifiableSequencedMap(SequencedMap m), które zwracają niemodyfikowalne widoki na sekwencyjne kolekcje.

3. JEP 441: Pattern Matching dla switch

Pattern Matching dla switch zostało po raz pierwszy wprowadzone w Java 17 jako funkcja podglądowa pozwalająca formułować instrukcje i wyrażenia switch dla dowolnego obiektu. Przykład:

  //Przed Java 21
    public String formatter(Object obj) {
        String formatted = "unknown";
        if (obj instanceof Integer i) {
            formatted = String.format("int %d", i);
        } else if (obj instanceof Long l) {
            formatted = String.format("long %d", l);
        } else if (obj instanceof Double d) {
            formatted = String.format("double %f", d);
        } else if (obj instanceof String s) {
            formatted = String.format("String %s", s);
        }
        return formatted;
    }
    //Java 21
    public String formatterPatternSwitch(Object obj) {
        return switch (obj) {
            case Integer i -> String.format("int %d", i);
            case Long l    -> String.format("long %d", l);
            case Double d  -> String.format("double %f", d);
            case String s  -> String.format("String %s", s);
            default        -> obj.toString();
        };
    }

Pamiętajmy, że switch musi obejmować wszystkie przypadki, dlatego dla klasy Object konieczne jest użycie default. Możemy jednak stworzyć interfejs i używać go w switch, a dla każdej implementacji wykonywać konkretną operację. Wtedy default nie będzie potrzebny 🙂

Dodatkowo w Java 21 wprowadzono klauzulę when w instrukcji switch. Dzięki niej możliwe jest dodanie dodatkowych warunków do etykiet case, co ułatwia i upraszcza złożone instrukcje warunkowe. Na przykład, zamiast stosować zagnieżdżone instrukcje if wewnątrz case, możemy użyć when do określenia warunku.

4. JEP 440: Record Patterns

Record Patterns zostały wprowadzone po raz pierwszy w Javie 19 jako funkcja w wersji próbnej. Mogą być one łączone z dopasowywaniem wzorców dla instanceof oraz dopasowywaniem wzorców dla switch, aby uzyskać dostęp do pól rekordu bez jawnych rzutowań i bez użycia metod dostępowych.

Od Java 16 mamy możliwość korzystania z automatycznego rzutownia podczas instanceof. Taki kod jest bardziej zwięzły i czytelny, a także bezpieczniejszy, ponieważ eliminuje potrzebę jawnych rzutowań.

Automatyczne rzutowanie dostępne od Java 16

Record patterns w Java 21 dają nam jeszcze możliwości, bezpośredniego dostępu do pól rekordów bez potrzeby używania metod dostępowych, deklarując je od razu przy robieniu instanceof.

Dla chętnych bardziej rozbudowany przykład :D.
Zacznijmy od prostego rekordu (record dostępny jest w Java od wersji 14):

    public record Discount(String type, int percent) {}

    public record Reward(String type, String name) {}

Załóżmy teraz, że mamy dowolny obiekt i chcemy wykonać określoną akcję w zależności od jego klasy – na przykład wypisać coś na konsoli.

    //Java 16
    public void processRewardOrDiscount16(Object obj) {
        if (obj instanceof Discount discount) {
            String type = discount.type();
            int percent = discount.percent();
            printDiscount(type, percent);
        } else if (obj instanceof Reward reward) {
            String type = reward.type();
            String name = reward.name();
            printReward(type, name);
        } else {
            printUnknownType();
        }
    }
    //Java 21
    public void processRewardOrDiscount21(Object obj) {
        if (obj instanceof Discount(String type, int percent)) { //od razu deklaracja pól
            printDiscount(type, percent);
        } else if (obj instanceof Reward(String type, String name)) {
            printReward(type, name);
        } else {
            printUnknownType();
        }
    }

Oczywiście możemy wykorzystać Pattern Matching razem z Record patterns dla switch:

      //Java 21
    public void patternMatchingWithRecordPattern(Object obj) {
        switch (obj) {
            case String s when s.length() > 7 -> System.out.println(s.toUpperCase());
            case String s -> System.out.println(s.toLowerCase());
            case Discount(String type, int percent) -> printDiscount(type, percent);
            case Reward(String type, String name) -> printReward(type, name);
            default -> printUnknownType();
        }
    }

5. JEP 430: String Templates (Wersja próbna)

Kolejna z nowości od Java 21 to String Templates. Funkcja ta jest w wersji próbnej i pozwala na tworzenie szablonów tekstowych z osadzonymi wyrażeniami, ewaluowane w trakcie działania programu. Szablony te mogą zawierać zmienne, metody lub pola, które można potem renderować w ciąg tekstowy.

Składnia wyrażenia szablonowego jest dość podobna do składni w TypeScript.

W TypeScript mamy {{ name }}, a w Java \{ name }.

Korzystać z tej funkcji w Java możemy za pomocą procesora szablonówSTR. Pozwala on wygenerować ciąg tekstowy zastępując każde osadzone wyrażenie w szablonie wartością tego wyrażenia.

String Templates

Oczywiście efektem powyższego kodu będzie pojawienie się na konsoli Siema Marek!.

6. JEP 443: Unnamed Patterns and Variables (Wersja próbna)

W innych językach programowania, takich jak Scala czy Python, istnieje możliwość pominięcia nadawania nazwy zmiennej, której nie zamierzamy używać w przyszłości. Teraz, począwszy od Java 21, też możemy to zrobić za pomocą znaku podkreślenia (_). Przykładowo:

Unnamed Patterns w catch

Oczywiście można to wykorzystywać również w innych miejscach, takich jak lambda czy switch.

Unnamed Patterns w lambda
Unnamed Patterns w switch

Ciekawą opcją wydaje się użycie w instrukcji switch rekordów w połączeniu z nowymi funkcjami, takimi jak Pattern Matching i Unnamed Patterns:

Unnamed Patterns dla record w switch

7. JEP 445: Unnamed Classes and Instance (Wersja próbna)

Często krytykowano Javę za wymaganie rozbudowanego kodu nawet do napisania prostego programu „Hello World”. Od Java 21 możemy używać nieoznaczonych klas i metod głównych z uproszczoną składnią. Ta zmiana przyniesie korzyści przede wszystkim początkującym, którzy dopiero zaczynają uczyć się Javy.

Metoda main przed Java 21
Metoda main od Java 21

Główne zmiany:

  • Nie jest wymagana deklaracja klasy (czyli, żeby odpalić program w Java nie trzeba mieć metody main w pliku, który ma class i deklaracje pakietu)
  • Słowa kluczowe Java public i static nie są już wymagane w metodzie main
  • Argument metody głównej args jest opcjonalny

8. JEP 446: Scoped Values (Wersja próbna)

Scoped Values pozwala na dzieleniem danych między różnymi częściami aplikacji działającej w obrębie pojedynczego wątku. W przeszłości programiści często polegali na zmiennych ThreadLocal do osiągnięcia tego celu. Jednakże, z wprowadzeniem wirtualnych wątków i potrzebą skalowalności, dzielenie danych za pomocą zmiennych ThreadLocal nie zawsze jest efektywne. Tutaj właśnie pojawiają się Scoped Values.

Scoped Values zachowują się jak dodatkowy parametr dla każdej metody w sekwencji wywołań, ale żadna z metod faktycznie nie deklaruje tego parametru. Tylko metody, które mają dostęp do obiektu ScopedValue, mogą pobrać jego wartość, reprezentującą przekazywane dane. ScopedValue to wartość ustalona jednokrotnie i następnie dostępna do odczytu w określonym zakresie. Wartość parametru ScopedValue może być dziedziczona przez wątki utworzone w ramach danego zakresu. Aby to osiągnąć, można użyć StructuredTaskScope, który jest opisany w następnej sekcji 🙂

Scoped Values są niemutowalne i mają jasno określony i dobrze zdefiniowany czas życia, co zwiększa bezpieczeństwo.

    private static final ScopedValue<String> USER_SESSION = ScopedValue.newInstance();

    void runSomeProcess() {
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            executor.submit(() -> processRequest("User1_Session"));
            executor.submit(() -> processRequest("User2_Session"));
        }
    }

    void processRequest(String sessionId) {
        ScopedValue.runWhere(USER_SESSION, sessionId, () -> {
            System.out.println("Start procesowania dla: " + USER_SESSION.get());
            performSomeOperation();
        });
    }

    void performSomeOperation() {
        System.out.println("Dodatkowa operacja dla:  " + USER_SESSION.get());
    }

Wartość dla USER_SESSION jest ustawiana w metodzie processRequestdla danego zakresu. W tym zakresie wartość ta pozostaje stała i inne metody mogą ją bezpiecznie pobierać bez potrzeby jawnego przekazywania.

Metoda runWhere wymaga określonej wartości i obiektu USER_SESSION, do którego ma być przypisana, następnie wartość jest przypisywana do bieżącego wątku i wykonuje się na nim metoda run wyknujaca wyrażenie lambda. Wszystkie metody wywoływane w ramach tego wyrażenia bezpośrednio lub pośrednio mają dostęp do wartości USER_SESSION. Na przykład, metoda performSomeOperation,wyświetli na konsoli wartość USER_SESSION dla tego zakresu w jakim została wywołana. Po zakończeniu działania metody run, przypisanie jest usuwane.

9. JEP 453: Structured Concurrency (Wersja próbna)

Structured Concurrency to nowa funkcja w Javie 21, która ma na celu uproszczenie programowania współbieżnego poprzez traktowanie wielu wątków jako jednej jednostki pracy. Wątki te są tworzone z tego samego wątku nadrzędnego, co ułatwia zarządzanie nimi, anulowanie i obsługę błędów.

StructuredTaskScope oferuje dwie statyczne klasy wewnętrzne: ShutdownOnFailure i ShutdownOnSuccess.

ShutdownOnSuccess przechwytuje pierwszy wynik i zamyka zakres zadania, przerywając niedokończone wątki i budząc wątek główny (zadanie nadrzędne).

ShutdownOnFailure monitoruje wykonanie wszystkich podzadań i w przypadku niepowodzenia któregokolwiek z nich, przerywa wykonanie pozostałych, jeszcze niedokończonych podzadań.

Każde wywołanie scope.fork() uruchamia nowy wątek do wykonania podzadania, który domyślnie jest wirtualnym wątkiem.

Przykład użycia StructuredTaskScope.ShutdownOnFailure

10. Pozostałe zmiany

JEP 451: Prepare to Disallow the Dynamic Loading of Agents

Java zaczyna ostrzegać użytkowników przed dynamicznym ładowaniem agentów, które mogą modyfikować kod aplikacji podczas jej działania. Narzędzia diagnostyczne często to wykorzystują, ale może to także stanowić ryzyko dla bezpieczeństwa. W przyszłości Java planuje całkowicie zabronić tej praktyki.

JEP 452: Key Encapsulation Mechanism API

API umożliwiające bezpieczne szyfrowanie kluczy symetrycznych za pomocą kryptografii asymetrycznej to kolejna z nowości w Java 21, co ma kluczowe znaczenie w obronie przed cyberatakami i prawdopodobnie stanie się częścią nowej generacji standardów kryptograficznych.

JEP 439: Generational ZGC

Optymalizacji procesu garbage collector. Pozwala częściej zbierać „młode” obiekty redukując obciążenie CPU i pamięci sterty.

JEP 442: Foreign Function and Memory API (Wersja próbna)

API dla funkcji i pamięci zewnętrznej umożliwia bezpieczną interakcję aplikacji Java z kodem i danymi poza środowiskiem uruchomieniowym Javy. Jest to trzecia wersja podglądowa tego API.

JEP 448: Vector API (Wersja próbna)

API wektorowe zwiększa wydajność obliczeń wektorowych, które są kompilowane w czasie wykonywania do optymalnych instrukcji wektorowych. Jest to szósta wersja inkubacyjna tego API.

JEP 449: Deprecate the Windows 32-bit x86 Port for Removal

Java planuje usunąć wsparcie dla 32-bitowej wersji Windows z powodu zakończenia wsparcia dla Windows 10 i braku korzyści z użycia wirtualnych wątków na tej platformie.

Bądź na bieżąco!

Zostaw swój e-mail, a raz na jakiś czas wyśle Ci info o nowych postach na blogu. Tylko wartościowe e-mail!


Logo github

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Zacznij wpisywać wyszukiwane hasło powyżej i naciśnij Enter, aby wyszukać. Naciśnij ESC, aby anulować.

Do góry