Das Neuste aus der Java Welt: JDK 22 Hands-On Einblicke für Developer

15.05.2024Tom Trapp
Tech Java JVM openjdk Cloud Computing DevOps Stay Tuned

Banner

Nach 21 kommt? Genau, 22! 💡 JDK 22 ist das letzte JDK Update, welches seit dem 19. März 2024 verfügbar ist.

In diesem TechUp wollen wir uns anschauen, was es Neues gibt und was sich geändert hat.

Selbstverständlich findest du alle Code-Beispiele in unserem TechHub GitHub JDK 22 Repository.

Solltest du den letzten LTS (Long Term Support) Release JDK 21 noch nicht kennen, dann schaue dir doch mein TechUp zu JDK 21 an.

Schauen wir uns nun einige der spannenden JEP - JDK Enhancement Proposals - an, die in JDK 22 enthalten sind.

Preview Features

Nachfolgend werden wir uns einige Preview-Features ansehen, die in JDK 22 enthalten sind. Diese speziellen Features sind noch nicht vollständig und können sich in zukünftigen Versionen ändern oder sogar entfernt werden. Die können mit dem Flag -enable-preview aktiviert werden. Alternativ erkennt z.B. IntelliJ die Verwendung von Preview-Features und schlägt vor, das Flag zu setzen.

img.png

423: Region Pinning for G1

Ein sehr technisches Feature, welches die Garbage Collection verbessern soll. An dieser Stelle möchte ich nicht weiter darauf eingehen, da es sehr spezifisch ist und nicht für jeden Entwickler relevant ist. Weitere Informationen findest du hier.

447: Statements before super(…) (Preview)

Bei JEP-447 handelt es sich um ein Preview-Feature, um Logik vor dem Aufruf des Super-Konstruktors zu platzieren.

In Java 8 wurde die Möglichkeit eingeführt, dass Konstruktoren von Subklassen den Konstruktor der Superklasse aufrufen können. Allerdings musste dies als erste Anweisung im Konstruktor geschehen. Mit JEP-447 wird diese Einschränkung aufgehoben und es ist nun möglich, Anweisungen vor dem Aufruf des Superkonstruktors zu platzieren.

Bisher musste das immer wie folgt aussehen:

1
2
3
4
5
6
public class MyClass extends SuperClass {
    public MyClass() {
        super();
        // do something
    }
}

Dadurch ergeben sich teilweise umständliche Aufrufe, wie beispielsweise:

1
2
3
4
5
6
7
8
public class MyClass extends SuperClass {
    public MyClass(String name) {
        super();
        if (name == null) {
            throw new IllegalArgumentException("Name must not be null");
        }
    }
}

In diesem Beispiel ist zu sehen, dass die Superklasse unnötigerweise initialisiert wird, obwohl der Name noch gar nicht geprüft wurde. Ist der Name null, so würde eine Exception geworfen werden. Die Initialisierung der Superklasse war in diesem Fall unnötig.

Mit JEP-447 kann dies nun wie folgt aussehen:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class MyClass extends SuperClass {
	public MyClass(String name) {
		if (name == null) {
			throw new IllegalArgumentException("Name must not be null");
		} else {
			System.out.println(STR."Hello \{name}");
		}
		super();
	}
}

Das war bisher in einer abgespeckten Art und Weise, mit sogenannten Hilfs-Methoden möglich, allerdings nur auf einer Zeile innerhalb des super-Konstruktor-Aufrufs.

Das ist nicht nur bei Input-Validation praktisch, sondern auch, wenn die Parameter vorbereitet werden müssen für den Super-Konstruktor.

Beispielsweise wäre eine solche Logik neu einfacher zu implementieren:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public FileReader(File file) throws IOException {
	String content = readFileAsString(file.getPath());
	if (content.isEmpty())
		throw new IllegalArgumentException("File content cannot be empty");

	int processedData = switch (content)
	{
		case String data when data.startsWith("XML") -> processXML(data);
		case String data when data.startsWith("JSON") -> processJSON(data);
		default -> handleDefaultFormat(content);
	};
	super(processedData);
}

