Blog
navigate_next
Java
JDK 23: What to Expect?
Gaurav Sharma
September 9, 2024

Exploring JDK 23: Key Enhancements and Features

JDK 23, scheduled to be released on September 17, 2024, is set to bring a host of significant enhancements and advancements to the Java programming language and its supporting ecosystem. This release marks the second consecutive non-Long-Term Support (non-LTS) version of the JDK, following the previous LTS release of JDK 21.

It's worth noting that the next scheduled LTS release of the Java Development Kit is JDK 25, expected to be released in September 2025, and it is anticipated to follow the same long-term support model as previous LTS versions.

💡 JDK 23 is now in the Release Candidate stage, meaning its features are finalized. No new JEPs will be added to this release.

💡A Release Candidate (RC) in the context of JDK is like the final version of the software before it's officially launched. It's almost ready for public release, but it goes through one last round of testing to catch any minor issues or bugs that might have been missed. If no major problems are found, this RC version will be the one that gets released to everyone. It's the final step in making sure the software is stable and ready for widespread use.

To get started with JDK 23, you can download it from https://jdk.java.net/23/

Screenshot of a Command Prompt showing java -version command output, confirming the use of OpenJDK version 23 on a 64-bit server VM. This setup demonstrates compatibility for running Java 23, relevant for Java development and bytecode analysis tasks.

JEPs that are frozen for JDK 23:

JDK 23 includes a dozen new features, most of which are in preview mode, with only a few being finalized.

List of JEPs (Java Enhancement Proposals) frozen for JDK 23, including updates to the Core Java Library, Java Language Specification, HotSpot, and Java Tools. Features include the Class-File API (JEP 466), Structured Concurrency (JEP 480), Primitive Types in Patterns (JEP 455), and ZGC Generational Mode by Default (JEP 474), showcasing new functionalities in Java SE 23.

Brief discussion on JEP (JDK Enhancement Proposal):

💡Oracle Corporation developed JEP as a system for gathering suggestions on how to enhance the Java Development Kit and OpenJDK.

These JEPs are divided into three categories based on features:

  1. Standard:
    • Standard JEPs are fully developed features that are now part of the JDK. These have been thoroughly tested and are ready for use in regular operations. They're reliable and stable, making them great for everyday development.
  1. Preview:
    • Preview JEPs introduce new features that are complete but not yet finalized or fully supported in production. They're included in the JDK so developers can try them out and give their feedback. This trial phase helps decide if these features should be adjusted, made permanent, or removed in future updates.This feature is enabled using the —enable-preview flag at compile time or run time.
  1. Incubator:
    • Incubator JEPs are experimental features that are still being tested and refined. They are included for developers to explore and provide feedback on. These are marked with incubating keyword. This helps shape the final design of the features, which might change significantly or even be dropped based on what developers think.

💡 To learn about the features of JDK 22, please check out our blog on JDK 22. We have covered most of it in great detail.

Link to the blog

💡 These new JEPs mostly come from current major projects in the Java ecosystem like Amber, Loom, Panama, and Valhalla. Amber’s working on making the Java language better, Loom is all about making multi-threading easier, Panama is improving how Java interacts with native code, and Valhalla is enhancing Java’s type system.

Banner to that takes you to the latest version of the Unlogged plugin on the Jetbrains marketplace

JEP 466: Class-File API (Second Preview)

Primarily designed for Java bytecode manipulation. Previously known as JEP 457 in JDK 22, it may not be essential for most regular developers.

Is this API for everyone?

While the Class-File API is a powerful tool, it’s primarily targeted at developers who need to manipulate Java bytecode. This includes:

Framework developers: who need to dynamically generate or modify classes at runtime.

Tooling developers: who need to inspect and analyze class files for purposes like profiling, optimization, or debugging.

Advanced Java developers: working on performance tuning, method interception, or bytecode-level transformations.


Regular application developers likely won’t need this API for day-to-day programming tasks. Most Java developers write code that gets compiled into class files without ever needing to manually inspect or modify the bytecode. However, for those working with JVM internals or building tools and frameworks, this API offers a significant upgrade in terms of usability and control.

Java's ecosystem has long relied on bytecode manipulation for various tasks, from framework development to runtime optimization.  Historically, developers have relied on third-party libraries such as ASM, BCEL, or Javassist to perform these crucial tasks. However, with the introduction of the Class-File API, Java is taking a significant step towards reducing this dependency on external libraries.

Understanding Bytecode and Class Files

Before we dive into the Class-File API, let's briefly explain what bytecode and class files are for those new to the concept:

  • Java Bytecode: This is an intermediate representation of Java code that the Java Virtual Machine (JVM) can execute. It's what your .java files get compiled into.
  • Class Files: These .class files contain the bytecode along with other metadata about your Java classes.
Diagram explaining Java bytecode and class files, showing the compilation process where source code in .java files is converted by the javac compiler into bytecode in .class files, which the Java Virtual Machine (JVM) can execute.

Why Do We Need Bytecode Manipulation?

