Released September 2023 · Long-term support

Java 21 Features

Java 21 is a landmark long-term-support release. Virtual threads make highly concurrent code simple, record patterns and pattern matching for switch make data handling safer, and sequenced collections tidy up everyday code. Learn each feature with clear explanations and runnable examples.

  • Virtual ThreadsMillions of cheap threads
  • Record PatternsDeconstruct data
  • Switch PatternsType-safe branching
  • Sequenced CollectionsgetFirst, getLast, reversed
Feature 01

Virtual Threads

Virtual threads (JEP 444) are the biggest change in Java 21. They are lightweight threads managed by the JVM instead of the operating system, so you can create one per task, even millions. When a virtual thread blocks on I/O, the JVM parks it and frees the carrier thread, giving high throughput with simple, readable blocking code.

  • Create with Thread.ofVirtual().start(...) or Thread.startVirtualThread
  • Executors.newVirtualThreadPerTaskExecutor(): one virtual thread per task
  • Best for I/O-bound work such as web requests and database calls
  • Do not pool virtual threads; create a new one for each task
VirtualThreadDemo.java
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class VirtualThreadDemo {
    public static void main(String[] args) throws Exception {
        // Start a single virtual thread
        Thread vt = Thread.ofVirtual().name("worker-1").start(() -> {
            Thread t = Thread.currentThread();
            System.out.println("Running in " + t.getName()
                    + ", virtual = " + t.isVirtual());
        });
        vt.join();

        // One virtual thread per task: 10,000 tasks that each block 1 second
        long start = System.nanoTime();
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            List<Future<Integer>> results = new ArrayList<>();
            for (int i = 0; i < 10_000; i++) {
                int id = i;
                results.add(executor.submit(() -> {
                    Thread.sleep(Duration.ofSeconds(1)); // e.g. a slow I/O call
                    return id;
                }));
            }
            long sum = 0;
            for (Future<Integer> f : results) {
                sum += f.get();
            }
            System.out.println("Tasks completed: " + results.size());
            System.out.println("Sum of task ids: " + sum);
        } // close() waits for all tasks to finish

        long seconds = Duration.ofNanos(System.nanoTime() - start).toSeconds();
        // With 10,000 platform threads this would need huge memory;
        // virtual threads finish in about one second
        System.out.println("Finished in under 5 seconds: " + (seconds < 5));
    }
}
Output
Running in worker-1, virtual = true
Tasks completed: 10000
Sum of task ids: 49995000
Finished in under 5 seconds: true
Feature 02

Record Patterns

Record patterns (JEP 440) let you take a record apart while you test its type. Instead of matching a Point and then calling x() and y(), you write obj instanceof Point(int x, int y). Patterns can be nested to reach records inside records.

  • Works in instanceof and in switch
  • Nested patterns: Line(Point(var x1, var y1), Point p2)
  • var can be used for component types
  • Combine with when guards for extra conditions
RecordPatternDemo.java
public class RecordPatternDemo {
    record Point(int x, int y) { }

    record Line(Point start, Point end) { }

    enum Color { RED, GREEN }

    record ColoredPoint(Point point, Color color) { }

    static void print(Object obj) {
        // Before: instanceof, then call accessors one by one
        // Java 21: deconstruct the record directly into variables
        if (obj instanceof Point(int x, int y)) {
            System.out.println("Point at x=" + x + ", y=" + y);
        }

        // Nested patterns reach inside records within records
        if (obj instanceof Line(Point(var x1, var y1), Point(var x2, var y2))) {
            double length = Math.hypot(x2 - x1, y2 - y1);
            System.out.println("Line of length " + length);
        }
    }

    static String describe(Object obj) {
        // Record patterns also work in switch
        return switch (obj) {
            case ColoredPoint(Point(int x, int y), Color c) when x == y ->
                    c + " point on the diagonal";
            case ColoredPoint(Point p, Color c) -> c + " point " + p;
            default -> "Not a colored point";
        };
    }

    public static void main(String[] args) {
        print(new Point(3, 4));
        print(new Line(new Point(0, 0), new Point(3, 4)));

        var red = new ColoredPoint(new Point(2, 2), Color.RED);
        var green = new ColoredPoint(new Point(1, 5), Color.GREEN);
        System.out.println(describe(red));
        System.out.println(describe(green));
        System.out.println(describe("hello"));
    }
}
Output
Point at x=3, y=4
Line of length 5.0
RED point on the diagonal
GREEN point Point[x=1, y=5]
Not a colored point
Feature 03

Pattern Matching for switch

