Stable Values in Java (Preview)

Naveen AnamaneniUpdated · 7 min read

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:

ChoiceSet when?Can change?Fast reads?
final fieldIn the constructor or static initializer, alwaysNoYes
Normal field with lazy setupLater, when first neededYes, by mistake tooNot treated as constant
StableValueLater, when first neededNo, at most onceYes, once set

Running Preview Code

Without the flag, the compiler stops:

Compiler output
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.

Config.java▶ Try it
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());
    }
}
Output
Program started
Loading settings from disk...
termFee=15000, lateFine=500
termFee=15000, lateFine=500
termFee=15000, lateFine=500

Explanation:

  1. StableValue.of() creates an empty stable value. The field itself is final, but its content is filled in later.
  2. "Program started" prints first, so the settings are not loaded at startup.
  3. The first call to settings() runs the lambda, which prints "Loading settings..." and stores the result.
  4. Every later call returns the stored value. The loading code runs only once.

Methods

MethodDescription
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.

RollNumber.java▶ Try it
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());
        }
    }
}
Output
Set yet? false
First try: true
Second try: false
Roll number: 10A-07
Refused: The contents is already set

Explanation: 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.

Lazy.java
// 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:

FactoryDescription
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.

Squares.java▶ Try it
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));
    }
}
Output
Size: 5
Computing square of 3
9
9
Computing square of 1
1

Explanation: 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

  1. Easy: Change Config.java so the settings text includes the student name "Advitha Anamaneni". Confirm that "Loading settings from disk..." still prints only once.
  2. Harder: Create a StableValue.map with 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.

Quick check: Stable values

4 questions
1How many times can a StableValue be set?
2What does orElseSet do when the value is already set?
3What must you add to compile and run StableValue code in Java 25?
4What does trySet return when the value was already set?

Key Takeaways

Was this tutorial helpful?

Lesson 525 of 578