Java 17 Features mit Beispielen

Java 17 Features (mit Beispielen)

Autor-Bild
von Sven Woltmann – 28. Dezember 2021

Am 14. September 2021 war es endlich soweit: Nach den fünf "Zwischenversionen" Java 12 bis 15, die jeweils nur für ein halbes Jahr maintained wurden, wurde das aktuelle Long-Term-Support (LTS) Release, Java 17, veröffentlicht.

Oracle wird für Java 17 für mindestens fünf Jahre, also bis September 2026, kostenlose Upgrades zur Verfügung stellen – und bis September 2029 einen erweiterten, kostenpflichtigen Support.

In Java 17 wurden 14 JDK Enhancement Proposals umgesetzt. Ich habe die Änderungen nach Relevanz für die tägliche Programmierarbeit sortiert. Der Artikel beginnt mit Erweiterungen der Sprache und Änderungen am Modulsystem. Es folgen diverse Erweiterungen der JDK-Klassenbibliothek, Performance-Verbesserungen, neue Preview- und Incubator-Features, Deprecations und Löschungen und am Ende sonstige Änderungen, mit denen man in der täglichen Arbeit eher selten in Kontakt kommt.

Wie immer habe ich die englischen Bezeichnungen der JEPs und der sonstigen Änderungen verwendet. Eine Übersetzung ins Deutsche würde hier keinen Mehrwert bringen.

Sealed Classes

Die große Neuerung in Java 17 (neben dem Long-Term-Support) sind versiegelte Klassen und Interfaces.

Was versiegelte Klassen sind, wie sie genau funktionieren und warum wir sie brauchen, erkläre ich aufgrund des Umfangs des Themas in einem separaten Artikel: Sealed Classes in Java

(Sealed Classes wurden erstmals in Java 15 als Preview-Feature vorgestellt. In Java 16 wurden drei kleine Änderungen veröffentlicht. Mit dem JDK Enhancement Proposal 409 werden Sealed Classes in Java 17 ohne weitere Änderungen als produktionsreif deklariert.)

Strongly Encapsulate JDK Internals

In Java 9 wurde das Modulsystem (Project Jigsaw) eingeführt, insbesondere um Code besser modularisieren zu können und um die Sicherheit der Java-Plattform zu erhöhen.

Vor Java 16: Relaxed Strong Encapsulation

Bis Java 16 hatte dies nur wenige Auswirkungen auf existierenden Code, da die JDK-Entwickler für eine Übergangszeit den Modus "Relaxed Strong Encapsulation" bereitgestellt haben.

Dieser erlaubte es ohne Konfigurationsänderung per Deep Reflection auf nicht-öffentliche Klassen und Methoden von Paketen der JDK-Klassenbibliothek, die schon vor Java 9 existierten, zuzugreifen.

Das folgende Beispiel extrahiert die Bytes eines String, indem es dessen privates Feld value ausliest:

public class EncapsulationTest { public static void main(String[] args) throws ReflectiveOperationException { byte[] value = getValue("Happy Coding!"); System.out.println(Arrays.toString(value)); } private static byte[] getValue(String string) throws ReflectiveOperationException { Field VALUE = String.class.getDeclaredField("value"); VALUE.setAccessible(true); return (byte[]) VALUE.get(string); } }
Code-Sprache: Java (java)

Wenn wir dieses Programm mit Java 9 bis 15 aufrufen, erhalten wir folgende Ausgabe:

$ java EncapsulationTest.java WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by EncapsulationTest (file:/.../EncapsulationTest.java) to field java.lang.String.value WARNING: Please consider reporting this to the maintainers of EncapsulationTest WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release [72, 97, 112, 112, 121, 32, 67, 111, 100, 105, 110, 103, 33]
Code-Sprache: Klartext (plaintext)

Wir sehen zwar einige Warnungen, bekommen dann aber die gewünschten Bytes angezeigt.

Deep Reflection auf neue Pakete hingegen war standardmäßig nicht erlaubt und musste seit der Einführung des Modulsystems per "--add-opens" auf der Kommandozeile explizit erlaubt werden.

Das folgende Beispiel versucht die Klasse ConstantDescs aus dem in Java 12 (also nach der Einführung des Modulsystems) hinzugekommenen Paket java.lang.constant über deren privaten Constructor zu instanziieren:

Constructor<ConstantDescs> constructor = ConstantDescs.class.getDeclaredConstructor(); constructor.setAccessible(true); ConstantDescs constantDescs = constructor.newInstance();
Code-Sprache: Java (java)

Das Programm bricht mit folgender Fehlermeldung ab:

$ java ConstantDescsTest.java Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make private java.lang.constant.ConstantDescs() accessible: module java.base does not "opens java.lang.constant" to unnamed module @6c3f5566 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:361) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:301) at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:189) at java.base/java.lang.reflect.Constructor.setAccessible(Constructor.java:182) at ConstantDescsTest.main(ConstantDescsTest.java:7)
Code-Sprache: Klartext (plaintext)