Pattern matching for switch (JEP 441) lets a case match a type, not only a constant. Add a when guard for extra conditions and a case null to handle nulls. With a sealed hierarchy, the compiler checks that every subtype is covered, so no default is needed.

  • case Card c when c.amount() > 50_000 -> ...
  • case null instead of a separate null check
  • Exhaustive for sealed types: adding a subtype breaks the build, not production
  • More specific cases must come before general ones
SwitchPatternDemo.java
public class SwitchPatternDemo {
    sealed interface Payment permits Card, Upi, Cash { }

    record Card(String number, double amount) implements Payment { }

    record Upi(String id, double amount) implements Payment { }

    record Cash(double amount) implements Payment { }

    // No default needed: the compiler knows every Payment type
    static String process(Payment payment) {
        return switch (payment) {
            case Card c when c.amount() > 50_000 ->
                    "Card payment needs OTP: " + c.amount();
            case Card c -> "Card ending " + c.number().substring(12);
            case Upi u -> "UPI to " + u.id();
            case Cash cash -> "Cash " + cash.amount();
        };
    }

    // Switch on any object, including a null case
    static String format(Object obj) {
        return switch (obj) {
            case null -> "null value";
            case Integer i when i < 0 -> "negative int " + i;
            case Integer i -> "int " + i;
            case String s when s.isBlank() -> "blank string";
            case String s -> "string \"" + s + "\"";
            case int[] arr -> "int array of " + arr.length;
            default -> "other: " + obj.getClass().getSimpleName();
        };
    }

    public static void main(String[] args) {
        System.out.println(process(new Card("4111222233334444", 1_200)));
        System.out.println(process(new Card("4111222233334444", 75_000)));
        System.out.println(process(new Upi("asha@bank", 499)));
        System.out.println(process(new Cash(100)));

        Object[] values = { 42, -7, "Java 21", " ", new int[3], 2.5, null };
        for (Object v : values) {
            System.out.println(format(v));
        }
    }
}
Output
Card ending 4444
Card payment needs OTP: 75000.0
UPI to asha@bank
Cash 100.0
int 42
negative int -7
string "Java 21"
blank string
int array of 3
other: Double
null value
Feature 04

Sequenced Collections

Java 21 (JEP 431) added the SequencedCollection, SequencedSet and SequencedMap interfaces. Lists, deques, LinkedHashSet, TreeSet, LinkedHashMap and TreeMap now share one API to access both ends and to get a reversed view.

  • getFirst(), getLast(), addFirst(), removeLast()
  • reversed() returns a reversed view, not a copy
  • Maps: firstEntry(), lastEntry(), putFirst()
  • Replaces list.get(list.size() - 1)
SequencedDemo.java
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedMap;
import java.util.SequencedSet;

public class SequencedDemo {
    public static void main(String[] args) {
        // SequencedCollection: one API for first and last elements
        List<String> list = new ArrayList<>(List.of("B", "C"));
        list.addFirst("A");
        list.addLast("D");
        System.out.println("list       : " + list);
        System.out.println("getFirst() : " + list.getFirst());
        System.out.println("getLast()  : " + list.getLast());
        System.out.println("reversed() : " + list.reversed());

        // Before Java 21: list.get(list.size() - 1)

        SequencedSet<Integer> set = new LinkedHashSet<>(List.of(10, 20, 30));
        System.out.println("set first  : " + set.getFirst());
        System.out.println("removeLast : " + set.removeLast() + " -> " + set);

        // SequencedMap: entries in a defined order
        SequencedMap<String, Integer> scores = new LinkedHashMap<>();
        scores.put("Ravi", 72);
        scores.put("Meera", 91);
        scores.putFirst("Asha", 85);
        System.out.println("map        : " + scores);
        System.out.println("firstEntry : " + scores.firstEntry());
        System.out.println("lastEntry  : " + scores.lastEntry());
        System.out.println("reversed   : " + scores.reversed());
    }
}
Output
list       : [A, B, C, D]
getFirst() : A
getLast()  : D
reversed() : [D, C, B, A]
set first  : 10
removeLast : 30 -> [10, 20]
map        : {Asha=85, Ravi=72, Meera=91}
firstEntry : Asha=85
lastEntry  : Meera=91
reversed   : {Meera=91, Ravi=72, Asha=85}
Feature 05

New String, Math and Character Methods

Java 21 also added handy methods to core classes. StringBuilder.repeat appends text several times, String.splitWithDelimiters keeps the separators, Math.clamp limits a value to a range and Character.isEmoji recognises emoji.

  • StringBuilder.repeat(CharSequence, int)
  • String.indexOf(str, beginIndex, endIndex)
  • Math.clamp(value, min, max)
  • HashMap.newHashMap(n) (Java 19) creates a map sized for n entries
