Java 19 Features mit Beispielen

Java 19 Features (mit Beispielen)

Autor-Bild
von Sven Woltmann – 13. Juni 2022

Java 19 befindet sich seit dem 9. Juni 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 20. September 2022 angepeilt. Die aktuelle Early-Access-Version kannst du hier herunterladen.

Die spannendste Neuerung sind für mich die virtuellen Threads, die seit mehreren Jahren im Rahmen von Project Loom entwickelt werden und nun endlich als Preview im JDK enthalten sind.

Virtuelle Threads sind die Voraussetzung für Structured Concurrency, ein weiteres spannendes neues Incubator-Feature in Java 19.

Für alle, die auf Nicht-Java-Code (z. B. die C-Standard-Bibliothek) zugreifen wollen, gibt es ebenfalls gute Nachrichten: Die Foreign Function & Memory API hat nach fünf Incubator-Runden nun auch das Preview-Stadium erreicht.

Wie immer verwende ich die englischen Bezeichnungen der JEPs.

New System Properties for System.out and System.err

Über eine Änderung, die man nicht in den Java 19 Feature-Ankündigungen, sondern nur tief vergraben in den Release Notes findet, sollte jeder Java-Entwickler Bescheid wissen.

Wenn du eine bestehende Anwendung mit Java 19 ausführst, kann es nämlich passieren, dass du auf der Konsole statt Umlauten und anderen Sonderzeichen nur noch Fragezeichen siehst.

Das liegt daran, dass ab Java 19 für die Ausgabe nach System.out und System.err das Standard-Encoding des Betriebssystems verwendet wird – unter Windows z. B. "Cp1252". Um die Ausgabe auf UTF-8 umzustellen, musst du beim Aufruf der Anwendung folgende VM-Optionen hinzufügen:

-Dstdout.encoding=utf8 -Dstderr.encoding=utf8

Wenn du das nicht bei jedem Programmstart machen möchtest, kannst du diese Einstellungen auch global festlegen, indem du die folgende Umgebungsvariable definierst (ja, diese beginnt mit einem Unterstrich):

_JAVA_OPTIONS="-Dstdout.encoding=utf8 -Dstderr.encoding=utf8"

New Methods to Create Preallocated HashMaps

Wenn wir eine ArrayList für eine vorab bekannte Anzahl an Elementen (z. B. 120) erzeugen wollen, dann können wir das seit jeher wie folgt tun:

List<String> list = new ArrayList<>(120);
Code-Sprache: Java (java)

Dadruch wird das der ArrayList zugrunde liegende Array direkt für 120 Elemente allokiert und muss nicht mehrfach vergrößert (also neu angelegt und umkopiert) werden, um die 120 Elemente einzufügen.

Ebenso können wir seit jeher eine HashMap wie folgt erzeugen:

Map<String, Integer> map = new HashMap<>(120);
Code-Sprache: Java (java)

Intuitiv würde man denken, diese HashMap biete Platz für 120 Mappings.

Das ist allerdings nicht der Fall!

Denn die HashMap wird mit einem Default-Load-Faktor von 0,75 initialisiert. Das bedeutet: Sobald die HashMap zu 75 % gefüllt ist, wird sie mit doppelter Größe neu aufgebaut ("rehashed"). Dadurch soll sichergestellt werden, dass die Elemente möglichst gleichmäßig auf die Buckets der HashMap verteilt sind und möglichst kein Bucket mehr als ein Element enthält.

Die mit einer Kapazität von 120 initialisierte HashMap kann also nur 120 × 0,75 = 90 Mappings aufnehmen.

Um eine HashMap für 120 Mappings zu erzeugen, musste man bisher die Kapazität selbst berechnen, indem man die Anzahl der Mappings durch den Load-Faktor teilt: 120 ÷ 0,75 = 160.

Eine HashMap für 120 Mappings musste also wie folgt angelegt werden:

