PCSalt
YouTube GitHub
Back to Java
Java · 3 min read

Java 25 — What's New in the Next LTS

A practical overview of Java 25 LTS — the features that matter, what's finalized from preview, and what you need to know before upgrading from Java 21.


Java 25 was released on September 16, 2025 — the next Long-Term Support release after Java 21. If you skipped Java 22, 23, and 24, this is the version to upgrade to.

This post covers the features that matter for everyday development — not every JEP, just the ones you’ll actually use.

Why Java 25?

Java LTS releases get years of support. Java 21 (September 2023) is the current standard. Java 25 is the next one. If your team follows the “upgrade on LTS” strategy, Java 25 is your target.

Key themes:

  • Features that were preview in 21–24 are now finalized
  • Virtual threads improvements
  • Pattern matching enhancements
  • Project Panama (Foreign Function & Memory API) finalized
  • Performance improvements across the board

Finalized features (were preview in 21–24)

Structured Concurrency

First previewed in Java 21. Now stable in 25.

import java.util.concurrent.StructuredTaskScope;

record UserProfile(User user, List<Order> orders) {}

UserProfile fetchProfile(String userId) throws Exception {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        var userTask = scope.fork(() -> userService.getUser(userId));
        var ordersTask = scope.fork(() -> orderService.getOrders(userId));

        scope.join();
        scope.throwIfFailed();

        return new UserProfile(userTask.get(), ordersTask.get());
    }
}

Both tasks run concurrently. If either fails, the other is cancelled. If the parent thread is interrupted, both tasks are cancelled. No thread leaks, no forgotten futures.

This is the Java equivalent of Kotlin’s coroutineScope with structured concurrency.

Scoped Values

Replacement for ThreadLocal that works correctly with virtual threads:

import java.lang.ScopedValue;

static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

void handleRequest(User user) {
    ScopedValue.runWhere(CURRENT_USER, user, () -> {
        processOrder(); // can access CURRENT_USER.get()
    });
}

void processOrder() {
    User user = CURRENT_USER.get(); // available without passing as parameter
    // ...
}

Unlike ThreadLocal, scoped values:

  • Are immutable within a scope
  • Are automatically cleaned up when the scope exits
  • Work correctly with virtual threads (no thread-pool reuse issues)

String Templates

Write strings with embedded expressions:

String name = "Alice";
int age = 30;

// String template
String message = STR."Hello, \{name}. You are \{age} years old.";
// "Hello, Alice. You are 30 years old."

// With expressions
String calc = STR."2 + 3 = \{2 + 3}";
// "2 + 3 = 5"

// Multi-line
String json = STR."""
    {
        "name": "\{name}",
        "age": \{age}
    }
    """;

STR is the standard string template processor. The \{expr} syntax embeds expressions. This is safer than string concatenation because custom processors can handle SQL injection, HTML escaping, etc.

Unnamed Variables and Patterns

Use _ for variables you don’t need:

// Before
try {
    // ...
} catch (IOException e) {
    // e is unused
    log.warn("IO error occurred");
}

// After
try {
    // ...
} catch (IOException _) {
    log.warn("IO error occurred");
}

// In enhanced for loops
for (var _ : collection) {
    count++;
}

// In pattern matching
if (obj instanceof Point(var x, _)) {
    // only care about x coordinate
}

Gatherers (Stream API enhancement)

Custom intermediate stream operations:

import java.util.stream.Gatherers;

// Window sliding
List<List<Integer>> windows = List.of(1, 2, 3, 4, 5).stream()
    .gather(Gatherers.windowSliding(3))
    .toList();
// [[1, 2, 3], [2, 3, 4], [3, 4, 5]]

// Fixed-size groups
List<List<Integer>> groups = List.of(1, 2, 3, 4, 5).stream()
    .gather(Gatherers.windowFixed(2))
    .toList();
// [[1, 2], [3, 4], [5]]

Gatherers let you write custom stream operations that maintain state — something that was impossible with the existing Stream API without hacks.

Virtual Threads improvements