Bytecode manipulation is a powerful technique in Java programming that allows developers to modify or analyze compiled Java classes. Here are some key reasons why bytecode manipulation is important:

  1. Runtime Code Generation: It enables the creation of new classes or modification of existing ones at runtime, which is crucial for many frameworks and libraries.
  2. Aspect-Oriented Programming (AOP): Bytecode manipulation allows the implementation of cross-cutting concerns (like logging, security, or transactions) without modifying the original source code.
  3. Performance Optimization: It can be used to optimize code at runtime, improving application performance.
  4. Code Analysis and Metrics: Bytecode manipulation techniques can be used to analyze code structure and complexity and gather runtime metrics.
  5. Framework Development: Many Java frameworks like Spring, Hibernate use bytecode manipulation to implement features like dependency injection, lazy loading, or proxy generation. In frameworks like Spring, Inversion of Control (IoC) relies on bytecode manipulation to implement dependency injection and oversee object lifecycle management through IoC containers.
  6. Legacy Code Adaptation: It allows modification of compiled classes when source code is not available or cannot be modified directly.

The Current Landscape: ASM and Other Libraries

Currently, when developers need to work with bytecode, they often turn to libraries like ASM. These libraries allow you to read, write, and modify class files. They're used for tasks such as:

  1. Adding logging or performance monitoring to existing code
  2. Implementing aspect-oriented programming features
  3. Creating proxies or wrappers around existing classes
  4. Optimizing code at runtime

While these libraries are powerful, they come with some challenges:

  1. Learning Curve: Each library has its own API and concepts to learn.
  2. Version Compatibility: As Java evolves every 6 months now, these libraries need to be updated to support new features.
  3. Maintenance: The JDK itself uses ASM internally, which means Oracle has to maintain a fork of ASM.

ASM stands for Abstract Syntax Manipulation. It's a popular Java library used for reading, writing, and transforming Java bytecode directly. Essentially, ASM allows developers to interact with Java class files at the bytecode level, enabling tasks such as:

  1. Bytecode Generation: Developers can create new Java class files on the fly without needing to write Java source code.
  2. Bytecode Modification: ASM can modify existing Java class files. This is useful for things like adding logging, profiling, or even modifying the behavior of a class at runtime.
  3. Bytecode Analysis: It allows reading and understanding Java bytecode, which can be used for static code analysis or to check for specific patterns in compiled classes.

ASM is widely used in frameworks and tools that perform bytecode manipulation, such as Hibernate, Spring, and AspectJ.

The Chicken and Egg Problem: ASM and JDK Version Challenge

A key motivation behind the Class-File API is to address the long-standing "chicken and egg" dilemma between the ASM library and the JDK, often referred to as the "ASM N and JDK N+1 problem," where N represents the version number. This issue has consistently posed challenges for Java developers and framework maintainers.

The Root of the Problem

  1. JDK’s Dependence on ASM: The JDK relies on ASM for critical tasks like implementing lambda expressions at runtime and supporting tools like <span class="pink">jar</span> and <span class="pink">jlink</span>.
  2. ASM’s Version Lag: The version of ASM included in JDK N can’t be finalized until after JDK N is completed, because ASM needs to support all of JDK N’s new features.
  3. Delayed Feature Compatibility: This means that tools in JDK N can't fully support the new class file features introduced in that version. Consequently, <span class="pink">javac</span> in JDK N can't safely emit new class file formats until JDK N+1.

The Domino Effect

This creates a ripple effect across the Java ecosystem:

  1. Slower Feature Adoption: New Java features requiring updated class file formats can’t be fully utilized immediately after their release.
  2. Incompatible Tools: Frameworks and tools relying on ASM may struggle to process class files generated by the latest JDK until they adopt a newer ASM version.
  3. Developer Frustration: Developers eager to use new Java features find themselves held back by tool and framework limitations.

Example for better clarity:

To illustrate the problem, suppose:

  1. JDK 21 introduces new language features requiring updated class file formats.
  2. The version of ASM bundled with JDK 21 doesn’t support these updates, as it was finalized before JDK 21’s release.
  3. <span class="pink">javac</span> in JDK 21 can’t emit these new class file formats since ASM can’t process them.
  4. Developers must wait for JDK 22, which will include an updated ASM, to fully leverage the new JDK 21 features.

How the Class-File API Solves the Chicken and Egg Problem

The Class-File API addresses this problem by:

  1. Removing External Dependencies: Java introduces a built-in API for class file manipulation, eliminating the need for external libraries like ASM.
  2. Synchronized Evolution: The Class-File API evolves in tandem with the JDK, ensuring immediate support for new class file formats as soon as they’re released.
  3. Instant Feature Usage: Developers can use new class file features as soon as they’re introduced in a JDK release, without waiting for third-party libraries to catch up.
  4. Aligned Ecosystem: Tools and frameworks using the Class-File API will automatically support the latest JDK features, reducing compatibility issues across the ecosystem.
  5. Modern Java Features: The API takes advantage of newer Java features like pattern matching and records, making it more intuitive for developers familiar with modern Java.

💡 Note: The chicken and egg problem is one of many problems, but the major one is discussed here.

JEP 455: Primitive Types in Patterns, <span class="pink">instanceof</span>, and <span class="pink">switch</span> (Preview)

JEP 455 introduces several key enhancements to Java, especially in how primitive types are handled within patterns, <span class="pink">instanceof</span>, and <span class="pink">switch</span> statements. These changes aim to make the language more consistent and expressive by incorporating primitive types into various contexts.

Key Changes:

