Lazy Constants in Java 27 (JEP 531): A Beginner’s Guide

  • Last Updated: October 16, 2026
  • By: javahandson
  • Series
img

Lazy Constants in Java 27 (JEP 531): A Beginner’s Guide

Lazy Constants in Java 27 (JEP 531) let a field stay final yet load lazily. Learn LazyConstant.of(), get(), lazy lists, maps and sets with easy examples.

1. Introduction

Most Java fields sit in one of two camps. Some are final and set once, right at the start. Others stay mutable, so you can fill them in later. For years, we had to pick a side and live with the trade-off. Lazy Constants in Java 27, shipped as a preview through JEP 531, finally let us have both.

In this guide, we will learn what lazy constants are and why they matter. We will begin from the old patterns you already know. Then we move to the new LazyConstant type, one small step at a time.

By the end of this guide, you will understand:

  • the problem with final fields and old-style lazy initialization
  • what a lazy constant is, in plain words
  • how to create and read one with LazyConstant.of() and get()
  • lazy lists, lazy maps and lazy sets
  • real use cases like faster startup and simple caching
  • how this feature grew from Stable Values into JEP 531

One quick note before we dive in. JEP 531 is a preview feature in Java 27, which Oracle released on 15 September 2026. Preview means the design is complete, but the API can still change. So feel free to explore it, yet do not ship it to production just yet. Let us get started.

2. The Problem Lazy Constants Solve

Before the new API, let me show you the pain. Once you feel the problem, the fix makes a lot more sense. We will look at three old approaches and why each one hurts.

2.1 Eager Initialization With final

A final field is the gold standard for safety. You set it once, and the JVM trusts it fully. Thanks to that trust, the JIT compiler can optimize any code that reads it.

public class Locations {
    private final Logger logger =
        Logger.getLogger(Locations.class.getName());
 
    public void printLocations() {
        logger.info("Printing locations...");
    }
}

There is one catch here. You must set a final field when you build the object. We call this eager initialization. So even if you never call printLocations(), the logger still gets created. For a cheap object, that waste hardly matters. For a heavy one, you pay a cost you may never use.

2.2 The Old Lazy Way: null Check Plus synchronized

To delay the work, many developers reach for lazy initialization. You keep the field mutable, then build the value on first use. The classic pattern looks like this.

public class Customers {
    private Logger logger = null;
 
    synchronized Logger getLogger() {
        if (logger == null) {
            logger = Logger.getLogger(Customers.class.getName());
        }
        return logger;
    }
}

This works, but it brings three headaches:

  • You must always read the field through getLogger(). Touch the raw field too early, and you get a NullPointerException.
  • The synchronized keyword blocks every thread on each call. That hurts throughput when load is high.
  • The field is no longer final, so the JIT compiler cannot treat it as a constant. You lose those optimizations.

Drop the synchronized keyword, and a fresh bug appears. Two threads can race and build two loggers. In short, you just trade one problem for another.

2.3 The Holder-Class Idiom

Seasoned developers know a neat trick: the lazy holder idiom. You hide the value inside a static nested class. The JVM loads that class only on first access, which gives thread-safe laziness for free.

public class Config {
    private Config() {}
 
    private static class Holder {
        static final Config INSTANCE = new Config();
    }
 
    public static Config getInstance() {
        return Holder.INSTANCE;
    }
}

It is clever, but it is also boilerplate. You need a whole extra class for one lazy value. It also works only for static fields, not instance fields. In other words, every option so far asks you to give something up.

💡 Interview Insight
Interviewers often ask why a final field cannot be lazy. The reason is simple. You must assign a final field during construction or in a static initializer. After that, the JVM forbids any change, so there is no room to defer the work. Lazy constants break this deadlock, because they split when you declare the value from when you compute it.

3. What Is a Lazy Constant?

Now we reach the good part. A lazy constant gives you the laziness of the second approach and the speed of the first. It is a small idea with a big payoff.

3.1 The Core Idea

A lazy constant is an object of type LazyConstant<T>. It holds a single value, which is called its content. You hand it a recipe for that value up front. However, it does not run the recipe until you ask for the value the first time.

Think of it as a value that is eventually final. The content starts out empty. Once computed, it never changes again. Better still, the JVM treats it like a true constant, so you keep the constant-folding optimizations.

