Java 21 Features – Overview

Advertisements

Java 21, an LTS (long term support) version has been released on Sept 19th, 2023. It includes very nice features which have been evolving in previous versions. As with previous LTS versions (11, 17) it includes which could be ready on previous not LTS versions (18,19,20). Let’s see some of the most important and interesting features included in this new version.

Sequenced Collections

Java Collections lacks on a collection type to represent a sequence of elements with a defined encounter order, and uniform operations across them. Some people might think, but it has List and ArrayList , however it lacks of methods to get or add elements in some order, list.get(0) or list.get(list.size()-1) , or list.add(0, element) is not enough. Or Dequeue with getFirst or getLast methods. But, in the case of the purpose of dequeue is to be an stack, a collection with other purpose.

In top of that, the Collection interface is too generic to include methods to provide sequenced collections. That’s why some new interfaces have been added.

– Sequenced Collection

interface SequencedCollection<E> extends Collection<E> {
    SequencedCollection<E> reversed();
    void addFirst(E);
    void addLast(E);
    E getFirst();
    E getLast();
    E removeFirst();
    E removeLast();
}

– SequencedSet

interface SequencedSet<E> extends Set<E>, SequencedCollection<E> {
    SequencedSet<E> reversed();    // covariant override
}

– SequenceMap

interface SequencedMap<K,V> extends Map<K,V> {
    // new methods
    SequencedMap<K,V> reversed();
    SequencedSet<K> sequencedKeySet();
    SequencedCollection<V> sequencedValues();
    SequencedSet<Entry<K,V>> sequencedEntrySet();
    V putFirst(K, V);
    V putLast(K, V);
    // methods promoted from NavigableMap
    Entry<K, V> firstEntry();
    Entry<K, V> lastEntry();
    Entry<K, V> pollFirstEntry();
    Entry<K, V> pollLastEntry();
}

With the new three interfaces added, the hierarchy of collections has change to included them

Therefore, implementations of List or Deque are now properly sequenced collections. In the side of the Set the SortedSet, LinkedHashSet now implement the SequenceSet interface. And finally, on the Map interface, the SortedMap now implements SequencedMap.

Therefore, some implementations that in the past Java developers considered as sequential, are now properly defined as it. The introduction of a well-defined encounter order and a uniform API across the board is a welcomed addition to Java in my opinion. It will provide developers with a more straightforward way to simplify commons collection tasks, and little by little, Java adds more convenience to its types.


Virtual Threads

Regarding Virtual Threads there is a more complete article here, I recommend you to read it. Project Loom started in 2017, as an initiative to provide lightweight threads that are not tied to OS threads but are managed by the JVM. The existing “OS threads” or “Platform Threads” are expensive resource-wise, on the other hand “Virtual Threads” are lightweight handled by the JVM, making possible to have millions of them, the only limit is the JVM memory.

var thread = Thread.ofVirtual().start(() -> {
  System.out.println("Hello World!");
});

Virtual Threads are supported on other APIs of the JDK (Http request, Sockets, Async programming), so it’s usage will take advantage of optimizing the idle time – waiting for a response – allow to switch to other virtual threads.

It also has support with the Executor Service, to provide an Executor which creates a new Virtual Thread on each execution.

ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
executorService.execute(() -> {
  System.out.println("Hello World!");
});

Because the threads are lightweight an thousands or millions can be created, it’s not recommended to create a thread pool, there will not be optimization on doing that. Just create the virtual thread, use it, and discard it, then create new ones. That’s why the executor service creates a new virtual thread per execution.

Another consideration is that, if your logic is CPU extensive, it make not sense of using Virtual Threads. At the end there is a limit on physical cores, the virtual threads are useful when you concurrency depends on logic doing memory transformations not CPU, or waiting for external resources (Socket, HttpResponses) and optimizing that idle time, with a minimum fingerprint on the resources on the JVM.

Pattern Matching for switch

Java 17 introduced the “switch expression” feature, then the switch was not only an statement in Java, and could be used as an expression to return values. It has been refined on the newer versions and in Java 21 there is a cleanest feature including officially support that where in preview mode for Java 17.

