Java 25: The tl;dr Version

Here’s a quick, no-fluff rundown of the new features landing in Java 25.

JDK 25 packs 18 new features: 12 delivered, 4 in preview, 1 experimental, and 1 incubator. In this article, we’ll walk through all of them, show live code examples you can run immediately, and highlight 🪄 the most impactful piece inside the code.

Finalized Features

JEP 503: Remove the 32-bit x86 Port
# Java won’t work at all if you are on a 32-bit x86 OS
# This command will produce errors
java -version
The official removal of the 32-bit x86 architecture support from the OpenJDK HotSpot JVM:
JEP 506: Scoped Values

Scoped Values let you safely and efficiently share immutable data with child threads without using thread-locals:

void main() {
   ScopedValue<String> scopedValue = ScopedValue.newInstance();

   // 🪄 ScopedValue usage
   ScopedValue.where(scopedValue, "Java 25!").run(() -> {
       // Output: `Hello, Java 25!`
       IO.println("Hello, " + scopedValue.get());
   });
}
JEP 510: Key Derivation Function API

Finalizes a unified, standard Java API for secure key derivation (e.g., HKDF, Argon2), enabling crypto providers and protocols to safely generate new keys from existing key material:

void main() throws Exception {
   byte[] ikm = "supersecret".getBytes(StandardCharsets.UTF_8);
   byte[] salt = "salty".getBytes(StandardCharsets.UTF_8);
   byte[] info = "context".getBytes(StandardCharsets.UTF_8);
   int keyLen = 32; // 256-bit key

   KDF kdf = KDF.getInstance("HKDF-SHA256");
   HKDFParameterSpec params = HKDFParameterSpec.ofExtract()
                           .addIKM(ikm)
                           .addSalt(salt)
                           .thenExpand(info, keyLen);

   // 🪄 Derive the key
   SecretKey derived = kdf.deriveKey("AES", params);

   // Output: `Derived AES key (hex): 5852801d2f50087728...bdcbe268680b71fdbdb`
   IO.println("Derived AES key (hex): " +
       HexFormat.of().formatHex(derived.getEncoded()));
}
JEP 511: Module Import Declarations

Lets you import entire modules directly in code, reducing repetitive import statements:

/**
// Instead of this:
// import com.example.mymodule.api.SomeClass;
// import com.example.mymodule.api.AnotherClass;
// import com.example.mymodule.util.Helper;

import module com.example.mymodule;
*/

// Instead of:
// import java.util.List;
// import java.util.Map;
// import java.nio.file.Path;
// import java.time.LocalDate;

// One single module import:
import module java.base;

void main() {
   // 🪄 No explicit imports needed for core types like List, Path, or Map
   List<String> list = List.of("a", "b");
   Map<String, Integer> map = Map.of("x", 1);
   Path path = Path.of("test.txt");
   LocalDate now = LocalDate.now();
}
JEP 512: Compact Source Files and Instance Main Methods

Simplifies small programs by allowing class-less source files and instance main methods:

void main() {
   // 🪄 List auto-imported
   var nums = List.of(1, 2, 3, 4);
   // IO available without import
   // Output: `Numbers: [1, 2, 3, 4]`
   IO.println("Numbers: " + nums);
}

You will notice that all code samples in this article are following the new JEP 512 coding pattern.

JEP 513: Flexible Constructor Bodies

Removes the restriction that super(..) or this(..) must be the first statement in a constructor:

void main() {
   // Output: `My name is: Java 25`
   new Employee("Java 25");

   // Throws exception: `Name must not be blank`
   new Employee("");
}

class Person {
   final String name;
   public Person(String name) {
       this.name = name;
       IO.println("My name is: " + this.name);
   }
}

class Employee extends Person {
   public Employee(String name) {
       if (name == null || name.isBlank()) {
           throw new IllegalArgumentException("Name must not be blank");
       }

       // 🪄 super() allowed after validation
       super(name.trim());
   }
}
JEP 514: Ahead-of-Time Command-Line Ergonomics

Introduces a new -XX:AOTCacheOutput=.. JVM option that merges the training run and AOT cache creation into a single, streamlined command:

# 🪄 One command instead of two:
java -XX:AOTCacheOutput=app.aot -cp app.jar com.example.App
# More concrete demo in JEP 515
JEP 515: Ahead-of-Time Method Profiling

Speeds up Java application warm-up by embedding method execution profiles from a training run into the AOT cache, enabling immediate JIT optimization on startup without waiting for runtime profiling:

// Demo code that takes some time to execute
void main() {
   long start = System.nanoTime();

   // Simulate a "hot method" that gets called often
   for (int i = 0; i < 1_000_000; i++) {
       fibonacci(20);
   }

   long end = System.nanoTime();
   IO.println("Execution time: " + (end - start) / 1_000_000 + " ms");
}

