This article is also available in English

Es ist wieder so weit, am 19.9. hat mit JDK 21 das nächste Long-term Support (LTS) Release nach JDK 17 das Licht der Welt erblickt. Mit diesem werden nun auch die Features und Änderungen aus JDK 18, JDK 19 und JDK 20 vermehrt Einzug in unsere Anwendungen finden.

Doch Moment, wieso kommt jetzt bereits nach zwei Jahren wieder ein LTS-Release heraus, waren nicht alle drei Jahre geplant? Ja, das war der Plan, bis Oracle mit dem Erscheinen von JDK 17 vorschlug, den Zeitraum auf zwei Jahre zu verkürzen. Dadurch, dass auch alle anderen relevanten Hersteller diesem Vorschlag zustimmten beziehungsweise ihm folgen, haben wir nun schon bereits nach zwei Jahren ein neues Release mit mindestens fünf Jahren Support, auch wenn es in zwei Jahren mit JDK 25 eine neue Version geben wird.

Zwar ist es auch möglich, jedes halbe Jahr auf die neueste Version vom JDK zu updaten, oft ist es jedoch sinnvoll, von LTS- zu LTS-Release zu gehen, um die Frequenz von neuen Features etwas zu bremsen und die Stabilität zu erhöhen. Dieser Artikel enthält genau deswegen einen Überblick über die relevanten Neuigkeiten seit JDK 17, um zu zeigen, wieso sich ein Upgrade auf das neue LTS-Release lohnen kann. Notwendig ist es noch nicht, da es noch mehrere Jahre Support auch für JDK 17 geben wird.

Doch bevor wir loslegen, noch einmal die Erinnerung daran, dass es neben finalen und damit stabilen Features mittlerweile auch Incubator (JEP11) und Preview Features (JEP12) gibt. Beide sind weniger stabil und können sich bis zur finalen Version deutlich verändern. Bei Incubator Features besteht sogar die Gefahr, dass diese es nie bis zu einer finalen Version schaffen und vorher wieder entfernt werden. Und auch damit wir nicht aus Versehen ein nicht stabiles Preview Feature verwenden, müssen wir diese, sowohl beim Kompilieren als auch zur Laufzeit extra mit der Angabe von --enable-preview aktivieren.

Doch genug der langen Einleitung, nun wollen wir uns in die Menge der neuen Features stürzen.

Standardmäßige Nutzung von UTF-8

Aus der Historie heraus gibt es eine Anzahl von Methoden und Konstruktoren rund um das Lesen und Schreiben von Daten in der Klassenbibliothek, die ohne die Angabe einer Zeichencodierung aufgerufen werden können. Diese nutzen dann intern die Standardcodierung, welche über den Aufruf von Charset.defaultCharset() ermittelt wird.

Vor JDK 18 wurde hier je nach verwendetem Betriebssystem oder gesetztem Wert für die Systemvariable file.encoding eine unterschiedliche Codierung genutzt. Mit JEP 400 wird dies auf UTF-8 vereinheitlicht. Sollte dies zu Problemen führen, lässt sich weiterhin durch die Angabe von -Dfile.encoding=COMPAT beim Starten die bisherige Logik zur Ermittlung zu nutzen.

Da es sich hierbei um eine Änderung handelt, bei der Fehler unter Umständen erst zur Laufzeit entdeckt werden, sollte, vor einem Update auf JDK 18 oder später, durch die Angabe von -Dfile. encoding=UTF-8 getestet werden, ob die eigene Anwendung weiterhin fehlerfrei funktioniert. Analog zur Laufzeit erwartet von JDK 18 an auch der Compiler javac die Quelldateien in UTF-8-Codierung. Auch hier kann bereits vorab durch die Angabe von -encoding UTF-8 getestet werden, ob es bei einem Update zu Problemen kommt.

Die einzigen beiden Stellen, bei denen nicht auf UTF-8 gewechselt wird, sind System.out und System.err. Da diese direkt mit dem Betriebssystem interagieren, wird hier die Codierung von Console.charset() verwendet.

Ein einfacher Webserver

