Spis treści

Co nowego w Javie 21?

Przegląd najważniejszych zmian w języku i bibliotece

Java 21

To jest prawdziwa bomba! Java 21 to już zupełnie inny język. Zmiany, które pojawią się w wydaniu planowanym na wrzesień 2023, można z grubsza podzielić na dwie kategorie:

Zmiany w języku i bibliotekach

Oto najważniejsze i - moim zdaniem - najciekawsze zmiany, jakie czekają nas w Javie 21. Nie są one rewolucyjne, ale sprawiają, że pisanie kodu - oraz, co jest nawet dużo ważniejsze, jego czytanie - stanie się dużo łatwiejsze.

Pozostałe zmiany

To zmiany niskopoziomowe, ważne dla twórców bibliotek (API wektorowe, FFI, kryptografia) bądź związane z “oczyszczaniem” kodu. Są pewnie mniej interesujące dla typowego programisty, choć warto zajrzeć choćby do opisów w JEP–ach, aby z grubsza orientować się, czego dotyczą

Po kolei

Kolekcje sekwencyjne

Wprowadzone zostały nowe interfejsy, które poprawnie zamodelują porządek napotykania elementów. Dotychczasowe API było dość chaotyczne: pobieranie pierwszego bądź ostatniego elementu z różnych kolekcjach miało różne API:

Struktura danych First element Last element
List list.get(0) list.get(list.size() - 1)
Deque deque.getFirst() deque.getLast()
SortedSet ss.first() ss.last()
LinkedHashSet lhs.iterator().next() // missing

Dwa nowe interfejsy to:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
interface SequencedCollection<E> extends Collection<E> {
    // new method
    SequencedCollection<E> reversed();
    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

oraz

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

Wzorce dla rekordów

Pattern matching w Javie. Będzie można w końcu napisać tak:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// As of Java 21
record MyPair<S,T>(S fst, T snd){};

static void recordInference(MyPair<String, Integer> pair){
    switch (pair) {
        case MyPair(var f, var s) -> 
            ... // Inferred record pattern MyPair<String,Integer>(var f, var s)
        ...
    }
}

Wzorce w ‘switch’

Wyrażenie wybierające gałąź nie musi być typu Enum, a w odnogach case można używać kwalifikowanych nazw wartości enumeracyjnych:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// As of Java 21
sealed interface CardClassification permits Suit, Tarot {}
public enum Suit implements CardClassification { CLUBS, DIAMONDS, HEARTS, SPADES }
final class Tarot implements CardClassification {}

static void exhaustiveSwitchWithBetterEnumSupport(CardClassification c) {
    switch (c) {
        case Suit.CLUBS -> {
            System.out.println("It's clubs");
        }
        case Suit.DIAMONDS -> {
            System.out.println("It's diamonds");
        }
        case Suit.HEARTS -> {
            System.out.println("It's hearts");
        }
        case Suit.SPADES -> {
            System.out.println("It's spades");
        }
        case Tarot t -> {
            System.out.println("It's a tarot");
        }
    }
}

Wątki wirtualne

Jak je utworzyć:

Jakie mają cechy

  • są zawsze demonowe (Thread.setDaemon(false) jest noop)
  • mają ustalony i niezmienialny priorytet
  • nie można przypisać ich do grupy wątków
  • publiczny konstruktor zawsze tworzy wątki platformowe;

Dodatkowe API

  • Thread.isVirtual() - można sprawdzić, z jakim wątkiem mamy do czynienia
  • Thread.getAllStackTraces() zwraca mapę wątków platformowych (a nie wszystkoch)

Wzorce w napisach

To więcej, niż interpolacja napisów w innych językach. Ładne porównanie sposobów w JEP 430:

Language Code
C# $"{x} plus {y} equals {x + y}"
Visual Basic $"{x} plus {y} equals {x + y}"
Python f"{x} plus {y} equals {x + y}"
Scala s"$x plus $y equals ${x + y}"
Groovy "$x plus $y equals ${x + y}"
Kotlin "$x plus $y equals ${x + y}"
JavaScript `${x} plus ${y} equals ${x + y}`
Ruby "#{x} plus #{y} equals #{x + y}"
Swift "\(x) plus \(y) equals \(x + y)"

W Javie będzie tak:

1
2
3
4
5
6
String firstName = "Bill";
String lastName  = "Duck";
String fullName  = STR."\{firstName} \{lastName}";
| "Bill Duck"
String sortName  = STR."\{lastName}, \{firstName}";
| "Duck, Bill"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// html
tring title = "My Web Page";
String text  = "Hello, world";
String html = STR."""
        <html>
          <head>
            <title>\{title}</title>
          </head>
          <body>
            <p>\{text}</p>
          </body>
        </html>
        """;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// json
String name    = "Joan Smith";
String phone   = "555-123-4567";
String address = "1 Maple Drive, Anytown";
String json = STR."""
    {
        "name":    "\{name}",
        "phone":   "\{phone}",
        "address": "\{address}"
    }
    """;

Structured concurrency

Główna klasa: StructuredTaskScope

Pozwala nadać strukturę zadaniu złożonemu z wielu zależnych, współbieżnych pod-zadań (funckja fork()) i koordynować je: możemy użyć join() dla całej grupy wyforkowanych z głównego zadania podzadań, możemy też użyć nowego API joinUntil(java.time.Instant); mamy też możliwość anulowania shutdown() wszystkich podzadań na raz.

Zalety: lepsza obsługa błędów, propagowanie anulowania wątków (ale wciąż bez jawnego kontekstu), czytelna struktura kodu, czytelna struktura wątków w thread dump-ach:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Supplier<String>  user  = scope.fork(() -> findUser());
        Supplier<Integer> order = scope.fork(() -> fetchOrder());

        scope.join()            // Join both subtasks
             .throwIfFailed();  // ... and propagate errors

        // Here, both subtasks have succeeded, so compose their results
        return new Response(user.get(), order.get());
    }
}

Wzorce i zmienne nienazwane

Underscore tam, gdzie nie potrzebujemy wartości:

W switch:

1
2
3
4
    case Box(RedBall _), Box(BlueBall _) -> processBox(b);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();
}

W for:

1
2
3
4
5
6
int acc = 0;
for (Order _ : orders) {
    if (acc < LIMIT) { 
        ... acc++ ...
    }
}

…czy w blokach catch:

1
2
3
4
5
6
7
String s = ...
try { 
    int i = Integer.parseInt(s);
    ... i ...
} catch (NumberFormatException _) { 
    System.out.println("Bad number: " + s);
}

Klasy nienazwane

… to ułatwienie w napisaniu ‘hello, world’. Poprawnym programem w Javie będzie taki kod:

1
2
3
void main() {
    System.out.println("Hello, World!");
}

Plan

W kolejnych wpisach sprawdzę dokładniej czego te zmiany dotyczą, opiszę je oraz użyję ich w prostych programach. Może uda mi się zrobić to przed GA?

Happy coding!

Obrazki

Obrazki wygenerowane przez AI w https://creator.nightcafe.studio/