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
| Feature | What It Does | Status |
|---|---|---|
| Virtual Threads | Lightweight threads for high-throughput concurrency | Final |
| Pattern Matching for Switch | Concise, safe switch statements | Final |
| Sequenced Collections | Unified API for ordered collections | Final |
| String Templates | String interpolation with safety | Preview |
| Scoped Values | Better alternative to ThreadLocal | Preview |
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).
// 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
// 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.
// 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
// 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 SequencedCollection, SequencedSet, SequencedMap.
New Methods
// 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
// 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.
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.
// 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 Problem: ThreadLocal is mutable, expensive, and doesn't work well with virtual threads.
The Solution: Immutable, inheritable scoped values.
// 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.
// 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)
// 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
// 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 deprecatedUse
CleanerorAutoCloseableinstead
// 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 }