Why does that matter so much? A normal mutable field could change at any time, so the compiler plays it safe and reloads it from memory on each read. A lazy constant makes a promise: set once, then frozen. With that promise, the JIT can bake the value straight into the machine code. So you get late initialization without losing the speed of a constant.

3.2 A First Example

Here is the logger example again, rewritten with a lazy constant. Notice how little code it takes.

public class Orders {
    private final LazyConstant<Logger> logger =
        LazyConstant.of(
            () -> Logger.getLogger(Orders.class.getName())
        );
 
    public void printOrders() {
        logger.get().info("Printing orders...");
    }
}

Look how clean that is. The field stays final, so the JVM trusts it. The supplier () -> Logger.getLogger(…) is just the recipe. Nothing runs yet. The logger is born only when logger.get() first runs inside printOrders().

3.3 Why This Is Special

See what we got back in one move:

  • The field stays final, so the compiler can still optimize reads.
  • The value is lazy, built only on the first get() call.
  • There is no manual null check, no synchronized block and no holder class.

So lazy constants simply remove the trade-off. You no longer choose between safe and lazy. Instead, you get both from one tiny object.

Diagram of the Java 27 lazy constant lifecycle: declared empty, first get() runs the supplier and caches the value, later get() calls return the cached immutable value

4. Getting Started in Java 27

Lazy constants live inside the JDK, so there is no library to add. You only need two things: Java 27 and the preview flag.

4.1 Turn On Preview Features

Because JEP 531 is a preview, you must switch it on. Pass these flags when you compile and when you run.

# Compile
javac --release 27 --enable-preview Orders.java
 
# Run
java --enable-preview Orders

Using Maven? Set the same flags in the compiler plugin. On Gradle, add them to your compileJava options. Without these flags, the code simply will not compile.

4.2 The Import

The LazyConstant type lives in java.lang, so you often need no import at all. Your IDE may still add one, which is fine. The lazy collections live in java.util, and you reach them through List, Set and Map.

5. The LazyConstant API

The API is tiny on purpose. The designers wanted something you can learn in minutes, not hours. Let me walk you through each piece.

5.1 Creating One: LazyConstant.of()

You build a lazy constant with the static factory method LazyConstant.of(Supplier).

LazyConstant<Logger> logger =
    LazyConstant.of(() -> Logger.getLogger("orders"));

The Supplier you pass is the computing function. It describes how to make the value. The method returns right away, and the supplier does not run yet.

5.2 Reading It: get()

To read the value, you call get().

Logger log = logger.get();    // first call: runs the supplier
log.info("hello");
Logger same = logger.get();   // later calls: returns cached value

The first get() runs the supplier and stores the result. Every later get() returns that same stored value. The supplier never runs a second time.

One small thing to note. The content can never be null. If your supplier returns null, the constant enters an error state, and get() then throws a NoSuchElementException. So wrap an optional result in an Optional instead. That keeps the at-most-once rule clean and predictable.

5.3 Thread Safety Comes Built In

Here is a feature you get for free. Lazy constants are thread-safe. The content is set at most once, even when many threads race for it.

Say two threads call get() at the very same moment. Only one of them wins. That winning thread runs the supplier. The others simply wait, then receive the same result. You write zero locking code for any of this.

5.4 Watch Out for Circular Dependencies

One rule can trip you up. A lazy constant must not depend on itself, whether directly or through a chain. When it does, Java throws an IllegalStateException.

static class A {
    static final LazyConstant<B> b = LazyConstant.of(B::new);
    A() { b.get(); }    // triggers B
}
 
static class B {
    static final LazyConstant<A> a = LazyConstant.of(A::new);
    B() { a.get(); }    // triggers A again -> boom
}

Here A needs B, and B needs A. So the computing function calls itself in a loop. The JVM stops this with the message about a recursive invocation of the computing function. This guard protects you from a silent deadlock.

💡 Interview Insight
A sharp interviewer may ask how lazy constants stay thread-safe without any synchronized keyword in your code. The JVM handles the locking inside, and it writes the content only once. It also publishes the value safely, so other threads see a fully built object. This is the same safe-publication promise you already get from a final field.

6. Lazy Collections

A single lazy value is handy. A whole collection of lazy values is even better. JEP 531 adds three lazy collection factories, one for each core type.

6.1 Lazy Lists

A lazy list is an unmodifiable list backed by lazy constants. You create it with List.ofLazy(size, intFunction). Java computes each element on first access, using its index as the input.