Enhanced <span class="pink">instanceof</span>:

  • What Changed: Before this JEP, <span class="pink">instanceof</span> was limited to reference types and could not be directly applied to primitive types. This limitation meant you often needed to perform manual checks and typecasting.
  • New Feature: JEP 455 extends the <span class="pink">instanceof</span> operator to work with primitive types. This enhancement allows you to use <span class="pink">instanceof</span> to check if a primitive value fits within a certain type (e.g., checking if an <span class="pink">int</span> value fits within a <span class="pink">byte</span>). This makes type checking more precise and reduces the risk of errors associated with manual casting.

   Extended <span class="pink">switch</span>:

  • What Changed: Before JDK 23, <span class="pink">switch</span> statements were limited in the types they could handle. They could work with <span class="pink">int, char, enum</span>, and <span class="pink">String</pan>, but lacked support for other primitive types like <span class="pink">long, float, double</span>, and <span class="pink">boolean</span> directly in the switch cases. This limitation often required workarounds or less readable code when dealing with these types.
  • New Feature: JEP 455 extends the <span class="pink">switch</span> statement to support all primitive types, including <span class="pink">long, float, double</span>, and <span class="pink">boolean</span>. This enhancement allows you to use <span class="pink">switch</span> with a broader range of types. You can now write cleaner and more expressive <span class="pink">switch</span> statements that handle various primitive types directly.

<span class="pink">instanceof</span> with Primitive Types

JEP455InstanceofDemo.java

	
public class JEP455InstanceofDemo {
    public static void main(String[] args) {
        int number = 42;

        if (number instanceof byte b) {
            System.out.println("Number fits in a byte: " + b);
        } else if (number instanceof short s) {
            System.out.println("Number fits in a short: " + s);
        } else {
            System.out.println("Number is an int: " + number);
        }
    }
}

Explanation: The above code is a simple demo of how Java SE 23 handles primitive type patterns. In the <span class="pink">JEP455InstanceofDemo</span> class, the <span class="pink">main</span> method checks if an integer value, <span class="pink">number</span>, fits into smaller primitive types like <span class="pink">byte</span> or <span class="pink">short</span> using the <span class="pink">instanceof</span> operator. It prints the value if it fits or indicates it’s an <span class="pink">int</span> otherwise.

Before JDK 23, checking if an <span class="pink">int</span> could fit into a smaller type required manual casting and additional logic, making the code more complex and less readable. JDK 23’s introduction of primitive type patterns streamlines this by allowing direct use of <span class="pink">instanceof</span> with primitive types.

Compiling and Running the Java File:

Command Prompt screenshot showing the compilation and execution of `JEP455InstanceofDemo.java` using Java SE 23 with preview features enabled. The output verifies the instance check, displaying "Number fits in a byte: 42," demonstrating the new instance of enhancements in JEP 455.

Output: Number fits in a byte: 42

Note: I used the <span class="pink">--enable-preview</span> flag for compiling and running the program. This flag enables and tests preview features in Java that are new and not yet finalized, allowing both the compiler and JVM to recognize and handle these experimental features during their processes.

<span class="pink">switch</span> with Primitive Type Patterns

JEP455SwitchDemo.java

	
public class JEP455SwitchDemo {
    public static void main(String[] args) {
        float floatValue = 3.14f;
        switch (floatValue) {
            case 0f -> System.out.println("Zero");
            case 3.14f -> System.out.println("Pi");
            case float f when f < 0 -> System.out.println("Negative float: " + f);
            case float f -> System.out.println("Positive float: " + f);
        }
    }
}

Explanation:

The above code uses a switch statement to handle <span class="pink">float</span> values, which wasn’t allowed before. It checks if the <span class="pink">floatValue</span> matches specific values or patterns and prints a message accordingly.

Compiling and Running the Java File:

Command Prompt screenshot showing the compilation and execution of JEP455SwitchDemo.java using Java SE 23 with preview features enabled. The output demonstrates the new switch enhancements introduced in JEP 455, with the program outputting "Pi."

Output: Pi

JEP 477: Implicitly Declared Classes and Instance Main Methods (Third Preview)

  • Preview Status: This feature is currently in its third preview phase, earlier known as JEP 463 in JDK 22, indicating it's available for testing and feedback but not yet finalized for standard use.
  • Simplification of Java Programming: The feature significantly simplifies the Java programming experience.
  • No Explicit Class Definition Required: Developers can write and execute Java code without needing to explicitly define a public class.
  • Streamlined Main Method: The traditional <span class="pink">main</span> method structure, which typically requires <span class="pink">String[] args</span> as an argument, is no longer necessary.

💡 This JEP aims to make Java easier for beginners by allowing students to write their first programs without needing to learn features meant for more advanced, larger projects.

Main.java

	

public static void main() {
    System.out.println("Hey "+inform());
}


static String inform() {
    return "i am running this program with Implicit Class Main Method";
}

Explanation for the above code:

We haven’t defined a class, and we haven’t even passed the <span class="pink">String[] args</span> as an  argument in the main method, but still, this program runs.

Output:

Windows PowerShell screenshot showing the use of Java SE 23 to compile and run Main.java with the implicit class main method feature. The output displays the message: "Hey I am running this program with Implicit Class Main Method."

JEP 482: Flexible Constructor Bodies (Second Preview)

Previously known as Statements Before Super (JEP 447) in JDK 22, JEP 482 introduces new flexibility in constructor code.

What’s New:

  • Before JEP 482: You couldn’t run any code in a subclass constructor before calling <span class="pink">super()</span>. This often led to using static methods or blocks for tasks like validation before invoking the superclass constructor.
  • With JEP 482: You can now run code, such as initializing fields or performing validation, directly in the subclass constructor before calling<span class="pink">super()</span>.
  • This simplifies the code and reduces the need for extra static methods.

