Java 25 Features
Java 25 is the latest long-term-support release. It makes Java simpler to start with compact source files and module imports, more powerful with scoped values and flexible constructors, and cheaper to run with compact object headers and AOT caching. Learn each feature with clear explanations and runnable examples.
- Compact Sourcevoid main() without a class
- Scoped ValuesA better ThreadLocal
- Flexible ConstructorsCode before super()
- Faster RuntimeCompact headers, AOT cache
All Java 25 Features at a Glance
The new features of Java 25, plus the highlights of Java 22 to 24 that you gain when upgrading from Java 21.
Compact Source Files and Instance main
Write a program without a class or static main.
Module Import Declarations
Import a whole module with one line.
Flexible Constructor Bodies
Run code before super(...) or this(...).
Scoped Values
Share immutable data with callees and threads safely.
Unnamed Variables and Patterns
Use _ for values you don't need.
Stream Gatherers
Custom intermediate operations for streams.
Foreign Function and Memory API
Call native code and use off-heap memory without JNI.
Preview Features in Java 25
Primitive patterns, stable values and structured concurrency.
JVM and Platform Changes
Smaller objects, faster startup and better profiling.
Compact Source Files and Instance main
Java 25 (JEP 512) finalised compact source files. A file can contain just a void main() method, with no class declaration, no static and no String[] args. Every package of java.base is imported automatically, and the new IO class gives simple console input and output. It is perfect for beginners and small scripts.
void main()is a valid entry point;staticandargsare optionaljava.base(List, Map, Path, LocalDate...) is imported automaticallyIO.println,IO.printandIO.readlnfor console I/O- Grows naturally into a normal class when the program gets bigger
// A compact source file: no class, no static, no String[] args.
// All of java.base is imported automatically, and IO prints text.
void main() {
String name = "Asha";
IO.println("Hello, " + name + "!");
List<String> topics = List.of("Records", "Patterns", "Virtual threads");
for (String topic : topics) {
IO.println("- " + topic);
}
IO.println("Topics: " + topics.size());
}
$ java HelloCompact.java Hello, Asha! - Records - Patterns - Virtual threads Topics: 3
Module Import Declarations
With import module java.base; (JEP 511) you import every package a module exports, without listing them one by one. Your own code does not need to be a module. If a simple name exists in two imported packages, add a normal single-type import to choose one.
import module M;imports all packages exported byM- Also includes modules that
Mrequires transitively - Single-type imports win over module imports, which resolves clashes
- Compact source files get
import module java.baseautomatically
// One line imports every package that a module exports
import module java.base; // java.util, java.time, java.nio.file, ...
import module java.sql; // java.sql, javax.sql (and java.xml, java.logging)
// Some simple names exist in more than one package, so name the one
// you want: java.util.Date vs java.sql.Date, and
// java.sql.Timestamp vs java.security.Timestamp
import java.util.Date;
import java.sql.Timestamp;
public class ModuleImportDemo {
public static void main(String[] args) {
List<String> langs = new ArrayList<>(List.of("kotlin", "java"));
Collections.sort(langs);
System.out.println("java.util : " + langs);
LocalDate release = LocalDate.of(2025, Month.SEPTEMBER, 16);
System.out.println("java.time : " + release.getDayOfWeek());
Path path = Path.of("reports", "2025.csv");
System.out.println("java.nio.file : " + path.getFileName());
Timestamp ts = Timestamp.valueOf("2025-09-16 10:00:00");
System.out.println("java.sql : " + ts);
Date date = new Date(0);
System.out.println("Date type : " + date.getClass().getName());
}
}
java.util : [java, kotlin] java.time : TUESDAY java.nio.file : 2025.csv java.sql : 2025-09-16 10:00:00.0 Date type : java.util.Date
Flexible Constructor Bodies
Before Java 25, super(...) had to be the first statement in a constructor. Now (JEP 513) you can validate arguments, compute values and even assign fields before calling it. This fails fast on bad input and fixes the classic bug where an overridden method, called from the parent constructor, saw uninitialised fields.
- Validate arguments before the parent constructor runs
- Assign the subclass's own fields before
super(...) - You still cannot use
thisto read fields or call methods beforesuper(...) - Works for records' canonical and non-canonical constructors too
public class FlexibleConstructorDemo {
static class Employee {
final String name;
Employee(String name) {
this.name = name;
describe(); // calls the subclass version
}
void describe() {
System.out.println("Employee " + name);
}
}
static class Manager extends Employee {
final int reports;
Manager(String name, int reports) {
// Java 25: code before super(...) to validate and prepare
if (reports < 0) {
throw new IllegalArgumentException("reports < 0");
}
String cleanName = name.strip();
// Fields can be set before super(), so describe() sees them
this.reports = reports;
super(cleanName);
}
@Override
void describe() {
// Before Java 25, reports would still be 0 here
System.out.println("Manager " + name + " with "
+ reports + " reports");
}
}
public static void main(String[] args) {
new Employee("Ravi");
new Manager(" Asha ", 5);
try {
new Manager("Meera", -1);
} catch (IllegalArgumentException e) {
// Rejected before the Employee constructor ever ran
System.out.println("Rejected: " + e.getMessage());
}
}
}
Employee Ravi Manager Asha with 5 reports Rejected: reports < 0
Scoped Values
Scoped values (JEP 506, final in Java 25) are a safer, faster alternative to ThreadLocal. A value is bound for the duration of a call and is visible to every method called inside it, without passing it as a parameter. The binding is immutable and ends automatically, which suits virtual threads and request-scoped data such as the current user.
ScopedValue.where(KEY, value).run(...)or.call(...)- Read with
KEY.get()anywhere inside the scope - Immutable and automatically unbound, so nothing leaks between requests
- Cheap with millions of virtual threads, unlike
ThreadLocal
public class ScopedValueDemo {
// An immutable value shared with code called inside a scope
static final ScopedValue<String> USER = ScopedValue.newInstance();
public static void main(String[] args) throws Exception {
// Bind a value only while run() executes
ScopedValue.where(USER, "asha").run(() -> handleRequest());
// Each request gets its own binding, even in parallel threads
Thread t1 = Thread.ofVirtual().start(() ->
ScopedValue.where(USER, "ravi").run(() -> handleRequest()));
t1.join();
// Rebinding for a nested call; the outer value comes back after
ScopedValue.where(USER, "admin").run(() -> {
System.out.println("outer : " + USER.get());
ScopedValue.where(USER, "guest")
.run(() -> System.out.println("nested: " + USER.get()));
System.out.println("outer : " + USER.get());
});
// call() returns a result from the scope
int length = ScopedValue.where(USER, "meera")
.call(() -> USER.get().length());
System.out.println("length of user name: " + length);
// Outside any scope the value is not bound
System.out.println("bound now? " + USER.isBound());
}
static void handleRequest() {
// No parameter needed: read the value bound by the caller
System.out.println("Handling request for " + USER.get());
}
}
Handling request for asha Handling request for ravi outer : admin nested: guest outer : admin length of user name: 5 bound now? false
Unnamed Variables and Patterns
Finalised in Java 22 (JEP 456), the underscore _ marks a variable or pattern component that is intentionally unused. It makes code clearer and stops warnings about unused variables in loops, catch blocks, lambdas and record patterns.
- Record patterns:
Order(_, _, int qty) - Loops and catch blocks:
for (Order _ : orders),catch (Exception _) - Lambdas:
(_, value) -> ... - Several
_can appear in the same scope
import java.util.List;
import java.util.Map;
public class UnnamedDemo {
record Order(String id, String item, int qty) { }
public static void main(String[] args) {
List<Order> orders = List.of(
new Order("A1", "Book", 2), new Order("A2", "Pen", 5));
// _ in a record pattern: ignore components you don't need
int total = 0;
for (Order order : orders) {
if (order instanceof Order(_, _, int qty)) {
total += qty;
}
}
System.out.println("Total quantity: " + total);
// _ for a variable that is never used
int count = 0;
for (Order _ : orders) {
count++;
}
System.out.println("Orders: " + count);
// _ for an unused exception and lambda parameter
String input = "12x";
try {
Integer.parseInt(input);
} catch (NumberFormatException _) {
System.out.println("Not a number: " + input);
}
Map<String, Integer> stock = Map.of("Book", 3);
stock.forEach((_, qty) -> System.out.println("In stock: " + qty));
}
}
Total quantity: 7 Orders: 2 Not a number: 12x In stock: 3
Stream Gatherers
Java 24 (JEP 485) added Stream.gather(...), which does for intermediate operations what collect does for terminal ones. The Gatherers class provides ready-made operations for fixed and sliding windows, running totals and concurrent mapping, and you can write your own.
windowFixed(n)andwindowSliding(n)for batches and moving averagesscanfor running results,foldfor a single resultmapConcurrent(n, fn)maps on virtual threads, keeping order- Implement
Gathererto build your own operations
import java.util.List;
import java.util.stream.Gatherers;
import java.util.stream.Stream;
public class GathererDemo {
public static void main(String[] args) {
List<Integer> prices = List.of(100, 102, 98, 105, 110, 107);
// windowFixed: split into groups of n
System.out.println("windowFixed(2) : " + prices.stream()
.gather(Gatherers.windowFixed(2)).toList());
// windowSliding: overlapping windows, e.g. moving averages
List<String> movingAvg = prices.stream()
.gather(Gatherers.windowSliding(3))
.map(w -> w.stream().mapToInt(i -> i).average().orElse(0))
.map(avg -> "%.1f".formatted(avg))
.toList();
System.out.println("moving average : " + movingAvg);
// scan: running results, like a running total
System.out.println("running total : " + Stream.of(1, 2, 3, 4)
.gather(Gatherers.scan(() -> 0, Integer::sum)).toList());
// fold: combine everything into one result
System.out.println("fold (concat) : " + Stream.of("a", "b", "c")
.gather(Gatherers.fold(() -> "", String::concat))
.findFirst().orElse(""));
// mapConcurrent: run a mapping on up to n virtual threads
System.out.println("mapConcurrent(4): " + Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.mapConcurrent(4, n -> n * n)).toList());
}
}
windowFixed(2) : [[100, 102], [98, 105], [110, 107]] moving average : [100.0, 101.7, 104.3, 107.3] running total : [1, 3, 6, 10] fold (concat) : abc mapConcurrent(4): [1, 4, 9, 16, 25]
Foreign Function and Memory API
The FFM API (JEP 454, final in Java 22) replaces JNI for most uses. You can look up a C function, call it through a MethodHandle, and allocate native memory that an Arena frees safely. No C glue code or header files are needed.
Linkerfinds and calls native functionsArenacontrols the lifetime of native memoryMemorySegmentreads and writes memory safely- Run with
--enable-native-access=ALL-UNNAMEDto avoid warnings
import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.MethodHandle;
public class FfmDemo {
public static void main(String[] args) throws Throwable {
// Find the C function strlen in the standard C library
Linker linker = Linker.nativeLinker();
MethodHandle strlen = linker.downcallHandle(
linker.defaultLookup().find("strlen").orElseThrow(),
FunctionDescriptor.of(ValueLayout.JAVA_LONG,
ValueLayout.ADDRESS));
// An arena frees its native memory when it is closed
try (Arena arena = Arena.ofConfined()) {
MemorySegment text = arena.allocateFrom("Hello from Java 25");
long length = (long) strlen.invokeExact(text);
System.out.println("strlen() returned " + length);
// Off-heap memory without Unsafe or JNI
MemorySegment numbers = arena.allocate(ValueLayout.JAVA_INT, 5);
for (int i = 0; i < 5; i++) {
numbers.setAtIndex(ValueLayout.JAVA_INT, i, i * i);
}
System.out.print("off-heap ints :");
for (int i = 0; i < 5; i++) {
System.out.print(" "
+ numbers.getAtIndex(ValueLayout.JAVA_INT, i));
}
System.out.println();
}
}
}
strlen() returned 18 off-heap ints : 0 1 4 9 16
Preview Features in Java 25
Java 25 also includes preview features that you can try with --enable-preview. Primitive types in patterns let switch and instanceof work with int, double and other primitives, and stable values are set once and then treated like constants by the JVM.
- Primitive patterns:
case int i when i < 0,x instanceof byte b - Stable values: lazy initialisation that is safe and fast
- Structured concurrency: treat related tasks as one unit (fifth preview)
- Vector API for SIMD computations (incubator)
// Preview features in Java 25.
// Run with: java --enable-preview PreviewDemo.java
// Primitive types in patterns (JEP 507)
static String size(int value) {
return switch (value) {
case 0 -> "zero";
case int i when i < 0 -> "negative";
case int i when i < 100 -> "small";
default -> "large";
};
}
// Stable values (JEP 502): set once, then treated as a constant
static final StableValue<String> CONFIG = StableValue.of();
static String config() {
return CONFIG.orElseSet(() -> {
IO.println("(loading config once)");
return "prod";
});
}
void main() {
IO.println(size(0) + ", " + size(-4) + ", " + size(42) + ", " + size(500));
// instanceof with a primitive: does the value fit in a byte?
int big = 300;
int small = 42;
IO.println("300 fits in byte: " + (big instanceof byte));
if (small instanceof byte b) {
IO.println("42 as byte: " + b);
}
IO.println("config: " + config());
IO.println("config: " + config());
}
$ java --enable-preview PreviewDemo.java zero, negative, small, large 300 fits in byte: false 42 as byte: 42 (loading config once) config: prod config: prod
JVM and Platform Changes
Java 25 makes applications cheaper to run. Compact object headers shrink every object, the AOT cache speeds up startup and warm-up, Shenandoah gained a generational mode, and Flight Recorder can profile CPU time and time individual methods.
- Compact object headers: 8-byte headers on 64-bit platforms
- AOT cache: record a training run once, then start faster
- Generational Shenandoah and new JFR profiling events
- Key Derivation Function API; 32-bit x86 port removed
# Compact object headers (JEP 519): smaller objects, less heap
java -XX:+UseCompactObjectHeaders -jar app.jar
# AOT cache in one step (JEP 514/515): faster startup and warm-up
java -XX:AOTCacheOutput=app.aot -jar app.jar # training run
java -XX:AOTCache=app.aot -jar app.jar # production run
# Generational Shenandoah is now a product feature (JEP 521)
java -XX:+UseShenandoahGC -XX:ShenandoahGCMode=generational App
# JFR: CPU-time profiling (Linux, experimental) and method timing
java -XX:StartFlightRecording:jdk.CPUTimeSample#enabled=true App
# Security: Key Derivation Function API (JEP 510), and since
# Java 24 quantum-resistant ML-KEM and ML-DSA algorithms
# Removed: 32-bit x86 port; Security Manager is permanently disabled
Beyond Java 25
Java keeps a six-month release cycle, and several long-running OpenJDK projects are shaping its future:
| Release / project | What it brings |
|---|---|
| Java 26 (March 2026) | HTTP/3 in the HTTP Client, AOT object caching with any GC, Applet API removed |
| Java 27 (September 2026) | Non-LTS release continuing the six-month cadence; see the OpenJDK JDK 27 project page for its JEPs |
| Project Valhalla | Value classes and objects that behave like primitives (in development) |
| Project Leyden | Faster startup and warm-up through ahead-of-time optimisation |
| Project Babylon | Code reflection for GPUs and machine learning (in development) |