New features in Java 21

Farzin Pashaee
Technology Times

--

Java Development Kit (JDK) 21 is the most recent long-term support (LTS) version. This Version became widely available on September 19, 2023, under GPL-licensed, and according to the Oracle Website blog, it includes hundreds of new features and enhancements in addition to thousands of speed, stability, and security fixes.

15 of these enhancements are important enough to have their own JDK Enhancement Proposals (JEPs), which cover six preview features and one incubator feature. In this article, we will look at some of those changes and try to find out how they will change how we use Java.

1. JEP 430: String Templates (Preview)

Most programming languages have a feature known as a template string, known as template strings or interpolation. It is a common use case in which you need to embed a variable inside a String. There are different traditional approaches that are being used for text formatting:

// String concatenation
String output = "My name is " + name;

// StringBuilder class
String output = new StringBuilder()
.append("My name is ")
.append(name);

// String Format
String output = String.format("My name is %s", name);

// Message Format class
String output = new MessageFormat("My name is {0}!").format(name);

Java 21 template expressions are a brand-new type of expression. In addition to performing string interpolation, template expressions can be programmed in a way that makes it easier for developers to compose strings securely and effectively. The new STR as a template processor will substitute the value of each embedded expression in the template, and it carries out string interpolation.

String output = STR."My name is \{name}!";
String sum = STR."\{num1} + num2} = \{num1 + num2}";

2. JEP 431: Sequenced Collections

Have you always been looking for a collection with a defined order, well Java 21 has finally fulfilled that requirement. And that is not all, you can also get access to a uniform method for accessing the first and last element in the collection using three new interfaces.

Any collection that has SequenceCollection, SequenceSet, or SequencedMap as its superclass will have access to the methods to add, remove, and read from the beginning and end of the Collection and also a reverse method.

interface SequencedCollection<E> extends Collection<E> {

SequencedCollection<E> reversed();

void addFirst(E);
void addLast(E);

E getFirst();
E getLast();

E removeFirst();
E removeLast();
}

3. JEP 439: Generational ZGC

ZGC Garbage Collector which was added since JDK 15 is made for great scalability and low latency. The goal of this enhancement is to Lower the risks of location stalls, required memory overhead, and CPU usage overhead.

This improvement was made possible by keeping distinct generations for new and old objects which enables ZGC to be able to gather youthful objects — which typically pass away young — more frequently as a result. read more about this change here.

4. JEP 440: Record Patterns

Records provide a compact syntax to create immutable data classes so you can avoid boilerplate code. Records were a suggestion made by JEP 359 and included as a preview feature in JDK 14. In the JDK 16 Release, Records were no longer a preview feature and stable to use after feedback from versions 14 and 15. For more information visit this page.

public record Product(int id, String name, String category) {}

5. JEP 441: Pattern Matching for switch

First proposed by JEP 406 as a preview feature and deployed in JDK 17, Pattern Matching for switches was later proposed by JEP 420 as a second preview and released in JDK 18. It is common to wish to compare multiple options for a given variable. Java provides multi-way comparisons via switch statements and switch expressions (JEP 361), but the switch is always fairly constrained. The switch can only be used with integral primitive types (long excluded), their wrapped counterparts, enum types, and String values.

public String checkInput(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();
};
}

Using the new pattern-matching switch like the above example will utilize appropriate control construct and will make the intent of this code more clear. Additionally, it is optimizable, which raises the possibility that we can finish the dispatch in O(1) time in this case.

6. JEP 442: Foreign Function & Memory API (Third Preview)

Java programs can interact with data and other programs that aren’t operating on the Java runtime through the Foreign Function and Memory API. They will be able to call foreign functions and access external memory safely as a result. Ease of use, performance, safety, and generality are the objectives of this API. With performance similar to current libraries, it will be possible to replace the Java Native Interface (JNI) with a superior, pure Java development model.

