Skip to main content

Java 21 Features with Examples: Virtual Threads, Record Patterns, Sequenced Collections

All 15 JEPs in JDK 21: which became final (virtual threads, record patterns, pattern matching for switch, sequenced collections, Generational ZGC, the KEM API) and which are still preview or incubator, each with a short example and real output from JDK 21, plus interview questions.

Java 21 (19 September 2023) is a long-term-support release with 15 JEPs. Eight of them are final and ready for production: virtual threads (444), record patterns (440), pattern matching for switch (441), sequenced collections (431), Generational ZGC (439), the Key Encapsulation Mechanism API (452), and two housekeeping changes (449 and 451). The other seven are preview or incubator features that need a flag and may change: string templates, unnamed patterns and variables, unnamed classes with instance main, scoped values, structured concurrency, the foreign function and memory API, and the Vector API. This guide covers all fifteen, runs thirteen of them for real, and says plainly which two have no runnable example.
JEPFeatureStatus in 21Example on this page
444Virtual ThreadsFinalYes
440Record PatternsFinalYes
441Pattern Matching for switchFinalYes
431Sequenced CollectionsFinalYes
439Generational ZGCFinalYes
452Key Encapsulation Mechanism APIFinalYes
449Deprecate the Windows 32-bit x86 Port for RemovalFinalNo (a platform change)
451Prepare to Disallow the Dynamic Loading of AgentsFinalNo (a JVM warning policy)
430String TemplatesPreviewYes
443Unnamed Patterns and VariablesPreviewYes
445Unnamed Classes and Instance Main MethodsPreviewYes
446Scoped ValuesPreviewYes
453Structured ConcurrencyPreviewYes
442Foreign Function & Memory APIThird previewYes
448Vector APISixth incubatorYes
Tested against. OpenJDK 21.0.12.1. The JEP list and the 19 September 2023 release date come from the JDK 21 project page, which labels only some JEPs with a stage; the statuses for the rest are taken from each JEP’s own page title and from what the compiler accepted. Preview demos are compiled with --release 21 --enable-preview. The runnable project is the java21-features module; ./run.sh regenerates every output quoted here.

What is final, what is preview

Before the details, here is the one picture to keep in mind. Final features are safe to use in production code today. Preview and incubator features compile only with flags, and their API can change or disappear in the next release.

Final (8): use in production 444 Virtual Threads 440 Record Patterns 441 Pattern Matching for switch 431 Sequenced Collections 439 Generational ZGC 452 KEM API 449 and 451 (platform and policy) Preview or incubator (7): needs flags 430 String Templates 443 Unnamed Patterns and Variables 445 Unnamed Classes, Instance main 446 Scoped Values 453 Structured Concurrency 442 Foreign Function and Memory (3rd preview) 448 Vector API (6th incubator)

Virtual threads (JEP 444, final)

A virtual thread is a Thread that the JVM schedules onto a small pool of carrier threads. When it blocks on I/O or sleep, it is unmounted and the carrier runs something else, so blocking code scales to tens of thousands of concurrent tasks without an async framework. This demo starts 10,000 tasks that each sleep for one second.

try (ExecutorService ex = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 10_000; i++)
        ex.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); done.incrementAndGet(); return null; });
}
tasks completed : 10000
finished in < 5s: true   (10,000 x 1s sleeps)
isVirtual       : true, daemon=true
platform isVirtual: false

Source: VirtualThreadsDemo.java, output in 01-virtual-threads.txt. Only counts and a generous time bound are asserted, because timing depends on the machine. Virtual threads are always daemon threads, so a program that only waits on them will exit.

Going deeper: when virtual threads do not help

They help with blocking, I/O-bound work. They do not speed up CPU-bound loops, and you should not pool them: create one per task. In JDK 21 a virtual thread blocked inside a synchronized block pins its carrier, which can starve the scheduler; see the pinning diagnosis guide, the benchmarks and the migration walkthrough.

Record patterns (JEP 440, final)

A record pattern takes a record apart in instanceof or switch and binds its components, including nested records, in one step. See the records guide for the type itself.

if (o instanceof Line(Point(var x1, var y1), Point(var x2, var y2)))
    return "line from (" + x1 + "," + y1 + ") to (" + x2 + "," + y2 + ")";