Um das Programm lauffähig zu machen, müssen wir per --add-opens das neue Paket für Deep Reflection öffnen:

$ java --add-opens java.base/java.lang.constant=ALL-UNNAMED ConstantDescsTest.java
Code-Sprache: Klartext (plaintext)

Der Code läuft dann fehlerfrei und ohne Warnungen durch.

Seit Java 16: Standardmäßig Strong Encapsulation + Optional Relaxed Strong Encapsulation

In Java 16 wurde der Standardmodus von "Relaxed Strong Encapsulation" auf "Strong Encapsulation" geändert. Seither musste auch der Zugriff auf Pre-Java-9-Pakete explizit erlaubt werden.

Wenn wir das erste Beispiel unter Java 16 ausführen, ohne den Zugriff explizit zu erlauben, erhalten wir folgende Fehlermeldung:

$ java EncapsulationTest.java Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @62fdb4a6 at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:357) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:177) at java.base/java.lang.reflect.Field.setAccessible(Field.java:171) at EncapsulationTest.getValue(EncapsulationTest.java:12) at EncapsulationTest.main(EncapsulationTest.java:6)
Code-Sprache: Klartext (plaintext)

Java 16 bot allerdings noch einen Workaround: Per VM-Option --illegal-access=permit konnte auf "Relaxed Strong Encapsulation" zurückgeschaltet werden:

$ java --illegal-access=permit EncapsulationTest.java Java HotSpot(TM) 64-Bit Server VM warning: Option --illegal-access is deprecated and will be removed in a future release. WARNING: An illegal reflective access operation has occurred WARNING: Illegal reflective access by EncapsulationTest (file:/.../EncapsulationTest.java) to field java.lang.String.value WARNING: Please consider reporting this to the maintainers of EncapsulationTest WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations WARNING: All illegal access operations will be denied in a future release [72, 97, 112, 112, 121, 32, 67, 111, 100, 105, 110, 103, 33]
Code-Sprache: Klartext (plaintext)

Seit Java 17: Ausschließlich Strong Encapsulation

Per JDK Enhancement Proposal 403 wird diese Möglichkeit ab Java 17 entfernt. Die VM-Option --illegal-access wurde mit einer Warnung versehen, und der Zugriff auf String.value ist standardmäßig nicht mehr möglich:

java --illegal-access=permit EncapsulationTest.java OpenJDK 64-Bit Server VM warning: Ignoring option --illegal-access=permit; support was removed in 17.0 Exception in thread "main" java.lang.reflect.InaccessibleObjectException: Unable to make field private final byte[] java.lang.String.value accessible: module java.base does not "opens java.lang" to unnamed module @3e77a1ed at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354) at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297) at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178) at java.base/java.lang.reflect.Field.setAccessible(Field.java:172) at EncapsulationTest.getValue(EncapsulationTest.java:12) at EncapsulationTest.main(EncapsulationTest.java:6)
Code-Sprache: Klartext (plaintext)

Wer ab Java 17 Deep Reflection einsetzen will, muss dies nun explizit mit --add-opens erlauben:

$ java --add-opens java.base/java.lang=ALL-UNNAMED EncapsulationTest.java [72, 97, 112, 112, 121, 32, 67, 111, 100, 105, 110, 103, 33]
Code-Sprache: Klartext (plaintext)

Das Programm läuft, und wir sehen keine Warnungen mehr – die lange Übergangsphase seit Java 9 ist damit abgeschlossen.

Add java.time.InstantSource

Die Klasse java.time.Clock ist äußerst nützlich, um Tests zu schreiben, die zeitabhängige Funktionalität überprüfen.

Wenn Clock z. B. per Dependency Injection in die Anwendungsklassen injiziert wird, kann man diese in Tests mocken oder durch Clock.fixed() eine feste Uhrzeit für die Testausführung festlegen.

Da Clock die Methode getZone() bereitstellt, muss man sich immer Gedanken darüber machen, mit welcher konkreten Zeitzone man ein Clock-Objekt instanziiert.

Um alternative, Zeitzonen-unabhängige Zeitquellen zu ermöglichen, wurde in Java 17 das Interface java.time.InstantSource aus Clock extrahiert, welches zum Abfragen der Zeit nur noch die Methoden instant() und millis() bereitstellt, wobei millis() bereits als Default-Methode implementiert ist.

Die Timer-Klasse im folgenden Beispiel nutzt InstantSource, um die Start- und Endzeit der Ausführung eines Runnable festzustellen und daraus die Dauer der Ausführung zu berechnen:

