A Deep Dive into the New Features of Java 21

Java 21, released on September 19, 2023, is the latest Long-Term Support (LTS) version that introduces several impactful features aimed at enhancing developer productivity, code readability, and performance. This article will explore the new features and enhancements introduced in Java 21.

1. Record Patterns (JEP 440)

Record patterns were included as preview features in Java 19 and 20. With Java 21, they are out of the preview and include some refinements. This feature extends the existing pattern-matching capability to destructure record class instances, enabling more sophisticated data queries. Here’s an example:

record Point(int x, int y) {}

public static int beforeRecordPattern(Object obj) {
    int sum = 0;
    if (obj instanceof Point p) {
        int x = p.x();
        int y = p.y();
        sum = x + y;
    }
    return sum;
}

public static int afterRecordPattern(Object obj) {
    if (obj instanceof Point(int x, int y)) {
        return x + y;
    }
    return 0;
}

2. Pattern Matching for switch (JEP 441)

Pattern matching for the switch statement was initially introduced in JDK 17 and has been refined in subsequent versions. The goal of this feature is to allow patterns in switch case labels, improving the expressiveness of switch statements and expressions. Here’s an example:

class Account {
    double getBalance() {
        return 0;
    }
}

class SavingsAccount extends Account {
    double getSavings() {
        return 100;
    }
}

class TermAccount extends Account {
    double getTermBalance() {
        return 1000;
    }
}

class CurrentAccount extends Account {
    double getCurrentAccountBalance() {
        return 10000;
    }
}

static double getBalanceWithOutSwitchPattern(Account account) {
    double balance = 0;
    if (account instanceof SavingsAccount sa) {
        balance = sa.getSavings();
    } else if (account instanceof TermAccount ta) {
        balance = ta.getTermBalance();
    } else if (account instanceof CurrentAccount ca) {
        balance = ca.getCurrentAccountBalance();
    }
    return balance;
}

static double getBalanceWithSwitchPattern(Account account) {
    return switch (account) {
        case SavingsAccount sa -> sa.getSavings();
        case TermAccount ta -> ta.getTermAccount();
        case CurrentAccount ca -> ca.getCurrentAccount();
        default -> 0;
    };
}

3. Virtual Threads (JEP 444)

Virtual threads are JVM-managed lightweight threads that help in writing high-throughput concurrent applications. With Java 21, virtual threads are ready for production use. Think of them as "threads for humans" cheap to create, easy to use, and able to scale to millions of concurrent tasks.

Here’s an example of how to create a thousands of virtual thread:

public class ManyVirtualThreads {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 20000; i++) {
            Thread.ofVirtual().start(() -> {
                try {
                    Thread.sleep(100); // Simulate I/O delay
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }

        // Give all threads time to finish
        Thread.sleep(500);
        System.out.println("Launched 20,000 virtual threads!");
    }
}

4. String Templates (JEP 430)

String Templates, introduced as a preview feature in Java 21, offer a more readable and concise way to construct strings with embedded expressions.​ String Templates let you embed expressions directly into strings, similar to other other languages like JavaScript (`Hello, ${name}` ) or Python (f"Hello, {name}").

  • STR is a built-in template processor in Java 21 (part of java.lang.template).

  • Templates use the syntax: \{expression} inside a double-quoted template.

  • You can also create custom processors for things like SQL, JSON, HTML, etc.

This type of syntax simplifies string concatenation and enhances code clarity.

mport static java.lang.StringTemplate.STR;

public class StringTemplateExample {
    public static void main(String[] args) {
        String name = "Java";
        int version = 21;
        //Before
        String msg = "Welcome to " + name + " world, version " + version; 
        //with JEP 430
        String message = STR."Welcome to \{name} world, version \{version}!";
        System.out.println(message);

        //with JEP 430, formatting template
        String user = "John Doe";
        double balance = 1234.567;

        String message1 = STR."User: \{user}, Balance: \{String.format("%.2f", balance)}";
        System.out.println(message1);
    }
}

5. Structured Concurrency (JEP 446)

Structured Concurrency is a Java 21 preview feature introduced in JEP 446 that brings clarity and reliability to concurrent programming. It does this by:

  • Treating multiple tasks running in parallel as a single unit of work

  • Ensuring that child tasks are properly scoped, managed, and cleaned up

  • Reducing the chance of thread leaks or uncontrolled parallelism

Problem Before Structured Concurrency

With traditional threading (e.g., using ExecutorService), managing lifecycles of concurrent tasks is manual and error-prone. Issues include:

  • Forgetting to shutdown() an executor

  • Losing track of running threads

  • Inconsistent error handling between tasks

The Structured Concurrency Solution

Java 21 introduces:

  • StructuredTaskScope – a scoped task manager

  • Subclasses like StructuredTaskScope.ShutdownOnFailure and ShutdownOnSuccess

Now one can:

  • Start multiple subtasks

  • Wait for all to complete

  • Or cancel them automatically if one fails or completes

import java.util.concurrent.*;
import java.util.concurrent.StructuredTaskScope;

public class StructuredConcurrencyExample {

    static String fetchFromServiceA() throws InterruptedException {
        Thread.sleep(500); // Simulate latency
        return "Data from Service A";
    }