Key Update from JEP 447 to JEP 482:

  • Field Initialization: JEP 482 allows you to initialize subclass fields before calling the superclass constructor. This prevents the superclass constructor from encountering default or uninitialized values of these fields.

Understanding through an example:

In the given code below, the <span class="pink">Dog</span> constructor attempts to set the <span class="pink">bark</span> field before calling <span class="pink">super()</span>, which causes an error in Java prior to JEP 482. This is because subclass fields cannot be initialized before the superclass constructor executes. With JEP 482, this restriction is lifted, allowing such initialization.

Main.java

	

class Animal {
    Animal() {
        makeSound();
    }

    void makeSound() {
        System.out.println("Animal sound");
    }
}

class Dog extends Animal {
    private String bark;

    Dog() {
        // Before JEP 482, this isn't allowed:
        bark = "Woof";  // Error: Cannot reference 'bark' before supertype constructor has been called
        super();

    }

    @Override
    void makeSound() {
        System.out.println("Dog says: " + bark);
    }
}

public class Main {
    public static void main(String[] args) {
        new Dog();
    }
}

above code explanation:

Before JEP 482:

  • The <span class="pink">Dog</span> constructor tries to initialize <span class="pink">bark</span> before calling <span class="pink">super()</span>.
  • Java did not allow this, resulting in a compile-time error because subclass fields couldn't be accessed or modified before the superclass constructor was called.

With JEP 482:

  • JEP 482 allows field initialization in the subclass constructor before the superclass constructor (<span class="pink">super()</span>) is called.
  • Thus, you can now set <span class="pink">bark="Woof"</span> before calling <span class="pink">super()</span>.
  • The <span class="pink">bark</span> field is initialized properly before the <span class="pink">Animal</span> constructor executes.
  • This allows <span class="pink">makeSound()</span> to use the initialized value, resulting in the output:

	
Dog says: Woof

Running the code prior to this feature would result in:

Screenshot of a compilation error in `Main.java`, line 26, stating: "error: call to super must be first statement in constructor." This error highlights a common Java restriction where `super()` must appear as the first statement within a constructor.

Output Without Enabling Preview Mode in JDK 23: Initializing Variables Before <span class="pink">super()</span>

PowerShell output showing a compilation error in Java without enabling preview mode for JDK 23. The error in `Main.java` on line 17 states: "flexible constructors is a preview feature and is disabled by default." It highlights that a variable `bark` is referenced before the superclass constructor is called, which requires enabling preview features for flexible constructor initialization.

Output : Enabling Preview Features in JDK 23

PowerShell output demonstrating the execution of a Java file in JDK 23 with preview features enabled. The command successfully runs `Main.java` with `--enable-preview`, resulting in the output: "Dog says: Woof," confirming that the preview feature for flexible constructor initialization is working as intended.

JEP 474: ZGC: Generational Mode by Default

Before diving into the Z Garbage Collector (ZGC) and its new default Generational Mode, let’s first get a quick overview of Java’s garbage collection process and the different types of garbage collectors available in Java.

What is Garbage Collection?

Garbage Collection (GC) is Java's built-in memory management system that automatically frees up memory by identifying and removing objects no longer in use. It plays a key role in keeping your application running smoothly by managing memory efficiently and preventing memory leaks, so developers don't have to do it manually.

Types of Garbage Collectors in Java

Over time, Java has introduced several garbage collectors to cater to different needs, depending on the application type and memory requirements. Here are the four main garbage collectors in Java today:

  1. Serial GC: A single-threaded garbage collector, best suited for small applications with limited memory needs.
  2. Parallel GC: A multi-threaded collector that focuses on maximizing throughput, making it ideal for applications like batch processing.
  3. Garbage-First (G1) GC: Designed for large heaps, G1 strikes a balance between low pause times and high throughput.
  4. Z Garbage Collector (ZGC): ZGC is designed for very low-latency performance and can handle heap sizes ranging from 8 MB to 16 TB, making it perfect for large applications where pause times must be minimal.

Understanding Pause Time in Garbage Collection

Pause time
is the period when your application stops running because the system is cleaning up unused memory. If these pauses are too long, your app might feel slow or unresponsive, which can be frustrating for users. Minimizing pause time is important to keep your app running smoothly.

Among Java's garbage collectors, ZGC (Z Garbage Collector) is known for having the least pause time. It’s designed to keep pauses to just a few milliseconds, even with large amounts of memory. This makes it ideal for applications where minimal interruptions and smooth performance are critical.

Important:

💡 By default, Java uses the G1 Garbage Collector unless another option is specified. However, if you choose to enable the Z Garbage Collector (ZGC) over the default G1 garbage collector using the flag <span class="pink">-XX:+UseZGC</span>, starting from JDK 23 (with JEP 474), ZGC will automatically operate in Generational mode. While the non-generational version of ZGC is still supported, it is likely to be deprecated in the future. If you prefer to continue using the non-generational ZGC, you can explicitly opt out of Generational mode by including the flag <span class="pink">-XX:+UseZGC -XX:-ZGenerational</span>.

Comparison of Garbage Collectors

Comparison table of Java garbage collectors, detailing the pause times, throughput, heap size, and use cases for each collector. It includes Serial (long pauses, low throughput, small heap for small applications), Parallel (moderate pauses, high throughput for batch processing), G1 (short pauses, high throughput, large heap for general purpose), and ZGC (very short pauses, high throughput, very large heap for low latency in large applications).

