Lazy Constants in Java 27 (JEP 531): A Beginner’s Guide
-
Last Updated: October 16, 2026
-
By: javahandson
-
Series

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.
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:
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.
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.
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.
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:
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.
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. |
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.
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.
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().
See what we got back in one move:
So lazy constants simply remove the trade-off. You no longer choose between safe and lazy. Instead, you get both from one tiny object.

Lazy constants live inside the JDK, so there is no library to add. You only need two things: Java 27 and the preview flag.
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.
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.
The API is tiny on purpose. The designers wanted something you can learn in minutes, not hours. Let me walk you through each piece.
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.
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 valueThe 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.
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.
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. |
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.
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 oneThe 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.
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 callYou 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.
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 onceThis 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. |
Theory is nice, but where does this help day to day? Let me share a few spots where lazy constants really shine.
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.
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);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.
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.
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.
Let me put the options side by side. This table sums up the whole story at a glance.
| Approach | Lazy? | Immutable? | Thread-safe? | Constant folding? |
|---|---|---|---|---|
| final field | No (eager) | Yes | Yes | Yes |
| Mutable + synchronized | Yes | No | Yes, if coded well | No |
| Holder idiom | Yes | Yes | Yes | Yes, static only |
| Lazy constant | Yes | Yes | Yes | Yes |
The last row wins on every count. That single line is the promise of lazy constants in one picture.
This feature did not appear overnight. It has a short but busy history. Knowing it helps you read older tutorials without getting confused.
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:
Each preview trims the API toward something simpler. The third preview keeps that trend going.
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. |
A few habits will save you real debugging time. Here is what I would keep in mind.
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.
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.
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.
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.
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.
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.
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.
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:
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.