Java 25: Understanding Stable Values
Java 25 introduces an innovative feature: the StableValue API (JEP 502). This new API allows the creation of objects that combine lazy initialization with immutability. Once a StableValue is initialized, it becomes immutable, offering thread-safe and efficient behavior similar to final variables, but with deferred initialization. Let us delve into understanding Java 25 Stable Values and how they simplify concurrency handling, improve application performance, and ensure immutability without compromising thread safety.
1. Introduction to the Problem
Traditionally, Java developers used final fields for immutability. However, these must be initialized eagerly during construction, which can slow down application startup. Lazy initialization, on the other hand, risks creating concurrency issues or null pointer exceptions. For example:
class ServiceController {
private EmailService service = null;
EmailService getService() {
if (service == null) {
service = new EmailService();
}
return service;
}
}
This approach is not thread-safe, and any concurrent access could lead to multiple initializations. With the latest release, Java 25 introduces a new concept called Stable Values, which addresses this exact problem by offering safe and efficient lazy initialization.
2. How to Solve this Problem?
The introduction of the StableValue class in Java 25 provides an elegant solution to long-standing challenges with safe, lazy initialization and immutability. Traditionally, developers had to rely on synchronization blocks, volatile fields, or third-party libraries like Guava’s Suppliers.memoize() to ensure thread-safe, one-time initialization. These approaches often added unnecessary complexity or synchronization overhead.
The StableValue class simplifies this by holding a value of type T that can be assigned exactly once—at any time during the program’s execution—and then becomes permanently immutable. It combines the safety of final fields with the flexibility of lazy initialization.
You can set its value using methods like orElseSet() or orElseGet(), ensuring that initialization logic is executed exactly once, even under concurrent access. Once the value is set, all threads see the same, safely published immutable value.
2.1 What is StableSupplier?
The StableSupplier extends the idea of stable values to supplier functions. It ensures that a supplier’s computation runs only once, no matter how many threads call it simultaneously. Subsequent invocations simply return the cached, stable result.
This design is particularly useful for expensive or I/O-bound operations such as loading configuration files, initializing large datasets, or establishing database connections. The StableSupplier guarantees that the expensive computation happens once, avoiding redundant work and synchronization overhead.
StableSupplier configLoader = StableSupplier.of(() -> loadConfigFromFile()); String config = configLoader.get(); // Loaded once, reused safely
This ensures thread-safe, on-demand initialization while maintaining immutability.
2.2 What are Stable Collections?
Stable Collections extend this immutability concept to collection types like Lists and Maps. Using factory methods such as StableValue.list() and StableValue.map(), developers can build collections that become immutable after their first initialization.
This is especially beneficial in concurrent applications where shared data structures must be populated once and then accessed safely by multiple threads. Once stabilized, these collections prevent accidental mutation, ensuring predictable behavior across the system.
StableValue<List<String>> userList = StableValue.list();
userList.orElseSet(() -> List.of("Alice", "Bob", "Charlie"));
System.out.println(userList.get()); // Immutable list
Such stable collections reduce the need for defensive copies and synchronization mechanisms, resulting in cleaner and faster concurrent code.
2.3 What is StableFunction?
The StableFunction offers a powerful abstraction for caching the output of deterministic computations. It ensures that once a function computes a result for a given input, that result is stored and reused for all future invocations with the same input.
This approach blends memoization and immutability, making it ideal for CPU-heavy calculations or recursive algorithms where the same computation may be repeated multiple times.
StableFunction<Integer, Long> factorial = StableFunction.of(n -> {
if (n <= 1) return 1L;
return n * factorial.apply(n - 1);
});
System.out.println(factorial.apply(5)); // Computed once and cached
The results are thread-safe, deterministic, and automatically cached, offering a balance between performance and correctness in concurrent environments.
2.4 Code Example
The following Java 25 example demonstrates how StableValue and StableFunction can be used for lazy initialization and caching of computed results in a thread-safe manner:
// Compile with:
// javac --enable-preview --release 25 StableValueDemo.java
// Run with:
// java --enable-preview StableValueDemo
import java.lang.StableValue;
import java.util.Set;
import java.util.function.Function;
import java.util.logging.Logger;
import java.time.Instant;
public class StableValueDemo {
// StableValue example - lazy initialization of Logger
private static final StableValue<Logger> logger = StableValue.of();
private static Logger getLogger() {
// Initializes the logger only once
return logger.orElseSet(() -> Logger.getLogger(StableValueDemo.class.getName()));
}
// StableFunction example - compute and cache logarithmic values
private static final Set<Integer> POWERS = Set.of(1, 2, 4, 8, 16, 32);
private static final Function<Integer, Integer> LOG2_FN = i -> {
try {
// Simulate a small delay to illustrate caching effect
Thread.sleep(100);
} catch (InterruptedException ignored) {}
return 31 - Integer.numberOfLeadingZeros(i);
};
private static final Function<Integer, Integer> STABLE_LOG2 = StableValue.function(POWERS, LOG2_FN);
public static void main(String[] args) {
getLogger().info("=== StableValue and StableFunction Demo ===");
getLogger().info("Logger initialized once, reused afterwards.");
System.out.println("First run (with computation):");
for (int n : POWERS) {
long start = System.nanoTime();
int result = STABLE_LOG2.apply(n);
long end = System.nanoTime();
System.out.printf("[%s] log2(%d) = %d (took %d µs)%n",
Instant.now(), n, result, (end - start) / 1000);
}
System.out.println("\nSecond run (should reuse cached results instantly):");
for (int n : POWERS) {
long start = System.nanoTime();
int result = STABLE_LOG2.apply(n);
long end = System.nanoTime();
System.out.printf("[%s] log2(%d) = %d (took %d µs)%n",
Instant.now(), n, result, (end - start) / 1000);
}
}
}
In this code, we first create a StableValue<Logger> instance named logger which demonstrates lazy initialization: the logger is not created until getLogger() is called, and after its first creation, the same logger instance is reused for all subsequent calls, ensuring thread-safe, single-time initialization. Next, we define a StableFunction named STABLE_LOG2 that computes logarithms base 2 for a predefined set of powers of two; the first time a value is computed for a specific input, it is cached, so subsequent calls return the cached result without recomputation. The main method prints results of these computations, first demonstrating the initial computation and then showing that repeated calls reuse the cached results efficiently, highlighting both the lazy initialization and deterministic caching capabilities of Java 25’s stable value and function API.
2.4.1 Code Run and Output
Oct 29, 2025 11:22:00 PM StableValueDemo main INFO: === StableValue and StableFunction Demo === Oct 29, 2025 11:22:00 PM StableValueDemo main INFO: Logger initialized once, reused afterwards. First run (with computation): [2025-10-29T17:52:00Z] log2(1) = 0 (took 101238 µs) [2025-10-29T17:52:00Z] log2(2) = 1 (took 100972 µs) [2025-10-29T17:52:01Z] log2(4) = 2 (took 100847 µs) [2025-10-29T17:52:01Z] log2(8) = 3 (took 100919 µs) [2025-10-29T17:52:01Z] log2(16) = 4 (took 100842 µs) [2025-10-29T17:52:02Z] log2(32) = 5 (took 100801 µs) Second run (should reuse cached results instantly): [2025-10-29T17:52:02Z] log2(1) = 0 (took 21 µs) [2025-10-29T17:52:02Z] log2(2) = 1 (took 18 µs) [2025-10-29T17:52:02Z] log2(4) = 2 (took 17 µs) [2025-10-29T17:52:02Z] log2(8) = 3 (took 15 µs) [2025-10-29T17:52:02Z] log2(16) = 4 (took 19 µs) [2025-10-29T17:52:02Z] log2(32) = 5 (took 20 µs)
The output shows that the logger is initialized only once, as indicated by the INFO messages, demonstrating the lazy initialization behavior of StableValue. The log2 calculations illustrate StableFunction caching: the first computation generates and caches results for each input, while the second set of calls immediately reuses these cached values without recomputation. This confirms the deterministic, thread-safe behavior of the stable values API in action.
3. Conclusion
The Stable Values API in Java 25 addresses a long-standing issue with immutability and lazy initialization. By allowing values to be set once, at any time, and then become immutable, StableValue merges the strengths of final fields with the flexibility of lazy loading. With support for StableSupplier, StableFunction, and stable collections, developers can build thread-safe, efficient, and highly performant applications.