Hier ist schön zu sehen, dass wir erst den Input Parameter prüfen, anschliessend entsprechend darauf reagieren und dann, mit diesem Resultat, den Super-Konstruktor aufrufen.

Das ganze Programm findest du im Repo!

Wichtig zu beachten ist, dass nur static fields und static methods vor dem Super-Konstruktor-Aufruf aufgerufen werden können. Eigentlich logisch, da die Instanz ja noch nicht erstellt wurde.

454: Foreign Function & Memory API

Diese neue API erlaubt es, aus einer Java-Anwendung heraus, native Funktionen aufzurufen und Speicher zu verwalten.

Auch hier wollen wir nicht genauer auf JEP-454 eingehen, da es ein sehr spezifisches Feature ist und nicht für jeden Entwickler relevant ist.

456: Unnamed Variables & Patterns

Mit JEP-456 werden unbekannte Variablen eingeführt. 👻

Hierbei geht es um ungenutzte Variablen, die mit einem Unterstrich _ markiert werden können. Diese Variablen können in einem Pattern-Matching-Statement verwendet werden, ohne dass sie explizit genutzt werden müssen.

Schauen wir uns zuerst ein altes Beispiel an:

1
2
3
4
5
6
try {
    int i = Integer.parseInt(numberString);
    System.out.println(STR."Number: \{i}");
} catch (NumberFormatException ex) {
    System.out.println(STR."Bad number: \{numberString}");
}

In dem Code ist zu sehen, dass die Exception ex nicht verwendet wird. Mit JEP-456 kann dies nun wie folgt aussehen:

1
2
3
4
5
6
try {
    int i = Integer.parseInt(numberString);
    System.out.println(STR."Number: \{i}");
} catch (NumberFormatException _) {
    System.out.println(STR."Bad number: \{numberString}");
}

Zugegeben, das ist jetzt nicht die Welt, aber es zeigt, dass es in Zukunft möglich sein wird, ungenutzte Variablen zu markieren und somit den Code lesbarer zu machen.

Und es verbessert das Sonar Rating, wenn ungenutzte Variablen nicht mehr als Fehler angezeigt werden. 😉

Dies soll neu auch in anderen Building-Blocks von Java möglich sein, wie beispielsweise in switch-Statements oder try-with-resources-Statements.

457: Class-File API (Preview)

Bei JEP-457 handelt es sich um ein Preview-Feature, welches es ermöglicht, den Inhalt von Class-Dateien zu lesen und zu schreiben. Speziell geht es auch um das Parsing, Generieren und Transformieren von Java class files.

Mir stellt sich die Frage, ob und wann man das will? Dynamisch zur Laufzeit neue Java-Dateien generieren? Oder Java-Dateien zur Laufzeit verändern? Widerspricht das nicht dem Prinzip von Java, dass es eine statisch typisierte Sprache ist? Und dass der Code zur Compile-Zeit geprüft wird?

Hierbei handelt es sich um ein Preview-Feature, sollte das in zukünftigen Versionen von Java eine grössere Rolle spielen, so wird es sicherlich nochmals genauer angeschaut.

458: Launch Multi-File Source-Code Programs

Hierbei geht es darum, dass der Java Launcher automatisch erkennt, welche Abhängigkeiten ein Multi-File Source-Code Programm hat und diese automatisch lädt.

So kann mit JEP-458 ein Programm, welches andere Klassen importiert, direkt gestartet werden. Die importierten Klassen werden dann automatisch mit kompiliert und geladen.

459: String Templates (Second Preview)

Hier hat sich seit JDK 21 nicht viel geändert. Aus meiner Sicht weiterhin ein cooles Feature, alle Informationen findest du hier in meinem TechUp zu JDK 21.

Die Preview Features sind immer in unterschiedlichen Releases implementiert, daher ist es nicht verwunderlich, dass sich hier nicht viel getan hat.

460: Vector API (Seventh Incubator)

