Icon for gfsd IntelliJ IDEA

JDK 21: What is new in Java 21?

Java 21 is out, find out what's new!

JDK 21 is here! This article details all the updates and changes in the latest version Java 21 with quick descriptions and examples to help you understand what is new.

The previous JDK version, Java 20, was released on 21 March 2023. The new version Java 21, set to be released on 19 September 2023, is an LTS release that brings 15 finalized features including core language extensions, new features for developing multi-threaded code, and updates for the Z Garbage Collector and continued work on providing a JNI alternative.

Try Symflower to generate unit tests
Try Symflower in your IDE for test template & test suite generation!

Below, we’re providing a brief overview of all the updates and new features. To learn more about each change, just click the title you’re interested in to scroll further down for a detailed description of all the updates! For the release’s full documentation, head over to jdk.java.net.

If you’re curious about how JDK’s enhancement proposal & roadmap process works, check out our post covering the updates in Java 20 where we explain the process.

What is new in JDK 21? All the new Java 21 features

Core language extensions and updates you should definitely check out

Java 21 finally moves record patterns and pattern matching for switch statements from previews to ready-to-use features. That means it is now safe to use them in your projects as they are meant to stay. Both improve the readability of your code, so make sure to check them out. Unnamed patterns and variables intend to further improve the readability of record patterns but are still in a preview stage, meaning they might need to be taken with a grain of salt.

Have you ever had the issue that you needed a clearly defined order of the elements in your collections? Java 21 introduces sequenced collections to bring you this ability.

String templates (preview) is Java’s take on string interpolation, i.e. the process of evaluating a string literal containing placeholders. This has readability as well as security advantages, so make sure to update your Java know-how with string templates!

Finally, unnamed classes are meant to make the life of Java newbies easier, but are also a nice feature when you are prototyping a simple command line helper and want to get rid of the usual clutter of “public static void main(String[] args)”.

Extensions to multi-threaded code

Java 21 finalizes virtual threads, which make it easier to scale typical thread per request architectures to a theoretically unlimited number of threads. To keep their footprint as low as possible, make sure to also check out scoped values – but note that these are still in a preview stage and not yet finalized.

Another preview feature that helps in dealing with plentiful virtual threads is structured concurrency. It can be used to correctly and robustly coordinate virtual threads and allows observability tools to display threads as they are understood by the developer.

Performance and infrastructure improvements

Java 21 brings improvements to the Z Garbage Collector, another round of incubating an API for performant vector applications, and a preview of an alternative to the current Java native interface (JNI).


In case you are developing KEM (key encapsulation mechanisms) algorithms, you definitely need to check out the new Java API to do exactly that. Also, keep an eye out for the two features that are getting deprecated or are prepared to be removed altogether!

Details of the updates in JDK 21

OK, let’s jump right in! Below, we’re providing a detailed description of each new update that made it into Java 21, with examples to help explain the value of these changes in action.

Core Language Extensions and Updates

440: Record Patterns

Record patterns aim to improve data navigation and processing. They enable nested patterns, enabling you to use more sophisticated data queries. Record patterns were a preview feature in both JDK 19 and JDK 20, and the new release JDK 21 will finalize the feature based on the experience and feedback gathered from previous releases.

Besides some smaller editorial changes, the key change since the preview version is the removed support for record patterns that appear in the header of an enhanced for statement. Record patterns have been evolving alongside Pattern matching for switch expressions and statements (see below), which the new feature extends to switch over the destructured instances of record classes.

To better understand record patterns, check out the following code example taken from JEP 440:

record Point(int x, int y) {}