if (o instanceof Box<?>(String s)) return "box of String " + s;
line from (0,0) to (3,4) length^2=25
box of String hi
box of Integer 7
no match

Source: RecordPatternsDemo.java. The last line shows the fall-through: a Box<Double> matches neither pattern. JDK 21 removed the Java 20 preview forms that put record patterns in enhanced for headers and allowed named record patterns.

Pattern matching for switch (JEP 441, final)

switch can now test types, add a when guard, handle case null, and the compiler checks that a switch over a sealed type covers every case, so a sealed hierarchy needs no default.

static String size(Shape s) {
    return switch (s) {
        case Circle c when c.r() > 10 -> "big circle";
        case Circle c -> "circle";
        case Square q -> "square " + q.side();
        case Rect(double w, double h) -> "rect " + w + "x" + h;
    };
}
big circle | circle | square 2.0 | rect 2.0x3.0
null handled in the switch | large int | int | string of 4 | other

Source: PatternSwitchDemo.java. The two compiler checks are real errors, captured in 14-compile-errors.txt:

broken/NonExhaustive.java:6: error: the switch expression does not cover all possible input values
broken/DominatedCase.java:3: error: this case label is dominated by a preceding case label

The first appears when you add a new permitted subtype and forget a case; the second when a general case such as case Circle c comes before the guarded case Circle c when .... More forms are in the pattern matching guide.

Sequenced collections (JEP 431, final)

Three new interfaces, SequencedCollection, SequencedSet and SequencedMap, give every collection with a defined encounter order the same methods: getFirst, getLast, addFirst, addLast, removeFirst, removeLast and reversed(). List, Deque, LinkedHashSet, SortedSet and LinkedHashMap now implement them. Before 21, getting the last element of a LinkedHashSet meant iterating the whole set.

getFirst/getLast: a c
reversed view   : [c, b, a]
addFirst+removeLast: [z, a, b]
LinkedHashSet reversed: [3, 2, 1]
firstEntry/lastEntry : x=1 z=3
putFirst        : {w=0, x=1, y=2, z=3}
reversed map    : {z=3, y=2, x=1, w=0}
view sees add   : [3, 2, 1]
immutable list : UnsupportedOperationException
HashSet         : no getFirst (unordered)

Source: SequencedCollectionsDemo.java. Two details the output shows: reversed() is a live view, not a copy (the view saw the later add), and HashSet stays unordered, so it does not get these methods. The full story is in the sequenced collections post; for the older collection types see LinkedHashMap and the collections interview questions.

Going deeper: a source-compatibility catch

Because the new methods were added to existing interfaces, a class of yours that implements List and already declares its own getFirst() or reversed() with a different return type can stop compiling on 21. Default methods keep most code working, but check custom collections when you upgrade.

Key Encapsulation Mechanism API (JEP 452, final)

A KEM lets two parties agree on a shared secret using the receiver’s public key, which is the building block of hybrid public-key encryption and of post-quantum key exchange. JEP 452 adds the javax.crypto.KEM API so providers can plug in algorithms; JDK 21 itself ships one implementation, DHKEM (RFC 9180), which this demo uses with an X25519 key pair.

KEM kem = KEM.getInstance("DHKEM");
KEM.Encapsulated e = kem.newEncapsulator(kp.getPublic()).encapsulate();
SecretKey sender   = e.key();
SecretKey receiver = kem.newDecapsulator(kp.getPrivate()).decapsulate(e.encapsulation());
algorithm       : DHKEM
secrets equal   : true
secret length   : 32 bytes
ciphertext length: 32 bytes

Source: KemDemo.java. The sender sends only the 32-byte encapsulation; both sides derive the same 32-byte secret. Post-quantum algorithms such as ML-KEM arrived in later JDKs; this release provides the API they plug into.

Generational ZGC (JEP 439, final)

ZGC keeps pause times very low. Generational ZGC splits the heap into young and old generations so short-lived garbage is collected often and cheaply, which cuts CPU and memory overhead. In JDK 21 it is opt-in: you enable it with -XX:+UseZGC -XX:+ZGenerational. (A later release made it the default, so check your version before relying on this.) The easiest way to prove which collector ran is to list the GC beans:

$ javac  src/GcNamesDemo.java && java -XX:+UseZGC GcNamesDemo     (21.0.12.1)
gc bean         : ZGC Cycles
gc bean         : ZGC Pauses