ZGC (Z Garbage Collector)

ZGC (Z Garbage Collector) is a highly scalable, low-latency garbage collector introduced in Java 11 as an experimental feature and became production-ready in Java 15. Its primary goal is to handle large heaps (even multi-terabyte heaps) with minimal impact on application performance, particularly focusing on keeping garbage collection (GC) pause times consistently short—typically in the range of sub-milliseconds.

Key Features of ZGC:

  1. Low Pause Times: One of ZGC's hallmark features is the extremely short and consistent pause times, independent of heap size. Pause times are usually sub-10 ms, even for very large heaps, making it ideal for real-time, latency-sensitive applications.
  2. Scalability: ZGC is designed to handle large heap sizes, ranging from megabytes to multi-terabyte scales, without significant degradation in performance.
  3. Concurrent Processing: Most of the work in ZGC is done concurrently with the running application, including marking, relocation, and compaction of objects. This allows ZGC to achieve minimal interruptions to the application's processing.

Previous Mode of ZGC: Non-Generational Mode

Before the introduction of Generational Mode in JDK 21, the Z Garbage Collector (ZGC) used what’s known as a Non-Generational Mode. In simple terms, ZGC treated all objects the same, regardless of how long they existed in memory. Whether an object was created just a moment ago or had been around for a while, ZGC didn't differentiate between them when deciding what to clean up. This method worked fine for keeping pause times very low, but it wasn’t the most efficient for memory management, especially when it came to dealing with short-lived objects.

What is Generational Mode in ZGC?

Now, let’s talk about Generational Mode. This concept comes from the idea that most objects in a program are either short-lived or long-lived:

  • Short-lived objects: Think of temporary data like strings or numbers created in a method and forgotten when that method finishes. These objects only live for a brief time.
  • Long-lived objects: Some objects, like database connections or cached data, tend to hang around for the entire duration of the program.

In Generational Mode, the garbage collector splits the memory into two main parts:

  • Young Generation: This is where new, short-lived objects are stored. Since many objects are short-lived, they will be collected quickly and often.
  • Old Generation: Objects that survive long enough in the Young Generation are moved here. These objects live longer and are collected less frequently, meaning fewer pauses for the garbage collector to check on them.

crux of JEP 474

It specifies that Z Garbage Collector will now default to Generational Mode if selected, with the non-generational mode going to be deprecated. This is a confirmed feature in JDK 23, meaning from JDK 23 onward, you no longer need to use flags to enable ZGC's generational mode; it's the default setting.

How Does Generational Mode Help?

Generational mode helps in two major ways:

  1. Faster Cleanup for Short-Lived Objects: Since most objects are created and then quickly forgotten (like local variables), the garbage collector can focus on clearing out these short-lived objects more frequently. By cleaning up the Young Generation often, the system can reclaim memory faster and more efficiently without spending too much time on older objects.
  2. Less Work for Long-Lived Objects: Objects that survive longer get moved to the Old Generation, which is checked less often. This reduces the overhead of constantly checking objects that the system doesn’t need to worry about for a while. This makes the whole garbage collection process more efficient, saving time and resources.

In Short:

  • Old Mode (Non-Generational): Treated all objects the same, without knowing if they were short-lived or long-lived.
  • New Mode (Generational): Divides memory into Young (short-lived) and Old (long-lived) objects, cleaning up short-lived ones more frequently
  • Why it’s better: It makes garbage collection faster and more efficient by focusing on what really needs to be cleaned up right away, leading to better memory management and performance..

Strengths and Trade-offs:

Strengths:

  • Excellent for applications requiring both large heaps and low-latency operation.
  • Scales well with heap size and hardware (cores, memory).
  • Production-ready and stable in Java since version 15.

Trade-offs:

  • ZGC may not be the best fit for small-scale applications, where other garbage collectors (like G1) might be more efficient.
  • It’s designed to prioritize latency over throughput, so certain high-throughput applications might not see as much benefit from ZGC.

JEP 473: Stream Gatherers (Second Preview)

Official Note: We proposed Stream Gatherers as a preview feature in JEP 461 and delivered it in JDK 22. We here propose to re-preview the API in JDK 23, without change, in order to gain additional experience and feedback.

An extensible API for intermediate java.util.stream operations.

Similar to how the Collectors API provides a generic way to define terminal operations, the Gatherer API offers a generic approach for creating intermediate operations.

Note that the Collector's design has a significant influence on the Gatherer interface.

Stream Pipeline:

A stream pipeline in Java is a sequence of steps that processes a collection of data. It's similar to a factory assembly line where each step modifies or filters the data until it reaches its final form. The pipeline starts with a source (like a list), passes through a series of intermediate operations (like sorting or filtering), and ends with a terminal operation that either produces a single result or triggers some actions.

Understanding through a basic example:

Imagine you have a list of numbers and you want to find the sum of all even numbers greater than 10. Here’s how you would set up a stream pipeline in Java to achieve this:

	
import java.util.Arrays;
import java.util.List;

public class StreamExample {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(12, 19, 8, 13, 22, 15);

        int sum = numbers.stream()               // Create a stream from the list
                          .filter(n -> n > 10)   // Intermediate operation: keep only numbers greater than 10
                          .filter(n -> n % 2 == 0) // Intermediate operation: keep only even numbers
                          .reduce(0, Integer::sum); // Terminal operation: sum them up