public class Timer { private final InstantSource instantSource; public Timer(InstantSource instantSource) { this.instantSource = instantSource; } public Duration measure(Runnable runnable) { Instant start = instantSource.instant(); runnable.run(); Instant end = instantSource.instant(); return Duration.between(start, end); } }
Code-Sprache: Java (java)

In Produktion können wir Timer mit der System-Uhr instanziieren (wobei wir uns mangels alternativer InstantSource-Implementierungen Gedanken um die Zeitzone machen müssen – wir nehmen die Standardzeitzone des Systems):

Timer timer = new Timer(Clock.systemDefaultZone());
Code-Sprache: Java (java)

Testen können wir die measure()-Methode, indem wir InstantSource mocken, deren instant()-Methode zwei feste Werte zurückgeben lassen und den Rückgabewert von measure() mit der Differenz dieser Werte vergleichen:

@Test void shouldReturnDurationBetweenStartAndEnd() { InstantSource instantSource = mock(InstantSource.class); when(instantSource.instant()) .thenReturn(Instant.ofEpochMilli(1_640_033_566_000L)) .thenReturn(Instant.ofEpochMilli(1_640_033_567_750L)); Timer timer = new Timer(instantSource); Duration duration = timer.measure(() -> {}); assertThat(duration, is(Duration.ofMillis(1_750))); }
Code-Sprache: Java (java)

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

Hex Formatting and Parsing Utility

Um hexadezimale Zahlen auszugeben, konnten wir bisher die toHexString()-Methode der Klassen Integer, Long, Float und Double verwenden – oder String.format() einsetzen. Der folgende Code zeigt ein paar Beispiele:

System.out.println(Integer.toHexString(1_000)); System.out.println(Long.toHexString(100_000_000_000L)); System.out.println(Float.toHexString(3.14F)); System.out.println(Double.toHexString(3.14159265359)); System.out.println( "%x - %x - %a - %a".formatted(1_000, 100_000_000_000L, 3.14F, 3.14159265359));
Code-Sprache: Java (java)

Der Code führt zu folgender Ausgabe:

3e8 174876e800 0x1.91eb86p1 0x1.921fb54442eeap1 3e8 - 174876e800 - 0x1.91eb86p1 - 0x1.921fb54442eeap1
Code-Sprache: Klartext (plaintext)

Parsen konnten wir hexadezimale Zahlen mit den jeweiligen Pendants:

Integer.parseInt("3e8", 16); Long.parseLong("174876e800", 16); Float.parseFloat("0x1.91eb86p1"); Double.parseDouble("0x1.921fb54442eeap1");
Code-Sprache: Java (java)

Java 17 stellt die neue Klasse java.util.HexFormat zur Verfügung, mit der Darstellen und Parsen hexadezimaler Zahlen über eine einheitliche API abgebildet wird. HexFormat unterstützt dabei alle primitiven Ganzzahlen (int, byte, char, long, short) sowie byte-Arrays – allerdings keine Fließkommazahlen.

Hier ein Beispiel für die Umwandlung in hexadezimale Zahlen:

HexFormat hexFormat = HexFormat.of(); System.out.println(hexFormat.toHexDigits('A')); System.out.println(hexFormat.toHexDigits((byte) 10)); System.out.println(hexFormat.toHexDigits((short) 1_000)); System.out.println(hexFormat.toHexDigits(1_000_000)); System.out.println(hexFormat.toHexDigits(100_000_000_000L)); System.out.println(hexFormat.formatHex(new byte[] {1, 2, 3, 60, 126, -1}));
Code-Sprache: Java (java)

Die Ausgabe lautet:

0041 0a 03e8 000f4240 000000174876e800 0102033c7eff
Code-Sprache: Klartext (plaintext)

Es fällt auf, dass die Ausgabe jeweils mit vorangestellten Nullen erfolgt.

Die Darstellung können wir z. B. wie folgt anpassen:

HexFormat hexFormat = HexFormat.ofDelimiter(" ").withPrefix("0x").withUpperCase();
Code-Sprache: Java (java)
  • ofDelimiter() legt ein Trennzeichen für die Formatierung von Byte-Arrays fest.
  • withPrefix() definiert ein Prefix – allerdings nur für Byte-Arrays!
  • withUpperCase() schaltet die Ausgabe auf Großbuchstaben um.

Die Ausgabe lautet nun:

0041 0A 03E8 000F4240 000000174876E800 0x01 0x02 0x03 0x3C 0x7E 0xFF
Code-Sprache: Klartext (plaintext)

Die vorangestellten Nullen lassen sich nicht entfernen.

Ganzzahlen können wir wie folgt parsen:

int i = HexFormat.fromHexDigits("F4240"); long l = HexFormat.fromHexDigitsToLong("174876E800");
Code-Sprache: Java (java)

Entsprechende Methoden für char, byte und short existieren nicht.

Byte-Arrays parsen können wir z. B. wie folgt:

