Stable Values

By: Daniel Hinojosa

Published on: November 13, 2025

Stable Values

Stable Values is now preview with JEP JEP 502, but it will be renamed to Lazy Constants in JEP 526

Stable Values fill a need to lazily evaluate, so that you do not have to incur a performance hit of loading large objects immediately

Using java.time.LocalDateTime

To begin, I will run these examples using jshell. The REPL, also known as an interactive shell, is built into modern JDKs. I am also going to run it using the --enable-preview flag since StableValue is still in preview mode. When this JEP is finally released, you will not need the --enable-preview flag, and in all likelihood, you will be using LazyConstant instead of StableValue.

$ jshell --enable-preview

Let’s begin with a timestamp, LocalDateTime.now(). I invoke a timestamp, but I want it to be invoked lazily.

jshell> var timeString = LocalDateTime.now();
timeString ==> 2025-11-07T11:13:23.940621

Naturally, by hitting RETURN, I should get the time, but what time is being invoked? Let’s say for this blog post, the current time is November 7, 2025, at 11:13:23 AM Mountain Standard Time. Looking at the above snippet, we see it immediately ran it; that is not lazy, that is eager, it gives it to me right away.

How have we done laziness up to this point in Java? Or at least attempted to do so. Well, that would be a factory. If you are a long-time Java developer, then this is familiar territory.

jshell> class TimeFactory {
   ...>     public LocalDateTime getTime() {
   ...>        return LocalDateTime.now();
   ...>     }
   ...> }
|  created class TimeFactory

jshell> var factory = new TimeFactory()
factory ==> TimeFactory@2cdf8d8a

jshell> factory.getTime()
$4 ==> 2025-11-07T11:17:02.352269

Now, the above example is from Java circa 1995 to 2014, which was a long time ago. We have advanced since then, and we have, of course, Supplier, which we got with JDK 8. Let’s get with the times and use Supplier instead of a Factory.

jshell> Supplier<LocalDateTime> timeSupplier = () -> LocalDateTime.now();
timeSupplier ==> $Lambda/0x000001fc0104dce8@1698c449

We notice here that we indeed have Lambda ready to be called, $Lambda/0x000001fc0104dce8@1698c449, at a moment’s notice. This makes it lazy, since we can call it and materialize it on demand.

Let’s call it!

jshell> timeSupplier.get()
$6 ==> 2025-11-07T11:20:38.101959

Great, we got our laziness. Well, the name of the JEP is soon to be Lazy Constant. Do we have that consistency?

Let’s press on.

jshell> timeSupplier.get()
$7 ==> 2025-11-07T11:22:13.001363

Again,

jshell> timeSupplier.get()
$8 ==> 2025-11-07T11:22:28.409443

Time keeps on slipping, slipping, slipping…​ into the future

— Fly Like an Eagle
Steve Miller Band

The issue is that, yes, we do keep slipping into the future; every call to get() renders a new date time, and there is nothing consistent about that. What we want is to trigger the get() for the date time, and every other subsequent get() should return that same date time.

This answers why there is a need for lazy constants

Stable values (again, soon to be Lazy Constants) invoke objects lazily on demand and stay constant. The software term is called memoization

Memoization: In computing, memoization…​ is an optimization technique used primarily to speed up computer programs. It works by storing the results of expensive calls to pure functions so that these results can be returned quickly should the same inputs occur again.

— Wikipedia
on Memoization

Let’s begin with how to use StableValue

Invoking via a method

The first way we can get started is with StableValue.of. of is a kind of placeholder, to be filled in with what will be returned inevitably. One thing to notice is that I am not using var. That’s because I need to establish the type somehow, and that will have to be by the left-hand type assignment. One thing to notice here is that it is .unset, which is the return representation that I get when the value has not been realized.

jshell> private final StableValue<LocalDateTime> timeSupplier = StableValue.of();
timeSupplier ==> .unset

So, how do we fill it? To do so, we will create a custom method that will use the stable value reference, and we will call orElseSet, which is a Supplier that defines how we materialize the LocalDateTime when required.

jshell> LocalDateTime getLocalTimeAndStayThatWay() {
   ...>   return timeSupplier.orElseSet(() -> LocalDateTime.now());
   ...> };
