8376811: Implement JEP 531: Lazy Constants (Third Preview)

Reviewed-by: jvernee, vklang, mhaessig
This commit is contained in:
Per Minborg 2026-05-08 08:03:43 +00:00
parent 54146adae0
commit a6bd64be55
19 changed files with 2717 additions and 490 deletions

View File

@ -29,28 +29,35 @@ import jdk.internal.javac.PreviewFeature;
import jdk.internal.lang.LazyConstantImpl;
import java.io.Serializable;
import java.util.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Supplier;
/**
* A lazy constant is a holder of contents that can be set at most once.
* A lazy constant is a holder of content that can be initialized at most once.
* <p>
* A lazy constant is created using the factory method
* {@linkplain LazyConstant#of(Supplier) LazyConstant.of({@code <computing function>})}.
* <p>
* When created, the lazy constant is <em>not initialized</em>, meaning it has no contents.
* When created, the lazy constant is <em>not initialized</em>, meaning it has no content.
* <p>
* The lazy constant (of type {@code T}) can then be <em>initialized</em>
* (and its contents retrieved) by calling {@linkplain #get() get()}. The first time
* (and its content retrieved) by calling {@linkplain #get() get()}. The first time
* {@linkplain #get() get()} is called, the underlying <em>computing function</em>
* (provided at construction) will be invoked and the result will be used to initialize
* the constant.
* <p>
* Once a lazy constant is initialized, its contents can <em>never change</em>
* and will be retrieved over and over again upon subsequent {@linkplain #get() get()}
* invocations.
* Once a lazy constant is initialized, its content can <em>never change</em>
* and will always be returned by subsequent {@linkplain #get() get()} invocations.
* <p>
* Consider the following example where a lazy constant field "{@code logger}" holds
* an object of type {@code Logger}:
@ -83,12 +90,25 @@ import java.util.function.Supplier;
* may result in storage resources being prepared.
*
* <h2 id="exception-handling">Exception handling</h2>
* If the computing function returns {@code null}, a {@linkplain NullPointerException}
* is thrown. Hence, a lazy constant can never hold a {@code null} value. Clients who
* want to use a nullable constant can wrap the value into an {@linkplain Optional} holder.
* If evaluation of the computing function throws an unchecked exception (i.e., a runtime
* exception or an error), the lazy constant is not initialized but instead transitions to
* an error state whereafter a {@linkplain NoSuchElementException} is thrown with the
* unchecked exception as a cause. Subsequent {@linkplain #get() get()} calls throw
* {@linkplain NoSuchElementException} (without ever invoking the computing function
* again) with no cause and with a message that includes the name of the original
* unchecked exception's class.
* <p>
* If the computing function recursively invokes itself via the lazy constant, an
* {@linkplain IllegalStateException} is thrown, and the lazy constant is not initialized.
* All failures are handled in this way. There are two special cases that cause unchecked
* exceptions to be thrown:
* <p>
* If the computing function returns {@code null}, a {@linkplain NoSuchElementException}
* (with a {@linkplain NullPointerException} as a cause) will be thrown. Hence, a
* lazy constant can never hold a {@code null} value. Clients who want to use a nullable
* constant can wrap the value into an {@linkplain Optional} holder.
* <p>
* If the computing function recursively invokes itself via the lazy constant, a
* {@linkplain NoSuchElementException} (with an {@linkplain IllegalStateException} as a
* cause) will be thrown.
*
* <h2 id="composition">Composing lazy constants</h2>
* A lazy constant can depend on other lazy constants, forming a dependency graph
@ -98,31 +118,25 @@ import java.util.function.Supplier;
* which are held by lazy constants:
*
* {@snippet lang = java:
* public final class DependencyUtil {
* public static class Foo {
* // ...
* }
*
* private DependencyUtil() {}
*
* public static class Foo {
* public static class Bar {
* public Bar(Foo foo) {
* // ...
* }
*
* public static class Bar {
* public Bar(Foo foo) {
* // ...
* }
* }
* }
*
* private static final LazyConstant<Foo> FOO = LazyConstant.of( Foo::new );
* private static final LazyConstant<Bar> BAR = LazyConstant.of( () -> new Bar(FOO.get()) );
* static final LazyConstant<Foo> FOO = LazyConstant.of( Foo::new );
* static final LazyConstant<Bar> BAR = LazyConstant.of( () -> new Bar(FOO.get()) );
*
* public static Foo foo() {
* return FOO.get();
* }
*
* public static Bar bar() {
* return BAR.get();
* }
* public static Foo foo() {
* return FOO.get();
* }
*
* public static Bar bar() {
* return BAR.get();
* }
* }
* Calling {@code BAR.get()} will create the {@code Bar} singleton if it is not already
@ -134,13 +148,17 @@ import java.util.function.Supplier;
* competing threads are racing to initialize a lazy constant, only one updating thread
* runs the computing function (which runs on the caller's thread and is hereafter denoted
* <em>the computing thread</em>), while the other threads are blocked until the constant
* is initialized, after which the other threads observe the lazy constant is initialized
* and leave the constant unchanged and will never invoke any computation.
* is initialized (or computation fails), after which the other threads observe the lazy
* constant is initialized (or has transisioned to an error state) and leave the constant
* unchanged and will never invoke any computation.
* <p>
* The invocation of the computing function and the resulting initialization of
* the constant {@linkplain java.util.concurrent##MemoryVisibility <em>happens-before</em>}
* the initialized constant's content is read. Hence, the initialized constant's content,
* including any {@code final} fields of any newly created objects, is safely published.
* As subsequent retrieval of the content might be elided, there are no other memory
* ordering or visibility guarantees provided as a consequence of calling
* {@linkplain #get()} again.
* <p>
* Thread interruption does not cancel the initialization of a lazy constant. In other
* words, if the computing thread is interrupted, {@code LazyConstant::get} doesn't clear
@ -150,9 +168,9 @@ import java.util.function.Supplier;
* lazy constant may block indefinitely; no timeouts or cancellations are provided.
*
* <h2 id="performance">Performance</h2>
* The contents of a lazy constant can never change after the lazy constant has been
* The content of a lazy constant can never change after the lazy constant has been
* initialized. Therefore, a JVM implementation may, for an initialized lazy constant,
* elide all future reads of that lazy constant's contents and instead use the contents
* elide all future reads of that lazy constant's content and instead use the content
* that has been previously observed. We call this optimization <em>constant folding</em>.
* This is only possible if there is a direct reference from a {@code static final} field
* to a lazy constant or if there is a chain from a {@code static final} field -- via one
@ -160,15 +178,10 @@ import java.util.function.Supplier;
* {@linkplain Record record} fields, or final instance fields in hidden classes) --
* to a lazy constant.
*
* <h2 id="miscellaneous">Miscellaneous</h2>
* Except for {@linkplain Object#equals(Object) equals(obj)} and
* {@linkplain #orElse(Object) orElse(other)} parameters, all method parameters
* must be <em>non-null</em>, or a {@link NullPointerException} will be thrown.
*
* @apiNote Once a lazy constant is initialized, its contents cannot ever be removed.
* @apiNote Once a lazy constant is initialized, its content can't be removed.
* This can be a source of an unintended memory leak. More specifically,
* a lazy constant {@linkplain java.lang.ref##reachability strongly references}
* it contents. Hence, the contents of a lazy constant will be reachable as long
* its content. Hence, the content of a lazy constant will be reachable as long
* as the lazy constant itself is reachable.
* <p>
* While it's possible to store an array inside a lazy constant, doing so will
@ -185,7 +198,7 @@ import java.util.function.Supplier;
* @implNote
* A lazy constant is free to synchronize on itself. Hence, care must be
* taken when directly or indirectly synchronizing on a lazy constant.
* A lazy constant is unmodifiable but its contents may or may not be
* A lazy constant is unmodifiable but its content may or may not be
* immutable (e.g., it may hold an {@linkplain ArrayList}).
*
* @param <T> type of the constant
@ -205,39 +218,24 @@ public sealed interface LazyConstant<T>
permits LazyConstantImpl {
/**
* {@return the contents of this lazy constant if initialized, otherwise,
* returns {@code other}}
* {@return the initialized content of this constant, computing it if necessary}
* <p>
* This method never triggers initialization of this lazy constant and will observe
* initialization by other threads atomically (i.e., it returns the contents
* if and only if the initialization has already completed).
*
* @param other value to return if the content is not initialized
* (can be {@code null})
*/
T orElse(T other);
/**
* {@return the contents of this initialized constant. If not initialized, first
* computes and initializes this constant using the computing function}
* If this constant is not initialized, first computes and initializes it
* using the computing function.
* <p>
* After this method returns successfully, the constant is guaranteed to be
* initialized.
* <p>
* If the computing function throws, the throwable is relayed to the caller and
* the lazy constant remains uninitialized; a subsequent call to get() may then
* attempt the computation again.
* If an unchecked exception is thrown when evaluating the computing function or if
* the computing function returns {@code null}, this lazy constant is not initialized
* but transitions to an error state whereafter a {@linkplain NoSuchElementException}
* is thrown as described in the {@linkplain ##exception-handling Exception handling}
* section.
*
* @throws NoSuchElementException if this lazy constant is in an error state
*/
T get();
/**
* {@return {@code true} if the constant is initialized, {@code false} otherwise}
* <p>
* This method never triggers initialization of this lazy constant and will observe
* changes in the initialization state made by other threads atomically.
*/
boolean isInitialized();
// Object methods
/**
@ -245,7 +243,7 @@ public sealed interface LazyConstant<T>
* the provided {@code obj}, otherwise {@code false}}
* <p>
* In other words, equals compares the identity of this lazy constant and {@code obj}
* to determine equality. Hence, two distinct lazy constants with the same contents are
* to determine equality. Hence, two distinct lazy constants with the same content are
* <em>not</em> equal.
* <p>
* This method never triggers initialization of this lazy constant.
@ -267,11 +265,11 @@ public sealed interface LazyConstant<T>
* <p>
* This method never triggers initialization of this lazy constant and will observe
* initialization by other threads atomically (i.e., it observes the
* contents if and only if the initialization has already completed).
* content if and only if the initialization has already completed).
* <p>
* If this lazy constant is initialized, an implementation-dependent string
* containing the {@linkplain Object#toString()} of the
* contents will be returned; otherwise, an implementation-dependent string is
* content will be returned; otherwise, an implementation-dependent string is
* returned that indicates this lazy constant is not yet initialized.
*/
@Override
@ -280,31 +278,41 @@ public sealed interface LazyConstant<T>
// Factory
/**
* {@return a lazy constant whose contents is to be computed later via the provided
* {@code computingFunction}}
* {@return a new lazy constant whose content is to be computed later via the
* provided {@code computingFunction}}
* <p>
* The returned lazy constant strongly references the provided
* {@code computingFunction} at least until initialization completes successfully.
* {@code computingFunction} until computation completes (successfully or with
* failure).
* <p>
* If the provided computing function is already an instance of
* {@code LazyConstant}, the method is free to return the provided computing function
* directly.
* By design, the method always returns a new lazy constant even if the provided
* computing function is already an instance of {@code LazyConstant}. Clients that
* want to elide creation under this condition can write a utility method similar
* to the one in the snippet below and create lazy constants via this method rather
* than calling the built-in factory {@linkplain #of(Supplier)} directly:
*
* @implNote after initialization completes successfully, the computing function is
* no longer strongly referenced and becomes eligible for
* garbage collection.
* {@snippet lang = java:
* static <T> LazyConstant<T> ofFlattened(Supplier<? extends T> computingFunction) {
* return (computingFunction instanceof LazyConstant<? extends T> lc)
* ? (LazyConstant<T>) lc // unchecked cast is safe under normal generic usage
* : LazyConstant.of(computingFunction);
* }
* }
*
* @implNote after the computing function completes (regardless of whether it
* succeeds or throws an unchecked exception), the computing function is no
* longer strongly referenced and becomes eligible for garbage collection.
*
* @param computingFunction in the form of a {@linkplain Supplier} to be used
* to initialize the constant
* @param <T> type of the constant
* @throws NullPointerException if the provided {@code computingFunction} is
* {@code null}
*
*/
@SuppressWarnings("unchecked")
static <T> LazyConstant<T> of(Supplier<? extends T> computingFunction) {
Objects.requireNonNull(computingFunction);
if (computingFunction instanceof LazyConstant<? extends T> lc) {
return (LazyConstant<T>) lc;
}
return LazyConstantImpl.ofLazy(computingFunction);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -25,20 +25,21 @@
package java.util;
import jdk.internal.lang.LazyConstantImpl;
import jdk.internal.misc.Unsafe;
import jdk.internal.util.ImmutableBitSetPredicate;
import jdk.internal.vm.annotation.AOTSafeClassInitializer;
import jdk.internal.vm.annotation.ForceInline;
import jdk.internal.vm.annotation.Stable;
import java.lang.LazyConstant;
import java.lang.reflect.Array;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
import java.util.function.Predicate;
/**
* Container class for lazy collections implementations. Not part of the public API.
@ -54,6 +55,7 @@ final class LazyCollections {
// Unsafe allows LazyCollection classes to be used early in the boot sequence
private static final Unsafe UNSAFE = Unsafe.getUnsafe();
@jdk.internal.vm.annotation.TrustFinalFields
@jdk.internal.ValueBased
static final class LazyList<E>
extends ImmutableCollections.AbstractImmutableList<E> {
@ -62,18 +64,17 @@ final class LazyCollections {
private final E[] elements;
// Keeping track of `size` separately reduces bytecode size compared to
// using `elements.length`.
@Stable
private final int size;
@Stable
final FunctionHolder<IntFunction<? extends E>> functionHolder;
@Stable
private final FunctionHolder<IntFunction<? extends E>> functionHolder;
private final Mutexes mutexes;
private final Throwables throwables;
private LazyList(int size, IntFunction<? extends E> computingFunction) {
this.elements = newGenericArray(size);
this.size = size;
this.functionHolder = new FunctionHolder<>(computingFunction, size);
this.mutexes = new Mutexes(size);
this.throwables = new Throwables(size);
super();
}
@ -89,7 +90,7 @@ final class LazyCollections {
}
private E getSlowPath(int i) {
return orElseComputeSlowPath(elements, i, mutexes, i, functionHolder);
return orElseComputeSlowPath(elements, i, mutexes, throwables, i, functionHolder);
}
@Override
@ -109,6 +110,7 @@ final class LazyCollections {
@Override
public int indexOf(Object o) {
Objects.requireNonNull(o);
for (int i = 0; i < size; i++) {
if (Objects.equals(o, get(i))) {
return i;
@ -119,6 +121,7 @@ final class LazyCollections {
@Override
public int lastIndexOf(Object o) {
Objects.requireNonNull(o);
for (int i = size - 1; i >= 0; i--) {
if (Objects.equals(o, get(i))) {
return i;
@ -143,16 +146,14 @@ final class LazyCollections {
}
static final class LazyEnumMap<K extends Enum<K>, V>
@jdk.internal.vm.annotation.TrustFinalFields
private static final class LazyEnumMap<K extends Enum<K>, V>
extends AbstractLazyMap<K, V> {
@Stable
private final Class<K> enumType;
@Stable
// We are using a wrapper class here to be able to use a min value of zero that
// is also stable.
private final Integer min;
@Stable
private final IntPredicate member;
public LazyEnumMap(Set<K> set,
@ -182,29 +183,26 @@ final class LazyCollections {
if (member.test(ordinal)) {
@SuppressWarnings("unchecked")
final K k = (K) key;
return orElseCompute(k, indexForAsInt(k));
return orElseCompute(k, indexFor(k));
}
}
return defaultValue;
}
@ForceInline
@Override
Integer indexFor(K key) {
return indexForAsInt(key);
}
private int indexForAsInt(K key) {
int indexFor(K key) {
return key.ordinal() - min;
}
}
static final class LazyMap<K, V>
@jdk.internal.vm.annotation.TrustFinalFields
private static final class LazyMap<K, V>
extends AbstractLazyMap<K, V> {
// Use an unmodifiable map with known entries that are @Stable. Lookups through this map can be folded because
// it is created using Map.ofEntrie. This allows us to avoid creating a separate hashing function.
@Stable
// it is created using Map.ofEntries. This allows us to avoid creating a separate hashing function.
private final Map<K, Integer> indexMapper;
public LazyMap(Set<K> keys, Function<? super K, ? extends V> computingFunction) {
@ -232,29 +230,34 @@ final class LazyCollections {
@Override public boolean containsKey(Object o) { return indexMapper.containsKey(o); }
@ForceInline
@Override
Integer indexFor(K key) {
// This method will throw an NPE if the key does not exist. So, callers need to
// make sure the key exist before invoking this method.
int indexFor(K key) {
return indexMapper.get(key);
}
}
static sealed abstract class AbstractLazyMap<K, V>
@jdk.internal.vm.annotation.TrustFinalFields
private static abstract sealed class AbstractLazyMap<K, V>
extends ImmutableCollections.AbstractImmutableMap<K, V> {
// This field shadows AbstractMap.keySet which is not @Stable.
@Stable
Set<K> keySet;
private final Mutexes mutexes;
private final Throwables throwables;
private final int size;
private final FunctionHolder<Function<? super K, ? extends V>> functionHolder;
private final Set<Entry<K, V>> entrySet;
// This field shadows AbstractMap.values which is of another type
@Stable
final V[] values;
private final V[] values;
// This field shadows AbstractMap.keySet which is not trusted
private final Set<K> keySet;
// We are using a `long` here to get stable access even in the case
// that the 32-bit hash code is zero.
@Stable
Mutexes mutexes;
@Stable
private final int size;
@Stable
final FunctionHolder<Function<? super K, ? extends V>> functionHolder;
@Stable
private final Set<Entry<K, V>> entrySet;
private long hash;
private AbstractLazyMap(Set<K> keySet,
int size,
@ -264,14 +267,15 @@ final class LazyCollections {
this.functionHolder = new FunctionHolder<>(computingFunction, size);
this.values = newGenericArray(backingSize);
this.mutexes = new Mutexes(backingSize);
super();
this.throwables = new Throwables(backingSize);
this.keySet = keySet;
super();
this.entrySet = LazyMapEntrySet.of(this);
}
// Abstract methods
@Override public abstract boolean containsKey(Object o);
abstract Integer indexFor(K key);
abstract int indexFor(K key);
// Public methods
@Override public final int size() { return size; }
@ -279,6 +283,45 @@ final class LazyCollections {
@Override public final Set<Entry<K, V>> entrySet() { return entrySet; }
@Override public Set<K> keySet() { return keySet; }
@Override
public final boolean containsValue(Object value) {
Objects.requireNonNull(value);
for (K key : keySet) {
if (value.equals(orElseCompute(key, indexFor(key)))) {
return true;
}
}
return false;
}
@Override
public final int hashCode() {
// Racy computation
long h = hash;
if (h == 0) {
// Set a bit in the upper 32-bit region of the `long` to
// cater for the case the lower 32-bit hash is zero.
hash = h = expandToLong(hashCode0());
}
return reduceToInt(h);
}
private int hashCode0() {
int hash = 0;
for (K key : keySet) {
hash += key.hashCode() ^ orElseCompute(key, indexFor(key)).hashCode();
}
return hash;
}
@Override
public final void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (K key : keySet) {
action.accept(key, orElseCompute(key, indexFor(key)));
}
}
@ForceInline
@Override
public final V get(Object key) {
@ -293,15 +336,15 @@ final class LazyCollections {
if (v != null) {
return v;
}
return orElseComputeSlowPath(values, index, mutexes, key, functionHolder);
return orElseComputeSlowPath(values, index, mutexes, throwables, key, functionHolder);
}
@jdk.internal.vm.annotation.TrustFinalFields
@jdk.internal.ValueBased
static final class LazyMapEntrySet<K, V> extends ImmutableCollections.AbstractImmutableSet<Entry<K, V>> {
private static final class LazyMapEntrySet<K, V> extends ImmutableCollections.AbstractImmutableSet<Entry<K, V>> {
// Use a separate field for the outer class in order to facilitate
// a @Stable annotation.
@Stable
// a trusted field.
private final AbstractLazyMap<K, V> map;
private LazyMapEntrySet(AbstractLazyMap<K, V> map) {
@ -318,14 +361,13 @@ final class LazyCollections {
return new LazyMapEntrySet<>(outer);
}
@jdk.internal.vm.annotation.TrustFinalFields
@jdk.internal.ValueBased
static final class LazyMapIterator<K, V> implements Iterator<Entry<K, V>> {
// Use a separate field for the outer class in order to facilitate
// a @Stable annotation.
@Stable
// a trusted field.
private final AbstractLazyMap<K, V> map;
@Stable
private final Iterator<K> keyIterator;
private LazyMapIterator(AbstractLazyMap<K, V> map) {
@ -334,21 +376,22 @@ final class LazyCollections {
super();
}
@Override public boolean hasNext() { return keyIterator.hasNext(); }
@Override public boolean hasNext() { return keyIterator.hasNext(); }
@Override
public Entry<K, V> next() {
final K k = keyIterator.next();
return new LazyEntry<>(k, map, map.functionHolder);
return new LazyEntry<>(k, map);
}
@Override
public void forEachRemaining(Consumer<? super Entry<K, V>> action) {
Objects.requireNonNull(action);
final Consumer<? super K> innerAction =
new Consumer<>() {
@Override
public void accept(K key) {
action.accept(new LazyEntry<>(key, map, map.functionHolder));
action.accept(new LazyEntry<>(key, map));
}
};
keyIterator.forEachRemaining(innerAction);
@ -362,13 +405,12 @@ final class LazyCollections {
}
}
private record LazyEntry<K, V>(K getKey, // trick
AbstractLazyMap<K, V> map,
FunctionHolder<Function<? super K, ? extends V>> functionHolder) implements Entry<K, V> {
private record LazyEntry<K, V>(@Override K getKey, // trick
AbstractLazyMap<K, V> map) implements Entry<K, V> {
@Override public V setValue(V value) { throw ImmutableCollections.uoe(); }
@Override public V getValue() { return map.orElseCompute(getKey, map.indexFor(getKey)); }
@Override public int hashCode() { return hash(getKey()) ^ hash(getValue()); }
@Override public int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); }
@Override public String toString() { return getKey() + "=" + getValue(); }
@Override
@ -379,9 +421,6 @@ final class LazyCollections {
&& Objects.equals(getValue(), e.getValue());
}
private int hash(Object obj) {
return (obj == null) ? 0 : obj.hashCode();
}
}
@Override
@ -389,12 +428,12 @@ final class LazyCollections {
return LazyMapValues.of(this);
}
@jdk.internal.vm.annotation.TrustFinalFields
@jdk.internal.ValueBased
static final class LazyMapValues<K, V> extends ImmutableCollections.AbstractImmutableCollection<V> {
// Use a separate field for the outer class in order to facilitate
// a @Stable annotation.
@Stable
// a trusted field.
private final AbstractLazyMap<K, V> map;
private LazyMapValues(AbstractLazyMap<K, V> map) {
@ -416,7 +455,133 @@ final class LazyCollections {
}
static final class Mutexes {
@jdk.internal.vm.annotation.TrustFinalFields
private static final class LazySet<E>
extends ImmutableCollections.AbstractImmutableSet<E>
implements Set<E> {
private final Map<E, Boolean> map;
// -1 is used as a sentinel value for zero so we can get
// stable access for all `size` values. `size` is always non-negative.
@Stable
private int size;
// We are using a `long` here to get stable access even in the case
// that the 32-bit hash code is zero.
@Stable
private long hash;
public LazySet(Set<? extends E> elementCandidates,
Predicate<? super E> computingFunction) {
this.map = Map.ofLazy(elementCandidates, computingFunction::test);
super();
}
@Override
public boolean contains(Object o) {
return map.getOrDefault(o, Boolean.FALSE).booleanValue();
}
@Override
public int hashCode() {
// Racy computation
long h = hash;
if (h == 0) {
// Set a bit in the upper 32-bit region of the `long` to
// cater for the case the lower 32-bit hash is zero.
hash = h = expandToLong(hashCode0());
}
return reduceToInt(h);
}
private int hashCode0() {
int hash = 0;
for (var e: map.entrySet()) {
if (e.getValue()) {
hash += e.getKey().hashCode();
}
}
return hash;
}
@Override
public Iterator<E> iterator() {
return new LazySetIterator<>(map.entrySet().iterator());
}
@jdk.internal.vm.annotation.TrustFinalFields
static final class LazySetIterator<E> implements Iterator<E> {
private final Iterator<Map.Entry<E, Boolean>> iterator;
E current;
public LazySetIterator(Iterator<Map.Entry<E, Boolean>> iterator) {
this.iterator = iterator;
super();
}
@Override
public boolean hasNext() {
if (current != null) {
return true;
}
while (iterator.hasNext()) {
Map.Entry<E, Boolean> e = iterator.next();
if (e.getValue()) {
current = e.getKey();
return true;
}
}
return false;
}
@Override
public E next() {
E e = current;
if (e != null) {
return consumeCurrent(e);
}
if (!hasNext()) {
throw new NoSuchElementException();
}
return consumeCurrent(current);
}
private E consumeCurrent(E e) {
current = null;
return e;
}
}
@Override
public int size() {
// Racy computation
int s = size;
if (s == 0) {
s = size0();
if (s == 0) {
s = -1;
}
size = s;
}
return s == -1 ? 0 : s;
}
private int size0() {
int size = 0;
for (var e: map.entrySet()) {
if (e.getValue()) {
size++;
}
}
return size;
}
}
private static final class Mutexes {
private static final Object TOMB_STONE = new Object();
@ -431,9 +596,15 @@ final class LazyCollections {
this.counter = new AtomicInteger(length);
}
@ForceInline
private Object acquireMutex(long offset) {
assert mutexes != null;
// Snapshot
var mutexes = this.mutexes;
if (mutexes == null) {
// We have already computed all the elements and if we end up here
// there was at least one unchecked exception thrown by the
// computing function.
return null;
}
// Check if there already is a mutex (Object or TOMB_STONE)
final Object mutex = UNSAFE.getReferenceVolatile(mutexes, offset);
if (mutex != null) {
@ -447,7 +618,7 @@ final class LazyCollections {
private void releaseMutex(long offset) {
// Replace the old mutex with a tomb stone since now the old mutex can be collected.
UNSAFE.putReference(mutexes, offset, TOMB_STONE);
UNSAFE.putReferenceVolatile(mutexes, offset, TOMB_STONE);
if (counter != null && counter.decrementAndGet() == 0) {
mutexes = null;
counter = null;
@ -456,6 +627,33 @@ final class LazyCollections {
}
/** Holds the throwable class names produced by the computing function.
* <p>
* Class names are used instead of Class objects to avoid pinning class loaders after
* a failed computation.
* <p>
* This class is not thread safe across indices. However, it will always be accessed
* under the same monitor for a given index.
*/
private static final class Throwables {
@Stable
final String[] throwables;
Throwables(int size) {
this.throwables = new String[size];
super();
}
Optional<String> get(int index) {
return Optional.ofNullable(throwables[index]);
}
void set(int index, Throwable throwable) {
throwables[index] = throwable.getClass().getName().intern();
}
}
@ForceInline
private static long offsetFor(long index) {
return Unsafe.ARRAY_OBJECT_BASE_OFFSET + Unsafe.ARRAY_OBJECT_INDEX_SCALE * index;
@ -466,75 +664,81 @@ final class LazyCollections {
return (E[]) new Object[length];
}
public static <E> List<E> ofLazyList(int size,
IntFunction<? extends E> computingFunction) {
return new LazyList<>(size, computingFunction);
}
public static <K, V> Map<K, V> ofLazyMap(Set<K> keys,
Function<? super K, ? extends V> computingFunction) {
return new LazyMap<>(keys, computingFunction);
}
@SuppressWarnings("unchecked")
public static <K, E extends Enum<E>, V>
Map<K, V> ofLazyMapWithEnumKeys(Set<K> keys,
Function<? super K, ? extends V> computingFunction) {
// The input set is not empty
final Class<E> enumType = ((E) keys.iterator().next()).getDeclaringClass();
final BitSet bitSet = new BitSet(enumType.getEnumConstants().length);
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (K t : keys) {
final int ordinal = ((E) t).ordinal();
min = Math.min(min, ordinal);
max = Math.max(max, ordinal);
bitSet.set(ordinal);
}
final int backingSize = max - min + 1;
final IntPredicate member = ImmutableBitSetPredicate.of(bitSet);
return (Map<K, V>) new LazyEnumMap<>((Set<E>) keys, enumType, min, backingSize, member, (Function<E, V>) computingFunction);
}
@SuppressWarnings("unchecked")
static <T> T orElseComputeSlowPath(final T[] array,
private static <T> T orElseComputeSlowPath(final T[] array,
final int index,
final Mutexes mutexes,
final Throwables throwables,
final Object input,
final FunctionHolder<?> functionHolder) {
final long offset = offsetFor(index);
final Object mutex = mutexes.acquireMutex(offset);
preventReentry(mutex);
if (mutex == null) {
throwIfPreviousException(index, throwables, input);
// There must be an exception
throw cannotReachHere(functionHolder, input);
}
preventReentry(mutex, input);
synchronized (mutex) {
final T t = array[index]; // Plain semantics suffice here
if (t == null) {
final T newValue = switch (functionHolder.function()) {
case IntFunction<?> iFun -> (T) iFun.apply((int) input);
case Function<?, ?> fun -> ((Function<Object, T>) fun).apply(input);
default -> throw new InternalError("cannot reach here");
};
Objects.requireNonNull(newValue);
// Reduce the counter and if it reaches zero, clear the reference
// to the underlying holder.
functionHolder.countDown();
throwIfPreviousException(index, throwables, input);
try {
final T newValue = switch (functionHolder.function()) {
case IntFunction<?> iFun -> (T) iFun.apply((int) input);
case Function<?, ?> fun -> ((Function<Object, T>) fun).apply(input);
default -> throw cannotReachHere(functionHolder, input);
};
Objects.requireNonNull(newValue);
// The mutex is not reentrant so we know newValue should be returned
set(array, index, mutex, newValue);
// We do not need the mutex anymore
mutexes.releaseMutex(offset);
return newValue;
// The mutex is not reentrant so we know newValue should be returned
set(array, index, mutex, newValue);
return newValue;
} catch (Throwable x) {
throwables.set(index, x);
// Wrap the initial throwable without pinning its class loader.
throw noSuchElementException(x.getClass().getName(), input, x);
} finally {
// Reduce the counter and if it reaches zero, clear the reference
// to the underlying holder.
functionHolder.countDown();
// We do not need the mutex anymore
mutexes.releaseMutex(offset);
}
}
return t;
}
}
static void preventReentry(Object mutex) {
if (Thread.holdsLock(mutex)) {
throw new IllegalStateException("Recursive initialization of a lazy collection is illegal");
private static void throwIfPreviousException(int index, Throwables throwables, Object input) {
final var throwable = throwables.get(index);
if (throwable.isPresent()) {
throw noSuchElementException(throwable.get(), input, null);
}
}
static <T> void set(T[] array, int index, Object mutex, T newValue) {
private static NoSuchElementException noSuchElementException(String throwableName,
Object input,
Throwable cause) {
final String isolatedToString = LazyConstantImpl.isolateToString(input);
var message = "Unable to access the lazy collection because " + throwableName +
" was thrown at initial computation for input '" + isolatedToString + "'";
return new NoSuchElementException(message, cause);
}
private static InternalError cannotReachHere(FunctionHolder<?> functionHolder, Object input) {
return new InternalError("cannot reach here: " + functionHolder.function() + " for " + LazyConstantImpl.isolateToString(input));
}
private static void preventReentry(Object mutex, Object input) {
if (Thread.holdsLock(mutex)) {
throw new IllegalStateException("Recursive initialization of a lazy collection is illegal: " + LazyConstantImpl.isolateToString(input));
}
}
private static <T> void set(T[] array, int index, Object mutex, T newValue) {
assert Thread.holdsLock(mutex) : index + "didn't hold " + mutex;
// We know we hold the monitor here so plain semantic is enough
// This is an extra safety net to emulate a CAS op.
@ -551,7 +755,7 @@ final class LazyCollections {
* @param <U> the underlying function type
*/
@AOTSafeClassInitializer
static final class FunctionHolder<U> {
private static final class FunctionHolder<U> {
private static final long COUNTER_OFFSET = UNSAFE.objectFieldOffset(FunctionHolder.class, "counter");
@ -581,4 +785,50 @@ final class LazyCollections {
}
}
// Methods for supporting stable `int` values using a `long` field.
private static long expandToLong(int value) {
return (value + (1L << 33));
}
private static int reduceToInt(long value) {
return (int) value;
}
// Factories
static <E> List<E> ofLazyList(int size,
IntFunction<? extends E> computingFunction) {
return new LazyList<>(size, computingFunction);
}
static <K, V> Map<K, V> ofLazyMap(Set<K> keys,
Function<? super K, ? extends V> computingFunction) {
return new LazyMap<>(keys, computingFunction);
}
static <E> Set<E> ofLazySet(Set<? extends E> elementCandidates,
Predicate<? super E> computingFunction) {
return new LazySet<>(elementCandidates, computingFunction);
}
@SuppressWarnings("unchecked")
static <K, E extends Enum<E>, V> Map<K, V> ofLazyMapWithEnumKeys(Set<K> keys,
Function<? super K, ? extends V> computingFunction) {
// The input set is not empty
final Class<E> enumType = ((E) keys.iterator().next()).getDeclaringClass();
final BitSet bitSet = new BitSet(enumType.getEnumConstants().length);
int min = Integer.MAX_VALUE;
int max = Integer.MIN_VALUE;
for (K t : keys) {
final int ordinal = ((E) t).ordinal();
min = Math.min(min, ordinal);
max = Math.max(max, ordinal);
bitSet.set(ordinal);
}
final int backingSize = max - min + 1;
final IntPredicate member = ImmutableBitSetPredicate.of(bitSet);
return (Map<K, V>) new LazyEnumMap<>((Set<E>) keys, enumType, min, backingSize, member, (Function<E, V>) computingFunction);
}
}

View File

@ -1200,23 +1200,36 @@ public interface List<E> extends SequencedCollection<E> {
* {@return a new lazily computed list of 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 lazily computed via the
* provided {@code computingFunction} when they are first accessed
* for which the elements are lazily computed via the provided
* {@code computingFunction} when they are first accessed
* (e.g., via {@linkplain List#get(int) List::get}).
* <p>
* The provided computing function is guaranteed to be successfully
* The provided computing function is guaranteed to be
* 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 the computing function completes abnormally.
* <p>
* If invoking the provided computing function throws an exception, it is rethrown
* to the initial caller and no value for the element is recorded.
* If evaluation of the provided computing function throws an unchecked exception (for
* an index), the lazy element is not initialized but instead transitions to an error
* state whereafter a {@linkplain NoSuchElementException} is thrown with the unchecked
* exception as a cause. Subsequent {@linkplain List#get(int) List::get} calls for the
* same index throw {@linkplain NoSuchElementException} (without ever invoking the
* computing function again) with no cause and with a message that includes the name
* of the original unchecked exception's class.
* <p>
* If the provided computing function returns {@code null},
* a {@linkplain NullPointerException} will be thrown. Hence, just like other
* unmodifiable lists created via the {@code List::of} factories, a lazy list
* cannot contain {@code null} elements. Clients that want to use nullable elements
* can wrap elements into an {@linkplain Optional} holder.
* All failures are handled in this way. There are two special cases that cause
* unchecked exceptions to be thrown:
* <p>
* If the computing function returns {@code null},
* a {@linkplain NoSuchElementException} (with a {@linkplain NullPointerException} as
* a cause) will be thrown. Hence, just like other unmodifiable lists created via the
* {@code List::of} factories, a lazy list can never contain {@code null}
* elements. Clients that want to use nullable elements can wrap elements into an
* {@linkplain Optional} holder.
* <p>
* If the computing function recursively invokes itself (for the same index) via the
* returned lazy list, a {@linkplain NoSuchElementException}
* (with an {@linkplain IllegalStateException} as a cause) will be thrown.
* <p>
* The elements of any {@link List#subList(int, int) subList()} or
* {@link List#reversed()} views of the returned list are also lazily computed.
@ -1224,31 +1237,78 @@ public interface List<E> extends SequencedCollection<E> {
* The returned list and its {@link List#subList(int, int) subList()} or
* {@link List#reversed()} views implement the {@link RandomAccess} interface.
* <p>
* If the provided computing function recursively calls itself via the returned
* lazy list for the same index, an {@linkplain IllegalStateException}
* will be thrown.
* <p>
* The returned list's {@linkplain Object Object methods};
* {@linkplain Object#equals(Object) equals()},
* {@linkplain Object#hashCode() hashCode()}, and
* {@linkplain Object#toString() toString()} methods may trigger initialization of
* one or more lazy elements.
* one or more lazy elements. If initialization fails for at least one element,
* the {@linkplain Object Object methods} may throw {@linkplain NoSuchElementException}.
* <p>
* The returned lazy list strongly references its computing
* function used to compute elements at least as long as there are uninitialized
* elements.
* <p>
* The returned List is <em>not</em> {@linkplain Serializable}.
* <p>
* Here is an example involving an application that maintains three separate
* {@code OrderController} components. Depending on a thread's id, one of the
* three {@code OrderController} components will be selected. By using a lazy list,
* we ensure that at most three {@code OrderController} instances are created. Once
* created, the component retrieval is eligible for constant folding by the JVM:
* {@snippet lang = java:
* class Application {
*
* @implNote after all elements have been initialized successfully, the computing
* function is no longer strongly referenced and becomes eligible for
* garbage collection.
* private static final int POOL_SIZE = 3;
*
* static final List<OrderController> ORDERS
* = List.ofLazy(POOL_SIZE, _ -> new OrderController());
*
* public static OrderController orders() {
* long index = Thread.currentThread().threadId() % POOL_SIZE;
* return ORDERS.get((int)index);
* }
*
* // Eligible for constant folding
* OrderController orders = orders();
* }
* }
* <p>
* The returned {@code List<E>} can be thought of as a list backed by a
* {@code List<LazyConstant<E>>} field and where the {@linkplain List#get(int)}
* operation is equivalent to:
* {@snippet lang = java:
* class LazyList<E> extends AbstractList<E> {
*
* private final List<LazyConstant<E>> backingList;
*
* public LazyList(int size, IntFunction<E> computingFunction) {
* this.backingList = IntStream.range(0, size)
* .mapToObj(i -> LazyConstant.of(() -> computingFunction.apply(i)))
* .toList();
* }
*
* @Override
* public E get(int index) {
* return backingList.get(index).get();
* }
* }
*}
* Except, performance and storage efficiency might be better.
* <p>
* Elements in the returned list are eligible for certain performance optimizations
* such as <em>constant folding</em> as described in
* {@linkplain LazyConstant##performance LazyConstant}.
*
* @implNote after all elements have been initialized successfully or transitioned to
* an error state, the computing function is no longer strongly referenced
* and becomes eligible for garbage collection.
*
* @param size the size of the returned lazy list
* @param computingFunction to invoke whenever an element is first accessed
* (may not return {@code null})
* @param <E> the type of elements in the returned list
* @throws IllegalArgumentException if the provided {@code size} is negative.
* @throws NullPointerException if the provided {@code computingFunction} is {@code null}
*
* @see LazyConstant
* @since 26

View File

@ -1759,50 +1759,122 @@ public interface Map<K, V> {
* provided {@code computingFunction} when they are first accessed
* (e.g., via {@linkplain Map#get(Object) Map::get}).
* <p>
* The provided computing function is guaranteed to be successfully invoked
* The provided computing function is guaranteed to be 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
* threads accessing a value already under computation will block until a value
* is computed or the computing function completes abnormally.
* <p>
* If invoking the provided computing function throws an exception, it
* is rethrown to the initial caller and no value associated with the provided key
* is recorded.
* If evaluation of the provided computing function throws an unchecked exception (for
* a key), the lazy value is not initialized but instead transitions to an error
* state whereafter a {@linkplain NoSuchElementException} is thrown with the unchecked
* exception as a cause. Subsequent {@linkplain Map#get(Object) Map::get} calls for
* the same key throw {@linkplain NoSuchElementException} (without ever invoking the
* computing function again) with no cause and with a message that includes the name
* of the original unchecked exception's class.
* <p>
* If the provided computing function returns {@code null},
* a {@linkplain NullPointerException} will be thrown. Hence, just like other
* unmodifiable maps created via the {@code Map::of} factories, a lazy map
* cannot contain {@code null} values. Clients that want to use nullable values can
* wrap values into an {@linkplain Optional} holder.
* All failures are handled in this way. There are two special cases that cause
* unchecked exceptions to be thrown:
* <p>
* If the computing function returns {@code null},
* a {@linkplain NoSuchElementException} (with a {@linkplain NullPointerException} as
* a cause) will be thrown. Hence, just like other unmodifiable maps created via the
* {@code Map::of} factories, a lazy map can never contain {@code null} values.
* Clients that want to use nullable values can wrap elements into an
* {@linkplain Optional} holder.
* <p>
* If the computing function recursively invokes itself (for the same key) via the
* returned lazy map, a {@linkplain NoSuchElementException}
* (with an {@linkplain IllegalStateException} as a cause) will be thrown.
* <p>
* The values of any {@link Map#values()} or {@link Map#entrySet()} views of
* the returned map are also lazily computed.
* <p>
* If the provided computing function recursively calls itself via
* the returned lazy map for the same key, an {@linkplain IllegalStateException}
* will be thrown.
* <p>
* The returned map's {@linkplain Object Object methods};
* {@linkplain Object#equals(Object) equals()},
* {@linkplain Object#hashCode() hashCode()}, and
* {@linkplain Object#toString() toString()} methods may trigger initialization of
* one or more lazy elements.
* one or more lazy values. If initialization fails for at least one value,
* the {@linkplain Object Object methods} may throw {@linkplain NoSuchElementException}.
* <p>
* The returned lazy map strongly references its underlying
* computing function used to compute values at least as long as there are
* uncomputed values.
* <p>
* The returned Map is <em>not</em> {@linkplain Serializable}.
* <p>
* If the provided {@code Set} of {@code keys} is subsequently modified, the returned
* {@code Map} will not reflect such modifications.
* <p>
* The {@code Set} of {@code keys} must use {@linkplain Set#equals(Object) equals()}
* as its equivalence relation, or its comparison method must be consistent with
* equals, otherwise the behavior is unspecified.
* <p>
* Here is an example involving an application that caches the values returned by some
* {@code expensiveOperation(int param)} for a given set of input parameters. By
* using a lazy map, we ensure that the {@code expensiveOperation(int param)} is
* called at most once per distinct input parameter. Once created, the retrieval of
* values is eligible for constant folding by the JVM:
* {@snippet lang = java:
* class Application {
*
* @implNote after all values have been initialized successfully, the computing
* function is no longer strongly referenced and becomes eligible for
* garbage collection.
* private static final Map<Integer, Double> CACHE
* = Map.ofLazy(Set.of(0, 1, 3, 42, 97), param -> expensiveOperation(param));
*
* public static Optional<Double> cachedExpensiveOperation(int param) {
* return Optional.ofNullable(CACHE.get(param));
* }
*
* private static double expensiveOperation(int param) {
* // Calculate the value ...
* }
*
* // Eligible for constant folding
* double val = cachedExpensiveOperation(42).orElseThrow();
*
* }
* }
* <p>
* The returned {@code Map<K, V>} can be thought of as a map backed by a
* {@code Map<K, LazyConstant<V>>} field and where the {@linkplain Map#get(Object)}
* operation is equivalent to:
* {@snippet lang = java:
* class LazyMap<K, V> extends AbstractMap<K, V> {
*
* private final Map<K, LazyConstant<V>> backingMap;
*
* public LazyMap(Set<K> keys, Function<K, V> computingFunction) {
* this.backingMap = keys.stream()
* .collect(Collectors.toUnmodifiableMap(
* Function.identity(),
* k -> LazyConstant.of(() -> computingFunction.apply(k))));
* }
*
* @Override
* public V get(Object key) {
* var lazyConstant = backingMap.get(key);
* return lazyConstant == null
* ? null
* : lazyConstant.get();
* }
* }
*}
* Except, performance and storage efficiency might be better.
* <p>
* Values in the returned map are eligible for certain performance optimizations
* such as <em>constant folding</em> as described in
* {@linkplain LazyConstant##performance LazyConstant}.
*
* @implNote after all values have been initialized successfully or transitioned to
* an error state, the computing function is no longer strongly referenced
* and becomes eligible for garbage collection.
*
* @param keys the (non-null) keys in the returned computed map
* @param computingFunction to invoke whenever an associated value is first accessed
* @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 keys} is {@code null}
* or if the set of {@code keys} contains a {@code null} element.
* @throws NullPointerException if the provided set of {@code keys} is {@code null},
* if the set of {@code keys} contains a {@code null} element, or
* if the provided {@code computingFunction} is {@code null}
*
* @see LazyConstant
* @since 26

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 1997, 2023, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 1997, 2026, 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
@ -25,6 +25,11 @@
package java.util;
import jdk.internal.javac.PreviewFeature;
import java.io.Serializable;
import java.util.function.Predicate;
/**
* A collection that contains no duplicate elements. More formally, sets
* contain no pair of elements {@code e1} and {@code e2} such that
@ -77,7 +82,7 @@ package java.util;
* Set to behave inconsistently or its contents to appear to change.
* <li>They disallow {@code null} elements. Attempts to create them with
* {@code null} elements result in {@code NullPointerException}.
* <li>They are serializable if all elements are serializable.
* <li>Unless otherwise specified, they are serializable if all elements are serializable.
* <li>They reject duplicate elements at creation time. Duplicate elements
* passed to a static factory method result in {@code IllegalArgumentException}.
* <li>The iteration order of set elements is unspecified and is subject to change.
@ -734,4 +739,158 @@ public interface Set<E> extends Collection<E> {
return (Set<E>)Set.of(new HashSet<>(coll).toArray());
}
}
/**
* {@return a new lazily computed set whose logical membership for each distinct
* element candidate in the set of {@code elementCandidates} is
* computed via the provided {@code computingFunction} on demand}
* <p>
* In the following, the term <em>membership status</em> is used to indicate whether
* an element belongs to the returned set or not. That is, if the membership status
* for element {@code E} is {@code true}, then {@code E} <em>is</em> a member of the
* returned set. Conversely, if the membership status for element {@code E} is
* {@code false}, then {@code E} <em>is not</em> a member of the returned set.
* <p>
* The returned set is an {@linkplain Collection##unmodifiable unmodifiable} set. The
* elements in the returned set are derived from the element candidates given at
* construction in combination with evaluating each element's membership status. The
* set's element membership statuses are lazily computed via the provided
* {@code computingFunction} when first accessed (e.g., via
* {@linkplain Set#contains(Object) Set::contains}). Once the membership status has
* been successfully computed for an element candidate, the associated membership
* status is initialized (i.e., either as <em>a logical member</em> or as
* <em>a logical non-member</em>).
* <p>
* The provided computing function is guaranteed to be invoked at most once per
* element candidate, even in a multi-threaded environment. Competing threads
* accessing an element candidate already under membership status computation will
* block until the membership status of the element candidate is computed or the
* computing function completes abnormally.
* <p>
* If evaluation of the provided computing function throws an unchecked exception (for
* an element candidate), the lazy membership status is not initialized but instead
* transitions to an error state whereafter a {@linkplain NoSuchElementException} is
* thrown with the unchecked exception as a cause. Subsequent
* {@linkplain Set#contains(Object) Set::contains} calls for the same membership
* candidate throw {@linkplain NoSuchElementException} (without ever invoking the
* computing function again) with no cause and with a message that includes the name
* of the original unchecked exception's class.
* <p>
* All failures are handled in this way. There is a special case that causes
* unchecked exceptions to be thrown:
* <p>
* If the computing function recursively invokes itself (for the same membership
* candidate) via the returned lazy set, a {@linkplain NoSuchElementException}
* (with an {@linkplain IllegalStateException} as a cause) will be thrown.
* <p>
* The returned set's {@linkplain Object Object methods};
* {@linkplain Object#equals(Object) equals()},
* {@linkplain Object#hashCode() hashCode()}, and
* {@linkplain Object#toString() toString()} methods may trigger initialization of
* one or more lazy elements. If initialization fails for at least one element,
* the {@linkplain Object Object methods} may throw {@linkplain NoSuchElementException}.
* <p>
* The returned lazy set strongly references its underlying
* computing function used to compute membership status at least as long as there are
* uncomputed element candidates.
* <p>
* The returned Set is <em>not</em> {@linkplain Serializable}.
* <p>
* If the provided {@code Set} of {@code elementCandidates} is subsequently modified,
* the returned {@code Set} will not reflect such modifications.
* <p>
* The {@code Set} of {@code elementCandidates} must use
* {@linkplain Set#equals(Object) equals()} as its equivalence relation, or its
* comparison method must be consistent with {@code equals()}, otherwise the behavior
* is unspecified.
* <p>
* Here is an example involving an application that manages various configurable
* options -- commonly referred to as "switches" -- that control its behavior. The
* state of these switches can be determined through the command line, a configuration
* file, or even a database connection. By using a lazy set, we ensure that the states
* of these switches are evaluated only once. Once computed, the results are eligible
* for constant folding by the JVM:
* {@snippet lang = java:
* class Application {
*
* enum Option {VERBOSE, DRY_RUN, STRICT}
*
* // Lazily initialized Set of Options
* static final Set<Option> OPTIONS =
* Set.ofLazy(EnumSet.allOf(Option.class), Application::isEnabled);
*
* // Return true when the given Option is enabled
* private static boolean isEnabled(Option option) {
* // Parse command line, read configuration file, load database
* ...
* }
*
* public static void process() {
* // The if condition (and subsequent eliminated branch) is
* // eligible for constant folding (and code elimination).
* if (OPTIONS.contains(Option.DRY_RUN)) {
* // Skip processing in DRY_RUN mode
* return;
* }
* // Actual processing logic
* }
*
* }
* }
* <p>
* The returned {@code Set<E>} can be thought of as a set backed by a
* {@code Map<E, LazyConstant<Boolean>>} field and where the {@linkplain Set#contains(Object)}
* operation is equivalent to:
* {@snippet lang = java:
* class LazySet<E> extends AbstractCollection<E> implements Set<E> {
*
* private final Map<E, LazyConstant<Boolean>> backingMap;
*
* public LazySet(Set<E> elementCandidates, Predicate<E> computingFunction) {
* this.backingMap = elementCandidates.stream()
* .collect(Collectors.toUnmodifiableMap(
* Function.identity(),
* k -> LazyConstant.of(() -> computingFunction.test(k))));
* }
*
* @Override
* public boolean contains(Object o) {
* var lazyConstant = backingMap.get(o);
* return lazyConstant == null
* ? false
* : lazyConstant.get();
* }
* }
*}
* Except, performance and storage efficiency might be better.
* <p>
* Elements in the returned set are eligible for certain performance optimizations
* such as <em>constant folding</em> as described in
* {@linkplain LazyConstant##performance LazyConstant}.
*
* @implNote after all element membership statuses have been initialized
* successfully or transitioned to an error state, the computing function
* is no longer strongly referenced and becomes eligible for garbage
* collection.
*
* @param elementCandidates the (non-null) element candidates to be evaluated
* @param computingFunction to invoke whenever the membership status of an element
* candidate is first computed
* @param <E> the type of elements maintained by the returned set
* @throws NullPointerException if the provided set of {@code elementCandidates} is
* {@code null}, if the set of {@code elementCandidates}
* contains a {@code null} element, or if the provided
* {@code computingFunction} is {@code null}
*
* @see LazyConstant
* @since 27
*/
@PreviewFeature(feature = PreviewFeature.Feature.LAZY_CONSTANTS)
static <E> Set<E> ofLazy(Set<? extends E> elementCandidates,
Predicate<? super E> computingFunction) {
Objects.requireNonNull(elementCandidates);
Objects.requireNonNull(computingFunction);
return LazyCollections.ofLazySet(elementCandidates, computingFunction);
}
}

View File

@ -66,7 +66,7 @@ public @interface PreviewFeature {
public enum Feature {
@JEP(number=533, title="Structured Concurrency", status="Seventh Preview")
STRUCTURED_CONCURRENCY,
@JEP(number = 526, title = "Lazy Constants", status = "Second Preview")
@JEP(number = 531, title = "Lazy Constants", status = "Third Preview")
LAZY_CONSTANTS,
@JEP(number=524, title="PEM Encodings of Cryptographic Objects",
status="Second Preview")

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -30,6 +30,7 @@ import jdk.internal.vm.annotation.AOTSafeClassInitializer;
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;
@ -67,12 +68,13 @@ public final class LazyConstantImpl<T> implements LazyConstant<T> {
// The field needs to be `volatile` as a lazy constant can be
// created by one thread and computed by another thread.
// After the function is successfully invoked, the field is set to
// `null` to allow the function to be collected.
@Stable
private volatile Supplier<? extends T> computingFunction;
// `null` to allow the function to be collected. If the function fails, the field is
// set to the fully qualified name of the exception class. We are not storing the
// exception class as that would have pinned the class loader of the exception.
private volatile Object computingFunctionOrExceptionType;
private LazyConstantImpl(Supplier<? extends T> computingFunction) {
this.computingFunction = computingFunction;
this.computingFunctionOrExceptionType = computingFunction;
}
@ForceInline
@ -82,34 +84,53 @@ public final class LazyConstantImpl<T> implements LazyConstant<T> {
return (t != null) ? t : getSlowPath();
}
@SuppressWarnings("unchecked")
private T getSlowPath() {
preventReentry();
synchronized (this) {
T t = getAcquire();
if (t == null) {
t = computingFunction.get();
Objects.requireNonNull(t);
setRelease(t);
// Allow the underlying supplier to be collected after successful use
computingFunction = null;
switch (computingFunctionOrExceptionType) {
case Supplier<?> computingFunction -> {
try {
@SuppressWarnings("unchecked")
final T newT = (T) computingFunction.get();
t = newT;
Objects.requireNonNull(t);
setRelease(t);
// Allow the underlying supplier to be collected after
// a successful initialization
computingFunctionOrExceptionType = null;
} catch (Throwable ex) {
// Release the original computing function and replace it with
// an exception marker
final String exceptionType = ex.getClass().getName().intern();
computingFunctionOrExceptionType = exceptionType;
throw unableToAccessConstant(exceptionType, ex);
}
}
case String exceptionType ->
throw unableToAccessConstant(exceptionType, null);
default ->
throw new InternalError("Cannot reach here");
}
}
return t;
}
}
static NoSuchElementException unableToAccessConstant(String exceptionType, Throwable cause) {
return new NoSuchElementException("Unable to access the constant because " +
exceptionType + " was thrown at initial computation", cause);
}
// For testing only
@ForceInline
@Override
public T orElse(T other) {
final T t = getAcquire();
return (t == null) ? other : t;
}
@ForceInline
@Override
public boolean isInitialized() {
return getAcquire() != null;
}
@Override
public String toString() {
return super.toString() + "[" + toStringSuffix() + "]";
@ -121,16 +142,19 @@ public final class LazyConstantImpl<T> implements LazyConstant<T> {
return "(this LazyConstant)";
} else if (t != null) {
return t.toString();
} else {
// Volatile read
final Object cf = computingFunctionOrExceptionType;
// There could be a race here
if (cf != null) {
return (cf instanceof Supplier<?> supplier)
? "computing function=" + isolateToString(supplier)
: "failed with=" + cf;
}
// As we know `computingFunction` is `null` or via a volatile read, we
// can now be sure that this lazy constant is initialized
return getAcquire().toString();
}
// Volatile read
final Supplier<? extends T> cf = computingFunction;
// There could be a race here
if (cf != null) {
return "computing function=" + computingFunction.toString();
}
// As we know `computingFunction` is `null` via a volatile read, we
// can now be sure that this lazy constant is initialized
return getAcquire().toString();
}
@ -160,7 +184,16 @@ public final class LazyConstantImpl<T> implements LazyConstant<T> {
private void preventReentry() {
if (Thread.holdsLock(this)) {
throw new IllegalStateException("Recursive invocation of a LazyConstant's computing function: " + computingFunction);
throw new IllegalStateException("Recursive invocation of a LazyConstant's computing function: " + isolateToString(computingFunctionOrExceptionType));
}
}
public static String isolateToString(Object input) {
// Protect against user-controlled `input.toString` methods that might throw or recurse.
try {
return input.toString();
} catch (Throwable t) {
return Objects.toIdentityString(input);
}
}

View File

@ -0,0 +1,129 @@
/*
* Copyright (c) 2026, 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 Check LazyConstant and lazy collection constant folding
* @modules java.base/jdk.internal.lang
* @library /test/lib /
* @enablePreview
* @run main ${test.main.class}
*/
package compiler.stable;
import compiler.lib.ir_framework.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class LazyConstantsIrTest {
public static void main(String[] args) {
new TestFramework()
.addTestClassesToBootClassPath()
.addFlags(
"--enable-preview",
"-XX:+UnlockExperimentalVMOptions")
.addCrossProductScenarios(
Set.of("-XX:+TieredCompilation", "-XX:-TieredCompilation"))
.setDefaultWarmup(5000)
.start();
}
static final int THE_VALUE = 42;
static final LazyConstant<Integer> LAZY_CONSTANT = LazyConstant.of(() -> THE_VALUE);
static final List<Integer> LAZY_LIST = List.ofLazy(1, _ -> THE_VALUE);
static final Set<Integer> LAZY_SET = Set.ofLazy(Set.of(THE_VALUE), _ -> true);
static final Map<Integer, Integer> LAZY_MAP = Map.ofLazy(Set.of(0), _ -> THE_VALUE);
// For all tests:
// * Access should be folded.
// * No barriers expected for a folded access (as opposed to a non-folded).
// Lazy constant
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR })
static int foldLazyConstantGet() {
return LAZY_CONSTANT.get();
}
// Lazy list
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static int foldLazyListGet() {
return LAZY_LIST.get(0);
}
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static int foldLazyListSize() {
return LAZY_LIST.size();
}
// Lazy map
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static int foldLazyMapGet() {
return LAZY_MAP.get(0);
}
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static int foldLazyMapSize() {
return LAZY_MAP.size();
}
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static int foldLazyMapHashCode() {
return LAZY_MAP.hashCode();
}
// Lazy set
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static boolean foldLazySetContains() {
return LAZY_SET.contains(THE_VALUE);
}
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static int foldLazySetHashCode() {
return LAZY_SET.hashCode();
}
@Test
@IR(failOn = { IRNode.LOAD, IRNode.MEMBAR})
static int foldLazySetSize() {
return LAZY_SET.size();
}
}

View File

@ -58,15 +58,6 @@ final class DemoContainerInjectionTest {
assertContainerPopulated(container);
}
@Test
void SettableComponents() {
SettableContainer container = SettableScratchContainer.of(Set.of(Foo.class, Bar.class));
container.set(Foo.class, new FooImpl());
container.set(Bar.class, new BarImpl());
assertContainerPopulated(container);
}
@Test
void ProviderComponents() {
Container container = ProviderContainer.of(Map.of(
@ -93,10 +84,6 @@ final class DemoContainerInjectionTest {
<T> T get(Class<T> type);
}
interface SettableContainer extends Container {
<T> void set(Class<T> type, T implementation);
}
record ComputedContainer(Map<Class<?>, ?> components) implements Container {
@Override
@ -110,26 +97,6 @@ final class DemoContainerInjectionTest {
}
record SettableScratchContainer(Map<Class<?>, Object> scratch, Map<Class<?>, ?> components) implements SettableContainer {
@Override
public <T> void set(Class<T> type, T implementation) {
if (scratch.putIfAbsent(type, type.cast(implementation)) != null) {
throw new IllegalStateException("Can only set once for " + type);
}
}
@Override
public <T> T get(Class<T> type) {
return type.cast(components.get(type));
}
static SettableContainer of(Set<Class<?>> components) {
Map<Class<?>, Object> scratch = new ConcurrentHashMap<>();
return new SettableScratchContainer(scratch, Map.ofLazy(components, scratch::get));
}
}
record ProviderContainer(Map<Class<?>, ?> components) implements Container {

View File

@ -1,83 +0,0 @@
/*
* 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 Test of a demo of an imperative stable value based on a lazy constant
* @enablePreview
* @run junit DemoImperativeTest
*/
import org.junit.jupiter.api.Test;
import java.util.concurrent.atomic.AtomicReference;
import static org.junit.jupiter.api.Assertions.*;
final class DemoImperativeTest {
interface ImperativeStableValue<T> {
T orElse(T other);
boolean isSet();
boolean trySet(T t);
T get();
static <T> ImperativeStableValue<T> of() {
var scratch = new AtomicReference<T>();
return new Impl<>(scratch, LazyConstant.of(scratch::get));
}
}
private record Impl<T>(AtomicReference<T> scratch,
LazyConstant<T> underlying) implements ImperativeStableValue<T> {
@Override
public boolean trySet(T t) {
final boolean result = scratch.compareAndSet(null, t);
if (result) {
// Actually set the value
get();
}
return result;
}
@Override public T orElse(T other) { return underlying.orElse(other); }
@Override public boolean isSet() { return underlying.isInitialized(); }
@Override public T get() { return underlying.get(); }
}
@Test
void basic() {
var stableValue = ImperativeStableValue.<Integer>of();
assertFalse(stableValue.isSet());
assertEquals(13, stableValue.orElse(13));
assertTrue(stableValue.trySet(42));
assertFalse(stableValue.trySet(13));
assertTrue(stableValue.isSet());
assertEquals(42, stableValue.get());
assertEquals(42, stableValue.orElse(13));
}
}

View File

@ -0,0 +1,184 @@
/*
* Copyright (c) 2026, 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 LazyConstant should not retain throwable classes after failed computation
* @enablePreview
* @modules java.base/java.lang.ref:open
* @library /test/lib
* @build jdk.test.whitebox.WhiteBox
* @requires vm.opt.final.ClassUnloading
* @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
* @run main/othervm
* -Xbootclasspath/a:.
* -XX:+UnlockDiagnosticVMOptions
* -XX:+WhiteBoxAPI
* LazyConstantClassUnloading
*/
import jdk.test.lib.Utils;
import jdk.test.whitebox.WhiteBox;
import java.io.ByteArrayOutputStream;
import java.lang.LazyConstant;
import java.lang.ref.WeakReference;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.NoSuchElementException;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class LazyConstantClassUnloading {
private static final String THROWABLE_NAME = "test.lazyconstant.GeneratedProblem";
private static final String SUPPLIER_NAME = "test.lazyconstant.ThrowingSupplier";
private static WhiteBox wb;
public static void main(String[] args) throws Exception {
TestState state = createFailedLazyConstant();
if (!waitFor(() -> state.loaderRef().refersTo(null), Utils.adjustTimeout(4_000L))) {
throw new AssertionError("The throwing supplier class loader was not unloaded");
}
assertLazyAccessFails(state.lazyConstant(), THROWABLE_NAME);
}
private static TestState createFailedLazyConstant() throws Exception {
Path sourceDir = Files.createTempDirectory("lazy-constant-throwables-src");
Path classesDir = Files.createTempDirectory("lazy-constant-throwables-classes");
writeSource(sourceDir, "GeneratedProblem.java", """
package test.lazyconstant;
public class GeneratedProblem extends RuntimeException {
}
""");
writeSource(sourceDir, "ThrowingSupplier.java", """
package test.lazyconstant;
import java.util.function.Supplier;
public class ThrowingSupplier implements Supplier<String> {
@Override
public String get() {
throw new GeneratedProblem();
}
}
""");
compile(sourceDir, classesDir);
URLClassLoader loader = new URLClassLoader(new URL[] { classesDir.toUri().toURL() },
LazyConstantClassUnloading.class.getClassLoader());
WeakReference<ClassLoader> loaderRef = new WeakReference<>(loader);
Class<?> supplierClass = Class.forName(SUPPLIER_NAME, true, loader);
@SuppressWarnings("unchecked")
Supplier<String> supplier =
(Supplier<String>) supplierClass.getConstructor().newInstance();
LazyConstant<?> lazyConstant = LazyConstant.of(supplier);
assertLazyAccessFails(lazyConstant, THROWABLE_NAME);
supplier = null;
supplierClass = null;
loader.close();
loader = null;
return new TestState(lazyConstant, loaderRef);
}
private static void writeSource(Path sourceDir, String fileName, String source) throws Exception {
Path file = sourceDir.resolve("test/lazyconstant").resolve(fileName);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
private static void compile(Path sourceDir, Path classesDir) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new AssertionError("No system Java compiler available");
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
int exitCode = compiler.run(null, output, output,
"-d", classesDir.toString(),
sourceDir.resolve("test/lazyconstant/GeneratedProblem.java").toString(),
sourceDir.resolve("test/lazyconstant/ThrowingSupplier.java").toString());
if (exitCode != 0) {
throw new AssertionError("Compilation failed: " + output);
}
}
private static void assertLazyAccessFails(LazyConstant<?> lazyConstant, String throwableName) {
var x = assertThrows(NoSuchElementException.class, () -> lazyConstant.get());
var message = x.getMessage();
assertTrue(message.contains(throwableName), "Missing throwable name in message: " + message);
}
private static boolean waitFor(BooleanSupplier condition, long timeoutMillis) {
final long deadline = System.currentTimeMillis() + timeoutMillis;
wb = WhiteBox.getWhiteBox();
wb.fullGC();
boolean refProResult;
boolean conditionValue;
try {
do {
refProResult = wb.waitForReferenceProcessing();
conditionValue = condition.getAsBoolean();
if (System.currentTimeMillis() > deadline) {
throw new AssertionError("Timed out waiting for reference");
}
} while (refProResult || !conditionValue);
return conditionValue;
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
static <X> X assertThrows(Class<X> type, Runnable runnable) {
try {
runnable.run();
} catch (Throwable t) {
if (t.getClass().equals(type)) {
return (X) t;
}
throw new AssertionError("Expected " + type + " to be thrown", t);
}
throw new AssertionError("Expected " + type + " to be thrown but nothing was thrown.");
}
static void assertTrue(boolean value, String message) {
if (!value) {
throw new AssertionError(message);
}
}
private record TestState(LazyConstant<?> lazyConstant, WeakReference<ClassLoader> loaderRef) { }
}

View File

@ -30,6 +30,7 @@
* @run junit LazyConstantSafePublicationTest
*/
import jdk.internal.lang.LazyConstantImpl;
import jdk.test.lib.Utils;
import org.junit.jupiter.api.Test;
@ -64,18 +65,18 @@ final class LazyConstantSafePublicationTest {
static final class Consumer implements Runnable {
final LazyConstant<Holder>[] constants;
final LazyConstantImpl<Holder>[] constants;
final int[] observations = new int[SIZE];
int i = 0;
public Consumer(LazyConstant<Holder>[] constants) {
public Consumer(LazyConstantImpl<Holder>[] constants) {
this.constants = constants;
}
@Override
public void run() {
for (; i < SIZE; i++) {
LazyConstant<Holder> s = constants[i];
LazyConstantImpl<Holder> s = constants[i];
Holder h;
// Wait until the ComputedConstant has a holder value
while ((h = s.orElse(null)) == null) { Thread.onSpinWait();}
@ -91,15 +92,15 @@ final class LazyConstantSafePublicationTest {
static final class Producer implements Runnable {
final LazyConstant<Holder>[] constants;
final LazyConstantImpl<Holder>[] constants;
public Producer(LazyConstant<Holder>[] constants) {
public Producer(LazyConstantImpl<Holder>[] constants) {
this.constants = constants;
}
@Override
public void run() {
LazyConstant<Holder> s;
LazyConstantImpl<Holder> s;
long deadlineNs = System.nanoTime();
for (int i = 0; i < SIZE; i++) {
s = constants[i];
@ -114,7 +115,7 @@ final class LazyConstantSafePublicationTest {
@Test
void mainTest() {
final LazyConstant<Holder>[] constants = constants();
final LazyConstantImpl<Holder>[] constants = constants();
List<Consumer> consumers = IntStream.range(0, THREADS)
.mapToObj(_ -> new Consumer(constants))
.toList();
@ -150,7 +151,7 @@ final class LazyConstantSafePublicationTest {
assertEquals(THREADS * SIZE, histogram[63]);
}
static void join(final LazyConstant<Holder>[] constants, List<Consumer> consumers, Thread... threads) {
static void join(final LazyConstantImpl<Holder>[] constants, List<Consumer> consumers, Thread... threads) {
try {
for (Thread t:threads) {
long deadline = System.nanoTime() + Utils.adjustTimeout(TimeUnit.MINUTES.toNanos(4));
@ -180,11 +181,11 @@ final class LazyConstantSafePublicationTest {
}
}
static LazyConstant<Holder>[] constants() {
static LazyConstantImpl<Holder>[] constants() {
@SuppressWarnings("unchecked")
LazyConstant<Holder>[] constants = (LazyConstant<Holder>[]) new LazyConstant[SIZE];
LazyConstantImpl<Holder>[] constants = (LazyConstantImpl<Holder>[]) new LazyConstantImpl[SIZE];
for (int i = 0; i < SIZE; i++) {
constants[i] = LazyConstant.of(Holder::new);
constants[i] = LazyConstantImpl.ofLazy(Holder::new);
}
return constants;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -24,6 +24,7 @@
/* @test
* @summary Basic tests for the LazyConstant implementation
* @enablePreview
* @library /test/lib
* @modules java.base/jdk.internal.lang
* @run junit/othervm --add-opens java.base/jdk.internal.lang=ALL-UNNAMED LazyConstantTest
*/
@ -32,12 +33,18 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.Objects;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.lang.LazyConstant;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;
import jdk.test.lib.Utils;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;
@ -46,6 +53,8 @@ final class LazyConstantTest {
private static final int VALUE = 42;
private static final Supplier<Integer> SUPPLIER = () -> VALUE;
private static final long TIME_OUT_S = Utils.adjustTimeout(5);
private static final long OVERLAP_TIME_MS = 100;
@Test
void factoryInvariants() {
@ -57,7 +66,6 @@ final class LazyConstantTest {
void basic(Function<Supplier<Integer>, LazyConstant<Integer>> factory) {
LazyConstantTestUtil.CountingSupplier<Integer> cs = new LazyConstantTestUtil.CountingSupplier<>(SUPPLIER);
var lazy = factory.apply(cs);
assertFalse(lazy.isInitialized());
assertEquals(SUPPLIER.get(), lazy.get());
assertEquals(1, cs.cnt());
assertEquals(SUPPLIER.get(), lazy.get());
@ -67,24 +75,48 @@ final class LazyConstantTest {
@ParameterizedTest
@MethodSource("factories")
void exception(Function<Supplier<Integer>, LazyConstant<Integer>> factory) {
LazyConstantTestUtil.CountingSupplier<Integer> cs = new LazyConstantTestUtil.CountingSupplier<>(() -> {
throw new UnsupportedOperationException();
});
var lazy = factory.apply(cs);
assertThrows(UnsupportedOperationException.class, lazy::get);
assertEquals(1, cs.cnt());
assertThrows(UnsupportedOperationException.class, lazy::get);
assertEquals(2, cs.cnt());
assertTrue(lazy.toString().contains("computing function"));
void exceptionInComputingFunction(Function<Supplier<Integer>, LazyConstant<Integer>> factory) {
// Test different Throwable categories
for (LazyConstantTestUtil.Thrower thrower : LazyConstantTestUtil.throwers()) {
AtomicReference<Throwable> exceptionThrown = new AtomicReference<>();
LazyConstantTestUtil.CountingSupplier<Integer> cs = new LazyConstantTestUtil.CountingSupplier<>(() -> {
Throwable t = thrower.supplier().get();
exceptionThrown.set(t);
LazyConstantTestUtil.sneakyThrow(t);
return 42; // Unreachable
});
exceptionInComputingFunction(factory, cs, () -> exceptionThrown.get().getClass(), thrower.message());
}
}
@ParameterizedTest
@MethodSource("lazyConstants")
void orElse(LazyConstant<Integer> constant) {
assertNull(constant.orElse(null));
constant.get();
assertEquals(VALUE, constant.orElse(null));
@MethodSource("factories")
void nullInComputingFunction(Function<Supplier<Integer>, LazyConstant<Integer>> factory) {
LazyConstantTestUtil.CountingSupplier<Integer> cs = new LazyConstantTestUtil.CountingSupplier<>(() -> {
return null;
});
exceptionInComputingFunction(factory, cs, () -> NullPointerException.class, null);
}
void exceptionInComputingFunction(Function<Supplier<Integer>, LazyConstant<Integer>> factory,
LazyConstantTestUtil.CountingSupplier<Integer> cs,
Supplier<Class<? extends Throwable>> causeTypeSupplier,
String message) {
var lazy = factory.apply(cs);
var ix = assertThrows(NoSuchElementException.class, lazy::get);
// Now we can look at the throwable
var causeType = causeTypeSupplier.get();
assertEquals(causeType, ix.getCause().getClass());
if (message != null) {
assertEquals(message, ix.getCause().getMessage());
}
assertEquals(1, cs.cnt());
var x = assertThrows(NoSuchElementException.class, lazy::get);
assertEquals("Unable to access the constant because "+causeType.getName()+" was thrown at initial computation", x.getMessage());
assertEquals(1, cs.cnt());
var toString = lazy.toString();
assertTrue(toString.contains("failed with="+causeType.getName()), toString);
assertNull(x.getCause());
}
@ParameterizedTest
@ -94,34 +126,35 @@ final class LazyConstantTest {
}
@ParameterizedTest
@MethodSource("lazyConstants")
void isInitialized(LazyConstant<Integer> constant) {
assertFalse(constant.isInitialized());
constant.get();
assertTrue(constant.isInitialized());
}
@ParameterizedTest
@MethodSource("lazyConstants")
void testHashCode(LazyConstant<Integer> constant) {
assertEquals(System.identityHashCode(constant), constant.hashCode());
@MethodSource("factories")
void testHashCode(Function<Supplier<Integer>, LazyConstant<Integer>> factory) {
LazyConstantTestUtil.CountingSupplier<Integer> cs = new LazyConstantTestUtil.CountingSupplier<>(SUPPLIER);
var lazy = factory.apply(cs);
assertEquals(System.identityHashCode(lazy), lazy.hashCode());
assertEquals(System.identityHashCode(lazy), lazy.hashCode());
// The supplier should never be invoked
assertEquals(0, cs.cnt());
}
@ParameterizedTest
@MethodSource("lazyConstants")
void testEquals(LazyConstant<Integer> c0) {
assertNotEquals(null, c0);
@MethodSource("factories")
void testEquals(Function<Supplier<Integer>, LazyConstant<Integer>> factory) {
LazyConstantTestUtil.CountingSupplier<Integer> cs = new LazyConstantTestUtil.CountingSupplier<>(SUPPLIER);
var lazy = factory.apply(cs);
assertNotEquals(null, lazy);
LazyConstant<Integer> different = LazyConstant.of(SUPPLIER);
assertNotEquals(different, c0);
assertNotEquals(c0, different);
assertNotEquals("a", c0);
assertNotEquals(different, lazy);
assertNotEquals(lazy, different);
assertNotEquals("a", lazy);
// The supplier should never be invoked
assertEquals(0, cs.cnt());
}
@ParameterizedTest
@MethodSource("lazyConstants")
void testLazyConstantAsComputingFunction(LazyConstant<Integer> constant) {
LazyConstant<Integer> c1 = LazyConstant.of(constant);
assertSame(constant, c1);
assertNotSame(constant, c1);
}
@Test
@ -161,9 +194,156 @@ final class LazyConstantTest {
void recursiveCall() {
AtomicReference<LazyConstant<Integer>> ref = new AtomicReference<>();
LazyConstant<Integer> constant = LazyConstant.of(() -> ref.get().get());
LazyConstant<Integer> constant1 = LazyConstant.of(constant);
ref.set(constant1);
assertThrows(IllegalStateException.class, constant::get);
ref.set(constant);
var x = assertThrows(NoSuchElementException.class, constant::get);
assertEquals(IllegalStateException.class, x.getCause().getClass());
}
@Test
void recursiveCallWithComputingFunctionsToStringThrowing() {
AtomicReference<LazyConstant<Integer>> ref = new AtomicReference<>();
AtomicInteger cnt = new AtomicInteger();
final class NaughtySupplier implements Supplier<Integer> {
@Override
public Integer get() {
return ref.get().get();
}
@Override
public String toString() {
cnt.incrementAndGet();
throw new UnsupportedOperationException("I should never be seen");
}
}
LazyConstant<Integer> constant = LazyConstant.of(new NaughtySupplier());
ref.set(constant);
var x = assertThrows(NoSuchElementException.class, constant::get);
assertEquals(IllegalStateException.class, x.getCause().getClass());
assertEquals(1, cnt.get());
assertEquals("Unable to access the constant because java.lang.IllegalStateException was thrown at initial computation", x.getMessage());
assertTrue(x.getCause().getMessage().contains(NaughtySupplier.class.getName()), x.getCause().getMessage());
}
@Test
void atMostOnceComputationUnderContention() throws Exception {
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(3)) {
AtomicInteger calls = new AtomicInteger();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch competing = new CountDownLatch(2);
LazyConstant<Integer> constant = LazyConstant.of(() -> {
calls.incrementAndGet();
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return VALUE;
});
var f1 = CompletableFuture.supplyAsync(constant::get, testExecutor);
assertTrue(entered.await(TIME_OUT_S, TimeUnit.SECONDS));
var f2 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.get();
}, testExecutor);
var f3 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.get();
}, testExecutor);
assertTrue(competing.await(TIME_OUT_S, TimeUnit.SECONDS));
// While computation is blocked, only one thread should have entered supplier
assertEquals(1, calls.get());
release.countDown();
assertEquals(VALUE, f1.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(VALUE, f2.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(VALUE, f3.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(1, calls.get());
}
}
@Test
void competingThreadsBlockUntilInitializationCompletes() throws Exception {
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(2)) {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch waiting = new CountDownLatch(1);
LazyConstant<Integer> constant = LazyConstant.of(() -> {
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return VALUE;
});
var computingThread = CompletableFuture.supplyAsync(constant::get, testExecutor);
assertTrue(entered.await(TIME_OUT_S, TimeUnit.SECONDS));
var waitingThread = CompletableFuture.supplyAsync(() -> {
waiting.countDown();
return constant.get();
}, testExecutor);
assertTrue(waiting.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
assertFalse(waitingThread.isDone(), "contending thread should be be blocked");
release.countDown();
assertEquals(VALUE, computingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(VALUE, waitingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
}
}
@Test
void interruptStatusIsPreservedForComputingThread() throws Exception {
int unset = -1;
int notInterrupted = 0;
int interrupted = 1;
AtomicInteger observedInterrupted = new AtomicInteger(unset);
CountDownLatch supplierRunning = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
LazyConstant<Integer> constant = LazyConstant.of(() -> {
supplierRunning.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
observedInterrupted.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
Thread.currentThread().interrupt(); // restore if await cleared it
}
return VALUE;
});
AtomicInteger interruptedAfterGet = new AtomicInteger(unset);
Thread t = Thread.ofPlatform().start(() -> {
assertEquals(VALUE, constant.get());
interruptedAfterGet.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
});
assertTrue(supplierRunning.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
t.interrupt();
release.countDown();
t.join();
assertEquals(notInterrupted, observedInterrupted.get()); // Observed before restoration of the status
assertEquals(interrupted, interruptedAfterGet.get(), "get() cleared interrupt status");
}
@ParameterizedTest
@ -172,10 +352,10 @@ final class LazyConstantTest {
LazyConstantTestUtil.CountingSupplier<Integer> cs = new LazyConstantTestUtil.CountingSupplier<>(SUPPLIER);
var f1 = factory.apply(cs);
Supplier<?> underlyingBefore = LazyConstantTestUtil.computingFunction(f1);
Object underlyingBefore = LazyConstantTestUtil.computingFunction(f1);
assertSame(cs, underlyingBefore);
int v = f1.get();
Supplier<?> underlyingAfter = LazyConstantTestUtil.computingFunction(f1);
Object underlyingAfter = LazyConstantTestUtil.computingFunction(f1);
assertNull(underlyingAfter);
}
@ -187,15 +367,14 @@ final class LazyConstantTest {
});
var f1 = factory.apply(cs);
Supplier<?> underlyingBefore = LazyConstantTestUtil.computingFunction(f1);
Object underlyingBefore = LazyConstantTestUtil.computingFunction(f1);
assertSame(cs, underlyingBefore);
try {
int v = f1.get();
} catch (UnsupportedOperationException _) {
// Expected
}
Supplier<?> underlyingAfter = LazyConstantTestUtil.computingFunction(f1);
assertSame(cs, underlyingAfter);
var x = assertThrows(NoSuchElementException.class, f1::get);
assertEquals(UnsupportedOperationException.class, x.getCause().getClass());
Object underlyingAfter = LazyConstantTestUtil.computingFunction(f1);
assertEquals(UnsupportedOperationException.class.getName(), underlyingAfter);
}
private static Stream<LazyConstant<Integer>> lazyConstants() {

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -21,12 +21,12 @@
* questions.
*/
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.List;
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;
import java.util.function.*;
final class LazyConstantTestUtil {
@ -82,6 +82,22 @@ final class LazyConstantTestUtil {
}
public static final class CountingPredicate<T>
extends AbstractCounting<Predicate<T>>
implements Predicate<T> {
public CountingPredicate(Predicate<T> delegate) {
super(delegate);
}
@Override
public boolean test(T t) {
incrementCounter();
return delegate.test(t);
}
}
public static final class CountingBiFunction<T, U, R>
extends AbstractCounting<BiFunction<T, U, R>>
implements BiFunction<T, U, R> {
@ -150,11 +166,11 @@ final class LazyConstantTestUtil {
}
}
static Supplier<?> computingFunction(LazyConstant<?> o) {
static Object computingFunction(LazyConstant<?> o) {
try {
final Field field = field(o.getClass(), "computingFunction");
final Field field = field(o.getClass(), "computingFunctionOrExceptionType");
field.setAccessible(true);
return (Supplier<?>) field.get(o);
return field.get(o);
} catch (ReflectiveOperationException e) {
throw new RuntimeException(e);
}
@ -171,4 +187,27 @@ final class LazyConstantTestUtil {
}
}
static String expectedMessage(Class<? extends Throwable> throwableClass, Object input) {
return "Unable to access the lazy collection because " + throwableClass.getName() + " was thrown at initial computation for input '" + input + "'";
}
@SuppressWarnings("unchecked")
static <E extends Throwable> void sneakyThrow(Throwable e) throws E {
throw (E) e;
}
record Thrower(String message, Function<String, ? extends Throwable> factory) {
public Supplier<? extends Throwable> supplier() {
return () -> factory.apply(Thrower.this.message);
}
}
static List<Thrower> throwers() {
return List.of(
new Thrower("Initial checked exception", IOException::new),
new Thrower("Initial runtime exception", UnsupportedOperationException::new),
new Thrower("Initial Error", InternalError::new)
);
}
}

View File

@ -33,18 +33,15 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.Serializable;
import java.util.Comparator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.RandomAccess;
import java.util.Set;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.*;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
@ -57,6 +54,9 @@ final class LazyListTest {
private static final int SIZE = 31;
private static final IntFunction<Integer> IDENTITY = i -> i;
private static final long TIME_OUT_S = 5;
private static final long OVERLAP_TIME_MS = 100;
@Test
void factoryInvariants() {
assertThrows(NullPointerException.class, () -> List.ofLazy(SIZE, null));
@ -88,15 +88,44 @@ final class LazyListTest {
}
@Test
void getException() {
void exeptionInComputingFunction() {
LazyConstantTestUtil.CountingIntFunction<Integer> cif = new LazyConstantTestUtil.CountingIntFunction<Integer>(_ -> {
throw new UnsupportedOperationException();
throw new UnsupportedOperationException("Initial exception");
});
exceptionInComputingFunction(cif, UnsupportedOperationException.class);
}
@Test
void nullResultInComputingFunction() {
LazyConstantTestUtil.CountingIntFunction<Integer> cif = new LazyConstantTestUtil.CountingIntFunction<Integer>(_ -> {
return null;
});
exceptionInComputingFunction(cif, NullPointerException.class);
}
void exceptionInComputingFunction(LazyConstantTestUtil.CountingIntFunction<Integer> cif,
Class<? extends Throwable> causeType) {
var lazy = List.ofLazy(SIZE, cif);
assertThrows(UnsupportedOperationException.class, () -> lazy.get(INDEX));
var x = assertThrows(NoSuchElementException.class, () -> lazy.get(INDEX));
assertEquals(LazyConstantTestUtil.expectedMessage(causeType, INDEX), x.getMessage());
assertEquals(causeType, x.getCause().getClass());
assertEquals(1, cif.cnt());
assertThrows(UnsupportedOperationException.class, () -> lazy.get(INDEX));
assertEquals(2, cif.cnt());
var x2 = assertThrows(NoSuchElementException.class, () -> lazy.get(INDEX));
assertEquals(1, cif.cnt());
assertEquals(LazyConstantTestUtil.expectedMessage(causeType, INDEX), x2.getMessage());
// The initial cause should only be present on the _first_ unchecked exception
assertNull(x2.getCause());
for (int i = 0; i < SIZE; i++) {
// Make sure all values are touched
final int finalI = i;
assertThrows(Exception.class, () -> lazy.get(finalI));
}
var xToString = assertThrows(NoSuchElementException.class, lazy::toString);
assertEquals(LazyConstantTestUtil.expectedMessage(causeType, 0), xToString.getMessage());
assertEquals(SIZE, cif.cnt());
}
@Test
@ -109,14 +138,14 @@ final class LazyListTest {
void toArrayWithArrayLarger() {
Integer[] actual = new Integer[SIZE];
for (int i = 0; i < SIZE; i++) {
actual[INDEX] = 100 + i;
actual[i] = 100 + i;
}
var lazy = List.ofLazy(INDEX, IDENTITY);
assertSame(actual, lazy.toArray(actual));
Integer[] expected = IntStream.range(0, SIZE)
.mapToObj(i -> i < INDEX ? i : null)
.toArray(Integer[]::new);
assertArrayEquals(expected, actual);
for (int i = 0; i < INDEX; i++) {
assertEquals(i, actual[i]);
}
assertNull(actual[INDEX]);
}
@Test
@ -273,8 +302,148 @@ final class LazyListTest {
AtomicReference<IntFunction<Integer>> ref = new AtomicReference<>();
var lazy = List.ofLazy(SIZE, i -> ref.get().apply(i));
ref.set(lazy::get);
var x = assertThrows(IllegalStateException.class, () -> lazy.get(INDEX));
assertEquals("Recursive initialization of a lazy collection is illegal", x.getMessage());
var x = assertThrows(NoSuchElementException.class, () -> lazy.get(INDEX));
assertEquals(LazyConstantTestUtil.expectedMessage(IllegalStateException.class, INDEX), x.getMessage());
assertEquals("Recursive initialization of a lazy collection is illegal: " + INDEX, x.getCause().getMessage());
assertEquals(IllegalStateException.class, x.getCause().getClass());
}
@ParameterizedTest
@MethodSource("viewOperations")
void atMostOnceComputationUnderContention(UnaryOperation viewOp) throws Exception {
final int index = SIZE / 2;
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(3)) {
AtomicInteger calls = new AtomicInteger();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch competing = new CountDownLatch(2);
List<Integer> ref = viewOp.apply(newRegularList());
List<Integer> constant = viewOp.apply(List.ofLazy(SIZE, i -> {
calls.incrementAndGet();
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return i;
}));
var f1 = CompletableFuture.supplyAsync(() -> constant.get(index), testExecutor);
assertTrue(entered.await(5, TimeUnit.SECONDS));
var f2 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.get(index);
}, testExecutor);
var f3 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.get(index);
}, testExecutor);
assertTrue(competing.await(TIME_OUT_S, TimeUnit.SECONDS));
// While computation is blocked, only one thread should have entered supplier
Thread.sleep(OVERLAP_TIME_MS);
assertEquals(1, calls.get());
release.countDown();
assertEquals(ref.get(index), f1.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(ref.get(index), f2.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(ref.get(index), f3.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(1, calls.get());
}
}
@ParameterizedTest
@MethodSource("viewOperations")
void competingThreadsBlockUntilInitializationCompletes(UnaryOperation viewOp) throws Exception {
final int index = SIZE / 2;
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(2)) {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch waiting = new CountDownLatch(1);
List<Integer> ref = viewOp.apply(newRegularList());
List<Integer> constant = viewOp.apply(List.ofLazy(SIZE, i -> {
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return i;
}));
var computingThread = CompletableFuture.supplyAsync(() -> constant.get(index), testExecutor);
assertTrue(entered.await(TIME_OUT_S, TimeUnit.SECONDS));
var waitingThread = CompletableFuture.supplyAsync(() -> {
waiting.countDown();
return constant.get(index);
}, testExecutor);
assertTrue(waiting.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
assertFalse(waitingThread.isDone(), "contending thread should be be blocked");
release.countDown();
assertEquals(ref.get(index), computingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(ref.get(index), waitingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
}
}
@ParameterizedTest
@MethodSource("viewOperations")
void interruptStatusIsPreservedForComputingThread(UnaryOperation viewOp) throws Exception {
final int index = SIZE / 2;
int unset = -1;
int notInterrupted = 0;
int interrupted = 1;
AtomicInteger observedInterrupted = new AtomicInteger(unset);
CountDownLatch supplierRunning = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
List<Integer> constant = viewOp.apply(List.ofLazy(SIZE, i -> {
supplierRunning.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
observedInterrupted.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
Thread.currentThread().interrupt(); // restore if await cleared it
}
return i;
}));
AtomicInteger interruptedAfterGet = new AtomicInteger(unset);
Thread t = Thread.ofPlatform().start(() -> {
assertEquals(index, constant.get(index));
interruptedAfterGet.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
});
assertTrue(supplierRunning.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
t.interrupt();
release.countDown();
t.join();
assertEquals(notInterrupted, observedInterrupted.get()); // Observed before restoration of the status
assertEquals(interrupted, interruptedAfterGet.get(), "get() cleared interrupt status");
}
@ParameterizedTest
@MethodSource("iteratorOperations")
void iterators(ListFunction viewOp) throws Exception {
List<Integer> lazy = newLazyList();
Iterator<Integer> iter = (Iterator<Integer>) viewOp.apply(lazy);
List<Integer> actual = new ArrayList<>();
iter.forEachRemaining(actual::add);
assertEquals(newRegularList(), actual);
}
// Immutability
@ -374,16 +543,16 @@ final class LazyListTest {
// We need identity to capture all combinations
new UnaryOperation("identity", l -> l),
new UnaryOperation("reversed", List::reversed),
new UnaryOperation("subList", l -> l.subList(0, l.size()))
new UnaryOperation("subList", l -> l.subList(0, l.size() - 1))
);
}
static Stream<ListFunction> childOperations() {
static Stream<ListFunction> iteratorOperations() {
return Stream.of(
// We need identity to capture all combinations
new ListFunction("iterator", List::iterator),
new ListFunction("listIterator", List::listIterator),
new ListFunction("listIterator", List::stream)
new ListFunction("stream::iterator", l -> l.stream().iterator())
);
}
@ -391,6 +560,9 @@ final class LazyListTest {
return Stream.of(
new Operation("forEach", l -> l.forEach(null)),
new Operation("containsAll", l -> l.containsAll(null)),
new Operation("contains", l -> l.contains(null)),
new Operation("indexOf", l -> l.indexOf(null)),
new Operation("lastIndexOf", l -> l.lastIndexOf(null)),
new Operation("toArray", l -> l.toArray((Integer[]) null)),
new Operation("toArray", l -> l.toArray((IntFunction<Integer[]>) null))
);
@ -448,4 +620,5 @@ final class LazyListTest {
static List<Integer> newRegularList() {
return IntStream.range(0, SIZE).boxed().toList();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2025, 2026, 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
@ -32,21 +32,26 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.io.IOException;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import static java.util.stream.Collectors.joining;
@ -90,6 +95,9 @@ final class LazyMapTest {
private static final Value KEY = Value.FORTY_TWO;
private static final Integer VALUE = MAPPER.apply(KEY);
private static final long TIME_OUT_S = 5;
private static final long OVERLAP_TIME_MS = 100;
@ParameterizedTest
@MethodSource("allSets")
void factoryInvariants(Set<Value> set) {
@ -132,19 +140,58 @@ final class LazyMapTest {
assertNull(lazy.get(Value.ILLEGAL_BETWEEN));
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void getOrDefault(Set<Value> set) {
LazyConstantTestUtil.CountingFunction<Value, Integer> cf = new LazyConstantTestUtil.CountingFunction<>(MAPPER);
var lazy = Map.ofLazy(set, cf);
int cnt = 1;
for (Value v : set) {
assertEquals(MAPPER.apply(v), lazy.getOrDefault(v, Integer.MIN_VALUE));
assertEquals(cnt, cf.cnt());
assertEquals(MAPPER.apply(v), lazy.getOrDefault(v, Integer.MIN_VALUE));
assertEquals(cnt++, cf.cnt());
}
assertEquals(Integer.MIN_VALUE, lazy.getOrDefault(Value.ILLEGAL_BETWEEN, Integer.MIN_VALUE));
assertEquals(Integer.MIN_VALUE, lazy.getOrDefault("a", Integer.MIN_VALUE));
assertThrows(NullPointerException.class, () -> lazy.getOrDefault(null, Integer.MIN_VALUE));
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void exception(Set<Value> set) {
LazyConstantTestUtil.CountingFunction<Value, Integer> cif = new LazyConstantTestUtil.CountingFunction<>(_ -> {
throw new UnsupportedOperationException();
});
var lazy = Map.ofLazy(set, cif);
assertThrows(UnsupportedOperationException.class, () -> lazy.get(KEY));
assertEquals(1, cif.cnt());
assertThrows(UnsupportedOperationException.class, () -> lazy.get(KEY));
assertEquals(2, cif.cnt());
assertThrows(UnsupportedOperationException.class, lazy::toString);
assertEquals(3, cif.cnt());
// Test different Throwable categories
for (LazyConstantTestUtil.Thrower thrower : LazyConstantTestUtil.throwers()) {
AtomicReference<Throwable> exceptionThrown = new AtomicReference<>();
LazyConstantTestUtil.CountingFunction<Value, Integer> cif = new LazyConstantTestUtil.CountingFunction<>(_ -> {
Throwable t = thrower.supplier().get();
exceptionThrown.set(t);
LazyConstantTestUtil.sneakyThrow(t);
return 42; // Unreachable
});
var lazy = Map.ofLazy(set, cif);
var x = assertThrows(NoSuchElementException.class, () -> lazy.get(KEY));
assertEquals(LazyConstantTestUtil.expectedMessage(exceptionThrown.get().getClass(), KEY), x.getMessage());
assertEquals(exceptionThrown.get().getClass(), x.getCause().getClass());
assertEquals(thrower.message(), x.getCause().getMessage());
assertEquals(1, cif.cnt());
var x2 = assertThrows(NoSuchElementException.class, () -> lazy.get(KEY));
assertEquals(1, cif.cnt());
assertEquals(LazyConstantTestUtil.expectedMessage(exceptionThrown.get().getClass(), KEY), x2.getMessage());
// The initial cause should only be present on the _first_ unchecked exception
assertNull(x2.getCause());
for (Value v : set) {
// Make sure all values are touched
assertThrows(Exception.class, () -> lazy.get(v));
}
var xToString = assertThrows(NoSuchElementException.class, lazy::toString);
var xMessage = xToString.getMessage();
assertTrue(xMessage.startsWith(LazyConstantTestUtil.expectedMessage(exceptionThrown.get().getClass(), 0).substring(0, xMessage.indexOf("'"))));
assertEquals(set.size(), cif.cnt());
}
}
@ParameterizedTest
@ -155,6 +202,8 @@ final class LazyMapTest {
assertTrue(lazy.containsKey(v));
}
assertFalse(lazy.containsKey(Value.ILLEGAL_BETWEEN));
assertThrows(NullPointerException.class, () -> lazy.containsKey(null));
assertFalse(lazy.containsKey("a"));
}
@ParameterizedTest
@ -240,8 +289,36 @@ final class LazyMapTest {
@SuppressWarnings("unchecked")
Map<Value, Map<Value, Object>> lazy = Map.ofLazy(set, k -> (Map<Value, Object>) ref.get().get(k));
ref.set(lazy);
var x = assertThrows(IllegalStateException.class, () -> lazy.get(KEY));
assertEquals("Recursive initialization of a lazy collection is illegal", x.getMessage());
var x = assertThrows(NoSuchElementException.class, () -> lazy.get(KEY));
assertEquals(LazyConstantTestUtil.expectedMessage(IllegalStateException.class, KEY), x.getMessage());
assertEquals("Recursive initialization of a lazy collection is illegal: " + KEY, x.getCause().getMessage());
assertEquals(IllegalStateException.class, x.getCause().getClass());
}
@Test
void recursiveCallWithKeysToStringThrowing() {
AtomicInteger cnt = new AtomicInteger();
final class NaughtyKey {
@Override
public String toString() {
cnt.incrementAndGet();
throw new UnsupportedOperationException("I should never be seen");
}
}
final NaughtyKey key = new NaughtyKey();
final Set<NaughtyKey> set = Set.of(key);
final AtomicReference<Map<NaughtyKey, ?>> ref = new AtomicReference<>();
@SuppressWarnings("unchecked")
Map<NaughtyKey, Map<Value, Object>> lazy = Map.ofLazy(set, k -> (Map<Value, Object>) ref.get().get(k));
ref.set(lazy);
var x = assertThrows(NoSuchElementException.class, () -> lazy.get(key));
// We recurse here so `NaughtyKey.toString` is called twice before reentry is prevented
assertEquals(2, cnt.get());
assertTrue(x.getCause().getMessage().contains(NaughtyKey.class.getName()));
}
@ParameterizedTest
@ -252,6 +329,31 @@ final class LazyMapTest {
assertTrue(regular.equals(lazy));
assertTrue(lazy.equals(regular));
assertTrue(regular.equals(lazy));
assertEquals(lazy.hashCode(), regular.hashCode());
}
@ParameterizedTest
@MethodSource("allSets")
void entrySetHashCode(Set<Value> set) {
assertEquals(newRegularMap(set).entrySet().hashCode(),
newLazyMap(set).entrySet().hashCode());
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void keySet(Set<Value> set) {
var lazy = newLazyMap(set);
var keySet = lazy.keySet();
assertEquals(set, keySet);
assertThrows(UnsupportedOperationException.class, () -> lazy.remove(KEY));
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void entrySetValue(Set<Value> set) {
var entry = newLazyMap(set).entrySet().iterator().next();
assertThrows(UnsupportedOperationException.class, () -> entry.setValue(null));
assertThrows(UnsupportedOperationException.class, () -> entry.setValue(1));
}
@ParameterizedTest
@ -267,6 +369,15 @@ final class LazyMapTest {
assertTrue(toString.endsWith("]"));
}
@ParameterizedTest
@MethodSource("emptySets")
void emptyValues(Set<Value> set) {
var lazy = newLazyMap(set);
var lazyValues = lazy.values();
assertEquals(0, lazyValues.size());
assertTrue(lazyValues.isEmpty());
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void values(Set<Value> set) {
@ -276,6 +387,10 @@ final class LazyMapTest {
var val = lazyValues.stream().iterator().next();
assertEquals(lazy.size() - 1, functionCounter(lazy));
assertEquals(set.size(), lazyValues.size());
assertFalse(lazyValues.isEmpty());
assertTrue(lazyValues.contains(VALUE));
// Mod ops
assertThrows(UnsupportedOperationException.class, () -> lazyValues.remove(val));
assertThrows(UnsupportedOperationException.class, () -> lazyValues.add(val));
@ -395,7 +510,9 @@ final class LazyMapTest {
@Test
void nullResult() {
var lazy = Map.ofLazy(Set.of(0), _ -> null);
assertThrows(NullPointerException.class, () -> lazy.getOrDefault(0, 1));;
var x = assertThrows(NoSuchElementException.class, () -> lazy.getOrDefault(0, 1));
assertEquals(LazyConstantTestUtil.expectedMessage(NullPointerException.class, 0), x.getMessage());
assertEquals(NullPointerException.class, x.getCause().getClass());
assertTrue(lazy.containsKey(0));
}
@ -439,6 +556,129 @@ final class LazyMapTest {
assertNull(LazyConstantTestUtil.functionHolderFunction(holder));
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void atMostOnceComputationUnderContention(Set<Value> set) throws Exception {
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(3)) {
AtomicInteger calls = new AtomicInteger();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch competing = new CountDownLatch(2);
Map<Value, Integer> constant = Map.ofLazy(set, i -> {
calls.incrementAndGet();
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return MAPPER.apply(i);
});
var f1 = CompletableFuture.supplyAsync(() -> constant.get(KEY), testExecutor);
assertTrue(entered.await(5, TimeUnit.SECONDS));
var f2 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.get(KEY);
}, testExecutor);
var f3 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.get(KEY);
}, testExecutor);
assertTrue(competing.await(TIME_OUT_S, TimeUnit.SECONDS));
// While computation is blocked, only one thread should have entered supplier
Thread.sleep(OVERLAP_TIME_MS);
assertEquals(1, calls.get());
release.countDown();
assertEquals(VALUE, f1.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(VALUE, f2.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(VALUE, f3.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(1, calls.get());
}
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void competingThreadsBlockUntilInitializationCompletes(Set<Value> set) throws Exception {
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(2)) {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch waiting = new CountDownLatch(1);
Map<Value, Integer> constant = Map.ofLazy(set, i -> {
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return MAPPER.apply(i);
});
var computingThread = CompletableFuture.supplyAsync(() -> constant.get(KEY), testExecutor);
assertTrue(entered.await(TIME_OUT_S, TimeUnit.SECONDS));
var waitingThread = CompletableFuture.supplyAsync(() -> {
waiting.countDown();
return constant.get(KEY);
}, testExecutor);
assertTrue(waiting.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
assertFalse(waitingThread.isDone(), "contending thread should be be blocked");
release.countDown();
assertEquals(VALUE, computingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(VALUE, waitingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
}
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void interruptStatusIsPreservedForComputingThread(Set<Value> set) throws Exception {
int unset = -1;
int notInterrupted = 0;
int interrupted = 1;
AtomicInteger observedInterrupted = new AtomicInteger(unset);
CountDownLatch supplierRunning = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
Map<Value, Integer> constant = Map.ofLazy(set, i -> {
supplierRunning.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
observedInterrupted.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
Thread.currentThread().interrupt(); // restore if await cleared it
}
return MAPPER.apply(i);
});
AtomicInteger interruptedAfterGet = new AtomicInteger(unset);
Thread t = Thread.ofPlatform().start(() -> {
assertEquals(VALUE, constant.get(KEY));
interruptedAfterGet.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
});
assertTrue(supplierRunning.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
t.interrupt();
release.countDown();
t.join();
assertEquals(notInterrupted, observedInterrupted.get()); // Observed before restoration of the status
assertEquals(interrupted, interruptedAfterGet.get(), "get() cleared interrupt status");
}
@ParameterizedTest
@MethodSource("allSets")
void underlyingRefViaEntrySetForEach(Set<Value> set) {
@ -461,6 +701,7 @@ final class LazyMapTest {
@Test
void usesOptimizedVersion() {
// This test is using name magic but we are in control of the naming.
Map<Value, Integer> enumMap = Map.ofLazy(EnumSet.of(KEY), Value::asInt);
assertTrue(enumMap.getClass().getName().contains("Enum"), enumMap.getClass().getName());
Map<Value, Integer> emptyMap = Map.ofLazy(EnumSet.noneOf(Value.class), Value::asInt);
@ -498,7 +739,8 @@ final class LazyMapTest {
static Stream<Operation> nullAverseOperations() {
return Stream.of(
new Operation("forEach", m -> m.forEach(null))
new Operation("forEach", m -> m.forEach(null)),
new Operation("containsValue", m -> m.containsValue(null))
);
}
@ -571,4 +813,30 @@ final class LazyMapTest {
return LazyConstantTestUtil.functionHolderCounter(holder);
}
// Javadoc equivalent
class LazyMap<K, V> extends AbstractMap<K, V> {
private final Map<K, LazyConstant<V>> backingMap;
public LazyMap(Set<K> keys, Function<K, V> computingFunction) {
this.backingMap = keys.stream()
.collect(Collectors.toUnmodifiableMap(
Function.identity(),
k -> LazyConstant.of(() -> computingFunction.apply(k))));
}
@Override
public V get(Object key) {
var lazyConstant = backingMap.get(key);
return lazyConstant == null
? null
: lazyConstant.get();
}
@Override
public Set<Entry<K, V>> entrySet() {
return Set.of();
}
}
}

View File

@ -0,0 +1,591 @@
/*
* Copyright (c) 2026, 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 lazy set methods
* @enablePreview
* @modules java.base/java.util:+open
* @run junit LazySetTest
*/
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.lang.Class;
import java.lang.Override;
import java.util.*;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
final class LazySetTest {
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) {
@Override
public String toString() {
// getEnumConstants will be `null` for this enum as it is overridden
return super.toString()+" (Overridden)";
}
},
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 Value MEMBER = Value.FORTY_TWO;
private static final Value NON_MEMBER = Value.THIRTEEN;
private static final Set<Value> SET = Set.of(NON_MEMBER, MEMBER);
private static final Predicate<Value> PREDICATE = c -> c == MEMBER; ;
private static final long TIME_OUT_S = 5;
private static final long OVERLAP_TIME_MS = 100;
@ParameterizedTest
@MethodSource("allSets")
void factoryInvariants(Set<Value> set) {
assertThrows(NullPointerException.class, () -> Set.ofLazy(set, null), set.getClass().getSimpleName());
assertThrows(NullPointerException.class, () -> Set.ofLazy(null, PREDICATE));
Set<Value> setWithNull = new HashSet<>();
setWithNull.add(MEMBER);
setWithNull.add(null);
assertThrows(NullPointerException.class, () -> Set.ofLazy(setWithNull, PREDICATE));
}
@ParameterizedTest
@MethodSource("emptySets")
void empty(Set<Value> set) {
var lazy = newLazySet(set);
assertTrue(lazy.isEmpty());
assertEquals("[]", lazy.toString());
}
@ParameterizedTest
@MethodSource("allSets")
void size(Set<Value> set) {
assertEquals(newRegularSet(set).size(), newLazySet(set).size());
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void exception(Set<Value> set) {
LazyConstantTestUtil.CountingPredicate<Value> cif = new LazyConstantTestUtil.CountingPredicate<>(_ -> {
throw new UnsupportedOperationException("Initial exception");
});
var lazy = Set.ofLazy(set, cif);
var x = assertThrows(NoSuchElementException.class, () -> lazy.contains(MEMBER));
assertEquals(LazyConstantTestUtil.expectedMessage(UnsupportedOperationException.class, MEMBER), x.getMessage());
assertEquals(UnsupportedOperationException.class, x.getCause().getClass());
assertEquals(1, cif.cnt());
var x2 = assertThrows(NoSuchElementException.class, () -> lazy.contains(MEMBER));
assertEquals(LazyConstantTestUtil.expectedMessage(UnsupportedOperationException.class, MEMBER), x2.getMessage());
// The initial cause should only be present on the _first_ unchecked exception
assertNull(x2.getCause());
for (Value v : set) {
// Make sure all values are touched
assertThrows(Exception.class, () -> lazy.contains(v));
}
var xToString = assertThrows(NoSuchElementException.class, lazy::toString);
var xMessage = xToString.getMessage();
assertTrue(xMessage.startsWith(LazyConstantTestUtil.expectedMessage(UnsupportedOperationException.class, 0).substring(0, xMessage.indexOf("'"))));
assertEquals(set.size(), cif.cnt());
}
@ParameterizedTest
@MethodSource("allSets")
void contains(Set<Value> set) {
var lazy = newLazySet(set);
var expected = newRegularSet(set);
for (Value v : set) {
assertEquals(expected.contains(v), lazy.contains(v));
}
assertFalse(lazy.contains(Value.ILLEGAL_BETWEEN));
}
@ParameterizedTest
@MethodSource("allSets")
void forEach(Set<Value> set) {
var lazy = newLazySet(set);
var expected = newRegularSet(set);
Set<Value> actual = new HashSet<>();
lazy.forEach(actual::add);
assertEquals(expected, actual);
}
@ParameterizedTest
@MethodSource("emptySets")
void toStringTestEmpty(Set<Value> set) {
var lazy = newLazySet(set);
assertEquals("[]", lazy.toString());
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void toStringTest(Set<Value> set) {
var lazy = newLazySet(set);
var expected = newRegularSet(set);
var toString = lazy.toString();
assertTrue(toString.startsWith("["));
assertTrue(toString.endsWith("]"));
// Key order is unspecified
for (Value key : expected) {
assertTrue(toString.contains(key.toString()), key + " is not in" + toString);
}
// One between the values
assertEquals(expected.size() - 1, toString.chars().filter(ch -> ch == ',').count());
}
@ParameterizedTest
@MethodSource("allSets")
void hashCodeTest(Set<Value> set) {
var lazy = newLazySet(set);
var regular = newRegularSet(set);
assertEquals(regular.hashCode(), lazy.hashCode());
}
@ParameterizedTest
@MethodSource("allSets")
void zeroHashCodeTest() {
specificHashCodeTest(0);
}
@ParameterizedTest
@MethodSource("allSets")
void negativeHashCodeTest() {
specificHashCodeTest(-1);
specificHashCodeTest(-42);
specificHashCodeTest(Integer.MIN_VALUE);
}
@ParameterizedTest
@MethodSource("allSets")
void positiveHashCodeTest() {
specificHashCodeTest(1);
specificHashCodeTest(42);
specificHashCodeTest(Integer.MAX_VALUE);
}
void specificHashCodeTest(int hc) {
final class ZeroHashCode {
@Override
public int hashCode() {
return hc;
}
}
var lazy = Set.ofLazy(Set.of(new ZeroHashCode()), e -> true);
assertEquals(hc, lazy.hashCode());
}
@ParameterizedTest
@MethodSource("allSets")
void equality(Set<Value> set) {
var lazy = newLazySet(set);
var regular = newRegularSet(set);
assertEquals(regular, lazy);
assertEquals(lazy, regular);
assertNotEquals("A", lazy);
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void recursiveCall(Set<Value> set) {
final AtomicReference<Set<Value>> ref = new AtomicReference<>();
@SuppressWarnings("unchecked")
Set<Value> lazy = Set.ofLazy(set, k -> ref.get().contains(k));
ref.set(lazy);
var x = assertThrows(NoSuchElementException.class, () -> lazy.contains(MEMBER));
assertEquals(LazyConstantTestUtil.expectedMessage(java.lang.IllegalStateException.class, MEMBER), x.getMessage());
assertEquals("Recursive initialization of a lazy collection is illegal: " + MEMBER, x.getCause().getMessage());
assertEquals(IllegalStateException.class, x.getCause().getClass());
}
@ParameterizedTest
@MethodSource("allSets")
void iteratorNext(Set<Value> set) {
Set<Value> encountered = new HashSet<>();
var expected = newRegularSet(set);
var iterator = newLazySet(set).iterator();
while (iterator.hasNext()) {
var entry = iterator.next();
encountered.add(entry);
}
assertEquals(expected, encountered);
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void iteratorForEachRemaining(Set<Value> set) {
Set<Value> encountered = new HashSet<>();
var expected = newRegularSet(set);
var iterator = newLazySet(set).iterator();
var value = iterator.next();
encountered.add(value);
iterator.forEachRemaining(encountered::add);
assertEquals(expected, encountered);
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void atMostOnceComputationUnderContention(Set<Value> set) throws Exception {
// Make sure to exercise both member and non-member statuses
for (Value candidate : set) {
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(3)) {
AtomicInteger calls = new AtomicInteger();
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch competing = new CountDownLatch(2);
Set<Value> constant = Set.ofLazy(set, i -> {
calls.incrementAndGet();
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return PREDICATE.test(i);
});
var f1 = CompletableFuture.supplyAsync(() -> constant.contains(candidate), testExecutor);
assertTrue(entered.await(5, TimeUnit.SECONDS));
var f2 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.contains(candidate);
}, testExecutor);
var f3 = CompletableFuture.supplyAsync(() -> {
competing.countDown();
return constant.contains(candidate);
}, testExecutor);
assertTrue(competing.await(TIME_OUT_S, TimeUnit.SECONDS));
// While computation is blocked, only one thread should have entered supplier
Thread.sleep(OVERLAP_TIME_MS);
assertEquals(1, calls.get());
release.countDown();
assertEquals(PREDICATE.test(candidate), f1.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(PREDICATE.test(candidate), f2.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(PREDICATE.test(candidate), f3.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(1, calls.get());
}
}
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void competingThreadsBlockUntilInitializationCompletes(Set<Value> set) throws Exception {
// Make sure to exercise both member and non-member statuses
for (Value candidate : set) {
// Mitigate thread starvation via a dedicated thread pool != FJP
try (var testExecutor = Executors.newFixedThreadPool(2)) {
CountDownLatch entered = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
CountDownLatch waiting = new CountDownLatch(1);
Set<Value> constant = Set.ofLazy(set, i -> {
entered.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return PREDICATE.test(i);
});
var computingThread = CompletableFuture.supplyAsync(() -> constant.contains(candidate), testExecutor);
assertTrue(entered.await(TIME_OUT_S, TimeUnit.SECONDS));
var waitingThread = CompletableFuture.supplyAsync(() -> {
waiting.countDown();
return constant.contains(candidate);
}, testExecutor);
assertTrue(waiting.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
assertFalse(waitingThread.isDone(), "contending thread should be be blocked");
release.countDown();
assertEquals(PREDICATE.test(candidate), computingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
assertEquals(PREDICATE.test(candidate), waitingThread.get(TIME_OUT_S, TimeUnit.SECONDS));
}
}
}
@ParameterizedTest
@MethodSource("nonEmptySets")
void interruptStatusIsPreservedForComputingThread(Set<Value> set) throws Exception {
// Make sure to exercise both member and non-member statuses
for (Value candidate : set) {
int unset = -1;
int notInterrupted = 0;
int interrupted = 1;
AtomicInteger observedInterrupted = new AtomicInteger(unset);
CountDownLatch supplierRunning = new CountDownLatch(1);
CountDownLatch release = new CountDownLatch(1);
Set<Value> constant = Set.ofLazy(set, i -> {
supplierRunning.countDown();
try {
assertTrue(release.await(TIME_OUT_S, TimeUnit.SECONDS));
} catch (InterruptedException e) {
observedInterrupted.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
Thread.currentThread().interrupt(); // restore if await cleared it
}
return PREDICATE.test(i);
});
AtomicInteger interruptedAfterGet = new AtomicInteger(unset);
Thread t = Thread.ofPlatform().start(() -> {
assertEquals(PREDICATE.test(candidate), constant.contains(candidate));
interruptedAfterGet.set(Thread.currentThread().isInterrupted() ? interrupted : notInterrupted);
});
assertTrue(supplierRunning.await(TIME_OUT_S, TimeUnit.SECONDS));
Thread.sleep(OVERLAP_TIME_MS);
t.interrupt();
release.countDown();
t.join();
assertEquals(notInterrupted, observedInterrupted.get()); // Observed before restoration of the status
assertEquals(interrupted, interruptedAfterGet.get(), "get() cleared interrupt status");
}
}
// 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) {
for (Set<Value> set : allSets().toList()) {
var lazy = newLazySet(set);
assertThrows(expectedType, () -> operation.accept(lazy), set.getClass().getSimpleName() + " " + operation);
}
}
// Implementing interfaces
@ParameterizedTest
@MethodSource("allSets")
void serializable(Set<Value> set) {
var lazy = newLazySet(set);
assertFalse(lazy instanceof Serializable);
}
@Test
void overriddenEnum() {
final var overridden = Value.THIRTEEN;
Set<Value> enumMap = Set.ofLazy(EnumSet.of(overridden), PREDICATE);
assertEquals(PREDICATE.test(overridden), enumMap.contains(overridden), enumMap.toString());
}
// Support constructs
record Operation(String name,
Consumer<Set<Value>> consumer) implements Consumer<Set<Value>> {
@Override
public void accept(Set<Value> set) { consumer.accept(set); }
@Override
public String toString() { return name; }
}
static Stream<Operation> nullAverseOperations() {
return Stream.of(
new Operation("forEach", m -> m.forEach(null)),
new Operation("containsAll", m -> m.containsAll(null)),
new Operation("contains", m -> m.contains(null))
);
}
static Stream<Operation> unsupportedOperations() {
return Stream.of(
new Operation("clear", Set::clear),
new Operation("add", m -> m.add(MEMBER)),
new Operation("addAll", m -> m.addAll(Set.of(MEMBER))),
new Operation("remove", m -> m.remove(MEMBER)),
new Operation("removeAll",m -> m.removeAll(Set.of(MEMBER))),
new Operation("retainAll",m -> m.retainAll(Set.of(MEMBER))),
new Operation("iter.rm", m -> m.iterator().remove())
);
}
static Set<Value> newLazySet(Set<Value> set) {
return Set.ofLazy(set, PREDICATE);
}
static Set<Value> newRegularSet(Set<Value> set) {
return set.stream()
.filter(PREDICATE)
.collect(Collectors.toSet());
}
private static Stream<Set<Value>> nonEmptySets() {
return Stream.of(
Set.of(MEMBER, NON_MEMBER),
linkedHashSet(NON_MEMBER, MEMBER),
treeSet(MEMBER, NON_MEMBER),
EnumSet.of(MEMBER, NON_MEMBER)
);
}
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;
}
// JEP Example
class Application {
enum Option { VERBOSE, DRY_RUN, STRICT }
// Return true when the given Option is enabled
private static boolean isEnabled(Option option) {
// Parse command line, read configuration file, load database
return true;
}
// Lazily initialized Set of Options
static final Set<Option> OPTIONS =
Set.ofLazy(EnumSet.allOf(Option.class), Application::isEnabled);
public static void process() {
if (OPTIONS.contains(Option.DRY_RUN)) {
// Skip processing in DRY_RUN mode
return;
}
// Actual Processing logic
}
}
// Javadoc equivalent
class LazySet<E> extends AbstractCollection<E> implements Set<E> {
private final Map<E, LazyConstant<Boolean>> backingMap;
public LazySet(Set<E> elementCandidates, Predicate<E> computingFunction) {
this.backingMap = elementCandidates.stream()
.collect(Collectors.toUnmodifiableMap(
Function.identity(),
k -> LazyConstant.of(() -> computingFunction.test(k))));
}
@Override
public boolean contains(Object o) {
var lazyConstant = backingMap.get(o);
return lazyConstant == null
? false
: lazyConstant.get();
}
@Override
public Iterator<E> iterator() {
return null;
}
@Override
public int size() {
return 0;
}
}
}

View File

@ -0,0 +1,191 @@
/*
* Copyright (c) 2026, 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 LazyCollections should not retain throwable classes after failed computation
* @modules java.base/java.lang.ref:open
* @library /test/lib
* @build jdk.test.whitebox.WhiteBox
* @requires vm.opt.final.ClassUnloading
* @run driver jdk.test.lib.helpers.ClassFileInstaller jdk.test.whitebox.WhiteBox
* @run main/othervm
* -Xbootclasspath/a:.
* -XX:+UnlockDiagnosticVMOptions
* -XX:+WhiteBoxAPI
* --add-opens java.base/java.util=ALL-UNNAMED ThrowablesClassUnloading
*/
import jdk.test.lib.Utils;
import jdk.test.whitebox.WhiteBox;
import java.io.ByteArrayOutputStream;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.function.BooleanSupplier;
import java.util.function.IntFunction;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public class ThrowablesClassUnloading {
private static final String THROWABLE_NAME = "test.lazycollection.GeneratedProblem";
private static final String FUNCTION_NAME = "test.lazycollection.ThrowingFunction";
private static WhiteBox wb;
public static void main(String[] args) throws Exception {
TestState state = createFailedLazyList();
if (!waitFor(() -> state.loaderRef().refersTo(null), Utils.adjustTimeout(4_000L))) {
throw new AssertionError("The throwing function class loader was not unloaded");
}
assertLazyAccessFails(state.list(), THROWABLE_NAME);
}
private static TestState createFailedLazyList() throws Exception {
Path sourceDir = Files.createTempDirectory("lazy-throwables-src");
Path classesDir = Files.createTempDirectory("lazy-throwables-classes");
writeSource(sourceDir, "GeneratedProblem.java", """
package test.lazycollection;
public class GeneratedProblem extends RuntimeException {
}
""");
writeSource(sourceDir, "ThrowingFunction.java", """
package test.lazycollection;
import java.util.function.IntFunction;
public class ThrowingFunction implements IntFunction<String> {
@Override
public String apply(int value) {
throw new GeneratedProblem();
}
}
""");
compile(sourceDir, classesDir);
URLClassLoader loader = new URLClassLoader(new URL[] { classesDir.toUri().toURL() },
ThrowablesClassUnloading.class.getClassLoader());
WeakReference<ClassLoader> loaderRef = new WeakReference<>(loader);
Class<?> functionClass = Class.forName(FUNCTION_NAME, true, loader);
@SuppressWarnings("unchecked")
IntFunction<String> function =
(IntFunction<String>) functionClass.getConstructor().newInstance();
List<?> list = createLazyList(function);
assertLazyAccessFails(list, THROWABLE_NAME);
function = null;
functionClass = null;
loader.close();
loader = null;
return new TestState(list, loaderRef);
}
private static void writeSource(Path sourceDir, String fileName, String source) throws Exception {
Path file = sourceDir.resolve("test/lazycollection").resolve(fileName);
Files.createDirectories(file.getParent());
Files.writeString(file, source);
}
private static void compile(Path sourceDir, Path classesDir) throws Exception {
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if (compiler == null) {
throw new AssertionError("No system Java compiler available");
}
ByteArrayOutputStream output = new ByteArrayOutputStream();
int exitCode = compiler.run(null, output, output,
"-d", classesDir.toString(),
sourceDir.resolve("test/lazycollection/GeneratedProblem.java").toString(),
sourceDir.resolve("test/lazycollection/ThrowingFunction.java").toString());
if (exitCode != 0) {
throw new AssertionError("Compilation failed: " + output);
}
}
private static List<?> createLazyList(IntFunction<String> function) throws Exception {
Class<?> lazyCollections = Class.forName("java.util.LazyCollections");
Method ofLazyList = lazyCollections.getDeclaredMethod("ofLazyList", int.class, IntFunction.class);
ofLazyList.setAccessible(true);
return (List<?>) ofLazyList.invoke(null, 1, function);
}
private static void assertLazyAccessFails(List<?> list, String throwableName) {
var x = assertThrows(NoSuchElementException.class, () -> list.get(0));
var message = x.getMessage();
assertTrue(message.contains(throwableName), "Missing throwable name in message: " + message);
}
private static boolean waitFor(BooleanSupplier condition, long timeoutMillis) {
final long deadline = System.currentTimeMillis() + timeoutMillis;
wb = WhiteBox.getWhiteBox();
wb.fullGC();
boolean refProResult;
boolean conditionValue;
try {
do {
refProResult = wb.waitForReferenceProcessing();
conditionValue = condition.getAsBoolean();
if (System.currentTimeMillis() > deadline) {
throw new AssertionError("Timed out waiting for reference");
}
} while (refProResult || !conditionValue);
return conditionValue;
} catch (InterruptedException e) {
throw new AssertionError(e);
}
}
static <X> X assertThrows(Class<X> type, Runnable runnable) {
try {
runnable.run();
} catch (Throwable t) {
if (t.getClass().equals(type)) {
return (X) t;
}
throw new AssertionError("Expected " + type + " to be thrown", t);
}
throw new AssertionError("Expected " + type + " to be thrown but nothing was thrown.");
}
static void assertTrue(boolean value, String message) {
if (!value) {
throw new AssertionError(message);
}
}
private record TestState(List<?> list, WeakReference<ClassLoader> loaderRef) { }
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2005, 2025, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2005, 2026, 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
@ -325,8 +325,11 @@ public class MOAT {
// Immutable Set
testEmptySet(Set.of());
testEmptySet(Set.ofLazy(Set.of(), _ -> true));
testCollMutatorsAlwaysThrow(Set.of());
testCollMutatorsAlwaysThrow(Set.ofLazy(Set.of(1), _ -> true));
testEmptyCollMutatorsAlwaysThrow(Set.of());
testEmptyCollMutatorsAlwaysThrow(Set.ofLazy(Set.of(), _ -> false));
for (Set<Integer> set : Arrays.asList(
Set.<Integer>of(),
Set.of(1),
@ -339,7 +342,10 @@ public class MOAT {
Set.of(1, 2, 3, 4, 5, 6, 7, 8),
Set.of(1, 2, 3, 4, 5, 6, 7, 8, 9),
Set.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10),
Set.of(integerArray))) {
Set.of(integerArray),
Set.ofLazy(Set.<Integer>of(), _ -> true),
Set.ofLazy(Set.of(1), _ -> true),
Set.ofLazy(Set.of(1, 2, 3), _ -> true))) {
testCollection(set);
testImmutableSet(set, 99);
testCollMutatorsAlwaysThrow(set);