Java 24 & 25: The Next LTS — Simpler, Faster, Safer
Java 24 delivered ahead-of-time class loading and stream gatherers. Java 25 is the next LTS release, finalising scoped values, compact source files, flexible constructor bodies, module import declarations, and primitive type patterns — while introducing compact object headers for significant memory savings.
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 windowsStream.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 windowsStream.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 resultOptional<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 threadsStream.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 totalGatherer<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.
# Step 1: Create the AOT cache during a training runjava -XX:AOTMode=record -XX:AOTConfiguration=app.aotconf -jar myapp.jar
# Step 2: Use the cache on subsequent runsjava -XX:AOTMode=on -XX:AOTConfiguration=app.aotconf -jar myapp.jarThis 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 scopevoid 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 scopevoid 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
| Aspect | ThreadLocal | ScopedValue |
|---|---|---|
| Mutability | Mutable | Immutable within scope |
| Scope | Thread lifetime | Explicitly bounded |
| Inheritance | Copies to child threads | Inherited without copying |
| Virtual thread support | Poor (copies on inherit) | Excellent |
| Memory overhead | High | Low |
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 programvoid 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 methodclass 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 allowedclass 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%.
# Enable compact object headers (default in Java 25)java -XX:+UseCompactObjectHeaders MyApplication
# Disable if needed for compatibilityjava -XX:-UseCompactObjectHeaders MyApplicationPrimitive 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 25Object obj = 42;
// instanceof with primitive typeif (obj instanceof int i) { System.out.println("int: " + i);}
// switch with primitive typesString 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.
| Feature | Version | Key Benefit |
|---|---|---|
| Stream Gatherers (Final) | 24 | Custom, stateful stream transformations |
| AOT Class Loading | 24 | Faster startup through class cache |
| Scoped Values (Final) | 25 | Thread-safe, immutable context propagation |
| Compact Source Files (Final) | 25 | Minimal boilerplate for small programs |
| Flexible Constructor Bodies (Final) | 25 | Code before super() in constructors |
| Module Import Declarations (Final) | 25 | Import entire modules with one statement |
| Compact Object Headers (Final) | 25 | 8-byte headers; 10–20% heap reduction |
| Primitive Types in Patterns (Final) | 25 | Match primitives in instanceof and switch |