Java 21 LTS: Virtual Threads, Record Patterns und mehr
Zurück zum Blog

Java 21 LTS: Virtual Threads, Record Patterns und mehr

7 Min. Lesezeit
Lesen auf English

Java 21 LTS: Virtual Threads, Record Patterns und mehr

Am 19. September hat Oracle Java 21 veröffentlicht -- und wir sind überzeugt: Das ist das wichtigste Java-Release seit mindestens einem Jahrzehnt. Java 21 ist das nächste Long-Term-Support-Release nach Java 17, das wir vor zwei Jahren in unserem Java-17-Artikel ausführlich vorgestellt haben. Der zweijährige LTS-Zyklus, der mit Java 17 (September 2021) begann, liefert damit pünktlich das nächste Release mit langfristiger Unterstützung.

Wir bei encircle360 haben Virtual Threads bereits seit der Preview in Java 19 intensiv getestet und in Prototypen eingesetzt. Dass sie jetzt produktionsreif sind, verändert die Spielregeln für Server-seitiges Java fundamental. Aber der Reihe nach.

Warum dieses LTS-Release besonders ist

Zwischen Java 17 und Java 21 liegen drei Jahre Feature-Entwicklung in den Zwischenreleases 18 bis 21. Für Teams, die auf LTS-Versionen setzen, ist der kumulative Funktionsumfang enorm. Aber ein Feature sticht heraus: Virtual Threads (JEP 444) sind nach zwei Preview-Runden endlich final. Kein --enable-preview mehr, keine Einschränkungen -- produktionsbereit und voll unterstützt.

Hinzu kommen finalisierte Features wie Record Patterns, Pattern Matching for switch und die neuen Sequenced Collections, die alle zusammen Java als Sprache spürbar modernisieren. Wer nach dem Umstieg auf Spring Boot 3 mit Java 17 als Baseline arbeitet, bekommt mit Java 21 den nächsten massiven Schub.

Virtual Threads: Das Concurrency-Modell neu gedacht

Virtual Threads (JEP 444) sind das Flaggschiff-Feature von Java 21 und der Grund, warum dieses Release die Java-Welt in Aufregung versetzt. Die Grundidee: Statt schwergewichtiger Platform Threads, die jeweils einen OS-Thread belegen, bietet die JVM jetzt leichtgewichtige Threads, die vom Runtime selbst verwaltet werden.

Das Ergebnis ist beeindruckend simpel in der Nutzung:

// Einen einzelnen Virtual Thread starten
Thread.startVirtualThread(() -> {
    System.out.println("Hallo aus einem Virtual Thread!");
});

// Mit dem neuen ExecutorService: ein Thread pro Task
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 10_000).forEach(i ->
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
}

Dieses Beispiel startet 10.000 gleichzeitige Tasks -- jeder mit eigenem Virtual Thread. Mit Platform Threads wäre das entweder unmöglich oder würde einen riesigen Thread-Pool erfordern. Virtual Threads verbrauchen nur wenige Kilobyte Speicher und werden automatisch auf eine kleine Anzahl von Carrier Threads gemappt.

Für typische Server-Anwendungen, die viel Zeit mit Warten auf I/O verbringen -- Datenbank-Queries, HTTP-Calls, Dateizugriffe --, bedeutet das einen dramatischen Anstieg der möglichen Nebenläufigkeit. Statt Thread-Pools zu tunen und über reaktive Frameworks nachzudenken, schreibt man einfach blockierenden Code und lässt die JVM die Skalierung übernehmen.

Was uns besonders freut: Spring Boot 3.2, das in wenigen Wochen erscheinen wird, bringt native Unterstützung für Virtual Threads mit. Eine einzige Property (spring.threads.virtual.enabled=true) reicht aus, um den eingebetteten Tomcat auf Virtual Threads umzustellen. Das Ökosystem zieht nach, und zwar schnell.

Record Patterns: Destrukturierung für Records

Record Patterns (JEP 440) erweitern das Pattern Matching, das mit instanceof in Java 16 begann und das wir im Java-17-Artikel vorgestellt haben, um die Fähigkeit, Records direkt in ihre Bestandteile zu zerlegen:

record Point(int x, int y) {}
record Line(Point start, Point end) {}

// Record Pattern in instanceof
if (obj instanceof Point(int x, int y)) {
    System.out.println("Punkt bei " + x + ", " + y);
}

// Verschachtelte Record Patterns
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
    double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
    System.out.println("Linienlänge: " + length);
}

Die verschachtelte Destrukturierung ist besonders mächtig: In einem einzigen Pattern-Match kann man durch mehrere Ebenen von Records navigieren und die Werte direkt als lokale Variablen verwenden. Das eliminiert nicht nur Boilerplate, sondern macht die Absicht des Codes deutlich klarer.

Pattern Matching for switch: Endlich final

Pattern Matching in switch-Ausdrücken (JEP 441) war seit Java 17 als Preview verfügbar und hat über vier Releases hinweg Feinschliff erhalten. In Java 21 ist es endlich finalisiert -- kein --enable-preview mehr nötig. In Kombination mit Record Patterns und Sealed Classes entsteht ausdrucksstarker, kompakter Code:

sealed interface Shape permits Circle, Rectangle {}
record Circle(double radius) implements Shape {}
record Rectangle(double width, double height) implements Shape {}

static String describe(Shape shape) {
    return switch (shape) {
        case Circle(double r) when r > 100 ->
            "Großer Kreis mit Radius " + r;
        case Circle(double r) ->
            "Kreis mit Radius " + r;
        case Rectangle(double w, double h) ->
            "Rechteck: " + w + " x " + h;
    };
}