Auch in JDK 18 hinzugekommen ist ein einfacher Webserver, der für Entwicklungszwecke genutzt werden kann, um statische Dateien aus dem Dateisystem auszuliefern, ähnlich wie der aus Python bekannte Simple HTTP Server, der, in Version 2, mit python -m SimpleHTTPServer gestartet werden kann.

Die in JEP 408 für die JVM entwickelte Variante lässt sich durch den Aufruf von jwebserver starten und liefert anschließend über das Loopback-Interface auf Port 8000 das aktuelle Verzeichnis über HTTP 1.1 aus. Diese Standardeinstellungen lassen sich auch durch die Angabe von Optionen beim Start ändern. So kann mittels -d auch ein anderes Verzeichnis gewählt und mit -p der Port geändert werden. Alle Möglichkeiten lassen sich über die Angabe von -h oder --help ausgeben.

Alternativ kann der Webserver auch programmatisch als API in eigenem Code verwendet werden. Hierzu wurde der bereits bestehende HttpServer durch die Klassen SimpleFileServer, HttpHandlers und Request ergänzt. Listing 1 zeigt, wie die Nutzung von diesem aussehen kann.

public static void main(String[] args) throws IOException {
    var server = HttpServer.create(new InetSocketAddress(8000), 0);
    server.createContext("/dir",
        SimpleFileServer.createFileHandler(Path.of("/some/path")));
    server.createContext("/ping",
        HttpHandlers.of(200, new Headers(), "Pong"));
    server.createContext("/echo", exchange -> {
        exchange.sendResponseHeaders(200, 0);
        try (var out = exchange.getResponseBody()) {
            out.write(exchange.getRequestMethod().getBytes(UTF_8));
        }
    });
    server.start();
}
Listing 1: Verwendung der simplen Webserver-API

Weder das API noch der als Tool nutzbare Server für statische Dateien ist dabei für den Produktionsbetrieb ausgelegt. Beides soll lediglich ermöglichen, zu Test- oder Entwicklungszwecken schnell verwendbar zu sein. Für produktive Anwendungen wird weiterhin der Einsatz der bekannten Server wie Jetty, Netty oder Tomcat empfohlen.

Code Snippets in Javadoc

Bisher mussten wir, um Code-Snippets in Javadoc zu haben, eine Kombination aus dem HTML-Tag pre und dem @code-Doclet (s. Listing 2) nutzen. Dies ist nun nicht mehr notwendig und wir brauchen seit JDK 18 nur noch das, durch JEP 413 definierte, @snippet-Doclet. Neben der einfacheren Handhabung ist es hiermit nun auch möglich, auf Regionen in Dateien zu verweisen (s. Listing 3).

/**
 * Was used like
 * <pre>{@code
 *   var x = "Michael";
 *   System.out.println(x.toUpperCase());
 * }</pre>
 */
Listing 2: Javadoc-Snippets vor JDK 18
/**
 * Can be used like
 * {@snippet :
 *   var x = "Michael";
 *   System.out.println(x.toUpperCase());
 * }
 * or
 * {@snippet file="de/mvitz/Javadoc.java" region="example"}
 */
Listing 3: Javadoc-Snippets nach JDK 18

Der auffälligste Vorteil ist, dass durch das Referenzieren einer Region das Javadoc selbst kompakter wird. Wenn wir diese Datei nun aber auch noch kompilieren und gegebenenfalls sogar als Test ausführen, dann können wir auch stets sicher sein, dass das Beispiel auch richtig ist und funktioniert.

Beides, Kompilieren und Ausführen, ist jedoch nicht Teil des JEPs und muss von uns sichergestellt werden. Eine Möglichkeit, dies mit Maven zu bewerkstelligen, zeigt Nicolai Parlog in seinem Blogpost „Configuring Maven For Compiled And Tested Code in Javadoc“.

Sequenced Collections

