Java 21 released

Public version released Java 21. This release contains approximately 2500 closed tasks and 15 JEPs. Release Notes can be viewed Here. API changes – Here.

Java 21 is an LTS release, which means it will have updates at least 5 years since release.

You can download JDK 21 from these links:

Here is a list of JEPs that made it into Java 21.


Pattern Matching for switch (JEP 441)

Pattern matching for switch was finally finalized and became a stable language construct. Recall that it appeared in Java 17 and was able to preview four releases: 17, 18, 19 And 20.

New pattern matching significantly expands operator capabilities switch. Since Java 1.0, switch only supported comparisons with primitive constants. Later the list of types was expanded (Java 5 – enumerations, Java 7 – strings), but in branches case there could still only be constants.

Now switch supports in branches case so called patterns:

Object obj = …
return switch (obj) {
    case Integer i -> String.format("int %d", i);
    case Long l -> String.format("long %d", l);
    case Double d -> String.format("double %f", d);
    case String s -> String.format("String %s", s);
    default -> obj.toString();

Patterns can be conditionalized using the new keyword when:

Object obj = …
return switch (obj) {
    case Integer i when i > 0 -> String.format("positive int %d", i);
    case Integer i -> String.format("int %d", i);
    case String s -> String.format("String %s", s);
    default -> obj.toString();

Matching support has also been added null. You can do this using an explicit separate branch case null:

Object obj = …
switch (obj) {
    case null -> System.out.println("Null");
    case String s -> System.out.println("String: " + s);
    default -> System.out.println("Other");

If the branch case null is missing, then switch with passed into it null will always throw away NullPointerException (even if there is a thread default):

Object obj = null;
switch (obj) { // NullPointerException
    case String s -> System.out.println("String: " + s);
    default -> System.out.println("Other");

Branches null And default can be combined with each other:

String str = …
switch (str) {
    case "Foo", "Bar" -> System.out.println("Foo or Bar");
    case null, default -> System.out.println("Null or other");

The new pattern matching has a number of limitations.

First of all, everything switch (except those that were correct before Java 21) must be comprehensive. Those. branches should cover all possible cases:

Object obj = …
switch (obj) { // error: the switch statement does not cover all possible input values
    case String s -> System.out.println(s.length());
    case Integer i -> System.out.println(i);

The example above can be corrected by adding a branch Object o or default.

Secondly, all branches case must be arranged in such an order that there is no dominant branches:

return switch (obj) {
    case CharSequence cs ->
        "sequence of length " + cs.length();
    case String s -> // error: this case label is dominated by a preceding case label
        "string of length " + s.length();
    default -> "other";

Because CharSequence this is a broader type than Stringthen its branch should be located below.

Thirdly, several patterns in one branch will not work:

return switch (obj) {
    case String s, Integer i -> "string or integer"; // error: illegal fall-through from a pattern
    default -> "other";

Those. It is not yet possible to test multiple types in one branch (although the language grammar allows this). This can only be circumvented by turning on preview mode and replacing s And i to underscores (see JEP about unnamed variables below).

Overall, the new pattern matching significantly increases the expressiveness of the language. It pairs especially well with recordings. We will look at record patterns separately, since they have their own JEP (see the next section).

Record Patterns (JEP 440)

A separate type of patterns are post patterns. They appeared in Java 19 in preview mode and became stable in Java 21.

Record patterns allow you to deconstruct record values ​​in an extremely compact way:

record Point(int x, int y) {}

static void printSum(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        System.out.println(x + y);

Or through the operator switch:

static void printSum(Object obj) {
    switch (obj) {
        case Point(int x, int y) -> System.out.println(x + y);
        default -> System.out.println("Not a point");

The special power of post patterns is that they can be nested:

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)) {

Using varyou can shorten the code even further:

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

Recording patterns go well with patterns like:

record Box(Object obj) {}

static void test(Box box) {
    switch (box) {
        case Box(String s) -> System.out.println("string: " + s);
        case Box(Object o) -> System.out.println("other: " + o);

Inference of generic post types is supported:

record Box<T>(T t) {}

static void test(Box<Box<String>> box) {
    if (box instanceof Box(Box(var s))) { // Infers Box<Box<String>>(Box<String>(String s))
        System.out.println("String " + s);

Unfortunately, post patterns can only be used in instanceof And switchbut cannot be used on their own:

static void usePoint(Point p) {
    Point(var x, var y) = p; // Не сработает
    // Use x and y

Let’s hope that someday they will add this feature.

String Templates (Preview) (JEP 430)

String templates are a new syntactic feature that allows you to embed expressions in strings:

int x = 10;
int y = 20;
String str = STR."\{x} plus \{y} equals \{x + y}";
// В str будет лежать "10 + 20 equals 30"

Thus, string interpolation appeared in Java, which has long been present in many other well-known programming languages. However, in Java it only works in mode preview, i.e. it can only be used in Java 21 with the flag enabled --enable-preview.

The implementation of string templates in Java is different from most implementations in other languages: in Java, a string template is actually turned into an object first java.lang.StringTemplateand then CPUimplementing java.lang.StringTemplate.Processor, converts this object to a string (or an object of another class). In the example above STR."…" is nothing more than a shortened version of the following code:

StringTemplate template = RAW."\{x} plus \{y} equals \{x + y}";
String str = STR.process(template);

STR is the standard and most commonly used processor that performs simple value substitution into a pattern and returns a concatenated string. STR is implicitly imported into any source file, so it can be used without import.

RAW is a processor that does nothing with StringTemplate and just returns it. It is usually not used because… in practice, few people need raw representations of templates, but rather interpolation results in the form of ready-made objects.

Processors were introduced to allow customization of the interpolation process. For example, another standard processor FMT supports formatting using specifiers defined in java.util.Formatter:

double length = 46;
System.out.println(FMT."The length is %.2f\{height} cm");
// The length is 46.00 cm

Processors do not necessarily have to return String. Here is the general method signature process() interface Processor:

public interface Processor<R, E extends Throwable> {
    R process(StringTemplate stringTemplate) throws E;

This means that it is possible to implement a processor that will do almost anything and return anything. For example, a hypothetical processor JSON will create JSON objects directly (no intermediate object String) and still support quote escaping:

JSONObject doc = JSON."""
        "name":    "\{name}",
        "phone":   "\{phone}",
        "address": "\{address}"

If in name, phone or address will contain quotes, they will not spoil the object, because the processor will replace " on \".

Or, for example, a processor SQL will create PreparedStatements, protecting against SQL Injection attacks:

PreparedStatement ps = SQL."SELECT * FROM Person p WHERE = \{name}";

Thus, string patterns are a much more powerful tool than simple concatenating string interpolation. They solve not only the problem of easily embedding expressions in strings and increase readability, but also improve the security and flexibility of programs.

Unnamed Patterns and Variables (Preview) (JEP 443)

Another innovation in preview mode: you can now declare so-called nameless variables and patterns. This is done using the underscore character (_). This is often necessary when a variable or pattern is not being used:

int acc = 0;
for (Order _ : orders) {
    if (acc < LIMIT) {
        … acc++ …

In the example above, the fact that the element exists is important, but the variable itself is not needed. Therefore, in order not to come up with a name for this variable, an underscore was used instead of a name.

A fairly common example of the need for nameless variables is a block catch with an unused exception:

String s = …
try {
    int i = Integer.parseInt(s);
} catch (NumberFormatException _) {
    System.out.println("Bad number: " + s);

A complete list of cases in which unnamed variables can be used:

  • Local variable in block,
  • Declaring a resource in try-with-resources,
  • Heading for statement
  • Improved Loop Header for,
  • Exception in block catch,
  • Lambda expression parameter,
  • Pattern variable (see below).

The attentive reader will notice that the list above does not include method parameters. Indeed, they cannot be nameless, and for any methods (both interfaces and classes) you still always need to specify parameter names.

Underscores can also be used to indicate nameless patterns:

if (r instanceof ColoredPoint(Point(int x, int y), _)) {
    // Используются только x и y

Here the developer only needed the coordinates of the point, but not its color. Without the unnamed pattern, it would have to declare an unused type variable Color and come up with a name for her:

if (r instanceof ColoredPoint(Point(int x, int y), Color c)) { // Warning: unused c
    // Используются только x и y

This code is less readable and makes it less easy to focus on the main thing (coordinates). Also, some IDEs would highlight an unused variable cwhich is another additional inconvenience.

It is also possible to announce unnamed pattern variables:

if (r instanceof ColoredPoint(Point(int x, int y), Color _)) {

Unnamed patterns and pattern variables work well with switch:

switch (box) {
    case Box(RedBall _), Box(BlueBall _) -> processBox(box);
    case Box(GreenBall _)                -> stopProcessing();
    case Box(_)                          -> pickAnotherBox();

Overall, pattern matching and unnamed patterns together have great synergy and allow you to write really powerful, compact and expressive designs.

Unnamed Classes and Instance Main Methods (Preview) (JEP 445)

Now in preview mode you can run programs with methods main()which are not public static and which do not have a parameter String[] args:

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

In this case, the JVM itself will create an instance of the class (it must haveprivate constructor without parameters) and will call its method main().

The startup protocol will choose the method main() according to the following priority:

  1. static void main(String[] args)
  2. static void main()
  3. void main(String[] args)
  4. void main()

In addition, you can write programs without declaring a class at all:

String greeting = "Hello, World!";

void main() {

In this case, an implicit nameless class will be created (not to be confused with an anonymous class), which will own the method main() and other top-level declarations in the file:

// class <some name> { ← неявно
String greeting = "Hello, World!";

void main() {
// }

The unnamed class is synthetic And final. His simple name is an empty string:

void main() {
    System.out.println(getClass().isUnnamed()); // true
    System.out.println(getClass().isSynthetic()); // true
    System.out.println(getClass().getSimpleName()); // ""
    System.out.println(getClass().getCanonicalName()); // null

Wherein Name class is the same as the file name, but this behavior is not guaranteed.

This simplification of running Java programs was done for two purposes:

  1. Facilitate the language learning process. A beginner who has just started learning Java should not be overwhelmed with everything at once, but concepts should be introduced gradually, starting with the basic ones (variables, loops, procedures) and gradually moving on to more advanced ones (classes, scopes).
  2. Make it easier to write short programs and scripts. The number of ceremonies for them should be kept to a minimum.


Virtual Threads (JEP 444)

Virtual streams that have been developed within the project for many years Loom and appeared in Java 19 in preview mode, have now finally become stable.

Virtual threads, unlike operating system threads, are lightweight and can be created in huge numbers (millions of copies). This feature should make it much easier to write concurrent programs, since it will allow you to use a simple “one request – one thread” (or “one task – one thread”) approach without resorting to more complex asynchronous or reactive programming. At the same time, migration of existing code to virtual threads should be as simple as possible, because virtual threads are instances of an existing class java.lang.Thread and are almost completely compatible with classic streams: they support stack traces, interrupt(), ThreadLocal etc.

Virtual threads are implemented on top of regular threads and exist only for the JVM, not for the operating system (hence the name “virtual”). The thread on which the virtual thread is currently running is called the host thread. If platform threads rely on the operating system scheduler, then the scheduler for virtual threads is ForkJoinPool. When a virtual thread blocks on a blocking operation, it is unmounted from its carrier thread, allowing the carrier thread to mount another virtual thread and continue running. This mode of operation and the low cost of virtual streams allow them to scale very well. However, there are currently two exceptions: synchronized blocks and JNI. When executed, the virtual thread cannot be unmounted because it is bound to its host thread. This limitation may prevent scaling. Therefore, if you want to maximize the potential of virtual threads, it is recommended to avoid synchronized blocks and JNI operations that execute frequently or take a long time.

While virtual streams are attractive, you don’t have to stick to them and always avoid classic streams. For example, for tasks that use the CPU intensively and for a long time, regular threads are better suited. Or if you need a stream that is not demonthen you will also have to use a regular thread, because a virtual thread is always a daemon.

To create virtual threads and work with them, the following API has appeared:

Support for virtual streams has also been added in the JDK tools (debugger, JVM TI, Java Flight Recorder).

Sequenced Collections (JEP 431)

Three new interfaces have appeared SequencedCollection, SequencedSet And SequencedMap.

SequencedCollection is the heir Collection and is a collection with a set order of elements. Such collections are LinkedHashSet and all implementations List, SortedSet And Deque. These collections have a common element sequence property, but before Java 21 their common parent was Collectionwhich is too generic an interface and does not contain many of the methods specific to sequences (getFirst(), getLast(), addFirst(), addLast(), reversed() etc). Moreover, in the collections described above such methods were inconsistent with each other (for example, list.get(0) against sortedSet.first() against deque.getFirst()), or were completely absent (for example, linkedHashSet.getLast()).

SequencedCollection closed this hole in the hierarchy and brought the API to a common denominator:

interface SequencedCollection<E> extends Collection<E> {
    E getFirst();
    E getLast();
    void addFirst(E);
    void addLast(E);
    E removeFirst();
    E removeLast();
    SequencedCollection<E> reversed();

Now you no longer have to think about how to get the last element for a specific collection, because there is a universal method getLast()which also has ArrayListand TreeSetand ArrayDeque.

Of particular interest is the method reversed(), which returns the view of the collection with the reverse order. This makes reverse collection traversal much more concise:

var linkedList = new LinkedList<>(…);

// До Java 21
for (var it = linkedList.descendingIterator(); it.hasNext();) {
    var e =;

// С Java 21
for (var element : linkedList.reversed()) {

For LinkedHashSet There was no effective way to bypass it at all.

An interface was introduced for sequential sets SequencedSet:

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();

Its implementations are LinkedHashSet and heirs SortedSet.

We also introduced an interface SequencedMap:

interface SequencedMap<K,V> extends Map<K,V> {
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
    V putFirst(K, V);
    V putLast(K, V);
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    SequencedMap<K,V> reversed();

Its implementations are LinkedHashMap and heirs SortedMap.

Scoped Values ​​(Preview) (JEP 446)

Scoped Values, which appeared in Java 20 V incubation statushave now become Preview API.

New class ScopedValue allows you to exchange immutable data without passing it through method arguments. It is an alternative to the existing class ThreadLocal.

Classes ThreadLocal And ScopedValue are similar in that they solve the same problem: pass the value of a variable within one thread (or tree of threads) from one place to another without using an explicit parameter. When ThreadLocal for this purpose the method is called set()which puts the value of a variable for a given thread, and then the method get() called from another location to obtain the value of a variable. This approach has a number of disadvantages:

  • Uncontrolled mutability (set() can be called anytime and from anywhere).
  • Unlimited lifetime (the variable will be cleared only when the thread finishes execution or when it is called ThreadLocal.remove()but it is often forgotten).
  • High cost of inheritance (child threads are always forced to make a full copy of the variable, even if the parent thread will never change it).

These problems are exacerbated by the advent of virtual threads, which can be created in much larger numbers than regular threads.

ScopedValue free from the above disadvantages. Unlike ThreadLocal, ScopedValue has no method set(). A value is associated with an object ScopedValue by calling another method where(). Next the method is called run()during which this value can be obtained (via the method get()), but cannot be changed. Once the method is executed run() ends, the value is unbound from the object ScopedValue. Since the value does not change, the problem of expensive inheritance is also solved: child threads do not have to copy a value that remains constant during its lifetime.

Usage example ScopedValue:

private static final ScopedValue<FrameworkContext> CONTEXT = ScopedValue.newInstance();

void serve(Request request, Response response) {
    var context = createContext(request);
    ScopedValue.where(CONTEXT, context)
               .run(() -> Application.handle(request, response));

public PersistedObject readKey(String key) {
    var context = CONTEXT.get();
    var db = getDBConnection(context);

Generally ScopedValue is the preferred replacement ThreadLocal, because imposes on the developer a secure unidirectional model of working with immutable data. However, this approach is not always inapplicable for some problems, and for them ThreadLocal may be the only possible solution.

Structured Concurrency (Preview) (JEP 453)

Another API that was previously in incubation status (Java 19 And 20), and now it has become Preview API – this is Structured Concurrency.

Structured Concurrency is a multi-threaded programming approach that borrows principles from single-threaded structured programming. The main idea of ​​this approach is the following: if a task is split into several concurrent subtasks, then these subtasks are reunited in the code block of the main task. All subtasks are logically grouped and organized into a hierarchy. Each subtask is limited in lifetime by the scope of the main task’s code block.

At the center of the new API is the class StructuredTaskScopewhich has two main methods:

  • fork() – creates a subtask and runs it in a new virtual thread,
  • join() – waits until all subtasks are completed or until there is no scope stopped.

Usage example StructuredTaskScopewhich shows a task that runs two subtasks in parallel and waits for the result of their execution:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Supplier<String> user = scope.fork(() -> findUser());
    Supplier<Integer> order = scope.fork(() -> fetchOrder());

    scope.join()            // Join both forks
         .throwIfFailed();  // ... and propagate errors

    return new Response(user.get(), order.get());

It may seem that exactly the same code could be written using the classic ExecutorService And submit()but StructuredTaskScope There are several fundamental differences that make the code safer:

  • The lifetime of all subtask threads is limited by the block’s scope try-with-resources. Method close() is guaranteed not to complete until all subtasks complete.
  • If one of the operations findUser() And fetchOrder() fails, then the other operation is canceled automatically if it has not yet completed (in the case of a policy ShutdownOnFailureothers are possible).
  • If the main thread is interrupted while waiting join()then both operations findUser() And fetchOrder() are canceled when leaving the block.
  • The thread dump will show a hierarchy: threads executing findUser() And fetchOrder()will appear as children of the main thread.

Structured Concurrency should make it easier to write thread-safe programs thanks to a familiar structured approach.

Foreign Function & Memory API (Third Preview) (JEP 442)

Foreign Function & Memory API, which became a preview in Java 19, continues to be in this status. The API is in the package java.lang.foreign.

Let us remind you that the FFM API has been developed in the project for many years Panama with the goal of replacing JNI. IN Java 22 The API will exit the preview state.

Vector API (Sixth Incubator) (JEP 448)

Vector API in module jdk.incubator.vectorwhich appeared already in Java 16, remains in incubation status for the sixth time. This release contains only minor API changes, bug fixes and performance improvements.

The vector API will remain in the incubator until the necessary features of the project Valhalla will not become preview.

Key Encapsulation Mechanism API (JEP 452)

In the package javax.crypto a new API has appeared that implements key encapsulation mechanism.

Key Encapsulation Mechanism (KEM) is a modern cryptographic technique that allows symmetric keys to be exchanged using asymmetric encryption. While in the traditional technique a symmetric key is generated randomly and encrypted using the public key (which requires padding), in KEM the symmetric key is derived from the public key itself.

In Java, the KEM API consists of three main classes.

KEM – API entry point. He has a method getInstance()returning an object KEM for the specified algorithm.

Encapsulator – represents an encapsulation function that is called by the sender. This class has a method encapsulate()which takes a public key and returns a secret key, as well as a key encapsulation message (which is sent to the receiving party).

Decapsulator – decapsulation function, which is called by the receiving side. The class has a method decapsulate(), which takes a key encapsulation message and returns a secret key. Thus, both parties now have the same symmetric key, with which they can continue to exchange data using conventional symmetric encryption.

An example of generating a symmetric key and transmitting it:

// Receiver side
var kpg = KeyPairGenerator.getInstance("X25519");
var kp = kpg.generateKeyPair();

// Sender side
var kem1 = KEM.getInstance("DHKEM");
var sender = kem1.newEncapsulator(kp.getPublic());
var encapsulated = sender.encapsulate();
var k1 = encapsulated.key();

// Receiver side
var kem2 = KEM.getInstance("DHKEM");
var receiver = kem2.newDecapsulator(kp.getPrivate());
var k2 = receiver.decapsulate(encapsulated.encapsulation());

assert Arrays.equals(k1.getEncoded(), k2.getEncoded());

An interface has also been added for KEM KEMSpiallowing you to provide custom implementations of KEM algorithms.


Generational ZGC (JEP 439)

The ZGC garbage collector, which appeared in Java 15, added generational support. Generations in ZGC are currently disabled by default and require a key to enable them -XX:+ZGenerational:

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

In future versions of Java, generational mode will be the default, and the key -XX:+ZGenerational will no longer be required.

Generations in ZGC should improve the performance of Java programs because… Young objects, which tend to die early under the weak generational hypothesis, will be collected more often, and old objects more rarely. However, ZGC performance should not suffer from this: response times should still be ultra-low (< 1ms) and gigantic heap sizes (several terabytes) should continue to be supported.

Let us recall that also work in progress over generational support in another garbage collector Shenandoah, similar in characteristics to ZGC. However, I didn’t manage to get into Java 21 Generational Shenandoah.

The default garbage collector is still G1. It became the default garbage collector in Java 9 (before it, Parallel GC was the default)

Prepare to Disallow the Dynamic Loading of Agents (JEP 451)

When loading agents dynamically, a warning is now issued:

WARNING: A {Java,JVM TI} agent has been loaded dynamically (file:/u/bob/agent.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release

An agent is a component that can modify (instrument) the code of a Java application while running. Agent support was introduced in Java 5 to make it possible to write advanced tools like profilers that need to add event emission to classes, or AOP libraries. Command line options were required to enable agents -javaagent or -agentlibso all agents could then only be turned on explicitly when the application started.

However, in Java 6 there was Attach API, which, among other things, made it possible to load agents dynamically directly into the running JVM. Thanks to this, libraries were able to connect to the application and quietly change classes without the consent of the application owner. Moreover, not only application classes can change, but also JDK classes. Thus, strict encapsulation, which is one of the cornerstones of Java, is compromised.

To close such a potentially dangerous hole, in Java 9, along with the advent of modules, it was proposed to disable dynamic loading of agents by default. However, then it was decided to postpone such a radical decision indefinitely in order to give the authors of the tools time to prepare. As a result, the change has survived to this day, and was implemented only in Java 21, but in in the form of a warning.

To suppress the warning, you must run the JVM with the option -XX:+EnableDynamicAgentLoadingor load agents at JVM startup, explicitly listing them using options -javaagent or -agentlib.

Future versions of Java plan to completely disable dynamic loading by default, and it will no longer work without -XX:+EnableDynamicAgentLoading.

Deprecate the Windows 32-bit x86 Port for Removal (JEP 449)

The 32-bit port of OpenJDK for Windows has become deprecated for removal. In the future there are plans to get rid of it completely.

Removing the port will speed up the development of the platform. Another reason was the lack of a native implementation of virtual threads on the 32-bit version of JDK 21 for Windows: virtual threads in this version are implemented through platform threads.

Full list of JEPs included in JDK 21, starting with JDK 17: link.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *