1 /* 2 * Copyright (C) 2012 The Guava Authors 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.common.cache; 18 19 import static com.google.common.base.MoreObjects.firstNonNull; 20 import static com.google.common.base.Preconditions.checkNotNull; 21 import static com.google.common.base.Preconditions.checkState; 22 23 import com.google.common.base.Equivalence; 24 import com.google.common.base.Ticker; 25 import com.google.common.cache.AbstractCache.StatsCounter; 26 import com.google.common.collect.ImmutableMap; 27 import com.google.common.util.concurrent.ExecutionError; 28 import com.google.common.util.concurrent.UncheckedExecutionException; 29 import com.google.errorprone.annotations.CanIgnoreReturnValue; 30 import java.util.AbstractCollection; 31 import java.util.AbstractSet; 32 import java.util.Collection; 33 import java.util.HashMap; 34 import java.util.Iterator; 35 import java.util.LinkedHashMap; 36 import java.util.Map; 37 import java.util.Map.Entry; 38 import java.util.NoSuchElementException; 39 import java.util.Set; 40 import java.util.concurrent.Callable; 41 import java.util.concurrent.ConcurrentMap; 42 import java.util.concurrent.ExecutionException; 43 import org.checkerframework.checker.nullness.qual.Nullable; 44 45 /** 46 * LocalCache emulation for GWT. 47 * 48 * @param <K> the base key type 49 * @param <V> the base value type 50 * @author Charles Fry 51 * @author Jon Donovan 52 */ 53 public class LocalCache<K, V> implements ConcurrentMap<K, V> { 54 private static final int UNSET_INT = CacheBuilder.UNSET_INT; 55 56 private final LinkedHashMap<K, Timestamped<V>> cachingHashMap; 57 private final CacheLoader<? super K, V> loader; 58 private final RemovalListener removalListener; 59 private final StatsCounter statsCounter; 60 private final Ticker ticker; 61 private final long expireAfterWrite; 62 private final long expireAfterAccess; 63 LocalCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader)64 LocalCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { 65 this.loader = loader; 66 this.removalListener = builder.removalListener; 67 this.expireAfterAccess = builder.expireAfterAccessNanos; 68 this.expireAfterWrite = builder.expireAfterWriteNanos; 69 this.statsCounter = builder.getStatsCounterSupplier().get(); 70 71 /* Implements size-capped LinkedHashMap */ 72 final long maximumSize = builder.maximumSize; 73 this.cachingHashMap = 74 new CapacityEnforcingLinkedHashMap<K, V>( 75 builder.getInitialCapacity(), 76 0.75f, 77 (builder.maximumSize != UNSET_INT), 78 builder.maximumSize, 79 statsCounter, 80 removalListener); 81 82 this.ticker = firstNonNull(builder.ticker, Ticker.systemTicker()); 83 } 84 85 @Override size()86 public int size() { 87 return cachingHashMap.size(); 88 } 89 90 @Override isEmpty()91 public boolean isEmpty() { 92 return cachingHashMap.isEmpty(); 93 } 94 95 @Override get(Object key)96 public V get(Object key) { 97 checkNotNull(key); 98 Timestamped<V> value = cachingHashMap.get(key); 99 100 if (value == null) { 101 statsCounter.recordMisses(1); 102 return null; 103 } else if (!isExpired(value)) { 104 statsCounter.recordHits(1); 105 value.updateTimestamp(); 106 return value.getValue(); 107 } else { 108 statsCounter.recordEviction(); 109 statsCounter.recordMisses(1); 110 alertListenerIfPresent(key, value.getValue(), RemovalCause.EXPIRED); 111 cachingHashMap.remove(key); 112 return null; 113 } 114 } 115 116 @CanIgnoreReturnValue 117 @Override put(K key, V value)118 public V put(K key, V value) { 119 checkNotNull(key); 120 checkNotNull(value); 121 Timestamped<V> oldValue = cachingHashMap.put(key, new Timestamped<V>(value, ticker)); 122 if (oldValue == null) { 123 return null; 124 } 125 alertListenerIfPresent(key, oldValue.getValue(), RemovalCause.REPLACED); 126 return oldValue.getValue(); 127 } 128 129 @CanIgnoreReturnValue 130 @Override remove(Object key)131 public V remove(Object key) { 132 Timestamped<V> stamped = cachingHashMap.remove(key); 133 if (stamped != null) { 134 V value = stamped.getValue(); 135 136 if (!isExpired(stamped)) { 137 alertListenerIfPresent(key, value, RemovalCause.EXPLICIT); 138 return value; 139 } 140 141 alertListenerIfPresent(key, value, RemovalCause.EXPIRED); 142 } 143 return null; 144 } 145 146 @Override putAll(Map<? extends K, ? extends V> m)147 public void putAll(Map<? extends K, ? extends V> m) { 148 for (Entry<? extends K, ? extends V> entry : m.entrySet()) { 149 put(entry.getKey(), entry.getValue()); 150 } 151 } 152 153 @Override clear()154 public void clear() { 155 if (removalListener != null) { 156 for (Entry<K, Timestamped<V>> entry : cachingHashMap.entrySet()) { 157 alertListenerIfPresent(entry.getKey(), entry.getValue().getValue(), RemovalCause.EXPLICIT); 158 } 159 } 160 cachingHashMap.clear(); 161 } 162 163 @Override putIfAbsent(K key, V value)164 public V putIfAbsent(K key, V value) { 165 V currentValue = get(key); 166 if (currentValue != null) { 167 return currentValue; 168 } 169 return put(key, value); 170 } 171 172 @CanIgnoreReturnValue 173 @Override remove(Object key, Object value)174 public boolean remove(Object key, Object value) { 175 if (value.equals(get(key))) { 176 alertListenerIfPresent(key, value, RemovalCause.EXPLICIT); 177 remove(key); 178 return true; 179 } 180 return false; 181 } 182 183 @Override replace(K key, V oldValue, V newValue)184 public boolean replace(K key, V oldValue, V newValue) { 185 if (oldValue.equals(get(key))) { 186 alertListenerIfPresent(key, oldValue, RemovalCause.REPLACED); 187 put(key, newValue); 188 return true; 189 } 190 return false; 191 } 192 193 @Override replace(K key, V value)194 public V replace(K key, V value) { 195 V currentValue = get(key); 196 if (currentValue != null) { 197 alertListenerIfPresent(key, currentValue, RemovalCause.REPLACED); 198 return put(key, value); 199 } 200 return null; 201 } 202 203 @Override containsKey(Object key)204 public boolean containsKey(Object key) { 205 return cachingHashMap.containsKey(key) && !isExpired(cachingHashMap.get(key)); 206 } 207 208 @Override containsValue(Object value)209 public boolean containsValue(Object value) { 210 for (Timestamped<V> val : cachingHashMap.values()) { 211 if (val.getValue().equals(value)) { 212 if (!isExpired(val)) { 213 return true; 214 } 215 } 216 } 217 return false; 218 } 219 isExpired(Timestamped<V> stamped)220 private boolean isExpired(Timestamped<V> stamped) { 221 if ((expireAfterAccess == UNSET_INT) && (expireAfterWrite == UNSET_INT)) { 222 return false; 223 } 224 225 boolean expireWrite = (stamped.getWriteTimestamp() + expireAfterWrite <= currentTimeNanos()); 226 boolean expireAccess = (stamped.getAccessTimestamp() + expireAfterAccess <= currentTimeNanos()); 227 228 if (expireAfterAccess == UNSET_INT) { 229 return expireWrite; 230 } 231 if (expireAfterWrite == UNSET_INT) { 232 return expireAccess; 233 } 234 235 return expireWrite || expireAccess; 236 } 237 238 @SuppressWarnings("GoodTime") currentTimeNanos()239 private long currentTimeNanos() { 240 return ticker.read(); 241 } 242 alertListenerIfPresent(Object key, Object value, RemovalCause cause)243 private void alertListenerIfPresent(Object key, Object value, RemovalCause cause) { 244 if (removalListener != null) { 245 removalListener.onRemoval(RemovalNotification.create(key, value, cause)); 246 } 247 } 248 249 @SuppressWarnings("GoodTime") // timestamps as numeric primitives load(Object key)250 private V load(Object key) throws ExecutionException { 251 long startTime = ticker.read(); 252 V calculatedValue; 253 try { 254 /* 255 * This cast isn't safe, but we can rely on the fact that K is almost always passed to 256 * Map.get(), and tools like IDEs and Findbugs can catch situations where this isn't the 257 * case. 258 * 259 * The alternative is to add an overloaded method, but the chances of a user calling get() 260 * instead of the new API and the risks inherent in adding a new API outweigh this little 261 * hole. 262 */ 263 K castKey = (K) key; 264 calculatedValue = loader.load(castKey); 265 put(castKey, calculatedValue); 266 } catch (RuntimeException e) { 267 statsCounter.recordLoadException(ticker.read() - startTime); 268 throw new UncheckedExecutionException(e); 269 } catch (Exception e) { 270 statsCounter.recordLoadException(ticker.read() - startTime); 271 throw new ExecutionException(e); 272 } catch (Error e) { 273 statsCounter.recordLoadException(ticker.read() - startTime); 274 throw new ExecutionError(e); 275 } 276 277 if (calculatedValue == null) { 278 String message = loader + " returned null for key " + key + "."; 279 throw new CacheLoader.InvalidCacheLoadException(message); 280 } 281 statsCounter.recordLoadSuccess(ticker.read() - startTime); 282 return calculatedValue; 283 } 284 getIfPresent(Object key)285 private V getIfPresent(Object key) { 286 checkNotNull(key); 287 Timestamped<V> value = cachingHashMap.get(key); 288 289 if (value == null) { 290 return null; 291 } else if (!isExpired(value)) { 292 value.updateTimestamp(); 293 return value.getValue(); 294 } else { 295 alertListenerIfPresent(key, value.getValue(), RemovalCause.EXPIRED); 296 cachingHashMap.remove(key); 297 return null; 298 } 299 } 300 getOrLoad(K key)301 private V getOrLoad(K key) throws ExecutionException { 302 V value = get(key); 303 if (value != null) { 304 return value; 305 } 306 return load(key); 307 } 308 309 @SuppressWarnings("GoodTime") // timestamps as numeric primitives 310 private static class Timestamped<V> { 311 private final V value; 312 private final Ticker ticker; 313 private long writeTimestamp; 314 private long accessTimestamp; 315 Timestamped(V value, Ticker ticker)316 public Timestamped(V value, Ticker ticker) { 317 this.value = checkNotNull(value); 318 this.ticker = checkNotNull(ticker); 319 this.writeTimestamp = ticker.read(); 320 this.accessTimestamp = this.writeTimestamp; 321 } 322 getValue()323 public V getValue() { 324 return value; 325 } 326 updateTimestamp()327 public void updateTimestamp() { 328 accessTimestamp = ticker.read(); 329 } 330 getAccessTimestamp()331 public long getAccessTimestamp() { 332 return accessTimestamp; 333 } 334 getWriteTimestamp()335 public long getWriteTimestamp() { 336 return writeTimestamp; 337 } 338 equals(Object o)339 public boolean equals(Object o) { 340 return value.equals(o); 341 } 342 hashCode()343 public int hashCode() { 344 return value.hashCode(); 345 } 346 } 347 348 /** 349 * LocalManualCache is a wrapper around LocalCache for a cache without loading. 350 * 351 * @param <K> the base key type 352 * @param <V> the base value type 353 */ 354 public static class LocalManualCache<K, V> extends AbstractCache<K, V> { 355 final LocalCache<K, V> localCache; 356 LocalManualCache(CacheBuilder<? super K, ? super V> builder)357 LocalManualCache(CacheBuilder<? super K, ? super V> builder) { 358 this(builder, null); 359 } 360 LocalManualCache( CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader)361 protected LocalManualCache( 362 CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { 363 this.localCache = new LocalCache<K, V>(builder, loader); 364 } 365 366 // Cache methods 367 368 @Override get(K key, Callable<? extends V> valueLoader)369 public V get(K key, Callable<? extends V> valueLoader) throws ExecutionException { 370 V value = localCache.get(key); 371 if (value != null) { 372 return value; 373 } 374 375 try { 376 V newValue = valueLoader.call(); 377 localCache.put(key, newValue); 378 return newValue; 379 } catch (Exception e) { 380 throw new ExecutionException(e); 381 } 382 } 383 384 @Override getIfPresent(Object key)385 public @Nullable V getIfPresent(Object key) { 386 return localCache.getIfPresent(key); 387 } 388 389 @Override put(K key, V value)390 public void put(K key, V value) { 391 localCache.put(key, value); 392 } 393 394 @Override invalidate(Object key)395 public void invalidate(Object key) { 396 checkNotNull(key); 397 localCache.remove(key); 398 } 399 400 @Override invalidateAll()401 public void invalidateAll() { 402 localCache.clear(); 403 } 404 405 @Override size()406 public long size() { 407 return localCache.size(); 408 } 409 410 @Override asMap()411 public ConcurrentMap<K, V> asMap() { 412 return localCache; 413 } 414 } 415 416 /** 417 * LocalLoadingCache is a wrapper around LocalCache for a cache with loading. 418 * 419 * @param <K> the base key type 420 * @param <V> the base value type 421 */ 422 public static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> 423 implements LoadingCache<K, V> { 424 LocalLoadingCache( CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader)425 LocalLoadingCache( 426 CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { 427 super(builder, checkNotNull(loader)); 428 } 429 430 // Cache methods 431 432 @Override get(K key)433 public V get(K key) throws ExecutionException { 434 return localCache.getOrLoad(key); 435 } 436 437 @Override getUnchecked(K key)438 public V getUnchecked(K key) { 439 try { 440 return get(key); 441 } catch (ExecutionException e) { 442 throw new UncheckedExecutionException(e.getCause()); 443 } 444 } 445 446 @Override apply(K key)447 public final V apply(K key) { 448 return getUnchecked(key); 449 } 450 451 @Override getAll(Iterable<? extends K> keys)452 public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException { 453 Map<K, V> map = new HashMap<K, V>(); 454 for (K key : keys) { 455 map.put(key, localCache.getOrLoad(key)); 456 } 457 return ImmutableMap.copyOf(map); 458 } 459 460 @Override refresh(K key)461 public void refresh(K key) { 462 throw new UnsupportedOperationException(); 463 } 464 } 465 466 /** 467 * LinkedHashMap that enforces it's maximum size and logs events in a StatsCounter object and an 468 * optional RemovalListener. 469 * 470 * @param <K> the base key type 471 * @param <V> the base value type 472 */ 473 private class CapacityEnforcingLinkedHashMap<K, V> extends LinkedHashMap<K, Timestamped<V>> { 474 475 private final StatsCounter statsCounter; 476 private final RemovalListener removalListener; 477 private final long maximumSize; 478 CapacityEnforcingLinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder, long maximumSize, StatsCounter statsCounter, @Nullable RemovalListener removalListener)479 public CapacityEnforcingLinkedHashMap( 480 int initialCapacity, 481 float loadFactor, 482 boolean accessOrder, 483 long maximumSize, 484 StatsCounter statsCounter, 485 @Nullable RemovalListener removalListener) { 486 super(initialCapacity, loadFactor, accessOrder); 487 this.maximumSize = maximumSize; 488 this.statsCounter = statsCounter; 489 this.removalListener = removalListener; 490 } 491 492 @Override removeEldestEntry(Entry<K, Timestamped<V>> ignored)493 protected boolean removeEldestEntry(Entry<K, Timestamped<V>> ignored) { 494 boolean removal = (maximumSize == UNSET_INT) ? false : (size() > maximumSize); 495 if ((removalListener != null) && removal) { 496 removalListener.onRemoval( 497 RemovalNotification.create( 498 ignored.getKey(), ignored.getValue().getValue(), RemovalCause.SIZE)); 499 } 500 statsCounter.recordEviction(); 501 return removal; 502 } 503 } 504 505 /** 506 * Any updates to LocalCache.Strength used in CacheBuilder need to be matched in this class for 507 * compilation purposes. 508 */ 509 enum Strength { 510 /* 511 * TODO(kevinb): If we strongly reference the value and aren't loading, we needn't wrap the 512 * value. This could save ~8 bytes per entry. 513 */ 514 515 STRONG { 516 @Override defaultEquivalence()517 Equivalence<Object> defaultEquivalence() { 518 return Equivalence.equals(); 519 } 520 }, 521 522 SOFT { 523 @Override defaultEquivalence()524 Equivalence<Object> defaultEquivalence() { 525 return Equivalence.identity(); 526 } 527 }, 528 529 WEAK { 530 @Override defaultEquivalence()531 Equivalence<Object> defaultEquivalence() { 532 return Equivalence.identity(); 533 } 534 }; 535 defaultEquivalence()536 abstract Equivalence<Object> defaultEquivalence(); 537 } 538 539 /** 540 * Implementation for the EntryIterator, which is used to build Key and Value iterators. 541 * 542 * <p>Expiration is only checked on hasNext(), so as to ensure that a next() call never returns 543 * null when hasNext() has already been called. 544 */ 545 class EntryIterator implements Iterator<Entry<K, V>> { 546 Iterator<Entry<K, Timestamped<V>>> iterator; 547 Entry<K, Timestamped<V>> lastEntry; 548 Entry<K, Timestamped<V>> nextEntry; 549 EntryIterator()550 EntryIterator() { 551 this.iterator = LocalCache.this.cachingHashMap.entrySet().iterator(); 552 } 553 554 @Override next()555 public Entry<K, V> next() { 556 if (nextEntry == null) { 557 boolean unused = hasNext(); 558 559 if (nextEntry == null) { 560 throw new NoSuchElementException(); 561 } 562 } 563 564 lastEntry = nextEntry; 565 nextEntry = null; 566 return new WriteThroughEntry(lastEntry.getKey(), lastEntry.getValue().getValue()); 567 } 568 569 @Override hasNext()570 public boolean hasNext() { 571 if (nextEntry == null) { 572 while (iterator.hasNext()) { 573 Entry<K, Timestamped<V>> next = iterator.next(); 574 if (!isExpired(next.getValue())) { 575 nextEntry = next; 576 return true; 577 } 578 } 579 return false; 580 } 581 return true; 582 } 583 584 @Override remove()585 public void remove() { 586 checkState(lastEntry != null); 587 LocalCache.this.remove(lastEntry.getKey(), lastEntry.getValue()); 588 lastEntry = null; 589 } 590 } 591 592 /** KeyIterator build on top of EntryIterator. */ 593 final class KeyIterator implements Iterator<K> { 594 private EntryIterator iterator; 595 KeyIterator()596 KeyIterator() { 597 iterator = new EntryIterator(); 598 } 599 600 @Override hasNext()601 public boolean hasNext() { 602 return iterator.hasNext(); 603 } 604 605 @Override next()606 public K next() { 607 return iterator.next().getKey(); 608 } 609 610 @Override remove()611 public void remove() { 612 iterator.remove(); 613 } 614 } 615 616 /** ValueIterator build on top of EntryIterator. */ 617 final class ValueIterator implements Iterator<V> { 618 private EntryIterator iterator; 619 ValueIterator()620 ValueIterator() { 621 iterator = new EntryIterator(); 622 } 623 624 @Override hasNext()625 public boolean hasNext() { 626 return iterator.hasNext(); 627 } 628 629 @Override next()630 public V next() { 631 return iterator.next().getValue(); 632 } 633 634 @Override remove()635 public void remove() { 636 iterator.remove(); 637 } 638 } 639 640 Set<K> keySet; 641 642 @Override keySet()643 public Set<K> keySet() { 644 // does not impact recency ordering 645 Set<K> ks = keySet; 646 return (ks != null) ? ks : (keySet = new KeySet(this)); 647 } 648 649 Collection<V> values; 650 651 @Override values()652 public Collection<V> values() { 653 // does not impact recency ordering 654 Collection<V> vs = values; 655 return (vs != null) ? vs : (values = new Values(this)); 656 } 657 658 Set<Entry<K, V>> entrySet; 659 660 @Override entrySet()661 public Set<Entry<K, V>> entrySet() { 662 // does not impact recency ordering 663 Set<Entry<K, V>> es = entrySet; 664 return (es != null) ? es : (entrySet = new EntrySet(this)); 665 } 666 667 /** 668 * Custom Entry class used by EntryIterator.next(), that relays setValue changes to the underlying 669 * map. 670 */ 671 private final class WriteThroughEntry implements Entry<K, V> { 672 final K key; 673 V value; 674 WriteThroughEntry(K key, V value)675 WriteThroughEntry(K key, V value) { 676 this.key = checkNotNull(key); 677 this.value = checkNotNull(value); 678 } 679 680 @Override getKey()681 public K getKey() { 682 return key; 683 } 684 685 @Override getValue()686 public V getValue() { 687 return value; 688 } 689 690 @Override equals(@ullable Object object)691 public boolean equals(@Nullable Object object) { 692 // Cannot use key and value equivalence 693 if (object instanceof Entry) { 694 Entry<?, ?> that = (Entry<?, ?>) object; 695 return key.equals(that.getKey()) && value.equals(that.getValue()); 696 } 697 return false; 698 } 699 700 @Override hashCode()701 public int hashCode() { 702 // Cannot use key and value equivalence 703 return key.hashCode() ^ value.hashCode(); 704 } 705 706 @Override setValue(V newValue)707 public V setValue(V newValue) { 708 throw new UnsupportedOperationException(); 709 } 710 711 @Override toString()712 public String toString() { 713 return getKey() + "=" + getValue(); 714 } 715 } 716 717 // TODO(fry): Separate logic for consistency between emul and nonemul implementation. 718 // TODO(fry): Look into Maps.KeySet and Maps.Values, which can ideally be reused here but are 719 // currently only package visible. 720 abstract class AbstractCacheSet<T> extends AbstractSet<T> { 721 final ConcurrentMap<?, ?> map; 722 AbstractCacheSet(ConcurrentMap<?, ?> map)723 AbstractCacheSet(ConcurrentMap<?, ?> map) { 724 this.map = map; 725 } 726 727 @Override size()728 public int size() { 729 return map.size(); 730 } 731 732 @Override isEmpty()733 public boolean isEmpty() { 734 return map.isEmpty(); 735 } 736 737 @Override clear()738 public void clear() { 739 map.clear(); 740 } 741 } 742 743 private final class KeySet extends AbstractCacheSet<K> { 744 KeySet(ConcurrentMap<?, ?> map)745 KeySet(ConcurrentMap<?, ?> map) { 746 super(map); 747 } 748 749 @Override iterator()750 public Iterator<K> iterator() { 751 return new KeyIterator(); 752 } 753 754 @Override contains(Object o)755 public boolean contains(Object o) { 756 return map.containsKey(o); 757 } 758 759 @Override remove(Object o)760 public boolean remove(Object o) { 761 return map.remove(o) != null; 762 } 763 } 764 765 private final class Values extends AbstractCollection<V> { 766 final ConcurrentMap<?, ?> map; 767 Values(ConcurrentMap<?, ?> map)768 Values(ConcurrentMap<?, ?> map) { 769 this.map = map; 770 } 771 772 @Override iterator()773 public Iterator<V> iterator() { 774 return new ValueIterator(); 775 } 776 777 @Override contains(Object o)778 public boolean contains(Object o) { 779 return map.containsValue(o); 780 } 781 782 @Override size()783 public int size() { 784 return map.size(); 785 } 786 787 @Override isEmpty()788 public boolean isEmpty() { 789 return map.isEmpty(); 790 } 791 792 @Override clear()793 public void clear() { 794 map.clear(); 795 } 796 } 797 798 private final class EntrySet extends AbstractCacheSet<Entry<K, V>> { 799 EntrySet(ConcurrentMap<?, ?> map)800 EntrySet(ConcurrentMap<?, ?> map) { 801 super(map); 802 } 803 804 @Override iterator()805 public Iterator<Entry<K, V>> iterator() { 806 return new EntryIterator(); 807 } 808 809 @Override contains(Object o)810 public boolean contains(Object o) { 811 if (!(o instanceof Entry)) { 812 return false; 813 } 814 Entry<?, ?> e = (Entry<?, ?>) o; 815 Object key = e.getKey(); 816 if (key == null) { 817 return false; 818 } 819 V v = LocalCache.this.get(key); 820 821 return (v != null) && e.getValue().equals(v); 822 } 823 824 @Override remove(Object o)825 public boolean remove(Object o) { 826 if (!(o instanceof Entry)) { 827 return false; 828 } 829 Entry<?, ?> e = (Entry<?, ?>) o; 830 Object key = e.getKey(); 831 return (key != null) && LocalCache.this.remove(key, e.getValue()); 832 } 833 } 834 } 835