diff --git a/src/java.base/share/classes/java/lang/LazyConstant.java b/src/java.base/share/classes/java/lang/LazyConstant.java index 85f9d0e82fd..77d36bb2f52 100644 --- a/src/java.base/share/classes/java/lang/LazyConstant.java +++ b/src/java.base/share/classes/java/lang/LazyConstant.java @@ -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. *

* A lazy constant is created using the factory method * {@linkplain LazyConstant#of(Supplier) LazyConstant.of({@code })}. *

- * When created, the lazy constant is not initialized, meaning it has no contents. + * When created, the lazy constant is not initialized, meaning it has no content. *

* The lazy constant (of type {@code T}) can then be initialized - * (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 computing function * (provided at construction) will be invoked and the result will be used to initialize * the constant. *

- * Once a lazy constant is initialized, its contents can never change - * and will be retrieved over and over again upon subsequent {@linkplain #get() get()} - * invocations. + * Once a lazy constant is initialized, its content can never change + * and will always be returned by subsequent {@linkplain #get() get()} invocations. *

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

Exception handling

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

- * 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: + *

+ * 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. + *

+ * If the computing function recursively invokes itself via the lazy constant, a + * {@linkplain NoSuchElementException} (with an {@linkplain IllegalStateException} as a + * cause) will be thrown. * *

Composing lazy constants

* 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 = LazyConstant.of( Foo::new ); - * private static final LazyConstant BAR = LazyConstant.of( () -> new Bar(FOO.get()) ); + * static final LazyConstant FOO = LazyConstant.of( Foo::new ); + * static final LazyConstant 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 * the computing thread), 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. *

* The invocation of the computing function and the resulting initialization of * the constant {@linkplain java.util.concurrent##MemoryVisibility happens-before} * 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. *

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

Performance

- * 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 constant folding. * 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. * - *

Miscellaneous

- * Except for {@linkplain Object#equals(Object) equals(obj)} and - * {@linkplain #orElse(Object) orElse(other)} parameters, all method parameters - * must be non-null, 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. *

* 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 type of the constant @@ -205,39 +218,24 @@ public sealed interface LazyConstant 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} *

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

* After this method returns successfully, the constant is guaranteed to be * initialized. *

- * 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} - *

- * 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 * the provided {@code obj}, otherwise {@code false}} *

* 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 * not equal. *

* This method never triggers initialization of this lazy constant. @@ -267,11 +265,11 @@ public sealed interface LazyConstant *

* 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). *

* 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 // 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}} *

* 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). *