Advertisements
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();
    };

Providing pattern matching for null , better for support for enumerations , sealed classes , the guarded pattern to have conditions, and also support on records . For example, in previous version the “guarded pattern” used the && operator, the final version it uses when.

// As of Java 21
static void testStringNew(String response) {
    switch (response) {
        case null -> { }
        case String s when s.equalsIgnoreCase("YES") -> {
            System.out.println("You got it");
        }
        case String s when s.equalsIgnoreCase("NO") -> {
            System.out.println("Shame");
        }
        case String s -> {
            System.out.println("Sorry?");
        }
    }
}

Records Pattern

The true power of pattern matching is that it scales elegantly to match more complicated object graphs for example with nested records.

record Point(int x, int y) {}
enum Color { RED, GREEN, BLUE }
record ColoredPoint(Point p, Color c) {}
record Rectangle(ColoredPoint upperLeft, ColoredPoint lowerRight) {

// As of Java 21
static void printUpperLeftColoredPoint(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint ul, ColoredPoint lr)) {
         System.out.println(ul.c());
    }
}

With nested patterns we can deconstruct such a rectangle with code that echoes the structure of the nested constructors:

// As of Java 21
static void printXCoordOfUpperLeftPointWithPatterns(Rectangle r) {
    if (r instanceof Rectangle(ColoredPoint(Point(var x, var y), var c),
                               var lr)) {
        System.out.println("Upper-left corner: " + x);
    }
}

As mentioned before, the “records pattern” has evolved together with the “pattern matching for switch”. So, the same support has been added to the switch expression.

switch (obj) {
        case Rectangle(ColoredPoint ul, ColoredPoint lr) -> 
          System.out.println("The upper-left corner color is: "+ul.c());
        default        -> obj.toString();
    };


Features in Preview mode

So, Java has decided to include features which are not ready yet, they are in incubator or preview mode but are exposed (enabled by a feature flag) for people to use it and provide feedback. However, it is very common that features in previous mode have changes in future version which are not backward compatible. It’s not recommend to use them for production ready applications.

In this case, Java 21 includes some features in preview mode. For example the Structured Concurrency API , Vector API , Scoped Values , but I would like to mention to of them which have good opportunities to be released on future versions.

String Template

Many programming languages offer string interpolation as an alternative to string concatenation. 

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)"

In Java, the most current alternative is to use the String.formatted method.

var x = 5;
var y = 10; 
String myString = "%s plus %s equals %s".formatted(x, y, x+y);

But that approach is not very convenient, compared to features provided by other languages. Template expressions are a new kind of expression in the Java programming language. Template expressions can perform string interpolation but are also programmable in a way that helps developers compose strings safely and efficiently.

String name = "Joan";
String info = STR."My name is \{name}";
assert info.equals("My name is Joan");   // true

STR is a template processor defined in the Java Platform. It performs string interpolation by replacing each embedded expression in the template with the (stringified) value of that expression. The result of evaluating a template expression which uses STR is a String; e.g., "My name is Joan". Other template processor like dates, custom formats, etc, are being reviewed as part of this feature to provide a flexible API.

Unnamed pattern and variables

With the new pattern match and switch features, and even on lambda expressions there is a lack on unnamed variables. What happen when a lambda expression is used, but some of its input variables are not used, the developer need to define them always.

(x, y, z) -> x + y // z was never used, but the name was required
  
try{ ... 
} catch (NumberFormatException nfex){
   //Catch logic never use the nfex variable
}

The unnamed pattern is denoted by an underscore character _, it could be used on variables, or on pattern as a wildcard to indicate it does not matter which is the type of the name. That feature is implemented on other languages like scala.

(var x, var y, _) -> x + y //the third input is unnamed
  
try{ ...
} catch (NumberFormatException _){
   //Catch NumberFormatException but it does not matter the name of the variable
}

switch (obj) {
   case Rectangle(ColoredPoint ul, _) ->  //Only interested on the first parameter of rectangle
          System.out.println("The upper-left corner color is: "+ul.c());
        default        -> obj.toString();
};

References

Advertisements

Leave a Reply

Your email address will not be published. Required fields are marked *