Core Java

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.

Yatin Batra

An experience full-stack engineer well versed with Core Java, Spring/Springboot, MVC, Security, AOP, Frontend (Angular & React), and cloud technologies (such as AWS, GCP, Jenkins, Docker, K8).
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button