// A small CPU-heavy method to make profiling visible
static int fibonacci(int n) {
   if (n <= 1) return n;
   return fibonacci(n - 1) + fibonacci(n - 2);
}
# Running this will take some time to finish execution
java --source 25 Example.java
# Output `Execution time: 39401 ms`
# Incorporating the shorter command line from JEP 514
# And generate an AOT cache
java -XX:AOTCacheOutput=hotdemo.aot --source 25 Example.java
# Execution time will still take some time to finish, but this is AOT
# Output:
```
Execution time: 39463 ms
Temporary AOTConfiguration recorded: hotdemo.aot.config
Launching child process java to assemble AOT cache hotdemo.aot using configuration hotdemo.aot.config
Picked up JAVA_TOOL_OPTIONS: -Djdk.internal.javac.source=25 --add-modules=ALL-DEFAULT -XX:AOTCacheOutput=hotdemo.aot -XX:AOTConfiguration=hotdemo.aot.config -XX:AOTMode=create
Reading AOTConfiguration hotdemo.aot.config and writing AOTCache hotdemo.aot
AOTCache creation is complete: hotdemo.aot 25948160 bytes
Removed temporary AOT configuration file hotdemo.aot.config
```

# 🪄 Now we use the cache in subsequent runs
java -XX:AOTCache=hotdemo.aot --source 25 Example.java
# Output (varies based on platform) `Execution time: 26449 ms`
JEP 518: JFR Cooperative Sampling

Enhances Java Flight Recorder’s stability by delaying stack-walking to safe, well-defined “safepoints” via cooperative sampling—avoiding risky heuristics and improving reliability:

# Let’s reuse a simple code, like the one used in JEP 512, to observe this
# Start JFR recording with SafepointLatency events.
java -XX:StartFlightRecording=filename=cooperative.jfr,\
jdk.SafepointLatency#enabled=true --source 25 Example.java

# 🪄 Inspect the results
jfr print --events jdk.SafepointLatency cooperative.jfr

# Output:
```
dk.SafepointLatency {
  startTime = ...
  duration = 0.0315 ms
  threadState = "_thread_in_Java"
  eventThread = "main" (javaThreadId = 3)
  stackTrace = [
    jdk.internal.classfile.impl.DirectCodeBuilder.localAccess(int, int) line: 508
    jdk.internal.classfile.impl.DirectCodeBuilder.astore(int) line: 1029
    jdk.internal.classfile.impl.DirectCodeBuilder.storeLocal(TypeKind, int) line: 901
    ...
  ]
}

jdk.SafepointLatency {
  startTime = ...
  duration = 0.0459 ms
  threadState = "_thread_in_Java"
  eventThread = "main" (javaThreadId = 3)
  stackTrace = [
    java.util.ImmutableCollections$SetN.probe(Object) line: 1253
    java.util.ImmutableCollections$SetN.<init>(Object[]) line: 1162
    java.util.Set.of(Object[]) line: 706
    ...
  ]
}

...
```
JEP 519: Compact Object Headers

Promotes compact object headers—initially experimental—to a fully supported product feature, reducing object header size and optimizing memory efficiency and performance:

// Sample code that allocates a big number of Objects into the memory
void main() throws Exception {
   Runtime rt = Runtime.getRuntime();
   long before = rt.totalMemory() - rt.freeMemory();

   // Allocate many objects
   int count = 10_000_000;
   Object[] arr = new Object[count];
   for (int i = 0; i < count; i++) {
       arr[i] = new Object();
   }

   long after = rt.totalMemory() - rt.freeMemory();

   IO.println("Memory used: " + ((after - before) / (1024 * 1024)) + " MB");
}
# Running the code would output: `Memory used: 193 MB`
java --source 25 Example.java
# 🪄 Enabling compact header `-XX:+UseCompactObjectHeaders`
# Output: `Memory used: 116 MB`
# Saves ~77 MB in this simple example.
java -XX:+UseCompactObjectHeaders --source 25 Example.java
JEP 520: JFR Method Timing & Tracing

Enhances Java Flight Recorder by allowing precise method-level timing and tracing through bytecode instrumentation—without modifying source code. You can filter by method name, class, or annotation:

// Starting with a simple code with heavy method invocations
void main() {
   // Run some methods repeatedly so JFR has data to capture
   for (int i = 0; i < 5_000; i++) {
       doWork();
   }
}

void doWork() {
   double sum = 0;
   for (int i = 0; i < 1000; i++) {
       sum += Math.sqrt(i);
   }
}
# 🪄 Check the `doWork` method timing
java -XX:StartFlightRecording:filename=timing.jfr,\
jdk.MethodTiming#filter=Example::doWork --source 25 Example.java
jfr view method-timing timing.jfr

