News and views from members of the Java team at Oracle
JDK 25 was released on September 16th, and with it came 18 JEPs (JDK Enhancement Proposals), many of them being final features. Let's quickly review the major changes that came in JDK 25!
11 JEPs introduced or finalized new features in JDK 25.
The second major feature from Project Loom, Scoped Values are an immutable value that is available within a specific scope of an application. They can be useful for providing contextual information, similar to ThreadLocal, though aren't a direct replacement for them.
class Framework {
private static final ScopedValue<FrameworkContext> CONTEXT
= ScopedValue.newInstance();
void serve(Request request, Response response) {
var context = createContext(request);
where(CONTEXT, context)
.run(() -> Application.handle(request, response));
}
public PersistedObject readKey(String key) {
var context = CONTEXT.get();
var db = getDBConnection(context);
db.readKey(key);
}
}
The Key Derivation Function (KDF) API is a final feature in JDK 25, after previewing in JDK 24. The KDF API allows for the creation of additional keys from a secret key and other data. The sample code below demonstrates how to use the KDF API.
// 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
Module Import Declarations allow for the importing of all the exported API of a module in a single line with import module [module name] like in the example above.
import module java.base;
String[] fruits = new String[] { "apple", "berry", "citrus" };
Map<String, String> m =
Stream.of(fruits).collect(Collectors.toMap(s -> s.toUpperCase().substring(0,1), Function.identity()));
If you are importing multiple modules that contain the same class name, like Date in the above example, you can resolve the conflict by explicitly importing the class java.sql.Date.
import module java.base; // exports java.util, which has a public Date class
import module java.sql; // exports java.sql, which has a public Date class
import java.sql.Date; // resolve the ambiguity of the simple name Date!
...
Date d = ... // Ok! Date is resolved to java.sql.Date
...
The highlight of the "Paving the On-Ramp" feature set, Compact Source Files and Instance Main Methods is finalized in JDK 25. Compact source files and instance main methods simplify the requirements for writing very simple applications, including reducing "Hello World" to three lines, like in the example above. Helpful for educational purposes, and when writing "scripts" in Java.
void main() {
IO.printin("Hello, World!");
}
Flexible Constructor Bodies is finalized in JDK 25. This feature enables adding statements before the call to super in a constructor, which can be helpful for performing data validation or setting default values.
class Person {
...
int age;
void show() {
System.out.println("Age: " + this.age);
}
Person(..., int age) {
if (age < 0)
throw new IllegalArgumentException(...);
...
this.age = age;
show();
}
}
class Employee extends Person {
String officeID;
@Override
void show() {
System.out.println("Age: " + this.age);
System.out.println("Office: " + this.officeID);
}
Employee(..., int age, String officeID) {
super(..., age); // Potentially unnecessary work
if (age < 18 || age > 67)
throw new IllegalArgumentException(...);
this.officeID = officeID;
}
}
Ahead-of-Time Command-Line Ergonomics simplifies the process for creating an AOT cache by allowing it to be done in a single step. The two-step process of recording the JVM and creating the cache is still valid and supported. With JEP 514, a user can pass the command -XX:AOTCacheOutput=[cache name] to have a cache created on shut down of the JVM.
# Create the AOT cache
$ java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App ...
# Use the AOT cache
$ java -XX:AOTCache=app.aot -cp app.jar com.example.App ...
Ahead-of-Time Method Profiling allows method profiles to be added to an AOT cache. This improves application warmup by allowing the JIT to immediately start generating native code on application startup.
JFR Cooperative Sampling improves JFR's stability by redesigning its thread stack sampling mechanism. This is an implementation change which shouldn't have any behavior impacts, outside of improved stability.
Compact Object Headers is now a final feature after being added as an experimental feature in JDK 24. Compact Object Headers reduces the minimum size an object header can be, which reduces heap sizes by 10-20% and also reduces GC pressure reducing latency.
$ java -XX:+UseCompactObjectHeaders ...
JFR Method Timing & Tracing allows JFR to be configured to time and trace methods. This works via JFR events, and can be configured via the command line like in the respective examples below demonstrating method trace and timing and their output.
To enable method tracing, you will configure the JFR event jdk.MethodTrace like in the example below. You can use the jfr view command to review the results of the recording.
$ java -XX:StartFlightRecording:
jdk.MethodTrace#filter=
org.springframework.data.jpa.repository.support.SimpleJpaRepository::findAll, \
filename=recording.jfr ...
$jfr view --cell-height 30 --width 200 jdk.MethodTrace recording.jfr
Method Trace
Start Time Duration Event Thread Stack Trace Method
---------- -------- -------------------- ------------------------------------------------------------------------------- -------------------------------------------------------------------------------
15:49:48 43.3 ms http-nio-8080-exec-1 java.lang.invoke.LambdaForm$DMH.0x000040000115c000.invokeVirtual(...) org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(Pag
java.lang.invoke.LambdaForm$MH.0x00004000010a8c00.invoke(...) eable)
java.lang.invoke.Invokers$Holder.invokeExact_MT(Object, Object, Object, Object)
jdk.internal.reflect.DirectMethodHandleAccessor.invokeImpl(Object, Object[])
jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Object, Object[])
java.lang.reflect.Method.invoke(Object, Object[])
org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(...)
org.springframework.data.repository.core.support.RepositoryMethodInvoker$Rep...
org.springframework.data.repository.core.support.RepositoryMethodInvoker$Rep...
org.springframework.data.repository.core.support.RepositoryMethodInvoker.doI...
org.springframework.data.repository.core.support.RepositoryMethodInvoker.inv...
org.springframework.data.repository.core.support.RepositoryComposition$Repos...
org.springframework.data.repository.core.support.RepositoryComposition.invok...
org.springframework.data.repository.core.support.RepositoryFactorySupport$Im...
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed()
org.springframework.data.repository.core.support.QueryExecutorMethodIntercep...
org.springframework.data.repository.core.support.QueryExecutorMethodIntercep...
org.springframework.aop.framework.ReflectiveMethodInvocation.proceed()
org.springframework.transaction.interceptor.TransactionInterceptor$$Lambda.0...
...
To enable method timing, you will configure the JFR option method-timing like in the example below. You can use the jfr view command to review the results of the recording.
$ java -XX:StartFlightRecording=method-timing='org.springframework.data.jpa.repository.support.SimpleJpaRepository::findAll',dumponexit=true,filename=recording.jfr -jar target/spring-petclinic-3.5.0-SNAPSHOT.jar
$ jfr view method-timing recording.jfr
Method Timing
Timed Method Invocations Minimum Time Average Time Maximum Time
---------------------------------------------------------------------------------------------------------------------------------------------------- ----------- ------------ ------------ ------------
org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(Pageable) 2 3.440000 ms 25.700000 ms 47.900000 ms
org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(Specification, Pageable) 2 3.440000 ms 25.700000 ms 47.900000 ms
org.springframework.data.jpa.repository.support.SimpleJpaRepository.findAll(Specification, Specification, Pageable) 2 3.420000 ms 25.600000 ms 47.900000 ms
...
Generational Shenandoah has been made a final feature in JDK 25. If available on your JDK it can be enabled with the above JVM args.
$ java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational ...
There are six JEPs that covered experimental, preview, or incubator features in JDK 25.
PEM Encodings of Cryptographic Objects introduces an API for encoding objects that represent cryptographic keys, and certificate revocation lists into the widely-used Privacy-Enhanced Mail (PEM) transport format.
Stable Values is an API for holding immutable data that can be treated as a constant by the JVM. A StableValue can only be set once, put provides more fleixiblity than what the final keyword allows, which requires a variable to be set at either class or instance initialization.
class OrderController {
// OLD:
// private Logger logger = null;
// NEW:
private final StableValue<Logger> logger = StableValue.of();
Logger getLogger() {
return logger.orElseSet(() -> Logger.create(OrderController.class));
}
void submitOrder(User user, List<Product> products) {
getLogger().info("order started");
...
getLogger().info("order submitted");
}
}
Structured Concurrency treats groups of related tasks running in different threads as single units of work, thereby streamlining error handling and cancellation, improving reliability, and enhancing observability. The fifth preview introduces a major update to the API from previous versions of it.
Response handle() throws InterruptedException {
try (var scope = StructuredTaskScope.open()) {
Subtask<String> user = scope.fork(() -> findUser());
Subtask<Integer> order = scope.fork(() -> fetchOrder());
scope.join(); // Join subtasks, propagating exceptions
// Both subtasks have succeeded, so compose their results
return new Response(user.get(), order.get());
}
}
Primitive Types in Patterns, instanceof, and switch enhances pattern matching allowing primitives to be used in all contexts.
switch (x.getYearlyFlights()) {
case 0 -> standardRate();
case 1 -> standardRate();
case 2 -> issueDiscount();
case int i when i >= 100 -> issueGoldCard();
case int i when i › 2 && i < 100 -> issueSilverDiscount();
}
Vector API enables the expressing vector computations that reliably compile at runtime to optimal vector instructions on supported CPUs, thereby achieving performance superior to equivalent scalar computations. The Vector API will remain in incubator status until the promotion of Project Valhalla features into the mainline JDK.
JFR CPU-Time Profiling allows JFR to capture more accurate CPU-time profiling information. Note this feature only works on Linux.
$ java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true, filename=profile.jfr ...
Only a single JEP covers a feature that was removed in JDK 25.
Remove the 32-bit x86 Port removes all code and build support for 32-bit x86 ports. Subsequently 32-bit OpenJDK binaries will no longer be available starting with the JDK 25 release.
Java 25 represents yet another successful release of Java, following the six-month release cadence. Because this release will come with long-term support offerings (colloquially, an "LTS Release"), many Java developers will take a particular interest in it, as well as in 22, 23, and 24. To that end, I would highly suggest watching out Road to Java 25 video series, and checking out our Java 25 Launch Livestream where we covered all the important changes between Java 21 and 25.