In meinem vorangehenden Beitrag ging es um die sich noch in Entwicklung befindlichen Features, die als Vorschau in Java 25 enthalten sind. Nun folgen die Funktionalitäten, die in Java 25 finalisiert und für den produktiven Einsatz bereit sind.
Am 16.09.2025 ist Java 25 erschienen, und hat, wie jedes halbjährig erscheinende Release, wieder etliche neue Funktionalitäten mitgebracht. Und auch wenn es für die Sprache Java selbst grundsätzlich keine LTS-Releases gibt (obwohl es oft gerne so formuliert wird), werden wohl die meisten Herausgeber verlängerten Support für ihre Java-25-Distributionen anbieten.
JEP-506: Scoped Values
Scoped Values ermöglichen es, Daten innerhalb einer Aufrufhierarchie verfügbar zu machen, ohne sie explizit über Methodenparameter weiterreichen zu müssen. Der Gültigkeitsbereich (Scope) der geteilten Daten ist dabei klar auf einen bestimmten Ausführungskontext abgegrenzt, einschließlich aller direkt und indirekt aufgerufenen Methoden. Nach Beendung des entsprechenden Abschnitts wird der Wert wieder freigegeben.
Wie bei ThreadLocal ist der Wert zudem an den aktuell ausgeführten Thread gebunden. In unterschiedlichen Threads kann eine ThreadLocal-Variable also unterschiedliche Werte annehmen. Somit ist es beispielsweise möglich, Benutzer-Sessions in einem Web-Framework über einen ScopedValue bereitzustellen:
public class WebFramework {
final static ScopedValue<UserSession> USER_SESSION = ScopedValue.newInstance();
private final SessionStore sessionStore;
private final Service service;
public WebFramework(SessionStore sessionStore, Service service) {
this.sessionStore = sessionStore;
this.service = service;
}
void doRequest(Request request, Response response) {
UserSession userSession = sessionStore.currentSession();
ScopedValue.where(USER_SESSION, userSession)
.run(() -> service.handle(request, response));
// …
}
}
class Service {
public void handle(Request request, Response response) {
UserSession session = WebFramework.USER_SESSION.get();
if (session.isLoggedIn()) {
// …
}
}
}
Zuerst wird im WebFramework eine ScopedValue-Instanz vom Typ UserSession instanziert (Zeile 3). Die Referenz ist statisch, so dass auch aus anderen Codeteilen darauf zugegriffen werden kann.
In der doRequest(…)-Methode wird die userSession aus dem Store geholt (Zeile 14). Dann wird sie an die where(…)-Methode von ScopedValue übergeben (Zeile 16), und im Anschluss direkt run(…) aufgerufen (Zeile 17), wo die Bearbeitung an die service-Instanz weiter delegiert wird.
In der handle(…)-Methode der Klasse Service kann nun die Benutzersession via get() aus der ScopedValue-Instanz ausgelesen und genutzt werden.
Die in where(…) gesetzte User-Session (Z. 17) ist nur innerhalb des Scopes der run(…)-Methode gültig. Würde man in Zeile 18 versuchen, via USER_SESSION.get() auf die zuvor gesetzte userSession zuzugreifen, wäre das Ergebnis eine NoSuchElementException.
ScopedValues sind dabei leichtgewichtiger als ThreadLocal. Insbesondere sind sie geeignet für die Verwendung zusammen mit Virtual Threads, von denen es sehr viele geben kann, im Gegensatz zu den herkömmlichen Threads. ScopedValues können zudem wie ThreadLocals an Kind-Threads weitervererbt werden, sind dabei aber weniger ressourcenhungrig. Da ScopedValues unveränderlich sind, tragen sie auch zu robusterem sowie performanterem nebenläufigen Code bei (-> zum JEP-506).
JEP-510: Key Derivation Function API
In Java 24 erstmals als Preview enthalten, wird die Key Derivation Function API bereits ein Release später unverändert als offizielles Java-Feature veröffentlicht.
Key Derivation Functions (KDFs) nutzen kryptographische Eingaben wie Basisschlüssel, Salt und Pseudozufallsfunktionen, um neue, starke Schlüssel zu generieren. Sie erlauben die reproduzierbare und sichere Erzeugung verschiedener Schlüssel, ähnlich wie beim Password-Hashing. KDFs extrahieren oder erweitern Schlüsselmaterial, indem sie einen Keyed Hash mit zusätzlicher Entropie kombinieren. Angesichts der steigenden Bedrohung durch Quantencomputing wird post-quantensichere Kryptographie wichtiger. Die Java-Plattform strebt an, mit Hybrid Public Key Encryption (HPKE) und einer neuen KDF-API den Übergang zu quantensicheren Algorithmen zu erleichtern. Dies fördert Interoperabilität, unterstützt Standards wie PKCS#11 und ermöglicht modernere Password-Hashing-Verfahren (z.B. Argon2). Dadurch wird die Sicherheit und Flexibilität zukünftiger Anwendungen deutlich erhöht.
Codebeispiel aus dem JEP:
// Create a KDF object for the specified algorithm
KDF hkdf = KDF.getInstance(„HKDF-SHA256“);
// Create an ExtractExpand parameter specification
AlgorithmParameterSpec params =
HKDFParameterSpec.ofExtract()
.addIKM(initialKeyMaterial)
.addSalt(salt).thenExpand(info, 32);
// Derive a 32-byte AES key
SecretKey key = hkdf.deriveKey(„AES“, params);
// Additional deriveKey calls can be made with the same KDF object
-> zum JEP-510
JEP-511: Module Import Declarations
Mit dem neuen „import module“-Statement ist es möglich, alle Klassen, die von einem Modul exportiert werden, mit nur einem Statement zu importieren:
import module java.base;
import module java.sql;
// resolve ambiguous import of java.util.Date and java.sql.Date
import java.util.Date;
public class ModuleImports {
public static void main(String[] args) throws Exception {
Stream<String> myStream = Stream.of(„Alfred“, „Bea“, „Charlotte“);
List result = myStream.filter(n -> n.length() > 3).toList();
Function<Integer, Integer> square = (i) -> i * i;
Files.list(Paths.get(„.“)).forEach(System.out::println);
Connection con = DriverManager.getConnection(„jdbc:postgresql://localhost/test“);
XMLGregorianCalendar cal = DatatypeFactory.newInstance()
.newXMLGregorianCalendar(„2024-09-11T18:17:42.123Z“);
Date d = new Date();
}
}
Durch die Modul-Imports von java.base und java.sql stehen bereits alle in der main-Methode verwendete Klassen zur Verfügung, so dass diese nicht explizit importiert werden müssen. Durch den Import von java.base erhält man u.a. Zugriff auf java.util.stream.Stream, java.util.List, java.util.function.Function, java.nio.file.Files sowie java.nio.file.Paths.
Die Klassen java.sql.Connection und java.sql.DriverManager werden durch den Import des Moduls java.sql verfügbar.
Da das Modul java.sql eine transitive Abhängigkeit zum Modul java.xml hat, wird implizit das Modul java.xml mit importiert, so dass auch die Klassen javax.xml.datatype.XMLGregorianCalendar und javax.xml.datatype.DatatypeFactory ohne weitere Imports genutzt werden können.
Da sowohl das Modul java.base als auch java.sql eine Klasse namens Date exportieren, gibt es hier eine Mehrdeutigkeit (java.util.Date und java.sql.Date). Um diese aufzulösen, wird java.util.Date nochmal explizit importiert, so dass diese in Zeile 19 verwendet wird.
Die importierende Klasse muss sich übrigens nicht in einem Modul befinden (-> zum JEP-511).
JEP-512: Compact Source Files and Instance Main Methods
Um Einsteigern das Lernen von Java so einfach wie möglich zu machen, sieht das einfachste Java-Programm nun wie folgt aus:
void main() {
IO.println(„Hello World“);
}
So kommt das erste Erfolgserlebnis schnell und unkompliziert, ohne dass man sich mit Konzepten wie Klassen, statischen Methoden oder Methodensichtbarkeit auseinandersetzen muss. Erfahrene Entwickler können kleine Programme mit weniger Code schreiben, ohne Konstrukte nutzen zu müssen, die für große Codebasen gedacht sind.
Bei der Auswahl der auszuführenden Methode geht die JVM wie folgt vor: Gibt es eine main-Methode mit Parameter String[] args, wird diese ausgeführt. Existiert eine solche Methode nicht, wird die Methode main() ohne Parameter ausgeführt, sofern vorhanden. Ob statisch oder nicht, spielt dabei keine Rolle.
void main(String[] args) { // wird ausgeführt
IO.println(„Hello World!“);
}
static void main() {
IO.println(„I won’t be called!“);
}
Zudem wird die Klasse java.lang.IO eingeführt, die ohne Import verfügbar ist, und mit der die zeilenbasierte Ein- und Ausgabe vereinfacht werden soll (-> zum JEP-512).
JEP-513: Flexible Constructor Bodies
Bisher müssen Aufrufe von super(…) bzw. this(…) in Konstruktoren immer an erster Stelle stehen. Mit diesem JEP ist es nun möglich, bestimmte Statements diesen Aufrufen voranzustellen. Für solche Statements gibt es allerdings Einschränkungen, sie dürfen etwa keine Referenzen auf die zu erstellende Instanz beinhalten. Aufrufe statischer Methoden sind demnach u.a. erlaubt.
public class PositiveBigInteger extends BigInteger {
public PositiveBigInteger(String val) {
String trimmed = val.trim();
if (trimmed.startsWith(„-„)) {
throw new IllegalArgumentException(„Negative values are not allowed“);
}
super(trimmed);
}
}
Vor dem super(…)-Aufruf ist es nun möglich, wie in der oben gezeigten Klasse eine Validierung durchzuführen. Bisher hätte man die Validierung entweder erst nach dem super-Aufruf gemacht und im Fehlerfall ggf. unnötige Arbeit ausgeführt. Die Alternative wäre, val im super-Aufruf in eine Validierungsmethode zu wrappen. Demgegenüber ist der oben gezeigte Code sowohl effizienter als auch leichter verständlich.
Für this(…)-Aufrufe gilt dasselbe, wie das nachfolgende Beispiel zeigt:
public class EMailAddress {
public EMailAddress(String eMailAddress) {
if (!eMailAddress.contains(„@“)) {
throw new IllegalArgumentException(
„Invalid e-mail address: “ + eMailAddress);
}
String[] parts = eMailAddress.split(„@“);
this(parts);
}
private EMailAddress(String[] parts) {
// …
}
}
-> zum JEP-513
JEP-514: Ahead-of-Time Command-Line Ergonomics
Ahead-of-Time-Caches, eingeführt durch JEP-483 in Java 24, beschleunigen den Start von Java-Anwendungen. Für die Erzeugung eines AOT-Caches waren bisher 2 Schritte notwendig (Details siehe hier).
Um die Cache-Erzeugung für den Standardfall komfortabler zu machen, wurde die neue Kommandozeilenoption AOTCacheOutput eingeführt, anhand derer man die Zieldatei für den AOT-Cache angeben kann. Dadurch lässt sich der AOT-Cache nun mit nur einem Kommando erzeugen, ohne den Umweg über eine .aotconf-Datei:
$ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App …
Dieser Aufruf startet zuerst einen Trainingslauf (AOTMode=record), und erzeugt im Anschluss den AOT-Cache in der angegebenen Datei app.aot.
Dieser lässt sich dann nutzen, um das Programm mit beschleunigter Startzeit auszuführen:
$ java -XX:AOTCache=app.aot -cp app.jar com.example.App
Außerdem wurde eine neue Umgebungsvariable namens JDK_AOT_VM_OPTIONS eingeführt, mit der Kommandozeilenoptionen angegeben werden können, die ausschließlich die Cache-Erzeugung (AOTMode=record) betreffen, ohne den Trainingslauf (AOTMode=record) zu beeinflussen (zum JEP-514).
JEP-515: Ahead-of-Time Method Profiling
Zu Beginn der Laufzeit eines Java-Programms widmet die JVM einen Teil ihrer Ressourcen der Aufgabe, Methodenprofile zu erstellen, um sogenannte „hot methods“ zu identifizieren, die oft aufgerufen werden und viele Ressourcen, wie beispielsweise CPU-Zeit, benötigen. Diese werden dann vom Just-in-Time-Compiler zu nativem Code kompiliert. Dies hat zur Folge, dass das Programm während dieser Aufwärmphase weniger performant läuft.
Die Aufwärmdauer kann reduziert werden, indem die Profilerstellung in Trainingsläufe vorverlagert wird. Im AOT-Cache, der Klasseninformationen für einen schnelleren Programmstart bereithält, werden nun zusätzlich Methodenprofile gespeichert, die ansonsten in der Startphase der produktiven Ausführung des Programms gesammelt werden müssten.
Diese in Trainingsläufen angefertigten Profile verhindern keine zusätzliche Profilerstellung während des produktiven Laufs, da das Verhalten einer Anwendung in Produktion von dem im Training beobachteten Verhalten abweichen kann. Auch mit zuvor erstellten Profilen profiliert und optimiert die HotSpot-JVM die Anwendung während der Ausführung weiterhin, wobei sie die Vorteile von AOT-Profilen, Online-Profilerstellung und JIT-Kompilierung miteinander verbindet. Dies führt dazu, dass der JIT-Kompiler früher und mit größerer Genauigkeit ausgeführt wird, und die Profile zur Optimierung der „hot methods“ verwendet werden, so dass die Aufwärmphase sich verkürzt. Da die JIT-Kompilierung parallel zur Programmausführung geschieht, kann die tatsächliche Aufwärmzeit kürzer sein, wenn genügend Hardware-Ressourcen zur Verfügung stehen (zum JEP-515).
JEP-518: JFR Cooperative Sampling
Der JDK Flight Recorder (JFR) kann ein Laufzeitprofil erstellen das zeigt, welche Programmelemente einen Großteil der Zeit beanspruchen. Dazu werden die Ausführungs-Stacks von Programm-Threads in festen Intervallen, beispielsweise alle 20 Millisekunden, aufgezeichnet. Tools wie jfr und JDK Mission Control können eine Reihe solcher Aufzeichnungen zu einem textbasierten oder grafischen Profil aggregieren.
Dieser Sampling-Mechanismus wird neu designt, um den JFR stabiler und effizienter zu machen. Für Details zur Funktionalität des Samplings und den Optimierungen sei auf den JEP verwiesen (-> zum JEP-518).
JEP-520: JFR Method Timing & Tracing
Timing und Tracing von Methodenaufrufen können sehr hilfreich für die Performanceoptimierung oder der Suche nach den Ursachen von Bugs sein. Während es hier für die Entwicklungsphase bereits gute Werkzeuge gibt, wie z.B. Java Microbenchmark Harness (JMH) und Debugger, muss man sich in Test und Produktion noch mit Workarounds behelfen.
Um diese Lücke zu füllen, werden zwei neuen JFR-Events eingeführt: jdk.MethodTiming und jdk.MethodTrace. Mit einem Filter kann man spezifizieren, für welche Methoden die Events aufgezeichnet werden sollen.
So kann man beispielsweise folgendermaßen herausfinden, wodurch die resize()-Methode der Klasse HashMap ausgelöst wird:
$ java -XX:StartFlightRecording:jdk.MethodTrace#filter=java.util.HashMap::resize,filename=recording.jfr …
$ jfr print –events jdk.MethodTrace –stack-depth 20 recording.jfr
jdk.MethodTrace {
startTime = 00:39:26.379 (2025-03-05)
duration = 0.00113 ms
method = java.util.HashMap.resize()
eventThread = „main“ (javaThreadId = 3)
stackTrace = [
java.util.HashMap.putVal(int, Object, Object, boolean, boolean) line: 636
java.util.HashMap.put(Object, Object) line: 619
sun.awt.AppContext.put(Object, Object) line: 598
sun.awt.AppContext.<init>(ThreadGroup) line: 240
sun.awt.SunToolkit.createNewAppContext(ThreadGroup) line: 282
sun.awt.AppContext.initMainAppContext() line: 260
sun.awt.AppContext.getAppContext() line: 295
sun.awt.SunToolkit.getSystemEventQueueImplPP() line: 1024
sun.awt.SunToolkit.getSystemEventQueueImpl() line: 1019
java.awt.Toolkit.getEventQueue() line: 1375
java.awt.EventQueue.invokeLater(Runnable) line: 1257
javax.swing.SwingUtilities.invokeLater(Runnable) line: 1415
java2d.J2Ddemo.main(String[]) line: 674
]
}
Benötigt eine Anwendung viel Zeit für den Start, könnte eine Zeitmessung aller statischen Initialisierungsblöcke Aufschluss über die Ursache geben:
$ java ‚-XX:StartFlightRecording:method-timing=::<clinit>,filename=clinit.jfr‘ …
$ jfr view method-timing clinit.jfr
Method Timing
Timed Method Invocations Average Time
—————————————————— ———– ————
sun.font.HBShaper.<clinit>() 1 32.500000 ms
java.awt.GraphicsEnvironment$LocalGE.<clinit>() 1 32.400000 ms
java2d.DemoFonts.<clinit>() 1 21.200000 ms
java.nio.file.TempFileHelper.<clinit>() 1 17.100000 ms
sun.security.util.SecurityProviderConstants.<clinit>() 1 9.860000 ms
java.awt.Component.<clinit>() 1 9.120000 ms
sun.font.SunFontManager.<clinit>() 1 8.350000 ms
sun.java2d.SurfaceData.<clinit>() 1 8.300000 ms
java.security.Security.<clinit>() 1 8.020000 ms
sun.security.util.KnownOIDs.<clinit>() 1 7.550000 ms
…
$
Es ist auch möglich, auf Klassenebene zu filtern, so dass alle Methoden einer Klasse getimed oder getraced werden. Genauso kann man den Filter auf alle Methoden anwenden, die mit einer bestimmten Annotation versehen sind.
Es sind auch mehrere Filter möglich, und anstatt diese in der Kommandozeile zu spezifizieren, können diese auch in eine Konfigurationsdatei geschrieben werden (-> zum JEP-520).
JEP-519: Compact Object Headers
Kompakte Objekt-Header wurden im Rahmen von JEP-450 als experimentelles Feature in Java 24 eingeführt. Seitdem wurde die Funktionalität ausgiebig getestet, so dass sie nun ein Produktfeature in Java 25 ist:
$ java -XX:+UseCompactObjectHeaders …
Die Aktivierung über diesen Kommandozeilenparameter verkleinert Objektheader von 96-128 auf 64 Bit, was wiederum zu einer Verringerung des Gesamtspeicherverbrauchs (in einem Benchmark-Szenario 22%) und weniger Garbage-Collection-Läufen führt. Der Parameter
-XX:+UnlockExperimentalVMOptions
ist nun nicht mehr notwendig (-> zum JEP-519).
JEP-521: Generational Shenandoah
Der Shenandoah Garbage Collector wird um einen generationalen Modus erweitert, der den Speicherverbrauch, die CPU-/Energieeffizienz und die Resilienz bei Lastspitzen verbessern soll – ohne die bewährte nicht-generationale Variante zu ersetzen. In diesem Ansatz wird der Java-Heap in zwei Generationen unterteilt: eine junge Generation, in der die meisten kurzlebigen Objekte gesammelt werden, und eine alte Generation, die langfristig erhaltene Objekte verwaltet. Ziel ist es, den Speicherbedarf zu senken und gleichzeitig niedrige GC-Pausen beizubehalten.
Seit der Einführung als experimentelles Feature in Java 24 (JEP-404) wurden viele Stabilitäts- und Performance-Verbesserungen implementiert und ausgiebige Tests durchgeführt. Und das erfolgreich, so dass der Status „experimentell“ nun entfällt.
Der generationale Modus wird (zusammen mit dem Shenandoah GC) wie folgt aktiviert:
$ java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational
-> zum JEP-521.
JEP-503: Remove the 32-bit x86 Port
Die Kosten für die Pflege der 32-Bit-Portierung von Java übersteigen die Vorteile bei weitem. In Java 24 erfolgte die Markierung als Deprecated und Entfernung in einem späteren Release (JEP-501). Dadurch werden Entwicklerressourcen frei, um die Entwicklung neuer Features und Verbesserungen zu beschleunigen. In Java 25 wurde der Quellcode und die Build-Unterstützung für den 23-Bit-Port nun entfernt (-> zum JEP-503).
Fazit
Auch Java 25 wartet wieder mit einer Fülle neuer Features auf: APIs, die die Entwicklung komfortabler machen, sowie Optimierungen „unter der Haube“ in der JVM und dem JDK Flight Recorder.
Der Beitrag Java 25 – Die neuen Features erschien zuerst auf Business -Software- und IT-Blog – Wir gestalten digitale Wertschöpfung.