Das im JDK enthaltene Collections API ist zwar mächtig und enthält mit List, Set, Queue und Map diverse Typen, hatte bisher aber keine dedizierten Typen, um zu zeigen, dass eine Collection eine definierte Reihenfolge hat. Zwar haben List und Queue eine definierte Reihenfolge, nämlich die Reihenfolge des Einfügens, aber der gemeinsame Obertyp Collection macht dies nicht deutlich. Beim Set ist es wiederum so, dass es hier erst mal keine definierte Reihenfolge gibt, aber Subtypen wie SortedSet oder LinkedHashSet dann doch eine haben. Dies wird vor allem problematisch, wenn wir in unserer API ausdrücken wollen, dass wir eine sortierte Collection haben, diese aber nicht zwangsweise eine List sein muss. Hand in Hand hiermit ist es je nach Typ beliebig schwer, an das letzte Element zu gelangen oder rückwärts zu iterieren.

Um genau diese Probleme zu lösen, wurden in JDK 21 mit JEP 431 Sequenced Collections eingeführt. Diese bestehen vor allem aus den drei neuen Interfaces SequencedCollection, SequencedSet und SequencedMap, welche von den passenden vorhandenen Interfaces und Klassen implementiert werden. SequencedCollection fügt dabei die in Listing 4 zu sehenden Methoden hinzu, SequencedSet definiert lediglich den Rückgabetypen von reversed() auf SequencedSet.

public interface SequencedCollection<E>
        extends Collection<E> {

    SequencedCollection<E> reversed();

    void addFirst(E e);
    void addLast(E e);

    E getFirst();
    E getLast();

    E removeFirst();
    E removeLast();
}
Listing 4: SequencedCollection

Neben der neuen Methode reversed() kommen die anderen aus dem Deque-Interface und wurden nun hierhin verschoben. Bei unveränderlichen Implementierungen werfen die add- und remove-Methoden analog zu allen anderen Methoden, die den Zustand verändern, eine UnsupportedOperationException. Dasselbe passiert auch, wenn wir addFirst() oder addLast() bei einem SortedSet verwenden. Da hier die Reihenfolge nicht von außen bestimmt werden kann, wird auch hier eine UnsupportedOperationException geworfen. Bei anderen Sets, wie beispielsweise dem LinkedHashSet, wird bei der Nutzung von addFirst() oder addLast() das Element an die passende Stelle eingefügt. Sollte das Element vorher schon vorhanden gewesen sein, wird das alte entfernt, sodass semantisch eine Verschiebung des Elements durchgeführt wurde. Im Falle einer leeren Collection werfen die get- und remove-Methoden übrigens eine NoSuchElementException.

Analog zur SequencedCollection enthält SequencedMap Methoden für eine gleiche Verwendung, wie in Listing 5 zu sehen ist. Auch hier können die Methoden je nach Art UnsupportedOperationException oder NoSuchElementException werfen.

public interface SequencedMap<K,V> extends Map<K,V> {

    SequencedMap<K,V> reversed();

    Map.Entry<K, V> firstEntry();
    Map.Entry<K, V> lastEntry();
    Map.Entry<K, V> pollFirstEntry();
    Map.Entry<K, V> pollLastEntry();

    V putFirst(K k, V v);
    V putLast(K k, V v);

    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Map.Entry<K,V>> sequencedEntrySet();
}
Listing 5: SequencedMap

Parallel hierzu wurde auch die Klasse Collections um Methoden erweitert, um eine vorhandene SequencedCollection in eine unveränderliche umzuwandeln.

Pattern Matching in switch und Record Patterns

Nachdem es seit JDK 16 und JEP 394 möglich ist, bei der Nutzung von instanceof gleichzeitig das geprüfte Objekt an eine Variable mit dem spezifischen Typen zu binden, siehe Listing 6, wird diese Art der Funktionalität nun schrittweise erweitert und an andere Stellen übertragen.

if (x instanceof String s && s.length() > 4) {
    System.out.println(
        "String %s is longer than 4 chars".formatted(s));
}
Listing 6: Pattern Matching in instanceof mit Guard Clause

Eine dieser neuen Stellen ist die Verwendung dieses Musters im switch-Statement. Dieses Feature war bereits in JDK 17 als erste Preview vorhanden und hat es nun nach drei weiteren Previews (JEP 420 in JDK 18, JEP 427 in JDK 19 und JEP 433 in JDK 20) mit JEP 441 in JDK 21 als finales Feature geschafft.