HexFormat hexFormat = HexFormat.ofDelimiter(" ").withPrefix("0x").withUpperCase(); byte[] bytes = hexFormat.parseHex("0x01 0x02 0x03 0x3C 0x7E 0xFF");
Code-Sprache: Java (java)

Es gibt noch weitere Methoden, um z. B. nur einen Teilstring zu parsen. Eine vollständige Dokumentation findest du im HexFormat-JavaDoc.

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

Context-Specific Deserialization Filters

Deserialisierung von Objekten stellt ein erhebliches Sicherheitsrisiko dar. Böswillige Angreifer können über den zu deserialisierenden Datenstrom Objekte konstruieren, über die sie letztendlich beliebigen Code in beliebigen (auf dem Classpath verfügbaren) Klassen ausführen können.

In Java 9 wurden erstmals Deserialisierungsfilter eingeführt, d. h. die Möglichkeit festzulegen, welche Klassen deserialisiert werden dürfen (bzw. welche nicht).

Bisher gab es zwei Möglichkeiten Deserialisierungsfilter zu definieren:

  • per ObjectInputStream.setObjectInputFilter() für jede Deserialisierung separat,
  • systemweit einheitlich per System Property jdk.serialFilter oder per gleichnamiger Security Property in der Datei conf/security/java.properties.

Bei komplexen Anwendungen, insbesondere bei solchen mit Drittanbieter-Bibliotheken, welche ebenfalls Deserialisierungs-Code enthalten, sind diese Varianten nicht zufriedenstellend. So kann bspw. die Deserialisierung in Drittanbieter-Code nicht per ObjectInputStream.setObjectInputFilter() konfiguriert werden (es sei denn, man ändert deren Quellcode), sondern nur global.

JDK Enhancement Proposal 415 macht es ab Java 17 möglich, Deserialisierungsfilter kontextspezifisch festzulegen, z. B. für einen bestimmten Thread oder basierend auf dem Call Stack für eine bestimmte Klasse, ein Modul, oder eine Drittanbieter-Bibliothek.

Die Konfiguration der Filter ist nicht ganz einfach und würde den Rahmen dieses Artikels sprengen. Details findest du im oben verlinkten JEP.

JDK Flight Recorder Event for Deserialization

Ab Java 17 ist es außerdem möglich die Deserialisierung von Objekten über den JDK Flight Recorder (JFR) zu überwachen.

Deserialisierungsevents sind per Default deaktiviert und müssen über den Event-Bezeichner jdk.Deserialization in der JFR-Konfigurationsdatei aktiviert werden (ein Beispiel dazu findest du im unten verlinkten Artikel).

Wenn ein Deserialisierungsfilter aktiviert ist, enthält das JFR-Event die Angabe darüber, ob die Deserialisierung ausgeführt oder abgewiesen wurde.

Detailliertere Informationen und ein Beispiel findest du im Artikel "Monitoring Deserialization to Improve Application Security".

Die Flight Recorder Events für die Deserialisierung sind nicht Teil des o. g. JDK Enhancement Proposals; auch existiert für sie kein separater JEP.

Enhanced Pseudo-Random Number Generators

Bisher war es aufwändig die Zufallszahlen-generierenden Klassen Random und SplittableRandom in einer Anwendung auszutauschen (oder gar durch andere Algorithmen zu ersetzen) obwohl sie einen größtenteils übereinstimmenden Satz von Methoden anbieten (z. B. nextInt(), nextDouble() und Stream-erzeugende Methoden wie ints() und longs()).

Die Klassenhierarchie sah bisher so aus:

Pre-Java 17 Pseudo-Random Number Generators
Pre-Java 17 Pseudo-Random Number Generators

Durch JDK Enhancement Proposal 356 wurde in Java 17 ein Framework von voneinander erbenden Interfaces für die bestehenden und für neue Algorithmen eingeführt, so dass die konkreten Algorithmen in Zukunft einfach austauschbar sind:

Java 17 Pseudo-Random Number Generators
Java 17 Pseudo-Random Number Generators

Die allen Zufallszahlengeneratoren gemeinsamen Methoden wie nextInt() und nextDouble() sind in RandomGenerator definiert. Sofern du nur diese Methoden benötigst, solltest du also in Zukunft immer dieses Interface verwenden.

Das Framework enthält drei neue Typen von Zufallszahlengeneratoren:

  • JumpableGenerator: bietet Methoden, um eine große Anzahl Zufallszahlen (z. B. 264) zu überspringen.
  • LeapableGenerator: bietet Methoden, um eine sehr große Anzahl Zufallszahlen (z. B. 2128) zu überspringen.
  • ArbitrarilyJumpableGenerator: bietet zusätzliche Methoden, um eine beliebige Anzahl Zufallszahlen zu überspringen.

