Just Be Lazy
Per-Ake Minborg on July 29, 2025JEP 502: Stable Values (Preview) will finally bring blessed lazy constructs to Java. With this change, we get lazy values, functions, and collections! The HotSpot JIT compiler can constant-fold all these stable value variants if they are originally referenced from a constant (i.e., a static final
field) and are transitively trusted from there (more on that below).
Quick Start
In the code snippet below you can observe how to implement a high-performance lazy computation of a logger with JDK 25 stable values:
public record OrderControllerImpl(Supplier<Logger> logger)
implements OrderController {
@Override
public void submitOrder(User user, List<Product> products) {
logger.get().info("order started");
// ...
logger.get().info("order submitted");
}
/** {@return a new OrderController} */
public static OrderController create() {
return new OrderControllerImpl(
StableValue.supplier(
() -> Logger.create(OrderControllerImpl.class)));
}
}
private static final OrderControler ORDER_CONTROLLER = OrderControllerImpl.create();
ORDER_CONTROLLER.submitOrder(...);
We are using a small trick here to make the JIT compiler trust that the logger
field will never change by using a record
. Record components are very hard to change and, by virtue of this, are trusted by the JIT compiler.
Here is another example of general non-evicting value caching using the stable value API:
private static final Function<Integer, Double> SQRT_CACHE =
StableValue.function(ffff, i -> StrictMath.sqrt(i));
...
// Lazily computed but still eligible for constant folding by the JIT -> 4
double sqrt16 = SQRT_CACHE.apply(16);
Values in the SQRT_CACHE
will be unset initially. Once one of the values in the Set.of(1, 2, 4, 8, 16)
is referenced, the corresponding sqrt()
value will be computed and entered into the cache. When the JIT eventually compiles the code, it can constant-fold the computation sqrt(16)
and replace that with the
constant 4
.
Why Wasn’t it Called Lazy
?
This is perhaps the most frequently asked question we have received from the community. The properties of a “lazy” field are a subset of the properties of a “stable” field. Both are lazily computed in a broader sense. Still, the stable field is guaranteed to be computed at most once and could in theory be computed ahead of time (for example, during a previous training run).
If you want to call a stable supplier Lazy
instead, things become simpler:
@FunctionalInterface
interface Lazy<T> extends Supplier<T> {
static <T> Lazy<T> of(Supplier<? extends T> original) {
return StableValue.supplier(original)::get;
}
}
private static final Lazy<String> httpResponse = Lazy.of( () -> retrieveHttp(...) );
System.out.println("The response was " + httpResponse.get());
Method references (like StableValue.supplier(original)::get
above) are implemented using hidden classes. These are also trusted by the JIT compiler. Hence, the above class will provide the same constant folding optimizations as StableValue.supplier()
and will otherwise almost have the same performance characteristics.
Video
Here is a video with the first presentation of the Stable Values API that I made in Antwerp in late 2024.
What’s the Next Step?
Download JDK 25 and see how much performance and simplicity the Stable Value API can bring to your current applications.
To use the Stable Value API in JDK 25, make sure to enable preview features:
- Compile your program with
javac --release 25 --enable-preview Main.java
, and run it withjava --enable-preview Main
; or, - Run your program with just one command through the source code launcher
java --enable-preview Main.java
; or - When using
jshell
, start it withjshell --enable-preview
.
More on final fields
Once we, in the JDK community, ensure that final
instance fields are also trusted, we will be able to use any class to hold stable values, functions, and collections, and still achieve constant folding. The first step for this is already underway.