The Foreign Function & Memory (FFM) API (JEP 389) unifies the Foreign-Memory Access API (JEPs 370, 383, and 393) and the Foreign Linker API. It presents classes and interfaces that can Allocate foreign memory, Manipulate/Access structured foreign memory, and Call foreign functions.

7. JEP 443: Unnamed Patterns and Variables (Preview)

https://www.baeldung.com/java-unnamed-patterns-variables

8. JEP 444: Virtual Threads

Virtual threads are lightweight threads with small footprints which considerably reduce the effort of writing, maintaining, and observing high-throughput concurrent systems. For almost three decades, concurrent server applications have been built using threads by Java programmers. A sequential chunk of code that executes simultaneously with and generally independently of other such units is known as a “thread” in Java.

Because the JDK implements threads as wrappers around operating system (OS) threads, the total number of threads is unfortunately constrained. We cannot have too many OS threads because they are expensive, hence the implementation is not suitable for the thread-per-request method. We should work to maintain the thread-per-request style by implementing threads more effectively so they can be more plentiful, allowing apps to scale while remaining in harmony with the platform.

A virtual thread is a Java.lang.Thread the instance that is not tied to a particular OS thread. It works as a platform thread implemented like a thin wrapper around an OS thread. Because they are inexpensive and plentiful, virtual threads shouldn’t ever be pooled: Each task of the application should start a new virtual thread. Therefore, the majority of virtual threads will be short-lived and have shallow call stacks, carrying out just one HTTP client call or one JDBC query.

Executor executor = Executors.newVirtualThreadPerTaskExecutor();
executor.execute( () -> {
// do something inside thread
});

JEP 445: Unnamed Classes and Instance Main Methods (Preview)

With the goal of making Java much easier to learn for beginners, JDK 21 has introduced 2 new features.

  • Instance Main Method makes it possible to build the Java application lunch main method more flexible rather than just being public, static, and String array input.
class JavaApplication {
void main() {
System.out.println("Hello, Java 21");
}
}
  • Unnamed Classes are one important aspect that helps newcomers get easier on the learning curve. Without explicit class declarations, it permits the existence of fields, methods, and classes.

So using the combination of Unnamed Classes and Instance Main Methods you will get a lunch method as simple as the following example:

void main() {
System.out.println("Hello, Java 21");
}

JEP 446: Scoped Values

It is a common situation that you will need to access some values in different stages of a process in different layers and classes. The common solution might be to pass the values as parameters to the next method.

class Controller {
private void serve(HttpRequest request) {
// ...
Order order = getOrder(request);
orderService.processRequest(request, order);
// ...
}
}

class OrderService{
public processRequest(HttpRequest request, Order order){
// ...
paymentService(order);
// ...
}
}

class PaymentService{
public processRequest(Order order){
// ...
BigDecimal orderAmount = order.getAmount();
}
}

There are other solutions that you can use if you are familiar with ThreadLocal variables. but ThreadLocal has some potential issues:

  • Unconstrained mutability — All thread-local variable is mutable.
  • Expensive inheritance — Since child threads can inherit the thread-local variables of their parent thread, the overhead of thread-local variables may increase when utilizing a high number of threads.
  • Unbounded lifetime — A thread’s copy of a thread-local variable is kept at that value for as long as the thread exists, or until code within the thread invokes the delete method which developers most often forget.

A more contemporary option to thread locals is scoped values. Using Scoped Values you can rewrite the same code as follows:

class Controller {
public final static ScopedValue<Order> ORDER = ScopedValue.newInstance();
private void serve(HttpRequest request) {
// ...
Order order = getOrder(request);
ScopedValue.where(ORDER, order)
.run(() -> orderService.processRequest(request));
// ...
}
}

class OrderService{
public processRequest(HttpRequest request){
// ...
paymentService();
// ...
}
}

class PaymentService{
public processRequest(){
// ...
Order order = Controller.ORDER.get();
BigDecimal orderAmount = order.getAmount();
}
}

I hope this article helped you, and please support me by applauding 👏 for the story. If you don’t know how it works, it’s just like this:

Or buy me a coffee here!

--

--