Im Kern, siehe Listing 7, geht es dabei darum, in einem switch genau dann zu matchen, wenn die im case angegebene Klasse von der zu testenden Instanz implementiert oder erweitert wird. Passen mehrere case-Label auf eine Instanz, wird stets nur der erste Fall ausgewertet. Da unsere Instanz x in diesem Fall ein String ist und String auch CharSequence implementiert, wird hier nur „Michael is a String“ ausgegeben. Würden wir die beiden case-Fälle tauschen, sodass zuerst der CharSequence-Fall im Code steht, erhalten wir einen Kompilierungsfehler, da der String case-Fall nie erreicht werden kann. Dieser Fall wird vom anderen dominiert.

Object x = "Michael";
switch (x) {
    case String s:
        System.out.println("%s is a String".formatted(s));
        break;
    case CharSequence cs:
        System.out.println("%s is a CharSequence".formatted(cs));
        break;
    default:
        break;
}
Listing 7: Pattern Matching in switch

Zusätzlich wurde switch auch noch für den null-Fall erweitert und wir brauchen, wie in Listing 8 zu sehen, keine Prüfung auf null mehr vor dem switch. Außerdem ist es von nun an auch möglich, mittels when-Ausdruck neben dem Pattern Matching den case-Fall nur auf bestimmte Bedingungen reagieren zu lassen (s. Listing 9).

Object x = null;
switch (x) {
    case String s:
        System.out.println("%s is a String".formatted(s));
        break;
    case null:
        System.out.println("Null it is");
        break;
    default:
        break;
}
Listing 8: Pattern Matching in switch mit null
Object x = "Michael";
switch (x) {
    case CharSequence cs when cs.length() > 4:
        System.out.println("%s is a CharSequence".formatted(cs));
        break;
    case String s:
        System.out.println("%s is a String".formatted(s));
        break;
    default:
        break;
}
Listing 9: Pattern Matching in switch mit when

Ergänzend zu dieser neuen Möglichkeit von Pattern Matching wurde speziell für die Nutzung von Records die Möglichkeit eingebaut, diese während des Matchings zu destrukturieren. Auch dieses Feature, Record Patterns, ist mit JDK 21 und JEP 440, nach zwei Previews (JEP 405 in JDK 19 und JEP 432 in JDK 20), nun final fertig. Diese Destrukturierung kann dabei nicht nur auf erster Ebene, sondern wie in Listing 10 auch mit tiefer verschachtelten Records verwendet werden.

public static void main(String[] args) {
    Object o = new Point(0, 0, new Color(255, 255, 255));
    var value = switch (o) {
        case Point(int x, int y, Color(int r, int g, int b))
            -> x + y + r + g + b;
        default
            -> 0;
    };
    System.out.println(value);
}

record Color(int r, int g, int b) {}
record Point(int x, int y, Color color) {}
Listing 10: Record Patterns

Und als finale Ergänzung rund um das gesamte Pattern-Thema gibt es mit JEP 443 auch noch ein Preview-Feature in JDK 21, um für nicht genutzte Patterns den Unterstrich als Variablennamen zu nehmen, wie in Listing 11 zu sehen ist. Gleichzeitig ist es nun auch möglich, den Unterstrich für ungenutzte Variablen an diversen Stellen zu nutzen. Hierzu zählen vor allem for-Schleifen, lokale Zuweisungen, Exceptions in catch-Blöcken, Variablen aus try-with-resources und ungenutzte Parameter eines Lambda-Ausdruckes. Listing 12 zeigt die Anwendung.

public static void main(String[] args) {
    Object o = new Point(0, 0, new Color(255, 255, 255));
    var value = switch (o) {
        case Point(_, _, Color(int r, _, _)) -> r;
        default -> 0;
    };
    System.out.println(value);
}

record Color(int r, int g, int b) {}
record Point(int x, int y, Color color) {}
Listing 11: Unterstrich für ungenutzte Patterns
public static void main(String[] args) {
    try (var _ = new Bar(1); var _ = new Bar(2)) {
        var _ = new Object();
        System.out.println("Work");
    }
}