$ javac  src/GcNamesDemo.java && java -XX:+UseZGC -XX:+ZGenerational GcNamesDemo     (21.0.12.1)
gc bean         : ZGC Minor Cycles
gc bean         : ZGC Minor Pauses
gc bean         : ZGC Major Cycles
gc bean         : ZGC Major Pauses

Source: GcNamesDemo.java. Dashboards and alerts keyed to the old bean names ZGC Cycles and ZGC Pauses will go quiet after you turn the flag on, because the beans are now named Minor and Major. For choosing a JDK and tuning, see the JFR profiling guide.

Vector API (JEP 448, sixth incubator)

The Vector API expresses SIMD arithmetic that the JIT compiler can map to CPU vector instructions. It has been incubating for years, waiting for Project Valhalla, so it needs --add-modules jdk.incubator.vector and prints a warning. The demo adds two arrays with vectors and checks the result equals a plain loop.

for (; i < S.loopBound(n); i += S.length())
    IntVector.fromArray(S, a, i).add(IntVector.fromArray(S, b, i)).intoArray(c, i);
warning: using incubating module(s): jdk.incubator.vector
1 warning
WARNING: Using incubator modules: jdk.incubator.vector
vector == scalar: true
lanes >= 2      : true

Source: VectorDemo.java. The demo proves correctness, not speed; benchmark your own workload with JMH before adopting it. Valhalla background: value classes and JEP 401.

The preview features (compile with flags)

Every example in this section compiles with javac --release 21 --enable-preview and runs with java --enable-preview. Without the flag the compiler refuses, which is the point of a preview:

broken/PreviewWithoutFlag.java:3: error: string templates are a preview feature and are disabled by default.
  (use --enable-preview to enable string templates)

Classes compiled with preview features are tied to the exact JDK feature release that built them, so do not ship them as libraries. Treat these as things to try, not to depend on.

String templates (JEP 430, preview)

Embed expressions in a string with \{...} and process them with a template processor such as STR.

System.out.println(STR."Hello \{name}, \{a} + \{b} = \{a + b}");
Hello Ankur, 3 + 4 = 7

Source: StringTemplatesPreview.java. Honest status note: this feature was later withdrawn for redesign and is not in current JDKs, so do not rewrite your string code around it. For ordinary strings see the strings deep dive.

Unnamed patterns and variables (JEP 443, preview)

Use _ where you must declare something but never use it: a record component, a loop variable, a caught exception.

if (o instanceof Point(var x, _)) ...
for (var _ : List.of("a", "b", "c")) total++;
catch (NumberFormatException _) { ... }
x only         : 5
loop count      : 3
caught, name not needed

Source: UnnamedPreview.java.

Unnamed classes and instance main (JEP 445, preview)

A beginner can write a source file with only a void main() method: no class, no static, no String[] args. The compiler wraps it in an unnamed class.

void main() {
    System.out.println("no class, no static, no String[] args");
}
$ java --enable-preview --source 21 src/UnnamedMainPreview.java     (21.0.12.1)
no class, no static, no String[] args

Source: UnnamedMainPreview.java, run in single-file source mode.

Scoped values (JEP 446, preview)

A ScopedValue is immutable data shared with code you call, for exactly the duration of that call. It is the safer, cheaper replacement for ThreadLocal in a world of millions of virtual threads.

static final ScopedValue<String> USER = ScopedValue.newInstance();
ScopedValue.where(USER, "ankur").run(ScopedValuesPreview::handler);
inside handler  : user=ankur
outside         : bound=false

Source: ScopedValuesPreview.java. Outside the where(...).run(...) call the value is simply unbound.

Structured concurrency (JEP 453, preview)

Treat a group of subtasks as one unit of work: if one fails, the scope cancels its siblings, and no thread outlives its scope.

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    var user  = scope.fork(() -> { Thread.sleep(50); return "ankur"; });
    var order = scope.fork(() -> { Thread.sleep(80); return 42; });
    scope.join().throwIfFailed();
}
both subtasks   : ankur / 42
failure shuts down siblings: java.lang.IllegalStateException: boom

Source: StructuredConcurrencyPreview.java. The second scope forks a task that throws and one that sleeps five seconds; the failure ends the scope without waiting for the sleeper. See structured concurrency in depth.