        System.out.println("Sum of even numbers greater than 10: " + sum);
    }
}

💡 I've talked about the stream pipeline because Stream Gatherers are used to perform intermediary operations within the pipeline.

What is Stream::gather(Gatherer)?

Stream::gather(Gatherer) is a versatile intermediate operation in Java's Stream API that allows developers to process stream elements in highly customizable ways. It is similar to Stream::collect(Collector), which is used for terminal operations, but gather is used for intermediate operations, allowing for continued stream processing.

Purpose and Functionality:

Stream::gather enables the transformation and processing of stream elements through a custom mechanism defined by the developer. This operation can handle complex transformations, including one-to-one, one-to-many, many-to-one, and many-to-many relationships between input and output elements. It supports operations that might require state tracking, conditional processing, or transformations that adapt over time based on previously seen elements.

Gatherer Interface:

	
Interface Gatherer<T,A,R>

Interface Gatherer takes three type parameters :

  1. T→ the input type (the type of element that it consumes)
  2. A→state type of the gatherer operation (often hidden as an implementation detail)
  3. R→ what type of element this gatherer produces

Methods in this Gatherer interface:

  1. Initializer Function (optional): This function sets up a private "state" object that remembers information needed for processing elements in the stream. For example, it can keep track of the last element seen to help decide what to do with the next one.
  2. Integrator Function: This is the main function where each element of the stream is processed. It uses the state object and can add new elements to the output or even stop the stream early if a specific condition is met, like finding the highest number.
  3. Combiner Function (optional): When the stream is split to run in parallel, this function helps to merge the separate pieces back together accurately. It ensures that the results from parallel processing are combined correctly.
  4. Finisher Function (optional): After all the elements have been processed, this function can wrap things up. It might use the state object to add more elements to the stream based on what it has learned from processing all the previous elements.

Built-in gatherers

built-in gatherers in the <span class="pink">java.util.stream.Gatherers</span> class:

  • fold: This gatherer combines all the input elements step-by-step into a single result. It only shows this final result after all the inputs have been looked at. It's good for when you need to sum up or merge everything into one outcome.
  • mapConcurrent: This gatherer processes each input element individually by applying a given function, and it can handle several elements at once, up to a certain limit. This allows for fast, parallel processing of elements, making it efficient when you have a lot of data to process quickly.
  • scan: This one takes each element and the current state (like a running total), applies a function, and sends the result out right away. It updates continuously with each new element, useful for tasks where you want to see the progression over time.
  • windowFixed: This gatherer groups elements into fixed-size lists and sends out each list once it's full. It's helpful when you need to process or analyze data in consistent blocks, like batching tasks together.
  • windowSliding: Similar to windowFixed, but after the first group, each new group shifts by one element from the previous group. This method creates overlapping groups, which is useful for analyzing data that flows continuously, like monitoring trends or changes over a moving range.
Java code snippet demonstrating the use of the `gather` method with the `Gatherers` utility. The code initializes a list of strings and streams it, utilizing the gather method options like `fold`, `scan`, `mapConcurrent`, `windowFixed`, and `windowSliding`.

Understanding through example:

Problem Statement:

We have a List of String i.e. list of Java-related keywords. We have to organize into smaller groups. Each group should have three keywords, and we want to create only the first two groups.

With Stream Gatherers:

	

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

public class Main {
    public static List<List<String> > findGroupsOfThreeWithGatherer(List<String> input, int fixed_size, int grouping) {
        return Stream.iterate(0, i -> i + 1)
                .map(index -> input.get(index))
                .limit(input.size())
                .gather(Gatherers.windowFixed(fixed_size))
                .limit(grouping)
                .collect(Collectors.toList());
    }

    public static void main(String[] args) {
        List<String> javaProjects = List.of(
                "Project Valhalla",
                "Project Amber",
                "Project Loom",
                "Project Panama",
                "Project ZGC",
                "Project Shenandoah",
                "Project Leyden",
                "Project Metropolis"
        );
        List<List<String> > grouped = findGroupsOfThreeWithGatherer(javaProjects, 3, 2);
        System.out.println(grouped);
    }
}

Output:

Windows PowerShell output displaying Java version check, directory listing, and execution of a Java program. The Java program, compiled and run in preview mode for JDK 23, outputs a nested list of Java Project names, including Project Valhalla, Project Amber, Project Loom, Project Panama, Project ZGC, and Project Shenandoah.

	
[[Project Valhalla, Project Amber, Project Loom], [Project Panama, Project ZGC, Project Shenandoah]]

As one can see, the list of keywords is divided into two groups. consisting of three each.

JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal

The goal of JEP 471 is to phase out and eventually eliminate the memory-access methods from the <span class="pink">sun.misc.Unsafe</span> class.This is also one of the few final features of JDK 23. These methods, introduced in 2002, allowed Java developers to perform low-level, unsafe operations directly on memory. Though useful, they were never intended for public use and pose significant risks, including the potential for JVM crashes and unpredictable behavior. Therefore, they were not exposed as a standard API.

Many <span class="pink">sun.misc.Unsafe</span> methods provide direct access to both on-heap and off-heap memory:

  • On-heap memory is managed by Java’s garbage collector. Unsafe methods enabled developers to manipulate object fields and array elements at specific memory offsets.
  • Off-heap memory refers to memory outside of the garbage collector’s control. Unsafe allowed developers to allocate, modify, and free this memory manually, offering greater flexibility and performance benefits.

