mirror of
https://github.com/openjdk/jdk.git
synced 2026-05-09 13:09:43 +00:00
8376811: Implement JEP 531: Lazy Constants (Third Preview)
Reviewed-by: jvernee, vklang, mhaessig
This commit is contained in:
parent
54146adae0
commit
a6bd64be55
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
129
test/hotspot/jtreg/compiler/stable/LazyConstantsIrTest.java
Normal file
129
test/hotspot/jtreg/compiler/stable/LazyConstantsIrTest.java
Normal 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();
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
|
||||
@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
184
test/jdk/java/lang/LazyConstant/LazyConstantClassUnloading.java
Normal file
184
test/jdk/java/lang/LazyConstant/LazyConstantClassUnloading.java
Normal 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) { }
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
591
test/jdk/java/lang/LazyConstant/LazySetTest.java
Normal file
591
test/jdk/java/lang/LazyConstant/LazySetTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
191
test/jdk/java/lang/LazyConstant/ThrowablesClassUnloading.java
Normal file
191
test/jdk/java/lang/LazyConstant/ThrowablesClassUnloading.java
Normal 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) { }
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user