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!
- JEP 444: Virtual threads
- JEP 431: JEP 431: Sequenced Collections
- JEP 441: Pattern Matching dla switch
- JEP 440: Record Patterns
- JEP 430: String Templates (Wersja próbna)
- JEP 443: Unnamed Patterns and Variables (Wersja próbna)
- JEP 445: Unnamed Classes and Instance (Wersja próbna)
- JEP 446: Scoped Values (Wersja próbna)
- JEP 453: Structured Concurrency (Wersja próbna)
- Pozostałe zmiany
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.
//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ść.
@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:
- SequencedCollection: Definiuje metody takie jak
getFirst()
,getLast()
,addFirst(E)
,addLast(E)
,removeFirst()
,removeLast()
ireversed()
. - SequencedSet: Rozszerza
Set
iSequencedCollection
, dostarczając dodatkowe metody specyficzne dla zbiorów. - 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)
ireversed()
. - Metody pomocnicze w klasie Collections: Takie jak
unmodifiableSequencedCollection(SequencedCollection c)
,unmodifiableSequencedSet(SequencedSet s)
iunmodifiableSequencedMap(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ń.
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.
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:
Oczywiście można to wykorzystywać również w innych miejscach, takich jak lambda czy 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:
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.
Główne zmiany:
- Nie jest wymagana deklaracja klasy (czyli, żeby odpalić program w Java nie trzeba mieć metody
main
w pliku, który maclass
i deklaracje pakietu) - Słowa kluczowe Java
public
istatic
nie są już wymagane w metodziemain
- 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 processRequest
dla 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.
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.
Zostaw swój e-mail, a raz na jakiś czas wyśle Ci info o nowych postach na blogu. Tylko wartościowe e-mail!