Bei JEP-460, der Vector API, handelt es sich sage und schreibe um das siebte Inkubator-Release. Dies bedeutet, der Release Candidate ist noch nicht fertig und es wird weiterhin daran gearbeitet. Das erste Mal wurde die Vector API in JDK 16 eingeführt und wird seitdem stetig weiterentwickelt.

Um was geht es? Die API liefert einen Standard, um Vektorberechnungen zu formulieren. Dies ist besonders nützlich, da spezielle Vektorprozessoren für eine parallele Verarbeitung von Vektoroperationen sorgen. Im Gegensatz zur klassischen, skalaren Berechnung kann dies speziell bei datenintensiven Aufgaben zu einer massiven Performancesteigerung führen.

Laut Dokumentation werden aktuell x64 and AArch64 CPUs unterstützt.

461: Stream Gatherers (Preview)

Die Stream-API, die vor 10 Jahren mit Java 8 eingeführt wurde, ist ein sehr mächtiges Tool in Java und hat sich seitdem stetig weiterentwickelt Mit JEP-461 wird die Stream-API um sogenannte Gatherers erweitert.

Gatherers sind eine Art von Collectors, die es ermöglichen, Streams zu sammeln und zu verarbeiten. Bisher war es nur möglich, Streams zu sammeln und zu verarbeiten, wenn alle Elemente des Streams verfügbar waren. So können beispielsweise unterschiedliche, eigen implementierte GroupBy-Operationen durchgeführt werden.

Schauen wir uns ein Beispiel an. Wir haben eine Liste von Wörtern und wollen diese immer in Gruppen von 4 zusammenfassen.

Bisher wäre dies beispielsweise wie folgt möglich gewesen:

1
2
3
4
5
6
7
List<String> words = List.of("Hello", "World", "Java", "Is", "Awesome", "And", "So", "Are", "You");

List<List<String>> groupedWords = words.stream()
		.collect(Collectors.groupingBy(word -> words.indexOf(word) / 4))
		.values()
		.stream()
		.toList();

Nicht wirklich elegant und nicht direkt ersichtlich, was hier passiert. Mit JEP-461 wird dies nun wie folgt möglich sein:

1
2
3
4
5
List<String> words = List.of("Hello", "World", "Java", "Is", "Awesome", "And", "So", "Are", "You");

List<List<String>> groupedWords = words.stream()
		.gather(Gatherers.windowFixed(4))
        .toList();

Deutlich einfacher, mit Gatherers.windowFixed(4) wird die Liste in Gruppen von 4 zusammengefasst.

Die Ausgabe ist bei beiden Beispielen die gleiche:

1
[[Hello, World, Java, Is], [Awesome, And, So, Are], [You]]

Wichtig zu erwähnen ist hier, dass es sich bei Gathers um Intermediate-Operationen handelt, die nur in Streams verwendet werden können.

Grundlegend besteht ein Gather aus vier Teilen:

  • Integrator - die Hauptfunktion, welche einen State, ein Element und ein DownStream entgegennimmt und einen boolean zurückgibt
  • Initializer - die Funktion, welche den Initial-Zustand des Gatherers definiert, einfach gesagt handelt es sich hier um einen Supplier
  • Finishers - die Funktion, welche den Zustand des Gatherers abschliesst und das Resultat zurückgibt, technisch ein BiConsumer
  • Combiner - wird nur benötigt, wenn der Gatherer parallel verwendet wird, hier wird der Zustand zusammengeführt

Die Gatherers Klasse bietet neben windowFixed noch weitere Methoden an.

Schauen wir uns ein paar Beispiele von selbst implementierten Gatherers an.

Simple Gatherer

Schauen wir uns einen einfacher Gatherer an, wo wir nur einen Integrator nutzen. Selbstverständlich könnten wir hierfür auch eine normale map-Operation verwenden, aber es zeigt, wie Gatherer funktionieren.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void main()
{
	List<String> strings = List.of("a", "b", "c", "d", "e", "f", "g", "h", "i");

	System.out.println(
			strings.stream().gather(printUpper())
					.toList());
}

public static Gatherer<? super String, ?, String> printUpper()
{
	Gatherer.Integrator<Void, ? super String, String> integrator =
			(state, element, downstream) -> {
				downstream.push(element.toUpperCase());
				return true;
			};
	return Gatherer.ofSequential(integrator);
}

Zu sehen ist, dass innerhalb von unserem Integrator die Elemente in Grossbuchstaben umgewandelt und in den Stream geschrieben werden. Mit return true geben wir an, dass wir weitermachen wollen. Sobald wir false zurückgeben, wird der Stream abgebrochen.

Stateful Streams

Wir haben nun eine Liste von unsortierten Zahlen und wollen immer nur Zahlen in unseren Stream haben, welche grösser sind als die Zahlen, welche wir bereits gesehen haben.

Das folgende Programm gibt [1, 2, 5, 6, 7, 11, 20] aus.

 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
void main()
{
	List<Integer> numbers = List.of(1, 2, 5, 3, 2, 3, 4, 5, 6, 7, 5, 11, 8, 9, 10, 20);

	System.out.println(
			numbers.stream().gather(getOnlyIncreasingNumbers(Comparator.comparingInt(a -> a)))
					.toList());
}

public static <T> Gatherer<T, ?, T> getOnlyIncreasingNumbers(
		Comparator<T> comparator)
{
	Supplier<AtomicReference<T>> initializer = AtomicReference::new;
	Gatherer.Integrator<AtomicReference<T>, T, T> integrator =
			(state, element, downstream) -> {
				T largest = state.get();
				var isLarger = largest == null
						|| comparator.compare(element, largest) > 0;
				if (isLarger)
				{
					downstream.push(element);
					state.set(element);
				}
				return true;
			};
	return Gatherer.ofSequential(initializer, integrator);
}
  • Zuerst legen wir unsere Liste an
  • Dann initialisieren wir einen Stream (Source), rufen unseren Gatherer (Intermediate) auf und sammeln das Resultat (Terminal) und geben dies aus.
  • Der Gatherer nimmt einen Comparator entgegen, welcher die Zahlen vergleicht.
  • Anschliessend legen wir uns unseren Initializer an, welcher eine AtomicReference zurückgibt. Somit können wir den Zustand speichern.
  • Innerhalb von unserem Gatherer-Integrator prüfen wir, ob die Zahl grösser ist als die bisher grösste Zahl. Ist dies der Fall, so wird die Zahl in den Stream geschrieben und als neue grösste Zahl gespeichert.
  • Der Gatherer gibt immer true zurück, da wir immer weitermachen wollen. Würden wir hier false zurückgeben, so würde der Stream keine weiteren Elemente mehr verarbeiten.
  • Den Gatherer erstellen wir mit Gatherer.ofSequential(initializer, integrator)

Kein einfaches Beispiel, aber es zeigt, wie mächtig Gatherer sein können.

Stateful mit Finisher

Nehmen wir nochmals das vorherige Example, wir wollen die letzte Zahl, welche in der Liste ist multiplizieren mit 2. Im Integrator können wir das nicht tun, da wir nicht wissen, ob es die letzte Zahl ist. Daher müssen wir dies im Finisher machen.

Das folgende Programm gibt [1, 2, 5, 6, 7, 11, 20, 40] aus.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public static Gatherer<? super Integer, ?, Integer> getOnlyIncreasingNumbersWithFinisher(
		Comparator<Integer> comparator)
{
	Supplier<AtomicReference<Integer>> initializer = AtomicReference::new;
	Gatherer.Integrator<AtomicReference<Integer>, ? super Integer, Integer> integrator =
			(state, element, downstream) -> {
				Integer largest = state.get();
				var isLarger = largest == null
						|| comparator.compare(element, largest) > 0;
				if (isLarger)
				{
					downstream.push(element);
					state.set(element);
				}
				return true;
			};

	BiConsumer<AtomicReference<Integer>, Gatherer.Downstream<? super Integer>> finisher = (state, downstream) -> {
		Integer lastElement = state.get();
		downstream.push(lastElement * 2);
	};

	return Gatherer.ofSequential(initializer, integrator, finisher);
}

Hierfür nutzen wir nun einen vollständig typisierten Gatherer, welches einen Zustand, ein Element und einen Downstream entgegennimmt.

Im Finisher sehen wir, dass wir auf das letzte Element zugreifen können und dieses verarbeiten können. Wir können auch den downstream verändern, wenn wir wollen.

Spannendes Feature, welches die Stream-API nochmals erweitert und neue Möglichkeiten bietet.

462: Structured Concurrency (Second Preview)

Bei JEP-462 handelt es sich um ein Preview-Feature, welches es ermöglicht, asynchrone Operationen in einer strukturierten Art und Weise zu verwalten. So können zusammenhängende Tasks, welche parallel in unterschiedlichen Threads laufen, in einer Gruppe zusammengefasst und verwaltet werden. Das macht speziell das Error Handling und das Abbrechen von Tasks einfacher.

Schauen wir uns zuerst ein klassisches Beispiel 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
void main()
{
	ExecutorService executor = Executors.newFixedThreadPool(2);

	try
	{
		System.out.println("Starting the search for Tom");
		Future<String> futureTom = executor.submit(UserSearcher::findTom);

		System.out.println("Starting the search for Tim");
		Future<String> futureTim = executor.submit(UserSearcher::findTim);

		System.out.println("Something is running in the background...");

		String result = futureTom.get() + ", " + futureTim.get();

		System.out.println(result);
	}
	catch (Exception e)
	{
		e.printStackTrace();
	}
	finally
	{
		executor.shutdown();
	}
}

Beide Threads werden unabhängig voneinander gestartet und laufen parallel. Das Resultat wird erst dann zusammengeführt, wenn beide Threads fertig sind. Es gibt keine logische Verknüpfung zwischen den beiden Threads, sollte ein Thread fehlschlagen, so wird der andere Thread weiterhin ausgeführt.

Neu führt JEP-462 die Klasse StructuredTaskScope ein, welche in einem try-with-resources-Block verwendet werden kann.

Beim Starten wird neu ein Supplier definiert und nicht mehr ein Future Objekt.

 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
30
31
void main()
{
	ExecutorService executor = Executors.newFixedThreadPool(2);

	try (var scope = new StructuredTaskScope.ShutdownOnFailure())
	{
		System.out.println("Starting the search for Tom");
		Supplier<String> tom = scope.fork(UserSearcher::findTom);

		System.out.println("Starting the search for Tim");
		Supplier<String> tim = scope.fork(UserSearcher::findTim);

		System.out.println("Something is running in the background...");

		scope.join()            // Join both subtasks
				.throwIfFailed();  // ... and propagate errors

		System.out.println("Something is running still in the background...");

		String result = tom.get() + ", " + tim.get();
		System.out.println(result);
	}
	catch (Exception e)
	{
		e.printStackTrace();
	}
	finally
	{
		executor.shutdown();
	}
}

Schön zu sehen ist, dass mit scope.join() beide Subtasks zusammengeführt werden und mit throwIfFailed() Fehler propagiert werden. So würde ein Fehler in einem der Subtasks dazu führen, dass der andere Subtask abgebrochen wird, sollte dieser noch laufen.

Die Ausgabe der Logs ist identisch, spannend wird es, wenn unsere Subtasks fehlschlagen.

Error Handling

Gehen wir nun davon aus, dass unsere beiden Methoden wie folgt implementiert sind:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
	public static String findTom() throws InterruptedException
	{
		Thread.sleep(5000);
		System.out.println("Tom found");
		return "Tom";
	}

	public static String findTim()
	{
		throw new RuntimeException("Tim not found");
	}

Tom wird nach 5 Sekunden gefunden, Tim wird nicht gefunden und wirft eine Exception.

Unser Output in der alten Variante ist:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
Starting the search for Tom
Starting the search for Tim
Something is running in the background...
Tom found
java.util.concurrent.ExecutionException: java.lang.RuntimeException: Tim not found
	at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:122)
	at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
	at OldStyle.main(OldStyle.java:21)
Caused by: java.lang.RuntimeException: Tim not found
	at com.bnova.techhub.jep462.UserSearcher.findTim(UserSearcher.java:14)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642)
	at java.base/java.lang.Thread.run(Thread.java:1570)

Schön zu sehen ist, dass Tom gefunden wurde, Tim nicht und der Fehler korrekt ausgegeben wird. Das Programm benötigt aber die kompletten 5 Sekunden, bis es den Fehler erkennt.

Mit der neuen Variante wird der Fehler sofort erkannt und das Programm bricht ab:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
Starting the search for Tom
Starting the search for Tim
Something is running in the background...
java.util.concurrent.ExecutionException: java.lang.RuntimeException: Tim not found
	at java.base/java.util.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1323)
	at java.base/java.util.concurrent.StructuredTaskScope$ShutdownOnFailure.throwIfFailed(StructuredTaskScope.java:1300)
	at NewStyle.main(NewStyle.java:23)
Caused by: java.lang.RuntimeException: Tim not found
	at com.bnova.techhub.jep462.UserSearcher.findTim(UserSearcher.java:14)
	at java.base/java.util.concurrent.StructuredTaskScope$SubtaskImpl.run(StructuredTaskScope.java:892)
	at java.base/java.lang.VirtualThread.run(VirtualThread.java:329)

Process finished with exit code 0

Hier ist schön zu sehen, dass Tom nicht gefunden wird, weil der vorher verknüpfte Task fehlschlägt und daher der Thread sofort abgebrochen wird.

Das Feature bietet aber noch weitere Möglichkeiten, wie beispielsweise bestimmte custom shutdown policies oder das Processing von parallelen Results in einem Stream.

Cooles Feature, welches das Error Handling in parallelen Java Programmen nochmals verbessert. 🚀 Ich bin gespannt, ob und wie dieses Feature Einzug in Libraries wie Spring oder Quarkus halten wird.

463: Implicitly Declared Classes and Instance Main Methods (Second Preview)

Hier hat sich seit JDK 21 nichts geändert. Aus meiner Sicht weiterhin ein hilfreiches Feature, alle Informationen findest du hier in meinem TechUp zu JDK 21.

464: Scoped Values (Second Preview)

Und last but not least, JEP-464 - Scoped Values. Hierbei handelt es sich um die second Preview-Version, welche es ermöglicht, Werte in einem bestimmten Scope zu speichern und abzurufen. Konkret wird ein neuer ScopedValue-Type eingeführt, welcher immutable ist und es so einfacher macht, Informationen mit Child-Frames im gleichen Thread oder gar mit Sub-Threads zu teilen.

In der Vergangenheit musste man hierfür ThreadLocal Variablen verwenden, um auf beispielsweise Request-Informationen zuzugreifen, wenn diese nicht Teil der Methodensignatur waren.

Neu soll das via ScopedValue möglich sein, welches als Instanzvariable angelegt werden kann. Schauen wir uns ein einfaches Beispiel an:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private final static ScopedValue<String> NAME
		= ScopedValue.newInstance();

void main()
{
	ScopedValue.where(NAME, "Tom").run(() -> greet());
}

void greet()
{
	System.out.println(STR."Hello \{NAME.get()}!");
}

Zuerst legen wir uns ein ScopedValue an, welches den Namen speichert. In unserer main-Methode setzen wir den Wert auf Tom und rufen anschliessend die greet-Methode auf. Hier ist schön zu sehen, dass der Aufruf von greet im gleichen Scope stattfindet und somit auf den Wert Tom zugreifen kann. In der Methoden können wir dann auf das ScopedValue zugreifen und den Wert auslesen.

Nun wird es spannend, wir passen das ScopedValue an!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private final static ScopedValue<String> NAME
		= ScopedValue.newInstance();

void main()
{
	ScopedValue.where(NAME, "Tom").run(() -> greet());
}

void greet()
{
	System.out.println(STR."Hello \{NAME.get()}!");
	ScopedValue.where(NAME, "Tim").run(() -> goodbye());
	goodbye();
}

void goodbye()
{
	System.out.println(STR."GoodBye \{NAME.get()}!");
}

Was meinst du was kommt heraus? Wir haben eine neue Methode goodbye, welche den Wert aus dem ScopedValue ausliest und ausgibt. In der greet-Methode setzen wir, nach dem Begrüssen von Tom, den Wert auf Tim und rufen anschliessend die goodbye-Methode auf. Anschliessend rufen wir aber nochmals die goodbye-Methode auf.

Die Ausgabe ist recht spannend, und eigentlich ganz logisch:

1
2
3
Hello Tom!
GoodBye Tim!
GoodBye Tom!

Zuerst wird Hello Tom! ausgegeben, da wir den Wert auf Tom gesetzt haben. Der Wert ist für den Scope gültig, in welchem wir uns befinden. Dann starten wir aber einen neuen Scope und setzen diesen Wert auf Tim. In diesem Scope ist der Wert Tim gültig. Anschliessend rufen wir aber goodbye nochmals auf, ohne einen neuen Scope zu starten. Daher wird hier der Wert Tom ausgegeben.

Eine weitere Möglichkeit, welche uns die ScopedValue API bietet, ist das Empfangen von Werten aus dem entsprechenden, scoped Aufruf.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private final static ScopedValue<String> NAME = ScopedValue.newInstance();

void main() throws Exception
{
	var name = ScopedValue.where(NAME, "Tom").call(() -> getName());
	System.out.println(STR."The name is \{name}");
}

String getName()
{
	return NAME.get().toUpperCase();
}

Hier sehen wir, dass unsere getName-Methode den Wert aus dem ScopedValue ausliest und diesen in Grossbuchstaben zurückgibt. In unserer main Methoden rufen wir die getName-Methode auf und speichern das Resultat in einer Variable, diese Variable wird dann ausgegeben.

Auch hier haben wir wieder die Möglichkeit, mehrere Scopes zu starten und zu beenden, um unterschiedliche Werte zu setzen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private final static ScopedValue<String> NAME = ScopedValue.newInstance();

void main() throws Exception
{
	var name = ScopedValue.where(NAME, "Tom").call(() -> getName());
	var anotherName = ScopedValue.where(NAME, "Tim").call(() -> getName());
	System.out.println(STR."The name is \{name}");
	System.out.println(STR."The another name is \{anotherName}");
}

String getName()
{
	return NAME.get().toUpperCase();
}

Selbstverständlich können wir auch mehr als nur ein ScopedValue nutzen.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
private final static ScopedValue<String> NAME = ScopedValue.newInstance();
private final static ScopedValue<String> AGE = ScopedValue.newInstance();

void main() throws Exception
{
	ScopedValue.where(NAME, "Tom").where(AGE, "27").run(() -> getInfo());
}

void getInfo()
{
	System.out.println(STR."Hello \{NAME.get()}!");
	System.out.println(STR."Age \{AGE.get()}!");
}

Cool, oder? Das ist ein sehr mächtiges Feature, welches das Arbeiten mit Threads und Scopes nochmals vereinfacht. 🌶️

Fazit

JDK 22 ist der erste Release nach einem LTS Release und bringt wie erwartet ein paar hilfreiche Features, die das Entwickeln in Java noch einfacher machen. Anfangs war ich skeptisch, ob die Preview-Features wirklich so hilfreich sind, aber ich muss sagen, dass ich positiv überrascht bin. Gerade die Stream-Gatherer und die Scoped-Values sind sehr mächtige Features, die das Entwickeln in Java nochmals vereinfachen. Aber auch die Structured Concurrency und die Statements vor dem Super-Konstruktor sind nicht zu unterschätzen und können sich als hilfreich erweisen.

Das nächste TechUp zu JDK 23 wird sicherlich spannend, ich bin gespannt, was uns da erwartet, stay tuned!

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.