// for 120 mappings: 120 / 0.75 = 160 Map<String, Integer> map = new HashMap<>(160);
Code-Sprache: Java (java)

Java 19 macht uns das einfacher – wir können stattdessen jetzt folgendes schreiben:

Map<String, Integer> map = HashMap.newHashMap(120);
Code-Sprache: Java (java)

Wenn wir uns den Quellcode der neuen Methoden anschauen, sehen wir, dass im Endeffekt das gleiche passiert, was wir vorher manuell gemacht haben:

public static <K, V> HashMap<K, V> newHashMap(int numMappings) { return new HashMap<>(calculateHashMapCapacity(numMappings)); } static final float DEFAULT_LOAD_FACTOR = 0.75f; static int calculateHashMapCapacity(int numMappings) { return (int) Math.ceil(numMappings / (double) DEFAULT_LOAD_FACTOR); }
Code-Sprache: Java (java)

Die newHashMap()-Methode wurde ebenfalls in LinkedHashMap und WeakHashMap eingebaut.

Zu dieser Erweiterung gibt es kein JDK Enhancement Proposal.

Preview- und Incubator-Features

Java 19 liefert uns insgesamt sechs Preview- und Incubator-Features, also Features, die noch nicht fertiggestellt sind, die von der Entwickler-Community aber bereits getestet werden können. Das Feedback der Community geht in der Regel in die Weiterentwicklung und Fertigstellung dieser Features ein.

Pattern Matching for switch (Third Preview)

Fangen wir mit einem Feature an, das bereits zwei Preview-Runden hinter sich hat. Das in Java 17 erstmals vorgestellte "Pattern Matching for switch" erlaubte uns Code wie den folgenden zu schreiben:

switch (obj) { case String s && s.length() > 5 -> System.out.println(s.toUpperCase()); case String s -> System.out.println(s.toLowerCase()); case Integer i -> System.out.println(i * i); default -> {} }
Code-Sprache: Java (java)

Wir können damit innerhalb eines switch-Statements prüfen, ob ein Objekt von einer bestimmten Klasse ist und ob es ggf. weitere Eigenschaften (wie im Beispiel: länger als 5 Zeichen) aufweist.

In Java 19 wurde mit JDK Enhancement Proposal 427 die Syntax des sogenannten "Guarded Patterns" (im Beispiel oben "String s && s.length() > 5") verändert. Statt && muss nun das neue Keyword when verwendet werden.

Das Beispiel von oben wird in Java 19 wie folgt notiert:

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); default -> {} }
Code-Sprache: Java (java)

when ist ein sogenanntes "contextual keyword" und hat damit nur innerhalb eines case-Labels eine Bedeutung. Solltest du in deinem Code Variablen oder Methoden mit dem Namen "when" haben, musst du daran nichts ändern.

Record Patterns (Preview)

Wir bleiben beim Thema "Pattern Matching" und kommen zu den "Record Patterns". Wenn das Thema "Records" neu für dich ist, empfehle ich dir zunächst den Artikel "Records in Java" zu lesen.

Was ein Record Pattern ist, erkläre ich am besten an einem Beispiel. Nehmen wir an, wir haben folgenden Record definiert:

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

Außerdem haben wir eine print()-Methode, die beliebige Objekte, unter anderem auch Positionen, ausgeben kann:

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

Falls du über die verwendete Schreibweise stolperst – sie wurde in Java 16 als "Pattern Matching for instanceof" eingeführt.

Record Pattern mit instanceof

Ab Java 19 ermöglicht uns JDK Enhancement Proposal 405 ein sogenanntes "Record Pattern" einzusetzen. Damit können wir den Code auch wie folgt schreiben:

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

Anstatt auf "Position position" zu matchen und im nachfolgenden Code auf position zuzugreifen, matchen wir nun auf "Position(int x, int y)" und können im folgenden Code direkt auf x und y zugreifen.

Record Pattern mit switch

