JDK 21 Key Features — The Fourth LTS and Java's Biggest Update Ever

Why JDK 21 Is Special

JDK 21, released in September 2023, is the first LTS (Long-Term Support) release in 4 years since JDK 17. With preview features accumulated since JDK 14 all becoming final at once, a total of 15 JEPs are included. Virtual Threads, Record Patterns, and Pattern Matching for switch all finalized in this release — it’s no exaggeration to call this the biggest change since JDK 8.

This article organizes JDK 21’s key features by category.

Virtual Threads (Final, JEP 444)

Lightweight threads managed by the JVM that dramatically increase throughput for I/O-bound tasks. The key patterns are creating one per task with Executors.newVirtualThreadPerTaskExecutor() and using ReentrantLock instead of synchronized.

For detailed usage, performance comparisons, and caveats (Pinning, ThreadLocal), see the Virtual Threads Complete Guide post.

Record Patterns (Final, JEP 440)

A destructuring syntax that extracts record type fields directly in instanceof and switch. After first preview in JDK 19 and second preview in JDK 20, it’s finally officially released.

public class RecordPatternFinal {
    sealed interface JsonValue permits JsonString, JsonNumber, JsonArray {}
    record JsonString(String value) implements JsonValue {}
    record JsonNumber(double value) implements JsonValue {}
    record JsonArray(java.util.List<JsonValue> elements) implements JsonValue {}

    // Convert JSON values with Record Pattern
    static String toDisplay(JsonValue json) {
        return switch (json) {
            case JsonString(var s) -> "\"" + s + "\"";
            case JsonNumber(var n) -> n % 1 == 0
                ? String.valueOf((long) n)  // Remove decimal for integers
                : String.valueOf(n);
            case JsonArray(var elements) -> {
                var items = elements.stream()
                    .map(RecordPatternFinal::toDisplay)
                    .toList();
                yield items.toString();
            }
        };
    }

    public static void main(String[] args) {
        // Clean data processing with destructuring
        JsonValue name = new JsonString("Java");
        JsonValue version = new JsonNumber(21.0);
        JsonValue tags = new JsonArray(java.util.List.of(
            new JsonString("LTS"),
            new JsonString("virtual-threads")
        ));

        System.out.println(toDisplay(name));      // Output: "Java"
        System.out.println(toDisplay(version));    // Output: 21
        System.out.println(toDisplay(tags));       // Output: ["LTS", "virtual-threads"]
    }
}

The sealed interface + Record Pattern + switch combination is the signature pattern of JDK 21. Since the compiler verifies all subtypes, it’s safe without a default branch, and adding a new type produces a compile error in unhandled switch statements.

Pattern Matching for switch (Final, JEP 441)

Following JDK 17’s instanceof pattern matching, switch statements can now also use type patterns and when guards. Notably, null handling inside switch is now possible.

import java.util.List;

public class SwitchPatternFinal {
    // switch pattern matching including null handling
    static String classify(Object obj) {
        return switch (obj) {
            case null -> "null value";
            case Integer i when i < 0 -> "Negative: " + i;
            case Integer i -> "Non-negative: " + i;
            case String s when s.isBlank() -> "Blank string";
            case String s -> "String: " + s;
            case List<?> list when list.isEmpty() -> "Empty list";
            case List<?> list -> "List (size: " + list.size() + ")";
            default -> "Other: " + obj.getClass().getSimpleName();
        };
    }

    public static void main(String[] args) {
        // Test with various types
        Object[] testCases = {
            null, -42, 100, "", "Java 21",
            List.of(), List.of("a", "b", "c")
        };

        for (Object obj : testCases) {
            System.out.println(classify(obj));
        }
        // Output:
        // null value
        // Negative: -42
        // Non-negative: 100
        // Blank string
        // String: Java 21
        // Empty list
        // List (size: 3)
    }
}

Previously, passing null to switch would throw a NullPointerException. Starting with JDK 21, case null allows explicit handling, eliminating the need for separate null checks outside switch.

Sequenced Collections (JEP 431)

A new set of interfaces introducing the concept of order to the Java Collections Framework. List has index-based order, LinkedHashSet has insertion-order, and SortedSet has sort-order — each has “order” but there was no unifying interface for them.

import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.SequencedCollection;
import java.util.SequencedMap;