|  created method getLocalTimeAndStayThatWay()

We have our method, and we it is initially .unset, so let’s invoke!

jshell> getLocalTimeAndStayThatWay()
$4 ==> 2025-11-07T11:41:09.134613

But does it pass the test that it is constant, and we get our memoization?

jshell> getLocalTimeAndStayThatWay()
$4 ==> 2025-11-07T11:41:09.134613

jshell> getLocalTimeAndStayThatWay()
$5 ==> 2025-11-07T11:41:09.134613

jshell> getLocalTimeAndStayThatWay()
$6 ==> 2025-11-07T11:41:09.134613

jshell> getLocalTimeAndStayThatWay()
$7 ==> 2025-11-07T11:41:09.134613

That’s the power of memoization.

Using Supplier with StableValues

Yeah, but it is too much work. Is there a better way, maybe inline it somewhat? Yes, you can use a Supplier and make it a one-line declaration. I will assume that this is likely the way you will want to use it.

Create a one-liner stable value with a supplier method
jshell> private final Supplier<LocalDateTime> timeSupplierAndStayThatWay = StableValue.supplier(() ->
   ...>    LocalDateTime.now()
   ...> )
timeSupplierAndStayThatWay ==> .unset

Concise, readable.

Let’s invoke it and prove its laziness and memoization. Keep in mind that these are Suppliers, so you will need to invoke get to retrieve the values.

jshell> timeSupplierAndStayThatWay.get()
$13 ==> 2025-11-07T11:48:37.789473

jshell> timeSupplierAndStayThatWay.get()
$14 ==> 2025-11-07T11:48:37.789473

jshell> timeSupplierAndStayThatWay.get()
$15 ==> 2025-11-07T11:48:37.789473

jshell> timeSupplierAndStayThatWay.get()
$16 ==> 2025-11-07T11:48:37.789473

We see that we have constant timestamps after each invocation.

Using Lazy Lists

One of the intriguing aspects of the API is the use of lazy lists. Lazy lists are a fixed number of elements that will only be evaluated when called upon, and once called upon, it will stay in that state forever — memoization.

The JEP also includes a nifty example, where, based on the thread ID, it performs consistent hashing to locate a slot in the list and retrieves it. If it isn’t there, it runs the function to create it; if it already exists, then retrieve the memoized object.

Let’s start with an empty list and think of it as a platform with functions ready to serve you.

IntroStableValuesList

Now, let’s say given our thread, Thread-3, we wish to retrieve an element. Given our formula Thread-id % POOL_SIZE, we can retrieve an element from the slot. In the example below, given an id of 3, modulo 3 with 8, the size of the list, and obviously, you will get 3, so the third element will return the object, lazily.

StableValueList Thread3

Trying again.

This time, the thread-id is 54, and performing the math, 54 % 8, we will get 6, so we will lazily load the object, and forever it will return that same object.

StableValueList Thread54

Here is a code example to demonstrate

A lazy list where the elements are not there initially but lazily loaded
private static final String[] COLORS =
        {"red", "orange", "yellow", "green",
        "blue", "indigo", "violet", "black"}; (1)

public static final int SIZE = 8; (2)

record Ball(String color) {} (3)

List<Ball> list =
  StableValue.list(SIZE, new IntFunction<Ball>() {
      @Override
      public Ball apply(int i) {
          String color = COLORS[i];
          System.out.printf("Loading %s ball%n", color);
          return new Ball(color);
      }
  });  (4)

IntStream
  .range(1, 5)
  .forEach(_ ->
      Thread.ofVirtual().start(() -> {
          Ball ball =
             list.get(
                 (int) Thread.currentThread().threadId() % SIZE);
          System.out.printf("Thread %s got %s ball%n",
             Thread.currentThread().threadId(), ball.color());
      })); (5)
  1. Initializing some colors in a String array

  2. Constraining the size of the list

  3. Creating a value object to represent the Ball

  4. Creating the StableValue.list, which will load the ball lazily

  5. Create 5 Threads and get values from the list

The results will look something indeterminately like the following. Note that the indigo ball is loaded only once.