Despite the risks, <span class="pink">sun.misc.Unsafe</span> became a favorite over time for library developers seeking greater performance and control than what standard Java APIs could offer, such as atomic operations or advanced off-heap memory management. However, since these methods bypass Java’s safety mechanisms, they introduced potential dangers like JVM crashes and hard-to-debug errors.

Why the change?

Over the years, safer, standard APIs have been introduced to replace these operations.

  • java.lang.invoke.VarHandle, introduced in JDK 9 (JEP 193), provides methods to safely and efficiently manipulate on-heap memory, i.e., fields of objects, static fields of classes, and elements of arrays.
  • java.lang.foreign.MemorySegment, introduced in JDK 22 in Foreign Function and Memory API (JEP 454), provides methods to safely and efficiently access off-heap memory, sometimes in cooperation with <span class="pink">VarHandle</span>.

The goal is to encourage developers to transition from <span class="pink">Unsafe</span> to these safer, supported APIs, improving compatibility with future JDK versions while reducing the risks of crashes and undefined behavior.

Deprecation Timeline

The process will span several JDK releases, beginning with JDK 23. Initially, using these <span class="pink">Unsafe</span> methods will generate warnings. In later phases, they will trigger runtime exceptions, and eventually, the methods will be fully removed.

Additional Stuff: String Templates Dropped from JDK 23

In the upcoming release of Java Development Kit (JDK) 23, the highly anticipated String Templates feature has been removed from the list of JEPs and is not even included as a preview feature this time.

String Templates were intended to provide a more concise and readable way of working with string interpolation in Java, but after extensive debate and feedback from the Java community, the language designers have decided to remove it from JDK 23.

String templates, which were introduced as a preview feature in JDKs 21 and 22, were designed to make it easier and safer to embed variables in structured languages like SQL, HTML, and JSON. However, they were surprisingly withdrawn before JDK 23's release.

While many developers would prefer a straightforward string interpolation feature, the primary goal is to address security concerns, such as injection attacks, which remain a significant issue in the industry. Therefore, the new proposal will likely focus on making the secure option more elegant, rather than just making string concatenation simpler.

Background on String Templates

String Templates were first proposed as part of the Java Improvement Proposal (JEP) 378, which aimed to introduce a new syntax for string interpolation in Java. The key motivation was to provide a more ergonomic and expressive alternative to the traditional string concatenation and <span class="pink">String.format()</span> approaches.

The syntax for string templates looked like this:

	
String line = STR."My name is \{name}. My age is \{age}." ;

The above code uses Java’s String Templates with the <span class="pink">STR</span> processor to interpolate values into a string. The placeholders <span class="pink">\{name}</span> and <span class="pink">\{age}</span> are dynamically replaced with the values of <span class="pink">name</span> and <span class="pink">age</span> during runtime.

Template Processors in String Templates

Template Processors are at the heart of Java's String Template feature introduced in Java 21 (in preview). A template processor is responsible for converting a String Template (which contains both static text and embedded dynamic values) into a final result, which may be a <span class="pink">String</span> or any other type.

Introduced as part of the Java 21 Preview feature, String Templates extend beyond basic string interpolation, providing a more robust and secure way to handle dynamic values in strings. Template Processors facilitate this conversion process.

Key Components of Template Processors:

  1. Template Processor (e.g., STR, FMT, RAW):
    • The processor class implements the <span class="pink">StringTemplate.Processor</span> interface.
    • It is responsible for evaluating the template expressions and generating the output.
  2. Character DOT (<span class="pink">.</span>):
    • Separates the processor from the string template itself.
  3. String Template:
    • The template contains both static text (fragments) and dynamic parts (values or expressions). At runtime, these dynamic parts are filled with actual values.

The <span class="pink">process()</span> Method

The core of the Template Processor's functionality lies in the <span class="pink">process()</span> method. This method is responsible for transforming a <span class="pink">StringTemplate</span> into any desired output type. Its signature is general, allowing flexibility in the return type and making it highly versatile.

<span class="pink">StringTemplate.Processor</span> Interface:

	
interface Processor<R, E extends Throwable> {
    R process(StringTemplate stringTemplate) throws E;
}

<span class="pink">R</span> : The return type of the processor (not restricted to <span class="pink">String</span>). The method can return any type, enabling the transformation of a <span class="pink">StringTemplate</span> into more complex objects like JSON, XML, or even custom types.

<span class="pink">E</span>: The exception type that can be thrown during processing.

<span class="pink">process()</span>: This method takes a <span class="pink">StringTemplate</span> as input and processes it, returning the final result in the form of <span class="pink">R</span>. The actual transformation logic happens inside this method, where the fragments (static text) and values (dynamic parts) of the template are combined.

Inbuilt Template Processors

Java provides some inbuilt template processors named STR, FMT, and RAW to process string template expressions.

  1. STR:
    • The most basic template processor that simply performs string interpolation. It replaces placeholders (<span class="pink">\{}</span>) with actual values at runtime.
  2. FMT:
    • This processor handles format specifiers (like <span class="pink">%s</span>,<span class="pink"> %d</span>) in addition to interpolation, allowing for more advanced string formatting. It can be used to generate formatted reports.
  3. RAW:
    • The<span class="pink"> RAW</span> processor doesn't return a final string. It doesn't automatically process the string template like the STR template processors. Instead, it returns a <span class="pink">StringTemplate</span> object, from which you can extract the fragments (static text) and values (dynamic parts) for further manipulation.
	