public class SequencedCollectionExample {
    public static void main(String[] args) {
        // SequencedCollection: unified first/last element access
        SequencedCollection<String> languages = new ArrayList<>(
            List.of("Java", "Python", "Go", "Rust")
        );

        System.out.println("First: " + languages.getFirst());  // Java
        System.out.println("Last: " + languages.getLast());     // Rust

        // addFirst/addLast to add at either end
        languages.addFirst("Kotlin");
        languages.addLast("Swift");
        System.out.println("After adding: " + languages);
        // Output: After adding: [Kotlin, Java, Python, Go, Rust, Swift]

        // reversed() creates a reverse view (not a new collection)
        SequencedCollection<String> reversed = languages.reversed();
        System.out.println("Reversed: " + reversed);
        // Output: Reversed: [Swift, Rust, Go, Python, Java, Kotlin]

        // SequencedMap: ordered map
        SequencedMap<String, Integer> versions = new LinkedHashMap<>();
        versions.put("Java", 21);
        versions.put("Python", 3);
        versions.put("Go", 1);

        System.out.println("First entry: " + versions.firstEntry());  // Java=21
        System.out.println("Last entry: " + versions.lastEntry());    // Go=1

        // pollFirstEntry/pollLastEntry removes and returns
        var removed = versions.pollLastEntry();
        System.out.println("Removed entry: " + removed);
        // Output: Removed entry: Go=1

        // LinkedHashSet is also a SequencedCollection
        var techStack = new LinkedHashSet<>(List.of("Spring", "Docker", "Kubernetes"));
        System.out.println("First tech: " + techStack.getFirst());  // Spring
    }
}

SequencedCollection is an interface added above existing collection classes, so ArrayList, LinkedList, LinkedHashSet, etc. already implement it. reversed() returns a view of the original collection, so it doesn’t create a new collection and is memory-efficient.

String Templates (Preview, JEP 430)

A preview of string interpolation. The STR processor allows inserting expressions directly within strings.

public class StringTemplateExample {
    // Requires --enable-preview at runtime
    public static void main(String[] args) {
        String name = "Java";
        int version = 21;

        // STR template processor (preview)
        String message = STR."\{name} \{version} has been released!";
        System.out.println(message);
        // Output: Java 21 has been released!

        // Expressions can be used
        int x = 10, y = 20;
        String calc = STR."\{x} + \{y} = \{x + y}";
        System.out.println(calc);
        // Output: 10 + 20 = 30

        // Multi-line templates
        String html = STR."""
            <html>
                <body>
                    <h1>\{name} \{version}</h1>
                    <p>Features: \{version - 17} versions since last LTS</p>
                </body>
            </html>
            """;
        System.out.println(html);
    }
}

Note that String Templates will undergo API changes during review in JDK 22, so it’s best not to use them in production code yet.

Unnamed Patterns and Variables (Preview, JEP 443)

A feature using _ (underscore) for unused variables or patterns to clearly express intent.

import java.util.LinkedList;
import java.util.Queue;

public class UnnamedPreviewExample {
    sealed interface Result permits Success, Failure {}
    record Success(String data) implements Result {}
    record Failure(int code, String message) implements Result {}

    public static void main(String[] args) {
        // Use _ for unused variables (preview)
        Queue<Result> results = new LinkedList<>();
        results.add(new Success("OK"));
        results.add(new Failure(404, "Not Found"));
        results.add(new Success("Done"));

        int successCount = 0;
        for (Result r : results) {
            // code field is unused, so mark it with _
            switch (r) {
                case Success(var data) ->
                    System.out.println("Success: " + data);
                case Failure(var _, var message) ->
                    System.out.println("Failure: " + message);
            }
        }
        // Output:
        // Success: OK
        // Failure: Not Found
        // Success: Done
    }
}

Performance Improvements

Generational ZGC (JEP 439)

ZGC gained generational collection. By separating Young Generation and Old Generation collection, GC efficiency is greatly improved especially for workloads with many short-lived objects.

To enable:

java -XX:+UseZGC -XX:+ZGenerational -jar app.jar

Compared to standard ZGC, allocation speed improves, heap memory usage decreases, and GC pause times remain sub-millisecond as before.

Key Encapsulation Mechanism API (JEP 452)

A KEM (Key Encapsulation Mechanism) API was added for safely exchanging symmetric keys in public-key cryptography. As an encryption standard in preparation for the quantum computing era, it lays the foundation for future quantum-resistant algorithm support.

Summary

Here are the biggest changes when migrating from JDK 17 LTS to JDK 21 LTS:

CategoryFeatureImpact
ConcurrencyVirtual ThreadsHigh — Dramatically improves I/O-bound service throughput
LanguageRecord Patterns + switch pattern matchingHigh — Simplifies type-based branching code
CollectionsSequenced CollectionsMedium — Unified first/last element access
GCGenerational ZGCMedium — Improved GC efficiency for large heaps
SecurityKEM APILow — Foundation for future quantum-resistant encryption
LanguageString Templates (Preview)FYI — API may change before finalization

Checkpoints for JDK 17 -> 21 migration:

  1. synchronized -> ReentrantLock: Prevent Pinning when using Virtual Threads
  2. ThreadLocal -> ScopedValue: Save memory in Virtual Thread environments
  3. Multiple if-instanceof -> switch pattern matching: Simplify code
  4. Collection first/last access -> getFirst()/getLast(): Use unified API
  5. Gradle/Maven build tools: Upgrade to JDK 21 compatible versions

JDK 21 is an LTS release that will receive security updates for at least 8 years (until 2031). If you have projects stuck on JDK 8 or JDK 11, it’s highly recommended to actively consider migrating to JDK 21.

Was this article helpful?