<i18n dev> RFR: 8366178: Implement JEP 526: Lazy Constants (Second Preview)
ExE Boss
duke at openjdk.org
Mon Oct 13 11:51:02 UTC 2025
On Thu, 2 Oct 2025 10:51:08 GMT, Per Minborg <pminborg at openjdk.org> wrote:
> Implement JEP 526: Lazy Constants (Second Preview)
>
> The lazy list/map implementations are broken out from `ImmutableCollections` to a separate class.
>
> The old benchmarks are not moved/renamed to allow comparison with previous releases.
>
> `java.util.Optional` is updated so that its field is annotated with `@Stable`. This is to allow `Optional` instances to be held in lazy constants and still provide constant folding.
I’m gonna miss **Stable Values**, as it has some use cases which aren’t served by **Lazy Constants**, and on which I depend on in some of my code, so I’m stuck with using regular non‑`final` fields.
--------------------------------------------------------------------------------
Also, in the [JEP 526] table under “[Flexible initialization with lazy constants]”:
> | | Update count | Update location | Constant folding? | Concurrent updates?
> | -------------- | ------------ | --------------- | ----------------- | -------------------
> | `final` field | 1 | Constructor or static initializer | Yes | No
> | `LazyConstant` | [0, 1] | Anywhere | Yes, after initialization | Yes, by winner
> | Non-`final` field | [0, ∞) | Anywhere | No | Yes
The “Update location” of `LazyConstant` shouldn’t be “Anywhere”, as that was only accurate for `StableValue`, but not for `LazyConstant`, which is updated by calling the passed `Supplier`.
Similarly, concurrent updates are prevented for `LazyConstant`s by using `synchronized (this)`.
[JEP 526]: https://openjdk.org/jeps/526
[Flexible initialization with lazy constants]: https://openjdk.org/jeps/526#Flexible-initialization-with-lazy-constants
Getting access to the underlying `StableValue` API with **Lazy Constants** is way too hacky and convoluted (but doable):
<details>
<summary>StableVar.java</summary>
/*
* Any copyright is dedicated to the Public Domain.
* https://creativecommons.org/publicdomain/zero/1.0/
*/
import java.util.NoSuchElementException;
import java.util.function.Supplier;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import static java.lang.System.identityHashCode;
import static java.util.Objects.requireNonNull;
/// Horrible awful hack to get access to raw stable values in JDK 26+.
@NullMarked
public sealed interface StableVar<T> permits StableHacks.StableVarImpl {
boolean trySet(final T contents) throws NullPointerException, IllegalStateException;
@Nullable T orNull();
T orElse(final T other) throws NullPointerException;
T orElseThrow() throws NoSuchElementException;
boolean isSet();
T orElseSet(final Supplier<? extends T> supplier) throws NullPointerException, IllegalStateException;
void setOrThrow(final T contents) throws NullPointerException, IllegalStateException;
static <T> StableVar<T> of() {
return StableHacks.newInstance();
}
}
/// Encapsulates the actual implementation of `StableValue` on `LazyConstant`
///
/// @author ExE Boss
@NullMarked
/*package*/ final @Namespace class StableHacks {
private StableHacks() throws InstantiationException { throw new InstantiationException(StableHacks.class.getName()); }
private static final String UNSET_SUFFIX = ".unset";
private static final Object UNSET = new Object() {
@Override
public int hashCode() {
return 0;
}
@Override
public String toString() {
return "unset";
}
};
private static final ScopedValue<?> SCOPE = ScopedValue.newInstance();
private static final Supplier<?> SCOPE_GETTER = SCOPE::get;
/*package*/ static final <T> StableVarImpl<T> newInstance() {
return new StableValue<>();
}
/*package*/ sealed interface StableVarImpl<T> extends StableVar<T> {
}
private record StableValue<T>(
// Implemented as a record so that the JVM treats this as a trusted final field
// even when `-XX:+TrustFinalNonStaticFields` is not set
LazyConstant<T> contents
) implements StableVarImpl<T> {
@SuppressWarnings("unchecked")
private StableValue() {
this(LazyConstant.<T>of((Supplier) SCOPE_GETTER));
}
private StableValue {
if (contents.isInitialized()) throw new InternalError();
}
@SuppressWarnings("unchecked")
private final ScopedValue<T> scope() {
return (ScopedValue<T>) SCOPE;
}
private final void preventReentry() throws IllegalStateException {
if (Thread.holdsLock(this)) {
throw new IllegalStateException("Recursive initialization of a stable value is illegal");
}
}
@Override
public boolean trySet(final T contents) throws NullPointerException, IllegalStateException {
requireNonNull(contents);
if (this.contents.isInitialized()) return false;
preventReentry();
synchronized (this) {
return this.setImpl(contents);
}
}
@Override
@SuppressWarnings("unchecked")
public final @Nullable T orNull() {
return unwrapUnset(((LazyConstant) this.contents).orElse(UNSET));
}
@Override
public T orElse(T other) throws NullPointerException {
return this.contents.orElse(other);
}
@Override
public T orElseThrow() throws NoSuchElementException {
{ final T contents; if ((contents = this.orNull()) != null) {
return contents;
} }
throw new NoSuchElementException();
}
@Override
public boolean isSet() {
return this.contents.isInitialized();
}
@Override
public T orElseSet(final Supplier<? extends T> supplier) throws NullPointerException, IllegalStateException {
requireNonNull(supplier);
{ final T contents; if ((contents = this.orNull()) != null) {
return contents;
} }
return orElseSetSlowPath(supplier);
}
@Override
public void setOrThrow(final T contents) throws NullPointerException, IllegalStateException {
if (!trySet(contents)) {
throw new IllegalStateException();
}
}
private final T orElseSetSlowPath(
final Supplier<? extends T> supplier
) throws NullPointerException, IllegalStateException {
preventReentry();
synchronized (this) {
{ final T contents; if ((contents = this.orNull()) != null) {
return contents;
} }
final T newValue;
this.setImpl(newValue = requireNonNull(supplier.get()));
return newValue;
}
}
private final boolean setImpl(final T contents) {
assert Thread.holdsLock(this);
if (this.contents.isInitialized()) {
return false;
}
ScopedValue.where(this.scope(), contents).run(this.contents::get);
return true;
}
@Override
public final boolean equals(final Object obj) {
return this == obj;
}
@Override
public final int hashCode() {
return identityHashCode(this);
}
@Override
public String toString() {
final Object contents;
return renderValue(
"StableValue",
(contents = this.orNull()) != this
? contents
: "(this StableValue)"
);
}
}
@SuppressWarnings("unchecked")
private static final <T> @Nullable T unwrapUnset(final @Nullable Object obj) {
return (obj == UNSET) ? null : (T) obj;
}
private static final String renderValue(
final String type,
final @Nullable Object value
) throws NullPointerException {
return (value == null)
? type.concat(UNSET_SUFFIX)
: (type + '[' + value + ']');
}
}
</details>
-------------
PR Comment: https://git.openjdk.org/jdk/pull/27605#issuecomment-3367784825
PR Comment: https://git.openjdk.org/jdk/pull/27605#issuecomment-3368298279
More information about the i18n-dev
mailing list