

Lazy Constants (called Stable Values in Java 25) are values that can only be assigned once during the runtime of an application – though at any point in time – and remain constant thereafter. They standardize lazy initialization of constants and allow the JVM to optimize these constants in the same way as it can for final values.
In this article, you will learn:
- What are Lazy Constants, and how do you use them?
- What are the advantages of the immutability of a Lazy Constant?
- How have we implemented immutability so far, and what are the disadvantages of this?
- What are Lazy Lists and Lazy Maps?
- How do Lazy Constants work internally?
Lazy Constants are a preview feature that was released in Java 25 under the name Stable Values (JDK Enhancement Proposal 502) and was significantly simplified and renamed to Lazy Constants in Java 26 (JEP 526).
In the first few sections, I explain why we need Lazy Constants in the first place. If you can already guess, you may skip directly to the “The solution: Lazy Constants” section.
Why Immutability?
In the introduction, I explained that Lazy Constants are values that are only assigned once and then remain immutable. But what is the benefit of values being immutable? Immutability has several advantages:
1. An immutable object can be used by several threads without any problems.
There is no risk of race conditions, which, for mutable objects, we can only prevent through synchronization or memory barriers. Errors can easily creep in, even with experienced developers.
2. The JVM can optimize immutable objects, e.g. by constant folding.
For example, if the JVM recognizes that serviceRegistry.userService() is accessed in several places and it knows that serviceRegistry is constant and userService() returns a constant, then it can replace all calls to serviceRegistry.userService() with the userService constant.
3. Immutable objects make the code easier to read.
Code is more predictable, easier to understand, and easier to debug when you don’t have to worry about possible changes to object states. For mutable objects, we should create defensive copies for parameters and return values to ensure they are not inadvertently modified. This is not necessary for immutable objects.
Immutability with “final”
Until now, the only way to achieve immutability was to mark the fields of an object as final. Static final fields must be assigned during the declaration or in a static block and are initialized when the class is loaded. Final instance fields must be assigned during the declaration or in the constructor and are initialized when a new object of the class is created.
In the following example, a static Logger field is initialized:
public class UserService {
private static final Logger LOGGER = LoggerFactory.getLogger(UserService.class);
// . . .
}Code language: Java (java)Alternatively, with a static block:
public class UserService {
private static final Logger LOGGER;
static {
LOGGER = LoggerFactory.getLogger(UserService.class);
}
// . . .
}Code language: Java (java)In the following example, an unmodifiable UUID is generated for each new Task object:
public class Task {
private final UUID taskId = UUID.randomUUID();
// . . .
}Code language: Java (java)Alternatively, in the constructor:
public class Task {
private final UUID taskId;
public Task() {
taskId = UUID.randomUUID();
}
// . . .
}Code language: Java (java)The initialization of constants is not always that simple. I will show you some less trivial examples below.
Delayed a.k.a. Lazy Initialization
Final fields are initialized in any case, even if they are not used at all (or much later). However, if the initialization, e.g., the creation of the logger, takes a while (e.g., because it establishes a connection to an external logging system), but the logger is then never (or only later) used in the program flow, the start of the application may have been unnecessarily slowed down by the early initialization.
Fields that are expensive to initialize can be initialized with a delay, i.e. only when required (“lazy”). This is simple in a single-threaded application:
public class UserService {
private static Logger logger;
// Not thread-safe!!!
private static Logger getLogger() {
if (logger == null) {
logger = LoggerFactory.getLogger(UserService.class);
}
return logger;
}
// . . .
}Code language: Java (java)A second use case:
When creating an object, we do not always have all the information we need to initialize an immutable field. For example, a service could be created before a database connection has been established – but the service must access the database to initialize a field.
We can also initialize such a field lazily:
public class BusinessService {
private Settings settings;
// Not thread-safe!!!
private Settings getSettings() {
if (settings == null) {
settings = loadSettingsFromDatabase();
}
return settings;
}
// . . .
}Code language: Java (java)In a Spring or Jakarta EE application, we could also initialize the settings variable in a method annotated with @PostConstruct:
@Service
public class BusinessService {
private Settings settings;
@PostConstruct
private void initializeSettings() {
settings = loadSettingsFromDatabase();
}
// . . .
}Code language: Java (java)But these are all workarounds, and they have some significant disadvantages. You can find out what these are in the following section.
Disadvantages of “Homemade” Lazy Initialization
Looking at the examples from the previous section again, we notice that the logger and settings fields are no longer marked as final. This is only possible if they are initialized during the declaration, in a static block, or the constructor.
This, in turn, means that we cannot guarantee that the fields will not be modified after initialization. Without the guarantee that the values are immutable, the JVM cannot perform constant folding.
In addition, at least in the first two examples, we must ensure that we never access the fields directly but always via the getLogger() or getSettings() method.
And if we look at these methods again, we realize that they are not (yet) thread-safe! This means that they cannot be called from multiple threads.
To make the getSettings() method thread-safe, we could mark it with synchronized:
private synchronized Settings getSettings() {
if (settings == null) {
settings = loadSettingsFromDatabase();
}
return settings;
}Code language: Java (java)Although this makes it thread-safe, it also makes the application significantly slower, as the synchronized block must now be accessed every time the settings are accessed.
Double-checked locking is faster (but also more error-prone):
private volatile Settings settings; // ⟵ `settings` must be volatile!
private Settings getSettings() {
Settings localRef = settings;
if (localRef == null) {
synchronized (this) {
localRef = settings;
if (localRef == null) {
settings = localRef = loadSettingsFromDatabase();
}
}
}
return localRef;
}Code language: Java (java)You can find out why you should never forget volatile and the purpose of the additional (at first glance superfluous) variable localRef in the article on double-checked locking.
An alternative is the so-called initialization-on-demand holder idiom, which exploits the fact that the JVM loads classes lazily and thread-safe. But this is also a workaround. Not everyone knows it, and it only works with static fields, not with instance fields.
To summarize:
- Values initialized lazily cannot be marked as
final; immutability is therefore not guaranteed. - Accordingly, the JVM cannot optimize the code by constant folding.
- A lazily initialized value must always be accessed via a helper method.
- In multithreaded applications, this helper method must be thread-safe. Errors can easily creep in here, leading to subtle race conditions.
What we lack in Java is a middle ground between final and modifiable. A value that is initialized when it is needed. A value that is guaranteed to be initialized only once. And a value that is initialized correctly even if accessed from multiple threads.
And Lazy Constants are precisely this middle ground!
The Solution: Lazy Constants
A Lazy Constant is a container that holds an object, the so-called “content.” A Lazy Constant is initialized exactly once, before its content is accessed; after that, it is immutable. A Lazy Constant is thread-safe, i.e., if it is accessed from multiple threads, it will be initialized at most once. And the JVM can optimize a Lazy Constant through constant folding just as well as it can optimize a final field.
Below you can see how to implement the Settings example using a Lazy Constant.
public class BusinessService {
private final LazyConstant<Settings> settings =
LazyConstant.of(this::loadSettingsFromDatabase);
public Locale getLocale() {
return settings.get().getLocale(); // ⟵ Here we access the lazy constant
}
// . . .
}Code language: Java (java)The method passed to LazyConstant.of() – in this case, the method that loads the settings from the database – is called the computing function. On the first call to the get() method, the contents of the Lazy Constant are initialized exactly once by invoking this computing function.
By the way, you could also store the LazyConstant object returned by of() in a Supplier, since LazyConstant extends Supplier. However, LazyConstant offers two additional methods besides get():
isInitialized()– returns whether the value has already been initialized.orElse(T other)– returns the computed value if it has been initialized; otherwise, it returnsother.
Neither of these methods triggers initialization of the lazy constant.
Lazy Lists
We can not only define individual Lazy Constants but also a list of Lazy Constants, i.e. a list in which each individual element is lazily initialized when it is first accessed – e.g. with first(), get(int index) or last().
The following example creates a Lazy List in which each element is initialized with the square root of the list index the first time it is accessed:
List<Double> squareRoots = List.ofLazy(100, Math::sqrt);Code language: Java (java)The size of the list and its elements cannot be changed. The methods add(), set(), and remove() lead to an UnsupportedOperationException. Derived lists – e.g. with subList() or reversed() – are also Lazy Lists.
Here is a small demo program (I am using a simplified main method, which is available as a preview feature since Java 21 and as a production feature since Java 25.
void main() {
List<Double> squareRoots = List.ofLazy(100, i -> {
IO.println("Initializing list element at index " + i);
return Math.sqrt(i);
});
IO.println("squareRoots[0] = " + squareRoots.get(0));
IO.println("squareRoots[1] = " + squareRoots.get(1));
IO.println("squareRoots[2] = " + squareRoots.get(2));
IO.println("squareRoots[0] = " + squareRoots.get(0));
IO.println("squareRoots.first = " + squareRoots.getFirst());
IO.println("squareRoots.last = " + squareRoots.getLast());
}Code language: Java (java)The program prints the following:
Initializing list element at index 0
squareRoots[0] = 0.0
Initializing list element at index 1
squareRoots[1] = 1.0
Initializing list element at index 2
squareRoots[2] = 1.4142135623730951
squareRoots[0] = 0.0
squareRoots.first = 0.0
Initializing list element at index 99
squareRoots.last = 9.9498743710662Code language: plaintext (plaintext)You can see that the Lazy List only computes the element at position 0 once, although it is retrieved three times (twice with get(0) and once with getFirst()).
Lazy Map
Analogous to Lazy Lists, we can also create Lazy Maps. With a Lazy Map, for each key, the associated value is only initialized the first time it is looked up.
The following example shows a Lazy Map with which we can dynamically load localization resources per language when first accessed:
Set<Locale> supportedLocales = getSupportedLocales();
Map<Locale, ResourceBundle> resourceBundles =
Map.ofLazy(supportedLocales, this::loadResourceBundle);Code language: Java (java)The corresponding resource bundle is loaded via the loadResourceBundle(...) method passed as a method reference only when resourceBundles.get(...) is called for the first time.
How Do Lazy Constants Work Internally?
Lazy Constants are implemented exclusively in Java code. Changes to the compiler, bytecode, or JVM were not necessary, as you can see from the pull request for JEP 502.
The content of a Lazy Constants is stored in a non-final field. This field is annotated with the JDK-internal @Stable annotation, which is also used in other parts of the JDK code for optimization. This annotation indicates to the JVM that the value will not change after initialization. And so, once the value has been set, the JVM can start with its constant folding optimization.
Thread security is ensured by memory barriers set via the Unsafe class.
Does this mean that LazyConstant is just a wrapper we could implement ourselves?
Yes, but...
Firstly, we cannot use the JDK-internal @Stable annotation or the internal Unsafe class without explicitly making them available to our module via --add-exports java.base/jdk.internal.vm.annotation or --add-exports java.base/jdk.internal.misc.
Secondly, we should not use these JDK internals, as there is no guarantee that they will not change in a future Java release.
And thirdly, we do not write a ConcurrentHashMap ourselves either, for example. Since LazyConstant is implemented by JDK specialists, we can be sure that all known performance tricks have been applied and that further performance optimizations will be made in the future. And if, in the future, LazyConstant is used by millions of Java developers, we can also be sure that any bugs – even subtle concurrency bugs – will be found and fixed quickly.
Conclusion
Lazy Constants are constants that can be initialized “on demand” at any time. They are then immutable and are treated by the JVM exactly like final fields, e.g., optimized by constant folding.
Lazy Constants are thread-safe, so they can also be used in multithreaded programs without risking subtle concurrency bugs.
In addition to Lazy Constants, there are Lazy Lists and Lazy Maps, which initialize the elements in lists and maps only once and then store them immutably.
Lazy Constants are included as a preview feature in Java 25 (still under the name “Stable Values” there) and in the current early-access build of Java 26.
What do you think of Lazy Constants? Share your opinion in the comments!