NewApiDemo.java
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class NewApiDemo {
    public static void main(String[] args) {
        // StringBuilder.repeat (Java 21)
        StringBuilder sb = new StringBuilder("ab");
        sb.repeat("-", 5).repeat('*', 3);
        System.out.println("repeat()         : " + sb);

        // String.indexOf within a range (Java 21)
        String text = "java,spring,kafka,java";
        System.out.println("indexOf(range)   : " + text.indexOf("java", 1, 22));

        // String.splitWithDelimiters keeps the separators (Java 21)
        String expr = "10+20-5";
        System.out.println("splitWithDelims  : "
                + Arrays.toString(expr.splitWithDelimiters("[+-]", 0)));

        // Math.clamp keeps a value within a range (Java 21)
        System.out.println("clamp(150,0,100) : " + Math.clamp(150, 0, 100));
        System.out.println("clamp(-8,0,100)  : " + Math.clamp(-8, 0, 100));

        // Character.isEmoji (Java 21)
        int rocket = "🚀".codePointAt(0);
        System.out.println("isEmoji(rocket)  : " + Character.isEmoji(rocket));
        System.out.println("isEmoji('A')     : " + Character.isEmoji('A'));

        // HashMap.newHashMap sizes the map for n entries (Java 19)
        Map<String, Integer> map = HashMap.newHashMap(100);
        map.put("users", 100);
        System.out.println("newHashMap(100)  : " + map);
    }
}
Output
repeat()         : ab-----***
indexOf(range)   : 18
splitWithDelims  : [10, +, 20, -, 5]
clamp(150,0,100) : 100
clamp(-8,0,100)  : 0
isEmoji(rocket)  : true
isEmoji('A')     : false
newHashMap(100)  : {users=100}
Feature 06

Preview Features in Java 21

Java 21 also included preview features that you can try with --enable-preview. Unnamed patterns and variables (_) let you ignore values you don't need, and unnamed classes with an instance main make small programs much shorter. String templates, scoped values and structured concurrency were also previews.

  • _ for unused variables and pattern components (final in Java 22)
  • void main() without a class or static (final in Java 25)
  • String templates were previewed and later withdrawn
  • Scoped values and structured concurrency for concurrent code
HelloPreview.java
// Preview features in Java 21 (final in later releases).
// Run with:  java --enable-preview --source 21 HelloPreview.java

import java.util.List;

record Order(String item, int qty) { }

// Unnamed class with an instance main method: no class, no static
void main() {
    List<Object> orders = List.of(new Order("Book", 2), new Order("Pen", 5));

    int total = 0;
    for (Object o : orders) {
        // Unnamed pattern: _ ignores the component we don't need
        if (o instanceof Order(_, int qty)) {
            total += qty;
        }
    }
    System.out.println("Total quantity: " + total);

    // Unnamed variable: _ for a value we never use
    int count = 0;
    for (var _ : orders) {
        count++;
    }
    System.out.println("Orders: " + count);
}
Run it
$ java --enable-preview --source 21 HelloPreview.java
Total quantity: 7
Orders: 2
Feature 07

JVM and Platform Changes

Since Java 17 the platform has improved too. ZGC became generational for better throughput, UTF-8 became the default charset everywhere (Java 18), and the JDK gained a simple web server, jwebserver. Check the deprecations below when upgrading.

  • Generational ZGC: low pauses with less CPU and memory
  • UTF-8 by default, so text files behave the same on every OS
  • jwebserver for quick local testing
  • New KEM cryptography API; finalization deprecated for removal
Commands and notes
# Generational ZGC (JEP 439): better throughput, low pauses
java -XX:+UseZGC -XX:+ZGenerational -jar app.jar

# Java 18: UTF-8 is the default charset on every OS
# (files, readers and writers no longer depend on the platform)

# Java 18: a simple web server for static files and testing
jwebserver --port 8000 --directory ./site

# Java 21: Key Encapsulation Mechanism API (javax.crypto.KEM)
# Java 21: warning when an agent is loaded into a running JVM
# Deprecated: finalization (Java 18), 32-bit Windows port (Java 21)
What Came Next

Beyond Java 21

Java keeps a six-month release cycle. These are the highlights of the releases after Java 21:

VersionKey additions
Java 22Unnamed variables (_), Foreign Function & Memory API, multi-file source launch
Java 23Markdown documentation comments, primitive patterns (preview)
Java 24Stream gatherers, class-file API, virtual threads without pinning on synchronized
Java 25 (LTS)Compact source files and instance main, flexible constructor bodies, scoped values
Java 26HTTP/3 support in the HTTP Client, further preview features