Stable Values in Java (Preview)
On the first day of school, each student is given a roll number. It isn't decided before the student joins, but once it is written in the register it never changes. A final field in Java is like a roll number decided before the school even opens: it must be set in the constructor, right away. Sometimes you want the roll-number kind instead: set later, only when first needed, and then fixed for good. A stable value gives you exactly that. In this lesson, you will learn what a StableValue is, how to use it, and why it is still a preview feature in Java 25.
What Is a Stable Value?
A stable value in Java is an object of type java.lang.StableValue<T> that is used to hold a value which is set at most once. Before it is set, it is empty. After it is set, it can never change, and the JVM can treat it as a constant and optimize code that reads it, just as it does for final fields.
It combines the good parts of two older choices:
| Choice | Set when? | Can change? | Fast reads? |
|---|---|---|---|
final field | In the constructor or static initializer, always | No | Yes |
| Normal field with lazy setup | Later, when first needed | Yes, by mistake too | Not treated as constant |
StableValue | Later, when first needed | No, at most once | Yes, once set |
Running Preview Code
Without the flag, the compiler stops:
Config.java:2: error: StableValue is a preview API and is disabled by default.
private static final StableValue<String> SETTINGS = StableValue.of();
^
(use --enable-preview to enable preview APIs)With the source launcher, add the flag before the file name: java --enable-preview Config.java. With javac, also give the release: javac --release 25 --enable-preview Config.java.
Setting a Value Lazily
The most common method is orElseSet: if the value is already set, it returns it; if not, it runs your code, stores the result, and returns it. Let's see an example that loads settings only when they are first used.
public class Config {
private static final StableValue<String> SETTINGS = StableValue.of();
static String settings() {
return SETTINGS.orElseSet(() -> {
System.out.println("Loading settings from disk...");
return "termFee=15000, lateFine=500";
});
}
public static void main(String[] args) {
System.out.println("Program started");
System.out.println(settings());
System.out.println(settings());
System.out.println(settings());
}
}Program started
Loading settings from disk...
termFee=15000, lateFine=500
termFee=15000, lateFine=500
termFee=15000, lateFine=500Explanation:
StableValue.of()creates an empty stable value. The field itself isfinal, but its content is filled in later.- "Program started" prints first, so the settings are not loaded at startup.
- The first call to
settings()runs the lambda, which prints "Loading settings..." and stores the result. - Every later call returns the stored value. The loading code runs only once.
Methods
| Method | Description |
|---|---|
StableValue.of() | It is used to create an empty stable value. |
orElseSet(Supplier) | It returns the value, computing and storing it first if it is empty. |
trySet(value) | It sets the value if empty and returns true; returns false if it was already set. |
setOrThrow(value) | It sets the value, or throws IllegalStateException if it was already set. |
orElseThrow() | It returns the value, or throws NoSuchElementException if it is empty. |
orElse(other) | It returns the value, or other if it is empty. |
isSet() | It returns true if the value has been set. |
Let's see the "at most once" rule in action.
public class RollNumber {
public static void main(String[] args) {
StableValue<String> roll = StableValue.of();
System.out.println("Set yet? " + roll.isSet());
System.out.println("First try: " + roll.trySet("10A-07"));
System.out.println("Second try: " + roll.trySet("10A-99"));
System.out.println("Roll number: " + roll.orElseThrow());
try {
roll.setOrThrow("10B-01");
} catch (IllegalStateException e) {
System.out.println("Refused: " + e.getMessage());
}
}
}Set yet? false
First try: true
Second try: false
Roll number: 10A-07
Refused: The contents is already setExplanation: the first trySet succeeds, and the second is ignored and returns false. setOrThrow is the strict version: it throws instead of returning false. Once set, the roll number stays 10A-07.
Thread Safety
If several threads call orElseSet at the same moment on an empty stable value, only one of them runs the supplier. The others wait and then receive the same result. You don't need synchronized, volatile or the double-checked locking pattern you saw in the Singleton lesson.
// Before: double-checked locking, easy to get wrong
private static volatile Report instance;
static Report get() {
if (instance == null) {
synchronized (Report.class) {
if (instance == null) instance = new Report();
}
}
return instance;
}
// After: one line, thread-safe, and treated as a constant once set
private static final StableValue<Report> INSTANCE = StableValue.of();
static Report get() { return INSTANCE.orElseSet(Report::new); }Stable Suppliers, Lists and Maps
Factory methods wrap the pattern into familiar types:
| Factory | Description |
|---|---|
StableValue.supplier(Supplier) | It returns a Supplier that computes its value on the first get() and caches it. |
StableValue.list(size, IntFunction) | It returns an unmodifiable List whose elements are each computed on first access. |
StableValue.map(Set, Function) | It returns an unmodifiable Map whose values are each computed on first access, for a fixed set of keys. |
Let's see a stable list that computes each element only when it is asked for.
import java.util.List;
public class Squares {
public static void main(String[] args) {
List<Integer> squares = StableValue.list(5, i -> {
System.out.println("Computing square of " + i);
return i * i;
});
System.out.println("Size: " + squares.size());
System.out.println(squares.get(3));
System.out.println(squares.get(3));
System.out.println(squares.get(1));
}
}Size: 5
Computing square of 3
9
9
Computing square of 1
1Explanation: asking for the size computes nothing. get(3) computes element 3 once; the second get(3) reuses it. Elements 0, 2 and 4 are never computed because nobody asked for them.
In Practice
- Use stable values for things that are expensive to create and not always needed: loggers, configuration, database connection pools, large lookup tables.
- They help startup time, because nothing is created until it is used, while reads after that are as fast as reading a constant.
- Because this is a preview, the API is still being shaped. In the next preview (Java 26) it was reworked under the name
LazyConstant, with a smaller set of methods centered on lazy computation. The idea stays the same; check the exact method names for the JDK you use.
Common Mistakes
Forgetting --enable-preview. Both compiling and running need it.
Treating a stable value like a normal variable. There is no way to change or clear it once set. If the value must change later, use a normal field or an AtomicReference.
A supplier that throws. If the code inside orElseSet throws, nothing is stored, and the next call tries again.
Using it in production code on Java 25. Preview APIs can change or move between releases.
Practice
- Easy: Change
Config.javaso the settings text includes the student name "Advitha Anamaneni". Confirm that "Loading settings from disk..." still prints only once. - Harder: Create a
StableValue.mapwith the keys "Navika", "Srihitha" and "Advitha", whose value is the length of the name times 1000. Print one value twice and check how often your function runs.
Key Takeaways
Was this tutorial helpful?