- * 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 LazyConstant ofFlattened(Supplier computingFunction) { + * return (computingFunction instanceof LazyConstant lc) + * ? (LazyConstant) 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 type of the constant + * @throws NullPointerException if the provided {@code computingFunction} is + * {@code null} * */ @SuppressWarnings("unchecked") static LazyConstant of(Supplier computingFunction) { Objects.requireNonNull(computingFunction); - if (computingFunction instanceof LazyConstant lc) { - return (LazyConstant) lc; - } return LazyConstantImpl.ofLazy(computingFunction); } diff --git a/src/java.base/share/classes/java/util/LazyCollections.java b/src/java.base/share/classes/java/util/LazyCollections.java index 0bbdad87ac4..e15721db2ce 100644 --- a/src/java.base/share/classes/java/util/LazyCollections.java +++ b/src/java.base/share/classes/java/util/LazyCollections.java @@ -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 extends ImmutableCollections.AbstractImmutableList { @@ -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> functionHolder; - @Stable + private final FunctionHolder> functionHolder; private final Mutexes mutexes; + private final Throwables throwables; private LazyList(int size, IntFunction 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, V> + @jdk.internal.vm.annotation.TrustFinalFields + private static final class LazyEnumMap, V> extends AbstractLazyMap { - @Stable private final Class 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 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 + @jdk.internal.vm.annotation.TrustFinalFields + private static final class LazyMap extends AbstractLazyMap { // 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 indexMapper; public LazyMap(Set keys, Function 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 + @jdk.internal.vm.annotation.TrustFinalFields + private static abstract sealed class AbstractLazyMap extends ImmutableCollections.AbstractImmutableMap { - // This field shadows AbstractMap.keySet which is not @Stable. - @Stable - Set keySet; + private final Mutexes mutexes; + private final Throwables throwables; + private final int size; + private final FunctionHolder> functionHolder; + private final Set> 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 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> functionHolder; - @Stable - private final Set> entrySet; + private long hash; private AbstractLazyMap(Set 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> entrySet() { return entrySet; } @Override public Set 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 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 extends ImmutableCollections.AbstractImmutableSet> { + private static final class LazyMapEntrySet extends ImmutableCollections.AbstractImmutableSet> { // Use a separate field for the outer class in order to facilitate - // a @Stable annotation. - @Stable + // a trusted field. private final AbstractLazyMap map; private LazyMapEntrySet(AbstractLazyMap map) { @@ -318,14 +361,13 @@ final class LazyCollections { return new LazyMapEntrySet<>(outer); } + @jdk.internal.vm.annotation.TrustFinalFields @jdk.internal.ValueBased static final class LazyMapIterator implements Iterator> { // Use a separate field for the outer class in order to facilitate - // a @Stable annotation. - @Stable + // a trusted field. private final AbstractLazyMap map; - @Stable private final Iterator keyIterator; private LazyMapIterator(AbstractLazyMap 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 next() { final K k = keyIterator.next(); - return new LazyEntry<>(k, map, map.functionHolder); + return new LazyEntry<>(k, map); } @Override public void forEachRemaining(Consumer> action) { + Objects.requireNonNull(action); final Consumer 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 getKey, // trick - AbstractLazyMap map, - FunctionHolder> functionHolder) implements Entry { + private record LazyEntry(@Override K getKey, // trick + AbstractLazyMap map) implements Entry { @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 extends ImmutableCollections.AbstractImmutableCollection { // Use a separate field for the outer class in order to facilitate - // a @Stable annotation. - @Stable + // a trusted field. private final AbstractLazyMap map; private LazyMapValues(AbstractLazyMap map) { @@ -416,7 +455,133 @@ final class LazyCollections { } - static final class Mutexes { + @jdk.internal.vm.annotation.TrustFinalFields + private static final class LazySet + extends ImmutableCollections.AbstractImmutableSet + implements Set { + + private final Map 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 elementCandidates, + Predicate 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 iterator() { + return new LazySetIterator<>(map.entrySet().iterator()); + } + + @jdk.internal.vm.annotation.TrustFinalFields + static final class LazySetIterator implements Iterator { + + private final Iterator> iterator; + + E current; + + public LazySetIterator(Iterator> iterator) { + this.iterator = iterator; + super(); + } + + @Override + public boolean hasNext() { + if (current != null) { + return true; + } + while (iterator.hasNext()) { + Map.Entry 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. + *

+ * Class names are used instead of Class objects to avoid pinning class loaders after + * a failed computation. + *

+ * 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 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 List ofLazyList(int size, - IntFunction computingFunction) { - return new LazyList<>(size, computingFunction); - } - - public static Map ofLazyMap(Set keys, - Function computingFunction) { - return new LazyMap<>(keys, computingFunction); - } @SuppressWarnings("unchecked") - public static , V> - Map ofLazyMapWithEnumKeys(Set keys, - Function computingFunction) { - // The input set is not empty - final Class 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) new LazyEnumMap<>((Set) keys, enumType, min, backingSize, member, (Function) computingFunction); - } - - @SuppressWarnings("unchecked") - static T orElseComputeSlowPath(final T[] array, + private static 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) 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) 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 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 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 the underlying function type */ @AOTSafeClassInitializer - static final class FunctionHolder { + private static final class FunctionHolder { 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 List ofLazyList(int size, + IntFunction computingFunction) { + return new LazyList<>(size, computingFunction); + } + + static Map ofLazyMap(Set keys, + Function computingFunction) { + return new LazyMap<>(keys, computingFunction); + } + + static Set ofLazySet(Set elementCandidates, + Predicate computingFunction) { + return new LazySet<>(elementCandidates, computingFunction); + } + + @SuppressWarnings("unchecked") + static , V> Map ofLazyMapWithEnumKeys(Set keys, + Function computingFunction) { + // The input set is not empty + final Class 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) new LazyEnumMap<>((Set) keys, enumType, min, backingSize, member, (Function) computingFunction); + } + } diff --git a/src/java.base/share/classes/java/util/List.java b/src/java.base/share/classes/java/util/List.java index 5f9a90e1748..a906215f16b 100644 --- a/src/java.base/share/classes/java/util/List.java +++ b/src/java.base/share/classes/java/util/List.java @@ -1200,23 +1200,36 @@ public interface List extends SequencedCollection { * {@return a new lazily computed list of the provided {@code size}} *

* 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}). *

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

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

- * 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: + *

+ * 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. + *

+ * 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. *

* 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 extends SequencedCollection { * The returned list and its {@link List#subList(int, int) subList()} or * {@link List#reversed()} views implement the {@link RandomAccess} interface. *

- * If the provided computing function recursively calls itself via the returned - * lazy list for the same index, an {@linkplain IllegalStateException} - * will be thrown. - *

* 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}. *

* The returned lazy list strongly references its computing * function used to compute elements at least as long as there are uninitialized * elements. *

* The returned List is not {@linkplain Serializable}. + *

+ * 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 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(); + * } + * } + *

+ * The returned {@code List} can be thought of as a list backed by a + * {@code List>} field and where the {@linkplain List#get(int)} + * operation is equivalent to: + * {@snippet lang = java: + * class LazyList extends AbstractList { + * + * private final List> backingList; + * + * public LazyList(int size, IntFunction 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. + *

+ * Elements in the returned list are eligible for certain performance optimizations + * such as constant folding 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 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 diff --git a/src/java.base/share/classes/java/util/Map.java b/src/java.base/share/classes/java/util/Map.java index fa16fb89050..f5fb190f7d8 100644 --- a/src/java.base/share/classes/java/util/Map.java +++ b/src/java.base/share/classes/java/util/Map.java @@ -1759,50 +1759,122 @@ public interface Map { * provided {@code computingFunction} when they are first accessed * (e.g., via {@linkplain Map#get(Object) Map::get}). *

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

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

- * 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: + *

+ * 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. + *

+ * 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. *

* The values of any {@link Map#values()} or {@link Map#entrySet()} views of * the returned map are also lazily computed. *

- * If the provided computing function recursively calls itself via - * the returned lazy map for the same key, an {@linkplain IllegalStateException} - * will be thrown. - *

* 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}. *

* The returned lazy map strongly references its underlying * computing function used to compute values at least as long as there are * uncomputed values. *

* The returned Map is not {@linkplain Serializable}. + *

+ * If the provided {@code Set} of {@code keys} is subsequently modified, the returned + * {@code Map} will not reflect such modifications. + *

+ * 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. + *

+ * 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 CACHE + * = Map.ofLazy(Set.of(0, 1, 3, 42, 97), param -> expensiveOperation(param)); + * + * public static Optional 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(); + * + * } + * } + *

+ * The returned {@code Map} can be thought of as a map backed by a + * {@code Map>} field and where the {@linkplain Map#get(Object)} + * operation is equivalent to: + * {@snippet lang = java: + * class LazyMap extends AbstractMap { + * + * private final Map> backingMap; + * + * public LazyMap(Set keys, Function 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. + *

+ * Values in the returned map are eligible for certain performance optimizations + * such as constant folding 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 the type of keys maintained by the returned map * @param the type of mapped values in the returned map - * @throws NullPointerException if the provided set of {@code 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 diff --git a/src/java.base/share/classes/java/util/Set.java b/src/java.base/share/classes/java/util/Set.java index 5ce3bf04c7c..0c66d4ef53c 100644 --- a/src/java.base/share/classes/java/util/Set.java +++ b/src/java.base/share/classes/java/util/Set.java @@ -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. *

  • They disallow {@code null} elements. Attempts to create them with * {@code null} elements result in {@code NullPointerException}. - *
  • They are serializable if all elements are serializable. + *
  • Unless otherwise specified, they are serializable if all elements are serializable. *
  • They reject duplicate elements at creation time. Duplicate elements * passed to a static factory method result in {@code IllegalArgumentException}. *
  • The iteration order of set elements is unspecified and is subject to change. @@ -734,4 +739,158 @@ public interface Set extends Collection { return (Set)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} + *

    + * In the following, the term membership status 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} is a member of the + * returned set. Conversely, if the membership status for element {@code E} is + * {@code false}, then {@code E} is not a member of the returned set. + *

    + * 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 a logical member or as + * a logical non-member). + *

    + * 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. + *

    + * 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. + *

    + * All failures are handled in this way. There is a special case that causes + * unchecked exceptions to be thrown: + *

    + * 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. + *

    + * 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}. + *

    + * 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. + *

    + * The returned Set is not {@linkplain Serializable}. + *

    + * If the provided {@code Set} of {@code elementCandidates} is subsequently modified, + * the returned {@code Set} will not reflect such modifications. + *

    + * 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. + *

    + * 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

    + * The returned {@code Set} can be thought of as a set backed by a + * {@code Map>} field and where the {@linkplain Set#contains(Object)} + * operation is equivalent to: + * {@snippet lang = java: + * class LazySet extends AbstractCollection implements Set { + * + * private final Map> backingMap; + * + * public LazySet(Set elementCandidates, Predicate 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. + *

    + * Elements in the returned set are eligible for certain performance optimizations + * such as constant folding 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 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 Set ofLazy(Set elementCandidates, + Predicate computingFunction) { + Objects.requireNonNull(elementCandidates); + Objects.requireNonNull(computingFunction); + return LazyCollections.ofLazySet(elementCandidates, computingFunction); + } + } diff --git a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java index 2c8ab1f86c0..82c55d6f017 100644 --- a/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java +++ b/src/java.base/share/classes/jdk/internal/javac/PreviewFeature.java @@ -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") diff --git a/src/java.base/share/classes/jdk/internal/lang/LazyConstantImpl.java b/src/java.base/share/classes/jdk/internal/lang/LazyConstantImpl.java index 59d0174c4c9..e5a1dd62c11 100644 --- a/src/java.base/share/classes/jdk/internal/lang/LazyConstantImpl.java +++ b/src/java.base/share/classes/jdk/internal/lang/LazyConstantImpl.java @@ -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 implements LazyConstant { // 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 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 computingFunction) { - this.computingFunction = computingFunction; + this.computingFunctionOrExceptionType = computingFunction; } @ForceInline @@ -82,34 +84,53 @@ public final class LazyConstantImpl implements LazyConstant { 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 implements LazyConstant { 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 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 implements LazyConstant { 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); } } diff --git a/test/hotspot/jtreg/compiler/stable/LazyConstantsIrTest.java b/test/hotspot/jtreg/compiler/stable/LazyConstantsIrTest.java new file mode 100644 index 00000000000..b9f9343dd39 --- /dev/null +++ b/test/hotspot/jtreg/compiler/stable/LazyConstantsIrTest.java @@ -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 LAZY_CONSTANT = LazyConstant.of(() -> THE_VALUE); + static final List LAZY_LIST = List.ofLazy(1, _ -> THE_VALUE); + static final Set LAZY_SET = Set.ofLazy(Set.of(THE_VALUE), _ -> true); + static final Map 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(); + } + +} diff --git a/test/jdk/java/lang/LazyConstant/DemoContainerInjectionTest.java b/test/jdk/java/lang/LazyConstant/DemoContainerInjectionTest.java index df51a562210..0878ec03789 100644 --- a/test/jdk/java/lang/LazyConstant/DemoContainerInjectionTest.java +++ b/test/jdk/java/lang/LazyConstant/DemoContainerInjectionTest.java @@ -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 get(Class type); } - interface SettableContainer extends Container { - void set(Class type, T implementation); - } - record ComputedContainer(Map, ?> components) implements Container { @Override @@ -110,26 +97,6 @@ final class DemoContainerInjectionTest { } - record SettableScratchContainer(Map, Object> scratch, Map, ?> components) implements SettableContainer { - - @Override - public void set(Class type, T implementation) { - if (scratch.putIfAbsent(type, type.cast(implementation)) != null) { - throw new IllegalStateException("Can only set once for " + type); - } - } - - @Override - public T get(Class type) { - return type.cast(components.get(type)); - } - - static SettableContainer of(Set> components) { - Map, Object> scratch = new ConcurrentHashMap<>(); - return new SettableScratchContainer(scratch, Map.ofLazy(components, scratch::get)); - } - - } record ProviderContainer(Map, ?> components) implements Container { diff --git a/test/jdk/java/lang/LazyConstant/DemoImperativeTest.java b/test/jdk/java/lang/LazyConstant/DemoImperativeTest.java deleted file mode 100644 index bc1208e67f8..00000000000 --- a/test/jdk/java/lang/LazyConstant/DemoImperativeTest.java +++ /dev/null @@ -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 orElse(T other); - boolean isSet(); - boolean trySet(T t); - T get(); - - static ImperativeStableValue of() { - var scratch = new AtomicReference(); - return new Impl<>(scratch, LazyConstant.of(scratch::get)); - } - - } - - - private record Impl(AtomicReference scratch, - LazyConstant underlying) implements ImperativeStableValue { - - @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.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)); - } - -} diff --git a/test/jdk/java/lang/LazyConstant/LazyConstantClassUnloading.java b/test/jdk/java/lang/LazyConstant/LazyConstantClassUnloading.java new file mode 100644 index 00000000000..fcc9920aa39 --- /dev/null +++ b/test/jdk/java/lang/LazyConstant/LazyConstantClassUnloading.java @@ -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 { + @Override + public String get() { + throw new GeneratedProblem(); + } + } + """); + compile(sourceDir, classesDir); + + URLClassLoader loader = new URLClassLoader(new URL[] { classesDir.toUri().toURL() }, + LazyConstantClassUnloading.class.getClassLoader()); + WeakReference loaderRef = new WeakReference<>(loader); + + Class supplierClass = Class.forName(SUPPLIER_NAME, true, loader); + @SuppressWarnings("unchecked") + Supplier supplier = + (Supplier) 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 assertThrows(Class 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 loaderRef) { } +} diff --git a/test/jdk/java/lang/LazyConstant/LazyConstantSafePublicationTest.java b/test/jdk/java/lang/LazyConstant/LazyConstantSafePublicationTest.java index 3c51d4afd90..84e6ddd5444 100644 --- a/test/jdk/java/lang/LazyConstant/LazyConstantSafePublicationTest.java +++ b/test/jdk/java/lang/LazyConstant/LazyConstantSafePublicationTest.java @@ -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[] constants; + final LazyConstantImpl[] constants; final int[] observations = new int[SIZE]; int i = 0; - public Consumer(LazyConstant[] constants) { + public Consumer(LazyConstantImpl[] constants) { this.constants = constants; } @Override public void run() { for (; i < SIZE; i++) { - LazyConstant s = constants[i]; + LazyConstantImpl 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[] constants; + final LazyConstantImpl[] constants; - public Producer(LazyConstant[] constants) { + public Producer(LazyConstantImpl[] constants) { this.constants = constants; } @Override public void run() { - LazyConstant s; + LazyConstantImpl 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[] constants = constants(); + final LazyConstantImpl[] constants = constants(); List 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[] constants, List consumers, Thread... threads) { + static void join(final LazyConstantImpl[] constants, List 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[] constants() { + static LazyConstantImpl[] constants() { @SuppressWarnings("unchecked") - LazyConstant[] constants = (LazyConstant[]) new LazyConstant[SIZE]; + LazyConstantImpl[] constants = (LazyConstantImpl[]) new LazyConstantImpl[SIZE]; for (int i = 0; i < SIZE; i++) { - constants[i] = LazyConstant.of(Holder::new); + constants[i] = LazyConstantImpl.ofLazy(Holder::new); } return constants; } diff --git a/test/jdk/java/lang/LazyConstant/LazyConstantTest.java b/test/jdk/java/lang/LazyConstant/LazyConstantTest.java index 485138bc492..4faaf74a811 100644 --- a/test/jdk/java/lang/LazyConstant/LazyConstantTest.java +++ b/test/jdk/java/lang/LazyConstant/LazyConstantTest.java @@ -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 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, LazyConstant> factory) { LazyConstantTestUtil.CountingSupplier 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, LazyConstant> factory) { - LazyConstantTestUtil.CountingSupplier 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, LazyConstant> factory) { + // Test different Throwable categories + for (LazyConstantTestUtil.Thrower thrower : LazyConstantTestUtil.throwers()) { + AtomicReference exceptionThrown = new AtomicReference<>(); + LazyConstantTestUtil.CountingSupplier 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 constant) { - assertNull(constant.orElse(null)); - constant.get(); - assertEquals(VALUE, constant.orElse(null)); + @MethodSource("factories") + void nullInComputingFunction(Function, LazyConstant> factory) { + LazyConstantTestUtil.CountingSupplier cs = new LazyConstantTestUtil.CountingSupplier<>(() -> { + return null; + }); + exceptionInComputingFunction(factory, cs, () -> NullPointerException.class, null); + } + + void exceptionInComputingFunction(Function, LazyConstant> factory, + LazyConstantTestUtil.CountingSupplier cs, + Supplier> 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 constant) { - assertFalse(constant.isInitialized()); - constant.get(); - assertTrue(constant.isInitialized()); - } - - @ParameterizedTest - @MethodSource("lazyConstants") - void testHashCode(LazyConstant constant) { - assertEquals(System.identityHashCode(constant), constant.hashCode()); + @MethodSource("factories") + void testHashCode(Function, LazyConstant> factory) { + LazyConstantTestUtil.CountingSupplier 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 c0) { - assertNotEquals(null, c0); + @MethodSource("factories") + void testEquals(Function, LazyConstant> factory) { + LazyConstantTestUtil.CountingSupplier cs = new LazyConstantTestUtil.CountingSupplier<>(SUPPLIER); + var lazy = factory.apply(cs); + assertNotEquals(null, lazy); LazyConstant 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 constant) { LazyConstant c1 = LazyConstant.of(constant); - assertSame(constant, c1); + assertNotSame(constant, c1); } @Test @@ -161,9 +194,156 @@ final class LazyConstantTest { void recursiveCall() { AtomicReference> ref = new AtomicReference<>(); LazyConstant constant = LazyConstant.of(() -> ref.get().get()); - LazyConstant 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> ref = new AtomicReference<>(); + AtomicInteger cnt = new AtomicInteger(); + + final class NaughtySupplier implements Supplier { + @Override + public Integer get() { + return ref.get().get(); + } + + @Override + public String toString() { + cnt.incrementAndGet(); + throw new UnsupportedOperationException("I should never be seen"); + } + } + + LazyConstant 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 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 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 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 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> lazyConstants() { diff --git a/test/jdk/java/lang/LazyConstant/LazyConstantTestUtil.java b/test/jdk/java/lang/LazyConstant/LazyConstantTestUtil.java index 502f46b726d..31317c52490 100644 --- a/test/jdk/java/lang/LazyConstant/LazyConstantTestUtil.java +++ b/test/jdk/java/lang/LazyConstant/LazyConstantTestUtil.java @@ -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 + extends AbstractCounting> + implements Predicate { + + public CountingPredicate(Predicate delegate) { + super(delegate); + } + + @Override + public boolean test(T t) { + incrementCounter(); + return delegate.test(t); + } + + } + public static final class CountingBiFunction extends AbstractCounting> implements BiFunction { @@ -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 throwableClass, Object input) { + return "Unable to access the lazy collection because " + throwableClass.getName() + " was thrown at initial computation for input '" + input + "'"; + } + + @SuppressWarnings("unchecked") + static void sneakyThrow(Throwable e) throws E { + throw (E) e; + } + + record Thrower(String message, Function factory) { + public Supplier supplier() { + return () -> factory.apply(Thrower.this.message); + } + } + + static List throwers() { + return List.of( + new Thrower("Initial checked exception", IOException::new), + new Thrower("Initial runtime exception", UnsupportedOperationException::new), + new Thrower("Initial Error", InternalError::new) + ); + } + } diff --git a/test/jdk/java/lang/LazyConstant/LazyListTest.java b/test/jdk/java/lang/LazyConstant/LazyListTest.java index 4201fff3bb7..c4cc843646e 100644 --- a/test/jdk/java/lang/LazyConstant/LazyListTest.java +++ b/test/jdk/java/lang/LazyConstant/LazyListTest.java @@ -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 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 cif = new LazyConstantTestUtil.CountingIntFunction(_ -> { - throw new UnsupportedOperationException(); + throw new UnsupportedOperationException("Initial exception"); }); + exceptionInComputingFunction(cif, UnsupportedOperationException.class); + } + + @Test + void nullResultInComputingFunction() { + LazyConstantTestUtil.CountingIntFunction cif = new LazyConstantTestUtil.CountingIntFunction(_ -> { + return null; + }); + exceptionInComputingFunction(cif, NullPointerException.class); + } + + void exceptionInComputingFunction(LazyConstantTestUtil.CountingIntFunction cif, + Class 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> 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 ref = viewOp.apply(newRegularList()); + List 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 ref = viewOp.apply(newRegularList()); + List 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 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 lazy = newLazyList(); + Iterator iter = (Iterator) viewOp.apply(lazy); + List 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 childOperations() { + static Stream 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) null)) ); @@ -448,4 +620,5 @@ final class LazyListTest { static List newRegularList() { return IntStream.range(0, SIZE).boxed().toList(); } + } diff --git a/test/jdk/java/lang/LazyConstant/LazyMapTest.java b/test/jdk/java/lang/LazyConstant/LazyMapTest.java index 889ec43df6a..56285fcd52c 100644 --- a/test/jdk/java/lang/LazyConstant/LazyMapTest.java +++ b/test/jdk/java/lang/LazyConstant/LazyMapTest.java @@ -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 set) { @@ -132,19 +140,58 @@ final class LazyMapTest { assertNull(lazy.get(Value.ILLEGAL_BETWEEN)); } + @ParameterizedTest + @MethodSource("nonEmptySets") + void getOrDefault(Set set) { + LazyConstantTestUtil.CountingFunction 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 set) { - LazyConstantTestUtil.CountingFunction 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 exceptionThrown = new AtomicReference<>(); + LazyConstantTestUtil.CountingFunction 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> lazy = Map.ofLazy(set, k -> (Map) 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 set = Set.of(key); + + final AtomicReference> ref = new AtomicReference<>(); + @SuppressWarnings("unchecked") + Map> lazy = Map.ofLazy(set, k -> (Map) 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 set) { + assertEquals(newRegularMap(set).entrySet().hashCode(), + newLazyMap(set).entrySet().hashCode()); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void keySet(Set set) { + var lazy = newLazyMap(set); + var keySet = lazy.keySet(); + assertEquals(set, keySet); + assertThrows(UnsupportedOperationException.class, () -> lazy.remove(KEY)); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void entrySetValue(Set 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 set) { + var lazy = newLazyMap(set); + var lazyValues = lazy.values(); + assertEquals(0, lazyValues.size()); + assertTrue(lazyValues.isEmpty()); + } + @ParameterizedTest @MethodSource("nonEmptySets") void values(Set 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 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 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 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 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 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 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 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 enumMap = Map.ofLazy(EnumSet.of(KEY), Value::asInt); assertTrue(enumMap.getClass().getName().contains("Enum"), enumMap.getClass().getName()); Map emptyMap = Map.ofLazy(EnumSet.noneOf(Value.class), Value::asInt); @@ -498,7 +739,8 @@ final class LazyMapTest { static Stream 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 extends AbstractMap { + + private final Map> backingMap; + + public LazyMap(Set keys, Function 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> entrySet() { + return Set.of(); + } + } + } diff --git a/test/jdk/java/lang/LazyConstant/LazySetTest.java b/test/jdk/java/lang/LazyConstant/LazySetTest.java new file mode 100644 index 00000000000..4e168c7bcad --- /dev/null +++ b/test/jdk/java/lang/LazyConstant/LazySetTest.java @@ -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 SET = Set.of(NON_MEMBER, MEMBER); + private static final Predicate 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 set) { + assertThrows(NullPointerException.class, () -> Set.ofLazy(set, null), set.getClass().getSimpleName()); + assertThrows(NullPointerException.class, () -> Set.ofLazy(null, PREDICATE)); + Set setWithNull = new HashSet<>(); + setWithNull.add(MEMBER); + setWithNull.add(null); + assertThrows(NullPointerException.class, () -> Set.ofLazy(setWithNull, PREDICATE)); + } + + @ParameterizedTest + @MethodSource("emptySets") + void empty(Set set) { + var lazy = newLazySet(set); + assertTrue(lazy.isEmpty()); + assertEquals("[]", lazy.toString()); + } + + @ParameterizedTest + @MethodSource("allSets") + void size(Set set) { + assertEquals(newRegularSet(set).size(), newLazySet(set).size()); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void exception(Set set) { + LazyConstantTestUtil.CountingPredicate 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 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 set) { + var lazy = newLazySet(set); + var expected = newRegularSet(set); + Set actual = new HashSet<>(); + lazy.forEach(actual::add); + assertEquals(expected, actual); + } + + @ParameterizedTest + @MethodSource("emptySets") + void toStringTestEmpty(Set set) { + var lazy = newLazySet(set); + assertEquals("[]", lazy.toString()); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void toStringTest(Set 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 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 set) { + var lazy = newLazySet(set); + var regular = newRegularSet(set); + assertEquals(regular, lazy); + assertEquals(lazy, regular); + assertNotEquals("A", lazy); + } + + @ParameterizedTest + @MethodSource("nonEmptySets") + void recursiveCall(Set set) { + final AtomicReference> ref = new AtomicReference<>(); + @SuppressWarnings("unchecked") + Set 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 set) { + Set 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 set) { + Set 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 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 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 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 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 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 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 void assertThrowsForOperation(Class expectedType, Operation operation) { + for (Set set : allSets().toList()) { + var lazy = newLazySet(set); + assertThrows(expectedType, () -> operation.accept(lazy), set.getClass().getSimpleName() + " " + operation); + } + } + + // Implementing interfaces + + @ParameterizedTest + @MethodSource("allSets") + void serializable(Set set) { + var lazy = newLazySet(set); + assertFalse(lazy instanceof Serializable); + } + + @Test + void overriddenEnum() { + final var overridden = Value.THIRTEEN; + Set enumMap = Set.ofLazy(EnumSet.of(overridden), PREDICATE); + assertEquals(PREDICATE.test(overridden), enumMap.contains(overridden), enumMap.toString()); + } + + // Support constructs + + record Operation(String name, + Consumer> consumer) implements Consumer> { + @Override + public void accept(Set set) { consumer.accept(set); } + @Override + public String toString() { return name; } + } + + static Stream 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 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 newLazySet(Set set) { + return Set.ofLazy(set, PREDICATE); + } + + static Set newRegularSet(Set set) { + return set.stream() + .filter(PREDICATE) + .collect(Collectors.toSet()); + } + + private static Stream> 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> emptySets() { + return Stream.of( + Set.of(), + linkedHashSet(), + treeSet(), + EnumSet.noneOf(Value.class) + ); + } + + private static Stream> allSets() { + return Stream.concat( + nonEmptySets(), + emptySets() + ); + } + + static Set treeSet(Value... values) { + return populate(new TreeSet<>(Comparator.comparingInt(Value::asInt).reversed()),values); + } + + static Set linkedHashSet(Value... values) { + return populate(new LinkedHashSet<>(), values); + } + + static Set populate(Set set, Value... values) { + set.addAll(Arrays.asList(values)); + return set; + } + + // 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