Saturday, April 18, 2026

Java21 Features

 

Java 21 Features – Complete Guide

Java 21 (released September 19, 2023) is a Long-Term Support (LTS) release, following Java 17 LTS. It contains 15 major features including virtual threads, pattern matching, sequenced collections, and string templates.


Quick Overview – Top 5 Features

FeatureWhat It DoesStatus
Virtual ThreadsLightweight threads for high-throughput concurrencyFinal
Pattern Matching for SwitchConcise, safe switch statementsFinal
Sequenced CollectionsUnified API for ordered collectionsFinal
String TemplatesString interpolation with safetyPreview
Scoped ValuesBetter alternative to ThreadLocalPreview

1. Virtual Threads (JEP 444) – FINAL

The Problem: Platform threads are expensive (1MB stack), limiting concurrency to ~10,000 threads.

The Solution: Virtual threads (carrier threads on demand, millions possible).

java
// BEFORE – Platform thread pool
ExecutorService executor = Executors.newFixedThreadPool(200);
Future<?> future = executor.submit(() -> handleRequest());

// AFTER – Virtual threads (Java 21)
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
Future<?> future = executor.submit(() -> handleRequest());

// Even simpler – Thread.startVirtualThread()
Thread.startVirtualThread(() -> handleRequest());

// For web servers – Spring Boot 3.2+ with Tomcat
server.tomcat.threads.max=200  # Not needed with virtual threads
spring.threads.virtual.enabled=true

Real-World Impact

java
// Handle 1 million concurrent connections (impossible before)
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
for (int i = 0; i < 1_000_000; i++) {
    executor.submit(() -> {
        // Each virtual thread blocks on I/O without blocking platform thread
        var result = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        return result;
    });
}
// Runs on a few dozen platform threads!

Key points:

  • Virtual threads are cheap (~1KB memory vs 1MB)

  • Perfect for I/O-bound workloads

  • Never pool virtual threads (create per task)

  • Use semaphores for limiting concurrent access to limited resources


2. Pattern Matching for Switch (JEP 441) – FINAL

The Problem: Verbose, error-prone instanceof + cast code.

The Solution: Switch with type patterns and guards.

java
// BEFORE Java 21
Object obj = "Hello";
if (obj instanceof String) {
    String s = (String) obj;
    System.out.println(s.length());
} else if (obj instanceof Integer) {
    Integer i = (Integer) obj;
    System.out.println(i * 2);
}

// AFTER Java 21
switch (obj) {
    case String s -> System.out.println(s.length());
    case Integer i -> System.out.println(i * 2);
    case Long l -> System.out.println(l / 2);
    case null -> System.out.println("null");
    default -> System.out.println("Unknown");
}

// With guards (when clause)
switch (shape) {
    case Circle c when c.radius() > 10 -> System.out.println("Large circle");
    case Circle c -> System.out.println("Small circle");
    case Rectangle r when r.width() == r.height() -> System.out.println("Square");
    default -> System.out.println("Other");
}

Null Safety

java
// Switch now handles null explicitly (no NPE)
String result = switch (value) {
    case null -> "null value";
    case "A" -> "First";
    default -> "Other";
};

3. Sequenced Collections (JEP 431) – FINAL

The Problem: Inconsistent APIs for ordered collections (List, Deque, SortedSet, SortedMap).

The Solution: New interfaces SequencedCollectionSequencedSetSequencedMap.

New Methods

java
// For SequencedCollection (List, Deque, SortedSet)
interface SequencedCollection<E> extends Collection<E> {
    // Get first/last element
    E getFirst();
    E getLast();
    
    // Add to ends
    void addFirst(E e);
    void addLast(E e);
    
    // Remove from ends
    E removeFirst();
    E removeLast();
    
    // Reverse view
    SequencedCollection<E> reversed();
}

// For SequencedMap
interface SequencedMap<K,V> extends Map<K,V> {
    V putFirst(K k, V v);
    V putLast(K k, V v);
    
    Entry<K,V> firstEntry();
    Entry<K,V> lastEntry();
    
    SequencedSet<K> sequencedKeySet();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    SequencedCollection<V> sequencedValues();
    
    SequencedMap<K,V> reversed();
}

Usage Examples

java
// List now has getFirst/getLast
List<String> list = List.of("a", "b", "c");
String first = list.getFirst();  // "a" (was list.get(0))
String last = list.getLast();    // "c" (was list.get(list.size()-1))

// Reverse any ordered collection
SequencedCollection<String> reversed = list.reversed();  // ["c", "b", "a"]

// LinkedHashMap becomes SequencedMap
LinkedHashMap<String, Integer> map = new LinkedHashMap<>();
map.putFirst("a", 1);  // Insert at beginning
map.putLast("z", 26);  // Insert at end
var firstEntry = map.firstEntry();  // a=1
var lastEntry = map.lastEntry();    // z=26

4. Record Patterns (JEP 440) – FINAL

The Problem: Destructuring records required verbose nested pattern matching.

The Solution: Nest record patterns to decompose nested records.

java
record Point(int x, int y) {}
record Line(Point start, Point end) {}

// BEFORE Java 21
if (obj instanceof Line line) {
    Point start = line.start();
    Point end = line.end();
    System.out.println(start.x() + ", " + start.y());
}