Loading indigo ball
Thread 29 got indigo ball
Thread 37 got indigo ball
Loading black ball
Thread 31 got black ball
Loading orange ball
Thread 33 got orange ball

As another example. Let’s create another test where we select one element from the list and examine its representation.

I will use the same StableValue.list that I previously used, but I will only retrieve one element, specifically the third element.

List<Ball> list = StableValue.list(SIZE, i -> {
    String color = COLORS[i];
    System.out.printf("Loading %s ball%n", color);
    return new Ball(color);
});

Ball ball = list.get(3);

Performing a System.out.println on the list, and I get the following, showing that only the element that I plucked from the list is materialized. All others are .unset since they have not yet been called upon.

[.unset, .unset, .unset, Ball[color=green], .unset, .unset, .unset, .unset]

Using Lazy Maps

One of the central themes with StableValues is constraining the pool size of which you can retrieve elements.

In the previous example, with StableValue.list it had a fixed size. Well, the same will hold for map. You can create a stable map much like the list, but the rule is, you must have a fixed or closed set of keys.

In this example, we will use enum planets are our constrained set of keys. This is slightly more data-driven and anemic than the popular Oracle example we all know and love where the dimensions of the planets are defined in the enum.

enum of Planets
enum Planet {
    MERCURY,
    VENUS,
    EARTH,
    MARS,
    JUPITER,
    SATURN,
    URANUS,
    NEPTUNE;
}

Next, let’s create a basic value object that will represent details about the planets.

A record that represents the Planet data to be read from the file system
public record PlanetData(String name, double mass, double radius, int moons, String color) {
}

I have a planet.json file in src/main/resources that contains the data about the planets. What I would like to do is create a loader class that loads the document, parses it, and gets the planet data. I am purposefully not caching the mapped object that represents the file, and the point is to make it painfully slower and to add more latency. I also threw in Thread.sleep(4000) for good measure.

Load planet data from /src/main/resources and convert to JSON
public class PlanetLoader {

    private static final ObjectMapper mapper = new ObjectMapper();

    public static PlanetData load(Planet planet) {
        try {
            System.out.printf("Loading data for %s, please wait%n", planet);

            Thread.sleep(4000);

            List<PlanetData> planetDataList = mapper.readValue(
                PlanetLoader.class.getResourceAsStream("/planets.json"),
                new TypeReference<>() {
                }
            );

            return planetDataList.stream()
                .filter(planetData ->
                    planetData.name().equals(titleCase(planet.name())))
                .findFirst()
                .orElseThrow();

        } catch (Exception e) {
            throw new RuntimeException("Failed to load planet data", e);
        }
    }
}

Let’s now run StableValue.map. The first thing to notice is that we have a closed key set. Again, StableValue requires constraints, and in the case of a map, it is a constrained key set. Since I am using an enum, enums are already constrained so it works out well. For the value, section PlanetLoader.load is the static method we saw above that takes the key and loads the data element from the JSON file.

Establishing a closed set of keys, our planets, and the values are lazily loaded from the file system
Map<Planet, PlanetData> planetMap = StableValue.map
            (EnumSet.allOf(Planet.class), PlanetLoader::load);

Now, a test to run everything where we can assert that we get the correct number of moons for Earth is, of course, lazy and memoized.

Asserting the number of moons on Earth
PlanetData planetData = planetMap.get(Planet.EARTH);
      assertThat(planetData.moons()).isOne();
      System.out.println(planetMap);

Printing what the map looks like after retrieving Mother Earth from it shows that the map only materializes what it needs

{NEPTUNE=.unset, MARS=.unset, URANUS=.unset, SATURN=.unset, EARTH=PlanetData[name=Earth, mass=1.0, radius=1.0, moons=1, color=blue], JUPITER=.unset, VENUS=.unset, MERCURY=.unset}

Mentally, the following image should form as to what is happening.

ThreadPoolStableValuesMap
Figure 1. Before and After lazy loading Earth into our StableValue.Map

Using Lazy Functions

Lazy functions are similar in functionality to the map. Whereas the lazy map you provided a closed set of keys, with the function, you provide a closed set of inputs.

