The Latest from the Java World: JDK 21 Hands-On Developer Insights

28.02.2024β€’Tom Trapp
Tech Java jdk JVM Good-to-know

Banner

News from the Java world, a new version of the Java Development Kit (JDK) is here, namely JDK 21!

In this TechUp, we want to address the following questions:

  • What’s new with JDK 21? ❓
  • Collections with order? πŸš€
  • String Templating in a completely different way? πŸ€”
  • static void final whatever main(Something in here) {}? 🀯
  • Virtual Threading with Java? πŸ€”

JDK 21

JDK 21 is the latest LTS (Long Term Support) version of Java. This version was released in September 2023. Let’s take a look at the most important features and improvements.

So let’s get started! First, after what felt like an eternity until brew upgrade finally finished, I installed the JDK 21 from Temurin for my MacBook.

By the way, you can find all code examples as complete projects here in the b-nova-techhub/java-21 repository.

A little tip on the side: For the complete nerd experience, check out https://javaalmanac.io/. πŸš€

String Templates (Preview)

JEP 430: String Templates (Preview)

Those who have followed the latest Java releases will surely immediately think “String Templates or Interpolations again?”. In the last versions, it was often about multi-line strings, now it’s specifically about being able to define dynamic areas in a string more easily and beautifully.

Let’s look at how it would have been done before:

1
2
3
4
5
6
int x = 10;
int y = 20;

String a = x + " times " + y + " = " + x * y;
String b = String.format("%d times %d = %d", x, y, x * y);
String c = "%d times %d = %d".formatted(x, y, x * y);

We see three different possibilities, which were certainly used extensively in the past.

Now there are so-called StringTemplates, which provide us with a placeholder logic using the \{} syntax.

The new solution would look like this:

1
String newWay = STR."\{x} times \{y} = \{x * y}";

Let’s look at some more examples!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var name = "Tom";
var city = "Basel";
var random = new Random().nextInt(100);

System.out.println(STR."Hello \{name}, welcome to \{city}!");
System.out.println(STR."Hello \{name.toLowerCase().charAt(0)}, welcome to \{city.toUpperCase()}!");

System.out.println(STR."Your random number is \{random}.");
System.out.println(STR."Your random number is \{random} and the half of it is \{random/2}.");

System.out.println(STR."The podcast name is \{getPodcastName()}.");

You can see how we can very easily generate strings dynamically using a placeholder or even templating syntax. Method calls and calculations can also be directly incorporated into the string.

Besides STR, there is also FMT for specific formatting and RAW to define the actual StringTemplate once and then call it using a process method.

The possibilities in the future are certainly gigantic here, as it is very easy to write your own processors:

1
2
3
4
5
6
7
8
StringTemplate.Processor<String, RuntimeException> BNOVA = template -> template.fragments().stream().map(String::toUpperCase).collect(Collectors.joining()) + STR."; check out the podcast \{getPodcastName()}";

System.out.println(BNOVA."hello from basel, the beautiful city 'am rhy'");
	
...
String getPodcastName() {
	return "decodify";
}
1
2
3
StringTemplate.Processor<String, RuntimeException> LOG = template -> template.interpolate() +  STR."; TrackingID: \{new Random().nextInt(1000)}";

System.out.println(LOG."User \{name} logged in at \{LocalTime.now()}");

Here it is nice to see that we can not only use variables in the StringTemplate, but also call methods or, for example, perform calculations.

Sequenced Collections

JEP 431: Sequenced Collections

Another new feature that came with JDK 21 as a stable feature is the so-called “Sequenced Collections”. So it is now possible, for example, to get the first or last element of a collection. And not as in the past via get(0) or get(size()-1), but via the new methods getFirst() and getLast().

Of course, this only works if the implementation of the collection also supports this by implementing the interface SequencedCollection.

In the following code snippet, you can see that we can both read with getFirst() and getLast() and write with addFirst() and addLast().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add( "c");

System.out.println(list); //[a, b, c]

System.out.println(list.getFirst()); //a
System.out.println(list.getLast()); //c

list.addFirst("1");
list.addLast("99");
System.out.println(list); //[1, a, b, c, 99]

In addition, the interface also offers the methods removeFirst() and removeLast().

It should be noted that the writing methods such as add* and remove* only work if the collection also supports this. If it is an ImmutableCollection, an UnsupportedOperationException will be thrown.

Let’s take a quick look at the revised class diagram! It is immediately noticeable that the probably most used interface List now implements the interface SequencedCollection. This means that our code does not have to be adapted in most cases and we have direct access to the new methods.

img.png

Source: https://openjdk.org/jeps/431

You can also see that there is the analogous implementation as SequencedSet and as SequencedMap.

The new Main Method

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

Hold on tight, buckle up, here we go! πŸ‘¨πŸ»β€πŸš€

With the preview feature JEP 445: Unnamed Classes and Instance Main Methods (Preview), THE! groundbreaking innovation comes in the now 65th release of Java.

There are now so-called “unnamed classes” and “instance main methods”. This means that we no longer have to define the main method in a class, but it can simply be written directly.

When I think back to my Java beginnings 12 years ago, always this memorization of public static void main(String[] args), that is now history!

1
2
3
4
5
6
7
8
9
// The old way
package com.bnova.techhub;

public class OldStyle
{
	public static void main(String[] args) {
		System.out.println("Hello world!");
	}
}

This is a classic Java class with a main method, surely every Java newbie starts with such a class. But that is now a thing of the past.

1
2
3
4
// The new way
void main() {
	System.out.println("Hello world!");
}

And that’s all! No class, no package, no complicated parameters, just a main method, as you know it from other languages. The program is started as usual!

