

“Stable values” are values that can only be assigned once during the runtime of an application – though at any point in time – and remain constant thereafter. They standardize lazy initialization of constants and allow the JVM to optimize these constants in the same way as it can for final values.
In this article, you will learn:
- What are stable values, and how do you use them?
- What are the advantages of the immutability of a stable value?
- How have we implemented immutability so far, and what are the disadvantages of this?
- What are stable lists, stable maps, and stable functions?
- What do the individual methods of the
StableValue
class do? - How do stable values work internally?
Stable Values are a preview feature expected to be released in Java 25. They are defined in JDK Enhancement Proposal 502.
In the first few sections, I explain why we need Stable Values in the first place. If you can already guess, you may skip directly to the “The solution: Stable Values” section.
Why Immutability?
In the introduction, I explained that Stable Values are values that are only assigned once and then remain immutable. But what is the benefit of values being immutable? Immutability has several advantages:
1. An immutable object can be used by several threads without any problems.
There is no risk of race conditions, which, for mutable objects, we can only prevent through synchronization or memory barriers. Errors can easily creep in, even with experienced developers.
2. The JVM can optimize immutable objects, e.g. by constant folding.
For example, if the JVM recognizes that serviceRegistry.userService()
is accessed in several places and it knows that serviceRegistry
is constant and userService()
returns a constant, then it can replace all calls to serviceRegistry.userService()
with the userService
constant.
3. Immutable objects make the code easier to read.
Code is more predictable, easier to understand, and easier to debug when you don’t have to worry about possible changes to object states. For mutable objects, we should create defensive copies for parameters and return values to ensure they are not inadvertently modified. This is not necessary for immutable objects.
Immutability with “final”
Until now, the only way to achieve immutability was to mark the fields of an object as final
. Static final fields must be assigned during the declaration or in a static
block and are initialized when the class is loaded. Final instance fields must be assigned during the declaration or in the constructor and are initialized when a new object of the class is created.
In the following example, a static Logger
field is initialized:
public class UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
// . . .
}
Code language: Java (java)
Alternatively, with a static
block:
public class UserService {
private static final Logger LOGGER;
static {
LOGGER = LoggerFactory.getLogger(UserService.class);
}
// . . .
}
Code language: Java (java)
In the following example, an unmodifiable UUID is generated for each new Task
object:
public class Task {
private final UUID taskId = UUID.randomUUID();
// . . .
}
Code language: Java (java)
Alternatively, in the constructor:
public class Task {
private final UUID taskId;
public Task() {
taskId = UUID.randomUUID();
}
// . . .
}
Code language: Java (java)
The initialization of constants is not always that simple. I will show you some less trivial examples below.
Delayed a.k.a. Lazy Initialization
Final fields are initialized in any case, even if they are not used at all (or much later). However, if the initialization, e.g., the creation of the logger, takes a while (e.g., because it establishes a connection to an external logging system), but the logger is then never (or only later) used in the program flow, the start of the application may have been unnecessarily slowed down by the early initialization.
Fields that are expensive to initialize can be initialized with a delay, i.e. only when required (“lazy”). This is simple in a single-threaded application:
public class UserService {
private static Logger logger;
// Not thread-safe!!!
private static Logger getLogger() {
if (logger == null) {
logger = LoggerFactory.getLogger(UserService.class);
}
return logger;
}
// . . .
}
Code language: Java (java)
A second use case:
When creating an object, we do not always have all the information we need to initialize an immutable field. For example, a service could be created before a database connection has been established – but the service must access the database to initialize a field.
We can also initialize such a field lazily:
public class BusinessService {
private Settings settings;
// Not thread-safe!!!
private Settings getSettings() {
if (settings == null) {
settings = loadSettingsFromDatabase();
}
return settings;
}
// . . .
}
Code language: Java (java)
In a Spring or Jakarta EE application, we could also initialize the settings
variable in a method annotated with @PostConstruct
:
@Service
public class BusinessService {
private Settings settings;
@PostConstruct
private void initializeSettings() {
settings = loadSettingsFromDatabase();
}
// . . .
}
Code language: Java (java)
But these are all workarounds, and they have some significant disadvantages. You can find out what these are in the following section.
Disadvantages of “Homemade” Lazy Initialization
Looking at the examples from the previous section again, we notice that the logger
and settings
fields are no longer marked as final
. This is only possible if they are initialized during the declaration, in a static
block, or the constructor.
This, in turn, means that we cannot guarantee that the fields will not be modified after initialization. Without the guarantee that the values are immutable, the JVM cannot perform constant folding.
In addition, at least in the first two examples, we must ensure that we never access the fields directly but always via the getLogger()
or getSettings()
method.
And if we look at these methods again, we realize that they are not (yet) thread-safe! This means that they cannot be called from multiple threads.
To make the getSettings()
method thread-safe, we could mark it with synchronized
:
private synchronized Settings getSettings() {
if (settings == null) {
settings = loadSettingsFromDatabase();
}
return settings;
}
Code language: Java (java)
Although this makes it thread-safe, it also makes the application significantly slower, as the synchronized
block must now be accessed every time the settings are accessed.
Double-checked locking is faster (but also more error-prone):
private volatile Settings settings; // ⟵ `settings` must be volatile!
private Settings getSettings() {
Settings localRef = settings;
if (localRef == null) {
synchronized (this) {
localRef = settings;
if (localRef == null) {
settings = localRef = loadSettingsFromDatabase();
}
}
}
return localRef;
}
Code language: Java (java)
You can find out why you should never forget volatile
and the purpose of the additional (at first glance superfluous) variable localRef
in the Wikipedia article on double-checked locking.
An alternative is the so-called class-holder idiom, which exploits the fact that the JVM loads classes lazily and thread-safe. But this is also a workaround. Not everyone knows and understands it, and it only works with static fields, not with instance fields.
To summarize:
- Values initialized lazily cannot be marked as
final
; immutability is therefore not guaranteed. - Accordingly, the JVM cannot optimize the code by constant folding.
- A lazily initialized value must always be accessed via a helper method.
- In multithreaded applications, this helper method must be thread-safe. Errors can easily creep in here, leading to subtle race conditions.
What we lack in Java is a middle ground between final
and modifiable. A value that is initialized when it is needed. A value that is guaranteed to be initialized only once. And a value that is initialized correctly even if accessed from multiple threads.
And Stable Values are precisely this middle ground!
The Solution: Stable Values
A Stable Value is a container that contains an object, the so-called “content.” A stable value is initialized exactly once before its content is retrieved, after which it is immutable. A stable value is thread-safe. And the JVM can optimize a stable value through constant folding just as well as it can optimize a final field.
Below, I will show you two variants of how you can implement the Settings
example with a Stable Value.
First variant, with a getter:
public class BusinessService {
private final StableValue<Settings> settings = StableValue.of();
private Settings getSettings() {
return settings.orElseSet(this::loadSettingsFromDatabase);
}
public Locale getLocale() {
return getSettings().getLocale(); // ⟵ Here we access the stable value
}
// . . .
}
Code language: Java (java)
The static StableValue.of()
method generates an uninitialized Stable Value.
The orElseSet()
method checks whether the content of the Stable Value is already set. If yes, it returns it. If not, the value is retrieved via the given supplier (in the example, the method reference this::loadSettingsFromDatabase
) and stored within the Stable Value. Each subsequent call to orElseSet()
then returns the stored value.
The orElseSet()
method guarantees that the supplier is only called once, even if the method is called from several threads simultaneously.
This is already much less complicated than before, as the if
check is no longer necessary, and we no longer have to worry about thread safety. But we still have a helper method.
Can we use a Stable Value without the helper method?
Stable Supplier
Yes, we can – with the second variant, a supplier:
public class BusinessService {
private final Supplier<Settings> settings =
StableValue.supplier(this::loadSettingsFromDatabase);
public Locale getLocale() {
return settings.get().getLocale(); // ⟵ Here we access the stable value
}
// . . .
}
Code language: Java (java)
StableValue.supplier()
returns a so-called “Stable Supplier.” The first time the get()
method is invoked, the content of the Stable Value is initialized.
When using a Stable Supplier, the declaration and initialization of the field are directly next to each other, making the code easier to read than if the initialization is in a helper method.
So why should you even choose variant one, i.e. the helper method? Because there could be use cases in which the initialization of a stable value requires information that is only available through such a helper method.
Stable List
We can not only define individual Stable Values but also a list of Stable Values, i.e. a list in which each individual element is lazily initialized when it is first accessed – e.g. with first()
, get(int index)
or last()
.
The following example creates a Stable List in which each element is initialized with the square root of the list index the first time it is accessed:
List<Double> squareRoots = StableValue.list(100, Math::sqrt);
Code language: Java (java)
The size of the list and its elements cannot be changed. The methods add()
, set()
, and remove()
lead to an UnsupportedOperationException
. Derived lists – e.g. with subList()
or reversed()
– are also Stable Lists.
Here is a small demo program (I am using a simplified main method, which is available as a preview feature in Java 21 and will probably be finalized in Java 25):
void main() {
List<Double> squareRoots = StableValue.list(100, i -> {
println("Initializing list element at index " + i);
return Math.sqrt(i);
});
println("squareRoots[0] = " + squareRoots.get(0));
println("squareRoots[1] = " + squareRoots.get(1));
println("squareRoots[2] = " + squareRoots.get(2));
println("squareRoots[0] = " + squareRoots.get(0));
println("squareRoots.first = " + squareRoots.getFirst());
println("squareRoots.last = " + squareRoots.getLast());
}
Code language: Java (java)
The program prints the following:
Initializing list element at index 0
squareRoots[0] = 0.0
Initializing list element at index 1
squareRoots[1] = 1.0
Initializing list element at index 2
squareRoots[2] = 1.4142135623730951
squareRoots[0] = 0.0
squareRoots.first = 0.0
Initializing list element at index 99
squareRoots.last = 9.9498743710662
Code language: plaintext (plaintext)
You can see that the Stable List only computes the element at position 0 once, although it is retrieved three times (twice with get(0)
and once with getFirst()
).
Stable Map
Analogous to Stable Lists, we can also create Stable Maps. With a Stable Map, for each key, the associated value is only initialized the first time it is looked up.
The following example shows a Stable Map with which we can dynamically load localization resources per language when first accessed:
Set<Locale> supportedLocales = getSupportedLocales();
Map<Locale, ResourceBundle> resourceBundles =
StableValue.map(supportedLocales, this::loadResourceBundle);
Code language: Java (java)
The corresponding resource bundle is loaded via the loadResourceBundle(...)
method passed as a method reference only when resourceBundles.get(...)
is called for the first time.
Stable Function
We can also implement the same functionality with a Stable Function:
Set<Locale> supportedLocales = getSupportedLocales();
Function<Locale, ResourceBundle> resourceBundles =
StableValue.<em>function</em>(supportedLocales, this::loadResourceBundleForLocale);
Code language: Java (java)
Stable Function and Stable Map differ in their APIs:
- With a Stable Map, we can use the methods of the
Map
interface – e.g.get()
,containsKey()
, andsize()
–put()
, on the other hand, leads to anUnsupportedOperationException
. - With a Stable Function, we can use the methods of the
Function
interface –apply()
,andThen()
, andcompose()
.
Stable IntFunction
A special case of the Stable Function is a Stable IntFunction. This has an int
as an input parameter. It is, therefore, similar to the Stable List:
IntFunction<Double> squareRoots = StableValue.intFunction(100, Math::sqrt);
Code language: Java (java)
Stable IntFunction and Stable List differ in their APIs:
- With a Stable List, we can use the methods of the
List
interface – e.g.get()
,getFirst()
, andgetLast()
– but all modifying methods throw anUnsupportedOperationException
. - With a Stable IntFunction, we can use the (only) method of the
IntFunction
interface –apply()
.
StableValue API
In the course of this article, you got to know the static methods of()
, supplier()
, list()
, map()
, function()
, and intFunction()
– and the instance method orElseSet()
. Here are their definitions in a nutshell:
Static methods:
of()
– creates a Stable Value without content and without a content supplier.supplier(Supplier supplier)
– creates a Stable Supplier that initializes the content via the given supplier once when theget()
method is called.list(int size, IntFunction mapper)
– creates a Stable List, i.e. a list whose elements are, when accessed, initialized once via the passedmapper
function.map(Set keys, Function mapper)
– creates a Stable Map, analogous to the Stable List, which calls the givenmapper
function at most once for each input value from thekeys
set and caches the returned value.function(Set inputs, Function original)
– creates a stable wrapper around aFunction
, which calls theoriginal
function at most once for each input value from theinputs
set and caches the returned value.intFunction(int size, IntFunction original)
– creates a stable wrapper around anIntFunction
, which calls theoriginal
function at most once for eachint
value in the range0
tosize-1
and caches the returned value.
Instance methods:
orElseSet(Supplier supplier)
– returns the content of the Stable Value if it is set; otherwise, it sets the content by calling the given supplier and returns it.
The StableValue
class defines a few more methods, which I will only describe briefly here, without detailed examples.
Other static methods:
of(T content)
– generates a Stable Value already initialized withcontent
.
Other instance methods:
trySet(T content)
– sets the content and returnstrue
if the content is not yet set; otherwise returnsfalse
.orElse(T other)
– returns the content if it is set; otherwise, returnsother
without setting the content toother
.orElseThrow()
– returns the content if it is set; otherwise, throws aNoSuchElementException
.isSet()
– checks whether the content is set and returnstrue
orfalse
accordingly.setOrThrow(T content)
– similar totrySet(T content)
: sets the content if it is not yet set; otherwise throws anIllegalStateException
.
You can find preliminary Javadoc documentation here.
How Do Stable Values Work Internally?
Stable Values are implemented exclusively in Java code. Changes to the compiler, bytecode, or JVM were not necessary, as you can see from the pull request for JEP 502.
The content of a Stable Value is stored in a non-final field. This field is annotated with the JDK-internal @Stable
annotation, which is also used in other parts of the JDK code for optimization. This annotation indicates to the JVM that the value will not change after initialization. And so, once the value has been set, the JVM can start with its constant folding optimization.
Thread security is ensured by memory barriers set via the Unsafe
class.
Does this mean that StableValue
is just a wrapper we could implement ourselves?
Yes, but...
Firstly, we cannot use the JDK-internal @Stable
annotation or the internal Unsafe
class without explicitly making them available to our module via --add-exports java.base/jdk.internal.vm.annotation
or --add-exports java.base/jdk.internal.misc
.
Secondly, we should not use these JDK internals, as there is no guarantee that they will not change in a future Java release.
And thirdly, we do not write a ConcurrentHashMap
ourselves either, for example. Since StableValue
is implemented by JDK specialists, we can be sure that all known performance tricks have been applied and that further performance optimizations will be made in the future. And if, in the future, StableValue
is used by millions of Java developers, we can also be sure that any bugs – even subtle concurrency bugs – will be found and fixed quickly.
Conclusion
Stable Values are constants that can be initialized “on demand” at any time. They are then immutable and are treated by the JVM exactly like final fields, e.g., optimized by constant folding.
Stable Values are thread-safe, so they can also be used in multithreaded programs without risking subtle concurrency bugs.
In addition to Stable Values, there are Stable Lists, Stable Maps, and Stable Functions, which initialize the elements in lists, values in maps, and return values of functions once and then store them immutably.
Stable Values are a preview feature and are not yet included in the current early access build of Java 25 (build 17). If you want to experiment with them, you would currently have to copy the classes from the pull request.
What do you think of Stable Values? Share your opinion in the comments!
Would you like to be one of the first to be informed as soon as Stable Values are available in the early access release of Java 25 ... or as soon as Java 25 is released? Then click here to sign up for the HappyCoders newsletter, in which I will keep you informed about the latest developments in the Java world.