🧵 Java 21 Features (LTS)
Java 21 (September 2023) is a monumental Long-Term Support release delivering Project Loom's Virtual Threads, major Pattern Matching additions, and Sequenced Collections.
1. Virtual Threads (JEP 444 — Project Loom) ⭐
Virtual Threads represent the most significant update to Java's concurrency model since java.util.concurrent. They solve the fundamental problem with traditional platform threads, which map 1:1 to OS threads and carry a heavy overhead (approx 1MB stack size, expensive context switches).
Virtual threads are lightweight threads managed directly by the Java Virtual Machine (JVM). Millions of virtual threads can be created on standard hardware. They are perfect for I/O-bound tasks (like HTTP requests, database calls) but not intended for CPU-bound tasks (like complex mathematical calculations).
| Feature | Platform Thread | Virtual Thread |
|---|---|---|
| Managed by | Operating System (OS) | Java Virtual Machine (JVM) |
| Memory Footprint | Heavy (~1MB per thread) | Lightweight (~few hundreds of bytes) |
| Max Threads (Typical) | A few thousands | Millions |
| Best For | Long-running CPU tasks | Many concurrent I/O operations |
public class VirtualThreadBasics { public static void main(String[] args) throws InterruptedException { // 1. Creating a virtual thread using Thread.ofVirtual() Thread vThread = Thread.ofVirtual().start(() -> { System.out.println("Running in: " + Thread.currentThread()); }); vThread.join(); // Wait for it to finish // 2. Creating multiple virtual threads for (int i = 0; i < 10000; i++) { Thread.startVirtualThread(() -> { try { Thread.sleep(Duration.ofSeconds(1)); } catch (InterruptedException e) { // Handle exception } }); } System.out.println("Launched 10,000 virtual threads successfully!"); } }
For thread pools, you can now use Executors.newVirtualThreadPerTaskExecutor() which spins up a new virtual thread for every submitted task rather than reusing them.
import java.util.concurrent.*; import java.time.Duration; public class VirtualThreadExecutor { public static void main(String[] args) { try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 5; i++) { final int taskId = i; executor.submit(() -> { simulateNetworkCall(taskId); }); } } // Executor implicitly shuts down and waits for tasks to finish } private static void simulateNetworkCall(int id) { try { Thread.sleep(Duration.ofMillis(1000)); System.out.println("Task " + id + " completed on " + Thread.currentThread()); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } }
2. Sequenced Collections (JEP 431)
Before Java 21, fetching the first or last element of a collection was inconsistent. For List, you used list.get(0), but for a LinkedHashSet, you had to use iterators. Java 21 introduces three new interfaces: SequencedCollection, SequencedSet, and SequencedMap.
New unified methods available across sequenced collections:
getFirst()/getLast()addFirst(E e)/addLast(E e)removeFirst()/removeLast()reversed()(Returns a reverse-ordered view of the collection)
import java.util.*; public class SequencedCollectionDemo { public static void main(String[] args) { // Works with ArrayList List<String> list = new ArrayList<>(List.of("B", "C")); list.addFirst("A"); list.addLast("D"); System.out.println("First: " + list.getFirst()); // A System.out.println("Reversed: " + list.reversed()); // [D, C, B, A] // Works with LinkedHashSet LinkedHashSet<Integer> set = new LinkedHashSet<>(List.of(2, 3)); set.addFirst(1); System.out.println("Set first: " + set.getFirst()); // 1 // Works with LinkedHashMap LinkedHashMap<String, String> map = new LinkedHashMap<>(); map.putFirst("1", "One"); map.putLast("2", "Two"); System.out.println("Map last entry: " + map.lastEntry()); } }
3. Record Patterns (JEP 440)
Record patterns allow you to deconstruct record values directly in instanceof and switch expressions, making data extraction more concise and expressive.
public class RecordPatternDemo { record Point(int x, int y) {} record Rectangle(Point topLeft, Point bottomRight) {} public static void printDetails(Object obj) { // Destructuring a simple record if (obj instanceof Point(int x, int y)) { System.out.println("Point at X:" + x + " Y:" + y); } // Destructuring nested records! if (obj instanceof Rectangle(Point(int x1, int y1), Point(int x2, int y2))) { System.out.println("Rect from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")"); } } public static void main(String[] args) { Point p = new Point(10, 20); Rectangle r = new Rectangle(p, new Point(30, 40)); printDetails(p); printDetails(r); } }
4. Pattern Matching for Switch (JEP 441)
Switch expressions now support pattern matching, allowing them to test the type of the target variable and safely extract its values. It also introduces guarded patterns using the when clause and integrated null handling.
public class PatternSwitchDemo { static String processValue(Object obj) { return switch (obj) { case null -> "It's null"; case Integer i when i > 100 -> "Large Integer: " + i; case Integer i -> "Small Integer: " + i; case String s when s.isEmpty() -> "Empty String"; case String s -> "String of length " + s.length(); default -> "Unknown type"; }; } public static void main(String[] args) { System.out.println(processValue(null)); System.out.println(processValue(150)); System.out.println(processValue("Java 21")); } }
5. String Templates (JEP 430 — Preview)
--enable-preview flag.String templates improve upon string concatenation by safely and elegantly interpolating variables directly into strings using template processors like STR.
public class StringTemplatesDemo { public static void main(String[] args) { String name = "Alice"; int age = 25; // Old way String oldWay = "Name: " + name + ", Age: " + age; // Java 21 Preview using STR template processor String newWay = STR."Name: \{name}, Age: \{age}"; System.out.println(newWay); } }
6. Unnamed Classes and Instance Main Methods (JEP 445 — Preview)
Aimed at lowering the barrier to entry for beginners, Java 21 allows writing a simple script-like program without wrapping it in a public class and without the verbose public static void main(String[] args) signature.
// No class declaration needed! // No public static void main(String[] args) required! void main() { System.out.println("Hello, Java 21 Beginners!"); }
7. Structured Concurrency (JEP 453 — Preview)
Structured Concurrency simplifies multithreaded programming by treating multiple tasks running in different threads as a single unit of work. It helps prevent thread leaks and makes error handling across threads logical and clean.
import java.util.concurrent.StructuredTaskScope; import java.util.concurrent.Future; public class StructuredConcurrencyDemo { public static String fetchData() throws Exception { try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // Fork multiple concurrent tasks Future<String> dbTask = scope.fork(() -> fetchFromDb()); Future<String> apiTask = scope.fork(() -> fetchFromApi()); // Wait for both to complete or one to fail scope.join(); scope.throwIfFailed(); // Combine results safely return dbTask.resultNow() + " | " + apiTask.resultNow(); } } static String fetchFromDb() { return "DB Data"; } static String fetchFromApi() { return "API Data"; } }
Key Changes Migration Guide (Java 17 to 21)
| Area | Java 17 Approach | Java 21 Approach |
|---|---|---|
| Concurrency | Platform threads, heavy ExecutorService pools | Virtual Threads (Thread.ofVirtual()) allowing blocking I/O freely |
| Switch Statements | Classic switch or basic switch expressions | Pattern matching switch with when guards and record extraction |
| Collections | Inconsistent access for first/last elements | SequencedCollection interface unifying first/last access |