ThreadPoolStableValuesExportNegotiation

In the following example, we will use the same PlanetLoader.load to load the data, inefficiently. First, by establishing a closed set of inputs. and the function used, in our case, again, PlanetLoader's PlanetData load(Planet planet)

Establishing StableValue.function with a closed set of planets.
Function<Planet, PlanetData> planetMap =
   StableValue.function
     (EnumSet.allOf(Planet.class), PlanetLoader::load);

Now, what I think is the most compelling part of using a StableValue.function, plugging it into a Stream. In the following example, I stream the planets, and use map and my StableValue.function to load the planet’s data from the document. Following that map(planetMap), I can continue to perform further data manipulation to massage the data into what I need for the terminal operation. In the example below, I extract the color and uniquely store all the planet colors in a single set.

Stream data and lazily load the function and memoize it
Set<String> allPlanetColors =
        Stream.of(Planet.values())
            .map(planetMap)
            .map(PlanetData::color)
            .collect(Collectors.toSet());

What are some use cases?

So, where will we be using this? I am not in the business of predicting the future, but let’s navigate some possibilities.

Dependency Injection

The JEPs example uses an Order Controller as an example of something that can be loaded lazily, which is that you are not using Spring or Quarkus, and want to manage your loading mechanism for components. This looks like a great approach.

And yes, there are people who don’t use Spring or Quarkus on a daily basis, like me.

Lazy loading components for dependency injection
static class Application {
    final StableValue<OrderController> ORDERS = StableValue.of();
    final StableValue<ProductRepository> PRODUCTS = StableValue.of();
    final StableValue<UserService> USERS = StableValue.of();

    private final StableValue<Logger> logger = StableValue.of();

    Logger getLogger() {
        return logger.orElseSet(() -> {
            var logger = LoggerFactory.getLogger(Application.class);
            logger.info("Loading Application Logger");
            return logger;
        });
    }

    public OrderController orders() {
        return ORDERS.orElseSet(OrderController::new);
    }

    public ProductRepository products() {
        return PRODUCTS.orElseSet(ProductRepository::new);
    }

    public UserService users() {
        return USERS.orElseSet(UserService::new);
    }

    public void run() {
        getLogger().info("Application started");
        orders()
          .submitOrder(users().getUser("jdoe@localhost.com"), products().getProducts());
        getLogger().info("Application finished");
    }
}

Logging

Obviously, obtaining a Logger is something that we usually want to load lazily, and we only need to use one logger per class, so that seems like a good fit, and one that Spring and Quarkus developers may use within their services, repositories, controllers, etc.

Loading a logger lazily and only once
class OrderController {
    private final Supplier<Logger> logger =
        StableValue.supplier(() ->
            LoggerFactory.getLogger(OrderController.class));

    void submitOrder(User user, List<Product> products) {
        logger.get().info("Order Started for {} ordering {}", user, products);
        logger.get().info("Order Submitted");
    }
}

Singleton Design Pattern

Stable Values have the look and feel of a Singleton Pattern.

If you recall, one of the often overlooked items when creating a singleton is that many implementations don’t protect against multithreading. With Stable Values, thread safety is guaranteed. Per JEP 526:

Guarantee that lazy constants are initialized at most once, even in multithreaded programs.

— https://openjdk.org/jeps/526

Flyweight Design Pattern

The flyweight pattern is a cache that holds on to regularly used objects in case they need to be used again, they just return the immutable object instead of recreating it all over again.

Looking at the diagram below from Wikipedia’s page on the Flyweight Pattern, we can see clearly how this pattern is very easily implemented using either Stable.map or Stable.function

flyweight wikipedia
Figure 2. Flyweight pattern from Wikipedia

Conclusion

StableValues is a long-time necessary feature in Java, and will likely be a strong and essential part of Java’s future.

Immutability, lazy loading, and memoization are themes not only in Java programming but also in programming in general. We are continuing to see this migration to a more immutable, lazy, and memoized Java with every new release. It is easier to program, maintain, and above all, easier to reason with.

I offer training and coaching services. Take a look at some Java training and coaching offerings by me at https://evolutionnext.com/trainings