What's New in Java 25 in 2 Minutes - Sip of Java

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!

New Features

11 JEPs introduced or finalized new features in JDK 25.

JEP 506 - Scoped Values

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);
    }

}

JEP 510 - Key Derivation Function API

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

JEP 511 - Module Import Declarations

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
...

JEP 512 - Compact Source Files and Instance Main Methods

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!");

JEP 513 - Fleixble Constructor Bodies

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;
    }

}

JEP 514 - Ahead-of-Time Command-Line Ergonomics

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 ...

JEP 515 - Ahead-of-Time Method Profiling

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.

JEP 518 - JFR Cooperative Sampling

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.

JEP 519 - Compact Object Headers

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 ...

JEP 520 - JFR Method Timing & Tracing

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.

Method Tracing

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...                                                                                                                                                                       
...

Method Timing

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
...

JEP 521 - Generational Shenandoah

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 ...

Preview and Incubator Features

There are six JEPs that covered experimental, preview, or incubator features in JDK 25.

JEP 470 - PEM Encodings of Cryptographic Objects (Preview)

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.

JEP 502 - Stable Values (Preview)

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");
    }

}

JEP 505 - Structured Concurrency (Fifth Preview)

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());

    }

}

JEP 507 - Primitive Types in Patterns, instanceof, and switch (Third Preview)

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();
}

JEP 508 - Vector API (Tenth Incubator)

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.

JEP 509 - JFR CPU-Time Profiling (Experimental)

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 ...

Removed Features

Only a single JEP covers a feature that was removed in JDK 25.

JEP 503 - Remove the 32-bit x86 Port

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.

Conclusion

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.