Java 21 Features
Dashboard
Topic 24/30
Home › Java 21 Features

🧵 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
VirtualThreadBasics.javaJava
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.

VirtualThreadExecutor.javaJava
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)
SequencedCollectionDemo.javaJava
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.

RecordPatternDemo.javaJava
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.

PatternSwitchDemo.javaJava
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)

Note: String Templates are a Preview feature in Java 21 and must be enabled with the --enable-preview flag.

String templates improve upon string concatenation by safely and elegantly interpolating variables directly into strings using template processors like STR.

StringTemplatesDemo.javaJava
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.

HelloWorld.javaJava
// 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.

StructuredConcurrencyDemo.javaJava
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