java 20 featuresjava 20 features
HappyCoders Glasses

Java 20 Features
(mit Beispielen)

Sven Woltmann
Sven Woltmann
12. Dezember 2022

Java 20 befindet sich seit dem 8. Dezember 2022 in der sogenannten "Rampdown Phase One", d. h. es werden keine weiteren JDK Enhancement Proposals (JEPs) in das Release aufgenommen. Das Feature-Set steht also fest. Es werden nur noch Bugs gefixt und ggf. kleinere Verbesserungen durchgeführt.

Als Veröffentlichungsdatum ist der 21. März 2023 angepeilt. Die aktuelle Early-Access-Version kannst du hier herunterladen.

Nachdem wir in Java 19 mit virtuellen Threads eine der größten Erweiterungen der Java-Geschichte ausprobieren durften, fällt das Java-20-Release wieder etwas kleiner aus.

Die interessanteste Neuerung nennt sich "Scoped Values" und soll in zahlreichen Einsatzzwecken die mit diversen Nachteilen behafteten Thread-Local-Variablen ersetzen.

Die restlichen fünf der sechs in Java 20 veröffentlichten JEPs sind Wiedervorlagen bereits bekannter Incubator- und Preview-Features.

Ich verwende wie immer ausschließlich die englischen Bezeichnungen der JEPs.

Preview- und Incubator-Features

Bei allen sechs JDK Enhancement Proposals (JEPs), die es in das Java-20-Release geschafft haben, handelt es sich um Incubator- bzw. Preview-Features. Das sind noch nicht fertiggestellte Features, die explizit (mit --enable-preview bei den java und javac Kommandos) aktiviert werden müssen, um sie testen zu können.

Scoped Values (Incubator) – JEP 429

Scoped Values – genau wie virtuelle Threads in Project Loom entwickelt – sind eine moderne und insbesondere mit virtuellen Threads gut kombinierbare Alternative zu Thread Locals. Sie ermöglichen es, einen Wert für einen begrenzten Zeitraum so zu speichern, dass nur derjenige Thread den Wert lesen kann, der ihn geschrieben hat.

Mit dem JDK Enhancement Proposal 429 werden Scoped Values in Java 20 erstmals im Incubator-Stadium vorgestellt.

Wie Scoped Values genau funktionieren und warum sie gegenüber Thread Locals vorzuziehen sind, erfährst du im Hauptartikel über Scoped Values.

Record Patterns (Second Preview) – JEP 432

Record Patterns wurden erstmals in Java 19 vorgestellt. Ein Record Pattern kann mit instanceof oder switch verwendet werden, um ohne Cast und Aufruf von Zugriffsmethoden auf die Felder eines Records zuzugreifen.

Hier ein einfacher Beispiel-Record:

public record Position(int x, int y) {}
Code-Sprache: Java (java)

Mittels Record Pattern können wir nun einen instanceof-Ausdruck wie folgt schreiben:

Object object = ... if (object instanceof Position(int x, int y)) { System.out.println("object is a position, x = " + x + ", y = " + y); }
Code-Sprache: Java (java)

Wir können somit – sofern object vom Typ Position ist – direkt auf dessen x- und y-Werte zugreifen.

Das gleiche funktioniert in einem switch-Ausdruck:

