Contents

Contents

JEP 531 - Lazy Constants, Journey continues

JEP 531 - Lazy Constants, Journey continues featured image

In Java 25, a new feature called Stable Values was introduced. Today, we will take a look at JEP-531, the third preview of that feature, shipping in Java 27. It is still a preview feature, which means everything can be changed.

In JEP-526 Stable Values become Lazy Constants. Personally, I think that is way more suitable for that functionality.

This article was updated on 15 Sept 2026.

What are Lazy Constants

A LazyConstant <T> is a container that holds a single value of type T. Once assigned, that value becomes immutable. You can think of it as an eventually final value. It’s vital to notice that a reference to an object will be immutable. The object beneath can be changed.

Before Lazy Constants

Before Java 25, to achieve immutable data, we had to use the final keyword.

class Controller {
 private final EmailSender sender = new EmailSender();
}

There were some problems with this approach.

Whenever our code has a final field, it must be set eagerly, either initialized by a constructor or by having a static field. As a result, the application's startup may suffer. Not all fields are immediately needed, right?

So maybe we could remove the final keyword and do something like that:


class PetClinicController {

 private EmailSender sender = null;

 EmailSender getSender() {
   if (sender == null) {
     sender = new EmailSender();
   }
   return sender;
 }

 void adoptPet(User user, Pet pet) {
   // some logic here
   getSender().sendEmailTo(user, "You are great person!");
 }
}

It’s going to work, right? The startup will be faster. And yes, that's one way to solve this issue before Java 25. Unfortunately, it's not free.

  • Our sender is still mutable; we can assign a different value to this variable. However, we would need to use external tools to prevent us from doing that, or be very precise during code reviews.
  • Potential null pointer exception while accessing a field from another way than just using a getter.
  • It would be good to have “getSender” thread-safe.
  • But even when we do so, we prevent the JVM from optimizing access for this field. For example, using constant-folding.

Let's use some Lazy Constants

Using our example from before, let's migrate to the new Java.


class PetClinicController {

 private final LazyConstant<EmailSender> sender = LazyConstant.of(() -> new EmailSender());

 EmailSender getSender() {
   return sender.get();
 }

 void adoptPet(User user, Pet pet) {
   // some logic here
   getSender().sendEmailTo(user, "You are great person!");
 }
}

The code is very similar, but LazyConstant handles the nullability of EmailSender. Thanks to that, it’s impossible to call EmailSender without first calling a method to retrieve the value.

The value inside a LazyConstant is guaranteed to be set in a thread-safe way.

What has changed in Java 27?

Java 26 already made LazyConstant significantly smaller than Stable Values had been. Java 27 continues to adapt API.

Error handling

Java 27 changes how LazyConstant behaves when the computing function fails.

In Java 26, if the supplier threw an exception, that exception was propagated to the caller and the LazyConstant remained uninitialized. This meant that a subsequent call to get() could try to compute the value again.

var count = new AtomicInteger();
var value = LazyConstant.of(() -> {
   if (count.getAndIncrement() == 0) {
       throw new RuntimeException("Boom!");
   }
   return "Hello!";
});

Java 26

value.get(); // throws NoSuchElementException
value.get(); // "HELLO!" - computation is retried

Java 27

value.get(); // throws NoSuchElementException
value.get(); // throws NoSuchElementException, supplier is not called again

The low-level methods isInitialized and orElse are gone

in Java 27, both orElse and isInitialized were removed because they could be used in ways inconsistent with the design goals. With Java 26 value was not guaranteed to be there by the time you read it, when you did not invoke get() method.

In practice, this change simplifies the mental model: you always call get(), and you always get the value - unless error will be thrown, like I mentioned before.

LazyConstant<String> example = LazyConstant.of(() -> "Hello");
var result = example.get();
System.out.println(result);

Our result here will be "Hello" with no ”else” handling.

Lazy Collections

We can also use unmodifiable collections together with stable values.
Here is a split between versions.

Java 25

List<Integer> list = StableValue.list(SIZE, INT_FUNCTION);
Map<Color, HowCute> map = StableValue.map(KEYS, CUTE_FUNCTION);

Java 26

List<Integer> list = List.ofLazy(SIZE,INT_FUNCTION);
Map<Color, HowCute> map = Map.ofLazy(KEYS, CUTE_FUNCTION);

Java 27

List<Integer> list = List.ofLazy(SIZE,INT_FUNCTION);
Map<Color, HowCute> map = Map.ofLazy(KEYS, CUTE_FUNCTION);
Set<Dog> set = Set.ofLazy(ALL_DOGS, IS_ADOPTED_FUNCTION);

Feels useful for complex computations. Although it is limited, as a list can only use intFunction for input. Hope to see more in the future.

Potential issues

LazyConstants are simple and useful constructs. Yet it’s worth noting the potential pitfalls of using them.

Serializable

It’s worth noticing that Lazy Constants do not support Serializable. It’s not mentioned in JEP, but it’s possible to find it here.

So, if you use serializable and want to replace all finals with Stable Values, you may encounter problems.

Not Using Final

Whenever we use LazyConstant, we need to remember the possibility of assigning a different value to this variable. Nothing prevents us from doing something like that (nothing but common sense, of course):

private LazyConstant<EmailSender> sender = LazyConstant.of(() -> new EmailSender());

void adoptPet(User user, Pet pet) {
 sender = null;
    // some logic here
 sender.get() //Null pointer exception
}

You should use final with all your LazyConstant, but you are not forced to do so. It’s a case of Optional<> once again.

So why didn’t they introduce a new keyword? Something like lazy, just like we have in different languages? Let's look into their motivation.

What is the goal of this JEP?

From this JEP, we learn that the author's goal was to improve the startup of Java applications by decoupling the creation of lazy constants from initialization. Making sure it will work in a multi-threaded environment and give us the possibility to use JVM optimizations like constant-folding.

Their goal is not to:

  • It is not a goal to enhance the Java programming language with a means to declare lazy fields.
  • It is not a goal to alter the semantics of final fields.

So even if I personally would love to have a lazy keyword, I’m glad that they introduced something that will help us optimize code. Can’t wait for the following updates.

Reviewed by: Szymon Winiarz

Blog Comments powered by Disqus.