static final List<Orders> POOL =
    List.ofLazy(20, index -> new Orders());
 
Orders o = POOL.get(5);       // builds element 5 now
Orders again = POOL.get(5);   // returns the cached one

The list above can hold 20 Orders objects. None of them exist yet. The first get(5) builds that slot. Later reads of index 5 return the same object. This fits an object pool that you fill on demand.

6.2 Lazy Maps

A lazy map works the same way, but with keys instead of indexes. You give it the full set of allowed keys and a function. Java computes a key’s value the first time you ask for it.

static final Set<Integer> KEYS = Set.of(1, 2, 4, 8, 16, 32);
 
static final Map<Integer, Integer> LOG2 =
    Map.ofLazy(KEYS, i -> 31 - Integer.numberOfLeadingZeros(i));
 
int x = LOG2.get(16);   // computes 4 on first call

You must list every key up front. A key outside the set has no value. This turns the map into a fixed, lazy lookup table. It suits caches where the keys are known but the values are costly to build.

6.3 Lazy Sets

A lazy set decides membership on demand. You pass the candidate elements and a Predicate. The predicate runs at most once per candidate to check if it belongs.

enum Option { VERBOSE, DRY_RUN, STRICT }
 
static final Set<Option> ENABLED =
    Set.ofLazy(EnumSet.allOf(Option.class), MyApp::isEnabled);
 
boolean on = ENABLED.contains(Option.DRY_RUN);  // checks once

This fits expensive checks, like reading a config file or hitting a database. The predicate tests each option only when you first ask about it. After that, the set caches the answer for good.

💡 Interview Insight
A common question is how lazy collections differ from a plain HashMap used as a cache. The lazy map stays fixed in shape, thread-safe and eligible for constant folding. A plain HashMap cache needs your own locking, and it gives up the JIT constant optimizations. So lazy collections are usually safer and faster for fixed, compute-once data.

7. Real-World Use Cases

Theory is nice, but where does this help day to day? Let me share a few spots where lazy constants really shine.

7.1 Expensive Singletons

Some objects are costly to build: a database client, a big parser, a heavy config object. You do not want to create them at startup if a request may never need them. Wrap each one in a lazy constant, and the cost moves to first use. Nothing happens until something actually asks for it.

7.2 Faster Application Startup

This is the headline goal of JEP 531. Large apps often build every component up front. As a result, startup gets slow and heavy.

With lazy constants, you store each component in its own holder. Then the app starts fast, because nothing heavy runs yet. Each part wakes up only when first used. You can even compose them, where one lazy constant depends on another.

static final LazyConstant<Customers> CUSTOMERS =
    LazyConstant.of(Customers::new);
 
static final LazyConstant<Orders> ORDERS =
    LazyConstant.of(Orders::new);

7.3 Caching and Memoization

A lazy constant is a tiny, thread-safe cache for one value. A lazy map is a cache for many values. Reach for them when a result is costly but never changes. A parsed template or a compiled pattern is a perfect fit.

7.4 Configuration and Feature Flags

Feature flags often need a lookup that is slow the first time. A lazy set or a lazy map reads each flag once, then caches the answer. So your hot path stays quick after that first check.

7.5 Retiring Double-Checked Locking

You may have seen the double-checked locking pattern before. It uses a volatile field, two null checks and a synchronized block to build a value lazily and safely. The pattern is famous for being easy to get wrong.

A lazy constant replaces all of that with one line. The JVM already handles the volatile read, the locking and the safe publishing for you. So you delete a tricky pattern and keep the same guarantees. For most code, this alone is reason enough to switch.

8. Lazy Constants vs the Alternatives

Let me put the options side by side. This table sums up the whole story at a glance.

ApproachLazy?Immutable?Thread-safe?Constant folding?
final fieldNo (eager)YesYesYes
Mutable + synchronizedYesNoYes, if coded wellNo
Holder idiomYesYesYesYes, static only
Lazy constantYesYesYesYes

The last row wins on every count. That single line is the promise of lazy constants in one picture.

9. The Road to JEP 531

This feature did not appear overnight. It has a short but busy history. Knowing it helps you read older tutorials without getting confused.

9.1 From Stable Values to Lazy Constants

The idea first landed in Java 25 as Stable Values (JEP 502). That name confused people, so the team renamed it. In Java 26, it became Lazy Constants with a second preview (JEP 526). Java 27 now brings the third preview through JEP 531.

