diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index e20693118c3..f42663e9e8f 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java @@ -20,8 +20,10 @@ import java.lang.ref.SoftReference; import java.lang.ref.WeakReference; import java.lang.reflect.Array; +import java.util.AbstractCollection; import java.util.AbstractMap; import java.util.AbstractSet; +import java.util.Collection; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; @@ -29,6 +31,8 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; @@ -101,7 +105,17 @@ public class ConcurrentReferenceHashMap extends AbstractMap implemen /** * Late binding entry set. */ - private volatile @Nullable Set> entrySet; + private @Nullable Set> entrySet; + + /** + * Late binding key set. + */ + private @Nullable Set keySet; + + /** + * Late binding values collection. + */ + private @Nullable Collection values; /** @@ -512,6 +526,26 @@ public Set> entrySet() { return entrySet; } + @Override + public Set keySet() { + Set keySet = this.keySet; + if (keySet == null) { + keySet = new KeySet(); + this.keySet = keySet; + } + return keySet; + } + + @Override + public Collection values() { + Collection values = this.values; + if (values == null) { + values = new Values(); + this.values = values; + } + return values; + } + private @Nullable T doTask(@Nullable Object key, Task task) { int hash = getHash(key); return getSegmentForHash(hash).doTask(hash, key, task); @@ -940,7 +974,7 @@ private interface Entries { /** * Internal entry-set implementation. */ - private class EntrySet extends AbstractSet> { + private final class EntrySet extends AbstractSet> { @Override public Iterator> iterator() { @@ -976,13 +1010,133 @@ public int size() { public void clear() { ConcurrentReferenceHashMap.this.clear(); } + + @Override + public Spliterator> spliterator() { + return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT); + } + } + + /** + * Internal key-set implementation. + */ + private final class KeySet extends AbstractSet { + @Override + public Iterator iterator() { + return new KeyIterator(); + } + + @Override + public int size() { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ConcurrentReferenceHashMap.this.isEmpty(); + } + + @Override + public void clear() { + ConcurrentReferenceHashMap.this.clear(); + } + + @Override + public boolean contains(Object k) { + return ConcurrentReferenceHashMap.this.containsKey(k); + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT); + } + } + + /** + * Internal key iterator implementation. + */ + private final class KeyIterator implements Iterator { + + private final Iterator> iterator = entrySet().iterator(); + + @Override + public boolean hasNext() { + return this.iterator.hasNext(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + + @Override + public K next() { + return this.iterator.next().getKey(); + } + } + + /** + * Internal values collection implementation. + */ + private final class Values extends AbstractCollection { + @Override + public Iterator iterator() { + return new ValueIterator(); + } + + @Override + public int size() { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public boolean isEmpty() { + return ConcurrentReferenceHashMap.this.isEmpty(); + } + + @Override + public void clear() { + ConcurrentReferenceHashMap.this.clear(); + } + + @Override + public boolean contains(Object v) { + return ConcurrentReferenceHashMap.this.containsValue(v); + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliterator(this, Spliterator.CONCURRENT); + } } + /** + * Internal value iterator implementation. + */ + private final class ValueIterator implements Iterator { + + private final Iterator> iterator = entrySet().iterator(); + + @Override + public boolean hasNext() { + return this.iterator.hasNext(); + } + + @Override + public void remove() { + this.iterator.remove(); + } + + @Override + public V next() { + return this.iterator.next().getValue(); + } + } /** * Internal entry iterator implementation. */ - private class EntryIterator implements Iterator> { + private final class EntryIterator implements Iterator> { private int segmentIndex; diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java index 3cd060d7c7f..40aed3a1303 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -16,8 +16,8 @@ package org.springframework.util; -import java.util.ArrayList; -import java.util.Comparator; +import java.util.AbstractMap; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -25,6 +25,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.Spliterator; +import java.util.stream.Collectors; import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; @@ -32,12 +34,12 @@ import org.springframework.util.ConcurrentReferenceHashMap.Entry; import org.springframework.util.ConcurrentReferenceHashMap.Reference; import org.springframework.util.ConcurrentReferenceHashMap.Restructure; -import org.springframework.util.comparator.Comparators; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.Assertions.assertThatNoException; +import static org.assertj.core.api.Assertions.assertThatThrownBy; /** * Tests for {@link ConcurrentReferenceHashMap}. @@ -47,8 +49,6 @@ */ class ConcurrentReferenceHashMapTests { - private static final Comparator NULL_SAFE_STRING_SORT = Comparators.nullsLow(); - private TestWeakConcurrentCache map = new TestWeakConcurrentCache<>(); @@ -450,19 +450,176 @@ void keySet() { assertThat(this.map.keySet()).isEqualTo(expected); } + @Test + void keySetContains() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(this.map.keySet()).containsExactlyInAnyOrder(123, 456, null); + } + + @Test + void keySetRemove() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(this.map.keySet().remove(123)).isTrue(); + assertThat(this.map).doesNotContainKey(123); + assertThat(this.map.keySet().remove(123)).isFalse(); + } + + @Test + void keySetIterator() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Iterator it = this.map.keySet().iterator(); + assertThat(it).toIterable().containsExactlyInAnyOrder(123, 456, null); + assertThat(it).isExhausted(); + } + + @Test + void keySetIteratorRemove() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Iterator keySetIterator = this.map.keySet().iterator(); + while (keySetIterator.hasNext()) { + Integer key = keySetIterator.next(); + if (key != null && key.equals(456)) { + keySetIterator.remove(); + } + } + assertThat(this.map).containsOnlyKeys(123, null); + } + + @Test + void keySetClear() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + this.map.keySet().clear(); + assertThat(this.map).isEmpty(); + assertThat(this.map.keySet()).isEmpty(); + } + + @Test + void keySetAdd() { + assertThatThrownBy(() -> + this.map.keySet().add(12345) + ).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void keySetStream() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Set keys = this.map.keySet().stream().collect(Collectors.toSet()); + assertThat(keys).containsExactlyInAnyOrder(123, 456, null); + } + + @Test + void keySetSpliteratorCharacteristics() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Spliterator spliterator = this.map.keySet().spliterator(); + assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT, Spliterator.DISTINCT); + assertThat(spliterator.estimateSize()).isEqualTo(3L); + assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L); + } + @Test void valuesCollection() { this.map.put(123, "123"); this.map.put(456, null); this.map.put(null, "789"); - List actual = new ArrayList<>(this.map.values()); - List expected = new ArrayList<>(); - expected.add("123"); - expected.add(null); - expected.add("789"); - actual.sort(NULL_SAFE_STRING_SORT); - expected.sort(NULL_SAFE_STRING_SORT); - assertThat(actual).isEqualTo(expected); + assertThat(this.map.values()).containsExactlyInAnyOrder("123", null, "789"); + } + + @Test + void valuesCollectionAdd() { + assertThatThrownBy(() -> + this.map.values().add("12345") + ).isInstanceOf(UnsupportedOperationException.class); + } + + @Test + void valuesCollectionClear() { + Collection values = this.map.values(); + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(values).isNotEmpty(); + values.clear(); + assertThat(values).isEmpty(); + assertThat(this.map).isEmpty(); + } + + @Test + void valuesCollectionRemoval() { + Collection values = this.map.values(); + assertThat(values).isEmpty(); + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + assertThat(values).containsExactlyInAnyOrder("123", null, "789"); + values.remove(null); + assertThat(values).containsExactlyInAnyOrder("123", "789"); + assertThat(map).containsOnly(new AbstractMap.SimpleEntry<>(123, "123"), new AbstractMap.SimpleEntry<>(null, "789")); + values.remove("123"); + values.remove("789"); + assertThat(values).isEmpty(); + assertThat(map).isEmpty(); + } + + @Test + void valuesCollectionIterator() { + Iterator iterator = this.map.values().iterator(); + assertThat(iterator).isExhausted(); + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + iterator = this.map.values().iterator(); + assertThat(iterator).toIterable().containsExactlyInAnyOrder("123", null, "789"); + } + + @Test + void valuesCollectionIteratorRemoval() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Iterator iterator = this.map.values().iterator(); + while (iterator.hasNext()) { + String value = iterator.next(); + if (value != null && value.equals("789")) { + iterator.remove(); + } + } + assertThat(iterator).isExhausted(); + assertThat(this.map.values()).containsExactlyInAnyOrder("123", null); + assertThat(this.map).containsOnlyKeys(123, 456); + } + + @Test + void valuesCollectionStream() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + List values = this.map.values().stream().collect(Collectors.toList()); + assertThat(values).containsExactlyInAnyOrder("123", null, "789"); + } + + @Test + void valuesCollectionSpliteratorCharacteristics() { + this.map.put(123, "123"); + this.map.put(456, null); + this.map.put(null, "789"); + Spliterator spliterator = this.map.values().spliterator(); + assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT); + assertThat(spliterator.estimateSize()).isEqualTo(3L); + assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L); } @Test @@ -541,6 +698,17 @@ void containsViaEntrySet() { copy.forEach(entry -> assertThat(entrySet).doesNotContain(entry)); } + @Test + void entrySetSpliteratorCharacteristics() { + this.map.put(1, "1"); + this.map.put(2, "2"); + this.map.put(3, "3"); + Spliterator> spliterator = this.map.entrySet().spliterator(); + assertThat(spliterator).hasOnlyCharacteristics(Spliterator.CONCURRENT, Spliterator.DISTINCT); + assertThat(spliterator.estimateSize()).isEqualTo(3L); + assertThat(spliterator.getExactSizeIfKnown()).isEqualTo(-1L); + } + @Test void supportNullReference() { // GC could happen during restructure so we must be able to create a reference for a null entry