This article is also available in English
Es ist wieder so weit, am 19.9. hat mit JDK 21 das nächste Long-term Support (LTS) Release nach JDK 17 das Licht der Welt erblickt. Mit diesem werden nun auch die Features und Änderungen aus JDK 18, JDK 19 und JDK 20 vermehrt Einzug in unsere Anwendungen finden.
Doch Moment, wieso kommt jetzt bereits nach zwei Jahren wieder ein LTS-Release heraus, waren nicht alle drei Jahre geplant? Ja, das war der Plan, bis Oracle mit dem Erscheinen von JDK 17 vorschlug, den Zeitraum auf zwei Jahre zu verkürzen. Dadurch, dass auch alle anderen relevanten Hersteller diesem Vorschlag zustimmten beziehungsweise ihm folgen, haben wir nun schon bereits nach zwei Jahren ein neues Release mit mindestens fünf Jahren Support, auch wenn es in zwei Jahren mit JDK 25 eine neue Version geben wird.
Zwar ist es auch möglich, jedes halbe Jahr auf die neueste Version vom JDK zu updaten, oft ist es jedoch sinnvoll, von LTS- zu LTS-Release zu gehen, um die Frequenz von neuen Features etwas zu bremsen und die Stabilität zu erhöhen. Dieser Artikel enthält genau deswegen einen Überblick über die relevanten Neuigkeiten seit JDK 17, um zu zeigen, wieso sich ein Upgrade auf das neue LTS-Release lohnen kann. Notwendig ist es noch nicht, da es noch mehrere Jahre Support auch für JDK 17 geben wird.
Doch bevor wir loslegen, noch einmal die Erinnerung daran, dass es neben finalen
und damit stabilen Features mittlerweile auch Incubator (JEP11) und
Preview Features (JEP12) gibt. Beide sind weniger stabil und können
sich bis zur finalen Version deutlich verändern. Bei Incubator Features besteht
sogar die Gefahr, dass diese es nie bis zu einer finalen Version schaffen und
vorher wieder entfernt werden. Und auch damit wir nicht aus Versehen ein nicht
stabiles Preview Feature verwenden, müssen wir diese, sowohl beim Kompilieren
als auch zur Laufzeit extra mit der Angabe von --enable-preview
aktivieren.
Doch genug der langen Einleitung, nun wollen wir uns in die Menge der neuen Features stürzen.
Standardmäßige Nutzung von UTF-8
Aus der Historie heraus gibt es eine Anzahl von Methoden und Konstruktoren rund
um das Lesen und Schreiben von Daten in der Klassenbibliothek, die ohne die
Angabe einer Zeichencodierung aufgerufen werden können. Diese nutzen dann intern
die Standardcodierung, welche über den Aufruf von Charset.defaultCharset()
ermittelt wird.
Vor JDK 18 wurde hier je nach verwendetem Betriebssystem oder gesetztem Wert für
die Systemvariable file.encoding eine unterschiedliche Codierung genutzt. Mit
JEP 400 wird dies auf UTF-8 vereinheitlicht. Sollte dies zu Problemen
führen, lässt sich weiterhin durch die Angabe von -Dfile.encoding=COMPAT
beim
Starten die bisherige Logik zur Ermittlung zu nutzen.
Da es sich hierbei um eine Änderung handelt, bei der Fehler unter Umständen erst
zur Laufzeit entdeckt werden, sollte, vor einem Update auf JDK 18 oder später,
durch die Angabe von -Dfile. encoding=UTF-8
getestet werden, ob die eigene
Anwendung weiterhin fehlerfrei funktioniert. Analog zur Laufzeit erwartet von
JDK 18 an auch der Compiler javac die Quelldateien in UTF-8-Codierung. Auch
hier kann bereits vorab durch die Angabe von -encoding UTF-8
getestet werden,
ob es bei einem Update zu Problemen kommt.
Die einzigen beiden Stellen, bei denen nicht auf UTF-8 gewechselt wird, sind
System.out
und System.err
. Da diese direkt mit dem Betriebssystem
interagieren, wird hier die Codierung von Console.charset()
verwendet.
Ein einfacher Webserver
Auch in JDK 18 hinzugekommen ist ein einfacher Webserver, der für
Entwicklungszwecke genutzt werden kann, um statische Dateien aus dem Dateisystem
auszuliefern, ähnlich wie der aus Python bekannte Simple HTTP Server, der, in
Version 2, mit python -m SimpleHTTPServer
gestartet werden kann.
Die in JEP 408 für die JVM entwickelte Variante lässt sich durch den
Aufruf von jwebserver
starten und liefert anschließend über das
Loopback-Interface auf Port 8000 das aktuelle Verzeichnis über HTTP 1.1 aus.
Diese Standardeinstellungen lassen sich auch durch die Angabe von Optionen beim
Start ändern. So kann mittels -d
auch ein anderes Verzeichnis gewählt und mit
-p
der Port geändert werden. Alle Möglichkeiten lassen sich über die Angabe
von -h
oder --help
ausgeben.
Alternativ kann der Webserver auch programmatisch als API in eigenem Code verwendet werden. Hierzu wurde der bereits bestehende HttpServer durch die Klassen SimpleFileServer, HttpHandlers und Request ergänzt. Listing 1 zeigt, wie die Nutzung von diesem aussehen kann.
Weder das API noch der als Tool nutzbare Server für statische Dateien ist dabei für den Produktionsbetrieb ausgelegt. Beides soll lediglich ermöglichen, zu Test- oder Entwicklungszwecken schnell verwendbar zu sein. Für produktive Anwendungen wird weiterhin der Einsatz der bekannten Server wie Jetty, Netty oder Tomcat empfohlen.
Code Snippets in Javadoc
Bisher mussten wir, um Code-Snippets in Javadoc zu haben, eine Kombination aus dem HTML-Tag pre und dem @code-Doclet (s. Listing 2) nutzen. Dies ist nun nicht mehr notwendig und wir brauchen seit JDK 18 nur noch das, durch JEP 413 definierte, @snippet-Doclet. Neben der einfacheren Handhabung ist es hiermit nun auch möglich, auf Regionen in Dateien zu verweisen (s. Listing 3).
Der auffälligste Vorteil ist, dass durch das Referenzieren einer Region das Javadoc selbst kompakter wird. Wenn wir diese Datei nun aber auch noch kompilieren und gegebenenfalls sogar als Test ausführen, dann können wir auch stets sicher sein, dass das Beispiel auch richtig ist und funktioniert.
Beides, Kompilieren und Ausführen, ist jedoch nicht Teil des JEPs und muss von uns sichergestellt werden. Eine Möglichkeit, dies mit Maven zu bewerkstelligen, zeigt Nicolai Parlog in seinem Blogpost „Configuring Maven For Compiled And Tested Code in Javadoc“.
Sequenced Collections
Das im JDK enthaltene Collections API ist zwar mächtig und enthält mit List, Set, Queue und Map diverse Typen, hatte bisher aber keine dedizierten Typen, um zu zeigen, dass eine Collection eine definierte Reihenfolge hat. Zwar haben List und Queue eine definierte Reihenfolge, nämlich die Reihenfolge des Einfügens, aber der gemeinsame Obertyp Collection macht dies nicht deutlich. Beim Set ist es wiederum so, dass es hier erst mal keine definierte Reihenfolge gibt, aber Subtypen wie SortedSet oder LinkedHashSet dann doch eine haben. Dies wird vor allem problematisch, wenn wir in unserer API ausdrücken wollen, dass wir eine sortierte Collection haben, diese aber nicht zwangsweise eine List sein muss. Hand in Hand hiermit ist es je nach Typ beliebig schwer, an das letzte Element zu gelangen oder rückwärts zu iterieren.
Um genau diese Probleme zu lösen, wurden in JDK 21 mit JEP 431
Sequenced Collections eingeführt. Diese bestehen vor allem aus den drei neuen
Interfaces SequencedCollection, SequencedSet und SequencedMap, welche von
den passenden vorhandenen Interfaces und Klassen implementiert werden.
SequencedCollection fügt dabei die in Listing 4 zu sehenden Methoden hinzu,
SequencedSet definiert lediglich den Rückgabetypen von reversed()
auf
SequencedSet
Neben der neuen Methode reversed()
kommen die anderen aus dem
Deque-Interface und wurden nun hierhin verschoben. Bei unveränderlichen
Implementierungen werfen die add-
und remove-
Methoden analog zu allen
anderen Methoden, die den Zustand verändern, eine
UnsupportedOperationException. Dasselbe passiert auch, wenn wir addFirst()
oder addLast()
bei einem SortedSet verwenden. Da hier die Reihenfolge nicht
von außen bestimmt werden kann, wird auch hier eine
UnsupportedOperationException geworfen. Bei anderen Sets, wie beispielsweise
dem LinkedHashSet, wird bei der Nutzung von addFirst()
oder addLast()
das
Element an die passende Stelle eingefügt. Sollte das Element vorher schon
vorhanden gewesen sein, wird das alte entfernt, sodass semantisch eine
Verschiebung des Elements durchgeführt wurde. Im Falle einer leeren Collection
werfen die get-
und remove-
Methoden übrigens eine NoSuchElementException.
Analog zur SequencedCollection enthält SequencedMap Methoden für eine gleiche Verwendung, wie in Listing 5 zu sehen ist. Auch hier können die Methoden je nach Art UnsupportedOperationException oder NoSuchElementException werfen.
Parallel hierzu wurde auch die Klasse Collections um Methoden erweitert, um eine vorhandene SequencedCollection in eine unveränderliche umzuwandeln.
Pattern Matching in switch und Record Patterns
Nachdem es seit JDK 16 und JEP 394 möglich ist, bei der Nutzung von instanceof gleichzeitig das geprüfte Objekt an eine Variable mit dem spezifischen Typen zu binden, siehe Listing 6, wird diese Art der Funktionalität nun schrittweise erweitert und an andere Stellen übertragen.
Eine dieser neuen Stellen ist die Verwendung dieses Musters im switch-Statement. Dieses Feature war bereits in JDK 17 als erste Preview vorhanden und hat es nun nach drei weiteren Previews (JEP 420 in JDK 18, JEP 427 in JDK 19 und JEP 433 in JDK 20) mit JEP 441 in JDK 21 als finales Feature geschafft.
Im Kern, siehe Listing 7, geht es dabei darum, in einem switch genau dann zu matchen, wenn die im case angegebene Klasse von der zu testenden Instanz implementiert oder erweitert wird. Passen mehrere case-Label auf eine Instanz, wird stets nur der erste Fall ausgewertet. Da unsere Instanz x in diesem Fall ein String ist und String auch CharSequence implementiert, wird hier nur „Michael is a String“ ausgegeben. Würden wir die beiden case-Fälle tauschen, sodass zuerst der CharSequence-Fall im Code steht, erhalten wir einen Kompilierungsfehler, da der String case-Fall nie erreicht werden kann. Dieser Fall wird vom anderen dominiert.
Zusätzlich wurde switch auch noch für den null-Fall erweitert und wir brauchen, wie in Listing 8 zu sehen, keine Prüfung auf null mehr vor dem switch. Außerdem ist es von nun an auch möglich, mittels when-Ausdruck neben dem Pattern Matching den case-Fall nur auf bestimmte Bedingungen reagieren zu lassen (s. Listing 9).
Ergänzend zu dieser neuen Möglichkeit von Pattern Matching wurde speziell für die Nutzung von Records die Möglichkeit eingebaut, diese während des Matchings zu destrukturieren. Auch dieses Feature, Record Patterns, ist mit JDK 21 und JEP 440, nach zwei Previews (JEP 405 in JDK 19 und JEP 432 in JDK 20), nun final fertig. Diese Destrukturierung kann dabei nicht nur auf erster Ebene, sondern wie in Listing 10 auch mit tiefer verschachtelten Records verwendet werden.
Und als finale Ergänzung rund um das gesamte Pattern-Thema gibt es mit JEP 443 auch noch ein Preview-Feature in JDK 21, um für nicht genutzte Patterns den Unterstrich als Variablennamen zu nehmen, wie in Listing 11 zu sehen ist. Gleichzeitig ist es nun auch möglich, den Unterstrich für ungenutzte Variablen an diversen Stellen zu nutzen. Hierzu zählen vor allem for-Schleifen, lokale Zuweisungen, Exceptions in catch-Blöcken, Variablen aus try-with-resources und ungenutzte Parameter eines Lambda-Ausdruckes. Listing 12 zeigt die Anwendung.
Die Vorteile dieser Art der Verwendung sind vor allem, dass beim Lesen sofort ersichtlich ist, dass diese Variable nicht mehr verwendet wird und dass wir den Unterstrich mehrmals im selben Block verwenden können. Vorher mussten wir uns mit Namen wie ignored behelfen und dann mit ignored2 usw. fortsetzen.
Virtual Threads und Concurrency
Neben dem Pattern Matching sind die virtuellen Threads, die nach zwei Previews (JEP 425 in JDK 19 und JEP 436 in JDK 20) nun mit JEP 444 in JDK 21 final erschienen sind, das zweite Highlight.
Mit diesen steht uns nun eine sehr leichtgewichtige Alternative zu den bekannten klassischen Threads zur Verfügung. Dies wird dadurch erreicht, dass es nicht wie bei einem Thread eine Eins-zu-eins-Beziehung zu einem nativen Betriebssystemthread gibt, sondern diese nur in der JVM abgebildet und geschedult werden.
Hierdurch ist es möglich, mehrere tausend, der JEP spricht sogar von Millionen, von Threads zu erstellen, ohne an Grenzen zu stoßen. Dadurch, dass die JVM deren Verwaltung übernimmt, kann diese, beispielsweise während ein solcher virtueller Thread auf das Netzwerk wartet, andere virtuelle Threads ausführen und somit quasi nicht blockierend agieren und die Ressourcen somit besser auslasten. Deswegen ist es auch nicht notwendig, virtuelle Threads in einem Pool zu verwalten. Diese sind so leichtgewichtig, dass eine Wiederverwendung nicht notwendig ist, sondern für jede Aufgabe ein neuer virtueller Thread erzeugt werden soll.
Um nun einen virtuellen Thread zu starten, kann entweder ein TaskExecutor oder die neue Thread.Builder-API, wie in Listing 13 zu sehen, verwendet werden. Natürlich ist es auch möglich, den Code in diesen Threads zu debuggen und auch in einem Threaddump tauchen diese auf. Hier allerdings nicht so ausführlich wie die nativen Threads, da Threaddumps mit mehreren Tausend virtuellen Threads zu schnell zu groß werden würden.
Während der Umsetzung von diesen virtuellen Threads stellte sich schnell die Frage, was innerhalb von diesen mit dem bisher genutzten ThreadLocal-Mechanismus passieren soll. Stand jetzt kann ThreadLocal wie bisher genutzt werden. Im virtuellen Thread gesetzte Werte sind dann für diesen Thread sichtbar, aber isoliert von dem nativen Thread, der den virtuellen Thread ausführt. Auch andersherum sind Werte, die im nativen Thread gesetzt werden, nicht vom virtuellen Thread aus sichtbar.
Da der gesamte Mechanismus von ThreadLocal jedoch nie für eine solche Menge an Threads gedacht war und auch das Konzept, dass diese von allen Stellen aus änderbar sind und die Lebenszeit, gerade bei gepoolten Threads, unendlich lange sein kann, nicht so einfach änderbar ist, enthält das JDK 21 mit JEP 446, nach einem Incubator in JDK 20 (JEP 429) ein weiteres Preview Feature, nämlich für Scoped Values. Scoped Values erlauben es uns, wie in Listing 14 zu sehen, Werte zu setzen, die anschließend von allen Methoden, die innerhalb des Scopes aufgerufen werden, sichtbar sind. Allerdings werden diese nicht in neue virtuelle Threads übertragen. Listing 15 wirft deswegen eine NoSuchElementException.
Möchten wir auch in diesem Szenario den gesetzten Wert erhalten, müssen wir diesen entweder erneut binden oder das nächste Preview Feature, nämlich die Structured Concurrency API, nutzen. Nachdem diese bereits zwei Incubator-Versionen, JEP 428 in JDK 19 und JEP 437 in JDK 20, hinter sich hat, gibt es nun mit JEP 453 eine erste Preview.
Ziel dieser API ist es, basierend auf virtuellen Threads und mit Unterstützung von Scoped Values eine einfachere Möglichkeit für parallele Programmierung anzubieten, als es bisher im JDK vorhanden war. Herzstück der neuen API ist dabei der StructuredTaskScope, über den wir, wie in Listing 16 zu sehen, Subtasks erzeugen und koordinieren können.
Der Vorteil hier ist, dass an der Stelle, an der wir auf beide Subtasks mittels
join()
warten, sämtliche Koordination passiert. Wird diese Stelle ohne
Exception passiert, sind danach garantiert beide Werte vorhanden. Sollte der
aufrufende Thread abgebrochen werden oder einer der beiden Subtasks eine
Exception werfen, werden garantiert alle noch laufenden Subtasks abgebrochen.
Für diesen Abbruch sorgt die von uns gewählte Shutdown Policy ShutdownOnFailure. Alternativ ist es auch möglich, ShutdownOnSuccess zu wählen, dann werden alle Subtasks abgebrochen, so bald einer der Subtasks erfolgreich durchlaufen wurde, oder eigene Policies zu erstellen.
Da durch diese Struktur die Subtasks nun auch noch verbunden sind, können Threaddumps diese Verbindung nun auch enthalten und uns bei einer potenziellen Analyse unterstützen.
Und sonst so?
Neben diesen großen Themen haben sich über vier Releases auch noch weitere spannende JEPs angesammelt, welche wir uns jetzt hier nicht noch im Detail anschauen können, aber kurz erwähnt sollen diese trotzdem werden.
Im Rahmen von JEP 430 werden String Templates in JDK 21 als Preview Feature implementiert. Diese erlauben uns nicht nur innerhalb von Strings auf Variablen zuzugreifen, sondern ermöglichen auch noch Validierung oder Escaping von diesen.
JEP 445 unterstützt, auch als Preview Feature in JDK 21, das Ziel,
einen leichteren Einstieg in die Sprache zu bekommen. Hierzu ist es nun möglich,
die main-Methode nicht mehr static deklarieren zu müssen und auch auf die
Argumente verzichten zu können, sofern diese nicht benötigt werden. Außerdem
ermöglicht dieser JEP es, mit einigen Einschränkungen, sogar auf die Klasse der
main-Methode zu verzichten, wodurch
void main() { System.out. println("Hallo"); }
ein valides Java-Programm wird.
Mit JEP 418 ist es seit JDK 18 möglich, alternative Implementierungen für die DNS-Auflösung zu nutzen. Das JDK liefert allerdings nach wie vor die identische Implementierung aus, lediglich das Service Provider Interface (SPI) wurde im Rahmen dieses JEPs definiert.
Auch mit JDK 18 wurde das Thema „Finalization“ innerhalb von JEP 421
Deprecated und wird in Zukunft komplett entfernt. Wer schon jetzt ausprobieren
möchte, ob das zu Problemen führt, kann seine Anwendung mit der Option
--finalization=disabled
starten. Nun werden keinerlei Finalizer mehr
ausgeführt. In Zukunft wird diese Option erst zum Standard, um in einem späteren
Schritt dann die Funktionalität komplett zu entfernen.
Mit JEP 439 wird in JDK 21 der Z Garbage Collector (ZGC) um Generationen erweitert. Somit ist es nun auch für diesen möglich, Objekte, die nicht lange leben, früher und häufiger abzuräumen, und somit besser seine Arbeit zu verrichten.
Um in einem späteren Release das dynamische Laden von Java Agents zu verhindern, wurden im JDK 21 mit JEP 451 schon erste Vorarbeiten begonnen. Diese Änderung soll die Sicherheit erhöhen, indem sichergestellt wird, dass Code zur Laufzeit nicht plötzlich verändert wird. Aktuell kann das aber noch zu Problemen führen, denn vor allem Tools für Mocking nutzen die Möglichkeit, Java Agents dynamisch während der Laufzeit zu laden, noch an einigen Stellen.
In JDK 21 geht die neue Vector API mit JEP 448 in ihre sechste Incubator Version. Ziel ist es, Vektorberechnungen optimal auf der CPU auszuführen.
Zuletzt enthält das JDK 21 noch, mit JEP 442, die dritte Preview Version für den Zugriff auf native Funktionen und Speicherbereiche. Ziel ist es, hiermit einen verbesserten Nachfolger für das Java Native Interface (JNI) bereitzustellen.