DEV Community

Nextpatch
Nextpatch

Posted on

5 Java 21 changes you should already be using

ava 21 came out in September 2023, and plenty of projects still write code as if they were on Java 11. It compiles just the same, but they miss changes that make code shorter and easier to read, and that can come up in any technical interview.

Here are five you can start using on Monday. Each one comes with a two-minute challenge to check you've got it.

1. Virtual threads: scale, not speed

A platform thread, the Thread we've always had, wraps an operating system thread: it's expensive to create and there aren't many of them. That's why we've spent years with bounded pools or asynchronous code.

Java 21 finalises virtual threads (JEP 444). They are still instances of java.lang.Thread, but the JVM schedules them over a few OS threads. When one blocks on a JDK I/O operation, it unmounts and frees its carrier thread for another one.

try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    executor.submit(task);
}
Enter fullscreen mode Exit fullscreen mode

The most misunderstood part: they are not faster threads. What improves is throughput when you have thousands of tasks that spend most of their time waiting (HTTP requests, database queries). If your workload is CPU-bound, you gain nothing.

One caveat before using them in production on Java 21: inside a synchronized block the virtual thread gets pinned and blocks its carrier too. Java 24 fixed that with JEP 491, but on Java 21 it's worth switching the synchronized blocks that wrap network calls to a ReentrantLock.

Practise it: virtual threads, scale not speed.

2. Pattern matching for switch

Deciding what to do based on an object's type used to be a chain of if ... else if with instanceof. Java 21 finalises pattern matching for switch (JEP 441), after four preview rounds starting in Java 17:

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 String s  -> String.format("String %s", s);
        default        -> obj.toString();
    };
}
Enter fullscreen mode Exit fullscreen mode

The selector can be any reference type, and each case takes a type pattern just like instanceof. The first case that matches wins, in the order they're written.

The key point is that it compares the object's actual type, not its value: an Object holding a Long doesn't match case Integer i, even if the number fits in an int. On top of that, the compiler tells you when a case can never be reached because an earlier one already covers it.

Practise it: switch stops comparing only constants.

3. case null and when guards

A switch with a null selector used to throw NullPointerException, so the check lived outside it, in an if. Now you can bring it inside with case null, and add conditions to each case with when:

switch (s) {
    case null -> System.out.println("Oops!");
    case String v when v.equalsIgnoreCase("YES") -> System.out.println("You got it");
    case String v -> System.out.println("Sorry?");
}
Enter fullscreen mode Exit fullscreen mode

A guard only applies if the pattern matches and the condition is true. If not, the switch keeps trying the following cases. Order matters: the guarded case goes before the general one, because the other way round the general one would hide it and the compiler would report an error.

Only patterns take guards: case "hello" when ... doesn't compile, because a constant isn't a pattern.

And what happens to default when a null comes in? I'll leave that for the challenge at the end.

4. Record patterns

Pattern matching for instanceof saves you the cast, but you still end up writing p.x() and p.y(). Java 21 finalises record patterns (JEP 440), which pull the components out in the pattern itself:

record Point(int x, int y) {}

if (obj instanceof Point(int x, int y)) {
    System.out.println(x + y);
}
Enter fullscreen mode Exit fullscreen mode

The pattern checks the type and, if it matches, calls the record's accessors. The variable names don't have to match the component names, you can use var in each position, and they work the same in instanceof and in the case labels of a switch. Patterns can be nested: the whole thing only matches if all the subpatterns match, and if one fails there's no exception, the branch just doesn't run.

Combined with a switch over a sealed hierarchy, you handle each shape of your data in one line, with no casts, and the compiler checks that no case slips through.

Practise it: deconstructing a record inside the pattern.

5. Sequenced collections

Getting the first and last element depended on the type: list.get(0) and list.get(list.size() - 1) on a list, deque.getFirst() on a Deque, sortedSet.first() on a SortedSet... and on a LinkedHashSet there was no way to get the last one without walking through the whole set.

JEP 431 adds SequencedCollection, SequencedSet and SequencedMap for collections with a defined order, and fits them into the existing hierarchy:

String first = list.getFirst();
String last = list.getLast();
String lastInSet = linkedHashSet.getLast();
Enter fullscreen mode Exit fullscreen mode

Besides getFirst and getLast you get addFirst, addLast, removeFirst, removeLast and reversed(). Careful with that last one: it doesn't copy the collection, it returns a view in reverse order. And on an empty collection, getFirst() and getLast() throw NoSuchElementException, just as Deque already did.

Practise it: getFirst, getLast and reversed.

Mini challenge: null and default

This code has a default, but no case null:

static String describe(Object o) {
    return switch (o) {
        case String s -> "text";
        case Integer i -> "number";
        default -> "something else";
    };
}

describe(null);
Enter fullscreen mode Exit fullscreen mode

What happens when you call describe(null)? Does it return "something else", throw an exception, or not even compile?

Answer it in the case null and when guards challenge. No account needed, and it only takes a moment.


I write these while building NextPatch, a daily habit for Java developers: a 45-second summary of one change between versions and a 2-minute challenge. Which Java 21 feature did you actually adopt at work, and which one are you still ignoring? Tell me in the comments.

Top comments (0)