
Java 18, veröffentlicht am 22. März 2022, ist das erste „Zwischen-Release“ nach dem Long-Term-Support (LTS)-Release Java 17.
Der Umfang an Erweiterungen in Java 18 ist mit neun umgesetzten JEPs gegenüber den vorherigen Releases deutlich zurückgegangen. Nach dem LTS-Release Java 17 sei den JDK-Entwickler:innen eine kleine Verschnaufpause gegönnt.
Wie immer verwende ich die englischen Bezeichnungen der JEPs. Diese ins Deutsche zu übersetzen, würde eher verwirren, als einen Mehrwert zu bringen.
UTF-8 by Default
Lange mussten wir Java-Entwickler:innen mit der Tatsache umgehen, dass der Java-Standardzeichensatz je nach Betriebssystem und Spracheinstellungen unterschiedlich war.
Mit Java 18 hat sich das geändert.
Das Problem
Der Java-Standardzeichensatz bestimmt bei zahlreichen Methoden der JDK-Klassenbibliothek, wie Zeichenfolgen in Bytes umgewandelt werden und umgekehrt (z. B. beim Schreiben und Lesen einer Textdatei). Dazu gehören unter anderem:
- die Konstruktoren von
FileReader,FileWriter,InputStreamReader,OutputStreamWriter, - die Konstruktoren von
FormatterundScanner, - die statischen Methoden
URLEncoder.encode()undURLDecoder.decode().
Bis Java 17 konnte das zu unvorhergesehenem Verhalten führen, wenn eine Anwendung in einer Umgebung entwickelt und getestet – und dann in einer anderen Umgebung (in der sich Java für einen anderen Standardzeichensatz entschied) ausgeführt wurde.
Führen wir z. B. den folgenden Code unter Linux oder macOS aus:
try (FileWriter fw = new FileWriter("happy-coding.txt");
BufferedWriter bw = new BufferedWriter(fw)) {
bw.write("Fröhliches Programmieren!");
}
... und laden diese Datei dann mit folgendem Code unter Windows und Java 17:
try (FileReader fr = new FileReader("happy-coding.txt");
BufferedReader br = new BufferedReader(fr)) {
String line = br.readLine();
System.out.println(line);
}
Dann wird Folgendes angezeigt:
Fröhliches Programmieren!
Das liegt daran, dass Linux und macOS die Datei im UTF-8-Format speichern und Windows versucht, sie im Windows-1252-Format zu lesen. In UTF-8 belegt das „ö“ zwei Bytes, und Windows-1252 liest sie als zwei Zeichen: „Ó und „¶“.
Das Problem – Stufe zwei
Noch chaotischer wird es dadurch, dass neuere Methoden der Klassenbibliothek den Standardzeichensatz nicht berücksichtigen, sondern bei fehlender Angabe eines Zeichensatzes grundsätzlich UTF-8 verwenden. Zu diesen Methoden gehören z. B. Files.writeString(), Files.readString(), Files.newBufferedWriter() und Files.newBufferedReader().
Starten wir einmal das folgende Programm, das den Text per FileWriter schreibt und direkt danach per Files.readString() wieder einliest:
try (FileWriter fw = new FileWriter("happy-coding.txt");
BufferedWriter bw = new BufferedWriter(fw)) {
bw.write("Fröhliches Programmieren!");
}
String text = Files.readString(Path.of("happy-coding.txt"));
System.out.println(text);
Unter Linux und macOS wird der Text korrekt angezeigt. Unter Windows und Java 17 hingegen bricht das Programm mit einer Exception ab:
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 4
Das liegt daran, dass der FileWriter unter Windows die Datei mit dem Java-Standardzeichensatz Windows-1252 schreibt, Files.readString() die Datei aber als UTF-8 – unabhängig vom Standardzeichensatz – wieder einliest. Windows-1252 speichert das „ö“ als ein einzelnes Byte (hexadezimal F6), und das ist kein gültiges UTF-8.
Bisherige Lösungsmöglichkeiten
Um eine Anwendung gegen solche Fehler zu wappnen, gab es vor Java 18 zwei Möglichkeiten:
- Angabe des Zeichensatzes beim Aufruf aller Methoden, welche Zeichenfolgen in Bytes umwandeln und umgekehrt.
- Setzen des Standardzeichensatzes per System Property
file.encoding.
Die erste Möglichkeit führt zu einer Menge Code Duplication und ist somit unschön und fehleranfällig:
FileWriter fw = new FileWriter("happy-coding.txt", StandardCharsets.UTF_8);
// ...
FileReader fr = new FileReader("happy-coding.txt", StandardCharsets.UTF_8);
// ...
Files.readString(Path.of("happy-coding.txt"), StandardCharsets.UTF_8);
Die Angabe der Zeichensatz-Parameter verhindert außerdem, dass wir Methodenreferenzen verwenden können, wie in folgendem Beispiel:
Stream<String> encodedParams = ...
Stream<String> decodedParams = encodedParams.map(URLDecoder::decode);
Stattdessen müssten wir schreiben:
Stream<String> encodedParams = ...
Stream<String> decodedParams =
encodedParams.map(s -> URLDecoder.decode(s, StandardCharsets.UTF_8));
Die zweite Möglichkeit (System Property file.encoding) war zum einen bis einschließlich Java 17 nicht offiziell dokumentiert (s. Dokumentation der System Properties).
Zum anderen wird, wie oben erläutert, der so festgelegte Zeichensatz nicht für alle API-Methoden verwendet. Die Variante ist also ebenfalls fehleranfällig, wie wir an dem Beispiel von oben zeigen können:
public class Jep400Example {
public static void main(String[] args) throws IOException {
try (FileWriter fw = new FileWriter("happy-coding.txt");
BufferedWriter bw = new BufferedWriter(fw)) {
bw.write("Fröhliches Programmieren!");
}
String text = Files.readString(Path.of("happy-coding.txt"));
System.out.println(text);
}
}
Compilieren wir das Programm und lassen es mit dem Standard-Encoding ISO-8859-1 laufen:
$ javac Jep400Example.java
$ java -Dfile.encoding=ISO-8859-1 Jep400Example
Exception in thread "main" java.nio.charset.MalformedInputException: Input length = 4
Das Programm bricht ab, da zwar FileWriter das eingestellte Standard-Encoding berücksichtigt, Files.readString() es allerdings ignoriert und immer UTF-8 verwendet. Diese Variante funktioniert also nur dann zuverlässig, wenn einheitlich UTF-8 eingesetzt wird:
$ java -Dfile.encoding=UTF-8 Jep400Example
Fröhliches Programmieren!
JEP 400 als Retter in der Not
Seit Java 18 gehören die genannten Probleme durch JDK Enhancement Proposal 400 – zumindest größtenteils – der Vergangenheit an.
Das Standard-Encoding ist auf allen Betriebssystemen und unabhängig von den Gebiets- und Spracheinstellungen immer UTF-8.
Außerdem ist die System Property file.encoding seit Java 18 dokumentiert, womit wir sie legitim einsetzen können. Das sollten wir allerdings mit Bedacht tun. Denn an der Tatsache, dass die Files-Methoden das eingestellte Standard-Encoding ignorieren, hat auch JEP 400 nichts geändert.
Laut Dokumentation sollten ohnehin nur die Werte UTF-8 und COMPAT verwendet werden, wobei UTF-8 für einheitliche Codierung sorgt und COMPAT das Verhalten von vor Java 18 simuliert. Alle anderen Werte führen zu unspezifiziertem Verhalten.
Im besten Fall setzen wir -Dfile.encoding immer auf UTF-8 oder lassen es komplett weg.
Auslesen der Encodings zur Laufzeit
Das aktuelle Standard-Encoding kann zur Laufzeit über Charset.defaultCharset() oder die System Property file.encoding ausgelesen werden. Seit Java 17 gibt es außerdem die System Property native.encoding, über die das Encoding ausgelesen werden kann, das – vor Java 18 – das Standard-Encoding wäre, wenn keines angegeben wird:
System.out.println("Default charset : " + Charset.defaultCharset());
System.out.println("file.encoding : " + System.getProperty("file.encoding"));
System.out.println("native.encoding : " + System.getProperty("native.encoding"));
Ohne Angabe von -Dfile.encoding gibt das Programm unter Linux und macOS mit Java 17 und Java 18 das Folgende aus:
Default charset : UTF-8
file.encoding : UTF-8
native.encoding : UTF-8
Unter Windows und Java 17 lautet die Ausgabe wie folgt:
Default charset : windows-1252
file.encoding : Cp1252
native.encoding : Cp1252
Und unter Windows und Java 18:
Default charset : UTF-8
file.encoding : UTF-8
native.encoding : Cp1252
Das native Encoding unter Windows bleibt also gleich, das Standard-Encoding hingegen ändert sich durch JEP 400 auf UTF-8.
Der bisherige „default“-Zeichensatz
Wenn wir das kleine Programm von oben unter Linux oder macOS und Java 17 mit dem Parameter -Dfile.encoding=default aufrufen, bekommen wir die folgende Ausgabe:
Default charset : US-ASCII
file.encoding : default
native.encoding : UTF-8
Das liegt daran, dass der Name default bis Java 17 als Alias für das Encoding US-ASCII erkannt wurde.
Seit Java 18 wird default nicht mehr erkannt; die Ausgabe sieht wie folgt aus:
Default charset : UTF-8
file.encoding : default
native.encoding : UTF-8
Als System Property file.encoding wird default zwar noch ausgegeben – an der Stelle würden wir aber auch jede andere ungültige Eingabe sehen. Bei einer ungültigen file.encoding-Eingabe ist der Default-Zeichensatz UTF-8 – in Java 17 wie in Java 18.
Charset.forName() Taking Fallback Default Value
Unabhängig von JEP 400 gibt es seit Java 18 die neue Methode Charset.forName(String charsetName, Charset fallback). Sie gibt bei unbekanntem Zeichensatz-Namen oder nicht unterstütztem Zeichensatz den angegebenen Fallback-Wert zurück, anstatt eine IllegalCharsetNameException oder eine UnsupportedCharsetException zu werfen.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8270490 aufgeführt.
Simple Web Server
Fast alle modernen Programmiersprachen bieten die Möglichkeit, einen rudimentären HTTP-Server hochzufahren, z. B. um schnell einige Webfunktionen zu testen.
Durch JDK Enhancement Proposal 408 bietet auch Java ab Version 18 diese Möglichkeit.
Der einfachste Weg, den mitgelieferten Webserver zu starten, ist das Kommando jwebserver. Es startet den Server auf localhost:8000 und liefert dort einen Datei-Browser für das aktuelle Verzeichnis:
$ jwebserver
Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
Serving /home/sven and subdirectories on 127.0.0.1 port 8000
URL http://127.0.0.1:8000/
Wie angezeigt, kannst du mit dem Parameter -b die IP-Adresse angeben, auf der der Webserver hören soll. Mit -p kannst du den Port ändern und mit -d das Verzeichnis, das der Server ausliefern soll. Mit -o kannst du die Logausgaben konfigurieren. Also z. B.:
$ jwebserver -b 127.0.0.100 -p 4444 -d /tmp -o verbose
Serving /tmp and subdirectories on 127.0.0.100 port 4444
URL http://127.0.0.100:4444/
Eine Liste der Optionen mit Erklärungen bekommst du mit jwebserver -h angezeigt.
Funktionsumfang
Der Webserver ist sehr rudimentär und hat folgende Einschränkungen:
- Das einzig unterstützte Protokoll ist HTTP/1.1.
- HTTPS wird nicht angeboten.
- Erlaubt sind nur die HTTP-Methoden GET und HEAD.
Java-API: SimpleFileServer
jwebserver ist ein Launcher für das Modul jdk.httpserver. Dasselbe erreichst du mit java -m jdk.httpserver. In beiden Fällen wird SimpleFileServerImpl.start(…) aufgerufen – von jwebserver über die Klasse sun.net.httpserver.simpleserver.JWebServer, von java -m jdk.httpserver über die Klasse Main im selben Package.
SimpleFileServerImpl.start(…) wertet die Kommandozeilen-Parameter aus und erzeugt schließlich den eigentlichen Server mit SimpleFileServer.createFileServer(…).
Mit SimpleFileServer.createFileServer(…) kannst du auch per Java-Code einen Server starten:
HttpServer server =
SimpleFileServer.createFileServer(
new InetSocketAddress(8080), Path.of("/tmp"), OutputLevel.INFO);
server.start();
Über die Java-API kannst du den Webserver erweitern, z. B. kannst du bestimmte Verzeichnisse des Dateisystems über andere HTTP-Pfade erreichbar machen und den Server um eigene Handler für bestimmte Pfade und HTTP-Methoden (z. B. PUT) erweitern.
Ein vollständiges Tutorial würde den Rahmen dieses Artikels sprengen. Weitere Details findest du in den Abschnitten „API“ und „Enhanced request handling“ im JEP.
Code Snippets in Java API Documentation
Wenn wir vor Java 18 mehrzeilige Code-Schnipsel in JavaDoc integrieren wollten, mussten wir das umständlich über <pre>…</pre> ggf. in Kombination mit {@code … } machen. Dabei mussten wir auf zwei Dinge achten:
- Zwischen
<pre>und Code sowie zwischen Code und</pre>dürfen keine Zeilenumbrüche erfolgen. - Der Code beginnt direkt hinter den Sternchen; d. h. wenn zwischen Sternchen und Code Leerzeichen stehen, landen diese auch im JavaDoc. Der Code muss also um ein Zeichen nach links verschoben sein gegenüber dem restlichen Text im JavaDoc-Kommentar.
Hier ein Beispiel mit <pre>:
/**
* How to write a text file with Java 7:
*
* <pre><b>try</b> (BufferedWriter writer = Files.<i>newBufferedWriter</i>(path)) {
* writer.write(text);
*}</pre>
*/
Und eines mit <pre> und {@code … }:
/**
* How to write a text file with Java 7:
*
* <pre>{@code try (BufferedWriter writer = Files.newBufferedWriter(path)) {
* writer.write(text);
*}}</pre>
*/
Der Unterschied zwischen beiden Varianten ist, dass wir in der ersten Variante den Code mit HTML-Tags wie z. B. <b> und <i> formatieren können, während in der zweiten Variante solche Tags nicht ausgewertet, sondern dargestellt werden würden.
Das @snippet-Tag in Java 18
JDK Enhancement Proposal 413 erweitert die JavaDoc-Syntax um das @snippet-Tag, das speziell für die Darstellung von Quellcode entwickelt wurde. Mit dem @snippet-Tag können wir den Kommentar wie folgt schreiben:
/**
* How to write a text file with Java 7:
*
* {@snippet :
* try (BufferedWriter writer = Files.newBufferedWriter(path)) {
* writer.write(text);
* }
* }
*/
Wir können außerdem Teile des Codes mit @highlight hervorheben, z. B. alle Vorkommen von „text“ innerhalb der zweiten Codezeile:
/**
* {@snippet :
* try (BufferedWriter writer = Files.newBufferedWriter(path)) {
* writer.write(text); // @highlight substring="text"
* }
* }
*/
Das folgende Beispiel markiert innerhalb des mit @highlight region und @end gekennzeichneten Blocks alle Wörter, die mit „write“ beginnen. Mit type="…" können wir außerdem die Art der Hervorhebung festlegen: bold, italic oder highlighted (farblich hinterlegt).
/**
* {@snippet :
* // @highlight region regex="\bwrite.*?\b" type="highlighted"
* try (BufferedWriter writer = Files.newBufferedWriter(path)) {
* writer.write(text);
* }
* // @end
* }
*/
Mit @link können wir einen Teil des Texts verlinken, z. B. BufferedWriter mit dessen JavaDoc:
/**
* {@snippet :
* // @link regex="\bBufferedWriter\b" target="java.io.BufferedWriter" :
* try (BufferedWriter writer = Files.newBufferedWriter(path)) {
* writer.write(text);
* }
* }
*/
Der reguläre Ausdruck mit \b (Wortgrenze) sorgt dafür, dass nur der Typ BufferedWriter verlinkt wird. Mit substring="BufferedWriter" würde auch der gleichlautende Teil von newBufferedWriter zum Link.
Achtung: Der Doppelpunkt am Ende der Zeile mit dem @link-Tag ist in diesem Fall wichtig. Er bedeutet, dass der Kommentar sich auf die nächste Zeile bezieht. Wir könnten den Kommentar genauso an das Ende der folgenden Zeile schreiben, so wie im ersten @highlight-Beispiel – oder mit @link region und @end einen Bereich festlegen, innerhalb dessen alle Vorkommen von BufferedWriter verlinkt werden sollen.
Snippets aus anderen Dateien integrieren
Wir können auch auf markierten Code in einer anderen Datei verweisen:
/**
* How to write a text file with Java 7:
*
* {@snippet file="FileWriter.java" region="writeFile"}
*/
In der Datei FileWriter.java markieren wir den Code wie folgt:
// @start region="writeFile"
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
writer.write(text);
}
// @end
Damit javadoc die Datei findet, muss sie im Unterverzeichnis snippet-files des Packages liegen, dessen Quellcode das @snippet-Tag enthält – oder in einem Verzeichnis, das wir per --snippet-path angeben:
$ javadoc -d docs -sourcepath src --snippet-path snippets eu.happycoders.demo
Fehlt beides, bricht javadoc mit einem „file not found“-Fehler ab.
Das waren meiner Meinung nach die wichtigsten @snippet-Tags. Eine vollständige Referenz findest du im JEP.
Internet-Address Resolution SPI
Um in Java zu einem Hostnamen die IP-Adresse(n) herauszufinden, können wir InetAddress.getByName(…) oder InetAddress.getAllByName(…) verwenden. Hier ein Beispiel:
InetAddress[] addresses = InetAddress.getAllByName("www.happycoders.eu");
System.out.println("addresses = " + Arrays.toString(addresses));
Der Code gibt bei mir das Folgende aus (die Zeilenumbrüche habe ich der Übersichtlichkeit halber manuell eingefügt):
addresses = [www.happycoders.eu/91.99.59.195,
www.happycoders.eu/2a01:4f8:1c19:9fc0:0:0:0:1]
Für Reverse Lookups (also das Auflösen einer IP-Adresse zu einem Hostnamen) gibt es die Methoden InetAddress::getCanonicalHostName und InetAddress::getHostName.
Standardmäßig verwendet InetAddress den Resolver des Betriebssystems, d. h. in der Regel wird zunächst die hosts-Datei konsultiert und danach die konfigurierten DNS-Server.
Diese feste Verdrahtung hat ein paar Nachteile:
- Es ist nicht möglich, innerhalb von Tests einen Hostnamen auf die IP-Adresse eines gemockten Servers zu mappen.
- Neue Hostname-Lookup-Protokolle (wie DNS über QUIC, TLS oder HTTPS) können nicht ohne weiteres in Java implementiert werden.
- Die aktuelle Implementierung führt zu einem blockierenden Betriebssystem-Aufruf. Das allein ist schon unschön, da dieser Aufruf teilweise lange dauern kann und nicht unterbrechbar ist. Beim Einsatz von virtuellen Threads führt dies sogar so weit, dass der Betriebssystem-Thread währenddessen keine anderen virtuellen Threads bedienen kann.
Durch JDK Enhancement Proposal 418 wird ein Service Provider Interface (SPI) eingeführt, um den eingebauten Standard-Resolver der Plattform durch andere Resolver ersetzen zu können.
Internet-Address Resolution SPI / JEP 418 – Beispiel
Das folgende Beispiel zeigt, wie du einen einfachen Resolver implementierst und registrierst, der jede Anfrage mit der IP-Adresse 127.0.0.1 beantwortet. Du findest den Code auch in diesem GitHub-Repository.
Wir schreiben zunächst den Resolver, indem wir das in Java 18 eingeführte Interface java.net.spi.InetAddressResolver implementieren (Klasse HappyCodersInetAddressResolver in GitHub):
public class HappyCodersInetAddressResolver implements InetAddressResolver {
@Override
public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy)
throws UnknownHostException {
return Stream.of(InetAddress.getByAddress(new byte[] {127, 0, 0, 1}));
}
@Override
public String lookupByAddress(byte[] addr) {
throw new UnsupportedOperationException();
}
}
Da ich hier nur das grundsätzliche Prinzip vorstellen möchte, habe ich den Resolver so einfach wie möglich gehalten, und er unterstützt auch keine Reverse Lookups.
Als Zweites benötigen wir einen Resolver-Provider (Klasse HappyCodersInetAddressResolverProvider in GitHub):
public class HappyCodersInetAddressResolverProvider
extends InetAddressResolverProvider {
@Override
public InetAddressResolver get(Configuration configuration) {
return new HappyCodersInetAddressResolver();
}
@Override
public String name() {
return "HappyCoders Internet Address Resolver Provider";
}
}
Der Provider erstellt in der get()-Methode eine neue Instanz des zuvor implementierten Resolvers.
Im dritten Schritt müssen wir den Resolver registrieren. Dazu legen wir im Verzeichnis META-INF/services eine Datei mit dem Namen java.net.spi.InetAddressResolverProvider und mit folgendem Inhalt an (Datei in GitHub):
eu.happycoders.jep418.HappyCodersInetAddressResolverProvider
Jetzt führen wir erneut den Code von oben aus (Klasse Jep418Demo in GitHub):
InetAddress[] addresses = InetAddress.getAllByName("www.happycoders.eu");
System.out.println("addresses = " + Arrays.toString(addresses));
Die Ausgabe lautet nun:
addresses = [/127.0.0.1]
Das ist genau die IP-Adresse, die wir in unserem Resolver zurückgegeben haben.
Preview- und Incubator-Features
In den folgenden Abschnitten findest du Preview- und Incubator-Features, die wir bereits aus den vorangegangenen Releases kennen. Es handelt sich um Wiedervorlagen mit kleinen Änderungen.
Pattern Matching for switch (Second Preview)
„Pattern Matching for switch“ wurde in Java 17 erstmals vorgestellt und ermöglicht switch-Statements (und -Ausdrücke) wie z. B. das folgende (mehr dazu findest du im verlinkten Java-17-Artikel):
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 -> {}
}
Durch JDK Enhancement Proposal 420 wurden in Java 18 zwei Änderungen vorgenommen – eine in der Dominanzprüfung und eine in der Vollständigkeitsanalyse bei der Kombination mit versiegelten Typen.
Verbesserung der Dominanzprüfung
Was Dominanzprüfung ist, habe ich im oben verlinkten Artikel zu Java 17 beschrieben. Kurz gesagt führt der folgende Code zu einem Compilerfehler:
Object obj = ...
switch (obj) {
case String s -> System.out.println(s.toLowerCase());
case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
...
}
Der Grund dafür ist, dass das Pattern in Zeile 3 das längere Pattern aus Zeile 4 „dominiert“: Wenn obj ein String ist, wird es durch das Pattern in Zeile 3 gematcht, egal wie lang der String ist. Es gibt also kein Objekt, das durch das Pattern in Zeile 4 gematcht wird.
Ein Fall wurde in Java 17 jedoch nicht bedacht – und zwar die Kombination aus Konstante und Guarded Pattern (ein Pattern mit &&). So ist der folgende Code unter Java 17 noch erlaubt:
String string = ...
switch (string) {
case String s && s.length() > 5 -> System.out.println(s.toUpperCase());
case "foobar" -> System.out.println("baz");
...
}
Wenn string gleich „foobar“ ist, wird es allerdings nicht durch Zeile 4 gematcht, sondern bereits durch Zeile 3 (denn es ist ja auch länger als fünf Zeichen).
Da nicht erreichbarer Code offensichtlich nicht beabsichtigt ist, bekommen wir in Java 18 den folgenden Compilerfehler:
java --enable-preview --source 18 SwitchTest.java
SwitchTest.java:9: error: this case label is dominated by a preceding case label
case "foobar" -> System.out.println("baz");
^
Bugfix in der Vollständigkeitsanalyse mit Sealed Types
Was Vollständigkeitsanalyse ist, erfährst du im Artikel über versiegelte Typen.
Die Änderung in Java 18 erkläre ich an folgender versiegelter Beispiel-Klassenhierarchie aus dem JEP:
sealed interface I<T> permits A, B {}
final class A<X> implements I<String> {}
final class B<Y> implements I<Y> {}
Der folgende Code ist nicht compilierbar:
I<Integer> i = ...
switch (i) {
case A<Integer> a -> System.out.println("It's an A"); // not compilable
case B<Integer> b -> System.out.println("It's a B");
}
Sowohl Java 17 als auch Java 18 erkennen, dass I<Integer> nicht nach A<Integer> konvertiert werden kann (da A<Integer> ein I<String> ist) und melden:
incompatible types: I<Integer> cannot be converted to A<Integer>
Tatsächlich ist aufgrund der versiegelten Klassenhierarchie B<Integer> die einzige Klasse, die I<Integer> implementieren kann. Das switch-Statement ist also wie folgt vollständig:
I<Integer> i = ...
switch (i) {
case B<Integer> b -> System.out.println("It's a B");
}
Java 17 meldet hier allerdings:
the switch statement does not cover all possible input values
Das ist ein offensichtlicher Bug, der in Java 18 behoben wurde.
Vector API (Third Incubator)
Die Vector API wurde bereits in Java 16 und Java 17 als Incubator-Feature vorgestellt. Dabei geht es nicht um die java.util.Vector-Klasse aus Java 1.0, sondern um Vektorrechnung im mathematischen Sinn und deren Abbildung auf moderne Single-Instruction-Multiple-Data-(SIMD)-Architekturen.
Durch JDK Enhancement Proposal 417 wurde erneut die Leistung verbessert und der Support auf „ARM Scalable Vector Extension“ – eine optionale Erweiterung der ARM64-Plattform – erweitert.
Incubator-Stadium bedeutet, dass das Feature noch erhebliche Änderungen durchlaufen kann. Die Vector API befindet sich auch in Java 27 noch im Incubator-Stadium – in der zwölften Runde. Den Grund erkläre ich im Abschnitt Status der Vector API des Java-27-Artikels.
Foreign Function & Memory API (Second Incubator)
Die Foreign Function & Memory API entstand in Java 17 durch Kombination der „Foreign Memory Access API“ und der „Foreign Linker API“, die beide zuvor – jeweils einzeln – durch mehrere Incubator-Phasen liefen.
Die neue API wird in Project Panama entwickelt und ist als Ersatz für JNI (Java Native Interface) vorgesehen, welches bereits seit Java 1.1 Teil der Plattform ist. JNI ermöglicht den Aufruf von C-Code aus Java heraus. Wer einmal mit JNI gearbeitet hat, weiß: JNI ist äußerst kompliziert zu implementieren, fehleranfällig und langsam.
Ziel der neuen API ist es, JNI durch eine knappe, lesbare, reine Java-API zu ersetzen – mit einem Overhead, der dem von JNI vergleichbar oder geringer ist.
Durch JDK Enhancement Proposal 419 wurden weitgehende Änderungen an der API vorgenommen. In Java 19 erreichte die API das Preview-Stadium, und seit Java 22 ist sie produktionsreif.
Deprecations und Löschungen
Auch in Java 18 wurden wieder einige Features als „deprecated“ oder „deprecated for removal“ markiert oder komplett entfernt.
Deprecate Finalization for Removal
Finalization existiert seit Java 1.0 und soll helfen, Resource Leaks zu vermeiden, indem Klassen eine finalize()-Methode implementieren können, in der sie vom Betriebssystem angeforderte Ressourcen (wie Datei-Handles oder Nicht-Heap-Speicher) wieder freigeben können.
Die finalize()-Methode wird vom Garbage Collector aufgerufen, bevor er den Speicherplatz eines Objekts freigibt.
Das scheint eine sinnvolle Lösung zu sein. Es hat sich allerdings gezeigt, dass Finalization einige grundlegende, kritische Mängel hat:
Performance:
- Da nicht vorhersehbar ist, wann der Garbage Collector ein Objekt aufräumt (und ob er das überhaupt tut), kann es sein, dass – nachdem ein Objekt nicht mehr referenziert wird – es sehr lange dauert, bis dessen
finalize()-Methode aufgerufen wird (oder dass sie nie aufgerufen wird). - Wenn der Garbage Collector eine Full GC durchführt, kann es zu spürbaren Latenzen kommen, wenn viele der aufzuräumenden Objekte
finalize()-Methoden haben. - Die
finalize()-Methode wird für jede Instanz einer Klasse aufgerufen, selbst wenn es nicht nötig wäre. Es gibt keine Möglichkeit, in einzelnen Objekten festzulegen, dass diese keine Finalization benötigen.
Sicherheitsrisiken:
- Die
finalize()-Methode kann beliebigen Code ausführen, z. B. auch eine Referenz des zu löschenden Objekts speichern. Somit wird der Garbage Collector es nicht aufräumen. Wird die Referenz auf das Objekt später wieder entfernt und das Objekt durch den Garbage Collector gelöscht, wird diefinalize()-Methode nicht erneut aufgerufen. - Wenn der Konstruktor einer Klasse eine Exception wirft, liegt das Objekt zunächst trotzdem auf dem Heap. Wenn der Garbage Collector es entfernt, ruft er dessen
finalize()-Methode auf. Diese kann dann Operationen auf einem ggf. unvollständig initialisierten Objekt ausführen oder dieses im Objektgraphen speichern.
Fehleranfälligkeit:
- Eine
finalize()-Methode sollte immer auch diefinalize()-Methode der Elternklasse aufrufen. Der Compiler fordert dies allerdings nicht ein (im Gegensatz zum Konstruktor). Selbst wenn wir unseren eigenen Code fehlerfrei schreiben, könnte jemand anderes unsere Klasse erweitern, diefinalize()-Methode überschreiben, ohne die überschriebene Methode aufzurufen, und damit ein Resource Leak verursachen.
Multithreading:
- Die
finalize()-Methode wird in einem unspezifizierten Thread aufgerufen, so dass die Thread-Sicherheit des gesamten Objekts sichergestellt werden muss – selbst in einer Anwendung, die sonst kein Multithreading nutzt.
Alternativen zur Finalization
Es existieren folgende Alternativen zur Finalization:
- Das in Java 7 eingeführte „try-with-resources“ generiert für alle Klassen, die das
AutoCloseable-Interface implementieren, automatisch einenfinally-Block, in dem die entsprechendenclose()-Methoden aufgerufen werden. Gängige Tools für statische Codeanalyse finden und bemängeln Code, derAutoCloseable-Objekte nicht innerhalb von „try-with-resources“-Blöcken erzeugt. - Durch die in Java 9 eingeführte Cleaner-API können sogenannte „Cleaner Actions“ registriert werden. Diese werden aufgerufen, wenn ein Objekt nicht mehr erreichbar ist (nicht erst dann, wenn es aufgeräumt wird). Cleaner Actions haben keinen Zugriff auf das Objekt selbst (können also auch keine Referenz darauf speichern); wir brauchen sie für ein Objekt nur dann zu registrieren, wenn dieses spezifische Objekt sie benötigt; und wir können selbst bestimmen, in welchem Thread sie aufgerufen werden.
Aus den o. g. Gründen und aufgrund der Verfügbarkeit von hinreichenden Alternativen wurden die finalize()-Methoden in Object und zahlreichen weiteren Klassen der JDK-Klassenbibliothek bereits in Java 9 als „deprecated“ markiert.
Durch JDK Enhancement Proposal 421 werden die Methoden in Java 18 als „deprecated for removal“ markiert.
Außerdem wird die VM-Option --finalization=disabled eingeführt, welche die Finalization komplett deaktiviert. Damit können wir Applikationen testen, bevor wir sie auf eine zukünftige Java-Version aktualisieren, in der Finalization vollständig entfernt wurde.
Bis Java 27 ist das nicht geschehen: Finalization ist weiterhin standardmäßig aktiviert.
JDK Flight Recorder Event for Finalization
Neu in Java 18 ist auch das Flight Recorder Event jdk.FinalizerStatistics. Dieses ist standardmäßig aktiviert und protokolliert jede instantiierte Klasse mit einer nicht leeren finalize()-Methode. So kann man leicht diejenigen Klassen identifizieren, die noch einen Finalizer verwenden.
Wenn Finalization per --finalization=disabled deaktiviert ist, werden diese Events nicht getriggert.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8266936 aufgeführt.
Terminally Deprecate Thread.stop
Thread.stop() ist seit Java 1.2 „deprecated“. In Java 18 wurde die Methode als „deprecated for removal“ markiert.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8277861 aufgeführt.
Deprecate sun.misc.Unsafe Methods That Return Offsets
Die Klasse sun.misc.Unsafe hat drei Methoden, über die sich die Position eines Felds im Speicher ermitteln lässt: objectFieldOffset(), staticFieldOffset() und staticFieldBase(). In Java 18 wurden diese drei Methoden als „deprecated“ markiert.
Als Ersatz empfehlen die JDK-Entwickler:innen die in Java 9 eingeführte Klasse VarHandle, über die sich typsicher auf ein Feld zugreifen lässt.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8277863 aufgeführt.
Change the java.security.manager System Property Default Value to disallow
Der Security Manager wurde in Java 17 als „deprecated for removal“ markiert. Java 18 setzt den Standardwert der System Property java.security.manager auf disallow.
Ein Aufruf von System.setSecurityManager(…) wirft damit eine UnsupportedOperationException. In Java 17 war ein solcher Aufruf noch erlaubt und führte nur zu einer Warnung. Wer den Security Manager zur Laufzeit noch setzen will, muss die Anwendung mit -Djava.security.manager=allow starten.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8270380 aufgeführt.
Remove the Legacy PlainSocketImpl and PlainDatagramSocketImpl Implementation
In Java 13 wurden die Socket API und in Java 15 die DatagramSocket API neu implementiert.
Die alten Implementierungen konnten seither über die System Properties jdk.net.usePlainSocketImpl bzw. jdk.net.usePlainDatagramSocketImpl reaktiviert werden.
In Java 18 wurde der alte Code entfernt; die beiden System Properties haben seitdem keine Wirkung mehr.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8253119 aufgeführt.
Obsolete Biased Locking
Biased Locking ist seit Java 15 standardmäßig deaktiviert und „deprecated“. In Java 18 wurde der Code aus der JVM entfernt.
Die VM-Option -XX:+UseBiasedLocking und die fünf zugehörigen Optionen BiasedLockingStartupDelay, BiasedLockingBulkRebiasThreshold, BiasedLockingBulkRevokeThreshold, BiasedLockingDecayTime und UseOptoBiasInlining nimmt Java 18 noch an: Die JVM gibt eine Warnung aus und ignoriert die Optionen.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8256425 aufgeführt.
Sonstige Änderungen in Java 18
In diesem Abschnitt findest du diejenigen Änderungen, mit denen du in der täglichen Programmierarbeit eher selten in Berührung kommen wirst. Es schadet dennoch sicher nicht, sie einmal zu überfliegen.
Reimplement Core Reflection with Method Handles
Wenn du viel mit Java-Reflection zu tun hast, wirst du wissen, dass sprichwörtlich mehrere Wege nach Rom führen. Um beispielsweise das private value-Feld eines Strings per Reflection auszulesen, gibt es zwei Möglichkeiten:
1. Per sogenannter „Core Reflection“:
Field field = String.class.getDeclaredField("value");
field.setAccessible(true);
byte[] value = (byte[]) field.get(string);
2. Über „Method Handles“:
VarHandle handle =
MethodHandles.privateLookupIn(String.class, MethodHandles.lookup())
.findVarHandle(String.class, "value", byte[].class);
byte[] value = (byte[]) handle.get(string);
Es gibt noch eine dritte Form, die wir nicht direkt sehen können: Core Reflection verwendet für die ersten paar Aufrufe nach dem Start der JVM zusätzliche native JVM-Methoden und beginnt erst nach einer Weile damit, den Java-Bytecode der Reflection-Aufrufe zu compilieren und zu optimieren.
Alle drei Varianten zu pflegen, bedeutet einen erheblichen Aufwand für die JDK-Entwickler:innen. Daher wurde im Rahmen von JDK Enhancement Proposal 416 entschieden, den Code der Reflection-Klassen java.lang.reflect.Method, Field und Constructor auf Basis von Method Handles neu zu implementieren und somit den Entwicklungsaufwand zu reduzieren.
Die alte Implementierung ließ sich zunächst per VM-Option -Djdk.reflect.useDirectMethodHandle=false wieder aktivieren; in Java 22 wurde sie entfernt.
ZGC / SerialGC / ParallelGC Support String Deduplication
Seit Java 18 unterstützen auch der in Java 15 als produktionsreif veröffentlichte Z Garbage Collector sowie der serielle und der parallele Garbage Collector String Deduplication.
String Deduplication bedeutet, dass der Garbage Collector Strings erkennt, deren value- und coder-Felder die gleichen Bytes enthalten. Der GC löscht diese Byte-Arrays bis auf eines und lässt alle String-Instanzen dieses eine Byte-Array referenzieren.
Das heißt, es werden nicht die Strings dedupliziert (wie der Name des Features impliziert), sondern nur deren Byte-Arrays. An den Identitäten der String-Objekte selbst ändert sich nichts.
String Deduplication ist standardmäßig deaktiviert und muss per VM-Option -XX:+UseStringDeduplication explizit aktiviert werden. Sie kostet den Garbage Collector zusätzliche Arbeit, die sich laut JEP 192 erst auszahlt, wenn die Anwendung genügend doppelte Strings enthält.
String Deduplication wurde erstmals mit diesem JEP in Java 8u20 für den G1 veröffentlicht. Für den Einbau in den ZGC, den seriellen und den parallelen GC in Java 18 gibt es keinen JEP; die Änderungen werden im Bug-Tracker unter JDK-8267186, JDK-8272609 und JDK-8267185 aufgeführt.
Allow G1 Heap Regions up to 512MB
Der G1 Garbage Collector bestimmt die Größe der Heap-Regionen in der Regel automatisch. Je nach Heap-Größe wird die Größe der Regionen auf einen Wert zwischen 1 MB und 32 MB festgelegt.
Per VM-Option -XX:G1HeapRegionSize kann die Größe der Regionen auch manuell gesetzt werden. Auch hierbei waren bis Java 17 Größen zwischen 1 MB und 32 MB erlaubt.
In Java 18 wird die maximale Größe der Regionen auf 512 MB erhöht. Dies soll insbesondere die Heap-Fragmentierung bei sehr großen Objekten reduzieren.
Die Änderung bezieht sich nur auf das manuelle Festlegen der Regionengröße. Bei der automatischen Bestimmung durch die JVM (also ohne Angabe der VM-Option) bleibt die maximale Größe bei 32 MB.
Für diese Änderung gibt es keinen JEP; sie wird im Bug-Tracker unter JDK-8275056 aufgeführt.
Neue Methoden in Math und Duration
Die Klassen Math und StrictMath wurden in Java 18 um folgende Methoden erweitert:
ceilDiv()teilt zwei Ganzzahlen und rundet das Ergebnis auf – als Gegenstück zum abrundendenfloorDiv(). Damit berechnest du z. B., wie viele Seiten du für 23 Einträge brauchst, wenn zehn Einträge auf eine Seite passen:Math.ceilDiv(23, 10)ergibt 3,Math.floorDiv(23, 10)und23 / 10ergeben 2.ceilMod()liefert den Rest, der zuceilDiv()passt:Math.ceilMod(23, 10)ergibt-7, denn 3 · 10 − 7 = 23.divideExact(),floorDivExact()undceilDivExact()werfen eineArithmeticException, wenn das Ergebnis überläuft. Das passiert bei genau einer Division:Integer.MIN_VALUEbzw.Long.MIN_VALUEgeteilt durch-1. Der Operator/liefert in diesem Fall ohne Fehlermeldung wiederInteger.MIN_VALUE.unsignedMultiplyHigh()multipliziert zweilong-Werte als vorzeichenlose Zahlen und gibt die oberen 64 Bit des 128 Bit langen Produkts zurück.
Die Klasse Duration hat die Methode isPositive() bekommen. Sie gibt true zurück, wenn die Dauer größer als null ist.
Für diese Änderungen gibt es keinen JEP; sie werden im Bug-Tracker unter JDK-8271602 (ceilDiv(), ceilMod(), ceilDivExact()), JDK-8271225 (floorDivExact()), JDK-8075806 (divideExact()), JDK-8188044 (unsignedMultiplyHigh()) und JDK-8171382 (Duration.isPositive()) aufgeführt.
Vollständige Liste aller Änderungen in Java 18
Neben den in diesem Artikel präsentierten JDK Enhancement Proposals und Änderungen an den Klassenbibliotheken gibt es noch weitere Änderungen (z. B. an den Kryptographie-Modulen), die den Rahmen dieses Artikels sprengen würden. Eine vollständige Liste findest du in den JDK 18 Release Notes.
Fazit
Mit Java 18 begann der nächste Zyklus von Non-LTS-Releases, der im September 2023 mit dem LTS-Release Java 21 endete. Wer mitgerechnet hat, stellt fest: Zwischen Java 11 und Java 17 lagen fünf Non-LTS-Releases und drei Jahre – zwischen Java 17 und 21 nur noch drei Non-LTS-Releases und zwei Jahre. Oracle hatte nämlich kurz vor dem Release von Java 17 angekündigt, den Release-Zyklus zu verkürzen.
Java 18 ist übersichtlich und enthält – zum ersten Mal seit Langem – keine Änderung an der Sprache selbst (nach zahlreichen Spracherweiterungen wie Records und Sealed Classes).
Die wichtigsten Änderungen von Java 18 sind:
- UTF-8 ist auf allen Betriebssystemen und unabhängig von Sprach- und Gebietseinstellungen der Standard-Zeichensatz.
- Per
jwebserver-Kommando (oder mit derSimpleFileServer-Klasse) können wir schnell einen rudimentären Webserver starten. - Mit dem
@snippet-Tag bekommen wir ein mächtiges Werkzeug, um Quellcode-Schnipsel in unsere JavaDoc-Dokumentation zu integrieren. - Mit dem „Internet-Address Resolution SPI“ können wir den Standard-Resolver für IP-Adressen ersetzen, was insbesondere in Tests hilfreich ist.
- Die Preview- und Incubator-Features „Pattern Matching for switch“, „Vector API“ und „Foreign Function & Memory API“ wurden in die jeweils nächste Preview- bzw. Incubator-Runde geschickt.
- Finalization und
Thread.stop()wurden als „deprecated for removal“ markiert, und der Code für Biased Locking wurde aus der JVM entfernt.
Diverse sonstige Änderungen runden wie immer das Release ab. Du kannst Java 18 herunterladen.
Wenn dir dieser Artikel gefallen hat, freue ich mich über eine Bewertung auf meinem ProvenExpert-Profil.




