Das Neuste aus der Java Welt: JDK 21 Hands-On Entwicklereinblicke

28.02.2024Tom Trapp
Tech Java jdk JVM Good-to-know

Banner

Neues aus der Java-Welt, eine neue Version des Java Development Kits (JDK) ist da, nämlich JDK 21!

Auf folgende Fragen wollen wir in diesem TechUp eingehen:

  • Was kam Neues mit JDK 21? ❓
  • Collections mit Reihenfolge? 🚀
  • String Templating ganz anders? 🤔
  • static void final whatever main(Something in here) {}? 🤯
  • Virtual Threading mit Java? 🤔

JDK 21

JDK 21 ist die letzte LTS (Long Term Support) Version von Java. Diese Version wurde im September 2023 veröffentlicht. Schauen wir uns die wichtigsten Features und Verbesserungen an.

Also starten wir! Zuerst, nach einer gefühlten Ewigkeit, bis endlich brew upgrade durchgelaufen ist, habe ich die JDK 21 von Temurin für mein MacBook installiert.

Übrigens, alle Codebeispiele findet ihr als komplettes Projekte hier im b-nova-techhub/java-21 Repository.

Kleiner Tipp am Rande: Für die komplette Nerd-Experience schaut mal bei https://javaalmanac.io/ vorbei. 🚀

String Templates (Preview)

JEP 430: String Templates (Preview)

Wer die letzten Java Releases verfolgt hat, denkt sicher sofort “schon wieder String Templates oder Interpolations?”. In den letzten Versionen ging es oft um mehrzeilige String, nun geht es konkret darum, dynamische Bereiche in einem String einfacher und schöner definieren zu können.

Schauen wir uns an, wie man das vorher gemacht hätte:

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

Wir sehen drei unterschiedliche Möglichkeiten, welche in der Vergangenheit sicher zahlreich genutzt wurden.

Neu gibt es sogenannte StringTemplates, welches uns eine Placeholder-Logik mittels der \{} Syntax bereitstellen.

Die neue Lösung würde wie folgt aussehen:

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

Schauen wir uns noch weitere Beispiele an!

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

Zu sehen ist, wie wir sehr einfach über ein Placeholder oder gar Templating-Syntax Strings dynamisch generieren können. Ebenfalls lassen sich Methodenaufrufe und Berechnungen direkt in den String einbauen.

Neben STR gibt es auch noch FMT für bestimmte Formatting und RAW um das eigentliche StringTemplate einmalig zu definieren und mittels einer process Methode dann aufzurufen.

Die Möglichkeiten in Zukunft sind hier sicher gigantisch, da sehr einfach eigene Processors geschrieben werden können:

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

Hier ist schön zu sehen, dass wir nicht nur Variablen im StringTemplate nutzen können, sondern auch Methoden aufrufen können oder beispielsweise Berechnungen durchführen können.

Sequenced Collections

JEP 431: Sequenced Collections

Eine weitere Neuerung, welche mit JDK 21 als stable Feature dazu kam, sind die sogenannten “Sequenced Collections”. So ist es neu möglich, beispielsweise das erste oder das letzte Element einer Collection zu erhalten. Und das nicht wie in der Vergangenheit über get(0) oder get(size()-1), sondern über die neuen Methoden getFirst() und getLast().

Selbstverständlich funktioniert dies nur, wenn die Implementation der Collection dies auch unterstützt, indem das Interface SequencedCollection implementiert ist.

Im nachfolgenden Codeausschnitt ist zu lesen, dass wir sowohl mit getFirst() und getLast() lesen als auch mit addFirst() und addLast() schreiben können.

 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]

Ausserdem bietet das Interface auch die Methoden removeFirst() und removeLast() an.

Zu beachten ist, dass die schreibenden Methoden wie add* und remove* nur dann funktionieren, wenn die Collection dies auch unterstützt. Sollte es sich um eine ImmutableCollection handeln, so wird eine UnsupportedOperationException geworfen.

Schauen wir uns kurz das überarbeitete Klassendiagramm an! Sofort fällt auf, dass das wahrscheinlich am meisten genutzte Interface List neu das Interface SequencedCollection implementiert. Das bedeutet, unser Code muss in den meisten Fällen nicht angepasst werden und wir haben direkt Zugriff auf die neuen Methoden.

img.png

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

Zu sehen ist auch, dass es die analoge Implementation auch als SequencedSet und als SequencedMap gibt.

Die neue Main Methode

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

Festhalten, Anschnallen, Los geht’s! 👨🏻‍🚀

Mit dem Preview-Feature JEP 445: Unnamed Classes and Instance Main Methods (Preview) kommt im inzwischen 65. Release von Java DIE! bahnbrechende Neuerung.

Neu gibt es sogenannte “unbenannte Klassen” und “Instanz-Main-Methoden”. Das bedeutet, dass wir die Main-Methode nicht mehr in einer Klasse definieren müssen, sondern einfach direkt geschrieben werden kann.

Wenn ich mich zurückerinnere an meine Java Anfänge vor 12 Jahren, immer diese Auswendiglernerei von public static void main(String[] args), das ist jetzt Geschichte!

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!");
	}
}

Das ist eine klassische Java-Klasse mit einer main Methode, sicher jeder Java-Neuling beginnt mit so einer Klasse. Aber das ist jetzt Schnee von gestern.

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

Und das ist alles! Keine Klasse, kein package, keine komplizierten Parameter einfach nur eine main Methode, wie man es aus anderen Sprachen kennt. Gestartet wird das Programm wie gewohnt!