Hands-down, this is really a successful innovation for Java. This makes it easier to get started and makes the language more accessible, but it is also a welcome innovation for experienced developers if you want to quickly test a few lines of code.

The concept of the unnamed class is not entirely new, in past versions there were also unnamed modules and unnamed packages.

It is also exciting to mention here that you can define several main methods with different parameters. This is another step towards flexibility and simplicity. In this case, a corresponding launch protocol then decides which main method is called.

Virtual Threading

JEP 444: Virtual Threads

Last but not least, certainly the most important feature of the JDK 21 release, the virtual threads!

Let’s assume we have an API that we need to query, each API call takes one second.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
package com.bnova.techhub.threads;

public class Api
{
	public static void callApi()
	{
		try {
			Thread.sleep(1000); // Simulate work by sleeping for 1 second
			System.out.println("Calling the API...");
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
			System.err.println(STR."Thread interrupted: \{e.getMessage()}");
		}
	}
}

We have a long list of objects that we need to enrich with data from the API. The API has enough power, so we can work in parallel to get the data faster.

Until now, we have done this with Thread or ExecutorService, but that is not really efficient because the threads have too much overhead.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import com.bnova.techhub.threads.Api;


void main() {
	var startTime = LocalTime.now();
	System.out.println(STR."Start Time: \{startTime}");

	List<Thread> threads = new ArrayList<>();
	for (int i = 0; i < 100; i++) {
		var thread = new Thread(Api::callApi);
		threads.add(thread);
		thread.start();
	}

	for (Thread thread : threads) {
		try {
			thread.join();
		} catch (InterruptedException e) {
			System.err.println(STR."Failed to join thread: \{e.getMessage()}");
		}
	}

	var endTime = LocalTime.now();
	System.out.println(STR."End Time: \{endTime}");
	System.out.println(STR."Duration: \{java.time.Duration.between(startTime, endTime).toMillis()} milliseconds");
}

In the code snippet, we have 100 objects, for each object we start a “real” thread that queries the API. The execution of this program takes an average of 1100 milliseconds. (And we use String Templates, cool! πŸš€)

Now let’s look at the same example with virtual threads:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;
import com.bnova.techhub.threads.Api;


void main(){
	var startTime = LocalTime.now();
	System.out.println(STR."Start Time: \{startTime}");

	List<Thread> threads = new ArrayList<>();
	for (int i = 0; i < 100; i++) {
		var thread = Thread.startVirtualThread(Api::callApi);
		threads.add(thread);
	}

	for (Thread thread : threads) {
		try {
			thread.join();
		} catch (InterruptedException e) {
			System.err.println(STR."Failed to join thread: \{e.getMessage()}");
		}
	}

	var endTime = LocalTime.now();
	System.out.println(STR."End Time: \{endTime}");
	System.out.println(STR."Duration: \{java.time.Duration.between(startTime, endTime).toMillis()} milliseconds");
}

Not much has changed, the program also runs around 1100 milliseconds, but the difference is that we no longer use “real” threads, but “virtual” threads.

Using a diff, it is easy to see what the effective changes are:

img_1.png

So it is very easy to switch from a real to a virtual thread.

But now @Scale! What happens if we suddenly have 100,000 objects? So we adjust the number in the loop and restart the program.

The old solution runs: 19222 milliseconds, which corresponds to almost 20 seconds. Quite long! 😴

The new solution with virtual threads runs: 1722 milliseconds, which corresponds to almost 2 seconds. That is a huge difference!

Now let’s think this further, if we have, for example, a web server that has to process millions of requests per second, then that is a huge difference. Or is that only in theory?

Numerous benchmarks on the Internet show that virtual threads are significantly faster up to a certain point. From this point on, virtual threads perform equally well or even worse than “real” threads. You can find more information here.

Surely the question arises here how Java applications will be built in the future, classically sequentially or with real threads, according to the Reactive Programming principle or with virtual threads?

This question is difficult to answer, as it depends very much on the application and the requirements. We will keep an eye on virtual threads in the future and will certainly experiment with RunOnVirtualThread in one or the other Quarkus project.

If you want to learn more about virtual threads at this point, then I recommend this Quarkus article!

Varia

Briefly noted:

With Math.clamp() we can now check whether a value is within a certain range. If the value is outside, the corresponding min or max value is returned. That could be quite practical!

1
2
3
4
5
6
7
8
	var i = Math.clamp(2, 1, 3);
	System.out.println(i); // 2

	i = Math.clamp(5, 1, 3);
	System.out.println(i); // 3

	i = Math.clamp(6, 10, 100);
	System.out.println(i); // 10

In addition, as in the last releases, something has changed in the Switch Statements, where you can now directly implement pattern matching without annoying instanceof checks. Additionally, there are now also further matching possibilities for Record Types.

LTS

It should be mentioned again here that this is an LTS version, which is supported for longer than the normal releases.

From my point of view, the change is worth it! You can find the complete list of all JEPs from the jump from the last LTS version Java 17 to JDK 21 here.

Conclusion

Cool and really practical features have been bestowed upon us with JDK 21! Virtual threads will certainly significantly improve the performance of web servers or even application servers in general! Sequenced Collections and String Templates are certainly features that we will use very often in the future.

Outlook

Java 22 is already in the starting blocks and is scheduled for release in March 2024. Currently, some features are already known, we will take a look at the most important and coolest features in the next TechUp! πŸš€ JDK 23 is also already in the planning stage and there are already some exciting features awaiting us developers.

This techup has been translated automatically by Gemini

Tom Trapp

Tom Trapp – ProblemlΓΆser, Innovator, Sportler. Am liebsten feilt Tom den ganzen Tag an der moderner Software und legt viel Wert auf objektiv sauberen, leanen Code.