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 the getLogger() method. If not, a NullPointerException will be thrown if logger hasn't been initialized.
  • Thread contention can occur when multiple threads try to simultaneously initialize logger. This example declares getLogger() as synchronized. 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 declaring getLogger() as synchronized can result in multiple logger objects being created.
  • Because logger isn't declared as final, 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();
The method 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.