Java Evolution — Part 9

Java 21, released on September 19, 2023, is the LTS release that every Java developer has been waiting for. It delivers virtual threads as a production-ready feature, finalises the pattern matching story, introduces sequenced collections, and previews string templates. It is the most significant LTS release since Java 8.


Virtual Threads — Finalised (JEP 444)

Virtual threads, previewed in Java 19 and 20, are now a stable, production-ready feature. They are the centrepiece of Project Loom and represent the most important concurrency improvement in Java’s history.

Creating Virtual Threads

// Method 1: Thread.ofVirtual()
Thread vt = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> System.out.println("Hello from virtual thread!"));
// Method 2: Thread factory
ThreadFactory factory = Thread.ofVirtual().factory();
Thread t = factory.newThread(() -> doWork());
// Method 3: Executor (recommended for most use cases)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
int taskId = i;
executor.submit(() -> processTask(taskId));
}
} // Waits for all tasks to complete

The Scalability Story

The following example demonstrates the scalability difference. With platform threads, creating 100,000 threads would exhaust memory. With virtual threads, it is trivial:

// This would fail with platform threads (OutOfMemoryError)
// With virtual threads, it completes in seconds
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
var futures = IntStream.range(0, 100_000)
.mapToObj(i -> executor.submit(() -> {
Thread.sleep(Duration.ofMillis(100)); // simulate I/O
return i * 2;
}))
.toList();
int total = futures.stream()
.mapToInt(f -> {
try { return f.get(); }
catch (Exception e) { throw new RuntimeException(e); }
})
.sum();
System.out.println("Total: " + total);
}

Virtual Threads and Frameworks

Spring Boot 3.2+, Quarkus, Micronaut, and other frameworks support virtual threads. In Spring Boot, enabling them is a single configuration property:

application.yml
spring:
threads:
virtual:
enabled: true

With this enabled, every request is handled by a virtual thread, giving you the scalability of reactive programming with the simplicity of synchronous code.

What Virtual Threads Are Not

Virtual threads are not a silver bullet for CPU-bound work. They excel at I/O-bound workloads — database queries, HTTP calls, file operations — where threads spend most of their time waiting. For CPU-intensive computation, parallel streams or ForkJoinPool remain the right tools.


Sequenced Collections (JEP 431)

Java 21 introduced three new interfaces to the collections framework to represent collections with a defined encounter order:

InterfaceExtendsMeaning
SequencedCollection<E>Collection<E>Has a first and last element
SequencedSet<E>Set<E>, SequencedCollection<E>Ordered set
SequencedMap<K,V>Map<K,V>Map with ordered entries

Before Java 21, getting the first or last element of a List or LinkedHashMap required awkward workarounds:

// Before Java 21 — getting first/last elements
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
String first = list.get(0); // IndexOutOfBoundsException if empty
String last = list.get(list.size() - 1); // verbose
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("one", 1); map.put("two", 2); map.put("three", 3);
String firstKey = map.keySet().iterator().next(); // awkward
// Java 21 — SequencedCollection methods
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
list.getFirst(); // "a"
list.getLast(); // "c"
list.addFirst("z"); // ["z", "a", "b", "c"]
list.addLast("z"); // ["a", "b", "c", "z"]
list.removeFirst(); // removes and returns first element
list.removeLast(); // removes and returns last element
list.reversed(); // a reversed view — no copy made
// SequencedMap
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.put("one", 1); map.put("two", 2); map.put("three", 3);
map.firstEntry(); // Map.Entry("one", 1)
map.lastEntry(); // Map.Entry("three", 3)
map.sequencedKeySet(); // SequencedSet view of keys
map.sequencedValues(); // SequencedCollection view of values
map.sequencedEntrySet(); // SequencedSet view of entries
map.reversed(); // reversed view of the map

Record Patterns — Finalised (JEP 440)

Record patterns, previewed in Java 19 and 20, are now stable. They allow you to deconstruct record components directly in pattern matching expressions.