static void printSumWithoutPatternMatching(Object obj) {
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();

static void printSumWithPatternMatching(Object obj) {
    if (obj instanceof Point(int x, int y)) {

The method printSumWithoutPatternMatching shows what the fields x and y looked like without record patterns. The second method printSumWithPatternMatching shows off the elegance of new pattern matching records. Note that the variables x and y are declared automatically by using them within the pattern Point(int x, int y).

Record patterns can also be nested, allowing to destructure instances of record classes containing records themselves. The following code shows the deconstruction of a Rectangle. Note that the first ColoredPoint is further deconstructed than the second in the provided record pattern:

record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {}

static void printColorOfUpperLeftPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
                               ColoredPoint lr)) {

441: Pattern matching for switch expressions and statements (4th preview)

Pattern matching for switch first appeared as a preview in JDK 17, with second and third previews delivered in subsequent versions. JDK 21 finalizes the feature to enable the continued co-evolution of pattern matching and record patterns.

This feature enables the testing of an expression against a number of patterns, each with a specific action. This makes it easier to express complex data-oriented queries in a more concise and safe way. All existing switch expressions and statements will still compile, and semantics haven’t changed.

The following example showcases most of the new switch functionality:

static void testStringNew(String response) {
    switch (response) {
        case null -> { System.out.println("So quiet Today?"); }
        case String s
        when s.equals("YES") -> {
            System.out.println("You got it");
        case String s
        when s.equalsIgnoreCase("NO") -> {
        case String s -> {

In traditional switch statements, switching over null immediately led to a NullPointerException. With the new ability to switch over different types, it makes sense to also allow a dedicated null case.

In order to stay backward compatible with existing code, the semantics of switch are now as follows:

  • In case the selector expression evaluates to null and there is no dedicated null case, a switch still throws a NullPointerException, thus preserving the old semantics of switch statements.
  • In case the selector expression evaluates to null and there is a dedicated null case, the body of that case is executed without throwing a NullPointerException.

The above code example does not yield a NullPointerException in case response is equal to null but simply prints So quiet Today?.

In addition, it is now possible to switch over types i.e. case String s matches if the selector expression is of the type String. To make this new ability even more useful, the when keyword has been introduced to further filter down which strings should match exactly. In the above example, the second case only matches in case the string equals “YES”.

Please note that with the ability to switch over types, several case branches could match. To keep things simple, the Java language developers decided to simply use the first matching case and not necessarily the best-fitting one.

443: Unnamed Patterns and Variables (preview)

Unnamed patterns and variables (a preview feature) aim to improve the readability of record patterns by avoiding the unnecessary use of nested patterns. By identifying variables that must be declared but will not be used, they also make it easier to maintain code.

Unnamed patterns match a record component without stating the component’s name or type, while unnamed variables are variables that can be initialized but not used. The underscore character ‘_' will be used to denote both unnamed patterns and unnamed variables

Our code example from the section Record Patterns can be thus be simplified to:

static void printColorOfUpperLeftPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(_, Color c),_) {

431: Sequenced Collections

So far, Java’s collection framework has lacked common collection types that would enable developers to determine the encounter order. This made it more difficult to execute operations related to encounter order (for instance, processing elements in reverse order is cumbersome or downright impossible). Sequenced collections solve that problem by providing new interfaces to represent collections with a defined encounter order.

Using a sequenced collection enables devs to define not just first and last elements, but also successors and predecessors for all the elements between first and last. Sequenced collections also support processing elements forward and in reverse.

Three new interfaces were added: SequencedCollection, SequencedSet, and SequencedMap. They all fit into the existing type hierarchy as follows:

Sequenced collection diagram
Image source: https://cr.openjdk.org/~smarks/collections/SequencedCollectionDiagram20220216.png

Let’s take a look at the methods provided by SequencedCollection:

interface SequencedCollection<E> extends Collection<E> {
    // new method
    SequencedCollection<E> reversed();
    // methods promoted from Deque
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();

The new method reversed is especially interesting because it gives a new view on the sequenced collection that allows for iterating through the collection backwards using the typical suspects: enhanced for loops, explicit iterator() loops, forEach(), stream(), parallelStream(), and toArray().

430: String Templates (preview)

String templates are a preview feature in JDK 21 and complement the string literals and text blocks that already exist in Java. To produce specialized results, string templates mix literal text (string literals or text blocks) with embedded expressions that are handled by template processors.

The new language feature and API make it easier to create non-string values that are computed at runtime using literal text and embedded expressions. The feature was actually on our wishlist in our post about Java 20 so we’re excited to see its preview in JDK 21!

Other advantages of this update are:

  • Improved readability of expressions that mix text and expressions
  • Improved security of Java programs that transfer strings from user-provided values to another system (such as queries for databases)
  • Simplified use of APIs that take strings coded in non-Java languages.

The following code example shows a simple string template:

String name = "Joan";
String info = STR."My name is \{name}";
assert info.equals("My name is Joan");   // true

Source: https://openjdk.org/jeps/430

In our example, STR is a template processor that performs string interpolations. It is automatically statically imported to any Java class and can evaluate template expressions such as STR."My name is \{name}". There are dedicated template processors such as FMT, which is able to interpret format specifiers. To learn how user-defined template processors can be specified, take a closer look at the JEP 430.

445: Unnamed Classes and Instance Main Methods (preview)

Unnamed Classes and Instance Main Methods serve to make it easier for Java students to get started, and support educators in gradually introducing programming concepts. This update helps students write their first programs without having to understand advanced language features such as classes and modifiers.

Unnamed classes and instance main methods make it easy for students to write streamlined declarations for single-class programs. It is also easier to seamlessly expand programs as their skills and needs evolve. Without this preview feature, the typical HelloWorld program in Java looks as follows:

public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello, World!");

By introducing unnamed classes and instance main methods that no longer require String[] args as a parameter, the above example now looks as follows:

void main() {
    System.out.println("Hello, World!");

The update reduces the ceremony involved in writing simple Java programs such as scripts and command-line utilities, letting students enhance their skills gradually. But well-versed Java programmers can also benefit from these extensions: small tools or scripts to try something out can be created faster and with less clutter.

Extensions to multi-threaded code

444: Virtual Threads

A preview feature in JDKs 19 and 20, virtual threads have now been finalized and made it into JDK 21 as a new feature.

The currently used platform threads are an instance of Java.lang.Thread that have a 1:1 mapping to OS threads. Because the number of OS threads is limited, the number of platform threads that can be created is limited as well.

Java now introduces virtual threads with an m:n mapping between a virtual thread and an OS thread, meaning that m virtual thread can run on n OS threads. This new approach makes it possible to create, in theory, unlimited amounts of virtual threads, scaling along with the requirements of today’s high-throughput server applications. The widespread thread-per-request style can be used with virtual threads because there is no longer a limitation on the amount of threads that can be created.

Virtual threads are designed in a way that existing applications can be ported to use virtual threads with minimal changes. Existing JDK tools can be used to troubleshoot, debug, and profile virtual threads. A virtual thread can be created over the Thread, Thread.Builder APIs or by using java.util.concurrent.Executors.

For examples of how virtual threads are created, check out the documentation on Oracle’s website.

Changes since the second preview in JDK 20 include:

  • Guaranteed support for thread-local variables .
  • The lifetime monitoring of virtual threads created directly with the Thread.Builder API is now provided. Such threads may be observed via a new thread dump.

446: Scoped Values (preview)

This preview feature enables the sharing of immutable data within and across threads in a large program without using method arguments.

Thread local variables already allow sharing data without method arguments but pose various design flaws:

  • They are mutable, often leading to a spaghetti-like data flow that is hard to read and understand.
  • Their lifetime is unbounded, leading to longer lifetime than needed.
  • They need to be allocated in inheriting child threads (i.e. memory needs to be allocated to thread local variables), even when the child thread only requires read access.

Through the immutability of scoped values, the above downsides of thread local variables are avoided. Scoped values are the preferred choice over thread-local variables especially in cases where a large number of virtual threads is used.

The goals of scoped values are to enhance robustness, performance, and comprehensibility while providing a simple programmatic model to share data within threads and across child threads.

453: Structured Concurrency (preview)

This feature was incubated in JDK 19 and JDK 20 and now finally reached the preview stage in JDK 21. In essence, structured concurrency lets you treat related tasks running in separate threads as a single unit of work.

The goal of this preview feature is to simplify multithreaded programming and to encourage developers to apply concurrent programming. The Structured concurrency API improves error handling and cancellation while improving the reliability, maintainability, and observability of code.

Performance and infrastructure improvements

439: Generational ZGC

Known to many Java devs, the Z Garbage Collector (ZGC) is a low-latency garbage collector that is highly scalable. Java 21 extends ZGC by introducing generational garbage collection to improve application performance.

This update extends the Z Garbage Collector (ZGC) to maintain separate generations of objects (e.g. young and old objects). With this update, ZGC will be able to collect young objects more frequently, since they are most likely to die young. Therefore, using generational ZGC can lead to reduced CPU requirements and memory overhead, and can help avoid allocation stalls.

Since using generational ZGC shouldn’t affect throughput, it should be a better solution for most use cases than today’s non-generational ZGC, meaning that the new feature may altogether replace the old one in the long run.

448: Vector API (6th incubator)

Vector API reaches the 6th incubator phase in JDK 21. This feature was first proposed in JDK 16, with further rounds of incubation in each subsequent release. Incubations are used to get early user feedback on non-final APIs, see openjdk.org for more information on incubator modules.

This API enables the expression of vector computations that reliably compile at runtime to optimal vector instructions on the used CPU architecture. It is a platform-agnostic, clear, and concise API that helps express a variety of vector computations that consist of sequences of vector operations composed within loops, possibly with control flow.

On supported CPU architectures, optimal vector instructions provide superior performance when compared to equivalent scalar computations. Notable changes in comparison to the Java 20 incubation include the addition of exclusive or (xor) operation to vector masks, and the improved performance of vector shuffles.

442: Foreign Function & Memory API (3rd preview)

The Foreign Function & Memory API was first previewed in JDK 19, then in JDK 20.

This new API improves the interoperability of Java programs with code and data outside the Java runtime without relying on the Java Native Interface (JNI). It aims to replace JNI altogether with a solution that’s easier to use, safer, and offers higher performance while providing generality by enabling apps to operate on different kinds of foreign memory.

The Foreign Function & Memory API enables Java applications to call native libraries and process native data by invoking foreign functions and safely accessing foreign memory (e.g. outside JVM). We’re looking forward to the first non-preview version of this feature in future Java releases!


452: Key Encapsulation Mechanism (KEM) API

KEMs (key encapsulation mechanisms) are an encryption technique used to secure symmetric keys using public key cryptography. A new API introduced by Java 21 enables the use of KEM algorithms in higher-level protocols and in cryptographic schemes.

Examples of KEM algorithms include the RSA Key Encapsulation Mechanism (RSA-KEM) and the Elliptic Curve Integrated Encryption Scheme (ECIES). This new API allows security providers to implement KEM in Java or native code to security symmetric keys using asymmetric or public key cryptography.

449: Deprecate the Windows 32-bit x86 Port for Removal

The days of the Windows 32-bit x86 port are over: it is planned for removal in a future release, which is why JDK 21 moves it into the deprecated state. This shouldn’t come as a surprise, since Windows 10 (the latest OS to support 32-bit operation) will reach its End of Life in October 2025. For now, a suppressible error message will be displayed when an attempt is made to configure a build for Windows 32-bit x86 (x86-32).

451: Prepare to Disallow the Dynamic Loading of Agents

In order to improve integrity since running code shouldn’t be arbitrarily changed, a future release will disallow the dynamic loading of agents.

Agents are components that are able to alter application code while the application is running. They provide a means for tools such as profilers to instrument classes.

JDK 21 requires that the dynamic loading of agents is approved by the application’s owner. To prepare for disallowing the dynamic loading of agents in a later release, warnings will be issued in JDK 21 when agents are loaded dynamically into a running JVM.


Java 21 brings neat core language extensions that every Java developer should be aware of, from record patterns and sequenced collections to string templates. By finalizing virtual threads and previewing additional features around them, Java language developers also took another swing at making the lives of devs who develop multi-threaded code way easier. Finally, the Z Garbage Collector also received some improvements, and work is continued on providing an alternative for JNI, which is still in the preview stage.

Psst, here’s a pro tip! Whether you’re switching over to JDK 21 right away, still using Java 20, or sticking with a previous version, you can use Symflower to generate test templates and complete test suites, saving the hassle of writing them manually. These tests will be mathematically precise to cover all possible paths in your code, and you won’t have to worry about maintaining them either, as that’s fully automated. What’s not to like? Try Symflower in your IDE!

Make sure you never miss any of our upcoming content by signing up for our newsletter and by following us on Twitter, LinkedIn or Facebook!

| 2023-09-13