Stable Values in Java (JEP 502): Deferred Immutability with JVM Trust

Stable Values in Java
Explore Stable Values - a new Java 25 upcoming feature (JEP 502) enabling lazy initialization with immutable performance and JVM-level optimizations.

Java is evolving — and one of the more interesting additions in JDK 25 is Stable Values, introduced under JEP 502. The idea is to let you defer initialization of immutable data, yet still allow the JVM to treat those values as constants (and thus apply optimizations like constant folding). According to the JEP summary: “stable values are treated as constants by the JVM, enabling the same performance optimizations that are enabled by declaring a field final”. If, at the time you explore this new feature, its status is still preview, then you must explicitly enable preview features when compiling and running your code.

The Goals of the JEP 502 Specification

The JEP 502 specification defines what Stable Values are and how they should behave. The key goals include the following ones: 

  • Improve startup performance of Java applications by breaking up monolithic initialization of application state.
  • Decouple the time when a stable value is created from when it is initialized, while still retaining high performance.
  • Guarantee that stable values are initialized at most once, even in multithreaded programs, so that there is no race that leads to multiple or conflicting assignments.
  • Let user-level code safely enjoy constant-folding and other compiler/JVM optimizations. 

The JEP 502 specification is very clear regarding whether this new addition comes to replace the final modifier and specifically states that it isn’t. 

The Motivation for Using Stable Values

Immutable data is a powerful tool in multi-threaded programming: immutable objects can be safely shared without synchronization. In Java, final fields are the principal way to enforce immutability. However, the use of the final modifier has limitations:

  • final instance fields must be initialized in the constructor or at their declaration.

  • final static fields must be initialized in a static initializer or at declaration.

  • The textual order of their declaration influences their initialization order.

  • We lose flexibility. We cannot delay the computation of a value until it is actually needed, nor can we choose to have the initialization logic executed at a later point.

 

These constraints mean that many Java applications suffer from eager initialization overhead, particularly at startup, even for components that might never be actually used. Stable Values fill this gap: they let us defer the initialization but still provide the strong guarantee that once set, the value never changes. This enables us to structure our code so that expensive initialization happens lazily (on first use) rather than up front, without giving up the assumptions and optimizations that immutable final fields enjoy.  Under the hood, a stable value is backed by a non-final field annotated with JDK’s internal @Stable, signaling to the JVM that, despite being mutable at the language level, it should be trusted as immutable once initialized. This instructs the JVM to treat access to stable values as constant references, under certain conditions.

Simple Example

The following code sample is a simple placeholder example to illustrate how a stable value might be lazily initialized. The Logger class is not a specific class in Java. The same applies to the other classes. 

				
					package com.lifemichael.samples.java;

import java.util.List;

class InvoiceController {
    private final StableValue<Logger> logger = 
        StableValue.<Logger>of();

    Logger getLogger() {
        return logger.orElseSet(
            () -> Logger.create(InvoiceController.class));
    }

    void createInvoice(User user, List<Product> products) {
        logger.orElseSet(
            () -> Logger.create(InvoiceController.class))
                .info("creating invoice started");
        // … invoice logic …
        logger.orElseSet(
            () -> Logger.create(InvoiceController.class))
                .info("the invoice was issued");
    }
}
				
			

In this code sample:

  • We declare a StableValue<Logger> instance without initializing it immediately.

  • In getLogger(), we call logger.orElseSet(...): if the stable value hasn’t been set yet, the supplier will run, and the result will be stored exactly once; subsequent calls return the same value.

  • Importantly, once set, the JVM can optimize reads through it as though it were constant, thanks to the internal trust semantics.


This pattern avoids eager initialization in the constructor or at class load time, yet still gives you strong immutability guarantees.

Specifying Initialization at The Variable Declaration

Sometimes it’s more elegant to express how a stable value should be initialized right next to its declaration. JEP 502 supports this via stable suppliers. The following code sample shows how to do it.

				
					package com.lifemichael.samples.java;

import java.util.List;
import java.util.function.Supplier;

class InvoiceControllerWithSupplier {

    private final Supplier<Logger> logger = StableValue.supplier(
            () -> Logger.create(InvoiceController.class)
    );

    Logger getLogger() {
        return logger.get();
    }

    void createInvoice(User user, List<Product> products) {
        logger.get().info("creating invoice started");
        // … invoice logic …
        logger.get().info("the invoice was issued");
    }
}
				
			

The StableValue.supplier(...) function call returns a Supplier<Logger> object that calling the .get() method on it, lazily initializes the logger only once. Internally, that supplier is backed by a StableValue<Logger>. This allows you to colocate the initialization logic with the declaration, improving readability and maintainability, while still achieving deferred initialization and JVM optimizations. When expressed this way, client code just calls logger.get() and doesn’t need to worry about orElseSet or related API calls. Under the hood, the JVM can still treat access via that supplier as constant once the initialization has occurred.

Aggregating Stable Values

Many applications work not just with single stable values, but also with collections of deferred immutable values (e.g., object pools, caches). JEP 502 supports aggregation patterns such as stable lists (and by extension, stable maps or functions that lazily initialize elements. The following code sample shows how to do it. 

				
					package com.lifemichael.samples.java;

import java.util.List;

public class Application {
    static final int POOL_SIZE = 10;
    static final List<InvoiceController> controllers =
            StableValue.list(POOL_SIZE, _ -> new InvoiceController());
    public static InvoiceController getController(int id) {
        return controllers.get((int) id);
    }
}
				
			

This code sample shows how simple it is to create a list of stable values. 

 

  • StableValue.list(size, mapper) produces a standard List<T> where each element is backed by its own stable value.

  • Initially, none of the elements are initialized.

  • The first time ORDERS.get(i) is called, the mapper (e.g. i -> new OrderController()) is invoked and stored for that slot; subsequent accesses reuse the stored instance.

  • Even though you’re using a standard List<T> interface, accesses can be optimized by the JVM as though the underlying values are constants, so long as the list is held in a trusted (e.g. static final) context.


Through such aggregation, we can build complex deferred-initialization data structures, with each element converging to immutability once created.In 

The Stable Values (JEP 502) specification opens up a new, powerful approach to designing immutable, high-performance Java applications. They let developers defer initialization of objects without losing the immutability guarantees or JVM optimizations usually tied to final fields. Beyond improving startup performance, the use of Stable Values also introduces a clean, elegant way to implement singletons — without synchronization blocks, volatile fields, or double-checked locking. Once initialized, the object behaves as a true constant, ensuring both thread safety and runtime efficiency. Stable Values are set to become a cornerstone of modern Java architectures, allowing us to bridge the gap between flexibility and immutability.

Share:

The Beauty of Code

Coding is Art! Developing Code That Works is Simple. Develop Code with Style is a Challenge!

Update cookies preferences