<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