Besonders hervorzuheben ist die when-Klausel (Guarded Patterns), die es erlaubt, zusätzliche Bedingungen an ein Pattern zu knüpfen. Der Compiler prüft nach wie vor die Vollständigkeit -- bei Sealed Classes ist kein default-Branch nötig. Das ist Pattern Matching, wie man es aus funktionalen Sprachen kennt, aber vollständig in Java integriert.

Sequenced Collections: Die Ordnung im Collection-Framework

Sequenced Collections (JEP 431) schließen eine seit Jahren bestehende Lücke im Java Collections Framework. Bisher gab es keine einheitliche Schnittstelle für Collections, die eine definierte Reihenfolge haben. List hat eine Reihenfolge, LinkedHashSet auch, SortedSet ebenfalls -- aber es gab kein gemeinsames Interface dafür.

Java 21 führt drei neue Interfaces ein: SequencedCollection, SequencedSet und SequencedMap.

// SequencedCollection: einheitlicher Zugriff auf erstes/letztes Element
SequencedCollection<String> names = new ArrayList<>(List.of("Anna", "Ben", "Clara"));

String first = names.getFirst();     // "Anna"
String last = names.getLast();       // "Clara"

names.addFirst("Zara");             // Zara wird vorne eingefügt
names.addLast("Daniel");            // Daniel wird hinten angefügt

// Umgekehrte Ansicht
SequencedCollection<String> reversed = names.reversed();
System.out.println(reversed.getFirst()); // "Daniel"

// Funktioniert genauso mit LinkedHashMap
SequencedMap<String, Integer> map = new LinkedHashMap<>();
map.put("a", 1);
map.put("b", 2);
map.put("c", 3);

var firstEntry = map.firstEntry();   // a=1
var lastEntry = map.lastEntry();     // c=3
var reversedMap = map.reversed();

Das klingt unspektakulär, löst aber ein reales Problem. Wie oft hat man list.get(list.size() - 1) geschrieben, um das letzte Element zu bekommen, oder sich mit ((TreeSet<T>) set).first() beholfen? Die neuen Interfaces sind sauber in die bestehende Hierarchie integriert: List erweitert jetzt SequencedCollection, SortedSet erweitert SequencedSet, und so weiter.

String Templates: Ein Ausblick (Preview)

Als Preview-Feature liefert Java 21 mit JEP 430 sogenannte String Templates -- eine typsichere Alternative zur String-Verkettung und zu String.format():

// Preview-Feature: --enable-preview erforderlich
String name = "encircle360";
int year = 2017;

String message = STR."Willkommen bei \{name}, gegründet \{year}!";
// -> "Willkommen bei encircle360, gegründet 2017!"

// Auch mit Ausdrücken
String info = STR."Die Firma ist \{2023 - year} Jahre alt.";

Der STR-Template-Processor verarbeitet die eingebetteten Ausdrücke zur Compile-Zeit typsicher. Das ist ein deutlicher Fortschritt gegenüber String.format(), wo Typ-Fehler erst zur Laufzeit auffallen. Als Preview muss das Feature noch mit --enable-preview aktiviert werden, aber die Richtung ist klar.

Unnamed Patterns und Variables (Preview)

Ein weiteres Preview-Feature (JEP 443) adressiert ein bekanntes Ärgernis: Variablen, die man deklarieren muss, aber nie benutzt. In Java 21 kann man dafür den Unterstrich verwenden:

// Nur der Name des Records interessiert, nicht die Details
if (obj instanceof Point(int x, _)) {
    System.out.println("x-Koordinate: " + x);
}

// In try-with-resources, wenn die Exception nicht genutzt wird
try {
    // ...
} catch (NumberFormatException _) {
    System.out.println("Ungültiges Zahlenformat");
}

Das verbessert die Lesbarkeit und signalisiert deutlich, welche Werte relevant sind und welche nicht.

Unsere Empfehlung: Jetzt migrieren

Wir bei encircle360 haben unsere internen Services in der Woche nach dem Release auf Java 21 aktualisiert. Die Migration von Java 17 ist unkompliziert -- deutlich einfacher als damals der Sprung von Java 11 auf 17. Die größten Gewinne liegen in den Virtual Threads: Für unsere I/O-lastigen Microservices konnten wir die Thread-Pool-Konfigurationen drastisch vereinfachen.

Ein pragmatischer Migrationsplan:

  1. JDK 21 installieren -- Eclipse Temurin oder Amazon Corretto bieten bereits fertige Builds
  2. Kompilieren und Tests laufen lassen -- die meisten Projekte bauen ohne Änderungen
  3. Source Level auf 21 setzen -- im Compiler-Plugin von Maven oder Gradle
  4. Virtual Threads evaluieren -- zunächst in nicht-kritischen Services mit Executors.newVirtualThreadPerTaskExecutor()
  5. Auf Spring Boot 3.2 warten -- das native Virtual-Thread-Support mitbringt und den Umstieg trivial macht

Java 21 ist nicht einfach nur das nächste LTS-Release. Es ist das Release, das die Antwort auf die Frage liefert, warum reaktive Frameworks für die meisten Anwendungen überflüssig werden könnten. Virtual Threads verändern das Concurrency-Modell, Record Patterns und Pattern Matching for switch modernisieren die Sprache, und Sequenced Collections räumen eine jahrzehntealte Designlücke auf. Das ist der größte Schritt, den Java in einem einzigen LTS-Release gemacht hat -- und wir sind begeistert.

Teilen

Patrick Hütter

Geschrieben von

Patrick Hütter

Gründer & Software-Architekt

Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.