Here is the timeline at a glance:

  • Java 25 – JEP 502, Stable Values (first preview)
  • Java 26 – JEP 526, Lazy Constants (second preview, renamed)
  • Java 27 – JEP 531, Lazy Constants (third preview)
  • Java 28 – a fourth preview is already drafted

9.2 What Changed in the Third Preview

Each preview trims the API toward something simpler. The third preview keeps that trend going.

  • The team removed a few methods, such as isInitialized() and orElse(), from LazyConstant, because they did not fit the design goals.
  • The team also rounded out the lazy-collection factories, so List, Set and Map all gained an ofLazy form.

The result is a smaller, cleaner API. If you followed an older guide, double-check the method names. Some of them changed between previews.

💡 Interview Insight
Expect a question like, is this production-ready? Be honest in your answer. JEP 531 is still a preview in Java 27. The design is complete, but the API may shift once more. There is even a fourth preview planned for Java 28. So use it to learn and prototype, not for critical production code yet.

10. Best Practices and Common Mistakes

A few habits will save you real debugging time. Here is what I would keep in mind.

10.1 Best Practices

  • Prefer a lazy constant over a hand-written null check. It is shorter and far safer.
  • Keep the supplier free of side effects. It may run on any thread, so avoid surprises.
  • Store heavy components in lazy constants to speed up startup.
  • Declare the holder field as final. That is where the constant-folding magic comes from.

10.2 Common Mistakes

  • Forgetting –enable-preview. The code will not compile without it.
  • Creating a circular dependency between two lazy constants. That throws an IllegalStateException.
  • Putting slow or risky work in the supplier that you do not want on a random thread.
  • Shipping preview code to production, since the API can still change.
  • Expecting to re-set the value. A lazy constant is set once and never again.

10.3 When a Lazy Constant Is Not the Answer

Lazy constants are not a hammer for every nail. Skip them when the value is cheap to build, since eager final is simpler there. Skip them too when the value must change over time, because a lazy constant is write-once. For data that updates, a normal field or a proper cache library fits better. Pick the tool that matches the job.

11. FAQ’s on Lazy Constants in Java 27

Q: What are Lazy Constants in Java 27?

A: A lazy constant is an object of type LazyConstant that holds one value, computed on first use. You give it a supplier up front, and the value is built only when you first call get(). It then stays immutable, and the JVM treats it like a true constant, so you keep constant-folding optimizations.

Q: Is JEP 531 production-ready in Java 27?

A: No. JEP 531 is a preview feature in Java 27. The design is complete, but the API may still change. A fourth preview is already planned for Java 28. So use lazy constants to learn and prototype, not for critical production code yet.

Q: How are lazy constants different from a final field?

A: A final field must be set when the object is built, which is eager. A lazy constant keeps the final-field speed and safety, but it defers the work until the first get() call. So you get both lazy initialization and constant-level optimization in one small object.

Q: How do I enable Lazy Constants in Java 27?

A: Because it is a preview, pass the preview flags. Compile with javac –release 27 –enable-preview, and run with java –enable-preview. In Maven or Gradle, add the same flags to the compiler options. Without them, the code will not compile.

Q: Can a lazy constant hold a null value?

A: No. The content can never be null. If your supplier returns null, the constant enters an error state and get() throws a NoSuchElementException. If you need a nullable result, wrap it in an Optional instead.

Q: What are lazy lists, maps, and sets?

A: They are unmodifiable collections backed by lazy constants. List.ofLazy builds each element on first access by index, Map.ofLazy computes a key’s value on first lookup, and Set.ofLazy checks membership on demand. Each value is computed at most once, then cached.

12. Conclusion

Lazy constants close an old gap in Java. For years, we had to choose between final safety and lazy flexibility. Now we get both from one small, clean type.

Here is a quick recap of the steps:

  • A lazy constant holds one value, computed on the first get() call.
  • It is final-friendly, immutable and thread-safe.
  • List.ofLazy, Map.ofLazy and Set.ofLazy extend the idea to collections.
  • The feature speeds up startup and makes caching simple.
  • It is a preview in Java 27, so learn it now and watch for changes.

Start small with your next change. Replace one clunky lazy field with a LazyConstant. After that, you will likely never go back to the old pattern.

13. Further Reading

Leave a Comment