Virtual threads shipped in Java 21. Java 25 refines them:

  • Better integration with synchronized blocks (less pinning)
  • Improved debugging and profiling support
  • Better stack trace reporting

The core API hasn’t changed — you still use Thread.ofVirtual() or Executors.newVirtualThreadPerTaskExecutor(). But the runtime behavior is more reliable.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    List<Future<String>> futures = urls.stream()
        .map(url -> executor.submit(() -> fetch(url)))
        .toList();

    List<String> results = futures.stream()
        .map(f -> {
            try { return f.get(); }
            catch (Exception e) { return "error"; }
        })
        .toList();
}

One virtual thread per task. Millions of concurrent tasks. No thread pool tuning.

Foreign Function & Memory API (Project Panama)

Finalized. Call native code without JNI:

import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.SymbolLookup;
import java.lang.foreign.ValueLayout;

// Call strlen from C standard library
Linker linker = Linker.nativeLinker();
SymbolLookup stdlib = linker.defaultLookup();
var strlen = linker.downcallHandle(
    stdlib.find("strlen").orElseThrow(),
    FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
);

try (var arena = Arena.ofConfined()) {
    MemorySegment str = arena.allocateFrom("Hello, Panama!");
    long len = (long) strlen.invoke(str);
    System.out.println("Length: " + len); // 14
}

Most developers won’t use this directly, but libraries like database drivers, crypto, and compression will benefit from replacing JNI with a safer API.

Pattern matching enhancements

Building on Java 21’s pattern matching:

Primitive patterns in instanceof

// Java 25 — pattern match with primitives
Object obj = 42;
if (obj instanceof int i) {
    System.out.println("Integer value: " + i);
}

Enhanced switch patterns

record Point(int x, int y) {}

String describe(Object obj) {
    return switch (obj) {
        case Integer i when i > 0 -> "Positive: " + i;
        case Integer i -> "Non-positive: " + i;
        case String s when s.length() > 10 -> "Long string";
        case String s -> "Short string: " + s;
        case Point(var x, var y) when x == 0 && y == 0 -> "Origin";
        case Point(var x, var y) -> "Point(%d, %d)".formatted(x, y);
        case null -> "null";
        default -> "Unknown: " + obj;
    };
}

Guards (when), record patterns, null handling — all in one switch expression.

Migration from Java 21

What breaks?

Very little. Java has strong backward compatibility. Common issues:

  1. Deprecated APIs removed — Check for @Deprecated(forRemoval=true) usage
  2. Security Manager removed — If your code uses SecurityManager, it’s gone
  3. Internal API access--add-opens flags might change

Steps

  1. Update your build tool’s JDK to 25
  2. Update sourceCompatibility and targetCompatibility to 25
  3. Build and run tests
  4. Fix deprecation warnings
  5. Start using new features incrementally
// build.gradle.kts
java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(25))
    }
}

Framework compatibility

FrameworkJava 25 Support
Spring Boot 4.0+Full support
Spring Boot 3.xWorks (requires 3.4+)
Kotlin 2.1+Full support
Gradle 9.xFull support
Maven 3.9+Full support

Should you upgrade?

Yes, if:

  • You’re on Java 17 or 21 and your framework supports 25
  • You want structured concurrency and virtual thread improvements
  • You’re planning a Spring Boot 4 migration (it targets Java 25)

Wait, if:

  • You’re on Java 11 or earlier (jump to 21 first, then 25)
  • Critical dependencies don’t support Java 25 yet
  • You’re mid-release and stability matters more

Summary

Java 25 LTS finalizes the most anticipated features from Java 21–24:

  • Structured Concurrency — safe concurrent task management
  • Scoped Values — better than ThreadLocal for virtual threads
  • String Templates — embedded expressions in strings
  • Unnamed Variables_ for unused variables
  • Gatherers — custom stream operations
  • Foreign Function API — native calls without JNI
  • Pattern matching — primitives, guards, records in switch

It’s a substantial upgrade from Java 21, and the first LTS to fully embrace the virtual threads ecosystem.