8351565: Implement JEP 502: Stable Values (Preview)

Co-authored-by: Maurizio Cimadamore <mcimadamore@openjdk.org>
Reviewed-by: vklang, jvernee, alanb, liach
This commit is contained in:
Per Minborg 2025-04-30 16:03:25 +00:00
parent 4c695fa8a4
commit fbc4691bfa
30 changed files with 4806 additions and 11 deletions

View File

@ -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.
* <p>
* A {@code StableValue<T>} is typically created using the factory method
* {@linkplain StableValue#of() {@code StableValue.of()}}. When created this way,
* the stable value is <em>unset</em>, which means it holds no <em>contents</em>.
* Its contents, of type {@code T}, can be <em>set</em> 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()}.
* <p>
* 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 <em>unset</em>, which means it holds no contents. Later in the example, the
* state of the "{@code logger}" field is checked and if it is still <em>unset</em>,
* the contents is <em>set</em>:
*
* {@snippet lang = java:
* public class Component {
*
* // Creates a new unset stable value with no contents
* // @link substring="of" target="#of" :
* private final StableValue<Logger> 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");
* // ...
* }
* }
*}
* <p>
* 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.
* <p>
* 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> logger = StableValue.of();
*
* private Logger getLogger() {
* return logger.orElseSet( () -> Logger.create(Component.class) );
* }
*
* public void process() {
* getLogger().info("Process started");
* // ...
* }
* }
*}
* <p>
* The {@code getLogger()} method calls {@code logger.orElseSet()} on the stable value to
* retrieve its contents. If the stable value is <em>unset</em>, 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 <em>set</em> before it returns.
* <p>
* 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.
*
* <h2 id="stable-functions">Stable Functions</h2>
* Stable values provide the foundation for higher-level functional abstractions. A
* <em>stable supplier</em> 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> 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()}.
* <p>
* A <em>stable int function</em> 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<Integer> UNDERLYING_POWER_OF_TWO =
* v -> 1 << v;
*
* private static final IntFunction<Integer> 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 <em>partial function</em> that only
* allows a subset {@code [0, 5]} of the underlying function's {@code UNDERLYING_POWER_OF_TWO}
* input range.
*
* <p>
* A <em>stable function</em> 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<Integer> KEYS =
* Set.of(1, 2, 4, 8, 16, 32);
* private static final UnaryOperator<Integer> UNDERLYING_LOG2 =
* i -> 31 - Integer.numberOfLeadingZeros(i);
*
* private static final Function<Integer, Integer> 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 <em>partial function</em> that only allows
* a subset {@code {1, 2, 4, 8, 16, 32}} of the underlying function's
* {@code UNDERLYING_LOG2} input range.
*
* <h2 id="stable-collections">Stable Collections</h2>
* Stable values can also be used as backing storage for
* {@linkplain Collection##unmodifiable unmodifiable collections}. A <em>stable list</em>
* 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<Integer> UNDERLYING_POWER_OF_TWO =
* v -> 1 << v;
*
* private static final List<Integer> 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
*
* }
* <p>
* Similarly, a <em>stable map</em> 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<Integer> KEYS =
* Set.of(1, 2, 4, 8, 16, 32);
* private static final UnaryOperator<Integer> UNDERLYING_LOG2 =
* i -> 31 - Integer.numberOfLeadingZeros(i);
*
* private static final Map<Integer, INTEGER> 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
*
*}
*
* <h2 id="composition">Composing stable values</h2>
* 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> FOO = StableValue.supplier(Foo::new);
* private static final Supplier<Bar> 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.
* <p>
* 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<Integer> 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)}.
* <p>
* 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.
*
* <h2 id="thread-safety">Thread Safety</h2>
* 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.
* <p>
* The at-most-once write operation on a stable value that succeeds
* (e.g. {@linkplain #trySet(Object) trySet()})
* {@linkplain java.util.concurrent##MemoryVisibility <em>happens-before</em>}
* any successful read operation (e.g. {@linkplain #orElseThrow()}).
* A successful write operation can be either:
* <ul>
* <li>a {@link #trySet(Object)} that returns {@code true},</li>
* <li>a {@link #setOrThrow(Object)} that does not throw, or</li>
* <li>an {@link #orElseSet(Supplier)} that successfully runs the supplier</li>
* </ul>
* A successful read operation can be either:
* <ul>
* <li>a {@link #orElseThrow()} that does not throw,</li>
* <li>a {@link #orElse(Object) orElse(other)} that does not return the {@code other} value</li>
* <li>an {@link #orElseSet(Supplier)} that does not {@code throw}, or</li>
* <li>an {@link #isSet()} that returns {@code true}</li>
* </ul>
* <p>
* 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.
*
* <h2 id="performance">Performance</h2>
* 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.
* <p>
* 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 <em>non-null</em> 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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
* <em>array reference</em> as a stable value but <em>not its components</em>.
* 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.
* <p>
* Stable values, functions, and collections are not {@link Serializable}.
*
* @param <T> type of the contents
*
* @since 25
*/
@PreviewFeature(feature = PreviewFeature.Feature.STABLE_VALUES)
public sealed interface StableValue<T>
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.
* <p>
* 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}}
* <p>
* 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.
* <p>
* 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);
* }
* <p>
* When this method returns successfully, the contents is always set.
* <p>
* 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}.
* <p>
* 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}
* <p>
* An unset stable value has no contents.
*
* @param <T> type of the contents
*/
static <T> StableValue<T> of() {
return StableValueImpl.of();
}
/**
* {@return a new pre-set stable value with the provided {@code contents}}
*
* @param contents to set
* @param <T> type of the contents
*/
static <T> StableValue<T> of(T contents) {
final StableValue<T> stableValue = StableValue.of();
stableValue.trySet(contents);
return stableValue;
}
/**
* {@return a new stable supplier}
* <p>
* 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.
* <p>
* 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.
* <p>
* If the provided {@code underlying} supplier throws an exception, it is rethrown
* to the initial caller and no contents is recorded.
* <p>
* 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 <T> the type of results supplied by the returned supplier
*/
static <T> Supplier<T> supplier(Supplier<? extends T> underlying) {
Objects.requireNonNull(underlying);
return StableSupplier.of(underlying);
}
/**
* {@return a new stable {@linkplain IntFunction}}
* <p>
* 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.
* <p>
* 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.
* <p>
* If invoking the provided {@code underlying} function throws an exception, it is
* rethrown to the initial caller and no contents is recorded.
* <p>
* 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 <R> the type of results delivered by the returned IntFunction
* @throws IllegalArgumentException if the provided {@code size} is negative.
*/
static <R> IntFunction<R> intFunction(int size,
IntFunction<? extends R> underlying) {
StableUtil.assertSizeNonNegative(size);
Objects.requireNonNull(underlying);
return StableIntFunction.of(size, underlying);
}
/**
* {@return a new stable {@linkplain Function}}
* <p>
* 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.
* <p>
* 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.
* <p>
* If invoking the provided {@code underlying} function throws an exception, it is
* rethrown to the initial caller and no contents is recorded.
* <p>
* 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 <T> the type of the input to the returned Function
* @param <R> the type of results delivered by the returned Function
* @throws NullPointerException if the provided set of {@code inputs} contains a
* {@code null} element.
*/
static <T, R> Function<T, R> 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}}
* <p>
* 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}).
* <p>
* 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.
* <p>
* 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.
* <p>
* Any direct {@link List#subList(int, int) subList} or {@link List#reversed()} views
* of the returned list are also stable.
* <p>
* The returned list and its {@link List#subList(int, int) subList} or
* {@link List#reversed()} views implement the {@link RandomAccess} interface.
* <p>
* The returned list is unmodifiable and does not implement the
* {@linkplain Collection##optional-operation optional operations} in the
* {@linkplain List} interface.
* <p>
* 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 <E> the type of elements in the returned list
* @throws IllegalArgumentException if the provided {@code size} is negative.
*/
static <E> List<E> 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}}
* <p>
* 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}).
* <p>
* 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.
* <p>
* 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.
* <p>
* Any direct {@link Map#values()} or {@link Map#entrySet()} views
* of the returned map are also stable.
* <p>
* The returned map is unmodifiable and does not implement the
* {@linkplain Collection##optional-operations optional operations} in the
* {@linkplain Map} interface.
* <p>
* 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 <K> the type of keys maintained by the returned map
* @param <V> the type of mapped values in the returned map
* @throws NullPointerException if the provided set of {@code inputs} contains a
* {@code null} element.
*/
static <K, V> Map<K, V> map(Set<K> 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);
}
}

View File

@ -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.
*
* <p>Certain methods are specified to be
* <p><a id="optional-operations"></a>Certain methods are specified to be
* <i>optional</i>. 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

View File

@ -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 <E> List<E> listFromTrustedArrayNullsAllowed(Object[] array) {
return ImmutableCollections.listFromTrustedArrayNullsAllowed(array);
}
public <E> List<E> stableList(int size, IntFunction<? extends E> mapper) {
return ImmutableCollections.stableList(size, mapper);
}
public <K, V> Map<K, V> stableMap(Set<K> keys, Function<? super K, ? extends V> mapper) {
return new StableMap<>(keys, mapper);
}
});
}
}
@ -250,6 +264,11 @@ class ImmutableCollections {
}
}
static <E> List<E> 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<E> 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<E>;
}
@Override
@ -551,6 +571,15 @@ class ImmutableCollections {
}
return array;
}
@Override
public String toString() {
if (root instanceof StableList<E> 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<E> extends AbstractImmutableList<E> {
@Stable
private final IntFunction<? extends E> mapper;
@Stable
final StableValueImpl<E>[] 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<E> delegate;
try {
delegate = delegates[i];
} catch (ArrayIndexOutOfBoundsException aioobe) {
throw new IndexOutOfBoundsException(i);
}
return delegate.orElseSet(new Supplier<E>() {
@Override public E get() { return mapper.apply(i); }});
}
@Override
@SuppressWarnings("unchecked")
public <T> 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> 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<E> reversed() {
return new StableReverseOrderListView<>(this);
}
@Override
public String toString() {
return StableUtil.renderElements(this, "StableList", delegates);
}
private static final class StableReverseOrderListView<E> extends ReverseOrderListView.Rand<E> {
private StableReverseOrderListView(List<E> base) {
super(base, false);
}
// This method does not evaluate the elements
@Override
public String toString() {
final StableValueImpl<E>[] delegates = ((StableList<E>)base).delegates;
final StableValueImpl<E>[] reversed = ArraysSupport.reverse(
Arrays.copyOf(delegates, delegates.length));
return StableUtil.renderElements(base, "Collection", reversed);
}
@Override
public List<E> 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<K,V> extends AbstractMap<K,V> implements Serializable {
abstract static class AbstractImmutableMap<K,V> extends AbstractMap<K,V> {
@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<K,V> extends AbstractImmutableMap<K,V> {
static final class Map1<K,V> extends AbstractImmutableMap<K,V> implements Serializable {
@Stable
private final K k0;
@Stable
@ -1215,7 +1354,7 @@ class ImmutableCollections {
* @param <V> the value type
*/
// Not a jdk.internal.ValueBased class; disqualified by fields in superclass AbstractMap
static final class MapN<K,V> extends AbstractImmutableMap<K,V> {
static final class MapN<K,V> extends AbstractImmutableMap<K,V> 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<K, V>
extends AbstractImmutableMap<K, V> {
@Stable
private final Function<? super K, ? extends V> mapper;
@Stable
private final Map<K, StableValueImpl<V>> delegate;
StableMap(Set<K> 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<Map.Entry<K, V>> 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<V> stable = delegate.get(key);
if (stable == null) {
return defaultValue;
}
@SuppressWarnings("unchecked")
final K k = (K) key;
return stable.orElseSet(new Supplier<V>() {
@Override public V get() { return mapper.apply(k); }});
}
@jdk.internal.ValueBased
final class StableMapEntrySet extends AbstractImmutableSet<Map.Entry<K, V>> {
@Stable
private final Set<Map.Entry<K, StableValueImpl<V>>> delegateEntrySet;
StableMapEntrySet() {
this.delegateEntrySet = delegate.entrySet();
}
@Override public Iterator<Map.Entry<K, V>> 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<Map.Entry<K, V>> {
@Stable
private final Iterator<Map.Entry<K, StableValueImpl<V>>> delegateIterator;
LazyMapIterator() {
this.delegateIterator = delegateEntrySet.iterator();
}
@Override public boolean hasNext() { return delegateIterator.hasNext(); }
@Override
public Entry<K, V> next() {
final Map.Entry<K, StableValueImpl<V>> inner = delegateIterator.next();
final K k = inner.getKey();
return new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier<V>() {
@Override public V get() { return mapper.apply(k); }}));
}
@Override
public void forEachRemaining(Consumer<? super Map.Entry<K, V>> action) {
final Consumer<? super Map.Entry<K, StableValueImpl<V>>> innerAction =
new Consumer<>() {
@Override
public void accept(Entry<K, StableValueImpl<V>> inner) {
final K k = inner.getKey();
action.accept(new NullableKeyValueHolder<>(k, inner.getValue().orElseSet(new Supplier<V>() {
@Override public V get() { return mapper.apply(k); }})));
}
};
delegateIterator.forEachRemaining(innerAction);
}
}
}
@Override
public Collection<V> values() {
return new StableMapValues();
}
final class StableMapValues extends AbstractImmutableCollection<V> {
@Override public Iterator<V> 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<StableValueImpl<?>[]> GENERATOR = new IntFunction<StableValueImpl<?>[]>() {
@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 ----------

View File

@ -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 {
<E> List<E> listFromTrustedArray(Object[] array);
<E> List<E> listFromTrustedArrayNullsAllowed(Object[] array);
<E> List<E> stableList(int size, IntFunction<? extends E> mapper);
<K, V> Map<K, V> stableMap(Set<K> keys, Function<? super K, ? extends V> mapper);
}

View File

@ -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.

View File

@ -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 <E> the type of the input to the function
* @param <R> the type of the result of the function
*/
public record StableEnumFunction<E extends Enum<E>, R>(Class<E> enumType,
int firstOrdinal,
IntPredicate member,
@Stable StableValueImpl<R>[] delegates,
Function<? super E, ? extends R> original) implements Function<E, R> {
@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<R> delegate;
// Since we did the member.test above, we know the index is in bounds
delegate = delegates[index];
return delegate.orElseSet(new Supplier<R>() {
@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<Map.Entry<E, StableValueImpl<R>>> 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 <T, E extends Enum<E>, R> Function<T, R> of(Set<? extends T> inputs,
Function<? super T, ? extends R> original) {
// The input set is not empty
final Class<E> enumType = (Class<E>)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<T, R>) new StableEnumFunction<E, R>(enumType, min, member, StableUtil.array(size), (Function<E, R>) original);
}
}

View File

@ -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 <T> the type of the input to the function
* @param <R> the type of the result of the function
*/
public record StableFunction<T, R>(Map<? extends T, StableValueImpl<R>> values,
Function<? super T, ? extends R> original) implements Function<T, R> {
@ForceInline
@Override
public R apply(T value) {
final StableValueImpl<R> stable = values.get(value);
if (stable == null) {
throw new IllegalArgumentException("Input not allowed: " + value);
}
return stable.orElseSet(new Supplier<R>() {
@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 <T, R> StableFunction<T, R> of(Set<? extends T> inputs,
Function<? super T, ? extends R> original) {
return new StableFunction<>(StableUtil.map(inputs), original);
}
}

View File

@ -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.
* <p>
* 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 <R> the return type
*/
public record StableIntFunction<R>(@Stable StableValueImpl<R>[] delegates,
IntFunction<? extends R> original) implements IntFunction<R> {
@ForceInline
@Override
public R apply(int index) {
final StableValueImpl<R> delegate;
try {
delegate = delegates[index];
} catch (ArrayIndexOutOfBoundsException ioob) {
throw new IllegalArgumentException("Input not allowed: " + index, ioob);
}
return delegate.orElseSet(new Supplier<R>() {
@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 <R> StableIntFunction<R> of(int size, IntFunction<? extends R> original) {
return new StableIntFunction<>(StableUtil.array(size), original);
}
}

View File

@ -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.
* <p>
* @implNote This implementation can be used early in the boot sequence as it does not
* rely on reflection, MethodHandles, Streams etc.
*
* @param <T> the return type
*/
public record StableSupplier<T>(StableValueImpl<T> delegate,
Supplier<? extends T> original) implements Supplier<T> {
@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 <T> StableSupplier<T> of(Supplier<? extends T> original) {
return new StableSupplier<>(StableValueImpl.of(), original);
}
}

View File

@ -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 <R> String renderElements(Object self,
String selfName,
StableValueImpl<?>[] delegates) {
return renderElements(self, selfName, delegates, 0, delegates.length);
}
public static <R> 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 <K, V> String renderMappings(Object self,
String selfName,
Iterable<Map.Entry<K, StableValueImpl<V>>> 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 <T> StableValueImpl<T>[] array(int size) {
assertSizeNonNegative(size);
@SuppressWarnings("unchecked")
final var stableValues = (StableValueImpl<T>[]) new StableValueImpl<?>[size];
for (int i = 0; i < size; i++) {
stableValues[i] = StableValueImpl.of();
}
return stableValues;
}
public static <K, T> Map<K, StableValueImpl<T>> map(Set<K> keys) {
Objects.requireNonNull(keys);
@SuppressWarnings("unchecked")
final var entries = (Map.Entry<K, StableValueImpl<T>>[]) 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);
}
}
}

View File

@ -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 <T> type of the contents
*/
public final class StableValueImpl<T> implements StableValue<T> {
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.
* <p>
* 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> T unwrap(Object t) {
return t != NULL_SENTINEL ? (T) t : null;
}
// Factory
public static <T> StableValueImpl<T> of() {
return new StableValueImpl<>();
}
}

View File

@ -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<Value, Integer> MAPPER = Value::asInt;
@ParameterizedTest
@MethodSource("allSets")
void factoryInvariants(Set<Value> inputs) {
assertThrows(NullPointerException.class, () -> StableValue.function(null, MAPPER));
assertThrows(NullPointerException.class, () -> StableValue.function(inputs, null));
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void basic(Set<Value> inputs) {
basic(inputs, MAPPER);
toStringTest(inputs, MAPPER);
basic(inputs, _ -> null);
toStringTest(inputs, _ -> null);
}
void basic(Set<Value> inputs, Function<Value, Integer> mapper) {
StableTestUtil.CountingFunction<Value, Integer> 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<Value> inputs, Function<Value, Integer> 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<Value> inputs) {
Function<Value, Integer> f0 = StableValue.function(inputs, Value::asInt);
Function<Value, Integer> 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<Value> inputs) {
StableTestUtil.CountingFunction<Value, Integer> 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<Value> inputs) {
final AtomicReference<Function<?, ?>> ref = new AtomicReference<>();
Function<Value, 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<Value> inputs) {
Function<Value, Integer> mapper = Value::asInt;
Function<Value, Integer> f0 = StableValue.function(inputs, mapper);
Function<Value, Integer> f1 = StableValue.function(inputs, mapper);
// No function is equal to another function
assertNotEquals(f0, f1);
}
@ParameterizedTest
@MethodSource("allSets")
void hashCodeStable(Set<Value> inputs) {
Function<Value, Integer> 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<Value> inputs = new HashSet<>();
inputs.add(Value.FORTY_TWO);
inputs.add(null);
assertThrows(NullPointerException.class, () -> StableValue.function(inputs, MAPPER));
}
@Test
void usesOptimizedVersion() {
Function<Value, Integer> enumFunction = StableValue.function(EnumSet.of(Value.FORTY_TWO), Value::asInt);
assertEquals("jdk.internal.lang.stable.StableEnumFunction", enumFunction.getClass().getName());
Function<Value, Integer> emptyFunction = StableValue.function(Set.of(), Value::asInt);
assertEquals("jdk.internal.lang.stable.StableFunction", emptyFunction.getClass().getName());
}
private static Stream<Set<Value>> 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<Set<Value>> emptySets() {
return Stream.of(
Set.of(),
linkedHashSet(),
treeSet(),
EnumSet.noneOf(Value.class)
);
}
private static Stream<Set<Value>> allSets() {
return Stream.concat(
nonEmptySets(),
emptySets()
);
}
static Set<Value> treeSet(Value... values) {
return populate(new TreeSet<>(Comparator.comparingInt(Value::asInt).reversed()),values);
}
static Set<Value> linkedHashSet(Value... values) {
return populate(new LinkedHashSet<>(), values);
}
static Set<Value> populate(Set<Value> set, Value... values) {
set.addAll(Arrays.asList(values));
return set;
}
}

View File

@ -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<Integer> 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<Integer> mapper) {
StableTestUtil.CountingIntFunction<Integer> 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<Integer> 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<IntFunction<?>> ref = new AtomicReference<>();
IntFunction<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<Integer> f0 = StableValue.intFunction(8, MAPPER);
IntFunction<Integer> f1 = StableValue.intFunction(8, MAPPER);
// No function is equal to another function
assertNotEquals(f0, f1);
}
@Test
void hashCodeStable() {
IntFunction<Integer> f0 = StableValue.intFunction(8, MAPPER);
assertEquals(System.identityHashCode(f0), f0.hashCode());
f0.apply(4);
assertEquals(System.identityHashCode(f0), f0.hashCode());
}
}

View File

@ -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<Integer> 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<Integer> 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<Integer> 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<Integer> 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<IntFunction<Integer>> 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 <T extends Throwable> void assertThrowsForOperation(Class<T> 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<Integer> 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<Integer>[] array = StableUtil.array(SIZE);
assertEquals(SIZE, array.length);
// Check, every StableValue is distinct
Map<StableValue<Integer>, Boolean> idMap = new IdentityHashMap<>();
for (var e: array) {
idMap.put(e, true);
}
assertEquals(SIZE, idMap.size());
}
// Support constructs
record Operation(String name,
Consumer<List<Integer>> consumer) implements Consumer<List<Integer>> {
@Override public void accept(List<Integer> list) { consumer.accept(list); }
@Override public String toString() { return name; }
}
static Stream<Operation> 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<Integer[]>) null))
);
}
static Stream<Operation> 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<Operation> unsupportedOperations() {
final Set<Integer> 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<Integer> newList() {
return StableValue.list(SIZE, IDENTITY);
}
static List<Integer> newEmptyList() {
return StableValue.list(ZERO, IDENTITY);
}
static List<Integer> newRegularList() {
return IntStream.range(0, SIZE).boxed().toList();
}
static String asString(String first, List<Integer> list) {
return "[" + first + ", " + Stream.generate(() -> ".unset")
.limit(list.size() - 1)
.collect(Collectors.joining(", ")) + "]";
}
}

View File

@ -0,0 +1,357 @@
/*
* 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 StableMap methods
* @modules java.base/jdk.internal.lang.stable
* @enablePreview
* @run junit StableMapTest
*/
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.AbstractMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
final class StableMapTest {
private static final int NOT_PRESENT = 147;
private static final int KEY = 7;
private static final Set<Integer> KEYS = Set.of(0, KEY, 13);
private static final Set<Integer> EMPTY = Set.of();
private static final Function<Integer, Integer> IDENTITY = Function.identity();
@Test
void factoryInvariants() {
assertThrows(NullPointerException.class, () -> StableValue.map(KEYS, null));
assertThrows(NullPointerException.class, () -> StableValue.map(null, IDENTITY));
}
@Test
void isEmpty() {
assertFalse(newMap().isEmpty());
assertTrue(newEmptyMap().isEmpty());
}
@Test
void size() {
assertEquals(KEYS.size(), newMap().size());
assertEquals(EMPTY.size(), newEmptyMap().size());
}
@Test
void get() {
StableTestUtil.CountingFunction<Integer, Integer> cf = new StableTestUtil.CountingFunction<>(IDENTITY);
var lazy = StableValue.map(KEYS, cf);
int cnt = 1;
for (int i : KEYS) {
assertEquals(i, lazy.get(i));
assertEquals(cnt, cf.cnt());
assertEquals(i, lazy.get(i));
assertEquals(cnt++, cf.cnt());
}
assertNull(lazy.get(NOT_PRESENT));
}
@Test
void getException() {
StableTestUtil.CountingFunction<Integer, Integer> cf = new StableTestUtil.CountingFunction<>(_ -> {
throw new UnsupportedOperationException();
});
var lazy = StableValue.map(KEYS, cf);
assertThrows(UnsupportedOperationException.class, () -> lazy.get(KEY));
assertEquals(1, cf.cnt());
assertThrows(UnsupportedOperationException.class, () -> lazy.get(KEY));
assertEquals(2, cf.cnt());
}
@Test
void containsKey() {
var lazy = newMap();
for (int i : KEYS) {
assertTrue(lazy.containsKey(i));
}
assertFalse(lazy.containsKey(NOT_PRESENT));
}
@Test
void containsValue() {
var lazy = newMap();
for (int i : KEYS) {
assertTrue(lazy.containsValue(i));
}
assertFalse(lazy.containsValue(NOT_PRESENT));
}
@Test
void forEach() {
var lazy = newMap();
Set<Map.Entry<Integer, Integer>> expected = KEYS.stream()
.map(i -> new AbstractMap.SimpleImmutableEntry<>(i , i))
.collect(Collectors.toSet());
Set<Map.Entry<Integer, Integer>> actual = new HashSet<>();
lazy.forEach((k, v) -> actual.add(new AbstractMap.SimpleImmutableEntry<>(k , v)));
assertEquals(expected, actual);
}
@Test
void toStringTest() {
assertEquals("{}", newEmptyMap().toString());
var map = StableValue.map(Set.of(KEY), IDENTITY);
assertEquals("{" + KEY + "=.unset}", map.toString());
map.get(KEY);
assertEquals("{" + KEY + "=" + KEY + "}", map.toString());
String actual = newMap().toString();
assertTrue(actual.startsWith("{"));
for (int key : KEYS) {
assertTrue(actual.contains(key + "=.unset"));
}
assertTrue(actual.endsWith("}"));
}
@Test
void hashCodeTest() {
assertEquals(Map.of().hashCode(), newEmptyMap().hashCode());
assertEquals(newRegularMap().hashCode(), newMap().hashCode());
}
@Test
void equalsTest() {
assertTrue(newEmptyMap().equals(Map.of()));
assertTrue(Map.of().equals(newEmptyMap()));
assertTrue(newMap().equals(newRegularMap()));
assertTrue(newRegularMap().equals(newMap()));
assertFalse(newMap().equals("A"));
}
@Test
void entrySet() {
var regular = newRegularMap().entrySet();
var actual = newMap().entrySet();
assertTrue(regular.equals(actual));
assertTrue(actual.equals(regular));
assertTrue(regular.equals(actual));
}
@Test
void entrySetToString() {
var map = newMap();
var entrySet = map.entrySet();
var toString = entrySet.toString();
for (var key : KEYS) {
assertTrue(toString.contains(key + "=.unset"));
}
assertTrue(toString.startsWith("["));
assertTrue(toString.endsWith("]"));
map.get(KEY);
for (var key : KEYS) {
if (key.equals(KEY)) {
continue;
}
assertTrue(entrySet.toString().contains(key + "=.unset"));
}
assertTrue(entrySet.toString().contains(KEY + "=" + KEY));
}
@Test
void values() {
var map = newMap();
var values = map.values();
// Look at one of the elements
var val = values.stream().iterator().next();
var toString = map.toString();
for (var key : KEYS) {
if (key.equals(val)) {
assertTrue(toString.contains(key + "=" + key));
} else {
assertTrue(toString.contains(key + "=.unset"));
}
}
// Mod ops
assertThrows(UnsupportedOperationException.class, () -> values.remove(KEY));
assertThrows(UnsupportedOperationException.class, () -> values.add(KEY));
assertThrows(UnsupportedOperationException.class, values::clear);
assertThrows(UnsupportedOperationException.class, () -> values.addAll(Set.of(1)));
assertThrows(UnsupportedOperationException.class, () -> values.removeIf(i -> true));
assertThrows(UnsupportedOperationException.class, () -> values.retainAll(Set.of(KEY)));
}
@Test
void valuesToString() {
var map = newMap();
var values = map.values();
assertEquals("[.unset, .unset, .unset]", values.toString());
map.get(KEY);
var afterGet = values.toString();
assertTrue(afterGet.contains(Integer.toString(KEY)), afterGet);
}
@Test
void iteratorNext() {
Set<Integer> encountered = new HashSet<>();
var iterator = newMap().entrySet().iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
assertEquals(entry.getKey(), entry.getValue());
encountered.add(entry.getValue());
}
assertEquals(KEYS, encountered);
}
@Test
void iteratorForEachRemaining() {
Set<Integer> encountered = new HashSet<>();
var iterator = newMap().entrySet().iterator();
var entry = iterator.next();
assertEquals(entry.getKey(), entry.getValue());
encountered.add(entry.getValue());
iterator.forEachRemaining(e -> {
assertEquals(e.getKey(), e.getValue());
encountered.add(e.getValue());
});
assertEquals(KEYS, encountered);
}
// 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);
}
static <T extends Throwable> void assertThrowsForOperation(Class<T> expectedType, Operation operation) {
var lazy = newMap();
assertThrows(expectedType, () -> operation.accept(lazy));
}
// Implementing interfaces
@Test
void serializable() {
serializable(newMap());
serializable(newEmptyMap());
}
void serializable(Map<Integer, Integer> map) {
assertFalse(map instanceof Serializable);
assertFalse(map.entrySet() instanceof Serializable);
assertFalse(map.keySet() instanceof Serializable);
assertFalse(map.values() instanceof Serializable);
}
@Test
void distinct() {
Map<Integer, StableValueImpl<Integer>> map = StableUtil.map(Set.of(1, 2, 3));
assertEquals(3, map.size());
// Check, every StableValue is distinct
Map<StableValue<Integer>, Boolean> idMap = new IdentityHashMap<>();
map.forEach((k, v) -> idMap.put(v, true));
assertEquals(3, idMap.size());
}
@Test
void nullResult() {
var map = StableValue.map(Set.of(0), _ -> null);
assertNull(map.getOrDefault(0, 1));;
assertTrue(map.containsKey(0));
assertNull(map.get(0));
}
@Test
void nullKeys() {
Set<Integer> inputs = new HashSet<>();
inputs.add(0);
inputs.add(null);
assertThrows(NullPointerException.class, () -> StableValue.map(inputs, IDENTITY));
}
// Support constructs
record Operation(String name,
Consumer<Map<Integer, Integer>> consumer) implements Consumer<Map<Integer, Integer>> {
@java.lang.Override
public void accept(Map<Integer, Integer> map) { consumer.accept(map); }
@java.lang.Override
public String toString() { return name; }
}
static Stream<Operation> nullAverseOperations() {
return Stream.of(
new Operation("forEach", m -> m.forEach(null))
);
}
static Stream<Operation> unsupportedOperations() {
return Stream.of(
new Operation("clear", Map::clear),
new Operation("compute", m -> m.compute(KEY, (_, _) -> 1)),
new Operation("computeIfAbsent", m -> m.computeIfAbsent(KEY, _ -> 1)),
new Operation("computeIfPresent", m -> m.computeIfPresent(KEY, (_, _) -> 1)),
new Operation("merge", m -> m.merge(KEY, KEY, (a, _) -> a)),
new Operation("put", m -> m.put(0, 0)),
new Operation("putAll", m -> m.putAll(Map.of())),
new Operation("remove1", m -> m.remove(KEY)),
new Operation("remove2", m -> m.remove(KEY, KEY)),
new Operation("replace2", m -> m.replace(KEY, 1)),
new Operation("replace3", m -> m.replace(KEY, KEY, 1)),
new Operation("replaceAll", m -> m.replaceAll((a, _) -> a))
);
}
static Map<Integer, Integer> newMap() {
return StableValue.map(KEYS, IDENTITY);
}
static Map<Integer, Integer> newEmptyMap() {
return StableValue.map(EMPTY, IDENTITY);
}
static Map<Integer, Integer> newRegularMap() {
return KEYS.stream().collect(Collectors.toMap(IDENTITY, IDENTITY));
}
}

View File

@ -0,0 +1,104 @@
/*
* 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 StableSupplier methods
* @enablePreview
* @run junit StableSupplierTest
*/
import org.junit.jupiter.api.Test;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
import static org.junit.jupiter.api.Assertions.*;
final class StableSupplierTest {
private static final Supplier<Integer> SUPPLIER = () -> 42;
@Test
void factoryInvariants() {
assertThrows(NullPointerException.class, () -> StableValue.supplier(null));
}
@Test
void basic() {
basic(SUPPLIER);
basic(() -> null);
}
void basic(Supplier<Integer> supplier) {
StableTestUtil.CountingSupplier<Integer> cs = new StableTestUtil.CountingSupplier<>(supplier);
var cached = StableValue.supplier(cs);
assertEquals(".unset", cached.toString());
assertEquals(supplier.get(), cached.get());
assertEquals(1, cs.cnt());
assertEquals(supplier.get(), cached.get());
assertEquals(1, cs.cnt());
assertEquals(Objects.toString(supplier.get()), cached.toString());
}
@Test
void exception() {
StableTestUtil.CountingSupplier<Integer> cs = new StableTestUtil.CountingSupplier<>(() -> {
throw new UnsupportedOperationException();
});
var cached = StableValue.supplier(cs);
assertThrows(UnsupportedOperationException.class, cached::get);
assertEquals(1, cs.cnt());
assertThrows(UnsupportedOperationException.class, cached::get);
assertEquals(2, cs.cnt());
assertEquals(".unset", cached.toString());
}
@Test
void circular() {
final AtomicReference<Supplier<?>> ref = new AtomicReference<>();
Supplier<Supplier<?>> cached = StableValue.supplier(ref::get);
ref.set(cached);
cached.get();
String toString = cached.toString();
assertTrue(toString.startsWith("(this StableSupplier)"));
assertDoesNotThrow(cached::hashCode);
}
@Test
void equality() {
Supplier<Integer> f0 = StableValue.supplier(SUPPLIER);
Supplier<Integer> f1 = StableValue.supplier(SUPPLIER);
// No function is equal to another function
assertNotEquals(f0, f1);
}
@Test
void hashCodeStable() {
Supplier<Integer> f0 = StableValue.supplier(SUPPLIER);
assertEquals(System.identityHashCode(f0), f0.hashCode());
f0.get();
assertEquals(System.identityHashCode(f0), f0.hashCode());
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.
*/
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiFunction;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Supplier;
final class StableTestUtil {
private StableTestUtil() {}
public static final class CountingSupplier<T>
extends AbstractCounting<Supplier<T>>
implements Supplier<T> {
public CountingSupplier(Supplier<T> delegate) {
super(delegate);
}
@Override
public T get() {
incrementCounter();
return delegate.get();
}
}
public static final class CountingIntFunction<R>
extends AbstractCounting<IntFunction<R>>
implements IntFunction<R> {
public CountingIntFunction(IntFunction<R> delegate) {
super(delegate);
}
@Override
public R apply(int value) {
incrementCounter();
return delegate.apply(value);
}
}
public static final class CountingFunction<T, R>
extends AbstractCounting<Function<T, R>>
implements Function<T, R> {
public CountingFunction(Function<T, R> delegate) {
super(delegate);
}
@Override
public R apply(T t) {
incrementCounter();
return delegate.apply(t);
}
}
public static final class CountingBiFunction<T, U, R>
extends AbstractCounting<BiFunction<T, U, R>>
implements BiFunction<T, U, R> {
public CountingBiFunction(BiFunction<T, U, R> delegate) {
super(delegate);
}
@Override
public R apply(T t, U u) {
incrementCounter();
return delegate.apply(t, u);
}
}
abstract static class AbstractCounting<D> {
private final AtomicInteger cnt = new AtomicInteger();
protected final D delegate;
protected AbstractCounting(D delegate) {
this.delegate = delegate;
}
protected final void incrementCounter() {
cnt.incrementAndGet();
}
public final int cnt() {
return cnt.get();
}
@Override
public final String toString() {
return cnt.toString();
}
}
}

View File

@ -0,0 +1,43 @@
/*
* 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 StableValue factory implementations
* @modules java.base/jdk.internal.lang.stable
* @enablePreview
* @run junit StableValueFactoriesTest
*/
import jdk.internal.lang.stable.StableUtil;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
final class StableValueFactoriesTest {
@Test
void array() {
assertThrows(IllegalArgumentException.class, () -> StableUtil.array(-1));
}
}

View File

@ -0,0 +1,389 @@
/*
* 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 StableValue implementations
* @enablePreview
* @run junit StableValueTest
*/
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.UnaryOperator;
import java.util.stream.IntStream;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
final class StableValueTest {
private static final int VALUE = 42;
private static final int VALUE2 = 13;
@Test
void trySet() {
trySet(VALUE);
trySet(null);
}
@Test
void preSet() {
StableValue<Integer> stable = StableValue.of(VALUE);
assertTrue(stable.isSet());
assertEquals(VALUE, stable.orElseThrow());
assertEquals(VALUE, stable.orElse(VALUE2));
assertEquals(VALUE, stable.orElseSet(() -> VALUE2));
assertFalse(stable.trySet(VALUE2));
var e = assertThrows(IllegalStateException.class, () -> stable.setOrThrow(VALUE2));
assertEquals(
"The contents is already set",
e.getMessage());
}
void trySet(Integer initial) {
StableValue<Integer> stable = StableValue.of();
assertTrue(stable.trySet(initial));
assertFalse(stable.trySet(null));
assertFalse(stable.trySet(VALUE));
assertFalse(stable.trySet(VALUE2));
assertEquals(initial, stable.orElseThrow());
}
@Test
void setOrThrowValue() {
StableValue<Integer> stable = StableValue.of();
stable.setOrThrow(VALUE);
var e = assertThrows(IllegalStateException.class, () -> stable.setOrThrow(VALUE2));
assertEquals("The contents is already set", e.getMessage());
}
@Test
void setOrThrowNull() {
StableValue<Integer> stable = StableValue.of();
stable.setOrThrow(null);
var e = assertThrows(IllegalStateException.class, () -> stable.setOrThrow(null));
assertEquals("The contents is already set", e.getMessage());
}
@Test
void orElse() {
StableValue<Integer> stable = StableValue.of();
assertEquals(VALUE, stable.orElse(VALUE));
assertNull(stable.orElse(null));
stable.trySet(VALUE);
assertEquals(VALUE, stable.orElse(VALUE2));
}
@Test
void orElseThrow() {
StableValue<Integer> stable = StableValue.of();
var e = assertThrows(NoSuchElementException.class, stable::orElseThrow);
assertEquals("No contents set", e.getMessage());
stable.trySet(VALUE);
assertEquals(VALUE, stable.orElseThrow());
}
@Test
void isSet() {
isSet(VALUE);
isSet(null);
}
void isSet(Integer initial) {
StableValue<Integer> stable = StableValue.of();
assertFalse(stable.isSet());
stable.trySet(initial);
assertTrue(stable.isSet());
}
@Test
void testOrElseSetSupplier() {
StableTestUtil.CountingSupplier<Integer> cs = new StableTestUtil.CountingSupplier<>(() -> VALUE);
StableValue<Integer> stable = StableValue.of();
assertThrows(NullPointerException.class, () -> stable.orElseSet(null));
assertEquals(VALUE, stable.orElseSet(cs));
assertEquals(1, cs.cnt());
assertEquals(VALUE, stable.orElseSet(cs));
assertEquals(1, cs.cnt());
}
@Test
void testHashCode() {
StableValue<Integer> stableValue = StableValue.of();
// Should be Object::hashCode
assertEquals(System.identityHashCode(stableValue), stableValue.hashCode());
}
@Test
void testEquals() {
StableValue<Integer> s0 = StableValue.of();
assertNotEquals(null, s0);
StableValue<Integer> s1 = StableValue.of();
assertNotEquals(s0, s1); // Identity based
s0.setOrThrow(42);
s1.setOrThrow(42);
assertNotEquals(s0, s1);
assertNotEquals("a", s0);
StableValue<Integer> null0 = StableValue.of();
StableValue<Integer> null1 = StableValue.of();
null0.setOrThrow(null);
null1.setOrThrow(null);
assertNotEquals(null0, null1);
}
@Test
void toStringUnset() {
StableValue<Integer> stable = StableValue.of();
assertEquals(".unset", stable.toString());
}
@Test
void toStringNull() {
StableValue<Integer> stable = StableValue.of();
assertTrue(stable.trySet(null));
assertEquals("null", stable.toString());
}
@Test
void toStringNonNull() {
StableValue<Integer> stable = StableValue.of();
assertTrue(stable.trySet(VALUE));
assertEquals(Objects.toString(VALUE), stable.toString());
}
@Test
void toStringCircular() {
StableValue<StableValue<?>> stable = StableValue.of();
stable.trySet(stable);
String toString = assertDoesNotThrow(stable::toString);
assertEquals("(this StableValue)", toString);
assertDoesNotThrow(stable::hashCode);
assertDoesNotThrow((() -> stable.equals(stable)));
}
@Test
void recursiveCall() {
StableValue<Integer> stable = StableValue.of();
AtomicReference<StableValue<Integer>> ref = new AtomicReference<>(stable);
assertThrows(IllegalStateException.class, () ->
stable.orElseSet(() -> {
ref.get().trySet(1);
return 1;
})
);
assertThrows(IllegalStateException.class, () ->
stable.orElseSet(() -> {
ref.get().orElseSet(() -> 1);
return 1;
})
);
}
@Test
void intFunctionExample() {
final class SqrtUtil {
private SqrtUtil() {}
private static final int CACHED_SIZE = 10;
private static final IntFunction<Double> SQRT =
// @link substring="intFunction" target="#intFunction(int,IntFunction)" :
StableValue.intFunction(CACHED_SIZE, StrictMath::sqrt);
public static double sqrt(int a) {
return SQRT.apply(a);
}
}
double sqrt9 = SqrtUtil.sqrt(9); // May eventually constant fold to 3.0 at runtime
assertEquals(3, sqrt9);
assertThrows(IllegalArgumentException.class, () -> SqrtUtil.sqrt(16));
}
@Test
void intFunctionExample2() {
final class PowerOf2Util {
private PowerOf2Util() {}
private static final int SIZE = 6;
private static final IntFunction<Integer> ORIGINAL_POWER_OF_TWO =
v -> 1 << v;
private static final IntFunction<Integer> POWER_OF_TWO =
// @link substring="intFunction" target="#intFunction(int,IntFunction)" :
StableValue.intFunction(SIZE, ORIGINAL_POWER_OF_TWO);
public static int powerOfTwo(int a) {
return POWER_OF_TWO.apply(a);
}
}
int pwr4 = PowerOf2Util.powerOfTwo(4); // May eventually constant fold to 16 at runtime
assertEquals(16, pwr4);
assertEquals(1, PowerOf2Util.powerOfTwo(0));
assertEquals(8, PowerOf2Util.powerOfTwo(3));
assertEquals(32, PowerOf2Util.powerOfTwo(5));
assertThrows(IllegalArgumentException.class, () -> PowerOf2Util.powerOfTwo(10));
}
@Test
void functionExample() {
class Log2Util {
private Log2Util() {}
private static final Set<Integer> CACHED_KEYS =
Set.of(1, 2, 4, 8, 16, 32);
private static final UnaryOperator<Integer> LOG2_ORIGINAL =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Function<Integer, Integer> LOG2_CACHED =
// @link substring="function" target="#function(Set,Function)" :
StableValue.function(CACHED_KEYS, LOG2_ORIGINAL);
public static double log2(int a) {
if (CACHED_KEYS.contains(a)) {
return LOG2_CACHED.apply(a);
} else {
return LOG2_ORIGINAL.apply(a);
}
}
}
double log16 = Log2Util.log2(16); // May eventually constant fold to 4.0 at runtime
double log256 = Log2Util.log2(256); // Will not constant fold
assertEquals(4, log16);
assertEquals(8, log256);
}
@Test
void functionExample2() {
class Log2Util {
private Log2Util() {}
private static final Set<Integer> KEYS =
Set.of(1, 2, 4, 8);
private static final UnaryOperator<Integer> LOG2_ORIGINAL =
i -> 31 - Integer.numberOfLeadingZeros(i);
private static final Function<Integer, Integer> LOG2 =
// @link substring="function" target="#function(Set,Function)" :
StableValue.function(KEYS, LOG2_ORIGINAL);
public static double log2(int a) {
return LOG2.apply(a);
}
}
double log16 = Log2Util.log2(8); // May eventually constant fold to 3.0 at runtime
assertEquals(3, log16);
assertThrows(IllegalArgumentException.class, () -> Log2Util.log2(3));
}
private static final BiPredicate<StableValue<Integer>, Integer> TRY_SET = StableValue::trySet;
private static final BiPredicate<StableValue<Integer>, Integer> SET_OR_THROW = (s, i) -> {
try {
s.setOrThrow(i);
return true;
} catch (IllegalStateException e) {
return false;
}
};
@Test
void raceTrySet() {
race(TRY_SET);
}
@Test
void raceSetOrThrow() {
race(SET_OR_THROW);
}
@Test
void raceMixed() {
race((s, i) -> switch (i % 2) {
case 0 -> TRY_SET.test(s, i);
case 1 -> SET_OR_THROW.test(s, i);
default -> fail("should not reach here");
});
}
void race(BiPredicate<StableValue<Integer>, Integer> winnerPredicate) {
int noThreads = 10;
CountDownLatch starter = new CountDownLatch(1);
StableValue<Integer> stable = StableValue.of();
Map<Integer, Boolean> winners = new ConcurrentHashMap<>();
List<Thread> threads = IntStream.range(0, noThreads).mapToObj(i -> new Thread(() -> {
try {
// Ready, set ...
starter.await();
// Here we go!
winners.put(i, winnerPredicate.test(stable, i));
} catch (Throwable t) {
fail(t);
}
}))
.toList();
threads.forEach(Thread::start);
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(1));
// Start the race
starter.countDown();
threads.forEach(StableValueTest::join);
// There can only be one winner
assertEquals(1, winners.values().stream().filter(b -> b).count());
}
private static void join(Thread thread) {
try {
thread.join();
} catch (InterruptedException e) {
fail(e);
}
}
}

View File

@ -0,0 +1,179 @@
/*
* 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 making sure StableValue publishes values safely
* @modules java.base/jdk.internal.misc
* @enablePreview
* @run junit StableValuesSafePublicationTest
*/
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
final class StableValuesSafePublicationTest {
private static final int SIZE = 100_000;
private static final int THREADS = Runtime.getRuntime().availableProcessors();
private static final StableValue<Holder>[] STABLES = stables();
static StableValue<Holder>[] stables() {
@SuppressWarnings("unchecked")
StableValue<Holder>[] stables = (StableValue<Holder>[]) new StableValue[SIZE];
for (int i = 0; i < SIZE; i++) {
stables[i] = StableValue.of();
}
return stables;
}
static final class Holder {
// These are non-final fields but should be seen
// fully initialized thanks to the HB properties of StableValue.
int a, b, c, d, e;
Holder() {
a = b = c = d = e = 1;
}
}
static final class Consumer implements Runnable {
final int[] observations = new int[SIZE];
final StableValue<Holder>[] stables = STABLES;
int i = 0;
@Override
public void run() {
for (; i < SIZE; i++) {
StableValue<Holder> s = stables[i];
Holder h;
// Wait until the StableValue has a holder value
while ((h = s.orElse(null)) == null) {}
int a = h.a;
int b = h.b;
int c = h.c;
int d = h.d;
int e = h.e;
observations[i] = a + (b << 1) + (c << 2) + (c << 3) + (d << 4) + (e << 5);
}
}
}
static final class Producer implements Runnable {
final StableValue<Holder>[] stables = STABLES;
@Override
public void run() {
StableValue<Holder> s;
long deadlineNs = System.nanoTime();
for (int i = 0; i < SIZE; i++) {
s = stables[i];
s.trySet(new Holder());
deadlineNs += 1000;
while (System.nanoTime() < deadlineNs) {
Thread.onSpinWait();
}
}
}
}
@Test
void main() {
List<Consumer> consumers = IntStream.range(0, THREADS)
.mapToObj(_ -> new Consumer())
.toList();
List<Thread> consumersThreads = IntStream.range(0, THREADS)
.mapToObj(i -> Thread.ofPlatform()
.name("Consumer Thread " + i)
.start(consumers.get(i)))
.toList();
Producer producer = new Producer();
Thread producerThread = Thread.ofPlatform()
.name("Producer Thread")
.start(producer);
join(consumers, producerThread);
join(consumers, consumersThreads.toArray(Thread[]::new));
int[] histogram = new int[64];
for (Consumer consumer : consumers) {
for (int i = 0; i < SIZE; i++) {
histogram[consumer.observations[i]]++;
}
}
// unless a = 1, ..., e = 1, zero observations should be seen
for (int i = 0; i < 63; i++) {
assertEquals(0, histogram[i]);
}
// a = 1, ..., e = 1 : index 2^5-1 = 63
// All observations should end up in this bucket
assertEquals(THREADS * SIZE, histogram[63]);
}
static void join(List<Consumer> consumers, Thread... threads) {
try {
for (Thread t:threads) {
long deadline = System.nanoTime() + TimeUnit.MINUTES.toNanos(1);
while (t.isAlive()) {
t.join(TimeUnit.SECONDS.toMillis(10));
if (t.isAlive()) {
String stack = Arrays.stream(t.getStackTrace())
.map(Objects::toString)
.collect(Collectors.joining(System.lineSeparator()));
System.err.println(t + ": " + stack);
for (int i = 0; i < consumers.size(); i++) {
System.err.println("Consumer " + i + ": " + consumers.get(i).i);
}
}
if (System.nanoTime() > deadline) {
long nonNulls = CompletableFuture.supplyAsync(() ->
Stream.of(STABLES)
.map(s -> s.orElse(null))
.filter(Objects::nonNull)
.count(), Executors.newSingleThreadExecutor()).join();
fail("Giving up! Set stables seen by a new thread: " + nonNulls);
}
}
}
} catch (InterruptedException ie) {
fail(ie);
}
}
}

View File

@ -0,0 +1,124 @@
/*
* 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 TrustedFieldType implementations
* @modules jdk.unsupported/sun.misc
* @modules java.base/jdk.internal.lang.stable
* @modules java.base/jdk.internal.misc
* @enablePreview
* @run junit/othervm --add-opens java.base/jdk.internal.lang.stable=ALL-UNNAMED -Dopens=true TrustedFieldTypeTest
* @run junit/othervm -Dopens=false TrustedFieldTypeTest
*/
import jdk.internal.lang.stable.StableValueImpl;
import jdk.internal.misc.Unsafe;
import org.junit.jupiter.api.Test;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.lang.reflect.Field;
import java.lang.reflect.InaccessibleObjectException;
import static org.junit.jupiter.api.Assertions.*;
final class TrustedFieldTypeTest {
@Test
void varHandle() throws NoSuchFieldException, IllegalAccessException {
MethodHandles.Lookup lookup = MethodHandles.lookup();
StableValue<Integer> originalValue = StableValue.of();
@SuppressWarnings("unchecked")
StableValue<Integer>[] originalArrayValue = new StableValue[10];
final class Holder {
private final StableValue<Integer> value = originalValue;
}
final class ArrayHolder {
private final StableValue<Integer>[] array = originalArrayValue;
}
VarHandle valueVarHandle = lookup.findVarHandle(Holder.class, "value", StableValue.class);
Holder holder = new Holder();
assertThrows(UnsupportedOperationException.class, () ->
valueVarHandle.set(holder, StableValue.of())
);
assertThrows(UnsupportedOperationException.class, () ->
valueVarHandle.compareAndSet(holder, originalValue, StableValue.of())
);
VarHandle arrayVarHandle = lookup.findVarHandle(ArrayHolder.class, "array", StableValue[].class);
ArrayHolder arrayHolder = new ArrayHolder();
assertThrows(UnsupportedOperationException.class, () ->
arrayVarHandle.set(arrayHolder, new StableValue[1])
);
assertThrows(UnsupportedOperationException.class, () ->
arrayVarHandle.compareAndSet(arrayHolder, originalArrayValue, new StableValue[1])
);
}
@Test
void updateStableValueContentVia_j_i_m_Unsafe() {
StableValue<Integer> stableValue = StableValue.of();
stableValue.trySet(42);
jdk.internal.misc.Unsafe unsafe = Unsafe.getUnsafe();
long offset = unsafe.objectFieldOffset(stableValue.getClass(), "contents");
assertTrue(offset > 0);
// Unfortunately, it is possible to update the underlying data via jdk.internal.misc.Unsafe
Object oldData = unsafe.getAndSetReference(stableValue, offset, 13);
assertEquals(42, oldData);
assertEquals(13, stableValue.orElseThrow());
}
@Test
void updateStableValueContentViaSetAccessible() throws NoSuchFieldException, IllegalAccessException {
if (Boolean.getBoolean("opens")) {
// Unfortunately, add-opens allows direct access to the `value` field
Field field = StableValueImpl.class.getDeclaredField("contents");
field.setAccessible(true);
StableValue<Integer> stableValue = StableValue.of();
stableValue.trySet(42);
Object oldData = field.get(stableValue);
assertEquals(42, oldData);
field.set(stableValue, 13);
assertEquals(13, stableValue.orElseThrow());
} else {
Field field = StableValueImpl.class.getDeclaredField("contents");
assertThrows(InaccessibleObjectException.class, ()-> field.setAccessible(true));
}
}
}

View File

@ -30,6 +30,7 @@
* @summary Run many tests on many Collection and Map implementations
* @author Martin Buchholz
* @modules java.base/java.util:open
* @enablePreview
* @run main MOAT
* @key randomness
*/
@ -219,10 +220,15 @@ public class MOAT {
// Immutable List
testEmptyList(List.of());
testEmptyList(List.of().subList(0,0));
testEmptyList(StableValue.list(0, i -> i));
testEmptyList(StableValue.list(3, i -> i).subList(0, 0));
testListMutatorsAlwaysThrow(List.of());
testListMutatorsAlwaysThrow(List.<Integer>of().subList(0,0));
testListMutatorsAlwaysThrow(StableValue.list(0, i -> i));
testEmptyListMutatorsAlwaysThrow(List.of());
testEmptyListMutatorsAlwaysThrow(List.<Integer>of().subList(0,0));
testEmptyListMutatorsAlwaysThrow(StableValue.list(0, i -> i));
testEmptyListMutatorsAlwaysThrow(StableValue.list(3, i -> i).subList(0, 0));
for (List<Integer> list : Arrays.asList(
List.<Integer>of(),
List.of(1),
@ -244,7 +250,10 @@ public class MOAT {
Stream.of((Integer)null).toList(),
Stream.of(1, null).toList(),
Stream.of(1, null, 3).toList(),
Stream.of(1, null, 3, 4).toList())) {
Stream.of(1, null, 3, 4).toList(),
StableValue.list(0, i -> i),
StableValue.list(3, i -> i),
StableValue.list(10, i -> i))) {
testCollection(list);
testImmutableList(list);
testListMutatorsAlwaysThrow(list);
@ -356,6 +365,9 @@ public class MOAT {
testEmptyMap(Map.of());
testMapMutatorsAlwaysThrow(Map.of());
testEmptyMapMutatorsAlwaysThrow(Map.of());
testEmptyMap(StableValue.map(Set.of(), k -> k));
testMapMutatorsAlwaysThrow(StableValue.map(Set.of(), k -> k));
testEmptyMapMutatorsAlwaysThrow(StableValue.map(Set.of(), k -> k));
for (Map<Integer,Integer> map : Arrays.asList(
Map.<Integer,Integer>of(),
Map.of(1, 101),
@ -368,7 +380,10 @@ public class MOAT {
Map.of(1, 101, 2, 202, 3, 303, 4, 404, 5, 505, 6, 606, 7, 707, 8, 808),
Map.of(1, 101, 2, 202, 3, 303, 4, 404, 5, 505, 6, 606, 7, 707, 8, 808, 9, 909),
Map.of(1, 101, 2, 202, 3, 303, 4, 404, 5, 505, 6, 606, 7, 707, 8, 808, 9, 909, 10, 1010),
Map.ofEntries(ea))) {
Map.ofEntries(ea),
StableValue.map(Set.<Integer>of(), k -> k),
StableValue.map(Set.of(1), k -> k),
StableValue.map(Set.of(1, 2, 3), k -> k))) {
testMap(map);
testImmutableMap(map);
testMapMutatorsAlwaysThrow(map);

View File

@ -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.
*
* 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 org.openjdk.bench.java.lang.stable;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Benchmark measuring StableValue performance
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark) // Share the same state instance (for contention)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(value = 2, jvmArgsAppend = {
"--enable-preview"
})
@Threads(Threads.MAX) // Benchmark under contention
@OperationsPerInvocation(100)
public class StableFunctionBenchmark {
private static final int SIZE = 100;
private static final Set<Integer> SET = IntStream.range(0, SIZE).boxed().collect(Collectors.toSet());
private static final Map<Integer, Integer> MAP = StableValue.map(SET, Function.identity());
private static final Function<Integer, Integer> FUNCTION = StableValue.function(SET, Function.identity());
private final Map<Integer, Integer> map = StableValue.map(SET, Function.identity());
private final Function<Integer, Integer> function = StableValue.function(SET, Function.identity());
@Benchmark
public int map() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += map.get(i);
}
return sum;
}
@Benchmark
public int function() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += function.apply(i);
}
return sum;
}
@Benchmark
public int staticSMap() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += MAP.get(i);
}
return sum;
}
@Benchmark
public int staticIntFunction() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += FUNCTION.apply(i);
}
return sum;
}
}