Außerdem wurde in den existierenden Klassen duplizierter Code eliminiert und Code in nicht öffentliche abstrakte Klassen (nicht sichtbar im Klassendiagramm) extrahiert, um ihn für zukünftige Implementierungen von Zufallszahlengeneratoren wiederverwendbar zu machen.

Neue Zufallszahlengeneratoren können in Zukunft über das Service Provider Interface (SPI) hinzugefügt und über RandomGeneratorFactory instanziiert werden.

Performance

Java 17 bringt mit asynchronem Logging eine längst überfällige Performance-Verbesserung des in Java 9 eingeführten Unified JVM Logging Systems.

Unified Logging Supports Asynchronous Log Flushing

Asynchrones Logging ist ein Feature, das alle Java-Logging-Frameworks unterstützen. Dabei werden Log-Nachrichten durch den Anwendungsthread zunächst in eine Queue geschrieben. Ein separater I/O-Thread leitet sie dann an den konfigurierten Ausgang (Konsole, Datei oder Netzwerk) weiter.

Der Anwendungs-Thread muss so nicht darauf warten, dass das I/O-Subsytem die Nachricht verarbeitet hat.

Ab Java 17 kann auch für die JVM selbst asynchrones Logging aktiviert werden. Dies geschieht über folgende VM-Option:

-Xlog:async

Die Logging-Queue ist auf eine festgelegte Größe beschränkt. Wenn die Anwendung mehr Log-Nachrichten sendet als der I/O-Thread abarbeiten kann, läuft die Queue voll. Weitere Nachrichten verwirft sie dann kommentarlos.

Die Größe der Queue kann über folgende VM-Option angepasst werden:

-XX:AsyncLogBufferSize=<Bytes>

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

Preview- und Incubator-Features

Auch wenn Java 17 ein Long-Term-Support (LTS) Release darstellt, enthält es Preview- und Incubator-Features, die voraussichtlich in einem der nächsten "Zwischen-Releases" Produktionsreife erlangen werden. Wer nur LTS-Releases einsetzt, muss also mindestens bis Java 23 warten, um diese Features einzusetzen.

Pattern Matching for switch (Preview)

In Java 16 wurde "Pattern Matching for instanceof" eingeführt. Damit wurden explizite Casts nach instanceof-Prüfungen überflüssig. Das ermöglicht z. B. Code wie den folgenden:

if (obj instanceof String s) { if (s.length() > 5) { System.out.println(s.toUpperCase()); } else { System.out.println(s.toLowerCase()); } } else if (obj instanceof Integer i) { System.out.println(i * i); }
Code-Sprache: Java (java)

Durch JDK Enhancement Proposal 406 lässt sich die Prüfung, ob ein Objekt eine Instanz einer bestimmten Klasse ist, in Zukunft auch als switch-Statement (oder -Ausdruck) schreiben.

Pattern Matching für switch-Statements

Hier das Beispiel von oben umgeschrieben in ein switch-Statement:

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

Auffällig ist, dass der default-Fall angegeben werden muss – in diesem Fall mit einem leeren Codeblock, da nur für Strings und Integers eine Aktion ausgeführt werden soll.

Deutlich lesbarer wird der Code, wenn wir case- und if-Ausdrücke per logischem "und" kombinieren (das nennt sich dann "guarded pattern"):

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)

Wichtig dabei ist, dass ein sogenanntes "dominierendes Pattern" nach einem "dominierten Pattern" stehen muss. Im Beispiel dominiert das kürzere Pattern aus Zeile 3 "String s" das längere aus Zeile 2.

Wenn wir diese Zeilen vertauschen würden, sähe das so aus:

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

Der Compiler würde in diesem Fall Zeile 3 mit folgender Fehlermeldung bemängeln:

Label is dominated by a preceding case label 'String s'

Der Grund dafür ist, dass nun jeder String – egal welcher Länge – durch das Pattern "String s" (Zeile 2) gematcht wird und gar nicht erst bis zur zweiten Fallprüfung (Zeile 3) kommt.

Pattern Matching für switch Expressions

Pattern Matching kann auch für switch-Ausdrücke (also switch mit Rückgabewert) eingesetzt werden:

String output = switch (obj) { case String s && s.length() > 5 -> s.toUpperCase(); case String s -> s.toLowerCase(); case Integer i -> String.valueOf(i * i); default -> throw new IllegalStateException("Unexpected value: " + obj); };
Code-Sprache: Java (java)

Hierbei muss der default-Fall einen Wert zurückgeben, da sonst der Rückgabewert des switch-Ausdrucks undefiniert sein könnte – oder, wie im Beispiel, eine Exception werfen.

Vollständigkeitsprüfung mit Sealed Classes

Bei der Verwendung von Sealed Classes kann der Compiler übrigens prüfen, ob switch-Statement bzw. -Ausdruck vollständig sind. Wenn das der Fall ist, wird ein default-Fall nicht benötigt.

