It’s 2023 and finally time for another gold starred Java LTS release, with Java 21.

To make the most of Java’s modern language features, you’ll want to upgrade to this version. That way, you can write more maintainable code with new features like sequenced collections and pattern matching for switch.

But before we dive into these, what’s so fantastic about LTS versions of Java?

What’s the benefit of Java LTS versions?

A new Java version is released every 6 months.

Every 4th Java version, released once every 2 years, gets a special LTS (long term support) label. LTS that means updates are available for longer.

What kind of updates? Think critical bug fixes and security patches.

Oracle JDK, one of the many JDK options to develop Java software, has this update schedule:

  • LTS version updates until 1 year after the next LTS release
  • non-LTS version updates until the next version release

Java LTS versions are a good choice if you want to use a recent Java version, while continuing to get updates even after the next Java release.

Now let’s jump in and start building with Java 21.

How to use Java 21

For users of the Gradle build tool, simply increment the Java version in your build.gradle.kts build script’s toolchain configuration.

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(21))
    }
}

This ensures that Java 21 is used for compiling code when running assemble or build.

In order for Gradle to download Java automagically, you must also configure a toolchain download repository by adding this snippet to settings.gradle.kts.

plugins {
    id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0")
}

To validate Gradle is using the correct Java version, run the compileJava task with the --info flag and inspect the output.

./gradlew compileJava --info
...
Starting process 'Gradle Worker Daemon 1'. Working directory: C:\Users\Tom\.gradle\workers Command: C:\Users\Tom\gradle\jdks\oracle_corporation-21-amd64-windows\jdk-21\bin\java.exe
...

What’s new in Java 21?

Like most Java releases, Java 21 adds many new features.

Some of these are preview features, enabled only when the --enable-preview compiler argument is set.

Some are fully delivered features, and we’ll look at the two most important, sequenced collections and pattern matching for switch.

1) Sequenced collections

There’s a new interface in town, and it’s called SequencedCollection.

It’s implemented by collection classes that have ordering e.g. List and LinkedHashSet.

The SequencedCollection interface offers a common way to update the first and last items of a collection or easily create a reversed view of it.

The new methods are:

  • getFirst()
  • getLast()
  • addFirst(E e)
  • addLast(E e)
  • removeFirst()
  • removeLast()
  • reversed()

Prior to Java 21, getting the last element of a List required a verbose operation such as list.get(list.size() - 1).

Here’s an example using the new SequencedCollection API to interact with a List.

var dailyRoutine = new ArrayList<>(
        asList("wake up", "make bed")
);

System.out.println("Last item: " + dailyRoutine.getLast());
// prints Last item: make bed

dailyRoutine.addLast("begin typing");

System.out.println("Last item: " + dailyRoutine.getLast());
// prints Last item: begin typing

System.out.println("Items reversed: " + dailyRoutine.reversed());
// prints Items reversed: [begin typing, make bed, wake up]

The new methods are quite self-explanatory, but you can learn more in the SequencedCollection API documentation.

Once you’ve updated to Java 21, start using sequenced collections by doing a global find and replace and enjoy a slightly more streamlined codebase.

2) Pattern matching for switch

The next new feature is slightly confusingly named pattern matching for switch.

For anyone without an encyclopaedic knowledge of Java release features, you can think of this as type matching for switch statements and expressions.

Before Java 21, switch statements and expressions could only take primitive types (e.g. int, char), their boxed types (e.g. Integer, Character), enums, and String.

Here’s a switch expression, valid in any recent Java version, that takes an int and returns an appropriate message based on its value.

    static String birthdayMessage(int age) {
        return switch (age) {
            case 20 -> "You're a proper grown up person now!";
            case 30 -> "Did you <insert life milestone> yet?";
            case 40 -> "Let's keep this one quiet, shall we?";
            default -> "Congratulations on surviving another solar orbit!";
        };
    }

Using switch we avoid if / else statements and clearly express the various alternatives.

Now in Java 21, switch can take any type of object and can match on type.

Here’s a clear example, slightly modified from the official feature description.

    private static String formatter(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);
            default        -> obj.toString();
        };
    }

Here, the switch statement takes an object and handles the cases when it’s one of Integer, Long, or Double.

Contrast that with the verbose pre-Java 21 equivalent, which uses the instanceof operator.

    private static String formatter(Object obj) {
        if (obj instanceof Integer i) {
            return String.format("int %d", i);
        } else if (obj instanceof Long l) {
            return String.format("long %d", l);
        } else if (obj instanceof Double d) {
            return String.format("double %f", d);
        }

        return obj.toString();
    }

The switch expression can now also handle null values. Let’s extend the example to handle null.

    private static String formatter(Object obj) {
        return switch (obj) {
            case null  -> "Pass something non-null";
            case Integer i -> String.format("int %d", i);
            case Long l    -> String.format("long %d", l);
            case Double d  -> String.format("double %f", d);
            default        -> obj.toString();
        };
    }

This helps reduce boilerplate code as the null check is integrated into the switch statement.

What else?

Other new features in Java 21 include:

  • Generational ZGC
  • Record patterns
  • Virtual threads

These are for more advanced use cases, but you can learn more in the Java 21 release notes.

If you’re feeling adventurous, why not try the preview features like Unnamed classes and instance main methods?

Could this finally be the death of Java’s infamous public static void main?

Stop reading Gradle articles like these

This article helps you fix a specific problem, but it doesn't teach you the Gradle fundamentals you need to really help your team succeed.

Instead, follow a step-by-step process that makes getting started with Gradle easy.

Download this Free Quick-Start Guide to building simple Java projects with Gradle.

  • Learn to create and build Java projects in Gradle.
  • Understand the Gradle fundamentals.