Feedback on LazyConstants (formerly StableValues)

Per-Ake Minborg per-ake.minborg at oracle.com
Wed Sep 24 10:38:24 UTC 2025


Hello David, and thank you for getting back to us with feedback on the Lazy Constants API.

In the second preview, LazyConstant can not be set imperatively. In other words, we have removed the old StableValue methods: trySet(), setOrThrow() and orElseSet() as they were generally difficult to use and were prone to creating boxing, lambda capturing, and locking issues, which could negatively impact both performance, code readability, and application maintenance costs.

Instead, an underlying computing function must now be given at construction time. The various use cases we have identified indicate that this simplification of the API still covers a vast majority of those cases. However, there are other, more complicated cases that are not easily supported by LazyConstant and for which a more lower-level access capability would be appropriate.  We are planning to address this in future works, separate from LazyConstant. We will add that piece of information to the JEP, so thank you for identifying this. With respect to adding separate language constructs, such as a lazy keyword, the bar for this is extremely high and would have to be motivated and gauged against other potential language improvements, such as evolving pattern matching.

Note that LazyConstant now has a get() method, and that it also implements Supplier for easy integration with existing code.

Here is what your first example could look like with the new API:

```

        private static final LazyConstant<MySingleton> MESSAGE =
                LazyConstant.of(() -> new MySingleton("Hello Java"));

        void main(){
            IO.println(MESSAGE.get().message());
        }

        private record MySingleton(String message){ }

```

This will provide constant folding of the `MESSAGE.get().message()` sequence in case the JIT compiler decides to compile it.

I think the other examples you show (albeit I didn't fully get how they were supposed to work) would have issues regardless of whether there were language or library support for lazy computation, because the initialization logic is not known at declaration time. Some other languages (like Kotlin's `lateinit` [1]) have constructs that would seem to fit at a first glance, but they come with other severe semantic limitations (such as lack of thread-safety, lack of support for primitive types, and no constant folding). We want to do better and will, as previously said, address this down the road.

Thanks again for your feedback, David.

Best, Per Minborg

[1] https://medium.com/@anandgaur2207/lateinit-vs-lazy-in-kotlin-9608fb286fc6
[https://miro.medium.com/v2/resize:fit:1200/0*BqZDTJExg-RoB9qm.png]<https://medium.com/@anandgaur2207/lateinit-vs-lazy-in-kotlin-9608fb286fc6>
Lateinit vs Lazy in Kotlin - Medium<https://medium.com/@anandgaur2207/lateinit-vs-lazy-in-kotlin-9608fb286fc6>
Lateinit vs Lazy in Kotlin When building Android apps in Kotlin, you often need to initialize variables, but the exact moment of initialization may vary depending on your app’s requirements ...
medium.com

________________________________
From: amber-dev <amber-dev-retn at openjdk.org> on behalf of david Grajales <david.1993grajales at gmail.com>
Sent: Wednesday, September 24, 2025 4:02 AM
To: amber-dev <amber-dev at openjdk.org>
Subject: Feedback on LazyConstants (formerly StableValues)


Dear Amber team,

I hope this message finds you well. First of all, I want to extend my gratitude for all your hard work in continuously improving the Java platform — it is very much appreciated.

I have been experimenting with the StableValues API (now renamed LazyConstants) and attempting to use it in place of some of my older automation scripts for personal projects. While doing so, I noticed that the current design feels a bit rigid in certain scenarios, and this may indicate that a keyword or annotation-based solution could be a better fit.

Let me illustrate with a simple example. A common case of deferred initialization is the lazy singleton pattern:

void main(){
    var message = MySingleton.getInstance("Hello java").getMessage();
    println(message);
}

private static class MySingleton{
    private static MySingleton instance;
    private final String message;

    private MySingleton(String message) {
        this.message = message;
    }

    public static  MySingleton getInstance(String message) {
        if (instance == null) {
            instance = new MySingleton(message);
        }
        return instance;
    }
    public String getMessage() {
        return message;
    }
}


While this works, the result feels neither more concise nor more readable. The code is now cluttered by the API’s requirements rather than improved by them.


public class MySingleton2 {
    private static final StableValue<MySingleton2> instance = StableValue.of();
    private final String message;

    private MySingleton2(String message) {
        this.message = message;
    }

    public static MySingleton2 getInstance(String message){
        return instance.orElse(new MySingleton2(message));
    }

    public String getMessage(){
        return message;
    }
}

The issue here is that the current API does not offer a way to lazily capture constructor parameters — for example, through a Function or Supplier-based variant. Something like this would feel more natural:

public  <T, R>StableValue<R> createLazy(T t, Function<T, R> underlying){
    return StableValue.of(underlying.apply(t));
}

final StableValue<MySingleton> instanceLazy = createLazy("Hello", MySingleton::getInstance);

Now let's take a look to the case where the message field is lazy too.

public class MySingleton3 {
    private static final LazyConstant<MySingleton3> instance =
            LazyConstant.of(ew MySingleton3());

    private LazyConstant<String> message;

    private MySingleton3() {
        // constructor doesn't accept the dynamic value in this pattern,
        // so the message must be initialized separately.
    }


    public static MySingleton3 getInstance(String value) {
        // obtain the singleton (or create one if absent)
        MySingleton3 s = instance.orElse(new MySingleton3());

        // awkward: we must initialise the instance's lazy message here to capture `value`
        if (s.message == null) {
            s.message = LazyConstant.of(s.computeMessage(value));
        }

        return s;
    }

    public String getMessage() {
        // we assume getInstance(...) has already set `message`; read it with orElse(null)
        // (using `null` here to avoid any extra computation as orElse takes a concrete value).
        return message.orElse(null);
    }

    private String computeMessage(String value) {
        return "Hello " + value;
    }

    public static void main(String[] args) {
        // prints "Computing message..." once, then "Hello Java"
       println(MySingleton3.getInstance("Java").getMessage());

    }
}

As you can see, there is no real improvement from a code perspective — in fact, the resulting implementation is harder to reason about. The mental model for the developer becomes more complex, since they must explicitly manage when and how values are captured. Additionally, the lack of a simple get() method (having only orElse(...) and orElseThrow(...)) makes the code more verbose and slightly redundant, especially in cases where you know the value has already been initialized.

Now, the issue is not the lack of this or other methods. The issue relies on the infinite possible combinations that makes the design of a simple but flexible enough API a hard to achieve task. Seeing this limitation and the past limitations I have already shared in this mailing list, i am not sure if an API based approach is the best solution. I understand the good thing about API based features are how easy they are to grow, at least compared to a language level feature, but the current approach i fear may lead to an ever expanding API that would never be enough.


Considering this, and similar concerns I have previously shared here, I wonder if an API-based approach is really the right long-term solution. While API-based features are easier to evolve than language features, I worry that this approach could lead to an ever-expanding API that still never quite covers all the practical cases developers face. My current impression is that the API gets in the way more than it helps. Java already provides more natural ways to achieve lazy initialization, and LazyConstants ends up feeling like a low-level optimization, ceremony aimed at squeezing out marginal performance gains that may not even justify the added complexity.

I would love to hear your thoughts, pointing me to a better direction about how to use the API for this kind of cases. Is there something that i am not seeing?

Thank you for your time and for continuing to improve the Java platform.

Best regards and always yours: David Grajales Cardenas.

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20250924/3a8b9d80/attachment-0001.htm>


More information about the amber-dev mailing list