Java 21: new tools and technologies to keep developers ahead
technical articlesMay 10, 2024

Java 21: new tools and technologies to keep developers ahead

Article presentation


Despite being close to three decades old and having really good competitors, Java is still a widely used programming language in 2024. Known for its variety of use cases ranging from web application development to mobile app development and large-scale application development, Java ranks 7th in popular technologies amongst professional developers according to StackOverflow's 2023 yearly survey.  

Released on the 19th of September 2023, Java 21 will surely become even more popular amongst developers. Without further ado let's have a look at some of the most exciting features this new Java version has to offer.


Preview Features

Before starting let's have a look at how to enable and use the preview features. Preview features are features of Java language, JVM, or Java SE API that are meant to generate developer feedback so that they become permanent features.

To compile and run the preview features and examples you need to:

  • Compile a Java sample program with the `javac --release 21 --enable-preview`
  • Run the program with `java --enable-preview MyProgram`

String templates (Preview)

Most popular programming languages these days offer the ability to write string literals with embedded variables or expressions which are then interpolated into the string at runtime. Features like this improve the readability of the code making it easier for the end user, the developer, to understand bits of code.

Let's have a look at some examples in other languages:

  • C#:  $"Hello {name} and welcome, you have {notificationsCount} notifications"
  • Javascript: `Hello ${name} and welcome, you have ${notificationsCount} notifications\`
  •  Python: f"Hello ${name} and welcome, you have ${notificationsCount} notifications" 

To better understand how this feature improves the language we have to look at how string interpolation could be achieved in Java before Java 21:


 1 String name = "John Doe";
 2 int notificationCount = 10;
 3 String welcomeMessage = "Hello " + name + " and welcome, you have " + notificationCount + " new notifications";

String format

 1 String name = "John Doe";
 2 int notificationCount = 10;
 3 String welcomeMessage = String.format("Hello %s and welcome, you have %d notifications", name, notificationCount);

String Builder

 1 String name = "John Doe";
 2 int notificationCount = 10;
 3 String welcomeMessage = new StringBuilder("Hello")
 4        .append(name)
 5        .append(" and welcome, you have ")
 6        .append(notificationCount)
 7        .append(" notifications")
 8        .toString();

As you've already noticed things could be made simpler. With the new release of Java, now you can do:

 1 String name = "John Doe";
 2 int notificationCount = 10;
 3 String welcomeMessage = STR."Hello \{name}, and welcome, you have \{notificationCount} notifications";

Sequenced Collections

Until Sequenced Collections, Java's collections framework lacked a collection type that represents a sequence of elements with a defined encounter order. Methods like getFirst() or getLast() would not be available for collections like `List`, you would have to do myList.get(0) or myList.get(myList.size() - 1). Some collections would have these like Dequeue and SortedSet but they would not be uniform:  deque.getFirst() and sortedSet.first().

With this feature, new interfaces for sequenced collections, sequenced sets, and sequenced maps were added to Java's collections framework. 


  • Operations on first and last elements were added as seen above:
 1 void addFirst(E);
 2 void addLast(E);
 3 E getFirst();
 4 E getLast();
 5 E removeFirst();
 6 E removeLast();
  • Also, the reversed() method would return a reverse-ordered view of the original collection. Now you can do something like myList.reversed().stream()...

Pattern Matching for Switch

From time to time we encounter this type of code in our codebases

 1 if (obj instance of String) {
 2     String s = (String) obj;
 3 }

Two steps are done here: 

  •  test  if an object has a particular type
  •  downcast if there is a match and "extract the data" into a variable for us to use

Since the release of `Pattern matching for instanceof operator` in Java 16 we can now write code like this:

 1 if (obj instanceof String s) {
 2     // do something with s
 3 }

In Java 21 this was extended to switch statements as well, prior to it, in order to use this feature you'd have to chain if else statements:

 1 void myFunction(Object obj) {
 2     if (obj instanceof Dog dog) {
 3         dog.bark();
 4     } else if (obj instanceof Cat cat) {
 5         cat.meow();
 6     } else if (obj instanceof Human human) {
 7         human.speak();
 8     }
 9 }

Now you'll be able to use switch statements as well with pattern matching like this:

 1 void myFunction(Object obj) {
 2     switch (x) {
 3         case Human h -> h.speak();
 4         case Dog d -> d.bark();
 5         case Cat c -> c.meow();
 6         default -> throw new RuntimeException("Unknown type");
 7     }
 8 }

Record patterns

Another feature that was finalized in this release is record patterns. Now we are able to destruct record values. This feature was built on pattern matching for instanceof and can be used with pattern matching for switch statements

Let's have a look at a simple example, in Java 16 we could write code like this:

 1 record Money(BigDecimal amount, String currency);
 2 ...
 3 public void print(Object o) {
 4     if (o instanceof Money m) {
 5         System.out.println(String.format("%s %s", m.amount(), m.currency()))
 6     }
 7 }

As of Java 21, this can be simplified to:

 1 record Money(BigDecimal amount, String currency);
 2 ...
 3 public void print(Object o) {
 4     if (o instanceof Money(BigDecimal amount, String currency)) {
 5         System.out.println(String.format("%s %s", amount, currency))
 6     }
 7 }

This can also be combined with pattern matching for switch statements so we would have:

 1 record Money(BigDecimal amount, String currency);
 2 ...
 3 public void print(Object o) {
 4     switch (o) {
 5         case Money(BigDecimal amount, String currency) -> System.out.printf("%s %s%n", amount, currency);
 6         case String s -> System.out.printf("Sting -> %s%n", s);
 7         default -> throw new RuntimeException("Unknown type");
 8     }
 9 }

This feature will make the code more readable and less error prone.

Virtual Threads

To understand why we need this feature we have to look at how Java handled platform threads until now, each time a platform thread was created an operating system thread would be allocated to it. In a high throughput application like a backend application which uses a thread per request model, this means that a platform thread will be used for every request. Often we do blocking operations on those threads like calling an API or doing some database operation, this results in us not fully utilizing the CPU resource that we have, operating system threads will wait and do nothing while other requests are waiting. Another problem is that we cannot launch too many platform threads at once, so scaling our backend applications would be a problem. 


These problems were tackled in the past using asynchronous programming (CompletableFuture or RxJava) but these kinds of solutions make the code hard to read and maintain. 

This new Java feature solves these problems by not mapping threads 1:1 to operating system threads. Virtual threads are created which are then attached to a platform thread (also called a carrier thread). Once a virtual thread is doing a blocking operation, the Java runtime suspends that virtual thread until work can be resumed on it, thus our operating system thread is now available and can do work for another virtual thread. This allows us to spawn a lot more threads than we ever could. 


Let's have a look at this example from the JEP's page:

 1 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
 2     IntStream.range(0, 10_000).forEach(i -> {
 3         executor.submit(() -> {
 4             Thread.sleep(Duration.ofSeconds(1));
 5             return i;
 6         });
 7     });
 8 }

Running this code with some debug information like the time difference between start and end time and counting the threads I got the following output:

 1 Total time taken: 1035 ms
 2 Total threads used: 1

Running the same code with a 1000 threads fixed thread pool Executors.newFixedThreadPool(1000),  I got the following results:

 1 Total time taken: 10046 ms
 2 Total threads used: 1000

This is a huge improvement! Those Thread.sleep(Duration.ofSeconds(1)) might as well be blocking operations in your application like a database call or an API request. This makes this feature the most exciting of all regarding the scalability of your applications.

As an ending note we have to mention that, although we've noticed a huge improvement in the total time taken for that snippet to run, the Virtual Threads feature is not a performance improvement feature but a throughput improvement for your application. 

Unnamed Classes and Instance Main Methods (Preview)

The last feature we will look into in this article is the unnamed classes and instance main methods feature. Until now when writing our first program in Java we would have to write something like this:

 1 public class MyApplication {
 3     public static void main(String[] args) {
 4         System.out.println("Hello world");
 5     }
 6 }

With this feature, we will only have to write this in our ``:

 1 void main() {
 2     System.out.println("Hello, Java21");
 3 }

The purpose of this is to make the language friendlier for students or developers wanting to learn Java without having to fully understand all the language features. This will make the programming language even more popular for starters.


With improvements in all directions, exciting times are ahead for the Java community with the release of this Java 21 version. Here is the Oceanobe community we are passionate about Java and if you are too you can always join us

If you are passionate about refactoring here is an article that might interest you - The importance of refactoring.