Skip to main content

JEP 531: Lazy Constants (Third Preview)

Introduction

Trading off performance against elegance is a daily reality for every Java developer. On the specific question of deferred initialization, that trade-off is especially glaring: you either choose the fast static final, which must be initialized eagerly, or you choose flexible lazy-loading idioms, which are clumsy and error-prone. The lazy constants proposed by JEP 531 exist to end that either-or dilemma.

The problem with deferred initialization

Java developers have long faced a dilemma.

On one hand, static final fields are extremely fast. Because their value never changes once assigned, the JVM can treat them as genuine constants and apply aggressive optimizations, constant folding, inlining, elimination of redundant reads, and so on. It is one of the sharpest tools in Java's performance model.

But it comes with a hard requirement: it must be initialized eagerly, at class-loading time. The moment the class is loaded, the field has to compute its value.

And here is the problem: what if that initialization is expensive? Compiling a complex regex, reading a large file, or establishing a costly connection. You do not want to pay that cost the instant the class loads; you want it deferred to the first time the value is actually used.

So you are forced to reach for one of the deferred-initialization idioms:

  • The holder idiom: a separate private static nested class per lazy value, exploiting the JVM's "initialize a class only on first use" semantics to achieve laziness.
  • Double-checked locking: hand-written, thread-safe lazy loading with a carefully placed volatile.
  • Supplier wrapping: hiding the value behind a function that computes it on first call.

These idioms share two flaws: they are easy to get wrong (correct double-checked locking is famously subtle), and they block the very constant-like optimization you want, because to the JVM the value behind an ordinary field or a Supplier could change at any time and cannot be treated as constant.

What a lazy constant is

The core idea of a lazy constant is genuinely simple: a holder that is set at most once and then never changes.

  • It starts empty when created.
  • Its value is computed on first access.
  • The whole initialization process is thread-safe and naturally free of the double-checked-locking pitfalls.
  • Most importantly: the JVM knows this value will not change once set, so it can treat the initialized content as trusted and apply the same aggressive optimizations it uses for final fields.

In other words, a lazy constant packages "deferral" and "constant optimization", two properties that used to be mutually exclusive, into a single concept for the first time.

Usage: the LazyConstant API

JEP 531 provides the preview API LazyConstant. Create one with LazyConstant.of(...), and put the initialization logic next to the field declaration:

class Config {
// computed on first access, then constant
private static final LazyConstant<Pattern> PATTERN =
LazyConstant.of(() -> Pattern.compile("complex-regex"));

// instance lazy field
private final LazyConstant<Logger> logger =
LazyConstant.of(() -> Logger.getLogger(getClass().getName()));
}

Notice there is no nested class, no volatile, no synchronized block. The initialization logic sits right beside the field declaration, so you can see at a glance what the field is and how it is computed. The compiler and JVM handle all the thread-safety and optimization details for you.

Before and after

Before, the holder idiom:

  • Each lazy value needs a private static nested class, or a hand-written synchronized / volatile double-checked lock.
  • This code is correct but verbose, and easy to break in subtle ways.
  • Worse, it fundamentally blocks constant-like optimization.

After, the lazy constant:

  • One field holds the lazy value.
  • The initialization logic sits next to the declaration and reads clearly.
  • It gets optimization opportunities close to static final.

Less code, fewer chances to get it wrong.

Where it pays off

Lazy constants shine for objects that are expensive to create but immutable once built:

  • Expensive singletons: caches, parsers, precompiled regex patterns. Costly to construct, but read-only once built.
  • Configuration read once and then treated as immutable.
  • Faster application startup. Because that expensive initialization work moves off the class-initialization critical path and is only paid for when the value is actually needed, cold-start time drops.
  • A stepping stone toward Leyden-style ahead-of-time initialization. The semantics of a lazy constant (assigned once, trusted thereafter) pave the way for Project Leyden's ahead-of-time initialization work.

Preview status

Keep in mind this is still a preview feature, and it is already the third preview.

  • It ships in JDK 27 as a third preview, and the API may still change based on community feedback.
  • You need the --enable-preview flag at both compile and run time.
  • If you followed the earlier "stable values" previews, this iteration is a refinement built on that foundation, incorporating the lessons from that feedback.

Conclusion

With lazy constants you no longer have to make the painful choice between "fast but eager" and "lazy but slow." Initialize on first use, get constant-like optimization afterward, with the LazyConstant API and built-in thread safety.