Foreign Function and Memory API (JEP 442, third preview)

Call native code and manage off-heap memory without JNI. This calls C’s strlen on a string allocated in a confined arena that frees it on close.

try (Arena arena = Arena.ofConfined()) {
    MemorySegment s = arena.allocateUtf8String("Hello Ankur");
    long len = (long) strlen.invokeExact(s);
}
strlen("Hello Ankur") = 11

Source: FfmPreview.java, run with --enable-preview --enable-native-access=ALL-UNNAMED. The API became final in JDK 22 with some renamed methods, such as allocateUtf8String becoming allocateFrom, so expect to edit this code when you upgrade.

The two JEPs with no runnable example

JEP 449: Deprecate the Windows 32-bit x86 port for removal. This is a platform decision, not an API. It affects you only if you build or run Java on 32-bit Windows, where the port is deprecated for removal (building it from source needs an explicit opt-in flag) and a later release removes it. There is nothing to demonstrate in code.

JEP 451: Prepare to disallow the dynamic loading of agents. Starting with 21, the JVM prints a warning when an agent is loaded into a running JVM after startup, as some profilers and mocking tools do. The planned end state is to forbid it unless you pass -XX:+EnableDynamicAgentLoading. I did not build a demo because triggering it needs an external attach tool, so please read the JEP rather than trust a made-up example here.

Common mistakes

Pooling virtual threads

Platform threads are expensive, so we pool them. Virtual threads are cheap, so create one per task with Executors.newVirtualThreadPerTaskExecutor(). Limit concurrency with a Semaphore if a downstream service needs it.

Putting a default in a switch over a sealed type

A default silences the exhaustiveness check, so adding a new permitted subtype later compiles without a warning and fails at run time. Omit default and let the compiler tell you what is missing.

Putting a general case before a guarded one

case Circle c above case Circle c when c.r() > 10 is a compile error: “this case label is dominated by a preceding case label”. Order cases from most specific to least specific.

Shipping preview features

Preview classes only load on the same JDK feature release that compiled them, and the API may change or vanish. String templates were later withdrawn and the FFM API was reshaped before it went final.

Assuming Generational ZGC is on by default

On 21 you must add -XX:+ZGenerational. Without it you get the non-generational collector, as the bean names above show.

Common interview questions

What is the difference between a virtual thread and a platform thread?

A platform thread maps one to one to an OS thread and costs megabytes of stack. A virtual thread is scheduled by the JVM onto a few carrier threads and is unmounted while blocked, so you can have millions. Same Thread API, different cost model. More in the concurrency interview questions.

Which Java 21 features are final and which are preview?

Final: virtual threads, record patterns, pattern matching for switch, sequenced collections, Generational ZGC, the KEM API. Preview: string templates, unnamed patterns and variables, unnamed classes and instance main, scoped values, structured concurrency, and the FFM API. The Vector API is still an incubator module.

How does the compiler know a switch is exhaustive?

For a sealed type it knows every permitted subtype, so a switch expression without default must cover them all, including through record patterns. Otherwise the code does not compile.

Why does reversed() not copy the list?

It returns a view backed by the original, so it costs O(1) and reflects later changes, as the output above shows. Copy it yourself if you need a snapshot.

Why scoped values instead of ThreadLocal?

A scoped value is immutable and bound for a bounded call, so it cannot leak and needs no cleanup, and it is cheap to inherit by child threads in a structured scope.

FAQ

Is Java 21 an LTS release?

Yes. Java 21 is a long-term-support release and followed Java 17. Check the free-update timeline for which vendor builds are still supported.

How many features does Java 21 have?

Fifteen JEPs, of which six are API or language features that are final, two are housekeeping changes, and seven are preview or incubating.

Are virtual threads production ready in Java 21?

They are final, but watch for pinning inside synchronized blocks on 21 and measure before migrating a large service.

How do I upgrade from Java 17 to 21?

Follow the direct migration guide; see also what came before in Java 17 features and what came after in Java 21 to 25.

Should you move to Java 21? If you are on 17, yes, mainly for virtual threads, pattern matching for switch and sequenced collections, all of which are final. Leave the preview features alone in production code. If you are choosing a target today, also look at the newer LTS in the Java 21 to 25 guide.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.