diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java index 7b3fab7ce4..313aeb27e4 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/BoundedLocalCache.java @@ -40,6 +40,7 @@ import java.util.OptionalLong; import java.util.Queue; import java.util.Set; +import java.util.Spliterator; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionException; import java.util.concurrent.ConcurrentHashMap; @@ -2122,11 +2123,6 @@ public void clear() { cache.clear(); } - @Override - public Iterator iterator() { - return new KeyIterator<>(cache); - } - @Override public boolean contains(Object obj) { return cache.containsKey(obj); @@ -2137,6 +2133,16 @@ public boolean remove(Object obj) { return (cache.remove(obj) != null); } + @Override + public Iterator iterator() { + return new KeyIterator<>(cache); + } + + @Override + public Spliterator spliterator() { + return new KeySpliterator<>(cache); + } + @Override public Object[] toArray() { if (cache.collectKeys()) { @@ -2193,6 +2199,75 @@ public void remove() { } } + /** An adapter to safely externalize the key spliterator. */ + static final class KeySpliterator implements Spliterator { + final Spliterator> spliterator; + final BoundedLocalCache cache; + + KeySpliterator(BoundedLocalCache cache) { + this(cache, cache.data.values().spliterator()); + } + + KeySpliterator(BoundedLocalCache cache, Spliterator> spliterator) { + this.spliterator = requireNonNull(spliterator); + this.cache = requireNonNull(cache); + } + + @Override + public void forEachRemaining(Consumer action) { + requireNonNull(action); + long now = cache.expirationTicker().read(); + Consumer> consumer = node -> { + K key = node.getKey(); + V value = node.getValue(); + if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) { + action.accept(key); + } + }; + spliterator.forEachRemaining(consumer); + } + + @Override + public boolean tryAdvance(Consumer action) { + requireNonNull(action); + boolean[] advanced = { false }; + long now = cache.expirationTicker().read(); + Consumer> consumer = node -> { + K key = node.getKey(); + V value = node.getValue(); + if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) { + action.accept(key); + advanced[0] = true; + } + }; + for (;;) { + if (spliterator.tryAdvance(consumer)) { + if (advanced[0]) { + return true; + } + continue; + } + return false; + } + } + + @Override + public Spliterator trySplit() { + Spliterator> split = spliterator.trySplit(); + return (split == null) ? null : new KeySpliterator<>(cache, split); + } + + @Override + public long estimateSize() { + return spliterator.estimateSize(); + } + + @Override + public int characteristics() { + return Spliterator.DISTINCT | Spliterator.CONCURRENT | Spliterator.NONNULL; + } + } + /** An adapter to safely externalize the values. */ static final class ValuesView extends AbstractCollection { final BoundedLocalCache cache; @@ -2211,6 +2286,11 @@ public void clear() { cache.clear(); } + @Override + public boolean contains(Object o) { + return cache.containsValue(o); + } + @Override public boolean removeIf(Predicate filter) { requireNonNull(filter); @@ -2229,8 +2309,8 @@ public Iterator iterator() { } @Override - public boolean contains(Object o) { - return cache.containsValue(o); + public Spliterator spliterator() { + return new ValueSpliterator<>(cache); } } @@ -2258,6 +2338,75 @@ public void remove() { } } + /** An adapter to safely externalize the value spliterator. */ + static final class ValueSpliterator implements Spliterator { + final Spliterator> spliterator; + final BoundedLocalCache cache; + + ValueSpliterator(BoundedLocalCache cache) { + this(cache, cache.data.values().spliterator()); + } + + ValueSpliterator(BoundedLocalCache cache, Spliterator> spliterator) { + this.spliterator = requireNonNull(spliterator); + this.cache = requireNonNull(cache); + } + + @Override + public void forEachRemaining(Consumer action) { + requireNonNull(action); + long now = cache.expirationTicker().read(); + Consumer> consumer = node -> { + K key = node.getKey(); + V value = node.getValue(); + if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) { + action.accept(value); + } + }; + spliterator.forEachRemaining(consumer); + } + + @Override + public boolean tryAdvance(Consumer action) { + requireNonNull(action); + boolean[] advanced = { false }; + long now = cache.expirationTicker().read(); + Consumer> consumer = node -> { + K key = node.getKey(); + V value = node.getValue(); + if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) { + action.accept(value); + advanced[0] = true; + } + }; + for (;;) { + if (spliterator.tryAdvance(consumer)) { + if (advanced[0]) { + return true; + } + continue; + } + return false; + } + } + + @Override + public Spliterator trySplit() { + Spliterator> split = spliterator.trySplit(); + return (split == null) ? null : new ValueSpliterator<>(cache, split); + } + + @Override + public long estimateSize() { + return spliterator.estimateSize(); + } + + @Override + public int characteristics() { + return Spliterator.CONCURRENT | Spliterator.NONNULL; + } + } + /** An adapter to safely externalize the entries. */ static final class EntrySetView extends AbstractSet> { final BoundedLocalCache cache; @@ -2276,11 +2425,6 @@ public void clear() { cache.clear(); } - @Override - public Iterator> iterator() { - return new EntryIterator<>(cache); - } - @Override public boolean contains(Object obj) { if (!(obj instanceof Entry)) { @@ -2311,6 +2455,16 @@ public boolean removeIf(Predicate> filter) { } return removed; } + + @Override + public Iterator> iterator() { + return new EntryIterator<>(cache); + } + + @Override + public Spliterator> spliterator() { + return new EntrySpliterator<>(cache); + } } /** An adapter to safely externalize the entry iterator. */ @@ -2373,6 +2527,75 @@ public void remove() { } } + /** An adapter to safely externalize the entry spliterator. */ + static final class EntrySpliterator implements Spliterator> { + final Spliterator> spliterator; + final BoundedLocalCache cache; + + EntrySpliterator(BoundedLocalCache cache) { + this(cache, cache.data.values().spliterator()); + } + + EntrySpliterator(BoundedLocalCache cache, Spliterator> spliterator) { + this.spliterator = requireNonNull(spliterator); + this.cache = requireNonNull(cache); + } + + @Override + public void forEachRemaining(Consumer> action) { + requireNonNull(action); + long now = cache.expirationTicker().read(); + Consumer> consumer = node -> { + K key = node.getKey(); + V value = node.getValue(); + if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) { + action.accept(new WriteThroughEntry(cache, key, value)); + } + }; + spliterator.forEachRemaining(consumer); + } + + @Override + public boolean tryAdvance(Consumer> action) { + requireNonNull(action); + boolean[] advanced = { false }; + long now = cache.expirationTicker().read(); + Consumer> consumer = node -> { + K key = node.getKey(); + V value = node.getValue(); + if ((key != null) && (value != null) && !cache.hasExpired(node, now) && node.isAlive()) { + action.accept(new WriteThroughEntry(cache, key, value)); + advanced[0] = true; + } + }; + for (;;) { + if (spliterator.tryAdvance(consumer)) { + if (advanced[0]) { + return true; + } + continue; + } + return false; + } + } + + @Override + public Spliterator> trySplit() { + Spliterator> split = spliterator.trySplit(); + return (split == null) ? null : new EntrySpliterator<>(cache, split); + } + + @Override + public long estimateSize() { + return spliterator.estimateSize(); + } + + @Override + public int characteristics() { + return Spliterator.DISTINCT | Spliterator.CONCURRENT | Spliterator.NONNULL; + } + } + /** Creates a serialization proxy based on the common configuration shared by all cache types. */ static SerializationProxy makeSerializationProxy( BoundedLocalCache cache, boolean isWeighted) { diff --git a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java index 4b6d8fa644..8e17689261 100644 --- a/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java +++ b/caffeine/src/main/java/com/github/benmanes/caffeine/cache/UnboundedLocalCache.java @@ -36,6 +36,7 @@ import java.util.concurrent.Executor; import java.util.function.BiConsumer; import java.util.function.BiFunction; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; @@ -512,45 +513,45 @@ public Set> entrySet() { /** An adapter to safely externalize the keys. */ static final class KeySetView extends AbstractSet { - final UnboundedLocalCache local; + final UnboundedLocalCache cache; - KeySetView(UnboundedLocalCache local) { - this.local = requireNonNull(local); + KeySetView(UnboundedLocalCache cache) { + this.cache = requireNonNull(cache); } @Override public boolean isEmpty() { - return local.isEmpty(); + return cache.isEmpty(); } @Override public int size() { - return local.size(); + return cache.size(); } @Override public void clear() { - local.clear(); + cache.clear(); } @Override public boolean contains(Object o) { - return local.containsKey(o); + return cache.containsKey(o); } @Override public boolean remove(Object obj) { - return (local.remove(obj) != null); + return (cache.remove(obj) != null); } @Override public Iterator iterator() { - return new KeyIterator(local); + return new KeyIterator(cache); } @Override public Spliterator spliterator() { - return local.data.keySet().spliterator(); + return cache.data.keySet().spliterator(); } } @@ -726,7 +727,9 @@ public Iterator> iterator() { @Override public Spliterator> spliterator() { - return cache.data.entrySet().spliterator(); + return (cache.writer == CacheWriter.disabledWriter()) + ? cache.data.entrySet().spliterator() + : new EntrySpliterator<>(cache); } } @@ -760,6 +763,55 @@ public void remove() { } } + /** An adapter to safely externalize the entry spliterator. */ + static final class EntrySpliterator implements Spliterator> { + final Spliterator> spliterator; + final UnboundedLocalCache cache; + + EntrySpliterator(UnboundedLocalCache cache) { + this(cache, cache.data.entrySet().spliterator()); + } + + EntrySpliterator(UnboundedLocalCache cache, Spliterator> spliterator) { + this.spliterator = requireNonNull(spliterator); + this.cache = requireNonNull(cache); + } + + @Override + public void forEachRemaining(Consumer> action) { + requireNonNull(action); + spliterator.forEachRemaining(entry -> { + Entry e = new WriteThroughEntry<>(cache, entry.getKey(), entry.getValue()); + action.accept(e); + }); + } + + @Override + public boolean tryAdvance(Consumer> action) { + requireNonNull(action); + return spliterator.tryAdvance(entry -> { + Entry e = new WriteThroughEntry<>(cache, entry.getKey(), entry.getValue()); + action.accept(e); + }); + } + + @Override + public EntrySpliterator trySplit() { + Spliterator> split = spliterator.trySplit(); + return (split == null) ? null : new EntrySpliterator<>(cache, split); + } + + @Override + public long estimateSize() { + return spliterator.estimateSize(); + } + + @Override + public int characteristics() { + return spliterator.characteristics(); + } + } + /* ---------------- Manual Cache -------------- */ static class UnboundedLocalManualCache diff --git a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java index c0343fde64..c4dd9f73f8 100644 --- a/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java +++ b/caffeine/src/test/java/com/github/benmanes/caffeine/cache/AsMapTest.java @@ -30,6 +30,7 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.Matchers.not; @@ -46,6 +47,8 @@ import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; @@ -69,6 +72,7 @@ import com.github.benmanes.caffeine.cache.testing.CheckNoWriter; import com.github.benmanes.caffeine.cache.testing.RejectingCacheWriter.DeleteException; import com.github.benmanes.caffeine.cache.testing.RejectingCacheWriter.WriteException; +import com.google.common.base.MoreObjects; import com.google.common.collect.ImmutableMap; import com.google.common.testing.SerializableTester; @@ -1570,12 +1574,45 @@ public void keyIterator_writerFails(Map map, CacheContext cont @CacheSpec @CheckNoWriter @CheckNoStats @Test(dataProvider = "caches") - public void keySpliterator(Map map, CacheContext context) { + public void keySpliterator_forEachRemaining(Map map, CacheContext context) { int[] count = new int[1]; map.keySet().spliterator().forEachRemaining(key -> count[0]++); assertThat(count[0], is(map.size())); } + @CacheSpec + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + public void keySpliterator_tryAdvance(Map map, CacheContext context) { + Spliterator spliterator = map.keySet().spliterator(); + int[] count = new int[1]; + boolean advanced; + do { + advanced = spliterator.tryAdvance(key -> count[0]++); + } while (advanced); + assertThat(count[0], is(map.size())); + } + + // FIXME: ConcurrentHashMap bug for SINGLETON and PARTIAL resulting in two empty spliterators + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + @CacheSpec(population = {Population.EMPTY, Population.FULL}) + public void keySpliterator_trySplit(Map map, CacheContext context) { + Spliterator spliterator = map.keySet().spliterator(); + Spliterator other = MoreObjects.firstNonNull( + spliterator.trySplit(), Spliterators.emptySpliterator()); + int size = (int) (spliterator.estimateSize() + other.estimateSize()); + assertThat(size, is(map.size())); + } + + @CacheSpec(population = Population.SINGLETON) + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + public void keySpliterator_estimateSize(Map map, CacheContext context) { + Spliterator spliterator = map.keySet().spliterator(); + assertThat((int) spliterator.estimateSize(), is(map.size())); + } + /* ---------------- Values -------------- */ @CheckNoWriter @CheckNoStats @@ -1734,12 +1771,45 @@ public void valueIterator_writerFails(Map map, CacheContext co @CacheSpec @CheckNoWriter @CheckNoStats @Test(dataProvider = "caches") - public void valueSpliterator(Map map, CacheContext context) { + public void valueSpliterator_forEachRemaining(Map map, CacheContext context) { + int[] count = new int[1]; + map.values().spliterator().forEachRemaining(value -> count[0]++); + assertThat(count[0], is(map.size())); + } + + @CacheSpec + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + public void valueSpliterator_tryAdvance(Map map, CacheContext context) { + Spliterator spliterator = map.values().spliterator(); int[] count = new int[1]; - map.values().spliterator().forEachRemaining(key -> count[0]++); + boolean advanced; + do { + advanced = spliterator.tryAdvance(value -> count[0]++); + } while (advanced); assertThat(count[0], is(map.size())); } + // FIXME: ConcurrentHashMap bug for SINGLETON and PARTIAL resulting in two empty spliterators + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + @CacheSpec(population = {Population.EMPTY, Population.FULL}) + public void valueSpliterator_trySplit(Map map, CacheContext context) { + Spliterator spliterator = map.values().spliterator(); + Spliterator other = MoreObjects.firstNonNull( + spliterator.trySplit(), Spliterators.emptySpliterator()); + int size = (int) (spliterator.estimateSize() + other.estimateSize()); + assertThat(size, is(map.size())); + } + + @CacheSpec(population = Population.SINGLETON) + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + public void valueSpliterator_estimateSize(Map map, CacheContext context) { + Spliterator spliterator = map.values().spliterator(); + assertThat((int) spliterator.estimateSize(), is(map.size())); + } + /* ---------------- Entry Set -------------- */ @CheckNoWriter @CheckNoStats @@ -1899,16 +1969,59 @@ public void entryIterator_writerFails(Map map, CacheContext co } } + @CacheSpec + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + public void entrySetSpliterator_forEachRemaining( + Map map, CacheContext context) { + int[] count = new int[1]; + map.entrySet().spliterator().forEachRemaining(entry -> { + if (!context.isGuava()) { + assertThat(entry, is(instanceOf(WriteThroughEntry.class))); + } + count[0]++; + }); + assertThat(count[0], is(map.size())); + } @CacheSpec @CheckNoWriter @CheckNoStats @Test(dataProvider = "caches") - public void entrySetSpliterator(Map map, CacheContext context) { + public void entrySetSpliterator_tryAdvance(Map map, CacheContext context) { + Spliterator> spliterator = map.entrySet().spliterator(); int[] count = new int[1]; - map.entrySet().spliterator().forEachRemaining(key -> count[0]++); + boolean advanced; + do { + advanced = spliterator.tryAdvance(entry -> { + if (!context.isGuava()) { + assertThat(entry, is(instanceOf(WriteThroughEntry.class))); + } + count[0]++; + }); + } while (advanced); assertThat(count[0], is(map.size())); } + // FIXME: ConcurrentHashMap bug for SINGLETON and PARTIAL resulting in two empty spliterators + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + @CacheSpec(population = {Population.EMPTY, Population.FULL}) + public void entrySetSpliterator_trySplit(Map map, CacheContext context) { + Spliterator> spliterator = map.entrySet().spliterator(); + Spliterator> other = MoreObjects.firstNonNull( + spliterator.trySplit(), Spliterators.emptySpliterator()); + int size = (int) (spliterator.estimateSize() + other.estimateSize()); + assertThat(size, is(map.size())); + } + + @CacheSpec(population = Population.SINGLETON) + @CheckNoWriter @CheckNoStats + @Test(dataProvider = "caches") + public void entrySetSpliterator_estimateSize(Map map, CacheContext context) { + Spliterator> spliterator = map.entrySet().spliterator(); + assertThat((int) spliterator.estimateSize(), is(map.size())); + } + /* ---------------- WriteThroughEntry -------------- */ @CheckNoStats