Das hat noch einen nicht sofort ersichtlichen Vorteil: Wird eines Tages die versiegelte Hierarchie erweitert, erkennt der Compilier den dann unvollständigen switch-Ausdruck, und man wird gezwungen sein, diesen zu vervollständigen. Das erspart einem unbemerkte Fehler.

"Pattern Matching for switch" wird in Java 18 noch einmal als Preview-Feature vorgelegt werden und vorraussichtlich in Java 19 Produktionsreife erlangen.

Foreign Function & Memory API (Incubator)

Seit Java 1.1 bietet das Java Native Interface (JNI) die Möglichkeit von Java heraus nativen C-Code aufzurufen. JNI ist jedoch äußert aufwändig in der Implementierung und langsam in der Ausführung.

Um einen JNI-Ersatz zu schaffen, wurde Project Panama ins Leben gerufen. Konkrete Ziele dieses Projekts sind a) den Aufwand der Implementierung zu vereinfachen (90 % der Arbeit soll eliminiert werden) und b) die Verbesserung der Leistung (um Faktor 4 bis 5).

In den vergangenen drei Java-Versionen wurden zwei neue APIs jeweils im Inkubator-Stadium vorgestellt:

  1. Die Foreign Memory Access API (eingeführt in Java 14, verfeinert in Java 15 und Java 16),
  2. Die Foreign Linker API (eingeführt in Java 16).

Durch JDK Enhancement Proposal 412 wurden beide APIs in Java 17 zur "Foreign Function & Memory API" zusammengefasst.

Diese befindet sich nach wie vor im Inkubator-Stadium, kann also noch wesentlichen Änderungen unterliegen. Ich werde die neue API im Artikel über Java 19 vorstellen, wenn sie das Preview-Stadium erreicht hat.

Vector API (Second Incubator)

Bei der Vector-API geht es – wie im Artikel über Java 16 bereits beschrieben – nicht um die alte java.util.Vector-Klasse, sondern um die Abbildung mathematischer Vektorrechnungen auf moderne CPU-Architekturen mit Single-Instruction-Multiple-Data-(SIMD)-Support.

Durch JDK Enhancement Proposal 414 wurde die Leistung verbessert und die API erweitert, z. B. um Unterstützung von Character (bisher wurden Byte, Short, Integer, Long, Float und Double unterstützt).

Da Features im Incubator-Status noch wesentlichen Änderungen erfahren können, werde ich das Feature dann vorstellen, wenn es den Preview-Status erreicht.

Deprecations und Löschungen

In Java 17 wurden wieder einige veraltete Features als "deprecated for removal" markiert bzw. vollständig entfernt.

Deprecate the Applet API for Removal

Java Applets werden von keinem modernen Webbrowser mehr unterstützt und wurden bereits in Java 9 als "deprecated" markiert.

Durch JDK Enhancement Proposal 398 werden sie in Java 17 als "deprecated for removal" markiert. Das bedeutet, dass sie in einem der nächsten Releases vollkommen entfernt werden.

Deprecate the Security Manager for Removal

Der Security Manager ist seit Java 1.0 Bestandteil der Plattform und sollte in erster Linie den Rechner und die Daten der User vor heruntergeladenen Java-Applets schützen. Diese wurden in einer Sandbox gestartet, in der der Security Manager den Zugriff auf Ressourcen wie das Dateisystem oder das Netzwerk grundsätzlich verwehrte.

Wie im vorangegangenen Abschnitt beschrieben, wurden Java Applets als “deprecated for removal” gekennzeichnet; dieser Aspekt des Security Managers wird also nicht länger von Bedeutung sein.

Neben der Browser-Sandbox, die den Zugriff auf Ressourcen generell verwehrte, konnte der Security Manager über Policy Files auch Server-Anwendungen absichern. Beispiele dafür sind Elasticsearch und Tomcat.

Allerdings besteht daran kein allzu großes Interesse mehr, da die Konfiguration kompliziert ist und sich Sicherheit über das Java-Modulsystem oder Isolierung durch Containerisierung heutzutage besser realisieren lässt.

Darüber hinaus stellt der Security Manager einen nicht unerheblichen Wartungsaufwand dar. Für alle Erweiterungen an der Java-Klassenbibliothek muss evaluiert werden, inwieweit diese über den Security Manager abgesichert werden müssen.

Aus diesen Gründen wurde der Security Manager durch JDK Enhancement Proposal 411 in Java 17 als "deprecated for removal" eingestuft.

Wann der Security Manager vollständig entfernt wird, steht noch nicht fest. In Java 18 wird er noch enthalten sein.

Remove RMI Activation

Remote Method Invocation ist eine Technologie zum Aufruf von Methoden auf "entfernten Objekten", also Objekten auf einer anderen JVM.

