16 Stable Values
A stable value is an object of type StableValue that holds a single data value, which is called
its contents. A stable value's contents are immutable. The JVM treats stable
values as constants, which enables the same performance optimizations as
final
fields. However, you have greater flexibility with regards to
when you initialize a stable value's contents compared to final
fields.
Note:
This is a preview feature. A preview feature is a feature whose design, specification, and implementation are complete, but is not permanent. A preview feature may exist in a different form or not at all in future Java SE releases. To compile and run code that contains preview features, you must specify additional command-line options. See Preview Language and VM Features.For background information about stable values, see JEP 502.
Consider the following example that declares and initializes a Logger as a final field:
public class Locations {
private final Logger logger =
Logger.getLogger(Locations.class.getName());;
Logger getLogger() {
return logger;
}
public void printLocations() {
getLogger().info("Printing locations...");
}
}
Because logger
is a final
field, it must
be initialized when an instance of Locations
is created. This is an
example of eager initialization. However, the example doesn't use logger until printLocations()
is called. To
initialize logger
when this method is called, you can use lazy
initialization, which means that a result is produced (in this example, the
logger
field is initialized) only when it's needed:
public class Customers {
private Logger logger = null;
synchronized Logger getLogger() {
if (logger == null) {
logger = Logger.getLogger(Customers.class.getName());
}
return logger;
}
public void printCustomers() {
getLogger().info("Printing customers...");
}
}
In this example, logger
is initialized only when
getLogger()
is called and only if logger
hasn't
been initialized previously. However, there are several drawbacks with this
approach:
- Code must access the
logger
field through thegetLogger()
method. If not, a NullPointerException will be thrown iflogger
hasn't been initialized. - Thread contention can occur when multiple threads try to simultaneously
initialize
logger
. This example declaresgetLogger()
assynchronized
. As a result, when a thread invokes this method, all other threads are blocked from invoking it. This can create bottlenecks and slow down your application. However, not declaringgetLogger()
assynchronized
can result in multiple logger objects being created. - Because
logger
isn't declared asfinal
, the compiler can't apply performance optimizations related to constants to it.
It would be ideal if logger
were both lazily initialized
and immutable once it has been initialized. In other words, it would be ideal to
defer immutability. You can do this by using a stable value:
public class Orders {
private final StableValue<Logger> logger = StableValue.of();
Logger getLogger() {
return logger.orElseSet(
() -> Logger.getLogger(Orders.class.getName()));
}
public void printOrders() {
getLogger().info("Printing orders...");
}
}
The static factory method StableFactory.of() creates a stable value that holds no contents:
private final StableValue<Logger> logger = StableValue.of();
StableValue::orElseSet(Supplier)
retrieves the
contents of the stable value logger
. However, if the stable value
contains no contents, orElseSet attempts to compute and
set the contents with the provided Supplier:return logger.orElseSet(
() -> Logger.getLogger(Orders.class.getName()));
The Orders
example initializes a stable value when it's used, which is
when getLogger
is invoked. However, like the Customers
example, code must access the logger
stable value through the
getLogger()
method.
You can use a stable supplier, of type java.util.function.Supplier, to specify how to initialize a stable value when you declare it without actually initializing it. Afterward, you can access the stable value by invoking the supplier's get() method.
public class Products {
private final Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(Products.class.getName()));
public void printProducts() {
logger.get().info("Printing products...");
}
}
The method StableValue.supplier(Supplier) returns a java.util.function.Supplier:
private final Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(Products.class.getName()));
In this example, the first invocation of logger.get()
invokes the lambda expression provided as an argument to StableValue.supplier(Supplier):
() -> Logger.getLogger(Products.class.getName())
The StableValue.supplier(Supplier)
method initializes the
stable value's contents with the resulting value of this lambda expression and then
returns the value, which in this example is a new Logger
instance. Subsequent invocations of logger.get()
return the stable
value's contents immediately.
Aggregating and Composing Stable Values
You can aggregate multiple stable values in an application, which can
improve its startup time. In addition, you can compose stable values from other
stable values. The previous examples Orders
and
Products
show you how to store a logger component in a stable
value. The following example stores an Orders
,
Products
, Locations
, and
Customers
component in their own stable value.
public class Application {
static final StableValue<Locations> LOCATIONS = StableValue.of();
static final StableValue<Orders> ORDERS = StableValue.of();
static final StableValue<Customers> CUSTOMERS = StableValue.of();
static final StableValue<Products> PRODUCTS = StableValue.of();
public static Locations locations() {
return LOCATIONS.orElseSet(Locations::new);
}
public static Orders orders() {
return ORDERS.orElseSet(Orders::new);
}
public static Customers customers() {
return CUSTOMERS.orElseSet(Customers::new);
}
public static Products products() {
return PRODUCTS.orElseSet(Products::new);
}
public void main(String[] args) {
locations().printLocations();
orders().printOrders();
customers().printCustomers();
products().printProducts();
}
}
By storing an application's components in stable values, you can significantly improve its startup time. The application no longer initializes all of its components up front. This example initializes its components on demand through the stable value's orElseSet method.
Note that the Orders
and Products
components are using their own internal stable value for the logger. In particular,
the stable value ORDERS
is dependent on the stable value
Orders.logger
. The dependent Orders.logger
will first be created if ORDERS
does not already exist. The same
applies to the PRODUCTS
and Products.logger
stable
values.
If there's a circular dependency, then an IllegalStateException is thrown, which the following example demonstrates:
public class StableValueCircularDependency {
public static class A {
static final StableValue<B> b = StableValue.of();
A() {
// IllegalStateException: Recursive initialization
// is not supported
b.orElseSet(B::new);
}
}
public static class B {
static final StableValue<A> a = StableValue.of();
B() {
a.orElseSet(A::new);
}
}
public void main(String[] args) {
A myA = new A();
}
}
An IllegalStateException is thrown when the constructor of class
A
tries to initialize the stable value A.b
.
This stable value depends on B.a
, which is dependent on
A.b
.
Stable Lists and Stable int Functions
A stable list is an unmodifiable list, backed by an array of stable values and associated with an IntFunction. When you first access an element in a list, it's initialized with a value computed with the stable list's IntFunction, which uses the element's index as its parameter. When you access the element with the same index an additional time, its value is retrieved instead of being computed again.
When you create a stable list with the StableValue.list(int, IntFunction) factory method, you specify its size in the first argument, You specify how its elements are computed in the second argument.
The following example creates a pool of Products
objects. This enables different Products
to serve different
application requests, distributing the load across the pool. Instead of creating a
stable value for each Products
object in the pool, the example
creates a stable list. To simulate different application requests, the example
creates and starts two virtual threads. In each of these threads, a
Products
object is initialized in the stable list.
public class StableListExample {
private final static int POOL_SIZE = 20;
final static Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(StableListExample.class.getName()));
static final List<Products> PRODUCTS
= StableValue.list(POOL_SIZE, _ -> new Products());
public static Products products() {
long index = Thread.currentThread().threadId() % POOL_SIZE;
logger.get().info("Obtaining Products from stable list, index " + index);
return PRODUCTS.get((int)index);
}
public static void main(String[] args) {
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
System.out.println("Thread ID: " + Thread.currentThread().threadId());
products().printProducts();
};
try {
// name "worker-0"
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " terminated");
// name "worker-1"
Thread t2 = builder.start(task);
t2.join();
System.out.println(t2.getName() + " terminated");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
The example prints output similar to the following:
Thread ID: 22
Apr 24, 2025 6:59:16 PM StableListExample products
INFO: Obtaining Products from stable list, index 2
Apr 24, 2025 6:59:16 PM Products printProducts
INFO: Printing products...
worker-0 terminated
Thread ID: 26
Apr 24, 2025 6:59:16 PM StableListExample products
INFO: Obtaining Products from stable list, index 6
Apr 24, 2025 6:59:16 PM Products printProducts
INFO: Printing products...
worker-1 terminated
The following statement creates a stable list that holds 20
Products
objects. Note that none of the stable list's elements
are initialized yet.
static final List<Products> PRODUCTS
= StableValue.list(POOL_SIZE, _ -> new Products());
The first invocation of PRODUCTS.get(int index)
initializes the stable value's contents located at index
with the
following lambda expression:
_ -> new Products()
Tip:
The underscore character (_
) is an unnamed variable, which represents a variable that's
being declared but it has no usable name. It helps to indicate that a variable is
not used after its declaration. See Unnamed Variables and Patterns in Java Platform, Standard Edition Java Language Updates.
Subsequent invocations of PRODUCTS.get(int index)
with
the same index retrieve the element's contents immediately.
A stable int
function works similarly to a stable list.
A stable int
function is a function that takes an
int
parameter and uses it to compute a result, which is then
cached by the backing stable value storage for that parameter value. Consequently,
when you call the stable function with the same int
parameter an
additional time, its result is retrieved instead of being computed again.
Like a stable list, when you create a stable int
function with the StableValue.intFunction(int,
IntFunction) factory method, you specify its size in the first
argument. You specify how its elements are computed in the second argument.
The following example is just like StableListExample
except that it uses a stable int
function. Significant changes in
the code are highlighted:
public class StableIntFunctionExample {
private final static int POOL_SIZE = 20;
final static Supplier<Logger> logger =
StableValue.supplier(
() -> Logger.getLogger(StableIntFunctionExample.class.getName()));
static final IntFunction<Products> PRODUCTS
= StableValue.intFunction(POOL_SIZE, _ -> new Products());
public static Products products() {
long index = Thread.currentThread().threadId() % POOL_SIZE;
logger.get().info("Obtaining Products from stable int function, index " + index);
return PRODUCTS.apply((int)index);
}
public static void main(String[] args) {
Thread.Builder builder = Thread.ofVirtual().name("worker-", 0);
Runnable task = () -> {
System.out.println("Thread ID: " + Thread.currentThread().threadId());
products().printProducts();
};
try {
// name "worker-0"
Thread t1 = builder.start(task);
t1.join();
System.out.println(t1.getName() + " terminated");
// name "worker-1"
Thread t2 = builder.start(task);
t2.join();
System.out.println(t2.getName() + " terminated");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
The difference between a stable list and a stable int function is how you interact with it. You interact with a stable map like a List. You access its values with the Map::get(int) method. In addition, you can call the List interface's methods that don't modify its values such as List::subList and List::reversed(). You interact with a stable function like an IntFunction. You compute a value with the Function::apply method. You can't obtain a List view of the computed values stored in a stable int function, but you can use it in a statement or as a parameter that requires a lambda expression of type IntFunction<R>.
Stable Maps and Stable Functions
A stable map is an unmodifiable map whose keys you specify when you create it. It's also associated with a Function. When you first access a key's value, it's initialized with a value computed with the stable map's Function, which uses the key as its parameter. When you access the key's value an additional time, its value is retrieved instead of being computed again.
The following example calculates the log base 2 of an integer by counting the number of leading zeroes in its binary representation. Note that the example can only calculate the log base 2 of the first six powers of 2. When you create a stable map with the StableValue.map(Set, Function) factory method, you must specify all of the possible parameters that the stable map's Function can accept in the first argument. You specify how a key's value is computed in the second argument.
public class Log2StableMap {
private static final Set<Integer> KEYS =
Set.of(1, 2, 4, 8, 16, 32);
private static final Function<Integer, Integer> LOG2_FUNCTION =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Map<Integer, Integer> LOG2_SM =
StableValue.map(KEYS, LOG2_FUNCTION);
public static void main(String[] args) {
System.out.println("Log base 2 of 16 is " + LOG2_SM.get(16));
System.out.println();
LOG2_SM.entrySet()
.stream()
.forEach(e -> System.out.println(
"Log base 2 of " + e.getKey() + " is " + e.getValue()));
}
}
The example prints output similar to the following:
Log base 2 of 16 is 4
Log base 2 of 4 is 2
Log base 2 of 16 is 4
Log base 2 of 2 is 1
Log base 2 of 1 is 0
Log base 2 of 8 is 3
Log base 2 of 32 is 5
A stable function works similarly to a stable map. A stable function is a function that takes a parameter and uses it to compute a result, which is then cached by the backing stable value storage for that parameter value. Consequently, when you call the stable function with the same parameter an additional time, its result is retrieved instead of being computed again.
The following example is just like Log2StableMap
except it uses a
stable function instead of a stable map. Significant changes in the code are
highlighted:
public class Log2StableFunction {
private static final Set<Integer> KEYS =
Set.of(1, 2, 4, 8, 16, 32);
private static final Function<Integer, Integer> LOG2_FUNCTION =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Function<Integer, Integer> LOG2_SF =
StableValue.function(KEYS, LOG2_FUNCTION);
public static void main(String[] args) {
System.out.println("Log base 2 of 16 is " + LOG2_SF.apply(16));
System.out.println();
KEYS.stream()
.forEach(e -> System.out.println(
"Log base 2 of " + e + " is " + LOG2_SF.apply(e)));
}
}
Again, note that the example can only calculate the log base 2 of the first six powers of 2. When you create a stable function with the StableValue.function(Set, Function) factory method, you must specify all of the possible parameters that the stable function's Function can accept in the first argument. You specify how the stable function computes a result in the second argument.
As with stable lists and stable int
functions, the
difference between a stable map and a stable function is how you interact with it.
You interact with a stable map like a Map. You
access its values with the Map::get(Object key)
method. In addition, you can call the Map interface's
methods that don't modify its values such as Map::values() and Map::entrySet().
You interact with a stable function like a Function.
You compute a value with the Function::apply method.
You can't obtain a Collection view of the computed
values stored in a stable function, but you can use it in a statement or as a
parameter that requires a lambda expression of type Function<T,R>.
Constant Folding
Constant folding is a compiler optimization in which constant
expressions are evaluated at compile time instead of run time. The JIT compiler
sometimes performs constant folding for fields declared final
,
stable values that reside in static fields, records, and hidden classes. Constant
folding elides the need to load a value from memory because the value can instead be
embedded in the machine code emitted by the JIT compiler. Constant folding is often
the first step in a chain of optimizations that together can provide significant
performance improvements.
Thread Safety
As demonstrated in the example StableListExample and StableIntFunctionExample, stable values are thread-safe. The contents of a stable value is guaranteed to be set at most once. If competing threads are racing to set a stable value, only one update succeeds, while other updates are blocked until the stable value becomes set.