Object object = ... switch (object) { case Position(int x, int y) -> System.out.println("object is a position, x = " + x + ", y = " + y); // other cases ... }
Code-Sprache: Java (java)

Mit dem JDK Enhancement Proposal 432 wurden in Java 20 folgende Änderungen veröffentlicht:

Inferenz von Typargumenten generischer Record Patterns

Um diese Änderung zu erklären, brauchen wir ein komplexeres Beispiel.

Gegeben seien ein generisches Interface Multi<T> und zwei implementierende Records, Tuple<T> und Triple<T>, die zwei bzw. drei Werte des Typs T enthalten:

interface Multi<T> {} record Tuple<T>(T t1, T t2) implements Multi<T> {} record Triple<T>(T t1, T t2, T t3) implements Multi<T> {}
Code-Sprache: Java (java)

Mit folgendem Code können wir prüfen, um welche konkrete Implementierung es sich bei einem gegebenen Multi-Objekt handelt:

Multi<String> multi = ... if (multi instanceof Tuple<String>(var s1, var s2)) { System.out.println("Tuple: " + s1 + ", " + s2); } else if (multi instanceof Triple<String>(var s1, var s2, var s3)) { System.out.println("Triple: " + s1 + ", " + s2 + ", " + s3); }
Code-Sprache: Java (java)

Dabei müssen wir den Typ-Parameter (in diesem Fall String) bei jeder instanceof-Prüfung mit angeben.

Ab Java 20 kann der Compiler den Typ herleiten, so dass wir ihn bei den instanceof-Prüfungen weglassen können:

if (multi instanceof Tuple(var s1, var s2)) { System.out.println("Tuple: " + s1 + ", " + s2); } else if (multi instanceof Triple(var s1, var s2, var s3)) { System.out.println("Triple: " + s1 + ", " + s2 + ", " + s3); }
Code-Sprache: Java (java)

Was mir dabei nicht gefällt, ist dass hier die Syntax der sogenannten "Raw Types" verwendet wird. Raw Types führen normalerweise dazu, dass der Compiler jegliche Typinformation ignoriert. Das ist hier aber nicht der Fall.

Ich hielte es daher für konsistenter, hier den Diamond-Operator zu verwenden:

if (multi instanceof Tuple<>(var s1, var s2)) { System.out.println("Tuple: " + s1 + ", " + s2); } else if (multi instanceof Triple<>(var s1, var s2, var s3)) { System.out.println("Triple: " + s1 + ", " + s2 + ", " + s3); }
Code-Sprache: Java (java)

Auch bei switch-Statements kann der Typ-Parameter ab Java 20 weggelassen werden.

Record Patterns in for-Schleifen

Nehmen wir an, wir haben eine Liste von Positionen und wollen diese auf der Konsole ausgeben. Das konnten wir bisher so machen:

List<Position> positions = ... for (Position p : positions) { System.out.printf("(%d, %d)%n", p.x(), p.y()); }
Code-Sprache: Java (java)

Ab Java 20 können wir in der for-Schleife auch ein Record Pattern angeben und dann (genau wie bei instanceof und switch) direkt auf x und y zugreifen:

for (Position(int x, int y) : positions) { System.out.printf("(%d, %d)%n", x, y); }
Code-Sprache: Java (java)

Entfernung der Unterstützung für benannte Record Patterns

Bisher gab es folgende drei Möglichkeiten, Pattern Matching mit einem Record durchzuführen:

Object object = new Position(4, 3); // 1. Pattern Matching for instanceof if (object instanceof Position p) { System.out.println("object is a position, p.x = " + p.x() + ", p.y = " + p.y()); } // 2. Record Pattern if (object instanceof Position(int x, int y)) { System.out.println("object is a position, x = " + x + ", y = " + y); } // 3. Named Record Pattern if (object instanceof Position(int x, int y) p) { System.out.println("object is a position, p.x = " + p.x() + ", p.y = " + p.y() + ", x = " + x + ", y = " + y); }
Code-Sprache: Java (java)

Bei der dritten Variante ("Named Record Pattern") gibt es zwei Wege, um auf die Felder des Records zuzugreifen – entweder über die Variablen x und y – oder über p.x() und p.y().

Diese Variante wurde für überflüssig befunden und daher in Java 20 wieder entfernt.

Pattern Matching for switch (Fourth Preview) – JEP 433

Das nächste Feature hat bereits drei Preview-Runden hinter sich. "Pattern Matching for Switch" wurde in Java 17 erstmals vorgestellt und ermöglicht es uns, ein switch-Statement wie das folgende zu schreiben:

Object obj = getObject(); switch (obj) { case String s when s.length() > 5 -> System.out.println(s.toUpperCase()); case String s -> System.out.println(s.toLowerCase()); case Integer i -> System.out.println(i * i); case Pos(int x, int y) -> System.out.println(x + "/" + y); default -> {} }
Code-Sprache: Java (java)

So können wir in einem switch-Statement prüfen, ob ein Objekt von einer bestimmten Klasse ist (und ggf. weiteren Bedingungen genügt) und dieses Objekt gleichzeitig implizit auf die Zielklasse casten. Wir können das switch-Statement auch mit Record-Patterns kombinieren und so direkt auf die Record-Felder zugreifen.

Mit dem JDK Enhancement Proposal 433 wurden in Java 20 folgende Änderungen gemacht:

MatchException bei erschöpfendem Switch

Ein erschöpfender Switch (also ein Switch, der alle möglichen Werte umfasst) wirft eine MatchException (und nicht wie zuvor einen IncompatibleClassChangeError), wenn zur Laufzeit festgestellt wird, dass kein Switch-Label zutrifft.

Das kann dann passieren, wenn der Code nachträglich erweitert wird, aber nicht alle Klassen neu compiliert werden. Am besten zeige ich das an einem Beispiel:

Unter Verwendung des Position-Records aus dem Record-Pattern-Kapitel definieren wir ein versiegeltes Interface Shape mit den Implementierungen Rectangle und Circle:

public sealed interface Shape permits Rectangle, Circle {} public record Rectangle(Position topLeft, Position bottomRight) implements Shape {} public record Circle(Position center, int radius) implements Shape {}
Code-Sprache: Java (java)

Zusätzlich schreiben wir einen ShapeDebugger, der je nach Shape-Implementierung unterschiedliche Debug-Infos ausgibt:

public class ShapeDebugger { public static void debug(Shape shape) { switch (shape) { case Rectangle r -> System.out.println( "Rectangle: top left = " + r.topLeft() + "; bottom right = " + r.bottomRight()); case Circle c -> System.out.println( "Circle: center = " + c.center() + "; radius = " + c.radius()); } } }
Code-Sprache: Java (java)

Da der Compiler alle möglichen Implementierungen des versiegelten Shape-Interfaces kennt, kann er sicherstellen, dass dieser Switch-Ausdruck erschöpfend ist.

Mit folgendem Programm rufen wir den ShapeDebugger auf:

public class Main { public static void main(String[] args) { var rectangle = new Rectangle(new Position(10, 10), new Position(50, 50)); ShapeDebugger.debug(rectangle); var circle = new Circle(new Position(30, 30), 10); ShapeDebugger.debug(circle); } }
Code-Sprache: Java (java)

Wir compilieren den Code wie folgt und führen die Main-Klasse aus:

$ javac --enable-preview -source 20 *.java $ java --enable-preview Main Rectangle: top left = Position[x=10, y=10]; bottom right = Position[x=50, y=50] Circle: center = Position[x=30, y=30]; radius = 10
Code-Sprache: Klartext (plaintext)

Danach fügen wir eine weitere Form Oval hinzu, tragen diese in die permits-Liste des Shape-Interfaces ein und erweitern das Hauptprogramm:

public sealed interface Shape permits Rectangle, Circle, Oval {} public record Oval(Position center, int width, int height) implements Shape {} public class Main { public static void main(String[] args) { var rectangle = new Rectangle(new Position(10, 10), new Position(50, 50)); ShapeDebugger.debug(rectangle); var circle = new Circle(new Position(30, 30), 10); ShapeDebugger.debug(circle); var oval = new Oval(new Position(60, 60), 20, 10); ShapeDebugger.debug(oval); } }
Code-Sprache: Java (java)

Wenn wir das in einer IDE machen, wird diese uns sofort mitteilen, dass das Switch-Statement im ShapeDebugger nicht alle möglichen Werte abdeckt:

Java 20, JEP 433: IDE-Fehlermeldung bei erschöpfendem Switch

Wenn wir allerdings ohne IDE arbeiten, nur die geänderten Klassen neu kompilieren und dann das Hauptprogramm starten, passiert folgendes:

$ javac --enable-preview -source 19 Shape.java Oval.java Main.java $ java --enable-preview Main Rectangle: top left = Position[x=10, y=10]; bottom right = Position[x=50, y=50] Circle: center = Position[x=30, y=30]; radius = 10 Exception in thread "main" java.lang.MatchException at ShapeDebugger.debug(ShapeDebugger.java:3) at Main.main(Main.java:10)
Code-Sprache: Klartext (plaintext)

Zur Laufzeit kommt es nun zu einer MatchException, da der Switch-Ausdruck im ShapeDebugger kein Label für die Oval-Klasse hat.

Das gleiche kann bei einem erschöpfenden Switch-Ausdruck über alle Werte eines Enums passieren, wenn wir nachträglich das Enum erweitern.

Inferenz von Typargumenten für generische Record Patterns

Wie bei den zuvor besprochenen Record Patterns mit instanceof, können nun auch in Switch-Ausdrücken die Typargumente generischer Records vom Compiler hergeleitet werden.

Bisher mussten wir ein Switch-Statement (basierend auf den Beispielklassen aus dem Record-Patterns-Kapitel) wie folgt schreiben:

Multi<String> multi = ... switch(multi) { case Tuple<String>(var s1, var s2) -> System.out.println( "Tuple: " + s1 + ", " + s2); case Triple<String>(var s1, var s2, var s3) -> System.out.println( "Triple: " + s1 + ", " + s2 + ", " + s3); ... }
Code-Sprache: Java (java)

Ab Java 20 können wir die Typ-Argumente <String> innerhalb des Switch-Statements weglassen:

switch(multi) { case Tuple(var s1, var s2) -> System.out.println( "Tuple: " + s1 + ", " + s2); case Triple(var s1, var s2, var s3) -> System.out.println( "Triple: " + s1 + ", " + s2 + ", " + s3); ... }
Code-Sprache: Java (java)

Foreign Function & Memory API (Second Preview) – JEP 434

An der in Project Panama entwickelten "Foreign Function & Memory API" wird bereits seit Java 14 gearbeitet – damals noch in zwei separate JEPs "Foreign Memory Access API" und "Foreign Linker API".

Seit Java 19 befindet sich die vereinheitlichte API im Preview-Stadium. Ihr Ziel ist es, langfristig das umständlich zu benutzende, fehleranfällige und langsame Java Native Interface (JNI) ablösen.

Die API ermöglicht es, aus Java heraus auf nativen Speicher (also Speicher außerhalb des Java-Heaps) zuzugreifen und nativen Code (z. B. aus C-Libraries) auszuführen.

Mit dem JDK Enhancement Proposal 434 wurden einige Änderungen an der API durchgeführt – mehr als normalerweise für Preview-Features üblich. Da ich im Java-19-Artikel die Foreign Function & Memory API nicht im Detail erklärt habe, werde ich auch hier nicht auf die einzelnen Änderungen eingehen.

Stattdessen wiederhole ich das Beispiel aus dem Java-19-Artikel, angepasst an die Änderungen in Java 20. Das Beispiel-Programm speichert einen String im Off-Heap-Memory, ruft für diesen die "strlen"-Funktion der C-Standard-Library auf und gibt das Ergebnis auf der Konsole aus:

public class FFMTest20 { public static void main(String[] args) throws Throwable { // 1. Get a lookup object for commonly used libraries SymbolLookup stdlib = Linker.nativeLinker().defaultLookup(); // 2. Get a handle to the "strlen" function in the C standard library MethodHandle strlen = Linker.nativeLinker().downcallHandle( stdlib.find("strlen").orElseThrow(), FunctionDescriptor.of(JAVA_LONG, ADDRESS)); // 3. Convert Java String to C string and store it in off-heap memory try (Arena offHeap = Arena.openConfined()) { MemorySegment str = offHeap.allocateUtf8String("Happy Coding!"); // 4. Invoke the foreign function long len = (long) strlen.invoke(str); System.out.println("len = " + len); } // 5. Off-heap memory is deallocated at end of try-with-resources } }
Code-Sprache: Java (java)

Um das Programm mit Java 20 zu compilieren und zu starten, musst du folgende Parameter mit angeben:

$ javac --enable-preview -source 20 FFMTest20.java $ java --enable-preview --enable-native-access=ALL-UNNAMED FFMTest20 len = 13
Code-Sprache: Klartext (plaintext)

Da die meisten Java-Entwicklerinnen und -Entwickler eher selten mit der Foreign Function & Memory API in Berührung kommen werden, werde ich an dieser Stelle nicht tiefer in die Materie einsteigen. Interessierte finden weitere Details in JEP 434 und auf der Seite von Project Panama.

Virtual Threads (Second Preview) – JEP 436

Virtuelle Threads wurden in Java 19 erstmals als Incubator Feature vorgestellt. Virtuelle Threads sind leichtgewichtige Threads, die keine Betriebssystem-Threads blockieren, wenn sie z. B. auf Locks, blockierende Datenstrukturen oder Antworten von externen System warten müssen.

Alles über virtuelle Threads erfährst du im Hauptartikel über virtuelle Threads.

Mit JDK Enhancement Proposal 436 werden virtuelle Threads zur weiteren Sammlung von Feedback ohne Änderungen in einer zweiten Preview-Phase wiedervorgelegt.

Es wurden lediglich einige Änderungen aus dem ersten Preview, die nicht spezifisch für virtuelle Threads waren und bereits in Java 19 finalisiert wurden, nicht mehr explizit im aktuellen JEP aufgeführt:

Structured Concurrency (Second Incubator) – JEP 437

Genau wie virtuelle Threads wurde auch "Structured Concurrency" erstmals in Java 19 vorgestellt und in Java 20 mit JDK Enhancement Proposal 437 wiedervorgelegt.

Wenn eine Aufgabe aus mehreren Teilaufgaben besteht, die parallel abgearbeitet werden können, erlaubt Structured Concurrency es uns, dies auf eine Art und Weise zu implementieren, die besonders gut les- und wartbar ist.

Wie das genau funktioniert, kannst du im Hauptartikel über Structured Concurrency nachlesen.

In der zweiten Incubator-Phase wird StructuredTaskScope dahingehend erweitert, dass es die ebenfalls in Java 20 eingeführten "Scoped Values" automatisch an alle Kind-Threads weitervererbt.

Wie das funktioniert, liest du im Abschnitt StructuredTaskScope und Scoped Values im o. g. Artikel.

Deprecations und Löschungen

In Java 20 wurden einige Methoden als "deprecated" markiert bzw. komplett außer Betrieb gesetzt.

java.net.URL constructors are deprecated

Die Konstruktoren von java.net.URL wurden als "deprecated" markiert. Stattdessen sollen die Methoden URI.create(...) und URI.toURL() verwendet werden. Hier ein Beispiel:

Alter Code:

URL url = new URL("https://www.happycoders.eu");
Code-Sprache: Java (java)

Neuer Code:

URL url = URI.create("https://www.happycoders.eu").toURL();
Code-Sprache: Java (java)

Für diese Änderung existiert kein JDK Enhancement Proposal.

Thread.suspend/resume changed to throw UnsupportedOperationException

Thread.suspend() und resume() wurden bereits in Java 1.2 als "deprecated" markiert, da die Methoden anfällig für Deadlocks sind. In Java 14 wurden die Methoden als "deprecated for removal" markiert.

Ab Java 20 werfen beide Methoden eine UnsupportedOperationException.

Für diese Änderung existiert kein JDK Enhancement Proposal.

Sonstige Änderungen in Java 20

In diesem Abschnitt findest du einige von mir ausgewählte kleinere Änderungen in Java 20, für die es keine JDK Enhancements Proposals gibt.

Javac Warns about Type Casts in Compound Assignments with Possible Lossy Conversions

Ich halte es für wichtig, diese scheinbar kleine Änderung hier prominent zu erwähnen. Denn viele Java-Entwickler kennen eine Besonderheit der sogenannten "Compound Assignment Operators" (+=, *=, usw.) nicht. Das kann zu unerwarteten Fehlern führen.

Was ist der Unterschied zwischen den folgenden Operationen?

a += b; a = a + b;
Code-Sprache: Java (java)

Die meisten Java-Entwickler werden sagen: Es gibt keinen.

Doch das ist falsch.

Wenn z. B. a ein short ist und b ein int, dann führt die zweite Zeile zu einem Compilerfehler:

java: incompatible types: possible lossy conversion from int to short
Code-Sprache: Klartext (plaintext)

Denn a + b ergibt ein int, welches ohne expliziten Cast nicht der short-Variablen a zugewiesen werden kann.

Die erste Zeile hingegen ist erlaubt, da der Compiler bei einem Compound Assignment einen impliziten Cast einfügt. Wenn a ein short ist, dann ist a += b äquivalent zu:

a = (short) (a + b);
Code-Sprache: Java (java)

Bei einem Cast von int auf short werden die linken 16 Bit abgeschnitten. Es gehen also Informationen verloren, wie folgendes Beispiel zeigt:

short a = 30_000; int b = 50_000; a += b; System.out.println("a = " + a);
Code-Sprache: Java (java)

Das Programm gibt nicht 80000 (hexadezimal 0x13880) aus, sondern 14464 (hexadezimal 0x3880).

Um Entwickler vor diesem potentiell unerwünschten Verhalten zu warnen, wurde in Java 20 (endlich!) eine entsprechende Compiler-Warnung eingeführt.

Für diese Erweiterung gibt es kein JDK Enhancement Proposal.

Idle Connection Timeouts for HTTP/2

Über die System Property jdk.httpclient.keepalive.timeout kann eingestellt werden, wie lange inaktive HTTP/1.1-Verbindungen offen gehalten werden.

Ab Java 20 gilt diese Eigenschaft auch für HTTP/2-Verbindungen.

Desweiteren wurde die System Property jdk.httpclient.keepalive.timeout.h2 hinzugefügt, mit der dieser Wert speziell für HTTP/2-Verbindungen überschrieben werden kann.

HttpClient Default Keep Alive Time is 30 Seconds

Wird die eben genannte System Property jdk.httpclient.keepalive.timeout nicht definiert, galt bis Java 19 ein Standardwert von 1.200 Sekunden. In Java 20 wurde der Standardwert auf 30 Sekunden herabgesetzt.

IdentityHashMap's Remove and Replace Methods Use Object Identity

IdentityHashMap ist eine spezielle Map-Implementierung, bei der Keys nicht dann als gleich gelten, wenn die equals()-Methode true ergibt, sondern dann, wenn die Key-Objekte identisch sind, also der Vergleich mittels ==-Operator true ergibt.

Als in Java 8 dem Map-Interface die Default-Methoden remove(Object key, Object value) und replace(K key, V oldValue, V newValue) hinzugefügt wurden, wurde allerdings vergessen diese Methoden in IdentityHashMap so zu überschreiben, dass sie == anstelle von equals() verwenden.

Dieser Fehler wurde nun korrigiert – nach achteinhalb Jahren. Dass der Bug so lange nicht aufgefallen ist, ist ein Zeichen dafür, dass IdentityHashMap allgemein wenig genutzt wird (und möglicherweise weitere Bugs enthält).

The new method Path::getExtension returns the file’s extension

Die Path-Klasse hat eine neue Methode getExtension(), die die Dateinamenserweiterung zurückliefert – also denjenigen Teil, der dem letzten Punkt im Dateinamen folgt. Hier ein Beispiel:

Path p = Path.of("/foo/bar.txt"); System.out.println(p.getExtension());
Code-Sprache: Java (java)

Dieses Beispiel gibt "txt" aus.

Befindet sich der Punkt ganz am Ende, gibt die Methode einen leeren String zurück. Enthält der Dateiname keinen Punkt, gibt die Methode null zurück.

Support Unicode 15.0

Der Unicode-Support wird in Java 20 auf Version 15.0 angehoben. Relevant ist das u. a. für die Klassen String und Character, die mit den neuen Zeichen, Codeblöcken und Schriftsystemen umgehen können müssen.

Vollständige Liste aller Änderungen in Java 20

Neben den oben aufgelisteten JEPs und sonstigen Änderungen enthält auch Java 20 zahlreiche kleinere Änderungen, die den Rahmen dieses Artikels sprengen würden. Eine vollständige Liste findest du in den Java 20 Release Notes.

Fazit

Mit "Scoped Values" erhalten wir in Java 20 ein sehr nützliches Konstrukt, um einem Thread und ggf. einer Gruppe von Kind-Threads für deren Lebensdauer einen nur lesbaren, Thread-spezifischen Wert zur Verfügung zu stellen.

Alle anderen JEPs sind minimal (oder gar nicht) veränderte Wiedervorlagen vorangegangener JEPs.

Das aktuelle Java 20 Early-Access Release kannst du hier herunterladen.

Du möchtest keinen HappyCoders.eu-Artikel verpassen und immer über neue Java-Features informiert werden? Dann klicke hier, um dich für den kostenlosen HappyCoders-Newsletter anzumelden.