java 19 featuresjava 19 features
HappyCoders Glasses

Java 19 Features
(mit Beispielen)

Sven Woltmann
Sven Woltmann
Aktualisiert: 23. Januar 2024

Java 19 wurde am 20. September 2022 veröffentlicht. Du kannst es 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 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) – JEP 427

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) – JEP 405

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) – JEP 425

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.

Structured Concurrency (Incubator) – JEP 428

Ebenfalls in Project Loom entwickelt und mit dem JDK Enhancement Proposal 428 in Java 19 zunächst als Incubator-Feature veröffentlicht wird die sogenannte "Structured Concurrency".

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, erfährst du im Hauptartikel über Structured Concurrency.

Foreign Function & Memory API (Preview) – JEP 424

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 FFMTestCode-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.

Vector API (Fourth Incubator) – JEP 426

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.

Automatic Generation of the CDS Archive

Application Class Data Sharing (kurz: „Application CDS” oder „AppCDS”) wurde in Java 10 eingeführt, die Konfiguration wurde in Java 13 deutlich vereinfacht.

Application CDS ermöglicht es, beim Betrieb mehrerer JVMs auf einer Maschine, die Klassen einer Anwendung einmalig in den Speicher zu laden und diesen Speicherbereich mit allen JVMs zu teilen. Dadurch wird Speicher gespart sowie Zeit für das Laden der .jar- und .class-Dateien und deren Umwandeln in ein Platform-spezifisches Binärformat.

Mit Java 19 wurde die Konfiguration von AppCDS noch einmal vereinfacht. Mit folgendem Parameter kann nun automatisch ein CDS-Archiv erzeugt oder aktualisiert werden.

Die Anwendung aus den Beispielen der oben verlinkten Artikel zu Java 10 und 13 kann nun wie folgt gestartet werden:

java -XX:+AutoCreateSharedArchive -XX:SharedArchiveFile=helloworld.jsa \
    -cp target/helloworld.jar eu.happycoders.appcds.MainCode-Sprache: Klartext (plaintext)

Das Shared Archive wird nun erzeugt, wenn es nicht existiert oder wenn es von einer älteren Java-Version erzeugt wurde.

Linux/RISC-V Port – JEP 422

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

Additional Date-Time Formats

Über die Methoden DateTimeFormatter.ofLocalizedDate(...), ofLocalizedTime(...) und ofLocalizedDateTime(...) und dem nachfolgenden Aufruf von withLocale(...) können wir uns einen Datum-/Zeit-Formatter erzeugen lassen. Das genaue Format steuern wir dabei über das Enum FormatStyle, das die Werte FULL, LONG, MEDIUM und SHORT annehmen kann.

In Java 19 kam die Methode ofLocalizedPattern(String requestedTemplate) hinzu, mit der wir auch flexible Formate definieren können. Hier ein Beispiel:

LocalDate now = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofLocalizedPattern("yMMM");
System.out.println("US:      " + formatter.withLocale(Locale.US).format(now));
System.out.println("Germany: " + formatter.withLocale(Locale.GERMANY).format(now));
System.out.println("Japan:   " + formatter.withLocale(Locale.JAPAN).format(now));Code-Sprache: Java (java)

Der Code gibt folgendes aus:

US:      Jan 2024
Germany: Jan. 2024
Japan:   2024年1月
Code-Sprache: Klartext (plaintext)

Für diese Änderung gibt es kein JDK Enhancement Proposal. Du findest sie in den JDK 19 Release Notes.

New System Properties for System.out and System.err

Seit Version 18 verwendet Java bei der Ausgabe nach System.out und System.err automatisch die Zeichenkodierung der Konsole bzw. des Terminals. Unter Linux ist das in der Regel UTF-8 und unter Windows die Code Page 437.

Speichere einmal folgendes Programm in der Datei Test.java:

public class Test {
  public static void main(String[] args) {
    System.out.println("Á é ö ß € ¼");
  }
}Code-Sprache: Java (java)

Wenn du dieses unter Linux startest, werden wahrscheinlich alle Zeichen korrekt angezeigt:

$ java Test.java
Á é ö ß € ¼Code-Sprache: Klartext (plaintext)

Wenn du das Programm hingegen unter Windows ausführst, siehst du höchstwahrscheinlich folgende Ausgabe (ein Fragezeichen statt dem Á und dem €):

C:\...>java Test.java
? é ö ß ? ¼Code-Sprache: Klartext (plaintext)

Das liegt daran, dass unter Windows standardmäßig die Code Page 437 aktiviert ist, die die entsprechenden Zeichen nicht enthält.

Du kannst die Windows-Konsole wie folgt auf UTF-8 umschalten:

C:\...>chcp 65001
Active code page: 65001Code-Sprache: Klartext (plaintext)

Wenn du das Programm erneut startest, siehst du nun alle Zeichen korrekt.

Falls die automatische Erkennung des verwendeten Zeichensatzes nicht funktionieren sollte, kannst du ihn ab Java 19 über folgende VM-Optionen z. B. auf UTF-8 einstellen:

-Dstdout.encoding=utf-8 -Dstderr.encoding=utf-8

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=utf-8 -Dstderr.encoding=utf-8"

Für diese Änderung gibt es kein JDK Enhancement Proposal. Du findest sie in den JDK 19 Release Notes.

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. Du kannst Java 19 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.