View File

@ -0,0 +1,88 @@
/*
* 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.
*/
package org.openjdk.bench.java.lang.stable;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
/**
* Benchmark measuring StableValue performance
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark) // Share the same state instance (for contention)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(value = 2, jvmArgsAppend = {
"--enable-preview"
})
@Threads(Threads.MAX) // Benchmark under contention
public class StableFunctionSingleBenchmark {
private static final int SIZE = 100;
private static final Set<Integer> SET = IntStream.range(0, SIZE).boxed().collect(Collectors.toSet());
private static final Map<Integer, Integer> MAP = StableValue.map(SET, Function.identity());
private static final Function<Integer, Integer> FUNCTION = StableValue.function(SET, Function.identity());
private final Map<Integer, Integer> map = StableValue.map(SET, Function.identity());
private final Function<Integer, Integer> function = StableValue.function(SET, Function.identity());
@Benchmark
public int map() {
return map.get(1);
}
@Benchmark
public int function() {
return function.apply(1);
}
@Benchmark
public int staticSMap() {
return MAP.get(1);
}
@Benchmark
public int staticIntFunction() {
return FUNCTION.apply(1);
}
}

View File

@ -0,0 +1,102 @@
/*
* 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.
*/
package org.openjdk.bench.java.lang.stable;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
/**
* Benchmark measuring StableValue performance
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark) // Share the same state instance (for contention)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(value = 2, jvmArgsAppend = {
"--enable-preview"
})
@Threads(Threads.MAX) // Benchmark under contention
@OperationsPerInvocation(100)
public class StableIntFunctionBenchmark {
private static final int SIZE = 100;
private static final IntFunction<Integer> IDENTITY = i -> i;
private static final List<Integer> LIST = StableValue.list(SIZE, IDENTITY);
private static final IntFunction<Integer> INT_FUNCTION = StableValue.intFunction(SIZE, IDENTITY);
private final List<Integer> list = StableValue.list(SIZE, IDENTITY);
private final IntFunction<Integer> intFunction = StableValue.intFunction(SIZE, IDENTITY);
@Benchmark
public int list() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += list.get(i);
}
return sum;
}
@Benchmark
public int intFunction() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += intFunction.apply(i);
}
return sum;
}
@Benchmark
public int staticList() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += LIST.get(i);
}
return sum;
}
@Benchmark
public int staticIntFunction() {
int sum = 0;
for (int i = 0; i < SIZE; i++) {
sum += INT_FUNCTION.apply(i);
}
return sum;
}
}

View File

@ -0,0 +1,84 @@
/*
* 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.
*/
package org.openjdk.bench.java.lang.stable;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.IntFunction;
/**
* Benchmark measuring StableValue performance
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark) // Share the same state instance (for contention)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(value = 2, jvmArgsAppend = {
"--enable-preview"
})
@Threads(Threads.MAX) // Benchmark under contention
public class StableIntFunctionSingleBenchmark {
private static final int SIZE = 100;
private static final IntFunction<Integer> IDENTITY = i -> i;
private static final List<Integer> STABLE = StableValue.list(SIZE, IDENTITY);
private static final IntFunction<Integer> INT_FUNCTION = StableValue.intFunction(SIZE, IDENTITY);
private final List<Integer> stable = StableValue.list(SIZE, IDENTITY);
private final IntFunction<Integer> intFunction = StableValue.intFunction(SIZE, IDENTITY);
@Benchmark
public int list() {
return stable.get(1);
}
@Benchmark
public int intFunction() {
return intFunction.apply(1);
}
@Benchmark
public int staticList() {
return STABLE.get(1);
}
@Benchmark
public int staticIntFunction() {
return INT_FUNCTION.apply(1);
}
}

View File

@ -0,0 +1,136 @@
/*
* 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.
*/
package org.openjdk.bench.java.lang.stable;
import org.openjdk.bench.java.lang.stable.StableValueBenchmark.Dcl;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.lang.classfile.CodeBuilder;
import java.lang.classfile.constantpool.ConstantPoolBuilder;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import static java.lang.constant.ConstantDescs.*;
/**
* Benchmark measuring StableValue performance
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark) // Share the same state instance (for contention)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(value = 2, jvmArgsAppend = {
"--enable-preview"
})
@Threads(Threads.MAX) // Benchmark under contention
public class StableMethodHandleBenchmark {
private static final MethodHandle FINAL_MH = identityHandle();
private static final StableValue<MethodHandle> STABLE_MH;
private static MethodHandle mh = identityHandle();
private static final Dcl<MethodHandle> DCL = new Dcl<>(StableMethodHandleBenchmark::identityHandle);
private static final AtomicReference<MethodHandle> ATOMIC_REFERENCE = new AtomicReference<>(identityHandle());
private static final Map<String, MethodHandle> MAP = new ConcurrentHashMap<>();
private static final Map<String, MethodHandle> STABLE_MAP = StableValue.map(Set.of("identityHandle"), _ -> identityHandle());
static {
STABLE_MH = StableValue.of();
STABLE_MH.setOrThrow(identityHandle());
MAP.put("identityHandle", identityHandle());
}
@Benchmark
public int atomic() throws Throwable {
return (int) ATOMIC_REFERENCE.get().invokeExact(1);
}
@Benchmark
public int dcl() throws Throwable {
return (int) DCL.get().invokeExact(1);
}
@Benchmark
public int finalMh() throws Throwable {
return (int) FINAL_MH.invokeExact(1);
}
@Benchmark
public int map() throws Throwable {
return (int) MAP.get("identityHandle").invokeExact(1);
}
@Benchmark
public int nonFinalMh() throws Throwable {
return (int) mh.invokeExact(1);
}
@Benchmark
public int stableMap() throws Throwable {
return (int) STABLE_MAP.get("identityHandle").invokeExact(1);
}
@Benchmark
public int stableMh() throws Throwable {
return (int) STABLE_MH.orElseThrow().invokeExact(1);
}
Object cp() {
CodeBuilder cob = null;
ConstantPoolBuilder cp = ConstantPoolBuilder.of();
cob.ldc(cp.constantDynamicEntry(cp.bsmEntry(cp.methodHandleEntry(BSM_CLASS_DATA), List.of()),
cp.nameAndTypeEntry(DEFAULT_NAME, CD_MethodHandle)));
return null;
}
static MethodHandle identityHandle() {
var lookup = MethodHandles.lookup();
try {
return lookup.findStatic(StableMethodHandleBenchmark.class, "identity", MethodType.methodType(int.class, int.class));
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
}
private static int identity(int value) {
return value;
}
}

View File

@ -0,0 +1,94 @@
/*
* 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.
*/
package org.openjdk.bench.java.lang.stable;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Threads;
import org.openjdk.jmh.annotations.Warmup;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* Benchmark measuring StableValue performance
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark) // Share the same state instance (for contention)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(value = 2, jvmArgsAppend = {
"--enable-preview"
})
@Threads(Threads.MAX) // Benchmark under contention
@OperationsPerInvocation(2)
public class StableSupplierBenchmark {
private static final int VALUE = 42;
private static final int VALUE2 = 23;
private static final StableValue<Integer> STABLE = init(StableValue.of(), VALUE);
private static final StableValue<Integer> STABLE2 = init(StableValue.of(), VALUE2);
private static final Supplier<Integer> SUPPLIER = StableValue.supplier(() -> VALUE);
private static final Supplier<Integer> SUPPLIER2 = StableValue.supplier(() -> VALUE);
private final StableValue<Integer> stable = init(StableValue.of(), VALUE);
private final StableValue<Integer> stable2 = init(StableValue.of(), VALUE2);
private final Supplier<Integer> supplier = StableValue.supplier(() -> VALUE);
private final Supplier<Integer> supplier2 = StableValue.supplier(() -> VALUE2);
@Benchmark
public int stable() {
return stable.orElseThrow() + stable2.orElseThrow();
}
@Benchmark
public int supplier() {
return supplier.get() + supplier2.get();
}
@Benchmark
public int staticStable() {
return STABLE.orElseThrow() + STABLE2.orElseThrow();
}
@Benchmark
public int staticSupplier() {
return SUPPLIER.get() + SUPPLIER2.get();
}
private static StableValue<Integer> init(StableValue<Integer> m, Integer value) {
m.trySet(value);
return m;
}
}