# Output:
```
[0.727s][info][jfr,startup] Started recording 1. No limit specified, using maxsize=250MB as default.
[0.727s][info][jfr,startup] 
[0.727s][info][jfr,startup] Use jcmd 6911 JFR.dump name=1 to copy recording data to file.

                                 Method Timing

Timed Method                 Invocations Minimum Time Average Time Maximum Time
---------------------------- ----------- ------------ ------------ ------------
Example.doWork()                   5,000  0.000008 ms  0.001540 ms  0.097200 ms
# Method tracing can also be enabled (produces lengthy output, discussed in JEP 518)
java -XX:StartFlightRecording:filename=demo.jfr,\
jdk.MethodTrace#filter=Example::doWork --source 25 Example.java
JEP 521: Generational Shenandoah

Introduces a significant enhancement to the Shenandoah garbage collector by promoting its generational mode from an experimental feature to a fully supported product feature:

// Simple code to observe the Shenandoah GC effect
static class BigObject {
   int[] data = new int[10_000]; // ~40 KB
}

void main() throws Exception {
   List<BigObject> list = new ArrayList<>();

   for (int i = 0; i < 50_000; i++) {
       list.add(new BigObject());
       if (i % 1000 == 0) {
           Thread.sleep(50); // slow down allocation to observe GC
       }
   }

   Thread.sleep(10_000); // Keep app alive to observe GC logs
}
# 🪄 Run With Generational Shenandoah
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational \
-Xlog:gc* --source 25 Example.java
# Output:
```
[0.123s][info][gc,start     ] GC(0) Pause Young (G1 Evacuation)
[0.124s][info][gc,task      ] GC(0) Using 4 workers
[0.127s][info][gc,phases    ] GC(0)   PreEvacuate Young
[0.128s][info][gc,phases    ] GC(0)   Evacuate Young
[0.129s][info][gc,phases    ] GC(0)   PostEvacuate Young
[0.130s][info][gc,heap      ] GC(0) Heap before GC: 200 MB
[0.131s][info][gc,heap      ] GC(0) Heap after GC:  120 MB
[0.132s][info][gc,metaspace ] GC(0) Metaspace used: 10 MB
[0.133s][info][gc,stats     ] GC(0) Pause Young: 3.5 ms
...
```
# Young collections are very fast (a few ms), cleaning up short-lived objects.
# Mixed/Old collections are slightly longer but still low-latency.
# Heap usage decreases gradually,
# as generational GC separates short-lived objects from long-lived ones.
# Pause times are consistently low because Shenandoah does concurrent evacuation.

Preview Features

To run any of the provided sample code in this section, you must enable preview, e.g.

java --enable-preview --source 25 Example.java
JEP 470: PEM Encodings of Cryptographic Objects

Provides a built-in API to encode/decode cryptographic keys and certificates in PEM format, removing the need for manual parsing or external libraries:

void main() {
   String pemText = """
       -----BEGIN PUBLIC KEY-----
       MIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKCAYEAoO4f2xOp7o0dSnjf3IcF
       A/bVo1gz84YjQWflO+bXzPf2mWQWka3wYJGgP6OZGCIPffUkEG+0Ztf/2g4cWRE1
       Uop+eU3eyBTbtUkywWxNY/vLHjJ2voEGhn5kuI5oZCAOuaESJd0s6TkNErfg04XZ
       G2J+e6EtB0arpgPXIf5iSTjW1ccDEG+5/7YJShKPZY0+MDyxSxjApzdnICktDg8o
       4hYKYstb3o+2SPovoFhfBlMIFXL9mfUYDOQjFHG8fCnDxMfFUOBXqRwz/eYpai9w
       mhjGoeQX9FrMliKkabgTt8hEDiKaddkSVs9sVcx7mTvSeFyzzziaKgKVz60smE4S
       MTDC5aOuqoPkyWEc5CPgoFR/SZkLYmWcwLXv7RMZCwckJmq+P281H7C8aKx6AX0R
       ytJFQAfRGavdBTn5lOUTCVrIM7Rb81tyJA9+gQpWF6GI3cqjMa5XBHbit/y0MffO
       GvWlXx6Gi/vBJFegbDK/tsTxnPcXHVi/OTGpe2hauI7jAgMBAAE=
       -----END PUBLIC KEY-----
   """;

   // 🪄 Decode the key
   PublicKey pk = PEMDecoder.of().decode(pemText, PublicKey.class);

   // Output: `Algorithm: RSA`
   IO.println("Algorithm: " + pk.getAlgorithm());
}
JEP 502: Stable Values

Introduces single-assignment, immutable containers for values that are initialized once and safely shared across threads:

void main() {
   StableValue<String> value = StableValue.of();

   // 🪄 Output means that exception has been thrown.
   // Output: `Hello, Java 25!`
   try {
       value.setOrThrow("Hello, Java 25!");
       value.setOrThrow("❌ Can only be set once.");
   } catch (IllegalStateException e) {
       IO.println(value);
   }
}
JEP 505: Structured Concurrency

Adds an API to manage multiple tasks as a single unit of work, simplifying error handling and cancellation in concurrent programming:

void main() throws Exception {
   var scope = StructuredTaskScope.open();
   var f2 = scope.fork(() -> "Java 25");
   var f1 = scope.fork(() -> "Hello");

   // 🪄 Waits for all subtasks to complete or fail
   scope.join();

   // Output: `Hello, Java 25!`
   IO.println(f1.get() + ", " + f2.get() + "!");
}
JEP 507: Primitive Types in Patterns, instanceof, and switch

Expands pattern matching so that instanceof and switch can match primitive types—like int, long, or double—making pattern logic more expressive and uniform across all data types:

void main() {
   Object o = 42;

   // 🪄 instance of primative
   // Output: `It's an int: 42`
   if (o instanceof int i) {
       IO.println("It's an int: " + i);
   }

   o = 42d;

   // 🪄 switch on primative
   // Output: `Switched on double: 42.0`
   switch (o) {
       case int i    -> IO.println("Switched on int: " + i);
       case double i -> IO.println("Switched on double: " + i);
       default       -> IO.println(
           "Switched on " + o.getClass().getName() + ": " + o);
   }
}

