Exploring Java 23

Java 23, released on September 17, 2024, is a short-term release that brings a mix of innovative features to enhance developer productivity and explore potential future capabilities. While this version includes one incubator feature and several preview features, this blog highlights some key additions, including Stream Gatherers, Structured Concurrency, Scoped Values, and Flexible Constructor Bodies. These features, though in preview, offer a glimpse into the evolving capabilities of the Java language.

Markdown Documentation Comments

Java now lets you write documentation comments using Markdown, a simpler and cleaner way to format text.

1. Simple Syntax: Markdown is lightweight and more readable.

2. Cleaner Code: Your comments look neat and are easy to maintain.

3. Compatible: Old Javadoc style still works, so no breaking changes.

Old Javadoc:



/**
 * This is a simple example of adding two numbers.
 * <ul>
 *   <li>First number: <code>a</code></li>
 *   <li>Second number: <code>b</code></li>
 * </ul>
 * @param a the first number
 * @param b the second number
 * @return the sum of a and b
 */
public int add(int a, int b) {
    return a + b;
}



                

New Markdown Style:


/// Adds two numbers.
///
/// - **First number:** `a`
/// - **Second number:** `b`
///
/// @param a the first number
/// @param b the second number
/// @return the sum of `a` and `b`
public int add(int a, int b) {
    return a + b;
}

                

With Markdown in Java 23, writing documentation is simpler and less of a headache. It's perfect for developers who want clean, modern comments in their code.

Stream Gatherers

In Java, a stream is a sequence of data elements that can be processed in parallel or sequentially. The Stream API provides various operations to manipulate these data sequences, such as map, filter, and reduce. However, these built-in operations may not cover all data processing needs. Stream Gatherers address this limitation by allowing developers to define custom intermediate operations that can transform data in ways not possible with existing methods.

Stream Gatherers in Java 23 allow you to create custom rules for filtering or transforming data in a stream pipeline. Unlike common operations like filter or distinct, Stream Gatherers give you fine-grained control over how elements are processed in the middle of a stream.

if you want to handle only consecutive duplicates while keeping the rest? Or if you want more complex filtering logic? That’s where Stream Gatherers shine.

Example 1: Removing Consecutive Duplicates

You have a list of numbers, and you want to remove only consecutive duplicates, keeping other duplicates intact. For example:

Input: [1, 2, 2, 3, 3, 4, 2, 5, 5]

Output: [1, 2, 3, 4, 2, 5]

Using a Set would remove all duplicates, giving [1, 2, 3, 4, 5], which is not what you want.


import java.util.List;
import java.util.stream.Gatherers;

public class StreamGatherersExample {
    public static void main(String[] args) {
        // List with consecutive duplicates
        List numbers = List.of(1, 2, 2, 3, 3, 4, 2, 5, 5);

        // Use Stream Gatherer to remove only consecutive duplicates
        List result = numbers.stream()
            .gather(Gatherers.distinctConsecutive())
            .toList();

        System.out.println(result); // Output: [1, 2, 3, 4, 2, 5]
    }
}

                 

Gatherers.distinctConsecutive() removes duplicates only if they are next to each other.

Example 2: Custom Filtering (No More Than Two Consecutive Elements)

Input: [1, 2, 2, 2, 3, 3, 4, 4, 4, 5]

Output: [1, 2, 2, 3, 3, 4, 4, 5]

A Set or distinct cannot handle this, as they remove all duplicates, not limit them to a count.


import java.util.List;
import java.util.stream.Gatherer;

public class CustomGathererExample {
    public static void main(String[] args) {
        // List with numbers repeating more than twice consecutively
        List numbers = List.of(1, 2, 2, 2, 3, 3, 4, 4, 4, 5);

        // Custom gatherer: Keep at most two consecutive occurrences
        Gatherer limitTwoConsecutive = Gatherers.custom((prev, current) -> {
            long count = prev.stream().filter(e -> e.equals(current)).count();
            return count < 2; // Keep only if fewer than 2 of the same element are already present
        });

        // Apply the custom gatherer
        List result = numbers.stream()
            .gather(limitTwoConsecutive)
            .toList();

        System.out.println(result); // Output: [1, 2, 2, 3, 3, 4, 4, 5]
    }
}

                

Custom Gatherer Logic: Checks how many times the current element has appeared in the gathered sequence.

If it’s less than 2, the element is kept; otherwise, it’s skipped.

Structured Concurrency

In traditional concurrent programming, managing multiple threads can be complex and error-prone. Tasks may start and finish at unpredictable times, leading to issues like resource leaks or unhandled exceptions. Structured Concurrency addresses these challenges by ensuring that concurrent tasks are started and completed within a well-defined scope, much like how local variables are confined to the block in which they are declared.

Simplified Error Handling: Errors in concurrent tasks are propagated to the parent task, allowing for centralized handling.

Automatic Cancellation: If one task fails, related tasks can be automatically canceled, preventing unnecessary work.

Improved Readability: Code structure mirrors the logical flow of tasks, making it easier to understand and maintain.

Implementing Structured Concurrency with StructuredTaskScope

Java 23 introduces the StructuredTaskScope class to facilitate structured concurrency. This class allows developers to create a scope within which multiple tasks can be initiated and managed collectively.

Example: Consider a scenario where you need to fetch user details and order information concurrently to generate a response. Using StructuredTaskScope, you can manage these tasks as follows:


import java.util.concurrent.ExecutionException;
import java.util.concurrent.StructuredTaskScope;

public class StructuredConcurrencyExample {

    public static void main(String[] args) {
        try {
            Response response = handle();
            System.out.println(response);
        } catch (ExecutionException | InterruptedException e) {
            e.printStackTrace();
        }
    }

