[External] : Re: Feedback about LazyConstants API (JEP526)
Maurizio Cimadamore
maurizio.cimadamore at oracle.com
Tue Dec 9 11:25:07 UTC 2025
Hit send too fast...
Yes, orElse alone can't set/compute the uninitialized value. So that's a
problem (as you observed)
But orElse can't also be explained in terms of get() -- unless you
accept doing a double access of the underlying storage.
Uplevelling, I believe the fact that, in the current form, neither API
sits cleanly on top of the other is probably a part of the problem
highlighted in this discussion.
Maurizio
On 09/12/2025 11:22, Maurizio Cimadamore wrote:
>
>
> On 09/12/2025 11:15, Anatoly Kupriyanov wrote:
>> That's seems exactly opposite. The `get` returns initial value always
>> making the constant initialised by calling compute.
>> The `orElse(v)` is a shortcut for `c.isInitialized() ? c.get() : v`.
>
> Not really :-)
>
> c.isInitialized also calls `getAcquire` internally (so your snippet
> would end up calling getAcquire twice in some paths, once for
> isInitialized, and another for get).
>
> Technically, get() can be explained as:
>
> V value = lazyConstant.orElse(sentinel)
> if (value == sentinel) throw new NoSuchElementException
>
> That performs only one call to getAcquire, not two.
>
> (We don't implement it exactly this way, but we could).
>
> Maurizio
>
>>
>> WBR, Anatoly.
>>
>> On Tue, 9 Dec 2025, 10:25 Maurizio Cimadamore,
>> <maurizio.cimadamore at oracle.com> wrote:
>>
>> I agree with most of the conclusions in this thread.
>>
>>
>> One small nit is that, in reality, `orElse` is a "primitive" in
>> disguise. E.g. you can implement `get` in terms of `orElse` but
>> not the other way around (unless you are willing to do _two_
>> accessed to the underlying value). So, while we could drop it, we
>> would also lose something (which is why we decided to keep it, at
>> least for now).
>>
>>
>> Maurizio
>>
>>
>>
>> On 08/12/2025 12:31, Per-Ake Minborg wrote:
>>> So, it is nice that folks seem to agree that
>>> |LazyConstant| should only compute and initialize its contents
>>> from the Supplier/lambda given at declaration time. The
>>> |orElse| method seems to blur the contours of |LazyConstant| ,
>>> and so, as previously said, we might consider removing the
>>> method altogether in the next preview.
>>>
>>> It is also a fact that many have identified a need for
>>> "something else more low-level" that supports a more imperative
>>> programming model when working with constants that are lazily
>>> set. We do not rule out that such a thing might appear in a
>>> future JDK version.
>>>
>>> Best, Per
>>>
>>> Confidential- Oracle Internal
>>> ------------------------------------------------------------------------
>>> *From:* David Alayachew <davidalayachew at gmail.com>
>>> <mailto:davidalayachew at gmail.com>
>>> *Sent:* Friday, December 5, 2025 2:51 PM
>>> *To:* Red IO <redio.development at gmail.com>
>>> <mailto:redio.development at gmail.com>
>>> *Cc:* david Grajales <david.1993grajales at gmail.com>
>>> <mailto:david.1993grajales at gmail.com>; Per-Ake Minborg
>>> <per-ake.minborg at oracle.com>
>>> <mailto:per-ake.minborg at oracle.com>; amber-dev
>>> <amber-dev at openjdk.org> <mailto:amber-dev at openjdk.org>;
>>> core-libs-dev <core-libs-dev at openjdk.org>
>>> <mailto:core-libs-dev at openjdk.org>
>>> *Subject:* [External] : Re: Feedback about LazyConstants API
>>> (JEP526)
>>> Caveat -- I have only used the Java 25 version of this library.
>>>
>>> I agree that the name orElse() is not intuitive. It was made
>>> more intuitive by the existence of orElseSet(). In its absence,
>>> changing the name makes sense.
>>>
>>> Though, I'm definitely open to just removing the method. This is
>>> easy enough to accomplish ourselves. Would prefer a rename though.
>>>
>>> On Fri, Dec 5, 2025, 8:32 AM Red IO
>>> <redio.development at gmail.com> wrote:
>>>
>>> Hi David,
>>> As par already said the orElse method doesn't initializes
>>> the LazyConstant.
>>> It just checks rather the value is init and if not calls the
>>> supplier to get a substitute for the missing constant.
>>> Example:
>>> LazyConstant<String> x = LazyConstant.of(() -> "Const");
>>> var uninit1 = x.orElse(() -> "substitute 1");
>>> var uninit2 = x.orElse(() -> "substitute 2");
>>> var init1 = x.get();
>>> var init2 = x.orElse(() -> "substitute 3");
>>> uninit1 and uninit2 get the substitute 1/2
>>> And init1 and init2 get Const.
>>>
>>> This is surprising if you expect it to be a way to init it
>>> with an alternative value.
>>>
>>> My suggestion would to make the separation clear and allow
>>> for another use case by spliting this api in 2 parts:
>>> One class LazyConstant
>>> Takes a Supplier in static factory and exposes get()
>>>
>>> And
>>> Class LazyInit
>>> Which takes no arguments in the static factory and takes a
>>> supplier in the get method that gets called when get is
>>> called for the first time.
>>> In this case the source for the constant can be any piece of
>>> code that has access to the LazyConstant. This might be
>>> desired in some cases. In cases where it's not the other
>>> version can be used.
>>>
>>> This split makes it clear from which context the constant is
>>> initialized from (consumer or at declaration)
>>>
>>> Mixing those 2 or having methods that appear to do this is
>>> rather confusing.
>>>
>>>
>>>
>>> One solution for the "i might not want to init the constant"
>>> case the "orElse" method is meant to be is to have a method
>>> "tryGet" which returns Optional instead. This makes it clear
>>> that the value might not be there and is not initialized
>>> when calling the method. Nobody expects to init the constant
>>> when calling orElse on a returned Optional.
>>>
>>> My 2 suggestions here are completely independent and should
>>> be viewed as such.
>>>
>>> Great regards
>>> RedIODev
>>>
>>> On Fri, Dec 5, 2025, 13:55 david Grajales
>>> <david.1993grajales at gmail.com> wrote:
>>>
>>> HI Per. I pleasure to talk with you.
>>>
>>> You are right about one thing but this actually makes
>>> the API less intuitive and harder to read and reason about.
>>>
>>> LazyConstant<String> foo = LazyConstant.of(() -> "hello");
>>>
>>> void main() {
>>> if (someCondition()) {// asume false
>>> foo.get();
>>> }
>>> foo.orElse("hello2"); // ...
>>>
>>> println(foo.get()); // This prints "hello"
>>> }
>>>
>>> But if one assigns foo.orElse("hello2") to a variable,
>>> the variable actually gets the "hello2" value.
>>>
>>> void main() {
>>> if (someCondition()) {// asume false
>>> foo.get();
>>> }
>>> var res = foo.orElse("hello2"); // ...
>>> var res2 = foo.orElse("hello3");
>>> println(res); // This prints "hello2"
>>> println(res2);//This prints "hello3"
>>> }
>>>
>>> This is actually even more confusing and makes the API
>>> more error prone. I personally think once initialized
>>> the lazy constant should always return the same value
>>> (maybe through the .get() method only), and there should
>>> not be any possibility of getting a different
>>> values from the same instance either in the .of() static
>>> method or in any hypothetical instance method for
>>> conditional downstream logic. I guess one could achieve
>>> the latter with the static factory method
>>> through something like this (although less elegant)
>>>
>>> private class Bar{
>>> private final LazyConstant<String> foo;
>>> private Bar(Some some){
>>>
>>> if(some.condition){
>>> foo = LazyConstant.of(() -> "hello");
>>> }else {
>>> foo = LazyConstant.of(() -> "hello2");
>>> }
>>> }
>>> }
>>>
>>> Thank you for reading. This is all I have to report.
>>>
>>> Best regards.
>>>
>>>
>>>
>>> El vie, 5 dic 2025 a la(s) 6:05 a.m., Per-Ake Minborg
>>> (per-ake.minborg at oracle.com) escribió:
>>>
>>> Hi David,
>>>
>>> Thank you for trying out LazyConstant and providing
>>> feedback. That is precisely what previews are for!
>>>
>>> If you take a closer look at the specification of
>>> |LazyConstant::orElse,| it says that the method will
>>> /never trigger initialization./ And so, you
>>> /can/ actually be sure that in your first example,
>>> |foo| is always initialized to "hello" (if ever
>>> initialized). It is only if foo is not initialized
>>> that the method will return "hello2" (again, without
>>> initializing foo). This is similar to how
>>> |Optional| works.
>>>
>>> It would be possible to entirely remove the
>>> |orElse()| method from the API, and in the rare
>>> cases where an equivalent functionality is called
>>> for, rely on |LazyConstant::isInitialized| instead.
>>>
>>> Best, Per
>>>
>>>
>>> Confidential- Oracle Internal
>>> ------------------------------------------------------------------------
>>> *From:* amber-dev <amber-dev-retn at openjdk.org> on
>>> behalf of david Grajales <david.1993grajales at gmail.com>
>>> *Sent:* Friday, December 5, 2025 5:38 AM
>>> *To:* amber-dev <amber-dev at openjdk.org>;
>>> core-libs-dev at openjdk.org <core-libs-dev at openjdk.org>
>>> *Subject:* Feedback about LazyConstants API (JEP526)
>>> Dear Java Dev Team,
>>>
>>> I am writing to provide feedback and two specific
>>> observations regarding the LazyConstant API, which
>>> is currently a preview feature in OpenJDK 26.
>>>
>>> I appreciate the API's direction and I think it's a
>>> good improvement compared to its first iteration;
>>> however, I see potential for improved
>>> expressiveness, particularly in conditional scenarios.
>>>
>>>
>>> *1. Proposal: Zero-Parameter `LazyConstant.of()`
>>> Overload:*
>>>
>>> Currently, the mandatory use of a factory method
>>> receiving a `Supplier` (due to the lack of a public
>>> constructor) can obscure the expressiveness of
>>> conditional or multiple-value initialization paths.
>>> **The Issue:** When looking at the declaration:
>>>
>>> LazyConstant<String> foo = LazyConstant.of(() ->
>>> "hello");
>>>
>>> the code gives the strong, immediate impression that
>>> the value is *always* initialized to |"hello"|. This
>>> makes it difficult to infer that the constant might
>>> ultimately resolve to an alternative value set later
>>> via |orElse()| or another conditional path,
>>> especially when skimming the code:
>>>
>>> LazyConstant<String> foo = LazyConstant.of(() ->
>>> "hello"); // When skimming the code it's not always
>>> obvious that this may not be the actual value
>>> void main() {
>>> if (someCondition()) {
>>> foo.get(); // Trigger initialization to
>>> "hello"
>>> }
>>> // If someCondition is false, the final value of foo
>>> is determined here:
>>> var res1 = foo.orElse("hello2"); // ...
>>> }
>>>
>>> *My Suggestion:* I propose introducing a
>>> *zero-parameter overloaded static factory method*
>>> |of()|:
>>>
>>> LazyConstant<String> foo = LazyConstant.of();
>>>
>>> This form explicitly communicates that the constant
>>> is initialized to an *unresolved* state, suggesting
>>> that the value will be determined downstream by the
>>> first invocation of an initialization/computation
>>> method.
>>>
>>> LazyConstant<String> foo = LazyConstant.of(); //
>>> Clearly unresolved
>>> void main() {
>>> if (someCondition()) {
>>> foo.orElse("hello");
>>> }
>>> var res1 = foo.orElse("hello2"); // ...
>>> }
>>>
>>> This is specially useful for clarity when one has
>>> conditional initialization in places such as the
>>> constructor of a class. For example
>>>
>>> private class Bar{
>>> LazyConstant<String> foo = LazyConstant.of();
>>> private Bar(Some some){
>>> if(some.condition()){
>>> foo.orElse("foo");
>>> }
>>> foo.orElse("foo2");
>>> }
>>>
>>> String computeValue() {
>>> return "hello";
>>> }
>>>
>>> String computeValue2(){
>>> return "hello2";
>>> }
>>> }
>>>
>>>
>>> 2. Method Naming Suggestion and and supplier
>>> in instance method for consistency in the API
>>>
>>> My second, much more minor observation relates to
>>> the instance method |orElse(T t)|.
>>>
>>> While |orElse| fits a retrieval pattern, I
>>> personally feel that *|compute|* or
>>> *|computeIfAbsent|* would better express the intent
>>> of this method, as its primary function is not just
>>> to retrieve, but to trigger the computation and *set
>>> the final value* of the constant if it is currently
>>> uninitialized. Also, as the factory of() has a
>>> supplier i think this instance method should also
>>> receive a Supplier, This not only keeps the API
>>> consistent in the usage but makes more ergonomic the
>>> declaration of complex initialization logic inside
>>> the method.
>>>
>>>
>>> private class Bar{
>>> LazyConstant<InitParams> foo =
>>> LazyConstant.of(InitParam::default); // Under the
>>> current API this is mandatory but in reality the
>>> value is set in the constructor, default is never
>>> really used.
>>> private Bar(Some some){
>>> foo.compute(some::executeCallToCacheDBAndBringInitializationParams)
>>> //Real configuration happens here
>>>
>>> }
>>> }
>>>
>>> This last it's very common for initialization of
>>> configuration classes and singletons.
>>>
>>>
>>> Thank you so much for your attention, I hope you
>>> find this feedback useful.
>>>
>>> Always yours. David Grajales
>>>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://mail.openjdk.org/pipermail/amber-dev/attachments/20251209/5af09593/attachment-0001.htm>
More information about the amber-dev
mailing list