Skip to content

What is New in Java 21 (LTS), with Practical Examples

Updated:

In this blog post, we will find out what is new in Java 21, the latest Long-Term Support (LTS) version, which reached General Availability (GA) on 19.09.2023, but also what has been released since Java 17. Special focus is given to developer-oriented features.

Table of Contents

Features Overview

Finalized Features

Preview And Incubator Features

Finalized Features Breakdown

We will be focusing on finalized features, especially those that have a more practical application and are targeted towards developers.

Pattern Matching for switch

Use type matching instead of instanceof, including for null checks. You can also expand composite types, such as the Point record in the example below. For blocks of code you can use yield. A switch statement can immediately return a result.

Before Java 21:

record Point(int x, int y) {};

String format(Object obj) {
    if (obj == null) {
        return "Null!";
    }
    if (obj instanceof Integer i) {
        System.out.println("Test extra line!");
        return String.format("int %d", i);
    }
    if (obj instanceof String s) {
        return String.format("String %s", s);
    }
    if (obj instanceof Point p) {
        return String.format("Point(%d,%d)", p.x, p.y);
    }
    return "Unknown!";
}

In Java 21:

String formatUsingPatternMatching(Object obj) {
    return switch (obj) {
        case null -> "Null!";
        case Integer i -> {
            System.out.println("Test extra line!");
            yield String.format("int %d", i);
        }
        case String s -> String.format("String %s", s);
        case Point(int x, int y) -> String.format("Point(%d,%d)", x, y);
        case "foo", "FOO" -> "Foo!";
        default -> "Unknown!";
        // could also do: case null, default -> "Unknown or null!";
    };
}

You can further refine the cases by using the when keyword. Unused variables can be replaced with a _ if you enable the preview feature.

void refineCaseWithWhen(Point point) {
    switch (point) {
        case Point p when isZero(p) -> System.out.println("Zero Point");
        case Point(int x, int _) -> System.out.println("Point X: " + x);
        case null -> System.out.println("There is no Point!");
    }
}

boolean isZero(Point p) {
    return p.x == 0 && p.y == 0;
}

Record Patterns

Records can be deconstructed by listing their components as follows. This can be done in many nested levels of records.

int recordPattern(Point point) {
    return switch (point) {
        case Point(int x, int y) -> x + y;
        case null -> 0;
    };
}

Virtual Threads

Virtual threads preserve the familiar and easy to debug thread-per-request programming model, without being limited to the number of OS threads. The name virtual means that they are not tied to a particular OS thread, in contrast to platform threads.

Code running on a virtual thread will occupy the OS thread only when necessary. This allows for other virtual threads to take over when it’s blocked (work-stealing), resulting in scalability comparable to asynchronous or reactive style programming. This is completely transparent to developers while optimally using the available hardware capacity.

In practice, using virtual threads is as simple as changing the Executor implementation in existing code. Note that they should never be pooled because they are cheap and ideal for short-lived operations.

void run(List<Runnable> runnables) {
    // Before: Execute runnables with platform threads (bounded)
    int threads = Runtime.getRuntime().availableProcessors();
    try (var executor = Executors.newFixedThreadPool(threads)) {
        runnables.forEach(runnable -> executor.submit(runnable));
    }
    // Java 21: Execute runnables with virtual threads (unbounded)
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        runnables.forEach(runnable -> executor.submit(runnable));
    }
}

If you are using Spring Boot 3.2 or higher, you can simply enable them by setting the new property spring.threads.virtual.enabled to true.

Sequenced Collections

In order to “fix” inconsistencies such as the ones below across Collection APIs regarding accessing elements in a well defined order, Sequenced Collections are introduced (new interfaces).

               First element	                Last element
List	       list.get(0)	                    list.get(list.size() - 1)
Deque	       deque.getFirst()             	deque.getLast()
SortedSet	   sortedSet.first()	            sortedSet.last()
LinkedHashSet  linkedHashSet.iterator().next()	// missing

Using Sequenced Collections:
               list.getFirst()	                list.getLast()
               // See the rest of the methods in the interfaces below

The following diagram and implementations from “JEP 431: Sequenced Collections” demonstrates how three new interfaces have been integrated into the existing Collections ecosystem.

JEP 431: Sequenced Collections

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();
}

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

Other Finalized Features

For completeness, I am listing here the rest of the finalized features that are not practically or immediately applicable for the majority of Java developers. Please read the official specification for further details.

Generational ZGC

The specification promises:

Generational ZGC is not yet the default, but that is the goal of a future release. If you would like to enable it, use the following command-line option: java -XX:+UseZGC -XX:+ZGenerational

Key Encapsulation Mechanism API

If you are developing security libraries or tools, or you are curious about Post-Quantum Cryptography, read more here.

Prepare to Disallow the Dynamic Loading of Agents

If you are using dynamically loaded agents, read more here.

Deprecate the Windows 32-bit x86 Port for Removal

Java keeps evolving. Read more here.

Notable Features Since Java 17 (LTS)

If you are upgrading from Java 17, keep reading for the most relevant changes, or have a look here.

Simple Web Server

All you need to do is run jwebserver. Use --help for usage instructions.

$ jwebserver
  Binding to loopback by default. For all interfaces use "-b 0.0.0.0" or "-b ::".
  Serving /cwd and subdirectories on 127.0.0.1 port 8000
  URL: http://127.0.0.1:8000/

Code Snippets in Java API Documentation

Improve your documentation using @snippet as in the following example.

/**
 * This is how you can use the {@code Point} record:
 * {@snippet :
 * boolean isZero(Point p) {
 *  return p.x == 0 && p.y == 0;
 * }
 * }
 */

UTF-8 by Default

Deprecate Finalization for Removal