Durch RMI Activation können Objekte, die auf der Ziel-JVM zerstört wurden, automatisch wieder instanziiert werden, sobald auf sie zugegriffen wird. Dies soll eine Fehlerbehandlung auf der Client-Seite überflüssig machen.

RMI Activation ist jedoch relativ komplex und führt zu laufenden Wartungskosten; zudem ist es praktisch ungenutzt, wie Analysen von Open-Source-Projekten und Foren wie StackOverflow gezeigt haben.

Aus diesem Grund wurde RMI Activation in Java 15 als "veraltet" markiert und in Java 17 durch JDK Enhancement Proposal 407 vollständig entfernt.

Remove the Experimental AOT and JIT Compiler

In Java 9 wurde Graal als experimenteller Ahead-of-Time (AOT) Compiler dem JDK hinzugefügt. In Java 10 wurde Graal dann auch als Just-in-Time (JIT) Compiler verfügbar gemacht.

Allerdings wurde beide Features seither wenig genutzt. Da der Wartungsaufwand erheblich ist, wurde Graal in den von Oracle veröffentlichten JDK 16-Builds entfernt. Da sich niemand darüber beschwert hat, wurden sowohl AOT- als auch JIT-Compiler in Java 17 mit JDK Enhancement Proposal 410 vollständig entfernt.

Das zur Anbindung von Graal genutzte Java-Level JVM-Compiler-Interface (JVMCI) wurde nicht entfernt, und Graal selbst wird auch weiterentwickelt. Um Graal als AOT- oder JIT-Compiler zu verwenden, kannst du die Java-Distribution GraalVM herunterladen.

Sonstige Änderungen in Java 17

In diesem Abschnitt findest du kleinere Änderungen der Java-Klassenbibliothek, mit denen du nicht täglich in Kontakt kommen wirst. Dennoch empfehle ich dir sie zumindest einmal zu überfliegen, so dass du weißt, wo du nachschauen musst, wenn du eine entsprechende Funktion benötigst.

New API for Accessing Large Icons

Hier ist eine kleine Swing-Anwendung, die unter Windows das Dateisystem-Icon des Verzeichnisses C:\Windows darstellt:

