Java 25 has just been released, and it brings a set of powerful new features that continue to modernize the platform while simplifying developer experience.
Let’s take a quick look at some of the Java 25 new features and their corresponding JDK Enhancement Proposals.
1.Primitive Types in Patterns (JEP 507)
Pattern matching has been evolving since Java 14, but until now it only supported reference types.
In Java 25, primitives (int, long, double, etc.) can also be used in switch and instanceof.
public class Java25NewFeaturesTest1 {
public static void main(String[] args) {
Object obj = 42;
switch (obj) {
case Integer i -> System.out.println("It's an int: " + i);
case Long l -> System.out.println("It's a long: " + l);
case Double d -> System.out.println("It's a double: " + d);
default -> System.out.println("Unknown type");
}
}
}
/*
* Output: It's an int: 42
*/
2.Module Import Declarations (JEP 511)
You can now import all exported packages of a module in one go:
import module java.base;
public class Java25NewFeaturesTest2 {
public static void main(String[] args) {
Date d = new Date();
System.out.println("Resolved Date: " + d);
}
}
/*
* Output:
* Resolved Date: Mon Sep 22 09:11:14 CEST 2025
*/
Pay attention that module import declarations is a preview feature, so to use it, you need to add the –enable-preview flag when compiling and running your code.
// Compile with:
javac --enable-preview -source 25 Java25NewFeaturesTest2.java
// Run with:
java --enable-preview Java25NewFeaturesTest2
3.Compact Source Files & Instance Main (JEP 512)
Java now supports compact programs with less boilerplate.
The main method can even be non-static.
public class Java25NewFeaturesTest3 {
void main() {
System.out.println("Hello, Java 25!");
}
}
/*
* Output: Hello, Java 25!
*/
4.Flexible Constructor Bodies (JEP 513)
In Java 25, you can now perform operations before calling super() or this() inside a constructor.
public class Java25NewFeaturesTest4 {
private static class Person {
String name;
int age;
Person() {
this.name = "Unknown";
this.age = 0;
super();
System.out.println("new born person " + toString());
}
Person(String name, int age) {
if (age < 0 || age > 150) {
throw new IllegalArgumentException("Age must be between 0 and 150");
}
this();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
void main() {
Person p1 = new Person();
System.out.println(p1);
Person p2 = new Person("Alice", 30);
System.out.println(p2);
}
}
/*
* Output: new born person Person{name='Unknown', age=0}
* Person{name='Unknown', age=0}
* new born person Person{name='Unknown', age=0}
* Person{name='Alice', age=30}
*/
5.Scoped Values (JEP 506)
Scoped values are like ThreadLocal, but safer and faster.
They allow sharing immutable data across threads in a well-defined scope.
Below is a simple example of ThreadLocal :
public class ThreadLocalExample {
// Each thread gets its own Integer value
private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
Runnable task = () -> {
threadLocalValue.set((int) (Math.random() * 100));
System.out.println(Thread.currentThread().getName() + ": " + threadLocalValue.get());
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
/*
* Output: Thread-1: 42
* Thread-2: 87
*/
Below is an example of using Scoped Values instead of ThreadLocal :
public class ScopedValueExample {
static final ScopedValue<Integer> VALUE = ScopedValue.newInstance();
void main() {
Runnable task = () -> {
int randomValue = (int) (Math.random() * 100);
ScopedValue.where(VALUE, randomValue).run(() -> {
System.out.println(Thread.currentThread().getName() + ": " + VALUE.get());
});
};
Thread t1 = new Thread(task, "Thread-1");
Thread t2 = new Thread(task, "Thread-2");
t1.start();
t2.start();
}
}
/*
* Output: Thread-1: 42
* Thread-2: 87
*/
ScopedValue.newInstance() creates a new scoped value holder for an Integer.
ScopedValue.where(VALUE, randomValue) sets VALUE to a random value for the duration of the code block inside run.
Inside the run block, you can call VALUE.get() to retrieve the random value.
After the block finishes, the value is no longer available (it’s not global or thread-local).
ScopedValue is safer than ThreadLocal because they are only visible in the code block where they are set, avoiding leaks and accidental sharing.
6.Stable Values (JEP 502)
StableValue is for sharing immutable data safely and efficiently.
Once created, the value cannot be changed.
public class Java25NewFeaturesTest5 {
// Create a StableValue holding an immutable String
static final StableValue<String> MESSAGE = StableValue.of("Hello, StableValue!");
void main() {
System.out.println(MESSAGE); // Output: Hello, StableValue!
}
}
It provides objects that behave like constants but can be lazily initialized.
That means the value is not created until it’s actually needed.
import java.util.function.Supplier;
public class Java25NewFeaturesTest5 {
// StableValue declared but not initialized yet
static Supplier<String> greeting = StableValue.supplier(() -> {
System.out.println("Initializing greeting...");
return "Hello, stable world!";
}
);
void main() {
System.out.println("Program started");
// Nothing initialized yet
System.out.println("Has it initialized? Not until get() is called!");
// First call to get() → triggers initialization
System.out.println("Greeting: " + greeting.get());
// Second call to get() → reuses the cached value, no re-initialization
System.out.println("Greeting again: " + greeting.get());
}
}
/* Output:
Program started
Has it initialized? Not until get() is called!
Initializing greeting...
Greeting: Hello, stable world!
Greeting again: Hello, stable world!
*/
7.Compact Object Headers (JEP 519)
Java 25 reduces memory overhead of object headers on 64-bit platforms.
This benefits applications with millions of small objects.
public class RuntimeMemoryTest {
static class SmallObject {
int x;
int y;
}
public static void main(String[] args) {
Runtime runtime = Runtime.getRuntime();
// Warm up and GC
System.gc();
long before = runtime.totalMemory() - runtime.freeMemory();
int count = 10_000_000;
SmallObject[] objects = new SmallObject[count];
for (int i = 0; i < count; i++) {
objects[i] = new SmallObject();
}
long after = runtime.totalMemory() - runtime.freeMemory();
System.out.println("Approximate memory used: " + (after - before) / (1024 * 1024) + " MB");
}
}
/* Output:
run with Java 21 => Approximate memory used: 285 MB
run with Java 25 => Approximate memory used: 280 MB
*/
8.Vector API (JEP 508)
Vector API allows you to express vectorized computations (SIMD instructions) in Java, so the JVM can map them to CPU instructions (AVX, NEON, etc.), achieving much higher performance compared to scalar loops.
Example of scalar loop (normal loop that process one element at a time) :
public class ScalarLoopExample {
public static void main(String[] args) {
float[] a = { 1f, 2f, 3f, 4f };
float[] b = { 5f, 6f, 7f, 8f };
float[] c = new float[a.length];
for (int i = 0; i < a.length; i++) {
c[i] = a[i] + b[i]; // one addition per loop iteration
}
System.out.println(java.util.Arrays.toString(c));
}
}
/* Output:
[6.0, 8.0, 10.0, 12.0]
*/
Example of vector api :
import jdk.incubator.vector.FloatVector;
import jdk.incubator.vector.VectorSpecies;
public class VectorExample {
public static void main(String[] args) {
float[] a = { 1f, 2f, 3f, 4f };
float[] b = { 5f, 6f, 7f, 8f };
FloatVector aa = FloatVector.fromArray(FloatVector.SPECIES_128, a, 0);
FloatVector bb = FloatVector.fromArray(FloatVector.SPECIES_128, b, 0);
FloatVector cc = aa.add(bb);
float[] c = new float[FloatVector.SPECIES_128.length()];
cc.intoArray(c, 0);
System.out.println(java.util.Arrays.toString(c));
}
}
/*
* Output:
* [6.0, 8.0, 10.0, 12.0]
*/
Pay attention that you need to add the –enable-preview flag when compiling and running your code.
// Compile with :
javac --add-modules jdk.incubator.vector VectorExample.java
// Run with :
java --add-modules jdk.incubator.vector VectorExample
Conclusion
Java 25 continues the modernization of the Java language and platform.
From pattern matching for primitives, to scoped values, compact object headers, and crypto improvements, this release is a strong step forward for developers and operators alike.
If you’re building modern Java applications, upgrading to Java 25 will give you:
- Cleaner syntax (patterns, compact sources)
- Better performance (Shenandoah GC, compact headers)
- Stronger security (KDF API, PEM support)
- Improved developer ergonomics
Time to download JDK 25 and give these features a spin!