View File

@ -0,0 +1,196 @@
/*
* 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.
*/
package org.openjdk.bench.java.lang.stable;
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Supplier;
/**
* Benchmark measuring StableValue performance
*/
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark) // Share the same state instance (for contention)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 2)
@Fork(value = 2, jvmArgsAppend = {
"--enable-preview"
})
@Threads(Threads.MAX) // Benchmark under contention
@OperationsPerInvocation(2)
public class StableValueBenchmark {
private static final int VALUE = 42;
private static final int VALUE2 = 23;
private static final StableValue<Integer> STABLE = init(StableValue.of(), VALUE);
private static final StableValue<Integer> STABLE2 = init(StableValue.of(), VALUE2);
private static final StableValue<Integer> DCL = init(StableValue.of(), VALUE);
private static final StableValue<Integer> DCL2 = init(StableValue.of(), VALUE2);
private static final AtomicReference<Integer> ATOMIC = new AtomicReference<>(VALUE);
private static final AtomicReference<Integer> ATOMIC2 = new AtomicReference<>(VALUE2);
private static final Holder HOLDER = new Holder(VALUE);
private static final Holder HOLDER2 = new Holder(VALUE2);
private static final RecordHolder RECORD_HOLDER = new RecordHolder(VALUE);
private static final RecordHolder RECORD_HOLDER2 = new RecordHolder(VALUE2);
private final StableValue<Integer> stable = init(StableValue.of(), VALUE);
private final StableValue<Integer> stable2 = init(StableValue.of(), VALUE2);
private final StableValue<Integer> stableNull = StableValue.of();
private final StableValue<Integer> stableNull2 = StableValue.of();
private final Supplier<Integer> dcl = new Dcl<>(() -> VALUE);
private final Supplier<Integer> dcl2 = new Dcl<>(() -> VALUE2);
private final AtomicReference<Integer> atomic = new AtomicReference<>(VALUE);
private final AtomicReference<Integer> atomic2 = new AtomicReference<>(VALUE2);
private final Supplier<Integer> supplier = () -> VALUE;
private final Supplier<Integer> supplier2 = () -> VALUE2;
@Setup
public void setup() {
stableNull.trySet(null);
stableNull2.trySet(null);
}
@Benchmark
public int atomic() {
return atomic.get() + atomic2.get();
}
@Benchmark
public int dcl() {
return dcl.get() + dcl2.get();
}
@Benchmark
public int stable() {
return stable.orElseThrow() + stable2.orElseThrow();
}
@Benchmark
public int stableNull() {
return (stableNull.orElseThrow() == null ? VALUE : VALUE2) + (stableNull2.orElseThrow() == null ? VALUE : VALUE2);
}
// Reference case
@Benchmark
public int refSupplier() {
return supplier.get() + supplier2.get();
}
@Benchmark
public int staticAtomic() {
return ATOMIC.get() + ATOMIC2.get();
}
@Benchmark
public int staticDcl() {
return DCL.orElseThrow() + DCL2.orElseThrow();
}
@Benchmark
public int staticHolder() {
return HOLDER.get() + HOLDER2.get();
}
@Benchmark
public int staticRecordHolder() {
return RECORD_HOLDER.get() + RECORD_HOLDER2.get();
}
@Benchmark
public int staticStable() {
return STABLE.orElseThrow() + STABLE2.orElseThrow();
}
private static StableValue<Integer> init(StableValue<Integer> m, Integer value) {
m.trySet(value);
return m;
}
private static final class Holder {
private final StableValue<Integer> delegate = StableValue.of();
Holder(int value) {
delegate.setOrThrow(value);
}
int get() {
return delegate.orElseThrow();
}
}
private record RecordHolder(StableValue<Integer> delegate) {
RecordHolder(int value) {
this(StableValue.of());
delegate.setOrThrow(value);
}
int get() {
return delegate.orElseThrow();
}
}
// Handles null values
public static class Dcl<V> implements Supplier<V> {
private final Supplier<V> supplier;
private volatile V value;
private boolean bound;
public Dcl(Supplier<V> supplier) {
this.supplier = supplier;
}
@Override
public V get() {
V v = value;
if (v == null) {
if (!bound) {
synchronized (this) {
v = value;
if (v == null) {
if (!bound) {
value = v = supplier.get();
bound = true;
}
}
}
}
}
return v;
}
}
}