Experimental Features

JEP 509: JFR CPU-Time Profiling

Introduces Linux-only, CPU-time-based profiling to JDK Flight Recorder, capturing precise per-thread CPU usage—including time spent in native code—more accurately than traditional wall-clock sampling:

// To test this out, first create a simple, CPU-bound calculation 
// to generate a profileable workload, in a file let say CPUProfiling.java
void main() {
   IO.println("Starting CPU-intensive task...");
   for (long i = 0; i < 2_000_000_000L; i++) {
       Math.sqrt(Math.log(i + 1));
   }
   IO.println("Task complete.");
}
# Compile the code
javac --enable-preview --release 25 CPUProfiling.java
# 🪄 Run the program with CPU profiler (on Linux)
java -XX:StartFlightRecording=jdk.CPUTimeSample#enabled=true,\
jdk.CPUTimeSample#throttle=500/s,filename=cpu.jfr CPUProfiling

# Examine the content of cpu.jfr (or visualize with JDK Mission Control)
jfr view cpu-time-hot-methods cpu.jfr

# Sample output:
```
Method                                                          Samples Percent
--------------------------------------------------------------- ------- -------
CPUProfiling.main(String[])                                       2,711  99.93%
java.util.Properties$LineReader.readLine()                            1   0.04%
jdk.internal.classfile.impl.Util.buildingCode(Consumer)               1   0.04%
```

Incubator Features

To run the code in this section, you must enable preview and add related incubator module, e.g.

java --enable-preview --source 25 --add-modules jdk.incubator.vector Example.java
JEP 508: Vector API

Adds a new set of APIs to express vector computations that compile at runtime into optimal hardware instructions on supported CPU architectures:

import jdk.incubator.vector.*;

void main() {
   float[] a = {1f, 2f, 3f, 4f};
   float[] b = {5f, 6f, 7f, 8f};

   // A species defines the vector shape (bit-width & element type).
   // SPECIES_128 means 128-bit wide vectors, i.e. can hold 4 floats.
   // On supported CPUs, this maps to SIMD registers
   // e.g. XMM registers on x86, NEON on ARM, ..etc.
   var species = FloatVector.SPECIES_128;

   // Wraps the arrays into vector objects
   var va = FloatVector.fromArray(species, a, 0);
   var vb = FloatVector.fromArray(species, b, 0);

   // 🪄 Vector addition in parallel
   // [1,2,3,4] + [5,6,7,8] → [6,8,10,12]
   // On supported hardware, it's a single CPU instruction rather than a loop.
   var vc = va.add(vb);

   // Moves the SIMD register contents back into a normal Java array.
   float[] result = new float[species.length()];
   vc.intoArray(result, 0);

   // Output: `Vector sum: [6.0, 8.0, 10.0, 12.0]`
   IO.println("Vector sum: " + java.util.Arrays.toString(result));
}

This article is part of the magazine issue Java 25 – Part 1.
You can read the complete issue with all contributions here.

Total
0
Shares
Previous Post

Engineering Intelligence Live: JAVAPRO Benefits for JCON 2026

Next Post

Jakarta Data and NoSQL – Standardized Data Access for Jakarta EE

Related Posts

Java Records — Etched in Finality

record: to set down in writing or the like, as for the purpose of preserving evidence. [dictionary.com] The…
Read More