Abstract
JDK 25 - JEP 502: Stable Values (Preview). This JEP introduces an API for stable values, which are objects that hold immutable data. Stable values are treated as constants by the JVM, enabling the same performance optimizations that are enabled by declaring a field final
. Compared to final
fields, however, stable values offer greater flexibility as to the timing of their initialization.
Disclaimer
This post is solely informative. Critically think before using any information presented. Learn from it but ultimately make your own decisions at your own risk.
Requirements
I did all of the work for this post using the following major technologies. You may be able to do the same thing with different technologies or versions, but no guarantees.
- openjdk 25-ea 2025–09–16 (build 25-ea+27–3363)
- NetBeans 25
- Maven 3.9.6 (Bundled with NetBeans)
- maven-compiler-plugin-3.14.0
Download
Visit my GitHub page https://github.com/mjremijan to see all of my open source projects. The code for this post is located at https://github.com/mjremijan/thoth-jdk25
Compile and Run
This is a preview API, disabled by default. To use this API in JDK 25, you must enable preview APIs:
- Compile the program with
javac --release 25 --enable-preview Main.java
and run it withjava --enable-preview Main
- When using the source code launcher, run the program with
java --enable-preview Main.java
- When using jshell, start it with
jshell --enable-preview
A Traditional Logger
A Logger
object is a good example to introduce JEP 502: Stable Values. Ideally, creating a Logger
object should have the following characteristics:
- Created one time only
- Created only when needed (lazy initialization)
- Immutable
- Optimization by the JVM (code in-lining, etc)
The traditional way to create a Logger
is through a class-level final property. Listing 1 demonstrates this.
Listing 1 - Traditional Logger with Eager Initialization
package org.thoth.jdk25.jep502.main.traditional;
import java.util.logging.Logger;
public class TraditionalLoggerWithEagerInitialization {
private static final Logger log
= Logger.getLogger(TraditionalLoggerWithEagerInitialization.class.getName());
public void service() {
log.info("service() method start");
}
}
This code is good, but it has eager initialization. The Logger
object is created whether or not it is ever used. While creating this logger may be a fast operation, it is easy to imagine an expensive object creation composed of many different objects using external resources which either need to be created or connected to. It would be better if the initialization was on-demand (lazy). Listing 2 demonstrates this.
Listing 2 - Traditional Logger with Lazy Initialization
package org.thoth.jdk25.jep502.main.traditional;
import java.util.logging.Logger;
public class TraditionalLoggerWithLazyInitialization {
private static Logger log;
private static Logger getLog() {
if (log == null) {
log = Logger.getLogger(TraditionalLoggerWithLazyInitialization.class.getName());
}
return log;
}
public void service() {
getLog().info("service() method start");
}
}
This code implements lazy initialization, but introduces other problems. Threading is the first obvious issue. Threading potentially allows multiple loggers to be created. The possibility for multiple instances means the JVM cannot treat this object as immutable and thus cannot fully optimize its use. This may be addressed with synchronization, but mutli-threaded code is tricky to get right. This code also introduces a subtle NullPointerException
issue because if the getLog()
method is not used 100% of the time then it is possible the log
property is null
. Accessing the Logger
through the getLog()
method is not ideal.
Neither of these traditional ways of creating a Logger
have all of the characteristics we want for initializing the object. The purpose of JEP 502 is to address these issues. Let’s start looking at JEP 502.
StableValue<M>
StableValue<M>
has been introduced to the JVM in JEP 502: Stable Values (Preview). Its purpose is to:
- Hold immutable data
- Treated as constants by the JVM, enabling the same performance optimizations that are enabled by declaring a field
final
- Offer greater flexibility as to the timing of their initialization
The goal is to have the JVM handle the creation of these objects until they are needed. Since they are immutable, the JVM is able to optimize the bytecode for execution. Listing 3 shows the most basic use of StableValue
.
Listing 3 - StableValue Logger
package org.thoth.jdk25.jep502.main.proposed;
import java.util.logging.Logger;
public class StableValueLogger {
private final StableValue<Logger> logger = StableValue.of();
Logger getLog() {
return logger.orElseSet(() -> Logger.getLogger(StableValueLogger.class.getName()));
}
public void service() {
getLog().info("service() method start");
}
}
On line 6, see that the logger
property is no longer a Logger
instance but a StableValue<Logger>
instance instead. The getLog()
method calls the StableValue.orElseSet()
method. The supplied lambda to this method knows how to create the Logger
when needed. The orElseSet()
method guarantees the lambda is evaluated only once even in a multi-threaded environment.
While using StableValue<Logger>
results in a guaranteed single instance and thread safety, it unfortunately means accessing the Logger
through the getLog()
method to initialize the Logger
object and return it. If getLog()
is not used, there is no risk of a NullPointerException
, but, using the StableValue
API methods directly every time a Logger
was needed would be very verbose and ugly. It would be more convenient if the logger
property could be used more directly. This can be accomplished using a stable supplier. Listing 4 demonstrates this.
Listing 4 - Stable Supplier Logger
package org.thoth.jdk25.jep502.main.proposed;
import java.util.function.Supplier;
import java.util.logging.Logger;
public class StableSupplierLogger {
private final Supplier<Logger> logger
= StableValue.supplier(() -> Logger.getLogger(StableSupplierLogger.class.getName()));
public void service() {
logger.get().info("service() method start");
}
}
On line 7, see that the logger
property is no longer a Logger
instance but a Supplier<Logger>
instance instead. The lambda passed to StableValue.supplier()
is responsible for creating a Logger
when evaluated.
On line 11, see that the Supplier.get()
method is called. Similar to the orElseSet()
method, the lambda is guaranteed to evaluate only once even in a multi-threaded environment. While using the Supplier.get()
method is still an intermediary step needed to get the Logger
, using this instance method is a better user experience than using a custom private getLog()
method.
Summary
This has been a quick look into JEP 502: Stable Values (Preview). It is a new API, giving the JVM the ability to manage the creation of immutable objects. Having the JVM manage the creation of the objects conserves resources by creating objects only when needed and optimizes performance by treating them as final
constant values. This is a preview feature so look for it to be finalized in a future release.
References
Minborg, P., Cimadamore, M. (2023, July 24). JEP 502: Stable Values (Preview). https://openjdk.org/jeps/502.
No comments:
Post a Comment