Skip to content

About

Executable tour of Java 21 to 25 LTS: virtual threads, structured concurrency, scoped values, pattern matching, gatherers, FFM, KDF, Class-File API, with tests and JMH benchmarks

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

Modern Java Features (21 → 25 LTS)

CI Java 25 License: MIT

A tour of the Java language and platform features delivered between the Java 21 and Java 25 LTS releases. Every feature has:

  • a small but realistic example (fees, payments, product pages, request context), not a Foo/Bar snippet;
  • tests that act as executable documentation, which prove the claims (for example, that 10,000 blocking tasks finish in about the time of one);
  • a note on when to use it in a typical Spring/backend codebase.

Feature map

Feature JEP Final in Example Test
Record patterns, pattern matching for switch, sealed types 440, 441, 409 21 FeeCalculator, Result test
Virtual threads 444 21 BlockingWorkload test
Sequenced collections 431 21 RecentlyViewed test
Unnamed variables and patterns (_) 456 22 FeeCalculator, Result test
Foreign Function & Memory API 454 22 NativeLibC test
Markdown documentation comments (///) 467 23 used across the whole codebase —
Stream gatherers 485 24 StreamGatherers test
Class-File API 484 24 BytecodeInspector test
Virtual threads no longer pinned by synchronized 491 24 BlockingWorkload test
Scoped values 506 25 RequestContext test
Flexible constructor bodies 513 25 Rectangle test
Module import declarations 511 25 WordFrequency test
Compact source files and instance main 512 25 scripts/ run in CI
Key Derivation Function API (HKDF) 510 25 KeyDerivation test (RFC 5869 vector)
Structured concurrency 505 preview ProductPageService test

Highlights

Data-oriented programming: sealed types + records + pattern matching

Business rules become a single exhaustive switch. If someone adds a new payment method, every switch that does not handle it stops compiling, which no instanceof chain or visitor can guarantee as cheaply.

return switch (payment) {
    case Payment(Card(var network, var installments), var amount) when installments > 1 ->
            percent(amount, network.equals("AMEX") ? "4.5" : "3.5")
                    .add(new BigDecimal("0.10").multiply(BigDecimal.valueOf(installments)));
    case Payment(Card(var network, _), var amount) -> percent(amount, network.equals("AMEX") ? "3.0" : "2.0");
    case Payment(BankTransfer _, _) -> BANK_TRANSFER_FEE;
    case Payment(DigitalWallet(var provider), _) when provider.equals("PARTNER_WALLET") -> BigDecimal.ZERO;
    case Payment(DigitalWallet _, var amount) -> percent(amount, "1.5");
};   // no default: PaymentMethod is sealed

Virtual threads + structured concurrency + scoped values

These three work together: cheap threads that may block, a safe way to fan out and cancel, and immutable context that child tasks inherit.

try (var scope = StructuredTaskScope.open()) {
    Subtask<Product> product = scope.fork(() -> backends.product(id));
    Subtask<Integer> stock = scope.fork(() -> backends.stock(id));
    Subtask<List<String>> reviews = scope.fork(() -> backends.reviews(id));
    scope.join();          // first failure cancels the other calls and is rethrown
    return new ProductPage(product.get(), stock.get(), reviews.get());
}

The tests check the guarantees: the calls run in parallel, a failing call interrupts its slower siblings, and a deadline applies to the whole operation.

Stream gatherers

items.stream().gather(Gatherers.windowFixed(500))                  // batches for bulk inserts
sales.stream().gather(Gatherers.windowSliding(7))                  // moving averages
movements.stream().gather(Gatherers.scan(() -> opening, BigDecimal::add))  // running balance
ids.stream().gather(Gatherers.mapConcurrent(10, client::fetch))    // bounded parallel I/O, order kept

Benchmarks

VirtualThreadsBenchmark measures the time to complete N tasks that each block for 10 ms, which is roughly a fast DB query or HTTP call.

Strategy 1,000 tasks 10,000 tasks
Fixed pool of 200 platform threads 109 ms 571 ms
One platform thread per task 228 ms 3,424 ms
One virtual thread per task 16 ms 87 ms

Indicative numbers from a laptop (Intel i7-1165G7, 8 logical cores, WSL2, JDK 25.0.4), 1 fork × 3 measurement iterations, so the error margins are wide. The theoretical floor is 10 ms (every task in parallel). What matters is the trend: the bounded pool queues work (at least N / 200 × 10 ms), and thousands of OS threads are expensive to create and schedule, while virtual threads stay close to the floor.

./mvnw -P benchmarks package -DskipTests
java --enable-preview -jar target/benchmarks.jar

Running

Requires JDK 25. Structured concurrency is a preview API, so the build passes --enable-preview.

./mvnw verify                       # compile + all tests
java scripts/Hello.java             # compact source file, no compilation step
java scripts/TopWords.java README.md 5

When to use what (backend perspective)

Need Reach for
Many concurrent blocking calls (JDBC, REST clients) Virtual threads (spring.threads.virtual.enabled=true in Spring Boot 3.2+)
Call several services in parallel and fail fast Structured concurrency (preview), or CompletableFuture until it is final
Request id / tenant / user propagated through the call chain Scoped values instead of ThreadLocal
Closed domain hierarchies (payment methods, events, commands) Sealed interfaces + records + exhaustive switch
Batching, windows, running totals, bounded parallel map in streams Gatherers
Deriving per-purpose keys from one secret KDF API (HKDF)
Calling a native library FFM API instead of JNI

License

MIT

About

Executable tour of Java 21 to 25 LTS: virtual threads, structured concurrency, scoped values, pattern matching, gatherers, FFM, KDF, Class-File API, with tests and JMH benchmarks

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages