[EN] Java 24 & 25 Vertieft: Code-Beispiele für die neuen Sprach-Features

Julian | Sep 29, 2025 min read

Hello everyone,

In the last article we highlighted the big steps of Java 24 and Java 25 - from Stream Gatherers to Scoped Values. Now it’s time to roll up your sleeves. The real power of these releases lies in the small but powerful improvements to the language that make our code shorter, safer and more expressive.

We dive into the code examples for the finalized and advanced preview features.


1. Pattern Matching for switch and instanceof (Continued)

Pattern matching in Java becomes more powerful with every version. It makes our code that checks for types or values ​​incredibly much cleaner.

Before (Old, Pre-Java 21):

Checking and then casting a type required a lot of boilerplate:

// Pruefen und Casten mit instanceof
public static void process(Object obj) {
    if (obj instanceof String) {
        String s = (String) obj; // Manueller Cast
        System.out.println("String Laenge: " + s.length());
    } else if (obj instanceof Integer) {
        Integer i = (Integer) obj;
        System.out.println("Integer Wert: " + i * 2);
    }
}

Now (Java 24/25 Preview, Pattern Matching):

With advanced pattern matching (especially the extended instanceof and switch) this becomes much more elegant. This is called Pattern Matching for Primitives, which has been further developed in Java 25.

// Pattern Matching fuer switch (erweitert in Java 25 Preview)
public static void process(Object obj) {
    switch (obj) {
        case String s -> System.out.println("String Laenge: " + s.length());
        case Integer i -> System.out.println("Integer Wert: " + i * 2);
        // Pruefung auf null ist direkt moeglich
        case null -> System.out.println("Eingabe war null.");
        default -> System.out.println("Unbekannter Typ.");
    }
}

The new syntax not only reduces code, but also makes pattern matching for primitive types (like int, long) possible, which unifies all type analysis in Java.


2. Stream Gatherers (Java 24 Finalisiert)

As previously mentioned, stream gatherers revolutionize intermediate operations in stream pipelines by enabling new, complex operations such as “windowing” (grouping in fixed or sliding blocks).

The problem before:

For example, to process the elements of a stream in batches (groups), one often had to exit the stream, use a collector solution, or iterate manually.

The elegant solution (Java 24):

The Gatherers.windowFixed(n) is ideal for batch processing data, e.g. before sending it to an external API.

import java.util.stream.Gatherers;
import java.util.stream.Collectors;

List<Integer> numbers = List.of(10, 20, 30, 40, 50, 60, 70);

List<List<Integer>> batches = numbers.stream()
    // Erstellt feste Fenster der Groesse 3
    .gather(Gatherers.windowFixed(3)) 
    .collect(Collectors.toList());

// batches enthaelt: [[10, 20, 30], [40, 50, 60], [70]]

// Beispiel fuer die Verwendung des letzten Elements (folding/aggregation)
Optional<String> folded = numbers.stream()
    .map(String::valueOf)
    .gather(Gatherers.fold(() -> "", (acc, element) -> acc + element))
    .findFirst(); 

// folded enthaelt: "10203040506070" (Alle Elemente zusammengefaltet)

This is a massive gain in expressiveness and allows us to represent complex logic within the familiar Stream API.


3. Scoped Values (JEP 506, Java 25 Finalisiert)

Scoped Values solve the problems of ThreadLocal in modern Java applications, especially in the context of Virtual Threads (Loom). They make implicit data sharing secure, immutable and performant.

Das Problem mit ThreadLocal in Virtual Threads:

ThreadLocal can be slow in Loom applications and cause memory problems because each Virtual Thread would have to maintain a copy of the ThreadLocal value even if it is not needed. In addition, the value is mutable, which can lead to race conditions.

The clean solution (Java 25):

Scoped Values are bound to an execution context rather than a physical thread and are immutable.

// 1. Deklaration (sollte immer static final sein)
private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();

public void handleRequest(String id) {
    // 2. Wert binden (immutable) und Scope definieren
    ScopedValue.where(REQUEST_ID, id)
               .run(() -> businessLogic());
}

public void businessLogic() {
    // 3. Aufruf in tief verschachtelten Methoden ohne Parameteruebergabe
    logContext(); 
    // ... Aufrufe, auch ueber Virtual Threads hinweg, im selben Scope
}

public void logContext() {
    // 4. Wert sicher abrufen
    String currentId = REQUEST_ID.get(); 
    System.out.println("Verarbeite Anfrage mit ID: " + currentId);
}

// Aufruf:
// handleRequest("tx-456"); 
// Ausgabe: "Verarbeite Anfrage mit ID: tx-456"

This pattern is the standard for propagating tracing IDs, user sessions, or configurations in asynchronous and concurrent microservices. It is a necessary foundation for the future of server-side Java development.


Conclusion

Java 24 and Java 25 do not provide us with revolutionary new paradigms, but rather refine the language and the platform in key areas:

  1. Code Expressivity (Pattern Matching, Gatherers)
  2. Performance & Start Time (AOT Profiling)
  3. Concurrency & Security (Scoped Values)

By integrating these features into our daily projects, we make our code not only compliant with the latest Java version, but also fundamentally better, more robust and future-proof. It’s the perfect time to take advantage of these improvements and realize the full potential of modern Java.