diff --git a/src/java.base/share/classes/java/lang/StableValue.java b/src/java.base/share/classes/java/lang/StableValue.java
new file mode 100644
index 00000000000..b12d6f5e921
--- /dev/null
+++ b/src/java.base/share/classes/java/lang/StableValue.java
@@ -0,0 +1,757 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package java.lang;
+
+import jdk.internal.access.SharedSecrets;
+import jdk.internal.javac.PreviewFeature;
+import jdk.internal.lang.stable.StableEnumFunction;
+import jdk.internal.lang.stable.StableFunction;
+import jdk.internal.lang.stable.StableIntFunction;
+import jdk.internal.lang.stable.StableSupplier;
+import jdk.internal.lang.stable.StableUtil;
+import jdk.internal.lang.stable.StableValueImpl;
+
+import java.io.Serializable;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.RandomAccess;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.IntFunction;
+import java.util.function.Supplier;
+
+/**
+ * A stable value is a holder of contents that can be set at most once.
+ *
+ * A {@code StableValue} is typically created using the factory method
+ * {@linkplain StableValue#of() {@code StableValue.of()}}. When created this way,
+ * the stable value is unset, which means it holds no contents.
+ * Its contents, of type {@code T}, can be set by calling
+ * {@linkplain #trySet(Object) trySet()}, {@linkplain #setOrThrow(Object) setOrThrow()},
+ * or {@linkplain #orElseSet(Supplier) orElseSet()}. Once set, the contents
+ * can never change and can be retrieved by calling {@linkplain #orElseThrow() orElseThrow()}
+ * , {@linkplain #orElse(Object) orElse()}, or {@linkplain #orElseSet(Supplier) orElseSet()}.
+ *
+ * Consider the following example where a stable value field "{@code logger}" is a
+ * shallowly immutable holder of contents of type {@code Logger} and that is initially
+ * created as unset, which means it holds no contents. Later in the example, the
+ * state of the "{@code logger}" field is checked and if it is still unset,
+ * the contents is set:
+ *
+ * {@snippet lang = java:
+ * public class Component {
+ *
+ * // Creates a new unset stable value with no contents
+ * // @link substring="of" target="#of" :
+ * private final StableValue logger = StableValue.of();
+ *
+ * private Logger getLogger() {
+ * if (!logger.isSet()) {
+ * logger.trySet(Logger.create(Component.class));
+ * }
+ * return logger.orElseThrow();
+ * }
+ *
+ * public void process() {
+ * getLogger().info("Process started");
+ * // ...
+ * }
+ * }
+ *}
+ *
+ * If {@code getLogger()} is called from several threads, several instances of
+ * {@code Logger} might be created. However, the contents can only be set at most once
+ * meaning the first writer wins.
+ *
+ * In order to guarantee that, even under races, only one instance of {@code Logger} is
+ * ever created, the {@linkplain #orElseSet(Supplier) orElseSet()} method can be used
+ * instead, where the contents are lazily computed, and atomically set, via a
+ * {@linkplain Supplier supplier}. In the example below, the supplier is provided in the
+ * form of a lambda expression:
+ *
+ * {@snippet lang = java:
+ * public class Component {
+ *
+ * // Creates a new unset stable value with no contents
+ * // @link substring="of" target="#of" :
+ * private final StableValue logger = StableValue.of();
+ *
+ * private Logger getLogger() {
+ * return logger.orElseSet( () -> Logger.create(Component.class) );
+ * }
+ *
+ * public void process() {
+ * getLogger().info("Process started");
+ * // ...
+ * }
+ * }
+ *}
+ *
+ * The {@code getLogger()} method calls {@code logger.orElseSet()} on the stable value to
+ * retrieve its contents. If the stable value is unset, then {@code orElseSet()}
+ * evaluates the given supplier, and sets the contents to the result; the result is then
+ * returned to the client. In other words, {@code orElseSet()} guarantees that a
+ * stable value's contents is set before it returns.
+ *
+ * Furthermore, {@code orElseSet()} guarantees that out of one or more suppliers provided,
+ * only at most one is ever evaluated, and that one is only ever evaluated once,
+ * even when {@code logger.orElseSet()} is invoked concurrently. This property is crucial
+ * as evaluation of the supplier may have side effects, for example, the call above to
+ * {@code Logger.create()} may result in storage resources being prepared.
+ *
+ *
Stable Functions
+ * Stable values provide the foundation for higher-level functional abstractions. A
+ * stable supplier is a supplier that computes a value and then caches it into
+ * a backing stable value storage for subsequent use. A stable supplier is created via the
+ * {@linkplain StableValue#supplier(Supplier) StableValue.supplier()} factory, by
+ * providing an underlying {@linkplain Supplier} which is invoked when the stable supplier
+ * is first accessed:
+ *
+ * {@snippet lang = java:
+ * public class Component {
+ *
+ * private final Supplier logger =
+ * // @link substring="supplier" target="#supplier(Supplier)" :
+ * StableValue.supplier( () -> Logger.getLogger(Component.class) );
+ *
+ * public void process() {
+ * logger.get().info("Process started");
+ * // ...
+ * }
+ * }
+ *}
+ * A stable supplier encapsulates access to its backing stable value storage. This means
+ * that code inside {@code Component} can obtain the logger object directly from the
+ * stable supplier, without having to go through an accessor method like {@code getLogger()}.
+ *
+ * A stable int function is a function that takes an {@code int} parameter and
+ * uses it to compute a result that is then cached by the backing stable value storage
+ * for that parameter value. A stable {@link IntFunction} is created via the
+ * {@linkplain StableValue#intFunction(int, IntFunction) StableValue.intFunction()}
+ * factory. Upon creation, the input range (i.e. {@code [0, size)}) is specified together
+ * with an underlying {@linkplain IntFunction} which is invoked at most once per input
+ * value. In effect, the stable int function will act like a cache for the underlying
+ * {@linkplain IntFunction}:
+ *
+ * {@snippet lang = java:
+ * final class PowerOf2Util {
+ *
+ * private PowerOf2Util() {}
+ *
+ * private static final int SIZE = 6;
+ * private static final IntFunction UNDERLYING_POWER_OF_TWO =
+ * v -> 1 << v;
+ *
+ * private static final IntFunction POWER_OF_TWO =
+ * // @link substring="intFunction" target="#intFunction(int,IntFunction)" :
+ * StableValue.intFunction(SIZE, UNDERLYING_POWER_OF_TWO);
+ *
+ * public static int powerOfTwo(int a) {
+ * return POWER_OF_TWO.apply(a);
+ * }
+ * }
+ *
+ * int result = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime
+ *
+ *}
+ * The {@code PowerOf2Util.powerOfTwo()} function is a partial function that only
+ * allows a subset {@code [0, 5]} of the underlying function's {@code UNDERLYING_POWER_OF_TWO}
+ * input range.
+ *
+ *
+ * A stable function is a function that takes a parameter (of type {@code T}) and
+ * uses it to compute a result (of type {@code R}) that is then cached by the backing
+ * stable value storage for that parameter value. A stable function is created via the
+ * {@linkplain StableValue#function(Set, Function) StableValue.function()} factory.
+ * Upon creation, the input {@linkplain Set} is specified together with an underlying
+ * {@linkplain Function} which is invoked at most once per input value. In effect, the
+ * stable function will act like a cache for the underlying {@linkplain Function}:
+ *
+ * {@snippet lang = java:
+ * class Log2Util {
+ *
+ * private Log2Util() {}
+ *
+ * private static final Set KEYS =
+ * Set.of(1, 2, 4, 8, 16, 32);
+ * private static final UnaryOperator UNDERLYING_LOG2 =
+ * i -> 31 - Integer.numberOfLeadingZeros(i);
+ *
+ * private static final Function LOG2 =
+ * // @link substring="function" target="#function(Set,Function)" :
+ * StableValue.function(KEYS, UNDERLYING_LOG2);
+ *
+ * public static int log2(int a) {
+ * return LOG2.apply(a);
+ * }
+ *
+ * }
+ *
+ * int result = Log2Util.log2(16); // May eventually constant fold to 4 at runtime
+ *}
+ *
+ * The {@code Log2Util.log2()} function is a partial function that only allows
+ * a subset {@code {1, 2, 4, 8, 16, 32}} of the underlying function's
+ * {@code UNDERLYING_LOG2} input range.
+ *
+ * Stable Collections
+ * Stable values can also be used as backing storage for
+ * {@linkplain Collection##unmodifiable unmodifiable collections}. A stable list
+ * is an unmodifiable list, backed by an array of stable values. The stable list elements
+ * are computed when they are first accessed, using a provided {@linkplain IntFunction}:
+ *
+ * {@snippet lang = java:
+ * final class PowerOf2Util {
+ *
+ * private PowerOf2Util() {}
+ *
+ * private static final int SIZE = 6;
+ * private static final IntFunction UNDERLYING_POWER_OF_TWO =
+ * v -> 1 << v;
+ *
+ * private static final List POWER_OF_TWO =
+ * // @link substring="list" target="#list(int,IntFunction)" :
+ * StableValue.list(SIZE, UNDERLYING_POWER_OF_TWO);
+ *
+ * public static int powerOfTwo(int a) {
+ * return POWER_OF_TWO.get(a);
+ * }
+ * }
+ *
+ * int result = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime
+ *
+ * }
+ *
+ * Similarly, a stable map is an unmodifiable map whose keys are known at
+ * construction. The stable map values are computed when they are first accessed,
+ * using a provided {@linkplain Function}:
+ *
+ * {@snippet lang = java:
+ * class Log2Util {
+ *
+ * private Log2Util() {}
+ *
+ * private static final Set KEYS =
+ * Set.of(1, 2, 4, 8, 16, 32);
+ * private static final UnaryOperator UNDERLYING_LOG2 =
+ * i -> 31 - Integer.numberOfLeadingZeros(i);
+ *
+ * private static final Map LOG2 =
+ * // @link substring="map" target="#map(Set,Function)" :
+ * StableValue.map(CACHED_KEYS, UNDERLYING_LOG2);
+ *
+ * public static int log2(int a) {
+ * return LOG2.get(a);
+ * }
+ *
+ * }
+ *
+ * int result = Log2Util.log2(16); // May eventually constant fold to 4 at runtime
+ *
+ *}
+ *
+ * Composing stable values
+ * A stable value can depend on other stable values, forming a dependency graph
+ * that can be lazily computed but where access to individual elements can still be
+ * performant. In the following example, a single {@code Foo} and a {@code Bar}
+ * instance (that is dependent on the {@code Foo} instance) are lazily created, both of
+ * which are held by stable values:
+ * {@snippet lang = java:
+ * public final class DependencyUtil {
+ *
+ * private DependencyUtil() {}
+ *
+ * public static class Foo {
+ * // ...
+ * }
+ *
+ * public static class Bar {
+ * public Bar(Foo foo) {
+ * // ...
+ * }
+ * }
+ *
+ * private static final Supplier FOO = StableValue.supplier(Foo::new);
+ * private static final Supplier BAR = StableValue.supplier(() -> new Bar(FOO.get()));
+ *
+ * public static Foo foo() {
+ * return FOO.get();
+ * }
+ *
+ * public static Bar bar() {
+ * return BAR.get();
+ * }
+ *
+ * }
+ *}
+ * Calling {@code bar()} will create the {@code Bar} singleton if it is not already
+ * created. Upon such a creation, the dependent {@code Foo} will first be created if
+ * the {@code Foo} does not already exist.
+ *
+ * Another example, which has a more complex dependency graph, is to compute the
+ * Fibonacci sequence lazily:
+ * {@snippet lang = java:
+ * public final class Fibonacci {
+ *
+ * private Fibonacci() {}
+ *
+ * private static final int MAX_SIZE_INT = 46;
+ *
+ * private static final IntFunction FIB =
+ * StableValue.intFunction(MAX_SIZE_INT, Fibonacci::fib);
+ *
+ * public static int fib(int n) {
+ * return n < 2
+ * ? n
+ * : FIB.apply(n - 1) + FIB.apply(n - 2);
+ * }
+ *
+ * }
+ *}
+ * Both {@code FIB} and {@code Fibonacci::fib} recurse into each other. Because the
+ * stable int function {@code FIB} caches intermediate results, the initial
+ * computational complexity is reduced from exponential to linear compared to a
+ * traditional non-caching recursive fibonacci method. Once computed, the VM is free to
+ * constant-fold expressions like {@code Fibonacci.fib(5)}.
+ *
+ * The fibonacci example above is a directed acyclic graph (i.e.,
+ * it has no circular dependencies and is therefore a dependency tree):
+ *{@snippet lang=text :
+ *
+ * ___________fib(5)____________
+ * / \
+ * ____fib(4)____ ____fib(3)____
+ * / \ / \
+ * fib(3) fib(2) fib(2) fib(1)
+ * / \ / \ / \
+ * fib(2) fib(1) fib(1) fib(0) fib(1) fib(0)
+ *}
+ *
+ * If there are circular dependencies in a dependency graph, a stable value will
+ * eventually throw an {@linkplain IllegalStateException} upon referencing elements in
+ * a circularity.
+ *
+ *
Thread Safety
+ * The contents of a stable value is guaranteed to be set at most once. If competing
+ * threads are racing to set a stable value, only one update succeeds, while the other
+ * updates are blocked until the stable value is set, whereafter the other updates
+ * observes the stable value is set and leave the stable value unchanged.
+ *
+ * The at-most-once write operation on a stable value that succeeds
+ * (e.g. {@linkplain #trySet(Object) trySet()})
+ * {@linkplain java.util.concurrent##MemoryVisibility happens-before}
+ * any successful read operation (e.g. {@linkplain #orElseThrow()}).
+ * A successful write operation can be either:
+ *
+ * - a {@link #trySet(Object)} that returns {@code true},
+ * - a {@link #setOrThrow(Object)} that does not throw, or
+ * - an {@link #orElseSet(Supplier)} that successfully runs the supplier
+ *
+ * A successful read operation can be either:
+ *
+ * - a {@link #orElseThrow()} that does not throw,
+ * - a {@link #orElse(Object) orElse(other)} that does not return the {@code other} value
+ * - an {@link #orElseSet(Supplier)} that does not {@code throw}, or
+ * - an {@link #isSet()} that returns {@code true}
+ *
+ *
+ * The method {@link #orElseSet(Supplier)} guarantees that the provided
+ * {@linkplain Supplier} is invoked successfully at most once, even under race.
+ * Invocations of {@link #setOrThrow(Object)} form a total order of zero or more
+ * exceptional invocations followed by zero (if the contents were already set) or one
+ * successful invocation. Since stable functions and stable collections are built on top
+ * of the same principles as {@linkplain StableValue#orElseSet(Supplier) orElseSet()} they
+ * too are thread safe and guarantee at-most-once-per-input invocation.
+ *
+ *
+ * As the contents of a stable value can never change after it has been set, a JVM
+ * implementation may, for a set stable value, elide all future reads of that
+ * stable value, and instead directly use any contents that it has previously observed.
+ * This is true if the reference to the stable value is a constant (e.g. in cases where
+ * the stable value itself is stored in a {@code static final} field). Stable functions
+ * and collections are built on top of StableValue. As such, they might also be eligible
+ * for the same JVM optimizations as for StableValue.
+ *
+ * @implSpec Implementing classes of {@code StableValue} are free to synchronize on
+ * {@code this} and consequently, it should be avoided to
+ * (directly or indirectly) synchronize on a {@code StableValue}. Hence,
+ * synchronizing on {@code this} may lead to deadlock.
+ *
+ * Except for a {@code StableValue}'s contents itself,
+ * an {@linkplain #orElse(Object) orElse(other)} parameter, and
+ * an {@linkplain #equals(Object) equals(obj)} parameter; all
+ * method parameters must be non-null or a {@link NullPointerException}
+ * will be thrown.
+ *
+ * @implNote A {@code StableValue} is mainly intended to be a non-public field in
+ * a class and is usually neither exposed directly via accessors nor passed as
+ * a method parameter.
+ *
+ * Stable functions and collections make reasonable efforts to provide
+ * {@link Object#toString()} operations that do not trigger evaluation
+ * of the internal stable values when called.
+ * Stable collections have {@link Object#equals(Object)} operations that try
+ * to minimize evaluation of the internal stable values when called.
+ *
+ * As objects can be set via stable values but never removed, this can be a
+ * source of unintended memory leaks. A stable value's contents are
+ * {@linkplain java.lang.ref##reachability strongly reachable}.
+ * Be advised that reachable stable values will hold their set contents until
+ * the stable value itself is collected.
+ *
+ * A {@code StableValue} that has a type parameter {@code T} that is an array
+ * type (of arbitrary rank) will only allow the JVM to treat the
+ * array reference as a stable value but not its components.
+ * Instead, a {@linkplain #list(int, IntFunction) a stable list} of arbitrary
+ * depth can be used, which provides stable components. More generally, a
+ * stable value can hold other stable values of arbitrary depth and still
+ * provide transitive constantness.
+ *
+ * Stable values, functions, and collections are not {@link Serializable}.
+ *
+ * @param type of the contents
+ *
+ * @since 25
+ */
+@PreviewFeature(feature = PreviewFeature.Feature.STABLE_VALUES)
+public sealed interface StableValue
+ permits StableValueImpl {
+
+ // Principal methods
+
+ /**
+ * Tries to set the contents of this StableValue to the provided {@code contents}.
+ * The contents of this StableValue can only be set once, implying this method only
+ * returns {@code true} once.
+ *
+ * When this method returns, the contents of this StableValue is always set.
+ *
+ * @return {@code true} if the contents of this StableValue was set to the
+ * provided {@code contents}, {@code false} otherwise
+ * @param contents to set
+ * @throws IllegalStateException if a supplier invoked by {@link #orElseSet(Supplier)}
+ * recursively attempts to set this stable value by calling this method
+ * directly or indirectly.
+ */
+ boolean trySet(T contents);
+
+ /**
+ * {@return the contents if set, otherwise, returns the provided {@code other} value}
+ *
+ * @param other to return if the contents is not set
+ */
+ T orElse(T other);
+
+ /**
+ * {@return the contents if set, otherwise, throws {@code NoSuchElementException}}
+ *
+ * @throws NoSuchElementException if no contents is set
+ */
+ T orElseThrow();
+
+ /**
+ * {@return {@code true} if the contents is set, {@code false} otherwise}
+ */
+ boolean isSet();
+
+ /**
+ * {@return the contents; if unset, first attempts to compute and set the
+ * contents using the provided {@code supplier}}
+ *
+ * The provided {@code supplier} is guaranteed to be invoked at most once if it
+ * completes without throwing an exception. If this method is invoked several times
+ * with different suppliers, only one of them will be invoked provided it completes
+ * without throwing an exception.
+ *
+ * If the supplier throws an (unchecked) exception, the exception is rethrown and no
+ * contents is set. The most common usage is to construct a new object serving
+ * as a lazily computed value or memoized result, as in:
+ *
+ * {@snippet lang=java:
+ * Value v = stable.orElseSet(Value::new);
+ * }
+ *
+ * When this method returns successfully, the contents is always set.
+ *
+ * The provided {@code supplier} will only be invoked once even if invoked from
+ * several threads unless the {@code supplier} throws an exception.
+ *
+ * @param supplier to be used for computing the contents, if not previously set
+ * @throws IllegalStateException if the provided {@code supplier} recursively
+ * attempts to set this stable value.
+ */
+ T orElseSet(Supplier extends T> supplier);
+
+ // Convenience methods
+
+ /**
+ * Sets the contents of this StableValue to the provided {@code contents}, or, if
+ * already set, throws {@code IllegalStateException}.
+ *
+ * When this method returns (or throws an exception), the contents is always set.
+ *
+ * @param contents to set
+ * @throws IllegalStateException if the contents was already set
+ */
+ void setOrThrow(T contents);
+
+ // Object methods
+
+ /**
+ * {@return {@code true} if {@code this == obj}, {@code false} otherwise}
+ *
+ * @param obj to check for equality
+ */
+ boolean equals(Object obj);
+
+ /**
+ * {@return the {@linkplain System#identityHashCode(Object) identity hash code} of
+ * {@code this} object}
+ */
+ int hashCode();
+
+ // Factories
+
+ /**
+ * {@return a new unset stable value}
+ *
+ * An unset stable value has no contents.
+ *
+ * @param type of the contents
+ */
+ static StableValue of() {
+ return StableValueImpl.of();
+ }
+
+ /**
+ * {@return a new pre-set stable value with the provided {@code contents}}
+ *
+ * @param contents to set
+ * @param type of the contents
+ */
+ static StableValue of(T contents) {
+ final StableValue stableValue = StableValue.of();
+ stableValue.trySet(contents);
+ return stableValue;
+ }
+
+ /**
+ * {@return a new stable supplier}
+ *
+ * The returned {@linkplain Supplier supplier} is a caching supplier that records
+ * the value of the provided {@code underlying} supplier upon being first accessed via
+ * the returned supplier's {@linkplain Supplier#get() get()} method.
+ *
+ * The provided {@code underlying} supplier is guaranteed to be successfully invoked
+ * at most once even in a multi-threaded environment. Competing threads invoking the
+ * returned supplier's {@linkplain Supplier#get() get()} method when a value is
+ * already under computation will block until a value is computed or an exception is
+ * thrown by the computing thread. The computing threads will then observe the newly
+ * computed value (if any) and will then never execute.
+ *
+ * If the provided {@code underlying} supplier throws an exception, it is rethrown
+ * to the initial caller and no contents is recorded.
+ *
+ * If the provided {@code underlying} supplier recursively calls the returned
+ * supplier, an {@linkplain IllegalStateException} will be thrown.
+ *
+ * @param underlying supplier used to compute a cached value
+ * @param the type of results supplied by the returned supplier
+ */
+ static Supplier supplier(Supplier extends T> underlying) {
+ Objects.requireNonNull(underlying);
+ return StableSupplier.of(underlying);
+ }
+
+ /**
+ * {@return a new stable {@linkplain IntFunction}}
+ *
+ * The returned function is a caching function that, for each allowed {@code int}
+ * input, records the values of the provided {@code underlying}
+ * function upon being first accessed via the returned function's
+ * {@linkplain IntFunction#apply(int) apply()} method. If the returned function is
+ * invoked with an input that is not in the range {@code [0, size)}, an
+ * {@link IllegalArgumentException} will be thrown.
+ *
+ * The provided {@code underlying} function is guaranteed to be successfully invoked
+ * at most once per allowed input, even in a multi-threaded environment. Competing
+ * threads invoking the returned function's
+ * {@linkplain IntFunction#apply(int) apply()} method when a value is already under
+ * computation will block until a value is computed or an exception is thrown by
+ * the computing thread.
+ *
+ * If invoking the provided {@code underlying} function throws an exception, it is
+ * rethrown to the initial caller and no contents is recorded.
+ *
+ * If the provided {@code underlying} function recursively calls the returned
+ * function for the same input, an {@linkplain IllegalStateException} will
+ * be thrown.
+ *
+ * @param size the size of the allowed inputs in the continuous
+ * interval {@code [0, size)}
+ * @param underlying IntFunction used to compute cached values
+ * @param the type of results delivered by the returned IntFunction
+ * @throws IllegalArgumentException if the provided {@code size} is negative.
+ */
+ static IntFunction intFunction(int size,
+ IntFunction extends R> underlying) {
+ StableUtil.assertSizeNonNegative(size);
+ Objects.requireNonNull(underlying);
+ return StableIntFunction.of(size, underlying);
+ }
+
+ /**
+ * {@return a new stable {@linkplain Function}}
+ *
+ * The returned function is a caching function that, for each allowed
+ * input in the given set of {@code inputs}, records the values of the provided
+ * {@code underlying} function upon being first accessed via the returned function's
+ * {@linkplain Function#apply(Object) apply()} method. If the returned function is
+ * invoked with an input that is not in {@code inputs}, an {@link IllegalArgumentException}
+ * will be thrown.
+ *
+ * The provided {@code underlying} function is guaranteed to be successfully invoked
+ * at most once per allowed input, even in a multi-threaded environment. Competing
+ * threads invoking the returned function's {@linkplain Function#apply(Object) apply()}
+ * method when a value is already under computation will block until a value is
+ * computed or an exception is thrown by the computing thread.
+ *
+ * If invoking the provided {@code underlying} function throws an exception, it is
+ * rethrown to the initial caller and no contents is recorded.
+ *
+ * If the provided {@code underlying} function recursively calls the returned
+ * function for the same input, an {@linkplain IllegalStateException} will
+ * be thrown.
+ *
+ * @param inputs the set of (non-null) allowed input values
+ * @param underlying {@code Function} used to compute cached values
+ * @param the type of the input to the returned Function
+ * @param the type of results delivered by the returned Function
+ * @throws NullPointerException if the provided set of {@code inputs} contains a
+ * {@code null} element.
+ */
+ static Function function(Set extends T> inputs,
+ Function super T, ? extends R> underlying) {
+ Objects.requireNonNull(inputs);
+ // Checking that the Set of inputs does not contain a `null` value is made in the
+ // implementing classes.
+ Objects.requireNonNull(underlying);
+ return inputs instanceof EnumSet> && !inputs.isEmpty()
+ ? StableEnumFunction.of(inputs, underlying)
+ : StableFunction.of(inputs, underlying);
+ }
+
+ /**
+ * {@return a new stable list with the provided {@code size}}
+ *
+ * The returned list is an {@linkplain Collection##unmodifiable unmodifiable} list
+ * with the provided {@code size}. The list's elements are computed via the
+ * provided {@code mapper} when they are first accessed
+ * (e.g. via {@linkplain List#get(int) List::get}).
+ *
+ * The provided {@code mapper} function is guaranteed to be successfully invoked
+ * at most once per list index, even in a multi-threaded environment. Competing
+ * threads accessing an element already under computation will block until an element
+ * is computed or an exception is thrown by the computing thread.
+ *
+ * If invoking the provided {@code mapper} function throws an exception, it
+ * is rethrown to the initial caller and no value for the element is recorded.
+ *
+ * Any direct {@link List#subList(int, int) subList} or {@link List#reversed()} views
+ * of the returned list are also stable.
+ *
+ * The returned list and its {@link List#subList(int, int) subList} or
+ * {@link List#reversed()} views implement the {@link RandomAccess} interface.
+ *
+ * The returned list is unmodifiable and does not implement the
+ * {@linkplain Collection##optional-operation optional operations} in the
+ * {@linkplain List} interface.
+ *
+ * If the provided {@code mapper} recursively calls the returned list for the
+ * same index, an {@linkplain IllegalStateException} will be thrown.
+ *
+ * @param size the size of the returned list
+ * @param mapper to invoke whenever an element is first accessed
+ * (may return {@code null})
+ * @param the type of elements in the returned list
+ * @throws IllegalArgumentException if the provided {@code size} is negative.
+ */
+ static List list(int size,
+ IntFunction extends E> mapper) {
+ StableUtil.assertSizeNonNegative(size);
+ Objects.requireNonNull(mapper);
+ return SharedSecrets.getJavaUtilCollectionAccess().stableList(size, mapper);
+ }
+
+ /**
+ * {@return a new stable map with the provided {@code keys}}
+ *
+ * The returned map is an {@linkplain Collection##unmodifiable unmodifiable} map whose
+ * keys are known at construction. The map's values are computed via the provided
+ * {@code mapper} when they are first accessed
+ * (e.g. via {@linkplain Map#get(Object) Map::get}).
+ *
+ * The provided {@code mapper} function is guaranteed to be successfully invoked
+ * at most once per key, even in a multi-threaded environment. Competing
+ * threads accessing a value already under computation will block until an element
+ * is computed or an exception is thrown by the computing thread.
+ *
+ * If invoking the provided {@code mapper} function throws an exception, it
+ * is rethrown to the initial caller and no value associated with the provided key
+ * is recorded.
+ *
+ * Any direct {@link Map#values()} or {@link Map#entrySet()} views
+ * of the returned map are also stable.
+ *
+ * The returned map is unmodifiable and does not implement the
+ * {@linkplain Collection##optional-operations optional operations} in the
+ * {@linkplain Map} interface.
+ *
+ * If the provided {@code mapper} recursively calls the returned map for
+ * the same key, an {@linkplain IllegalStateException} will be thrown.
+ *
+ * @param keys the (non-null) keys in the returned map
+ * @param mapper to invoke whenever an associated value is first accessed
+ * (may return {@code null})
+ * @param the type of keys maintained by the returned map
+ * @param the type of mapped values in the returned map
+ * @throws NullPointerException if the provided set of {@code inputs} contains a
+ * {@code null} element.
+ */
+ static Map map(Set keys,
+ Function super K, ? extends V> mapper) {
+ Objects.requireNonNull(keys);
+ // Checking that the Set of keys does not contain a `null` value is made in the
+ // implementing class.
+ Objects.requireNonNull(mapper);
+ return SharedSecrets.getJavaUtilCollectionAccess().stableMap(keys, mapper);
+ }
+
+}
diff --git a/src/java.base/share/classes/java/util/Collection.java b/src/java.base/share/classes/java/util/Collection.java
index 0253dbc7e1a..43e8db55d7f 100644
--- a/src/java.base/share/classes/java/util/Collection.java
+++ b/src/java.base/share/classes/java/util/Collection.java
@@ -58,7 +58,7 @@ import java.util.stream.StreamSupport;
* constructors) but all of the general-purpose {@code Collection}
* implementations in the Java platform libraries comply.
*
- * Certain methods are specified to be
+ *
Certain methods are specified to be
* optional. If a collection implementation doesn't implement a
* particular operation, it should define the corresponding method to throw
* {@code UnsupportedOperationException}. Such methods are marked "optional
diff --git a/src/java.base/share/classes/java/util/ImmutableCollections.java b/src/java.base/share/classes/java/util/ImmutableCollections.java
index 205a6be6f89..9becf167176 100644
--- a/src/java.base/share/classes/java/util/ImmutableCollections.java
+++ b/src/java.base/share/classes/java/util/ImmutableCollections.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -36,11 +36,19 @@ import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
+import java.util.function.IntFunction;
import java.util.function.Predicate;
+import java.util.function.Supplier;
import java.util.function.UnaryOperator;
+
import jdk.internal.access.JavaUtilCollectionAccess;
import jdk.internal.access.SharedSecrets;
+import jdk.internal.lang.stable.StableUtil;
+import jdk.internal.lang.stable.StableValueImpl;
import jdk.internal.misc.CDS;
+import jdk.internal.util.ArraysSupport;
+import jdk.internal.util.NullableKeyValueHolder;
+import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.vm.annotation.Stable;
/**
@@ -128,6 +136,12 @@ class ImmutableCollections {
public List listFromTrustedArrayNullsAllowed(Object[] array) {
return ImmutableCollections.listFromTrustedArrayNullsAllowed(array);
}
+ public List stableList(int size, IntFunction extends E> mapper) {
+ return ImmutableCollections.stableList(size, mapper);
+ }
+ public Map stableMap(Set keys, Function super K, ? extends V> mapper) {
+ return new StableMap<>(keys, mapper);
+ }
});
}
}
@@ -250,6 +264,11 @@ class ImmutableCollections {
}
}
+ static List stableList(int size, IntFunction extends E> mapper) {
+ // A lazy list is not Serializable so, we cannot return `List.of()` if size == 0
+ return new StableList<>(size, mapper);
+ }
+
// ---------- List Implementations ----------
@jdk.internal.ValueBased
@@ -448,7 +467,7 @@ class ImmutableCollections {
private final int size;
private SubList(AbstractImmutableList root, int offset, int size) {
- assert root instanceof List12 || root instanceof ListN;
+ assert root instanceof List12 || root instanceof ListN || root instanceof StableList;
this.root = root;
this.offset = offset;
this.size = size;
@@ -499,7 +518,8 @@ class ImmutableCollections {
}
private boolean allowNulls() {
- return root instanceof ListN && ((ListN>)root).allowNulls;
+ return root instanceof ListN> listN && listN.allowNulls
+ || root instanceof StableList;
}
@Override
@@ -551,6 +571,15 @@ class ImmutableCollections {
}
return array;
}
+
+ @Override
+ public String toString() {
+ if (root instanceof StableList stableList) {
+ return StableUtil.renderElements(root, "StableList", stableList.delegates, offset, size);
+ } else {
+ return super.toString();
+ }
+ }
}
@jdk.internal.ValueBased
@@ -768,6 +797,116 @@ class ImmutableCollections {
}
}
+ @jdk.internal.ValueBased
+ static final class StableList extends AbstractImmutableList {
+
+ @Stable
+ private final IntFunction extends E> mapper;
+ @Stable
+ final StableValueImpl[] delegates;
+
+ StableList(int size, IntFunction extends E> mapper) {
+ this.mapper = mapper;
+ this.delegates = StableUtil.array(size);
+ }
+
+ @Override public boolean isEmpty() { return delegates.length == 0;}
+ @Override public int size() { return delegates.length; }
+ @Override public Object[] toArray() { return copyInto(new Object[size()]); }
+
+ @ForceInline
+ @Override
+ public E get(int i) {
+ final StableValueImpl delegate;
+ try {
+ delegate = delegates[i];
+ } catch (ArrayIndexOutOfBoundsException aioobe) {
+ throw new IndexOutOfBoundsException(i);
+ }
+ return delegate.orElseSet(new Supplier() {
+ @Override public E get() { return mapper.apply(i); }});
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public T[] toArray(T[] a) {
+ final int size = delegates.length;
+ if (a.length < size) {
+ // Make a new array of a's runtime type, but my contents:
+ T[] n = (T[])Array.newInstance(a.getClass().getComponentType(), size);
+ return copyInto(n);
+ }
+ copyInto(a);
+ if (a.length > size) {
+ a[size] = null; // null-terminate
+ }
+ return a;
+ }
+
+ @Override
+ public int indexOf(Object o) {
+ final int size = size();
+ for (int i = 0; i < size; i++) {
+ if (Objects.equals(o, get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ public int lastIndexOf(Object o) {
+ for (int i = size() - 1; i >= 0; i--) {
+ if (Objects.equals(o, get(i))) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @SuppressWarnings("unchecked")
+ private T[] copyInto(Object[] a) {
+ final int len = delegates.length;
+ for (int i = 0; i < len; i++) {
+ a[i] = get(i);
+ }
+ return (T[]) a;
+ }
+
+ @Override
+ public List reversed() {
+ return new StableReverseOrderListView<>(this);
+ }
+
+ @Override
+ public String toString() {
+ return StableUtil.renderElements(this, "StableList", delegates);
+ }
+
+ private static final class StableReverseOrderListView extends ReverseOrderListView.Rand {
+
+ private StableReverseOrderListView(List base) {
+ super(base, false);
+ }
+
+ // This method does not evaluate the elements
+ @Override
+ public String toString() {
+ final StableValueImpl[] delegates = ((StableList)base).delegates;
+ final StableValueImpl[] reversed = ArraysSupport.reverse(
+ Arrays.copyOf(delegates, delegates.length));
+ return StableUtil.renderElements(base, "Collection", reversed);
+ }
+
+ @Override
+ public List reversed() {
+ return base;
+ }
+
+ }
+
+ }
+
// ---------- Set Implementations ----------
@jdk.internal.ValueBased
@@ -1112,7 +1251,7 @@ class ImmutableCollections {
// ---------- Map Implementations ----------
// Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap
- abstract static class AbstractImmutableMap extends AbstractMap implements Serializable {
+ abstract static class AbstractImmutableMap extends AbstractMap {
@Override public void clear() { throw uoe(); }
@Override public V compute(K key, BiFunction super K,? super V,? extends V> rf) { throw uoe(); }
@Override public V computeIfAbsent(K key, Function super K,? extends V> mf) { throw uoe(); }
@@ -1143,7 +1282,7 @@ class ImmutableCollections {
}
// Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap
- static final class Map1 extends AbstractImmutableMap {
+ static final class Map1 extends AbstractImmutableMap implements Serializable {
@Stable
private final K k0;
@Stable
@@ -1215,7 +1354,7 @@ class ImmutableCollections {
* @param the value type
*/
// Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap
- static final class MapN extends AbstractImmutableMap {
+ static final class MapN extends AbstractImmutableMap implements Serializable {
@Stable
final Object[] table; // pairs of key, value
@@ -1405,6 +1544,130 @@ class ImmutableCollections {
return new CollSer(CollSer.IMM_MAP, array);
}
}
+
+ static final class StableMap
+ extends AbstractImmutableMap {
+
+ @Stable
+ private final Function super K, ? extends V> mapper;
+ @Stable
+ private final Map> delegate;
+
+ StableMap(Set keys, Function super K, ? extends V> mapper) {
+ this.mapper = mapper;
+ this.delegate = StableUtil.map(keys);
+ }
+
+ @Override public boolean containsKey(Object o) { return delegate.containsKey(o); }
+ @Override public int size() { return delegate.size(); }
+ @Override public Set> entrySet() { return new StableMapEntrySet(); }
+
+ @ForceInline
+ @Override
+ public V get(Object key) {
+ return getOrDefault(key, null);
+ }
+
+ @ForceInline
+ @Override
+ public V getOrDefault(Object key, V defaultValue) {
+ final StableValueImpl stable = delegate.get(key);
+ if (stable == null) {
+ return defaultValue;
+ }
+ @SuppressWarnings("unchecked")
+ final K k = (K) key;
+ return stable.orElseSet(new Supplier() {
+ @Override public V get() { return mapper.apply(k); }});
+ }
+
+ @jdk.internal.ValueBased
+ final class StableMapEntrySet extends AbstractImmutableSet> {
+
+ @Stable
+ private final Set>> delegateEntrySet;
+
+ StableMapEntrySet() {
+ this.delegateEntrySet = delegate.entrySet();
+ }
+
+ @Override public Iterator> iterator() { return new LazyMapIterator(); }
+ @Override public int size() { return delegateEntrySet.size(); }
+ @Override public int hashCode() { return StableMap.this.hashCode(); }
+
+ @Override
+ public String toString() {
+ return StableUtil.renderMappings(this, "StableSet", delegateEntrySet, false);
+ }
+
+ @jdk.internal.ValueBased
+ final class LazyMapIterator implements Iterator> {
+
+ @Stable
+ private final Iterator>> delegateIterator;
+
+ LazyMapIterator() {
+ this.delegateIterator = delegateEntrySet.iterator();
+ }
+
+ @Override public boolean hasNext() { return delegateIterator.hasNext(); }
+
+ @Override
+ public Entry next() {
+ final Map.Entry> inner = delegateIterator.next();
+ final K k = inner.getKey();
+ return new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier() {
+ @Override public V get() { return mapper.apply(k); }}));
+ }
+
+ @Override
+ public void forEachRemaining(Consumer super Map.Entry> action) {
+ final Consumer super Map.Entry>> innerAction =
+ new Consumer<>() {
+ @Override
+ public void accept(Entry> inner) {
+ final K k = inner.getKey();
+ action.accept(new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier() {
+ @Override public V get() { return mapper.apply(k); }})));
+ }
+ };
+ delegateIterator.forEachRemaining(innerAction);
+ }
+ }
+ }
+
+ @Override
+ public Collection values() {
+ return new StableMapValues();
+ }
+
+ final class StableMapValues extends AbstractImmutableCollection {
+ @Override public Iterator iterator() { return new ValueIterator(); }
+ @Override public int size() { return StableMap.this.size(); }
+ @Override public boolean isEmpty() { return StableMap.this.isEmpty();}
+ @Override public boolean contains(Object v) { return StableMap.this.containsValue(v); }
+
+ private static final IntFunction[]> GENERATOR = new IntFunction[]>() {
+ @Override
+ public StableValueImpl>[] apply(int len) {
+ return new StableValueImpl>[len];
+ }
+ };
+
+ @Override
+ public String toString() {
+ final StableValueImpl>[] values = delegate.values().toArray(GENERATOR);
+ return StableUtil.renderElements(StableMap.this, "StableMap", values);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return StableUtil.renderMappings(this, "StableMap", delegate.entrySet(), true);
+ }
+
+ }
+
}
// ---------- Serialization Proxy ----------
diff --git a/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java
index f88d57521ac..cac8785b158 100644
--- a/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java
+++ b/src/java.base/share/classes/jdk/internal/access/JavaUtilCollectionAccess.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2020, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2020, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -26,8 +26,14 @@
package jdk.internal.access;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.IntFunction;
public interface JavaUtilCollectionAccess {
List listFromTrustedArray(Object[] array);
List listFromTrustedArrayNullsAllowed(Object[] array);
+ List stableList(int size, IntFunction extends E> mapper);
+ Map stableMap(Set keys, Function super K, ? extends V> mapper);
}
diff --git a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
index deb786b42bd..2ed6dde794a 100644
--- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
+++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2019, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2019, 2025, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -80,6 +80,8 @@ public @interface PreviewFeature {
MODULE_IMPORTS,
@JEP(number=478, title="Key Derivation Function API", status="Preview")
KEY_DERIVATION,
+ @JEP(number = 502, title = "Stable Values", status = "Preview")
+ STABLE_VALUES,
LANGUAGE_MODEL,
/**
* A key for testing.
diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java
new file mode 100644
index 00000000000..88be2cea1b6
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableEnumFunction.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.lang.stable;
+
+import jdk.internal.util.ImmutableBitSetPredicate;
+import jdk.internal.vm.annotation.ForceInline;
+import jdk.internal.vm.annotation.Stable;
+
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.IntPredicate;
+import java.util.function.Supplier;
+
+/**
+ * Optimized implementation of a stable Function with enums as keys.
+ *
+ * @implNote This implementation can be used early in the boot sequence as it does not
+ * rely on reflection, MethodHandles, Streams etc.
+ *
+ * @param firstOrdinal the lowest ordinal used
+ * @param delegates a delegate array of inputs to StableValue mappings
+ * @param original the original Function
+ * @param the type of the input to the function
+ * @param the type of the result of the function
+ */
+public record StableEnumFunction, R>(Class enumType,
+ int firstOrdinal,
+ IntPredicate member,
+ @Stable StableValueImpl[] delegates,
+ Function super E, ? extends R> original) implements Function {
+ @ForceInline
+ @Override
+ public R apply(E value) {
+ if (!member.test(value.ordinal())) { // Implicit null-check of value
+ throw new IllegalArgumentException("Input not allowed: " + value);
+ }
+ final int index = value.ordinal() - firstOrdinal;
+ final StableValueImpl delegate;
+ // Since we did the member.test above, we know the index is in bounds
+ delegate = delegates[index];
+ return delegate.orElseSet(new Supplier() {
+ @Override public R get() { return original.apply(value); }});
+
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == this;
+ }
+
+ @Override
+ public String toString() {
+ final E[] enumElements = enumType.getEnumConstants();
+ final Collection>> entries = new ArrayList<>(enumElements.length);
+ int ordinal = firstOrdinal;
+ for (int i = 0; i < delegates.length; i++, ordinal++) {
+ if (member.test(ordinal)) {
+ entries.add(new AbstractMap.SimpleImmutableEntry<>(enumElements[ordinal], delegates[i]));
+ }
+ }
+ return StableUtil.renderMappings(this, "StableFunction", entries, true);
+ }
+
+ @SuppressWarnings("unchecked")
+ public static , R> Function of(Set extends T> inputs,
+ Function super T, ? extends R> original) {
+ // The input set is not empty
+ final Class enumType = (Class)inputs.iterator().next().getClass();
+ final BitSet bitSet = new BitSet(enumType.getEnumConstants().length);
+ int min = Integer.MAX_VALUE;
+ int max = Integer.MIN_VALUE;
+ for (T t : inputs) {
+ final int ordinal = ((E) t).ordinal();
+ min = Math.min(min, ordinal);
+ max = Math.max(max, ordinal);
+ bitSet.set(ordinal);
+ }
+ final int size = max - min + 1;
+ final IntPredicate member = ImmutableBitSetPredicate.of(bitSet);
+ return (Function) new StableEnumFunction(enumType, min, member, StableUtil.array(size), (Function) original);
+ }
+
+}
diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java
new file mode 100644
index 00000000000..1b10593e5e8
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableFunction.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.lang.stable;
+
+import jdk.internal.vm.annotation.ForceInline;
+
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+// Note: It would be possible to just use `LazyMap::get` with some additional logic
+// instead of this class but explicitly providing a class like this provides better
+// debug capability, exception handling, and may provide better performance.
+/**
+ * Implementation of a stable Function.
+ *
+ * @implNote This implementation can be used early in the boot sequence as it does not
+ * rely on reflection, MethodHandles, Streams etc.
+ *
+ * @param values a delegate map of inputs to StableValue mappings
+ * @param original the original Function
+ * @param the type of the input to the function
+ * @param the type of the result of the function
+ */
+public record StableFunction(Map extends T, StableValueImpl> values,
+ Function super T, ? extends R> original) implements Function {
+
+ @ForceInline
+ @Override
+ public R apply(T value) {
+ final StableValueImpl stable = values.get(value);
+ if (stable == null) {
+ throw new IllegalArgumentException("Input not allowed: " + value);
+ }
+ return stable.orElseSet(new Supplier() {
+ @Override public R get() { return original.apply(value); }});
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == this;
+ }
+
+ @Override
+ public String toString() {
+ return StableUtil.renderMappings(this, "StableFunction", values.entrySet(), true);
+ }
+
+ public static StableFunction of(Set extends T> inputs,
+ Function super T, ? extends R> original) {
+ return new StableFunction<>(StableUtil.map(inputs), original);
+ }
+
+}
diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableIntFunction.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableIntFunction.java
new file mode 100644
index 00000000000..8bb046a762d
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableIntFunction.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.lang.stable;
+
+import jdk.internal.vm.annotation.ForceInline;
+import jdk.internal.vm.annotation.Stable;
+
+import java.util.function.IntFunction;
+import java.util.function.Supplier;
+
+// Note: It would be possible to just use `LazyList::get` instead of this
+// class but explicitly providing a class like this provides better
+// debug capability, exception handling, and may provide better performance.
+/**
+ * Implementation of a stable IntFunction.
+ *
+ * For performance reasons (~10%), we are not delegating to a StableList but are using
+ * the more primitive functions in StableValueUtil that are shared with StableList/StableValueImpl.
+ *
+ * @implNote This implementation can be used early in the boot sequence as it does not
+ * rely on reflection, MethodHandles, Streams etc.
+ *
+ * @param the return type
+ */
+public record StableIntFunction(@Stable StableValueImpl[] delegates,
+ IntFunction extends R> original) implements IntFunction {
+
+ @ForceInline
+ @Override
+ public R apply(int index) {
+ final StableValueImpl delegate;
+ try {
+ delegate = delegates[index];
+ } catch (ArrayIndexOutOfBoundsException ioob) {
+ throw new IllegalArgumentException("Input not allowed: " + index, ioob);
+ }
+ return delegate.orElseSet(new Supplier() {
+ @Override public R get() { return original.apply(index); }});
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == this;
+ }
+
+ @Override
+ public String toString() {
+ return StableUtil.renderElements(this, "StableIntFunction", delegates);
+ }
+
+ public static StableIntFunction of(int size, IntFunction extends R> original) {
+ return new StableIntFunction<>(StableUtil.array(size), original);
+ }
+
+}
diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java
new file mode 100644
index 00000000000..bdb40648db6
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableSupplier.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.lang.stable;
+
+import jdk.internal.vm.annotation.ForceInline;
+
+import java.util.function.Supplier;
+
+/**
+ * Implementation of a stable supplier.
+ *
+ * @implNote This implementation can be used early in the boot sequence as it does not
+ * rely on reflection, MethodHandles, Streams etc.
+ *
+ * @param the return type
+ */
+public record StableSupplier(StableValueImpl delegate,
+ Supplier extends T> original) implements Supplier {
+
+ @ForceInline
+ @Override
+ public T get() {
+ return delegate.orElseSet(original);
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj == this;
+ }
+
+ @Override
+ public String toString() {
+ final Object t = delegate.wrappedContentAcquire();
+ return t == this ? "(this StableSupplier)" : StableValueImpl.renderWrapped(t);
+ }
+
+ public static StableSupplier of(Supplier extends T> original) {
+ return new StableSupplier<>(StableValueImpl.of(), original);
+ }
+
+}
diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableUtil.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableUtil.java
new file mode 100644
index 00000000000..f6f33f9b1e8
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableUtil.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.lang.stable;
+
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+
+public final class StableUtil {
+
+ private StableUtil() {}
+
+ public static String renderElements(Object self,
+ String selfName,
+ StableValueImpl>[] delegates) {
+ return renderElements(self, selfName, delegates, 0, delegates.length);
+ }
+
+ public static String renderElements(Object self,
+ String selfName,
+ StableValueImpl>[] delegates,
+ int offset,
+ int length) {
+ final StringJoiner sj = new StringJoiner(", ", "[", "]");
+ for (int i = 0; i < length; i++) {
+ final Object value = delegates[i + offset].wrappedContentAcquire();
+ if (value == self) {
+ sj.add("(this " + selfName + ")");
+ } else {
+ sj.add(StableValueImpl.renderWrapped(value));
+ }
+ }
+ return sj.toString();
+ }
+
+ public static String renderMappings(Object self,
+ String selfName,
+ Iterable>> delegates,
+ boolean curly) {
+ final StringJoiner sj = new StringJoiner(", ", curly ? "{" : "[", curly ? "}" : "]");
+ for (var e : delegates) {
+ final Object value = e.getValue().wrappedContentAcquire();
+ final String valueString;
+ if (value == self) {
+ valueString = ("(this ") + selfName + ")";
+ } else {
+ valueString = StableValueImpl.renderWrapped(value);
+ }
+ sj.add(e.getKey() + "=" + valueString);
+ }
+ return sj.toString();
+ }
+
+ public static StableValueImpl[] array(int size) {
+ assertSizeNonNegative(size);
+ @SuppressWarnings("unchecked")
+ final var stableValues = (StableValueImpl[]) new StableValueImpl>[size];
+ for (int i = 0; i < size; i++) {
+ stableValues[i] = StableValueImpl.of();
+ }
+ return stableValues;
+ }
+
+ public static Map> map(Set keys) {
+ Objects.requireNonNull(keys);
+ @SuppressWarnings("unchecked")
+ final var entries = (Map.Entry>[]) new Map.Entry, ?>[keys.size()];
+ int i = 0;
+ for (K key : keys) {
+ entries[i++] = Map.entry(key, StableValueImpl.of());
+ }
+ return Map.ofEntries(entries);
+ }
+
+ public static void assertSizeNonNegative(int size) {
+ if (size < 0) {
+ throw new IllegalArgumentException("size can not be negative: " + size);
+ }
+ }
+
+}
diff --git a/src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java b/src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java
new file mode 100644
index 00000000000..88c80eb6b39
--- /dev/null
+++ b/src/java.base/share/classes/jdk/internal/lang/stable/StableValueImpl.java
@@ -0,0 +1,218 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation. Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+package jdk.internal.lang.stable;
+
+import jdk.internal.misc.Unsafe;
+import jdk.internal.vm.annotation.DontInline;
+import jdk.internal.vm.annotation.ForceInline;
+import jdk.internal.vm.annotation.Stable;
+
+import java.util.NoSuchElementException;
+import java.util.Objects;
+import java.util.function.Supplier;
+
+/**
+ * The implementation of StableValue.
+ *
+ * @implNote This implementation can be used early in the boot sequence as it does not
+ * rely on reflection, MethodHandles, Streams etc.
+ *
+ * @param type of the contents
+ */
+public final class StableValueImpl implements StableValue {
+
+ static final String UNSET_LABEL = ".unset";
+
+ // Unsafe allows StableValue to be used early in the boot sequence
+ static final Unsafe UNSAFE = Unsafe.getUnsafe();
+
+ // Unsafe offsets for direct field access
+
+ private static final long CONTENT_OFFSET =
+ UNSAFE.objectFieldOffset(StableValueImpl.class, "contents");
+ // Used to indicate a holder value is `null` (see field `value` below)
+ // A wrapper method `nullSentinel()` is used for generic type conversion.
+ private static final Object NULL_SENTINEL = new Object();
+
+ // Generally, fields annotated with `@Stable` are accessed by the JVM using special
+ // memory semantics rules (see `parse.hpp` and `parse(1|2|3).cpp`).
+ //
+ // This field is used directly and reflectively via Unsafe using explicit memory semantics.
+ //
+ // | Value | Meaning |
+ // | -------------- | ------------ |
+ // | null | Unset |
+ // | NULL_SENTINEL | Set(null) |
+ // | other | Set(other) |
+ //
+ @Stable
+ private Object contents;
+
+ // Only allow creation via the factory `StableValueImpl::newInstance`
+ private StableValueImpl() {}
+
+ @ForceInline
+ @Override
+ public boolean trySet(T contents) {
+ if (wrappedContentAcquire() != null) {
+ return false;
+ }
+ // Prevent reentry via an orElseSet(supplier)
+ preventReentry();
+ // Mutual exclusion is required here as `orElseSet` might also
+ // attempt to modify the `wrappedValue`
+ synchronized (this) {
+ return wrapAndSet(contents);
+ }
+ }
+
+ @ForceInline
+ @Override
+ public void setOrThrow(T contents) {
+ if (!trySet(contents)) {
+ // Neither the set contents nor the provided contents is revealed in the
+ // exception message as it might be sensitive.
+ throw new IllegalStateException("The contents is already set");
+ }
+ }
+
+ @ForceInline
+ @Override
+ public T orElseThrow() {
+ final Object t = wrappedContentAcquire();
+ if (t == null) {
+ throw new NoSuchElementException("No contents set");
+ }
+ return unwrap(t);
+ }
+
+ @ForceInline
+ @Override
+ public T orElse(T other) {
+ final Object t = wrappedContentAcquire();
+ return (t == null) ? other : unwrap(t);
+ }
+
+ @ForceInline
+ @Override
+ public boolean isSet() {
+ return wrappedContentAcquire() != null;
+ }
+
+ @ForceInline
+ @Override
+ public T orElseSet(Supplier extends T> supplier) {
+ Objects.requireNonNull(supplier);
+ final Object t = wrappedContentAcquire();
+ return (t == null) ? orElseSetSlowPath(supplier) : unwrap(t);
+ }
+
+ @DontInline
+ private T orElseSetSlowPath(Supplier extends T> supplier) {
+ preventReentry();
+ synchronized (this) {
+ final Object t = contents; // Plain semantics suffice here
+ if (t == null) {
+ final T newValue = supplier.get();
+ // The mutex is not reentrant so we know newValue should be returned
+ wrapAndSet(newValue);
+ return newValue;
+ }
+ return unwrap(t);
+ }
+ }
+
+ // The methods equals() and hashCode() should be based on identity (defaults from Object)
+
+ @Override
+ public String toString() {
+ final Object t = wrappedContentAcquire();
+ return t == this
+ ? "(this StableValue)"
+ : renderWrapped(t);
+ }
+
+ // Internal methods shared with other internal classes
+
+ @ForceInline
+ public Object wrappedContentAcquire() {
+ return UNSAFE.getReferenceAcquire(this, CONTENT_OFFSET);
+ }
+
+ static String renderWrapped(Object t) {
+ return (t == null) ? UNSET_LABEL : Objects.toString(unwrap(t));
+ }
+
+ // Private methods
+
+ // This method is not annotated with @ForceInline as it is always called
+ // in a slow path.
+ private void preventReentry() {
+ if (Thread.holdsLock(this)) {
+ throw new IllegalStateException("Recursive initialization of a stable value is illegal");
+ }
+ }
+
+ /**
+ * Wraps the provided {@code newValue} and tries to set the contents.
+ *
+ * This method ensures the {@link Stable} field is written to at most once.
+ *
+ * @param newValue to wrap and set
+ * @return if the contents was set
+ */
+ @ForceInline
+ private boolean wrapAndSet(Object newValue) {
+ assert Thread.holdsLock(this);
+ // We know we hold the monitor here so plain semantic is enough
+ if (contents == null) {
+ UNSAFE.putReferenceRelease(this, CONTENT_OFFSET, wrap(newValue));
+ return true;
+ }
+ return false;
+ }
+
+
+ // Wraps `null` values into a sentinel value
+ @ForceInline
+ private static Object wrap(Object t) {
+ return (t == null) ? NULL_SENTINEL : t;
+ }
+
+ // Unwraps null sentinel values into `null`
+ @SuppressWarnings("unchecked")
+ @ForceInline
+ private static T unwrap(Object t) {
+ return t != NULL_SENTINEL ? (T) t : null;
+ }
+
+ // Factory
+
+ public static StableValueImpl of() {
+ return new StableValueImpl<>();
+ }
+
+}
diff --git a/test/jdk/java/lang/StableValue/StableFunctionTest.java b/test/jdk/java/lang/StableValue/StableFunctionTest.java
new file mode 100644
index 00000000000..4476c046957
--- /dev/null
+++ b/test/jdk/java/lang/StableValue/StableFunctionTest.java
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+ * @summary Basic tests for StableFunction methods
+ * @enablePreview
+ * @run junit StableFunctionTest
+ */
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Function;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+final class StableFunctionTest {
+
+ enum Value {
+ // Zero is here so that we have enums with ordinals before the first one
+ // actually used in input sets (i.e. ZERO is not in the input set)
+ ZERO(0),
+ ILLEGAL_BEFORE(-1),
+ // Valid values
+ THIRTEEN(13),
+ ILLEGAL_BETWEEN(-2),
+ FORTY_TWO(42),
+ // Illegal values (not in the input set)
+ ILLEGAL_AFTER(-3);
+
+ final int intValue;
+
+ Value(int intValue) {
+ this.intValue = intValue;
+ }
+
+ int asInt() {
+ return intValue;
+ }
+
+ }
+
+ private static final Function MAPPER = Value::asInt;
+
+ @ParameterizedTest
+ @MethodSource("allSets")
+ void factoryInvariants(Set inputs) {
+ assertThrows(NullPointerException.class, () -> StableValue.function(null, MAPPER));
+ assertThrows(NullPointerException.class, () -> StableValue.function(inputs, null));
+ }
+
+ @ParameterizedTest
+ @MethodSource("nonEmptySets")
+ void basic(Set inputs) {
+ basic(inputs, MAPPER);
+ toStringTest(inputs, MAPPER);
+ basic(inputs, _ -> null);
+ toStringTest(inputs, _ -> null);
+ }
+
+ void basic(Set inputs, Function mapper) {
+ StableTestUtil.CountingFunction cif = new StableTestUtil.CountingFunction<>(mapper);
+ var cached = StableValue.function(inputs, cif);
+ assertEquals(mapper.apply(Value.FORTY_TWO), cached.apply(Value.FORTY_TWO));
+ assertEquals(1, cif.cnt());
+ assertEquals(mapper.apply(Value.FORTY_TWO), cached.apply(Value.FORTY_TWO));
+ assertEquals(1, cif.cnt());
+ var x0 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_BEFORE));
+ assertEquals("Input not allowed: ILLEGAL_BEFORE", x0.getMessage());
+ var x1 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_BETWEEN));
+ assertEquals("Input not allowed: ILLEGAL_BETWEEN", x1.getMessage());
+ var x2 = assertThrows(IllegalArgumentException.class, () -> cached.apply(Value.ILLEGAL_AFTER));
+ assertEquals("Input not allowed: ILLEGAL_AFTER", x2.getMessage());
+ }
+
+ void toStringTest(Set inputs, Function mapper) {
+ var cached = StableValue.function(inputs, mapper);
+ cached.apply(Value.FORTY_TWO);
+ var toString = cached.toString();
+ assertTrue(toString.startsWith("{"));
+ // Key order is unspecified
+ assertTrue(toString.contains(Value.THIRTEEN + "=.unset"));
+ assertTrue(toString.contains(Value.FORTY_TWO + "=" + mapper.apply(Value.FORTY_TWO)));
+ assertTrue(toString.endsWith("}"));
+ // One between the values
+ assertEquals(1L, toString.chars().filter(ch -> ch == ',').count());
+ }
+
+ @ParameterizedTest
+ @MethodSource("emptySets")
+ void empty(Set inputs) {
+ Function f0 = StableValue.function(inputs, Value::asInt);
+ Function f1 = StableValue.function(inputs, Value::asInt);
+ assertEquals("{}", f0.toString());
+ assertThrows(NullPointerException.class, () -> f0.apply(null));
+ assertNotEquals(f0, f1);
+ assertNotEquals(null, f0);
+ }
+
+ @ParameterizedTest
+ @MethodSource("nonEmptySets")
+ void exception(Set inputs) {
+ StableTestUtil.CountingFunction cif = new StableTestUtil.CountingFunction<>(_ -> {
+ throw new UnsupportedOperationException();
+ });
+ var cached = StableValue.function(inputs, cif);
+ assertThrows(UnsupportedOperationException.class, () -> cached.apply(Value.FORTY_TWO));
+ assertEquals(1, cif.cnt());
+ assertThrows(UnsupportedOperationException.class, () -> cached.apply(Value.FORTY_TWO));
+ assertEquals(2, cif.cnt());
+ var toString = cached.toString();
+ assertTrue(toString.startsWith("{"));
+ // Key order is unspecified
+ assertTrue(toString.contains(Value.THIRTEEN + "=.unset"));
+ assertTrue(toString.contains(Value.FORTY_TWO + "=.unset"));
+ assertTrue(toString.endsWith("}"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("nonEmptySets")
+ void circular(Set inputs) {
+ final AtomicReference> ref = new AtomicReference<>();
+ Function> cached = StableValue.function(inputs, _ -> ref.get());
+ ref.set(cached);
+ cached.apply(Value.FORTY_TWO);
+ var toString = cached.toString();
+ assertTrue(toString.contains("FORTY_TWO=(this StableFunction)"), toString);
+ assertDoesNotThrow(cached::hashCode);
+ assertDoesNotThrow((() -> cached.equals(cached)));
+ }
+
+ @ParameterizedTest
+ @MethodSource("allSets")
+ void equality(Set inputs) {
+ Function mapper = Value::asInt;
+ Function f0 = StableValue.function(inputs, mapper);
+ Function f1 = StableValue.function(inputs, mapper);
+ // No function is equal to another function
+ assertNotEquals(f0, f1);
+ }
+
+ @ParameterizedTest
+ @MethodSource("allSets")
+ void hashCodeStable(Set inputs) {
+ Function f0 = StableValue.function(inputs, Value::asInt);
+ assertEquals(System.identityHashCode(f0), f0.hashCode());
+ if (!inputs.isEmpty()) {
+ f0.apply(Value.FORTY_TWO);
+ assertEquals(System.identityHashCode(f0), f0.hashCode());
+ }
+ }
+
+ @Test
+ void nullKeys() {
+ Set inputs = new HashSet<>();
+ inputs.add(Value.FORTY_TWO);
+ inputs.add(null);
+ assertThrows(NullPointerException.class, () -> StableValue.function(inputs, MAPPER));
+ }
+
+ @Test
+ void usesOptimizedVersion() {
+ Function enumFunction = StableValue.function(EnumSet.of(Value.FORTY_TWO), Value::asInt);
+ assertEquals("jdk.internal.lang.stable.StableEnumFunction", enumFunction.getClass().getName());
+ Function emptyFunction = StableValue.function(Set.of(), Value::asInt);
+ assertEquals("jdk.internal.lang.stable.StableFunction", emptyFunction.getClass().getName());
+ }
+
+ private static Stream> nonEmptySets() {
+ return Stream.of(
+ Set.of(Value.FORTY_TWO, Value.THIRTEEN),
+ linkedHashSet(Value.THIRTEEN, Value.FORTY_TWO),
+ treeSet(Value.FORTY_TWO, Value.THIRTEEN),
+ EnumSet.of(Value.FORTY_TWO, Value.THIRTEEN)
+ );
+ }
+
+ private static Stream> emptySets() {
+ return Stream.of(
+ Set.of(),
+ linkedHashSet(),
+ treeSet(),
+ EnumSet.noneOf(Value.class)
+ );
+ }
+
+ private static Stream> allSets() {
+ return Stream.concat(
+ nonEmptySets(),
+ emptySets()
+ );
+ }
+
+ static Set treeSet(Value... values) {
+ return populate(new TreeSet<>(Comparator.comparingInt(Value::asInt).reversed()),values);
+ }
+
+ static Set linkedHashSet(Value... values) {
+ return populate(new LinkedHashSet<>(), values);
+ }
+
+ static Set populate(Set set, Value... values) {
+ set.addAll(Arrays.asList(values));
+ return set;
+ }
+
+}
diff --git a/test/jdk/java/lang/StableValue/StableIntFunctionTest.java b/test/jdk/java/lang/StableValue/StableIntFunctionTest.java
new file mode 100644
index 00000000000..7397a688ee6
--- /dev/null
+++ b/test/jdk/java/lang/StableValue/StableIntFunctionTest.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+ * @summary Basic tests for StableIntFunction methods
+ * @enablePreview
+ * @run junit StableIntFunctionTest
+ */
+
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.IntFunction;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+final class StableIntFunctionTest {
+
+ private static final int SIZE = 2;
+ private static final IntFunction MAPPER = i -> i;
+
+ @Test
+ void factoryInvariants() {
+ assertThrows(IllegalArgumentException.class, () -> StableValue.intFunction(-1, MAPPER));
+ assertThrows(NullPointerException.class, () -> StableValue.intFunction(SIZE, null));
+ }
+
+ @Test
+ void basic() {
+ basic(MAPPER);
+ basic(i -> null);
+ }
+
+ void basic(IntFunction mapper) {
+ StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(mapper);
+ var cached = StableValue.intFunction(SIZE, cif);
+ assertEquals("[.unset, .unset]", cached.toString());
+ assertEquals(mapper.apply(1), cached.apply(1));
+ assertEquals(1, cif.cnt());
+ assertEquals(mapper.apply(1), cached.apply(1));
+ assertEquals(1, cif.cnt());
+ assertEquals("[.unset, " + mapper.apply(1) + "]", cached.toString());
+ assertThrows(IllegalArgumentException.class, () -> cached.apply(SIZE));
+ assertThrows(IllegalArgumentException.class, () -> cached.apply(-1));
+ assertThrows(IllegalArgumentException.class, () -> cached.apply(1_000_000));
+ }
+
+ @Test
+ void exception() {
+ StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(_ -> {
+ throw new UnsupportedOperationException();
+ });
+ var cached = StableValue.intFunction(SIZE, cif);
+ assertThrows(UnsupportedOperationException.class, () -> cached.apply(1));
+ assertEquals(1, cif.cnt());
+ assertThrows(UnsupportedOperationException.class, () -> cached.apply(1));
+ assertEquals(2, cif.cnt());
+ assertEquals("[.unset, .unset]", cached.toString());
+ }
+
+ @Test
+ void circular() {
+ final AtomicReference> ref = new AtomicReference<>();
+ IntFunction> cached = StableValue.intFunction(SIZE, _ -> ref.get());
+ ref.set(cached);
+ cached.apply(0);
+ String toString = cached.toString();
+ assertEquals("[(this StableIntFunction), .unset]", toString);
+ assertDoesNotThrow(cached::hashCode);
+ assertDoesNotThrow((() -> cached.equals(cached)));
+ }
+
+ @Test
+ void equality() {
+ IntFunction f0 = StableValue.intFunction(8, MAPPER);
+ IntFunction f1 = StableValue.intFunction(8, MAPPER);
+ // No function is equal to another function
+ assertNotEquals(f0, f1);
+ }
+
+ @Test
+ void hashCodeStable() {
+ IntFunction f0 = StableValue.intFunction(8, MAPPER);
+ assertEquals(System.identityHashCode(f0), f0.hashCode());
+ f0.apply(4);
+ assertEquals(System.identityHashCode(f0), f0.hashCode());
+ }
+
+}
diff --git a/test/jdk/java/lang/StableValue/StableListTest.java b/test/jdk/java/lang/StableValue/StableListTest.java
new file mode 100644
index 00000000000..f0ae081c76c
--- /dev/null
+++ b/test/jdk/java/lang/StableValue/StableListTest.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/* @test
+ * @summary Basic tests for StableList methods
+ * @modules java.base/jdk.internal.lang.stable
+ * @enablePreview
+ * @run junit StableListTest
+ */
+
+import jdk.internal.lang.stable.StableUtil;
+import jdk.internal.lang.stable.StableValueImpl;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
+
+import java.io.Serializable;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.IdentityHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.RandomAccess;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.IntFunction;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+final class StableListTest {
+
+ private static final int ZERO = 0;
+ private static final int INDEX = 7;
+ private static final int SIZE = 31;
+ private static final IntFunction IDENTITY = i -> i;
+
+ @Test
+ void factoryInvariants() {
+ assertThrows(NullPointerException.class, () -> StableValue.list(SIZE, null));
+ assertThrows(IllegalArgumentException.class, () -> StableValue.list(-1, IDENTITY));
+ }
+
+ @Test
+ void isEmpty() {
+ assertFalse(newList().isEmpty());
+ assertTrue(newEmptyList().isEmpty());
+ }
+
+ @Test
+ void size() {
+ assertEquals(SIZE, newList().size());
+ assertEquals(ZERO, newEmptyList().size());
+ }
+
+ @Test
+ void get() {
+ StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(IDENTITY);
+ var lazy = StableValue.list(SIZE, cif);
+ for (int i = 0; i < SIZE; i++) {
+ assertEquals(i, lazy.get(i));
+ assertEquals(i + 1, cif.cnt());
+ assertEquals(i, lazy.get(i));
+ assertEquals(i + 1, cif.cnt());
+ }
+ }
+
+ @Test
+ void getException() {
+ StableTestUtil.CountingIntFunction cif = new StableTestUtil.CountingIntFunction<>(_ -> {
+ throw new UnsupportedOperationException();
+ });
+ var lazy = StableValue.list(SIZE, cif);
+ assertThrows(UnsupportedOperationException.class, () -> lazy.get(INDEX));
+ assertEquals(1, cif.cnt());
+ assertThrows(UnsupportedOperationException.class, () -> lazy.get(INDEX));
+ assertEquals(2, cif.cnt());
+ }
+
+ @Test
+ void toArray() {
+ assertArrayEquals(new Object[ZERO], newEmptyList().toArray());
+ assertArrayEquals(newRegularList().toArray(), newList().toArray());
+ }
+
+ @Test
+ void toArrayWithArrayLarger() {
+ Integer[] actual = new Integer[SIZE];
+ for (int i = 0; i < SIZE; i++) {
+ actual[INDEX] = 100 + i;
+ }
+ var list = StableValue.list(INDEX, IDENTITY);
+ assertSame(actual, list.toArray(actual));
+ Integer[] expected = IntStream.range(0, SIZE)
+ .mapToObj(i -> i < INDEX ? i : null)
+ .toArray(Integer[]::new);
+ assertArrayEquals(expected, actual);
+ }
+
+ @Test
+ void toArrayWithArraySmaller() {
+ Integer[] arr = new Integer[INDEX];
+ Integer[] actual = newList().toArray(arr);
+ assertNotSame(arr, actual);
+ Integer[] expected = newRegularList().toArray(new Integer[0]);
+ assertArrayEquals(expected, actual);
+ }
+
+ @Test
+ void toArrayWithGenerator() {
+ Integer[] expected = newRegularList().toArray(Integer[]::new);
+ Integer[] actual = newList().toArray(Integer[]::new);
+ assertArrayEquals(expected, actual);
+ }
+
+ @Test
+ void firstIndex() {
+ var lazy = newList();
+ for (int i = INDEX; i < SIZE; i++) {
+ assertEquals(i, lazy.indexOf(i));
+ }
+ assertEquals(-1, lazy.indexOf(SIZE + 1));
+ }
+
+ @Test
+ void lastIndex() {
+ var lazy = newList();
+ for (int i = INDEX; i < SIZE; i++) {
+ assertEquals(i, lazy.lastIndexOf(i));
+ }
+ assertEquals(-1, lazy.lastIndexOf(SIZE + 1));
+ }
+
+ @Test
+ void toStringTest() {
+ assertEquals("[]", newEmptyList().toString());
+ var list = StableValue.list(2, IDENTITY);
+ assertEquals("[.unset, .unset]", list.toString());
+ list.get(0);
+ assertEquals("[0, .unset]", list.toString());
+ list.get(1);
+ assertEquals("[0, 1]", list.toString());
+ }
+
+ @Test
+ void hashCodeTest() {
+ assertEquals(List.of().hashCode(), newEmptyList().hashCode());
+ assertEquals(newRegularList().hashCode(), newList().hashCode());
+ }
+
+ @Test
+ void equalsTest() {
+ assertTrue(newEmptyList().equals(List.of()));
+ assertTrue(List.of().equals(newEmptyList()));
+ assertTrue(newList().equals(newRegularList()));
+ assertTrue(newRegularList().equals(newList()));
+ assertFalse(newList().equals("A"));
+ }
+
+ @Test
+ void equalsPartialEvaluationTest() {
+ var list = StableValue.list(2, IDENTITY);
+ assertFalse(list.equals(List.of(0)));
+ assertEquals("[0, .unset]", list.toString());
+ assertTrue(list.equals(List.of(0, 1)));
+ assertEquals("[0, 1]", list.toString());
+ }
+
+ @Test
+ void iteratorTotal() {
+ var iterator = newList().iterator();
+ for (int i = 0; i < SIZE; i++) {
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasNext());
+ assertEquals(i, iterator.next());
+ }
+ assertFalse(iterator.hasNext());
+ assertThrows(NoSuchElementException.class, iterator::next);
+ AtomicInteger cnt = new AtomicInteger();
+ iterator.forEachRemaining(_ -> cnt.incrementAndGet());
+ assertEquals(0, cnt.get());
+ }
+
+ @Test
+ void iteratorPartial() {
+ var iterator = newList().iterator();
+ for (int i = 0; i < INDEX; i++) {
+ assertTrue(iterator.hasNext());
+ assertTrue(iterator.hasNext());
+ assertEquals(i, iterator.next());
+ }
+ assertTrue(iterator.hasNext());
+ AtomicInteger cnt = new AtomicInteger();
+ iterator.forEachRemaining(_ -> cnt.incrementAndGet());
+ assertEquals(SIZE - INDEX, cnt.get());
+ assertFalse(iterator.hasNext());
+ assertThrows(NoSuchElementException.class, iterator::next);
+ }
+
+ @Test
+ void subList() {
+ var lazy = newList();
+ var lazySubList = lazy.subList(1, SIZE);
+ assertInstanceOf(RandomAccess.class, lazySubList);
+ var regularList = newRegularList();
+ var regularSubList = regularList.subList(1, SIZE);
+ assertEquals(regularSubList, lazySubList);
+ }
+
+ @Test
+ void subList2() {
+ var lazy = newList();
+ var lazySubList = lazy.subList(1, SIZE);
+ lazySubList.get(0);
+ var eq = newList();
+ eq.get(1);
+ assertEquals(eq.toString(), lazy.toString());
+ }
+
+ @Test
+ void subListToString() {
+ subListToString0(newList());
+ subListToString0(newList().subList(1, SIZE));
+ subListToString0(newList().subList(1, SIZE).subList(0, SIZE - 2));
+ }
+
+ void subListToString0(List subList) {
+ assertEquals(asString(".unset", subList), subList.toString());
+
+ var first = subList.getFirst();
+ assertEquals(asString(first.toString(), subList), subList.toString());
+ }
+
+ @Test
+ void reversed() {
+ var reversed = newList().reversed();
+ assertInstanceOf(RandomAccess.class, reversed);
+ assertEquals(SIZE - 1, reversed.getFirst());
+ assertEquals(0, reversed.getLast());
+
+ var reversed2 = reversed.reversed();
+ assertInstanceOf(RandomAccess.class, reversed2);
+ assertEquals(0, reversed2.getFirst());
+ assertEquals(SIZE - 1, reversed2.getLast());
+ // Make sure we get back a non-reversed implementation
+ assertEquals("java.util.ImmutableCollections$StableList", reversed2.getClass().getName());
+ }
+
+ @Test
+ void reversedToString() {
+ var reversed = newList().reversed();
+ subListToString0(reversed);
+ }
+
+ @Test
+ void subListReversedToString() {
+ var list = newList().subList(1, SIZE - 1).reversed();
+ // This combination is not lazy. There has to be a limit somewhere.
+ var regularList = newRegularList().subList(1, SIZE - 1).reversed();
+ assertEquals(regularList.toString(), list.toString());
+ }
+
+ @Test
+ void recursiveCall() {
+ AtomicReference> ref = new AtomicReference<>();
+ var lazy = StableValue.list(SIZE, i -> ref.get().apply(i));
+ ref.set(lazy::get);
+ var x = assertThrows(IllegalStateException.class, () -> lazy.get(INDEX));
+ assertEquals("Recursive initialization of a stable value is illegal", x.getMessage());
+ }
+
+ // Immutability
+
+ @ParameterizedTest
+ @MethodSource("unsupportedOperations")
+ void unsupported(Operation operation) {
+ assertThrowsForOperation(UnsupportedOperationException.class, operation);
+ }
+
+ // Method parameter invariant checking
+
+ @ParameterizedTest
+ @MethodSource("nullAverseOperations")
+ void nullAverse(Operation operation) {
+ assertThrowsForOperation(NullPointerException.class, operation);
+ }
+
+ @ParameterizedTest
+ @MethodSource("outOfBoundsOperations")
+ void outOfBounds(Operation operation) {
+ assertThrowsForOperation(IndexOutOfBoundsException.class, operation);
+ }
+
+ static void assertThrowsForOperation(Class expectedType, Operation operation) {
+ var lazy = newList();
+ assertThrows(expectedType, () -> operation.accept(lazy));
+ var sub = lazy.subList(1, SIZE / 2);
+ assertThrows(expectedType, () -> operation.accept(sub));
+ var subSub = sub.subList(1, sub.size() / 2);
+ assertThrows(expectedType, () -> operation.accept(subSub));
+ }
+
+ // Implementing interfaces
+
+ @Test
+ void serializable() {
+ serializable(newList());
+ serializable(newEmptyList());
+ }
+
+ void serializable(List list) {
+ assertFalse(list instanceof Serializable);
+ if (list.size()>INDEX) {
+ assertFalse(newList().subList(1, INDEX) instanceof Serializable);
+ }
+ assertFalse(list.iterator() instanceof Serializable);
+ assertFalse(list.reversed() instanceof Serializable);
+ assertFalse(list.spliterator() instanceof Serializable);
+ }
+
+ @Test
+ void randomAccess() {
+ assertInstanceOf(RandomAccess.class, newList());
+ assertInstanceOf(RandomAccess.class, newEmptyList());
+ assertInstanceOf(RandomAccess.class, newList().subList(1, INDEX));
+ }
+
+ @Test
+ void distinct() {
+ StableValueImpl[] array = StableUtil.array(SIZE);
+ assertEquals(SIZE, array.length);
+ // Check, every StableValue is distinct
+ Map, Boolean> idMap = new IdentityHashMap<>();
+ for (var e: array) {
+ idMap.put(e, true);
+ }
+ assertEquals(SIZE, idMap.size());
+ }
+
+ // Support constructs
+
+ record Operation(String name,
+ Consumer> consumer) implements Consumer> {
+ @Override public void accept(List list) { consumer.accept(list); }
+ @Override public String toString() { return name; }
+ }
+
+ static Stream nullAverseOperations() {
+ return Stream.of(
+ new Operation("forEach", l -> l.forEach(null)),
+ new Operation("containsAll", l -> l.containsAll(null)),
+ new Operation("toArray", l -> l.toArray((Integer[]) null)),
+ new Operation("toArray", l -> l.toArray((IntFunction) null))
+ );
+ }
+
+ static Stream outOfBoundsOperations() {
+ return Stream.of(
+ new Operation("get(-1)", l -> l.get(-1)),
+ new Operation("get(size)", l -> l.get(l.size())),
+ new Operation("sublist(-1,)", l -> l.subList(-1, INDEX)),
+ new Operation("sublist(,size)", l -> l.subList(0, l.size() + 1)),
+ new Operation("listIter(-1)", l -> l.listIterator(-1)),
+ new Operation("listIter(size)", l -> l.listIterator(l.size() + 1))
+ );
+ }
+
+ static Stream unsupportedOperations() {
+ final Set SET = Set.of(0, 1);
+ return Stream.of(
+ new Operation("add(0)", l -> l.add(0)),
+ new Operation("add(0, 1)", l -> l.add(0, 1)),
+ new Operation("addAll(col)", l -> l.addAll(SET)),
+ new Operation("addAll(1, coll)", l -> l.addAll(1, SET)),
+ new Operation("addFirst(0)", l -> l.addFirst(0)),
+ new Operation("addLast(0)", l -> l.addLast(0)),
+ new Operation("clear", List::clear),
+ new Operation("remove(Obj)", l -> l.remove((Object)1)),
+ new Operation("remove(1)", l -> l.remove(1)),
+ new Operation("removeAll", l -> l.removeAll(SET)),
+ new Operation("removeFirst", List::removeFirst),
+ new Operation("removeLast", List::removeLast),
+ new Operation("removeIf", l -> l.removeIf(i -> i % 2 == 0)),
+ new Operation("replaceAll", l -> l.replaceAll(i -> i + 1)),
+ new Operation("sort", l -> l.sort(Comparator.naturalOrder())),
+ new Operation("iterator().remove", l -> l.iterator().remove()),
+ new Operation("listIter().remove", l -> l.listIterator().remove()),
+ new Operation("listIter().add", l -> l.listIterator().add(1)),
+ new Operation("listIter().set", l -> l.listIterator().set(1))
+ );
+ }
+
+ static List newList() {
+ return StableValue.list(SIZE, IDENTITY);
+ }
+
+ static List newEmptyList() {
+ return StableValue.list(ZERO, IDENTITY);
+ }
+
+ static List newRegularList() {
+ return IntStream.range(0, SIZE).boxed().toList();
+ }
+
+ static String asString(String first, List