StringTemplate tmpl = RAW."My name is \{name}. My age is \{age}.";
List<String> fragments = tmpl.fragments(); // Extract fragments
List<Object> values = tmpl.values();       // Extract values

Custom Template Processor

You can create your own Template Processor by implementing the <span class="pink">Processor</span> interface, as shown below:

	
class CustomTemplateProcessor implements StringTemplate.Processor<String, Throwable> {

    @Override
    public String process(StringTemplate tmpl) throws Throwable {
        return null;  // logic to be implemented
    }
}

The process method could return anything depending on the logic you write.

Why String Templates Are Out (Possible Reasons)

Criticisms of First Preview for Java String Templates. Highlights issues with StringTemplate API design, including:  \{} notation differing from ${} Processor.template being overly specific Limitations of functionality within processors Requirement for RAW to store and pass StringTemplate instances Need for manual flattening in nested templates Challenges leading to a significantly different API design.

The preview of string templates in JDK 21 and 22 aimed to improve the security and readability of combining strings with runtime-computed values.

It allowed developers to embed variables within strings, which were then processed by template processors.

However, the feature faced criticism, particularly for its use of the<span class="pink">\{}</span> syntax instead of the more familiar <span class="pink">${}</span> syntax seen in other languages like JavaScript, TypeScript, and Groovy. Additionally, the special syntax for processor invocations (<span class="pink">$processor.$template</span>) was seen as unnecessary syntax sugar, complicating the feature.

Practical use of string templates also highlighted several limitations:

  • Template processors sometimes couldn't handle certain scenarios due to the requirement to return something.
  • The use of a special processor called <span class="pink">RAW</span> was necessary for simple cases, which was not elegant.
  • Nesting templates wasn't supported, which would have been useful.
  • Developers had to choose between methods accepting strings or template processors, leading to a cumbersome API design.

The Dollar Sign Debate

One of the most debated aspects of the original string template proposal was the use of the <span class="pink">\{}</span> syntax instead of the dollar sign (<span class="pink">$</span>) seen in other languages like JS. While familiarity with<span class="pink">$</span> might seem like a good reason to adopt it, the arguments against it are stronger. For instance, using <span class="pink">$</span> as a special character in string templates would require it to be escaped when appearing as-is, adding complexity. On the other hand, <span class="pink">\{}</span> is already an illegal character sequence in non-template strings, meaning it doesn't require escaping and won't appear in regular strings, making it a better choice.

Understanding Escaping Characters

In the context of programming, escaping refers to the process of using a special character (often a backslash <span class="pink">\</span>) to indicate that the following character should be treated differently than it normally would be. Specifically, in strings, some characters (like the dollar sign<span class="pink">$</span> or quotes <span class="pink">"</span>/<span class="pink">'</span>) have special meanings, and to include these characters as literal text, they need to be "escaped."

For example, in many programming languages, <span class="pink">$</span> is used as part of variable interpolation in strings. If you want to include an actual dollar sign in the string (instead of it being interpreted as part of a variable), you need to escape it by adding a backslash in front of it, like so: <span class="pink">\$</span>.

So in the case of the Dollar Sign Debate, if Java had chosen to use <span class="pink">$</span> for string interpolation (like in JavaScript), then any time a literal dollar sign appeared in a string (for example, when talking about money like <span class="pink">$100</span>), it would need to be escaped as <span class="pink">\$</span>. This would make writing such strings more cumbersome and prone to errors.

By using the <span class="pink">\{}</span> syntax in Java's string templates instead, there is no need to escape the dollar sign because <span class="pink">\{}</span> is an illegal character sequence in regular strings, making it easier and more intuitive to work with.

Elegance and Open Issues

The STR syntax (<span class="pink">STR."text \{variable}"</span>) in the original proposal was criticized for being cumbersome, and while the new proposal may remove this syntax, it won't necessarily result in a shorter or simpler solution. The new design will likely involve creating string templates as a new kind of literal, possibly using backticks as delimiters, but this remains speculative.

There are still many open questions about the future of string templates in Java:

  • What will the future APIs look like? Will they accept both strings and string templates?
  • How will string templates be marked without the use of a template processor?
  • How will concatenating and nesting templates be handled?

String Template Future

The new proposal for string templates will likely prioritize security over simplicity, and the <span class="pink">\{}</span> syntax is expected to remain.

Regular method calls will likely take place of the special syntax for processor invocations, which means that the template and processing code are no longer required to be adjacent.

💡Each feature has a lot to offer, and covering every detail in a single article isn't possible. If any of them catch your interest, feel free to explore further.

Cheers!

Happy Coding.

References:

JDK 23

JEP 455: Primitive Types in Patterns, instanceof, and switch...

JEP 466: Class-File API (Second Preview)

Java A Classfile API for the JDK #JVMLS

JEP 473: Stream Gatherers (Second Preview)

JEP 473: Stream Gatherers (Second Preview)

JEP 482: Flexible Constructor Bodies (Second Preview)

JEP 471: Deprecate the Memory-Access Methods in sun.misc.Uns...

JEP 474: ZGC: Generational Mode by Default

Update on String Templates (JEP 459)

Oracle Help Center Java Language Updates

Gaurav Sharma
September 9, 2024
Use Unlogged to
mock instantly
record and replay methods
mock instantly
Install Plugin