View File

@ -0,0 +1,174 @@
/*
* 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.
*/
package org.openjdk.bench.java.lang.stable;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import java.lang.foreign.Arena;
import java.lang.foreign.MemoryLayout;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.ValueLayout;
import java.lang.invoke.VarHandle;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Supplier;
import static java.lang.foreign.MemoryLayout.PathElement.groupElement;
import static java.util.concurrent.TimeUnit.*;
@Warmup(iterations = 5, time = 5, timeUnit = SECONDS)
@Measurement(iterations = 5, time = 5, timeUnit = SECONDS)
@Fork(value = 1, jvmArgs = { "--enable-preview" })
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(NANOSECONDS)
@State(Scope.Benchmark)
public class VarHandleHolderBenchmark {
private static final MemoryLayout LAYOUT = MemoryLayout.structLayout(
ValueLayout.JAVA_INT.withName("x"),
ValueLayout.JAVA_INT.withName("y")
);
private static final long SIZEOF = LAYOUT.byteSize();
private static final long OFFSET_X = LAYOUT.byteOffset(MemoryLayout.PathElement.groupElement("x"));
private static final long OFFSET_Y = LAYOUT.byteOffset(groupElement("y"));
static final class MyVarHandleLookup implements Function<String, VarHandle> {
@Override
public VarHandle apply(String name) {
return LAYOUT.arrayElementVarHandle(groupElement(name)).withInvokeExactBehavior();
}
}
private static final Function<String, VarHandle> VAR_HANDLE_FUNCTION = new MyVarHandleLookup();
private static final VarHandle VH_X = VAR_HANDLE_FUNCTION.apply("x");
private static final VarHandle VH_Y = VAR_HANDLE_FUNCTION.apply("y");
private static final Supplier<VarHandle> SV_X = StableValue.supplier(() -> VAR_HANDLE_FUNCTION.apply("x"));
private static final Supplier<VarHandle> SV_Y = StableValue.supplier(() -> VAR_HANDLE_FUNCTION.apply("y"));
private static final Map<String, VarHandle> U_MAP = Map.of(
"x", VH_X,
"y", VH_Y);
private static final Map<String, VarHandle> U_MAP_ELEMENT = Map.of(
"x", LAYOUT.varHandle(groupElement("x")),
"y", LAYOUT.varHandle(groupElement("y")));
private static final Map<String, VarHandle> S_MAP = StableValue.map(
Set.of("x", "y"),
VAR_HANDLE_FUNCTION);
private static final Function<String, VarHandle> S_FUN = StableValue.function(
Set.of("x", "y"),
VAR_HANDLE_FUNCTION);
private static final MemorySegment confined;
static {
var array = new int[512 * (int) SIZEOF / (int) ValueLayout.JAVA_INT.byteSize()];
var heap = MemorySegment.ofArray(array);
for(var i = 0; i < 512; i++) {
heap.set(ValueLayout.JAVA_INT, i * SIZEOF + OFFSET_X, i);
heap.set(ValueLayout.JAVA_INT, i * SIZEOF + OFFSET_Y, i);
}
confined = Arena.ofConfined().allocate(LAYOUT, 512);
confined.copyFrom(heap);
}
@Benchmark
public int confinedVarHandleLoop() {
var sum = 0;
for (var i = 0; i < 512; i++) {
var x = (int) VH_X.get(confined, 0L, (long) i);
var y = (int) VH_Y.get(confined, 0L, (long) i);
sum += x /*+y*/;
}
return sum;
}
@Benchmark
public int confinedStableValueLoop() {
var sum = 0;
for (var i = 0; i < 512; i++) {
var x = (int) SV_X.get().get(confined, 0L, (long) i);
var y = (int) SV_Y.get().get(confined, 0L, (long) i);
sum += x + y;
}
return sum;
}
@Benchmark
public int confinedStableMapLoop() {
var sum = 0;
for (var i = 0; i < 512; i++) {
var x = (int) S_MAP.get("x").get(confined, 0L, (long) i);
var y = (int) S_MAP.get("y").get(confined, 0L, (long) i);
sum += x + y;
}
return sum;
}
@Benchmark
public int confinedStableMapElementLoop() {
var sum = 0;
for (var i = 0; i < 512; i++) {
var x = (int) U_MAP_ELEMENT.get("x").get(confined, i * 8L);
var y = (int) U_MAP_ELEMENT.get("y").get(confined, i * 8L);
sum += x + y;
}
return sum;
}
@Benchmark
public int confinedUnmodifiableMapLoop() {
var sum = 0;
for (var i = 0; i < 512; i++) {
var x = (int) U_MAP.get("x").get(confined, 0L, (long) i);
var y = (int) U_MAP.get("y").get(confined, 0L, (long) i);
sum += x + y;
}
return sum;
}
@Benchmark
public int confinedStableFunctionLoop() {
var sum = 0;
for (var i = 0; i < 512; i++) {
var x = (int) S_FUN.apply("x").get(confined, 0L, (long) i);
var y = (int) S_FUN.apply("y").get(confined, 0L, (long) i);
sum += x + y;
}
return sum;
}
}