Hands-down, das ist wirklich eine gelungene Neuerung für Java. Dies erleichtert den Einstieg und macht die Sprache zugänglicher, aber auch für erfahrene Entwickler ist das eine willkommene Neuerung, wenn man schnell mal ein paar Zeilen Code testen möchte.

Das Konzept der unnamed Klasse ist nicht gänzlich neu, in vergangenen Versionen gab es auch schon unnamed Module und unnamed Packages.

Hier ist noch spannend zu erwähnen, dass man euch mehrere main Methoden mit unterschiedlichen Parametern definieren kann. Das ist ein weiterer Schritt in Richtung Flexibilität und Einfachheit. In diesem Fall entscheidet dann ein entsprechendes Launch-Protocol, welche main Methode aufgerufen wird.

Virtual Threading

JEP 444: Virtual Threads

Last but not least, sicherlich das wichtigste Features des JDK 21 Release, die virtual Threads!

Nehmen wir an, wir haben eine API, welche wir abfragen müssen, jeder API Call dauert eine Sekunde.

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

Wir haben eine lange Liste von Objekten, welche wir mit Daten aus der API anreichern müssen. Die API hat genug Power, also können wir parallel Arbeiten, um die Daten schneller zu erhalten.

Bis jetzt haben wir das mit Thread oder ExecutorService gemacht, aber das ist nicht wirklich effizient, da die Threads zu viel Overhead haben.

 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 dem Codeausschnitt haben wir 100 Objekte, für jedes Objekt starten wir einen “echten” Thread, welcher die API abfragt. Die Ausführung dieses Programmes dauert im Schnitt 1100 Millisekunden. (Und wir nutzen String Templates, cool! 🚀)

Schauen wir uns nun das gleiche Beispiel mit virtual Threads an:

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

Viel geändert hat sich nicht, das Programm läuft auch um die 1100 Millisekunden, aber der Unterschied ist, dass wir keine “echten” Threads mehr verwenden, sondern “virtuelle” Threads.

Mittels eines Diffs ist einfach zu sehen, was die effektiven Änderungen sind:

img_1.png

Es ist also sehr einfach, aus einem echten auf einen virtuellen Thread zu wechseln.

Nun aber @Scale! Was passiert, wenn wir plötzlich 100'000 Objekte haben? Also passen wir die Nummer in der Schleife an und starten das Programm neu.

Die alte Lösung läuft: 19222 Millisekunden, was knapp 20 Sekunden entspricht. Recht lange! 😴

Die neue Lösung mit virtual Threads läuft: 1722 Millisekunden, was knapp 2 Sekunden entspricht. Das ist ein riesiger Unterschied!

Denken wir das nun weiter, wenn wir beispielweise einen Webserver haben, welcher Millionen von Anfragen pro Sekunde verarbeiten muss, dann ist das ein riesiger Unterschied. Oder ist das nur in der Theorie?

Zahlreiche Benchmarks im Internet zeigen, dass die virtual Threads bis zu einem gewissen Punkt deutlich schneller sind. Ab diesem Punkt performen die virtual Threads gleich gut oder sogar schlechter als die “echten” Threads. Weitere Informationen findet ihr hier.

Sicher stellt sich hier die Frage, wie in Zukunft Java Applikationen aufgebaut werden, klassische sequentiell oder mit echten Thread, nach dem Reactive Programming Prinzip oder mit virtuellen Thread?

Diese Frage ist schwer zu beantworten, da es sehr stark von der Applikation und den Anforderungen abhängt. Wir werden in Zukunft die virtual Threads im Auge behalten und sicherlich bei dem einen oder anderen Quarkus Projekt mittels RunOnVirtualThread experimentieren.

Wenn du an diesem Punkt mehr über virtual Threads erfahren willst, dann lege ich dir diesen Quarkus Artikel ans Herzen!

Varia

Kurz notiert:

Mittels Math.clamp() können wir neu prüfen, ob ein Wert innerhalb eines bestimmten Bereichs liegt. Ist der Wert ausserhalb, wird der entsprechende Min- oder Max-Wert zurückgegeben. Das könnte ganz praktisch sein!

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

Ausserdem hat sich wieder, wie die letzten Releases etwas an den Switch Statements getan, dort kann man nun direkt ein Pattern-Matching ohne lästige instanceof Checks implementieren. Zusätzlich gibt es neu auch weitere Matching Möglichkeiten für Record Types.

LTS

Hier sei nochmals erwähnt, dass es sich um eine LTS Version handelt, welche länger als die normalen Releases unterstützt wird.

Aus meiner Sicht lohnt sich der Wechsel! Die komplette Liste aller JEPs vom Sprung von der letzten LTS-Version Java 17 auf JDK 21 findet ihr hier.

Fazit

Coole und wirklich praktische Features wurden uns mit JDK 21 beschert! Die virtual Threads werden sicherlich die Performance von Webservern oder gar generell Application Servern deutlich verbessern! Sequenced Collections und String Templates sind sicherlich Features, die wir in Zukunft sehr oft nutzen werden.

Ausblick

Java 22 steht bereits in den Startlöchern und soll im März 2024 veröffentlicht werden. Aktuell sind schon einige Features bekannt, die wichtigsten und coolsten Features schauen wir uns im nächsten TechUp an! 🚀 JDK 23 ist auch schon in Planung und es gibt schon einige spannende Features, die uns Entwickler erwarten.

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.