19.09.20237 min
Adam Kukołowicz

Adam KukołowiczCo-founderBulldogjob

Java 21 released! All new features explained

New LTS - Java 21 - brings 15 improvements, including syntax changes, new APIs and light threads.

Java 21 released! All new features explained

Welcome to Java 21, which will be actively supported for the next 5 years - because it is a Long Term Support version. It's worth taking a look at the features it brings. As usual, we will examine all the JEPs (Java Enhancement Proposal) introduced in the latest version, and there are as many as 15. In Java 20, I complained that it was boring, but this time there is a lot to go through.

Besides long-awaited changes, we have a lot of features marked as previews (which can still change slightly in the future) and in the incubator phase (which can change a lot, or never make it into stable Java at all). Let's see what Java 21 offers.

Pattern matching for switch and records

A big project that has been under development for several previous Java releases was adding pattern matching to the new switch syntax. In short, pattern matching involves testing an expression for certain characteristics. This seems to be very useful for the switch, which traditionally only dealt with values of a few basic types. Now we can check the type in a switch expression/statement:

static String formatterPatternSwitch(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();

Pattern matching also allows for additional testing of a provided value, with an expression that evaluates to Boolean.

static void testNew(Object obj) {
    switch (obj) {
        case String s when s.length() == 1 -> ...
        case String s                      -> ...

In addition to the more important features of switch, it is possible to handle the case when the value is null. More details are available in JEP 441.

Pattern matching also made it to the records, where it is used for quick value deconstruction, as outlined in JEP 440:

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

We no longer have to explicitly define variables to which we will directly assign values from the record. This syntax handles it. Also, you can certainly nest it, pulling values from deep within the record.

Lightweight Threads

Concurrency in Java is both a blessing and a curse. Historically, Java threads were also system threads, making them fairly heavy. This no longer fits with the modern approach and solutions like those in Go or Kotlin are becoming increasingly popular. Java creators decided it was time to tackle this and proposed virtual threads. Virtual threads are still run on system threads but are not directly associated with them, allowing a significantly larger number of virtual threads to be run and switched between without creating the same number of threads in the system. The thread API has been changed to reflect these changes, including a new syntax for creating threads - Thread.ofVirtual() for virtual, Thread.ofPlatform() for traditional. A virtual thread can, for example, be created like this:

Thread thread = Thread.ofVirtual().name("duke").unstarted(runnable)

You can find more information in JEP 444.

Ordered Collections

Java creators realized that the language has no type to signify a sequenced collection. That's why it looks strange when a List, where order is important, is a subclass of Collection, which does not care about sequence. Therefore, Java is introducing 3 new interfaces - SequencedSet, SequencedCollection, and SequencedMap, to sort this out. They will incorporate as follows:

Further changes - news from the incubator and preview.

String Templates

In 2023, people from the Java project realized that practically all modern languages (and even a few of the dusty ones) have syntax for string interpolation:

C#             $"{x} plus {y} equals {x + y}"
Visual Basic   $"{x} plus {y} equals {x + y}"
Python         f"{x} plus {y} equals {x + y}"
Scala          s"$x plus $y equals ${x + y}"
Groovy         "$x plus $y equals ${x + y}"
Kotlin         "$x plus $y equals ${x + y}"
JavaScript     `${x} plus ${y} equals ${x + y}`
Ruby           "#{x} plus #{y} equals #{x + y}"
Swift          "\(x) plus \(y) equals \(x + y)"

However, they are wary of this new feature as it can be dangerous and used for new system attacks. Therefore, they are proposing a new expression - named the template expression, which will look like this:

String name = "Joan";
String info = STR."My name is \{name}";

It may look a bit worse than competitors, but the STR is a template processor that can be replaced by any other. This means that you will be able to interpolate not only strings but anything you can imagine (including other types) if you use the API that creates new processors. This is a preview feature, described in JEP 430.

Unnamed Variables, Classes

You probably know the syntax from many languages ​​that allow you to ignore a variable by assigning it to _? The same function is in the preview of Java 21 (JEP 443). You can use it in record deconstruction via pattern matching or skip variables that will never be used. Here are examples of both:

r instanceof ColoredPoint(Point(int x, int _), Color _)

for (int i = 0, _ = sideEffect(); i < 10; i++) { ... i ... }

When it comes to unnamed classes and simplified methods, it is a preview feature aimed at beginners in Java (JEP 445). In short, the idea is that you don't have to write:

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

For simple programs, it's totally unnecessary and can deter future Java programming adepts. Instead, you can write:

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

Better, right? For Java developers it doesn't matter, but do you think it will help the language gain new fans?

Useful for concurrency

The Scoped Values ​​feature moves from the incubator to the preview phase. I described this feature when talking about Java 20, but I mention it again in the context of concurrency because JEP 446 confirms that this approach is useful for sharing values ​​in virtual threads. Scoped Values ​​are values ​​that can be passed for use in methods without passing them as method parameters. They work better than thread-local variables, mainly because they are stored only once and are only available for a specified period of time during execution. A bonus is that reading such a value is basically as fast as reading a local variable.

Another useful preview feature is structured concurrency. The StructuredTaskScope API allows for the creation of a task consisting of a set of concurrent subtasks and coordinating them as one entity. A subtask is executed in its own thread, with the result being collected together in the parent task. Here's an example from JEP 453:

Response handle() throws ExecutionException, InterruptedException {
    try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
        Supplier<String>  user  = scope.fork(() -> findUser());
        Supplier<Integer> order = scope.fork(() -> fetchOrder());

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

        // Here, both subtasks have succeeded, so compose their results
        return new Response(user.get(), order.get());

Other features and deprecations 

The ZGC garbage collector has been improved. ZGC is a garbage collector, designed for low latency and performance. It has now been modified so that garbage collection is generational, which is better for most applications. For more, have a look at JEP 439.

If you work with cryptography, you might be pleased with the new Key Encapsulation Mechanism API, which is used for safer storage of symmetric keys. More can be found in JEP 452.

The API for foreign functions and memory is still in the preview stage (JEP 442) - it is for interacting with non-JVM programs. The Vector API is reappearing in the incubator for the 6th time (JEP 448).

The JDK port for 32-bit Windows has been marked as for removal, thanks to JEP 449. Also, dynamic agent loading (java.lang.instrument) will start producing warnings from now on. In the future, such an option will be disabled, as mentioned in JEP 451.

What next for Java?

The new LTS, Java 21, brings a substantial package of changes. The syntax changes to modernize the language, but they are not a great revolution but rather a continuation of what was introduced in previous editions. An interesting change is virtual threads. Java seems to be betting on a new, lighter model of concurrency. I wonder if the Java community will accept it and how it will start using it.

Since we're talking about accepting novelties, I looked at the NewRelic report, which shows Java usage in January 2023 on instances they are monitoring.

It turns out that most projects are still running on Java 11, released in 2018, that is, 5 years ago. Java 17, released 2 years ago, is just starting to gain popularity. As you can see, it is not easy in the Java world. The good news is that updates to subsequent versions should be increasingly easier and maybe we will witness a moment when, when a fresh LTS is released, the previous version will be the most popular one.

For now, despite the six-month release cycle and the introduction of new features, the Java ecosystem is one of the least exciting on the market.