    static String fetchFromServiceB() throws InterruptedException {
        Thread.sleep(300); // Simulate latency
        return "Data from Service B";
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        try (var scope = new StructuredTaskScope.ShutdownOnSuccess<String>()) {
            // Fork both tasks
            StructuredTaskScope.Subtask<String> taskA = scope.fork(() -> fetchFromServiceA());
            StructuredTaskScope.Subtask<String> taskB = scope.fork(() -> fetchFromServiceB());

            // Wait until one of them succeeds
            scope.join();             // Wait for tasks to finish
            scope.throwIfFailed();    // Rethrow if any failed

            // Get the result of the first successful task
            String result = scope.result();
            System.out.println("First successful result: " + result);
        }
    }
}

This approach ensures that failures are handled gracefully, preventing orphaned threads

6. Sequenced Collections (JEP 431)

Java 21 introduces sequenced collections, a new set of interfaces in the Java Collections Framework that provides a well-defined encounter order for elements. This is ideal for collections where the order of elements matters, such as lists, queues, and ordered sets/maps.

Java 21 adds three new interfaces:

  • SequencedCollection<E> – base interface for all sequenced collections.

  • SequencedSet<E> – for sets that maintain order.

  • SequencedMap<K,V> – for maps with predictable key order.

These interfaces:

  • Allow access to first and last elements (getFirst(), getLast()).

  • Support reversed views of collections (reversed()).

  • Enable adding/removing elements from both ends, like a deque.

import java.util.*;

public class SequencedCollectionExample {
    public static void main(String[] args) {
        // Works with any List (like ArrayList)
        SequencedCollection<String> fruitsBasket = new ArrayList<>();

        fruitsBasket.addFirst("Apple");
        fruitsBasket.addLast("Banana");
        fruitsBasket.addLast("Orange");

        System.out.println("Original Order of fruits: " + fruitsBasket);
        System.out.println("First Fruit: " + fruitsBasket.getFirst());
        System.out.println("Last Fruit: " + fruitsBasket.getLast());

        // Reversed View
        SequencedCollection<String> reversedBasket = fruitsBasket.reversed();
        System.out.println("Reversed Order of fruits: " + reversedBasket);
    }
}

This feature enhances the flexibility of collections by providing more control over element order.

7. Unnamed Patterns and Variables (JEP 443)

JEP 443 introduces Unnamed Patterns and Variables in Java 21 as a preview feature. This makes pattern matching and variable declarations cleaner and more expressive, especially when you don’t need to name a variable. Often in Java, you destructure or match against types but don’t actually use the variable. Previously, you'd have to invent a meaningless name like _ or ignored.

    record Point(double x, double y) {}
    enum Color { RED, GREEN, BLUE }
    record ColoredPoint(Point pt, Color clr) {}

    double getDistanceBeforeUnnamedVariable(Object obj1, Object obj2) {
        if (obj1 instanceof ColoredPoint(Point pt1, Color clr1) &&
            obj2 instanceof ColoredPoint(Point pt2, Color clr2)) {
        return java.lang.Math.sqrt(
            java.lang.Math.pow(pt2.x - pt1.x, 2) +
            java.lang.Math.pow(pt2.y - pt1.y, 2));
        } else {
            return 0;
        }
    }

    double getDistanceAfterUnnamedVariable(Object obj1, Object obj2) {
        if (obj1 instanceof ColoredPoint(Point pt1, _) &&
            obj2 instanceof ColoredPoint(Point pt2, _)) {
        return java.lang.Math.sqrt(
            java.lang.Math.pow(pt2.x - pt1.x, 2) +
            java.lang.Math.pow(pt2.y - pt1.y, 2));
        } else {
            return 0;
        }
    }

8. Unnamed Classes and Instance Main Methods (JEP 445)

JEP 445 introduces unnamed classes and instance main methods, a preview feature in Java 21. The goal is to simplify the creation of simple Java programs, especially for beginners or quick prototypes or scripting. Traditionally, every Java program needs to be wrapped in a class with a public static void main(String[] args) method. JEP 445 removes some of that boilerplate:

  • You can write Java programs without declaring a class.

  • The entry point doesn't need to be static.

  • This encourages interactive learning and rapid development.

//BEFORE JEP 445 - HelloWorld.java
public class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello World! - I am from Named Class");
  }
} 
//AFTER JEP 445 - HelloWorld.java
void main() {
    System.out.println("Hello World! - I am from Unnamed Class!");
}

When you run such a program, the compiler wraps your code in a class automatically. For instance, it behaves like:

class HelloWorld {
    void main() {
        System.out.println("Hello World! - I am from Unnamed Class!");
    }

    public static void main(String[] args) {
        new HelloWorld().main();
    }
}

Other Notable Features

It also includes enhancements from Project Panama, such as the Foreign Function & Memory API and Vector API.

In conclusion, Java 21 brings a host of new features and enhancements that improve the language’s expressiveness and performance. Whether you’re a seasoned Java developer or just starting, these features offer exciting possibilities for your next project.

For more detailed information, you can refer to the official Oracle documentation.

0
Subscribe to my newsletter

Read articles from Sandeep Choudhary directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Sandeep Choudhary
Sandeep Choudhary