Java Evolution — Part 11

Java 24 (March 18, 2025) and Java 25 (September 16, 2025) complete the bridge between the Java 21 and Java 25 LTS releases. Java 25 is the next long-term support version, delivering a cohesive set of language improvements that make Java more expressive, more performant, and more accessible.


Stream Gatherers — Finalised (Java 24, JEP 485)

Stream Gatherers are a new intermediate stream operation that allows you to define custom, stateful stream transformations that are not possible with the existing filter, map, and flatMap operations.

The Stream.gather(Gatherer) method is the entry point. The Gatherer interface has four components:

  • Initialiser — creates the mutable state
  • Integrator — processes each element and optionally emits elements downstream
  • Combiner — merges state for parallel streams
  • Finisher — emits any remaining elements after the stream ends

Built-in Gatherers

Java 24 ships java.util.stream.Gatherers with several useful built-in gatherers:

import java.util.stream.Gatherers;
// windowFixed — emit fixed-size non-overlapping windows
Stream.of(1, 2, 3, 4, 5, 6, 7)
.gather(Gatherers.windowFixed(3))
.forEach(System.out::println);
// [1, 2, 3]
// [4, 5, 6]
// [7]
// windowSliding — emit sliding windows
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.windowSliding(3))
.forEach(System.out::println);
// [1, 2, 3]
// [2, 3, 4]
// [3, 4, 5]
// scan — running accumulation (like reduce but emits each intermediate result)
Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.scan(() -> 0, Integer::sum))
.forEach(System.out::println);
// 1, 3, 6, 10, 15
// fold — like reduce but always produces a result
Optional<Integer> sum = Stream.of(1, 2, 3, 4, 5)
.gather(Gatherers.fold(() -> 0, Integer::sum))
.findFirst();
// Optional[15]
// mapConcurrent — map with bounded concurrency using virtual threads
Stream.of("url1", "url2", "url3", "url4", "url5")
.gather(Gatherers.mapConcurrent(3, url -> fetchContent(url)))
.forEach(System.out::println);

Custom Gatherer Example

// A custom gatherer that emits elements in pairs with their running total
Gatherer<Integer, ?, String> runningTotalPairs = Gatherer.ofSequential(
() -> new int[]{0}, // state: running total
(state, element, downstream) -> {
state[0] += element;
return downstream.push(element + " (total: " + state[0] + ")");
}
);
Stream.of(10, 20, 30, 40)
.gather(runningTotalPairs)
.forEach(System.out::println);
// 10 (total: 10)
// 20 (total: 30)
// 30 (total: 60)
// 40 (total: 100)

Ahead-of-Time Class Loading and Linking (Java 24, JEP 483)

Java 24 introduced Ahead-of-Time (AOT) Class Loading and Linking, which allows the JVM to cache the results of class loading and linking from a previous run. On subsequent runs, the JVM loads classes from this cache instead of re-reading and re-processing class files, significantly reducing startup time.

Terminal window
# Step 1: Create the AOT cache during a training run
java -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -jar myapp.jar
# Step 2: Use the cache on subsequent runs
java -XX:AOTMode=on -XX:AOTConfiguration=app.aotconf -jar myapp.jar

This is particularly valuable for microservices and serverless functions where startup time directly affects cost and user experience.


Scoped Values — Finalised (Java 25, JEP 506)

Scoped Values, incubated in Java 20 and previewed since Java 21, are finalised in Java 25. They provide an elegant alternative to ThreadLocal for sharing data within a bounded scope — particularly well-suited to virtual threads.

The Problem with ThreadLocal

ThreadLocal has several drawbacks with virtual threads:

  • Values are inherited by child threads, requiring copying
  • Values persist for the thread’s lifetime unless explicitly removed
  • They are mutable, which can cause unexpected side effects

Scoped Values

// Define a scoped value (typically as a static final field)
public class RequestContext {
public static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();
public static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
}
// Bind values for a scope
void handleRequest(HttpRequest request) {
User user = authenticate(request);
String requestId = generateId();
ScopedValue.where(RequestContext.CURRENT_USER, user)
.where(RequestContext.REQUEST_ID, requestId)
.run(() -> processRequest(request));
}
// Access from anywhere in the call stack within the scope
void processRequest(HttpRequest request) {
User user = RequestContext.CURRENT_USER.get();
String id = RequestContext.REQUEST_ID.get();
log.info("[{}] Processing request for {}", id, user.name());
// ... delegate to service layer, repository, etc.
}

Scoped Values vs ThreadLocal

AspectThreadLocalScopedValue
MutabilityMutableImmutable within scope
ScopeThread lifetimeExplicitly bounded
InheritanceCopies to child threadsInherited without copying
Virtual thread supportPoor (copies on inherit)Excellent
Memory overheadHighLow