record Bar(int number) implements AutoCloseable {

    @Override
    public void close() {
        System.out.println("Bar[%s].close".formatted(number));
    }
}
Listing 12: Unterstrich für ungenutzte Variablen

Die Vorteile dieser Art der Verwendung sind vor allem, dass beim Lesen sofort ersichtlich ist, dass diese Variable nicht mehr verwendet wird und dass wir den Unterstrich mehrmals im selben Block verwenden können. Vorher mussten wir uns mit Namen wie ignored behelfen und dann mit ignored2 usw. fortsetzen.

Virtual Threads und Concurrency

Neben dem Pattern Matching sind die virtuellen Threads, die nach zwei Previews (JEP 425 in JDK 19 und JEP 436 in JDK 20) nun mit JEP 444 in JDK 21 final erschienen sind, das zweite Highlight.

Mit diesen steht uns nun eine sehr leichtgewichtige Alternative zu den bekannten klassischen Threads zur Verfügung. Dies wird dadurch erreicht, dass es nicht wie bei einem Thread eine Eins-zu-eins-Beziehung zu einem nativen Betriebssystemthread gibt, sondern diese nur in der JVM abgebildet und geschedult werden.

Hierdurch ist es möglich, mehrere tausend, der JEP spricht sogar von Millionen, von Threads zu erstellen, ohne an Grenzen zu stoßen. Dadurch, dass die JVM deren Verwaltung übernimmt, kann diese, beispielsweise während ein solcher virtueller Thread auf das Netzwerk wartet, andere virtuelle Threads ausführen und somit quasi nicht blockierend agieren und die Ressourcen somit besser auslasten. Deswegen ist es auch nicht notwendig, virtuelle Threads in einem Pool zu verwalten. Diese sind so leichtgewichtig, dass eine Wiederverwendung nicht notwendig ist, sondern für jede Aufgabe ein neuer virtueller Thread erzeugt werden soll.

Um nun einen virtuellen Thread zu starten, kann entweder ein TaskExecutor oder die neue Thread.Builder-API, wie in Listing 13 zu sehen, verwendet werden. Natürlich ist es auch möglich, den Code in diesen Threads zu debuggen und auch in einem Threaddump tauchen diese auf. Hier allerdings nicht so ausführlich wie die nativen Threads, da Threaddumps mit mehreren Tausend virtuellen Threads zu schnell zu groß werden würden.

// Thread.Builder API
Thread.ofVirtual()
    .name("ABC")
    .start(() -> System.out.println("Hello"));

// Executor
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(() -> System.out.println("Hello"));
}
Listing 13: Erzeugen von virtuellen Threads

Während der Umsetzung von diesen virtuellen Threads stellte sich schnell die Frage, was innerhalb von diesen mit dem bisher genutzten ThreadLocal-Mechanismus passieren soll. Stand jetzt kann ThreadLocal wie bisher genutzt werden. Im virtuellen Thread gesetzte Werte sind dann für diesen Thread sichtbar, aber isoliert von dem nativen Thread, der den virtuellen Thread ausführt. Auch andersherum sind Werte, die im nativen Thread gesetzt werden, nicht vom virtuellen Thread aus sichtbar.

Da der gesamte Mechanismus von ThreadLocal jedoch nie für eine solche Menge an Threads gedacht war und auch das Konzept, dass diese von allen Stellen aus änderbar sind und die Lebenszeit, gerade bei gepoolten Threads, unendlich lange sein kann, nicht so einfach änderbar ist, enthält das JDK 21 mit JEP 446, nach einem Incubator in JDK 20 (JEP 429) ein weiteres Preview Feature, nämlich für Scoped Values. Scoped Values erlauben es uns, wie in Listing 14 zu sehen, Werte zu setzen, die anschließend von allen Methoden, die innerhalb des Scopes aufgerufen werden, sichtbar sind. Allerdings werden diese nicht in neue virtuelle Threads übertragen. Listing 15 wirft deswegen eine NoSuchElementException.