    static Response handle() throws ExecutionException, InterruptedException {
        try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
            var userFuture = scope.fork(() -> findUser());
            var orderFuture = scope.fork(() -> fetchOrder());

            scope.join();           // Wait for all tasks to complete
            scope.throwIfFailed();  // Propagate exceptions if any

            String user = userFuture.resultNow();
            int order = orderFuture.resultNow();

            return new Response(user, order);
        }
    }

    static String findUser() {
        // Simulate fetching user data
        return "John Doe";
    }

    static int fetchOrder() {
        // Simulate fetching order data
        return 12345;
    }

    record Response(String user, int order) {}
}


                

Creating the Scope: A StructuredTaskScope.ShutdownOnFailure instance is created within a try-with-resources block. This ensures that resources are automatically managed, and if any task fails, remaining tasks are canceled.

Forking Tasks: The fork method initiates concurrent tasks to fetch user and order information.

Joining Tasks: The join method waits for all tasks within the scope to complete.

Handling Exceptions: The throwIfFailed method checks if any task has failed and propagates the exception.

Retrieving Results: The resultNow method retrieves the results of the completed tasks.

Returning the Response: A Response object is created using the fetched user and order information.

Pattern Matching with Primitive Types

In Java 23, pattern matching has been significantly enhanced, particularly with the introduction of primitive type patterns. This advancement allows developers to perform more expressive and concise type checks and data extractions, especially when working with primitive types.

Primitive Type Patterns: You can now use primitive types directly in pattern matching constructs, eliminating the need for unnecessary boxing and unboxing.

Extended instanceof and switch Support: The instanceof operator and switch expressions have been extended to work seamlessly with all primitive types.

Example: Pattern Matching with Primitive Types

Consider a scenario where you want to process different types of numeric values. With the enhancements in Java 23, you can handle both reference and primitive types uniformly:


public class PatternMatchingExample {
    public static void main(String[] args) {
        Object obj = 42; // This can be an Integer or an int

        switch (obj) {
            case Integer i -> System.out.println("Integer: " + i);
            case int i -> System.out.println("int: " + i);
            default -> System.out.println("Unknown type");
        }
    }
}


                

Output:


int: 42
                

Flexible Constructor Bodies

In Java, a constructor often needs to call another constructor in the same class (using this()) or a parent class (using super()). However, Java forced this call to be the very first line in your constructor. This meant you couldn’t:

- Add any logic like input checks before calling another constructor.

- Use multiple constructors for initialization in the same constructor.

- This was frustrating because you had to create workarounds for even simple tasks.

- Java 23 removes this restriction. Now, you can:

- Write code (like input validation) before calling another constructor.

- Call multiple constructors in one constructor if needed.


public class Car {
    private String name;
    private int speed;

    // Main constructor
    public Car(String name, int speed) {
        this.name = name;
        this.speed = speed;
    }

    // Constructor for just a name
    public Car(String name) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("Name cannot be empty");
        }
        this(name, 0); // Set speed to 0 if not provided
    }

    // Default constructor
    public Car() {
        System.out.println("Building a default car...");
        this("Unnamed Car", 50); // Default name and speed
    }
}


                  

Scoped Values

Java 23 introduces Scoped Values, a modern alternative to ThreadLocal variables

- ThreadLocal stores data tied to a specific thread, which can lead to data leakage or unexpected behavior, especially in multi-threaded environments.

- Scoped Values ensure data is explicitly defined and scoped to a particular operation, reducing such risks.

- Scoped Values are immutable, meaning the data cannot be modified once set, ensuring better consistency and safety.

- In applications with multiple threads, Scoped Values provide a clean, controlled way to share context without worrying about manual cleanup.


import java.lang.ScopedValue;

public class ScopedValueExample {
    // Define a Scoped Value for the user ID
    private static final ScopedValue USER_ID = ScopedValue.newInstance();

    public static void main(String[] args) {
        // Set the Scoped Value and run a task
        ScopedValue.where(USER_ID, "User123").run(() -> {
            System.out.println("Processing for: " + USER_ID.get());
        });

        // Attempting to access USER_ID outside the scope
        try {
            System.out.println("Outside scope: " + USER_ID.get());
        } catch (IllegalStateException e) {
            System.out.println("Error: " + e.getMessage());
        }
    }
}

                   

Output:


Processing for: User123
Error: No value is set for this ScopedValue
                 

New Garbage Collection (GC) Options

Garbage collection (GC) is a crucial feature in Java. It automatically frees up memory by removing objects that are no longer in use, ensuring your application runs efficiently without manually managing memory. Java 23 enhances two popular garbage collectors—Z Garbage Collector (ZGC) and Garbage-First Garbage Collector (G1GC)—to provide better performance for different types of applications.

Z Garbage Collector (ZGC)

ZGC is designed for applications that require extremely low latency, meaning the application should experience minimal pauses while GC is running.

- Ultra-Low Pause Times: Pauses are typically less than 1 millisecond, even with large heaps.

- Scalability: Works well for large heaps, making it ideal for big applications like gaming servers or financial trading systems.

- Concurrent Operation: Most of its work happens while your application is running, reducing interruptions.

Garbage-First Garbage Collector (G1GC)

G1GC is a general-purpose collector that balances low latency and high throughput. It divides the heap into regions and cleans the most “garbage-heavy” regions first, ensuring efficient memory usage.

- Low Latency: It minimizes pause times, though not as low as ZGC.

- High Throughput: Optimized for applications that frequently create and discard objects, like web servers or data processing systems.

- Region-Based Cleaning: Focuses on areas with the most garbage to free up memory quickly.

Enabling ZGC is straightforward. You just need to add JVM options when starting your application:


  java -XX:+UseZGC -Xms512m -Xmx512m -jar app.jar
                   

-XX:+UseZGC: Enables ZGC as the garbage collector.