FileSystemView fileSystemView = FileSystemView.getFileSystemView(); Icon icon = fileSystemView.getSystemIcon(new File("C:\\Windows")); JFrame frame = new JFrame(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.getContentPane().add(new JLabel(icon)); frame.pack(); frame.setVisible(true);
Code-Sprache: Java (java)

Das Icon hat eine Größe von 16 x 16 Pixeln, und es gab bisher keine Möglichkeit ein hochauflösenderes Icon anzuzeigen.

In Java 17 kam die Methode getSystemIcon(File f, int width, int height) hinzu, mit der du die Größe des Icons angeben kannst:

Icon icon = fileSystemView.getSystemIcon(new File("C:\\Windows"), 512, 512);
Code-Sprache: Java (java)

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

Add support for UserDefinedFileAttributeView on macOS

Der folgende Code zeigt, wie erweiterte Attribute einer Datei geschrieben und gelesen werden können:

Path path = ... UserDefinedFileAttributeView view = Files.getFileAttributeView(path, UserDefinedFileAttributeView.class); // Write the extended attribute with name "foo" and value "bar" view.write("foo", StandardCharsets.UTF_8.encode("bar")); // Print a list of all extended attribute names System.out.println("attribute names: " + view.list()); // Read the extended attribute "foo" ByteBuffer byteBuffer = ByteBuffer.allocate(1024); view.read("foo", byteBuffer); byteBuffer.flip(); String value = StandardCharsets.UTF_8.decode(byteBuffer).toString(); System.out.println("value of 'foo': " + value);
Code-Sprache: Java (java)

Diese Funktionalität existiert seit Java 7, wurde jedoch auch macOS bisher nicht unterstützt. Seit Java 17 ist die Funktion nun auch für macOS verfügbar.

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

System Property for Native Character Encoding Name

Über die System Property "native.encoding" kann ab Java 17 die Standard-Zeichenkodierung des Betriebssystems abgerufen werden:

System.out.println("native encoding: " + System.getProperty("native.encoding"));
Code-Sprache: Java (java)

Unter Windows wird Cp1252 ausgegeben, unter Linux und macOS UTF-8.

Rufst du diesen Code mit Java 16 oder früher auf, wird null ausgegeben.

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

Restore Always-Strict Floating-Point Semantics

Ein nahezu unbekanntes Java-Keyword ist strictfp. Es wird in Klassendefinitionen verwendet, um Fließkommaoperationen innerhalb einer Klasse "strict" zu machen. Das bedeutet, dass sie auf allen Architekturen zu den gleichen, vorhersagbaren Resultaten führen.

Strikte Fließkomma-Semantik war das Standardverhalten vor Java 1.2 (also vor mehr als 20 Jahren).

Ab Java 1.2 wurde standarmäßig die "Standard-Fließkomma-Semantik" verwendet, die je nach Prozessor-Architektur zu leicht abweichenden Ergebnissen führen konnte. Dafür war sie insbesondere auf dem damals verbreiteten x87-Gleitkomma-Koprozessor performanter, da dieser für die strikte Semantik zusätzliche Operationen ausführen musste (weitere Details findest du in diesem Wikipedia-Artikel).

Wer ab Java 1.2 weiterhin strikt rechnen wollte, musste dies durch das Keyword strictfp in der Klassendefinition kennzeichnen:

public strictfp class PredictiveCalculator { // ... }
Code-Sprache: Java (java)

Moderne Hardware kann die strikte Fließkomma-Semantik ohne Performance-Einbußen durchführen, so dass im JDK Enhancement Proposal 306 entschieden wurde, diese ab Java 17 wieder zur Standard-Semantik zu machen.

Das strictfp-Keyword ist damit überflüssig. Die Verwendung führt zu einer Compiler-Warnung:

$ javac PredictiveCalculator.java PredictiveCalculator.java:3: warning: [strictfp] as of release 17, all floating-point expressions are evaluated strictly and 'strictfp' is not required
Code-Sprache: Klartext (plaintext)

New macOS Rendering Pipeline

Apple hat 2018 die bisher von Java Swing für das Rendering unter macOS eingesetzte OpenGL-Bibliothek als "deprecated" markiert und das Metal-Framework als deren Nachfolger vorgestellt.

Durch JDK Enhancement Proposal 382 wird die Swing-Rendering-Pipeline für macOS auf die Metal API umgestellt.

macOS/AArch64 Port

Apple hat angekündigt, Macs langfristig von x64- auf AArch64-CPUs umzustellen. Dementsprechend wird mit JDK Enhancement Proposal 391 ein entsprechender Port bereitgestellt.

Der Code ist eine Erweiterung der in Java 9 und Java 16 veröffentlichten AArch64-Ports für Linux und Windows um macOS-spezifische Anpassungen.

New Page for "New API" and Improved "Deprecated" Page

In ab Java 17 generiertem JavaDoc gibt es eine Seite "NEW", die alle neuen Features, gruppiert nach Version anzeigt. Dazu werden die @since-Tags der Module, Pakete, Klassen, etc. ausgewertet.

"NEW"-Seite in seit Java 17 generiertem JavaDoc
"NEW"-Seite in seit Java 17 generiertem JavaDoc

Außerdem wurde die "DEPRECATED"-Seite überarbeitet. Bis Java 16 sehen wir hier eine ungruppierte Liste aller als "deprecated" markierten Features:

"DEPRECATED"-Seite von Java 16
"DEPRECATED"-Seite von Java 16

Ab Java 17 sehen wir die als "deprecated" markierten Features gruppiert nach Release:

 "DEPRECATED"-Seite von Java 17
"DEPRECATED"-Seite von Java 17

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

Vollständige Liste aller Änderungen in Java 17

Dieser Artikel hat alle Änderungen präsentiert, die in JDK Enhancement Proposals (JEPs) definiert sind sowie zahlreiche Erweiterungen der Klassenbibliothek, für die keine JEPs existieren. Weitere Änderungen, insbesondere an den Sicherheitsbibliotheken, findest du in den offiziellen Java 17 Release Notes.

Fazit

Auch wenn Java 17 das neueste LTS-Release darstellt, unterscheidet sich dieses Release nicht wesentlich von den vorherigen. Wir haben wieder eine Mischung bekommen aus:

  • neuen Sprach-Features (Sealed Classes),
  • API-Änderungen (InstantSource, HexFormat, kontextspezifische Deserialisierungsfilter),
  • einer Performance-Verbesserung (asynchrones Logging der JVM),
  • Deprecations und Löschungen (Applet API, Security Manager, RMI Activation, AOT und JIT Compiler)
  • und neuen Preview- und Incubator-Features (Pattern Matching for switch, Foreign Function & Memory API, Vector API).

Außerdem wurde der in Java 9 mit Project Jigsaw eingeschlagene Weg zum Ende gebracht, indem der übergangsweise bereitgestellte "Relaxed Strong Encapsulation"-Modus entfernt wurde und Zugriff auf private Members anderer Module (Deep Reflection) immer explizit freigegeben werden muss.

Hat dir der Artikel gefallen? Dann hinterlasse mir gerne einen Kommentar oder teile den Artikel über einen der Share-Buttons am Ende.

Java 18 steht bereits vor der Tür, die enthaltenen Features stehen fest. Diese werde ich im nächsten Artikel präsentieren. Möchtest du informiert werden, wenn der Artikel online geht? Dann klick hier, um dich für den HappyCoders-Newsletter anzumelden.