public class Scopes {

    static final ScopedValue<Integer> FOO = ScopedValue.newInstance();

    public static void main(String[] args) {
        ScopedValue.where(FOO, 1).run(() -> {
            log(); // 1
            ScopedValue.where(FOO, 2).run(() -> {
                log(); // 2
            });
            log(); // 1
        });
    }

    private static void log() {
        System.out.println(FOO.get());
    }
}
Listing 14: Verwendung von Scoped Values
ScopedValue.where(FOO, 1).run(() -> {
    log(); // 1
    ScopedValue.where(FOO, 2).run(() -> {
        log(); // 2
        try {
            Thread.ofVirtual().start(() -> {
                log(); // throws NoSuchElementException
            }).join();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    });
    log(); // 1
});
Listing 15: Scoped Values in neuen Threads

Möchten wir auch in diesem Szenario den gesetzten Wert erhalten, müssen wir diesen entweder erneut binden oder das nächste Preview Feature, nämlich die Structured Concurrency API, nutzen. Nachdem diese bereits zwei Incubator-Versionen, JEP 428 in JDK 19 und JEP 437 in JDK 20, hinter sich hat, gibt es nun mit JEP 453 eine erste Preview.

Ziel dieser API ist es, basierend auf virtuellen Threads und mit Unterstützung von Scoped Values eine einfachere Möglichkeit für parallele Programmierung anzubieten, als es bisher im JDK vorhanden war. Herzstück der neuen API ist dabei der StructuredTaskScope, über den wir, wie in Listing 16 zu sehen, Subtasks erzeugen und koordinieren können.

ScopedValue.where(FOO, 1).run(() -> {
    log(); // 1
    ScopedValue.where(FOO, 2).run(() -> {
        log(); // 2
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            scope.fork(() -> { log(); return null; });
            scope.join().throwIfFailed(); // 2
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    });
    log(); // 1
});
Listing 16: Verwendung von Structured Concurrency

Der Vorteil hier ist, dass an der Stelle, an der wir auf beide Subtasks mittels join() warten, sämtliche Koordination passiert. Wird diese Stelle ohne Exception passiert, sind danach garantiert beide Werte vorhanden. Sollte der aufrufende Thread abgebrochen werden oder einer der beiden Subtasks eine Exception werfen, werden garantiert alle noch laufenden Subtasks abgebrochen.

Für diesen Abbruch sorgt die von uns gewählte Shutdown Policy ShutdownOnFailure. Alternativ ist es auch möglich, ShutdownOnSuccess zu wählen, dann werden alle Subtasks abgebrochen, so bald einer der Subtasks erfolgreich durchlaufen wurde, oder eigene Policies zu erstellen.

Da durch diese Struktur die Subtasks nun auch noch verbunden sind, können Threaddumps diese Verbindung nun auch enthalten und uns bei einer potenziellen Analyse unterstützen.

Und sonst so?

Neben diesen großen Themen haben sich über vier Releases auch noch weitere spannende JEPs angesammelt, welche wir uns jetzt hier nicht noch im Detail anschauen können, aber kurz erwähnt sollen diese trotzdem werden.

Im Rahmen von JEP 430 werden String Templates in JDK 21 als Preview Feature implementiert. Diese erlauben uns nicht nur innerhalb von Strings auf Variablen zuzugreifen, sondern ermöglichen auch noch Validierung oder Escaping von diesen.

JEP 445 unterstützt, auch als Preview Feature in JDK 21, das Ziel, einen leichteren Einstieg in die Sprache zu bekommen. Hierzu ist es nun möglich, die main-Methode nicht mehr static deklarieren zu müssen und auch auf die Argumente verzichten zu können, sofern diese nicht benötigt werden. Außerdem ermöglicht dieser JEP es, mit einigen Einschränkungen, sogar auf die Klasse der main-Methode zu verzichten, wodurch void main() { System.out. println("Hallo"); } ein valides Java-Programm wird.

Mit JEP 418 ist es seit JDK 18 möglich, alternative Implementierungen für die DNS-Auflösung zu nutzen. Das JDK liefert allerdings nach wie vor die identische Implementierung aus, lediglich das Service Provider Interface (SPI) wurde im Rahmen dieses JEPs definiert.

Auch mit JDK 18 wurde das Thema „Finalization“ innerhalb von JEP 421 Deprecated und wird in Zukunft komplett entfernt. Wer schon jetzt ausprobieren möchte, ob das zu Problemen führt, kann seine Anwendung mit der Option --finalization=disabled starten. Nun werden keinerlei Finalizer mehr ausgeführt. In Zukunft wird diese Option erst zum Standard, um in einem späteren Schritt dann die Funktionalität komplett zu entfernen.

Mit JEP 439 wird in JDK 21 der Z Garbage Collector (ZGC) um Generationen erweitert. Somit ist es nun auch für diesen möglich, Objekte, die nicht lange leben, früher und häufiger abzuräumen, und somit besser seine Arbeit zu verrichten.

Um in einem späteren Release das dynamische Laden von Java Agents zu verhindern, wurden im JDK 21 mit JEP 451 schon erste Vorarbeiten begonnen. Diese Änderung soll die Sicherheit erhöhen, indem sichergestellt wird, dass Code zur Laufzeit nicht plötzlich verändert wird. Aktuell kann das aber noch zu Problemen führen, denn vor allem Tools für Mocking nutzen die Möglichkeit, Java Agents dynamisch während der Laufzeit zu laden, noch an einigen Stellen.

In JDK 21 geht die neue Vector API mit JEP 448 in ihre sechste Incubator Version. Ziel ist es, Vektorberechnungen optimal auf der CPU auszuführen.

Zuletzt enthält das JDK 21 noch, mit JEP 442, die dritte Preview Version für den Zugriff auf native Funktionen und Speicherbereiche. Ziel ist es, hiermit einen verbesserten Nachfolger für das Java Native Interface (JNI) bereitzustellen.

Fazit

In diesem Artikel haben wir einen detaillierten Blick auf die Version 21 vom JDK geworfen. Dieses ist am 19.9. erschienen und wird von mehreren Herstellern als Long-term Support Release mehrere Jahre unterstützt.

Wir haben uns dazu einige neue Features, die seit dem letzten LTS-Release, JDK 17, dazu gekommen sind im Detail angeschaut. Neben der Umstellung auf UTF-8 als Standardzeichencodierung, dem einfachen Webserver zur Unterstützung in der Entwicklung und der Möglichkeit, Code-Snippets in Javadoc zu verwenden, handelt es sich dabei um drei größere Themenblöcke.

Mit SequencedCollections sind nun Typen in der Collection API enthalten, um auszudrücken, dass eine Collection mit definierter Reihenfolge erwartet oder zurückgegeben wird.

Pattern Matching wird nun auch in switch unterstützt und auch das Destrukturieren von Records wird hier, und innerhalb von instanceof, ermöglicht. Außerdem wird es in Zukunft die Möglichkeit geben, durch einen Unterstrich unbenutzte Patterns oder Variablen kenntlich zu machen. All das zahlt auf das von Brian Goetz als „Data Oriented Programming“ genannte Paradigma ein.

Mit virtuellen Threads, Scoped Values und Structured Concurrency enthält JDK 21 auch einen großen Block, der sich dem Thema Parallelität verschreibt. Dieser ermöglicht es uns, auch ohne den Umstieg auf ein Reactives Modell die vorhandene Hardware besser auszulasten.

Und auch die weiteren, noch nicht ganz fertigen Themen, wie String Templates, unbenannte Klassen und Instanz-Main-Methoden, die Vector API oder der neue Zugriff auf native Funktionen und Speicherbereiche zeigen, dass das JDK noch weit entfernt von fertig ist und sich hier von Release zu Release noch viel tut.

Unerwähnt sollen auch die vielen weiteren kleinen API-Verbesserungen nicht sein. Diese lassen sich am einfachsten über die Vergleichsfunktion von JDK 17 zu 21 vom Java Version Almanac entdecken. Hierbei stößt man dann auf Sachen wie Character#isEmoji(int) oder Duration#isPositive().