Das ursprüngliche Beispiel können wir seit Java 17 auch als switch-Statement schreiben:

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

Auch im switch-Statement können wir seit Java 19 ein Record Pattern benutzen:

private void print(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)

Verschachtelte Record Patterns

Es ist auch möglich verschachtelte Records zu matchen – auch das möchte ich an einem Beispiel demonstrieren.

Wir definieren zunächst einen zweiten Record, Path, mit einer Start- und einer Zielposition:

public record Path(Position from, Position to) {}
Code-Sprache: Java (java)

Unsere print()-Methode kann nun mit Hilfe eines Record Patterns ganz einfach alle X- und Y-Koordinaten des Pfades ausgeben:

private void print(Object object) { if (object instanceof Path(Position(int x1, int y1), Position(int x2, int y2))) { System.out.println("object is a path, x1 = " + x1 + ", y1 = " + y1 + ", x2 = " + x2 + ", y2 = " + y2); } // else ... }
Code-Sprache: Java (java)

Auch das können wir alternativ als switch-Statement schreiben:

private void print(Object object) { switch (object) { case Path(Position(int x1, int y1), Position(int x2, int y2)) -> System.out.println("object is a path, x1 = " + x1 + ", y1 = " + y1 + ", x2 = " + x2 + ", y2 = " + y2); // other cases ... } }
Code-Sprache: Java (java)

Record Patterns bieten uns also eine elegante Möglichkeit nach einer Typprüfung auf die Elemente eines Records zuzugreifen.

Virtual Threads (Preview)

Die spannendeste Neuerung in Java 19 sind für mich "Virtual Threads". Diese werden seit einigen Jahren in Project Loom entwickelt und konnten bisher nur mit einem selbst compiliertem JDK getestet werden.

Mit JDK Enhancement Proposal 425 halten virtuelle Threads endlich Einzug in das offizielle JDK – und zwar direkt im Preview-Status, so dass keine wesentlichen Änderungen an der API mehr zu erwarten sind.

Warum wir virtuelle Threads brauchen, was sie sind, wie sie funktionieren und wie man sie einsetzt, erfährst du im Hauptartikel über virtuelle Threads, den du dir auf keinen Fall entgehen lassen solltest.

Foreign Function & Memory API (Preview)

Im Project Panama wird seit langem an einem Ersatz für das umständlich zu benutzende, fehleranfällige und langsame Java Native Interface (JNI) gearbeitet.

Bereits in Java 14 und Java 16 wurden die "Foreign Memory Access API" und die "Foreign Linker API" vorgestellt – beide zunächst einzeln im Incubator-Stadium. In Java 17 wurden diese APIs zur "Foreign Function & Memory API" (FFM API) vereint, die bis Java 18 im Incubator-Stadium blieb.

In Java 19 ist die neue API mit JDK Enhancement Proposal 424 endlich im Preview-Stadium angekommen, d. h. dass nur noch kleine Änderungen und Bugfixes vorgenommen werden. Damit ist es an der Zeit die neue API hier vorzustellen!

Die Foreign Function & Memory API ermöglicht den Zugriff auf nativen Speicher (also Speicher außerhalb des Java Heaps) sowie den Zugriff auf nativen Code (z. B. auf C-Libraries) direkt aus Java heraus.

Wie das funktioniert, zeige ich an einem Beispiel. Zu tief werde ich hier allerdings nicht in die Thematik einsteigen, da die meisten Java-Entwickler selten bis nie auf nativen Speicher und Code zugreifen müssen.

Hier ist ein einfaches Beispiel, das einen String im Off-Heap-Memory speichert und darauf die "strlen"-Funktion der C-Standard-Bibliothek aufruft:

public class FFMTest { 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.lookup("strlen").orElseThrow(), FunctionDescriptor.of(JAVA_LONG, ADDRESS)); // 3. Convert Java String to C string and store it in off-heap memory MemorySegment str = implicitAllocator().allocateUtf8String("Happy Coding!"); // 4. Invoke the foreign function long len = (long) strlen.invoke(str); System.out.println("len = " + len); } }
Code-Sprache: Java (java)

Interessant ist der FunctionDescriptor in Zeile 9: dieser definiert als ersten Parameter den Rückgabetyp der Funktion und als weitere Parameter die Argumente der Funktion. Der FunctionDescriptor sorgt dafür, dass alle Java-Typen ordnungsgemäß in C-Typen umgewandelt werden und umgekehrt.

Da die FFM API sich noch im Preview-Stadium befindet, müssen zum Compilieren und Starten ein paar zusätzliche Parameter angegeben werden:

$ javac --enable-preview -source 19 FFMTest.java $ java --enable-preview FFMTest
Code-Sprache: Klartext (plaintext)

Wer schon einmal mit JNI gearbeitet hat – und sich erinnert, wie viel Java- und C-Boilerplate-Code man schreiben und synchron halten musste – wird erkennen, dass sich der Aufwand für den Aufruf der nativen Funktion um Größenordnungen reduziert hat.

Wer sich tiefer mit der Materie beschäftigen möchte: im JEP findest du weitere, komplexere Beispiele.

Structured Concurrency (Incubator)

Wenn eine Aufgabe aus verschiedenen Teilaufgaben besteht, die parallel erledigt werden können (z. B. Zugriff auf Daten aus einer Datenbank, Aufruf einer Remote API und Laden einer Datei), so konnten wir hierfür bisher das Java-Executur-Framework einsetzen.

Das könnte dann z. B. so aussehen:

private final ExecutorService executor = Executors.newCachedThreadPool(); public Invoice createInvoice(int orderId, int customerId, String language) throws ExecutionException, InterruptedException { Future<Order> orderFuture = executor.submit(() -> loadOrderFromOrderService(orderId)); Future<Customer> customerFuture = executor.submit(() -> loadCustomerFromDatabase(customerId)); Future<String> invoiceTemplateFuture = executor.submit(() -> loadInvoiceTemplateFromFile(language)); Order order = orderFuture.get(); Customer customer = customerFuture.get(); String invoiceTemplate = invoiceTemplateFuture.get(); return Invoice.generate(order, customer, invoiceTemplate); }
Code-Sprache: Java (java)

Wir übergeben die drei Teilaufgaben an den Executor und warten auf die Teilergebnisse. Der Happy Path ist schnell implementiert. Aber wie behandeln wir Ausnahmen?

  • Wenn in einem Subtask ein Fehler auftritt – wie können wir dann die anderen abbrechen?
  • Wie können wir die Subtasks abbrechen, wenn die gesamte Rechnung nicht mehr benötigt wird?

Beides ist möglich, erfordert aber ziemlich komplexen, schwer wartbaren Code.

Und was, wenn wir Code dieser Art debuggen möchten? Ein Thread-Dump z. B. würde uns haufenweise Threads mit dem Namen "pool-X-thread-Y" liefern – wir wüssten aber nicht, welcher Pool-Thread zu welchem aufrufenden Threads gehört, da sich alle aufrufenden Threads den Thread-Pool des Executors teilen.

JDK Enhancement Proposal 428 führt eine API für sogenannte "structured concurrency" ein, ein Konzept, das die Implementierung, Lesbarkeit und Wartbarkeit von Code für Anforderungen dieser Art verbessern soll.

Mit Hilfe eines StructuredTaskScope können wir das Beispiel ab Java 19 wie folgt umschreiben:

Invoice createInvoice(int orderId, int customerId, String language) throws ExecutionException, InterruptedException { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { Future<Order> orderFuture = scope.fork(() -> loadOrderFromOrderService(orderId)); Future<Customer> customerFuture = scope.fork(() -> loadCustomerFromDatabase(customerId)); Future<String> invoiceTemplateFuture = scope.fork(() -> loadInvoiceTemplateFromFile(language)); scope.join(); scope.throwIfFailed(); Order order = orderFuture.resultNow(); Customer customer = customerFuture.resultNow(); String invoiceTemplate = invoiceTemplateFuture.resultNow(); return new Invoice(order, customer, invoiceTemplate); } }
Code-Sprache: Java (java)

Wir ersetzen also den im Scope der Klasse liegenden ExecutorService durch einen im Scope der Methode liegenden StructuredTaskScope – und executor.submit() durch scope.fork().

Mit scope.join() warten wir darauf, dass alle Tasks erledigt sind – oder mindestens einer fehlgeschlagen ist oder abgebrochen wurde. In den letztgenannten zwei Fällen wirft das anschließende throwIfFailed() eine ExecutionException bzw. eine CancellationException.

Der neue Ansatz bringt gegenüber dem alten folgende Verbesserungen:

  • Task und Subtasks bilden im Code eine abgeschlossene Einheit – es gibt keinen ExecutorService in einem höheren Scope. Die Threads kommen nicht aus einem Threadpool; stattdessen wird jeder Subtask in einem neuen virtuellen Thread ausgeführt.
  • Sobald in einem der Subtasks ein Fehler auftritt, werden alle anderen Subtasks abgebrochen.
  • Wenn der aufrufende Thread abgebrochen wird, werden auch die Subtasks abgebrochen.
  • Im Thread-Dump ist die Aufrufhierarchie zwischen aufrufendem Thread und Threads, die die Subtasks ausführen, ersichtlich.

Falls du das Beispiel selbst ausprobieren möchtest: Preview-Features müssen explizit freigeschaltet und Incubator-Module müssen explizit dem Modulpfad hinzugefügt werden. Wenn du den Code beispielsweise in einer Datei namens StructuredConcurrencyTest.java gespeichert hast, kannst du ihn wie folgt compilieren und starten:

$ javac --enable-preview -source 19 --add-modules jdk.incubator.concurrent StructuredConcurrencyTest.java $ java --enable-preview --add-modules jdk.incubator.concurrent StructuredConcurrencyTest
Code-Sprache: Klartext (plaintext)

Bitte beachte, dass Incubator-Features noch grundlegenden Änderungen unterliegen können.

Vector API (Fourth Incubator)

Die neue Vector-API hat nichts mit der java.util.Vector-Klasse zu tun. Tatsächlich geht es um eine neue API für mathematische Vektorrechnung und deren Abbildung auf moderne SIMD (Single-Instruction-Multiple-Data)-CPUs.

Die Vector-API ist seit Java 16 als Incubator Teil des JDK und wurde in Java 17 und Java 18 weiterentwickelt.

Mit JDK Enhancement Proposal 426 liefert Java 19 die vierte Iteration, in der die API um neue Vektoroperationen erweitert wurde – sowie um die Möglichkeit Vektoren in Memory-Segmenten (ein Feature der Foreign Function & Memory API) zu speichern und sie daraus zu lesen.

Incubator-Features können noch wesentlichen Änderungen unterliegen, ich werde die API daher hier nicht im Detail vorstellen. Das werde ich nachholen, sobald die Vector-API in den Preview-Status übergegangen ist.

Deprecations und Löschungen

In Java 19 wurden einige Funktionen als "deprecated" markiert bzw. außer Betrieb gesetzt.

Deprecation of Locale class constructors

In Java 19 wurden die öffentlichen Konstruktoren der Locale-Klasse als "deprecated" markiert.

Stattdessen sollten wir die neue statische Factory-Methode Locale.of() verwenden. Dadurch wird sichergestellt, dass es pro Locale-Konfiguration nur eine Instanz gibt.

Das folgende Beispiel zeigt die Nutzung der Factory-Methode im Vergleich zum Konstruktor:

Locale german1 = new Locale("de"); // deprecated Locale germany1 = new Locale("de", "DE"); // deprecated Locale german2 = Locale.of("de"); Locale germany2 = Locale.of("de", "DE"); System.out.println("german1 == Locale.GERMAN = " + (german1 == Locale.GERMAN)); System.out.println("germany1 == Locale.GERMANY = " + (germany1 == Locale.GERMANY)); System.out.println("german2 == Locale.GERMAN = " + (german2 == Locale.GERMAN)); System.out.println("germany2 == Locale.GERMANY = " + (germany2 == Locale.GERMANY));
Code-Sprache: Java (java)

Wenn du diesen Code startest, wirst du sehen, dass die über die Factory-Methode gelieferten Objekte identisch sind mit den Locale-Konstanten – die per Konstruktur erzeugten logischerweise nicht.

java.lang.ThreadGroup is degraded

In Java 14 und Java 16 wurden mehrere Thread- und ThreadGroup-Methoden als "deprecated for removal" markiert. Die Gründe dafür erfährst du in den verlinkten Abschnitten.

Die folgenden dieser Methoden wurden in Java 19 außer Betrieb gesetzt:

  • ThreadGroup.destroy() – der Aufruf dieser Methode wird ignoriert.
  • ThreadGroup.isDestroyed() – gibt immer false zurück.
  • ThreadGroup.setDaemon() – setzt das daemon-Flag, dies hat allerdings keine Wirkung mehr.
  • ThreadGroup.getDaemon() – gibt den Wert des ungenutzten daemon-Flags zurück.
  • ThreadGroup.suspend(), resume() und stop() werfen eine UnsupportedOperationException.

Sonstige Änderungen in Java 19

In diesem Abschnitt findest du Änderungen/Erweiterungen, die nicht für alle Java-Entwickler relevant sein dürften.

Linux/RISC-V Port

Aufgrund zunehmender Verbreitung von RISC-V-Hardware, wurde mit JEP 422 ein Port für die entsprechende Architektur zur Verfügung gestellt.

Vollständige Liste aller Änderungen in Java 19

Neben den in diesem Artikel präsentierten JDK Enhancement Proposals (JEPs) und Änderungen an den Klassenbibliotheken gibt es zahlreiche kleinere Änderungen, die den Rahmen dieses Artikels sprengen würden. Eine vollständige Liste findest du in den JDK 19 Release Notes.

Fazit

In Java 19 haben die lang ersehnten, in Project Loom entwickelten, virtuellen Threads endlich ihren Weg ins JDK gefunden (wenn auch zunächst im Preview-Stadium). Ich hoffe du bist ebenso begeistert wie ich und kannst es nicht abwarten virtuelle Threads in deinen Projekten einzusetzen!

Strukturierte Concurrency (noch im Incubator-Stadium) wird darauf aufbauend die Verwaltung von Tasks, die in parallele Subtasks aufgeteilt werden, deutlich vereinfachen.

Die in den letzten JDK-Versionen nach und nach weiterentwickelten Pattern-Matching-Möglichkeiten in instanceof und switch wurden um Record Patterns erweitert.

Die Preview- und Incubator Features "Pattern Matching for switch", "Foreign Function & Memory API" und "Vector API" wurden in die jeweils nächste Preview- bzw. Incubator-Runde geschickt.

Die Ausgabe auf die Konsole erfolgt standardmäßig im Standard-Encoding des Betriebssystems und muss ggf. per VM-Option umgestellt werden.

HashMaps bieten neue Factory-Methoden an, um Maps mit ausreichender Kapazität für eine vorgegebene Anzahl Mappings zu erstellen.

Diverse sonstige Änderungen runden wie immer das Release ab. Das aktuelle Java 19 Early-Access Release kannst du hier herunterladen.

Du möchtest keinen HappyCoders.eu-Artikel verpassen? Dann klicke hier, um dich für den kostenlosen HappyCoders-Newsletter anzumelden.