Java 21 was the biggest LTS since Java 8, and Java 25 (LTS, September 2025) finalizes most of the preview features that shipped alongside it. If you last touched modern Java during the Java 17 era, the jump to 25 is not just a syntax refresh β it fundamentally changes how you write concurrent code, pattern-match data, and model domain objects. This post walks through the six features that matter most for day-to-day Java work, with runnable code for each.
1. Records β Data Classes, Finally
Records collapse the boilerplate of constructors, accessors, equals, hashCode, and toString into a single declaration line. They are immutable by default, making them ideal for DTOs and value objects.
// Immutable DTO β all boilerplate auto-generated
public record Customer(Long id, String email, String country) {}
Customer c = new Customer(1L, "[email protected]", "IN");
System.out.println(c.email()); // accessor auto-generated: [email protected]
System.out.println(c); // toString auto-generated: Customer[id=1, [email protected], country=IN]
// Compact constructor for validation
public record Customer(Long id, String email, String country) {
public Customer {
Objects.requireNonNull(email, "email must not be null");
if (!email.contains("@")) throw new IllegalArgumentException("Invalid email");
}
}
2. Pattern Matching for Switch
Switch expressions can now destructure records and match by type, eliminating most instanceof chains. Combined with sealed interfaces, the compiler enforces exhaustiveness at compile time.
sealed interface Shape permits Circle, Square, Triangle {}
record Circle(double r) implements Shape {}
record Square(double side) implements Shape {}
record Triangle(double base, double height) implements Shape {}
static double area(Shape s) {
return switch (s) {
case Circle(double r) -> Math.PI * r * r;
case Square(double side) -> side * side;
case Triangle(double b, double h) -> 0.5 * b * h;
// No default needed β compiler verifies exhaustiveness against the sealed hierarchy
};
}
// Guard patterns (Java 21+): add a when clause to refine a match
static String classify(Shape s) {
return switch (s) {
case Circle c when c.r() > 100 -> "large circle";
case Circle c -> "small circle";
case Square sq -> "square";
case Triangle t -> "triangle";
};
}
3. Virtual Threads (Project Loom)
Virtual threads are cheap, JVM-managed threads backed by a small carrier-thread pool. You can spawn millions where platform threads would exhaust memory at a few thousand. The API is deliberately identical to Thread β no reactive callbacks required.
// 10,000 concurrent I/O-bound tasks β completes in ~100 ms
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i ->
executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // simulate DB/HTTP call
return i;
}));
} // auto-closes and waits for all tasks
// Single virtual thread β same API as platform threads
Thread vt = Thread.ofVirtual().name("my-vthread").start(() -> {
System.out.println("Running on: " + Thread.currentThread());
});
vt.join();
4. Sequenced Collections
Java 21 added a uniform API for collections with a defined encounter order. getFirst(), getLast(), addFirst(), addLast(), and reversed() now work consistently on List, LinkedHashSet, and LinkedHashMap.
List<String> names = new ArrayList<>(List.of("alex", "brian", "chris"));
System.out.println(names.getFirst()); // alex
System.out.println(names.getLast()); // chris
System.out.println(names.reversed()); // [chris, brian, alex] (view, not a copy)
// Works on LinkedHashSet too
LinkedHashSet<Integer> nums = new LinkedHashSet<>(List.of(3, 1, 4, 1, 5));
System.out.println(nums.getFirst()); // 3
System.out.println(nums.getLast()); // 5
5. Scoped Values (JEP 506, Final in Java 25)
Scoped values are an immutable, structured replacement for ThreadLocal. They are safer (read-only in inner scopes), automatically cleaned up when the scope exits, and far cheaper on virtual threads where a single request may hop across many virtual threads.
static final ScopedValue<String> CURRENT_USER = ScopedValue.newInstance();
// Bind a value for a specific scope β only code inside the lambda sees it
ScopedValue.where(CURRENT_USER, "ankur").run(() -> {
System.out.println("User: " + CURRENT_USER.get()); // ankur
callDeepMethod(); // can read CURRENT_USER without passing it as a parameter
});
// Outside the scope: CURRENT_USER.get() throws NoSuchElementException
static void callDeepMethod() {
System.out.println("Still: " + CURRENT_USER.get()); // ankur β scope propagates
}
6. Structured Concurrency (JEP 505, Final in Java 25)
Structured concurrency treats a group of concurrent subtasks as a single unit of work. When the scope closes, all forked tasks that have not finished are automatically cancelled. This eliminates the common bug where a partial failure leaves orphaned background tasks running.
// Fan-out: run two async calls, wait for both, cancel both if either fails
try (var scope = StructuredTaskScope.open()) {
var user = scope.fork(() -> fetchUser(userId));
var order = scope.fork(() -> fetchOrder(userId));
scope.join(); // waits for both; propagates any exception automatically
return new Dashboard(user.get(), order.get());
} // scope.close() cancels any still-running forks
// ShutdownOnFailure: cancel everything if the first task fails
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.anySuccessfulResultOrThrow())) {
var result = scope.fork(() -> riskyOperation());
scope.join();
return result.get();
}
How the Code Works
- Records generate a canonical constructor and public accessor methods at compile time. The compact constructor form lets you add validation without repeating the field assignments.
- Pattern matching deconstructs a sealed hierarchy exhaustively β the compiler rejects the switch if any permitted subtype is missing. Guard clauses (
when) let you add fine-grained conditions without nesting. - Virtual threads are mounted onto a small pool of carrier threads. When a virtual thread calls a blocking operation (
sleep, socket read, JDBC call), it is unmounted from its carrier so the carrier can run another virtual thread. The virtual thread resumes on any available carrier when the blocking call returns. - Sequenced collections unify first/last access across all ordered JDK types through three new interfaces:
SequencedCollection,SequencedSet, andSequencedMap. - Scoped values bind data to a dynamic scope (a call tree) rather than a thread, so they propagate naturally across fork-join boundaries in structured concurrency.
- Structured concurrency enforces a strict ownership relationship: the scope that forked a task is responsible for its lifetime. This makes concurrent code auditable the same way a sequential call stack is.
AI Prompts You Can Use
Copy these into Claude or ChatGPT and attach your file to apply each feature to your own code:
Prompt 1 β Convert POJOs to Records
What it does: Reviews a Java class and converts it to a record if it is a pure data carrier. Moves constructor validation into a compact constructor. Flags classes that cannot be converted (mutable state, inheritance, JPA @Entity) and explains the reason for each.
When to use it: During a modernization pass on DTO, request/response, and value-object classes.
Review the attached Java class and convert it to a Java 21 record if it is a pure data carrier. Preserve all validation from the old constructor inside a compact constructor. Flag fields that prevent conversion (e.g. mutable state, inheritance) and explain why.
Prompt 2 β Modernize instanceof Chains
What it does: Rewrites every if (x instanceof Type) { Type t = (Type) x; ... } pattern to pattern matching. Introduces a sealed interface if the types form a closed hierarchy. Ensures the resulting switch is exhaustive.
When to use it: Any file with long if-instanceof chains, visitor implementations, or type-dispatch logic.
Refactor this Java code to use Java 21 pattern matching for switch. Introduce a sealed interface if the types form a closed hierarchy. Ensure the switch is exhaustive and remove all redundant casts and intermediate variables.
Prompt 3 β Migrate to Virtual Threads
What it does: Finds every Executors.newFixedThreadPool and newCachedThreadPool in a project and replaces them with newVirtualThreadPerTaskExecutor() where the tasks are I/O-bound. Lists cases where platform threads should be kept and justifies each retention.
When to use it: When starting a migration to virtual threads β get the full picture before changing any code.
Find every usage of Executors.newFixedThreadPool and Executors.newCachedThreadPool in this project. Replace with Executors.newVirtualThreadPerTaskExecutor() where the tasks are I/O-bound. List any cases where platform threads should be kept (CPU-bound, native pinning, synchronized blocks) and justify each decision.
Prompt 4 β Adopt Sequenced Collections
What it does: Replaces verbose first/last access patterns (list.get(0), list.get(list.size()-1), Collections.reverse(list)) with the new Java 21 sequenced API. Keeps behaviour identical.
When to use it: Part of a Java 21+ modernization pass β quick wins in readability with zero behavioural risk.
Rewrite this code to use Java 21 sequenced collection APIs (getFirst, getLast, reversed) instead of list.get(0), list.get(list.size()-1), and manual reversal. Keep behaviour identical.
Prompt 5 β Replace ThreadLocal with ScopedValue
What it does: Identifies ThreadLocal usages in a file and proposes a migration to ScopedValue (final in Java 25). Shows before/after code and explains why ScopedValue is safer under virtual threads and structured concurrency.
When to use it: After migrating to virtual threads, when you need to pass request-scoped data (user ID, trace ID, tenant) without threading it through every method signature.
Identify ThreadLocal usages in this file. Propose a migration to Java 25 ScopedValue, showing before/after code and explaining why ScopedValue is safer under virtual threads and structured concurrency.
Prompt 6 β Structured Concurrency Refactor
What it does: Converts a CompletableFuture.allOf fan-out pipeline to a Java 25 StructuredTaskScope. Explains how error propagation and automatic cancellation differ from the CompletableFuture approach.
When to use it: When you have parallel fan-out logic that needs proper cancellation on failure β a common pattern in microservice aggregators.
Refactor this CompletableFuture.allOf pipeline to use Java 25 StructuredTaskScope. Preserve error propagation semantics and show what happens on cancellation. Explain the difference from the original CompletableFuture approach.
See Also
- Virtual Threads vs Platform Threads β Benchmarks and Code
- Java Streams API Deep Dive + Collectors Cookbook
- Java Concurrency Deep Dive: CompletableFuture, ExecutorService, ForkJoinPool
- Modern Java Testing: JUnit 6 + AssertJ + Mockito 5 + Testcontainers
- Java 21 to Java 25 Upgrade Guide (with AI Prompts)
- AI Prompts Playbook: Upgrading Java 8 β 11 β 17 β 21 β 25
- Demystifying Thread Safety in Java: A Practical Guide
- From Raw Threads to Virtual Threads: A Developerβs Guide
- Java Memory Management and Garbage Collection: Deep Dive
- Overview of New Features in Java 19
FAQs
Is Java 25 backward compatible with Java 21?
Yes for almost all application code. The main breakage areas are removed internal APIs (SecurityManager, Thread.stop/suspend/resume, Object.finalize), stricter module encapsulation, and the migration from javax.* to jakarta.* namespaces if you have not done that already for Java EE dependencies.
Do virtual threads replace reactive frameworks?
For most I/O-bound services, yes β you can write straight-line blocking code and get throughput comparable to reactive approaches. Reactive still has advantages for backpressure-heavy streaming pipelines (e.g., processing millions of events with fine-grained flow control) or when you need the functional composition style of Project Reactor or RxJava.
Can I use records with JPA/Hibernate?
Records work well as DTOs and projections (read-only query results). They cannot be JPA @Entity classes because JPA requires a no-arg constructor, mutable fields, and non-final classes. See the Hibernate Tutorials series for projection patterns.
Conclusion
Java 25 LTS is where Loom, Amber, and the concurrency overhaul all land in their final, non-preview form. Records and pattern matching change how you model data; virtual threads, structured concurrency, and scoped values change how you run it. Together they make idiomatic Java read closer to Kotlin or Scala β without sacrificing the ecosystem. The six prompts above give you a practical route to applying each feature to an existing codebase today.