record Point(int x, int y) {}
record Line(Point start, Point end) {}
// Deconstruct a record in instanceof
Object obj = new Line(new Point(0, 0), new Point(3, 4));
if (obj instanceof Line(Point(int x1, int y1), Point(int x2, int y2))) {
double length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
System.out.printf("Line length: %.2f%n", length); // 5.00
}
// Deconstruct in switch
String describe(Object o) {
return switch (o) {
case Point(int x, int y) when x == 0 && y == 0 -> "Origin";
case Point(int x, int y) when x == 0 -> "On Y-axis";
case Point(int x, int y) when y == 0 -> "On X-axis";
case Point(int x, int y) -> "Point(%d,%d)".formatted(x, y);
case Line(Point s, Point e) -> "Line from %s to %s".formatted(s, e);
default -> "Unknown";
};
}

Pattern Matching for switch — Finalised (JEP 441)

Pattern matching for switch, previewed since Java 17, is now a stable language feature.

// Stable in Java 21
sealed interface Expr permits Num, Add, Mul {}
record Num(double value) implements Expr {}
record Add(Expr left, Expr right) implements Expr {}
record Mul(Expr left, Expr right) implements Expr {}
double eval(Expr expr) {
return switch (expr) {
case Num(double v) -> v;
case Add(var l, var r) -> eval(l) + eval(r);
case Mul(var l, var r) -> eval(l) * eval(r);
};
}
// Usage
Expr expression = new Add(new Num(3), new Mul(new Num(4), new Num(5)));
System.out.println(eval(expression)); // 23.0

The combination of sealed interfaces, records, and pattern matching for switch enables a style of programming that was previously only available in functional languages.


String Templates (Preview — JEP 430)

String templates provide safe, extensible string interpolation. Unlike simple string concatenation, they allow the interpolation to be processed by a template processor.

// Java 21 preview — STR template processor
String name = "Alice";
int age = 30;
String greeting = STR."Hello, \{name}! You are \{age} years old.";
// "Hello, Alice! You are 30 years old."
// Expressions are evaluated
String result = STR."The sum of 2 + 2 is \{2 + 2}.";
// "The sum of 2 + 2 is 4."
// Multi-line with text blocks
String json = STR."""
{
"name": "\{name}",
"age": \{age}
}
""";

The FMT processor supports printf-style format specifiers:

double price = 1234.567;
String formatted = FMT."Price: $%,.2f\{price}";
// "Price: $1,234.57"

Note: String templates were removed in Java 23 and are being redesigned. The feature as described here (with STR. and FMT.) was available only in Java 21 and 22 as a preview.


Unnamed Classes and Instance Main Methods (Preview — JEP 445)

Java 21 began simplifying the entry point for small programs. The goal is to make Java more accessible to beginners by removing the ceremony of public class, static, and String[] args.

// Java 21 preview — unnamed class, simplified main
void main() {
System.out.println("Hello, World!");
}

This is a complete, runnable Java program. No class declaration, no static, no args. This feature was finalised in Java 25.


Generational ZGC (JEP 439)

Java 21 made Generational ZGC available, extending ZGC with generational garbage collection. Most objects die young — Generational ZGC exploits this by maintaining separate young and old generations, collecting the young generation more frequently. This reduces memory overhead and improves throughput compared to non-generational ZGC.

Terminal window
# Enable Generational ZGC
java -XX:+UseZGC -XX:+ZGenerational MyApplication

Summary

Java 21 is the most feature-rich LTS release since Java 8. It delivers on years of preview work and sets a new baseline for modern Java development.

FeatureKey Benefit
Virtual Threads (Final)Millions of lightweight threads; simple blocking code at scale
Sequenced CollectionsUniform API for first/last element access across all ordered collections
Record Patterns (Final)Deconstruct records in pattern matching
Pattern Matching for switch (Final)Type-safe, exhaustive dispatch
String Templates (Preview)Safe, extensible string interpolation
Unnamed Classes (Preview)Simplified entry points for small programs
Generational ZGCLower GC overhead through generational collection