The 5 most exciting features of Java 21

With Java 21 just released today, it’s the perfect time to have a closer look and evaluate it’s new features and improvements. Let’s have a closer look at some of the new features like Virtual Threads, Record Patterns, Sequenced Collections and Unnamed Classes.
JEP 444 – Virtual Threads
By far the biggest standout in Java 21. This JEP introduces virtual threads to the java platform and should reduce the effort of writing high-throughput concurrent applications. Since this is such an exciting topic, I have written a more in-depth post about virtual threads here
JEP 440 – Record Patterns
Record patterns will be introduced in Java 21 and enhance the java language to enable a powerful, declarative, and composable form of data navigation and processing. Since Java 16, patterns using instanceof like this has become very common:
if (obj instanceof String s) {
... use s ...
}
And have mostly replaced the old way of matching an instance like this:
if (obj instanceof String) {
String s = (String)obj;
... use s ...
}
In Java 21, patterns like these patterns will be further improved and used together with the new java records. Even with the improved construct above, it’s still a bit of a hassle if I want to access data from the object that matches instanceof:
record Point(int x, int y) {}
static void printSum(Object obj) {
if (obj instanceof Point p) {
int x = p.x();
int y = p.y();
System.out.println(x+y);
}
}
In this example, p is only used to invoke the accessor methods of x and y. Wouldn’t it be convenient if we could directly access those? In Java 21, we can!
static void printSum(Object obj) {
if (obj instanceof Point(int x, int y)) {
System.out.println(x+y);
}
}
This is called a record pattern, and lifts the declaration of local variables for extracted components into the pattern itself. Record patterns also support nesting, so even if a record contains another record, we are still able to access it.
static void printColorOfUpperLeftPoint(Rectangle r) {
if (r instanceof Rectangle(ColoredPoint(Point p, Color c),
ColoredPoint lr)) {
System.out.println(c);
}
}
Additional information about record patterns can be found here Now that we know about record patterns, let’s have a look at another feature that often will be used together with Record Patterns.
JEP 441 – Pattern Matching for switch
We often want to compare a variable such as obj against multiple, different objects. This can be achieved with a switch statements, but there are quite some limitations to it. There are only a few supported types and we can only test for exact equality against constants. So for testing the same variable against a number of different objects, we still end up with a long chain of if..else statements:
static String formatter(Object obj) {
String formatted = "unknown";
if (obj instanceof Integer i) {
formatted = String.format("int %d", i);
} else if (obj instanceof Long l) {
formatted = String.format("long %d", l);
} else if (obj instanceof Double d) {
formatted = String.format("double %f", d);
} else if (obj instanceof String s) {
formatted = String.format("String %s", s);
}
return formatted;
}
So even though we use the new instanceof esions, the code is still far from perfect and there is a lot of boilerplate for what we want to achieve. But, by extending switch statements to work with any type and allow case labels with patterns rather than just constants, we can now write this beautiful code:
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();
};
}
What is pretty cool is that you can also specify conditionals on a case with the when keyword. This is especially useful when you want to check for multiple conditions on the same object:
static String formatterPatternSwitch(Object obj) {
return switch (obj){
case Integer i when i > 3->String.format("int %d",i);
}
Another great thing about this is that we can also check for null now:
static void testFooBarNew(String s) {
switch (s) {
case null -> System.out.println("Oops");
case "Foo", "Bar" -> System.out.println("Great");
default -> System.out.println("Ok");
}
}
For additional and more advanced use cases, I recommend reading JEP-441
JEP 431 – Sequenced Collections
This jep introduces new interfaces to rent collections with a defined encouter order. Each of these collections has a defined first, second and so forth elements. The problem with current Collections can be seen nicely when we want to retrieve a specified element like first or last from them:
| Collection | 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 |
These are cumbersome at best or not even possible without iterating through the whole set in case of LinkedHashSet. To handle this, Java 21 introduces sequenced collections. As per definition:
“A sequenced collection is a Collection
whose elements have a defined encounter order. (The word “sequenced” as used here is the past participle of the verb to sequence, meaning “to arrange elements in a particular order.”) A sequenced collection has first and last elements, and the elements between them have successors andecessors. A sequenced collection supports common operations at either end, and it supports processing the elements from first to last and from last to first (i.e., forward and reverse).”
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();
}
So in the future, you will be able to easily retrieve the first and last elements from your Lists and Sets and as well be able to easily reverse them.
Further info about Sequenced collections can be found here
JEP 445 – Unnamed Classes and Instance Main Methods (Preview)
Even though the feature is only iniew mode, it’s still worth it to have a closer look at it. When you first start out learning Java, you have to write the following monster to make it print a simple “Hello World”.
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
If we compare this to other languages like python,
print("Hello, World!")
Rust
fn main() {
println!("Hello World!");
}
Or something closer to the JVM like Kotlin
fun main(args : Array<String>) {
println("Hello, World!")
}
We can grasp that the Java version is a bit outdated and more complex than necessary. After all, you need to understand what a class is, why you need to pass a String[] args parameter and why the static modifier is needed. Given our upcoming generation of students with their short attention spans you can see why this is a problem 😉 So in order that new programmers encounter the Java language concepts at the right time and not before they learn about the basics like variables and control flow, this JEP will be introduced in an upcoming Java version. How does this work? First, Java will be allowed instance main methods. Such methods are not static, don’t need to be public, and don’t need a String[] parameter. Like this:
class HelloWorld {
void main() {
System.out.println("Hello, World!");
}
}
Then, with the new possibility to create unnamed classes we can make the class declaration implicit:
void main() {
System.out.println("Hello, World!");
}
And this is it, this is how the new Java Main method will look like in the upcoming versions. Learn more about it here
Conclusion
The introduction of virtual threads is certainly something that will continue to make waves in the future and i’m thrilled to see what kind of impact they will have on the java ecosystem. After one to many discussions if projects should use webflux I’m personally very grateful that there seems to be a sensible alternative on the horizon for most run-of-the-mill CRUD Applications.
Other than that, the shorter release cycles seem to really pay of for java. I’ve personally run into the issues that will be solved by pattern matching for switch and sequenced collections many many times and it’s just great to see that they will be addressed soon.