// AFTER Java 21 – Nested record patterns
if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
    System.out.println(x1 + ", " + y1);
}

// With switch
switch (obj) {
    case Line(Point(var x1, var y1), Point(var x2, var y2)) -> 
        System.out.printf("Line from (%d,%d) to (%d,%d)", x1, y1, x2, y2);
    case Point(var x, var y) -> 
        System.out.printf("Point at (%d,%d)", x, y);
    default -> System.out.println("Unknown");
}

5. String Templates (JEP 430) – PREVIEW

The Problem: String concatenation is verbose and unsafe (SQL injection, etc.).

The Solution: String interpolation with built-in processors.

java
// BEFORE – Concatenation or StringBuilder
String name = "João";
int age = 35;
String message = "Hello, " + name + ". You are " + age + " years old.";

// Java 21 – String templates
String message = STR."Hello, \{name}. You are \{age} years old.";

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

// Custom processors for security
SQL sql = SQL."SELECT * FROM users WHERE name = '\{userInput}'";
// Automatically escapes SQL injection

// FMT processor for formatting
String formatted = FMT."Price: %.2f\{price}, Tax: %.2f\{tax}";

6. Scoped Values (JEP 446) – PREVIEW

The ProblemThreadLocal is mutable, expensive, and doesn't work well with virtual threads.

The Solution: Immutable, inheritable scoped values.

java
// BEFORE – ThreadLocal (mutable, expensive)
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
currentUser.set(user);
User u = currentUser.get();
currentUser.remove();  // Must remember to clean!

// AFTER – Scoped Values (immutable, automatic cleanup)
private static final ScopedValue<User> CURRENT_USER = ScopedValue.newInstance();

// Bind value for a scope
ScopedValue.where(CURRENT_USER, user)
    .run(() -> {
        // All code here sees CURRENT_USER.get() == user
        // Even across virtual threads!
        processRequest();
    });
// After run() completes, value is automatically cleared

// In deep call chain
public void processRequest() {
    User user = CURRENT_USER.get();  // Always available
    auditService.log(user);
}

Why Scoped Values?

  • Immutable (cannot be changed within scope)

  • No memory leaks (no need for remove())

  • Works with virtual threads (inherited automatically)

  • Faster than ThreadLocal (optimized JVM implementation)


7. Structured Concurrency (JEP 453) – PREVIEW

The Problem: Managing multiple concurrent tasks is error-prone (thread leaks, cancellation).

The Solution: Structured task scopes where children are automatically cleaned up.

java
// BEFORE – Manual thread management (easy to leak)
Future<String> task1 = executor.submit(() -> fetchUser());
Future<Integer> task2 = executor.submit(() -> fetchScore());
// If task1 fails, task2 continues running (leak)
String user = task1.get();
Integer score = task2.get();

// AFTER – Structured Concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    
    Subtask<String> userTask = scope.fork(() -> fetchUser());
    Subtask<Integer> scoreTask = scope.fork(() -> fetchScore());
    
    scope.join();           // Wait for both
    scope.throwIfFailed();  // Propagate first failure
    
    // If either fails, the other is automatically cancelled
    String user = userTask.get();
    Integer score = scoreTask.get();
    
} // Auto-closes, cancels any remaining tasks

// ShutdownOnSuccess – return first successful result
try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
    scope.fork(() -> fetchFromApi1());
    scope.fork(() -> fetchFromApi2());
    scope.fork(() -> fetchFromApi3());
    
    String result = scope.join().result();  // First to succeed
    // Others automatically cancelled
}

8. Other Notable Features

Unnamed Patterns & Variables (JEP 443)

java
// BEFORE
try {
    // ...
} catch (Exception e) {  // e is unused
    log.error("Error");
}

// AFTER – Underscore for unused variables
try {
    // ...
} catch (Exception _) {  // No variable name needed
    log.error("Error");
}

// In pattern matching
if (obj instanceof Point(_, int y)) {
    System.out.println(y);  // Don't care about x
}

// Lambda parameters
list.stream().map(_ -> "Constant");  // Unused parameter

Foreign Function & Memory API (JEP 454) – FINAL

java
// Call C libraries without JNI
try (Arena arena = Arena.ofConfined()) {
    MemorySegment segment = arena.allocate(100);
    segment.setUtf8String(0, "Hello from Java!");
    
    // Call strlen from C standard library
    Linker linker = Linker.nativeLinker();
    SymbolLookup stdlib = linker.defaultLookup();
    MethodHandle strlen = linker.downcallHandle(
        stdlib.find("strlen").get(),
        FunctionDescriptor.of(ValueLayout.JAVA_LONG, ValueLayout.ADDRESS)
    );
    
    long length = (long) strlen.invoke(segment);
    System.out.println(length);  // 17
}

Deprecation of Finalization

  • Object.finalize() is now deprecated

  • Use Cleaner or AutoCloseable instead

java
// BEFORE (don't do this)
@Override
protected void finalize() throws Throwable {
    cleanup();
}

// AFTER
public class Resource implements AutoCloseable {
    @Override
    public void close() {
        cleanup();
    }
}
// Use try-with-resources
try (Resource r = new Resource()) {
    // use resource
}