Stable Values
By: Daniel Hinojosa
Published on: November 13, 2025
Stable Values
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
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.
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.
supplier methodjshell> 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.
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.
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.
Here is a code example to demonstrate
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)
-
Initializing some colors in a
Stringarray -
Constraining the
sizeof the list -
Creating a value object to represent the
Ball -
Creating the
StableValue.list, which will load the ball lazily -
Create
5Threads 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 Planetsenum Planet {
MERCURY,
VENUS,
EARTH,
MARS,
JUPITER,
SATURN,
URANUS,
NEPTUNE;
}
Next, let’s create a basic value object that will represent details about the planets.
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.
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.
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.
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.
StableValue.MapUsing 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.
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)
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.
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.
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.
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.
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
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