Compact Source Files and Instance Main Methods — Finalised (Java 25, JEP 512)

Java 25 finalises the simplification of Java programs for beginners and scripting use cases. A complete Java program can now be written without a class declaration, static, or String[] args:

// A complete, runnable Java 25 program
void main() {
System.out.println("Hello, World!");
}

For slightly more complex programs, you can add imports and helper methods:

import module java.base;
void main() {
var names = List.of("Alice", "Bob", "Charlie");
names.stream()
.filter(n -> n.length() > 3)
.map(String::toUpperCase)
.forEach(System.out::println);
}

This dramatically lowers the barrier to entry for Java beginners and makes Java viable for scripting tasks.


Flexible Constructor Bodies — Finalised (Java 25, JEP 513)

Before Java 25, a constructor’s first statement had to be a call to super() or this(). This prevented you from computing or validating arguments before passing them to the superclass constructor.

// Before Java 25 — had to use a static helper method
class PositivePoint extends Point {
PositivePoint(int x, int y) {
super(validate(x), validate(y)); // only option
}
private static int validate(int v) {
if (v < 0) throw new IllegalArgumentException("Must be positive");
return v;
}
}
// Java 25 — code before super() is allowed
class PositivePoint extends Point {
PositivePoint(int x, int y) {
// Validation before super() — now legal
if (x < 0 || y < 0) {
throw new IllegalArgumentException(
"Coordinates must be non-negative, got: (%d, %d)".formatted(x, y)
);
}
super(x, y);
}
}

The restriction is that you cannot access this before super() is called — only the constructor parameters and static members are available.


Module Import Declarations — Finalised (Java 25, JEP 511)

Module import declarations, previewed in Java 23 and 24, are finalised in Java 25. You can now import all exported packages of a module with a single statement:

import module java.base; // imports all of java.lang, java.util, java.io, etc.
import module java.sql; // imports java.sql, javax.sql
public class DatabaseExample {
// No individual imports needed for List, Map, Connection, etc.
public List<Map<String, Object>> query(Connection conn, String sql) throws SQLException {
var results = new ArrayList<Map<String, Object>>();
try (var stmt = conn.prepareStatement(sql);
var rs = stmt.executeQuery()) {
while (rs.next()) {
var row = new LinkedHashMap<String, Object>();
for (int i = 1; i <= rs.getMetaData().getColumnCount(); i++) {
row.put(rs.getMetaData().getColumnName(i), rs.getObject(i));
}
results.add(row);
}
}
return results;
}
}

Compact Object Headers — Finalised (Java 25, JEP 519)

Every Java object has a header — metadata used by the JVM for identity hash codes, locking, GC state, and the class pointer. Before Java 25, this header was 12 bytes on 64-bit JVMs with compressed class pointers, or 16 bytes without.

Java 25 introduces compact object headers that reduce this to 8 bytes by compressing the class pointer into the mark word. For applications with many small objects (e.g., large graphs, caches, data processing pipelines), this can reduce heap usage by 10–20%.

Terminal window
# Enable compact object headers (default in Java 25)
java -XX:+UseCompactObjectHeaders MyApplication
# Disable if needed for compatibility
java -XX:-UseCompactObjectHeaders MyApplication

Primitive Types in Patterns — Finalised (Java 25, JEP 507)

Primitive type patterns, previewed in Java 23 and 24, are finalised in Java 25:

// Stable in Java 25
Object obj = 42;
// instanceof with primitive type
if (obj instanceof int i) {
System.out.println("int: " + i);
}
// switch with primitive types
String classify(Object o) {
return switch (o) {
case int i when i < 0 -> "negative int";
case int i -> "non-negative int";
case long l -> "long: " + l;
case double d -> "double: " + d;
case boolean b -> "boolean: " + b;
case String s -> "string: " + s;
case null -> "null";
default -> "other";
};
}

Summary

Java 24 and 25 complete the current evolution cycle. Java 25 is the LTS release that organisations will adopt as their next production baseline after Java 21.

FeatureVersionKey Benefit
Stream Gatherers (Final)24Custom, stateful stream transformations
AOT Class Loading24Faster startup through class cache
Scoped Values (Final)25Thread-safe, immutable context propagation
Compact Source Files (Final)25Minimal boilerplate for small programs
Flexible Constructor Bodies (Final)25Code before super() in constructors
Module Import Declarations (Final)25Import entire modules with one statement
Compact Object Headers (Final)258-byte headers; 10–20% heap reduction
Primitive Types in Patterns (Final)25Match primitives in instanceof and switch