View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.collections4.map;
18  
19  import java.io.IOException;
20  import java.io.ObjectInputStream;
21  import java.io.ObjectOutputStream;
22  import java.io.Serializable;
23  import java.util.Collection;
24  import java.util.HashMap;
25  import java.util.Iterator;
26  import java.util.Map;
27  import java.util.Objects;
28  import java.util.Set;
29  import java.util.concurrent.TimeUnit;
30  import java.util.function.Predicate;
31  
32  import org.apache.commons.collections4.collection.AbstractCollectionDecorator;
33  import org.apache.commons.collections4.iterators.AbstractIteratorDecorator;
34  import org.apache.commons.collections4.set.AbstractSetDecorator;
35  
36  /**
37   * Decorates a {@code Map} to evict expired entries once their expiration
38   * time has been reached.
39   * <p>
40   * When putting a key-value pair in the map this decorator uses a
41   * {@link ExpirationPolicy} to determine how long the entry should remain alive
42   * as defined by an expiration time value.
43   * </p>
44   * <p>
45   * When accessing the mapped value for a key, its expiration time is checked,
46   * and if it is a negative value or if it is greater than the current time, the
47   * mapped value is returned. Otherwise, the key is removed from the decorated
48   * map, and {@code null} is returned.
49   * </p>
50   * <p>
51   * When invoking methods that involve accessing the entire map contents (i.e
52   * {@link #containsValue(Object)}, {@link #entrySet()}, etc.) this decorator
53   * removes all expired entries prior to actually completing the invocation.
54   * </p>
55   * <p>
56   * <strong>Note that {@link PassiveExpiringMap} is not synchronized and is not
57   * thread-safe.</strong> If you wish to use this map from multiple threads
58   * concurrently, you must use appropriate synchronization. The simplest approach
59   * is to wrap this map using {@link java.util.Collections#synchronizedMap(Map)}.
60   * This class may throw exceptions when accessed by concurrent threads without
61   * synchronization.
62   * </p>
63   *
64   * @param <K> The type of the keys in this map
65   * @param <V> The type of the values in this map
66   * @since 4.0
67   */
68  public class PassiveExpiringMap<K, V>
69      extends AbstractMapDecorator<K, V>
70      implements Serializable {
71  
72      /**
73       * A {@link ExpirationPolicy ExpirationPolicy}
74       * that returns an expiration time that is a
75       * constant about of time in the future from the current time.
76       *
77       * @param <K> The type of the keys in the map
78       * @param <V> The type of the values in the map
79       * @since 4.0
80       */
81      public static class ConstantTimeToLiveExpirationPolicy<K, V>
82          implements ExpirationPolicy<K, V> {
83  
84          /** Serialization version */
85          private static final long serialVersionUID = 1L;
86  
87          /** The constant time-to-live value measured in milliseconds. */
88          private final long timeToLiveMillis;
89  
90          /**
91           * Default constructor. Constructs a policy using a negative
92           * time-to-live value that results in entries never expiring.
93           */
94          public ConstantTimeToLiveExpirationPolicy() {
95              this(-1L);
96          }
97  
98          /**
99           * Constructs a policy with the given time-to-live constant measured in
100          * milliseconds. A negative time-to-live value indicates entries never
101          * expire. A zero time-to-live value indicates entries expire (nearly)
102          * immediately.
103          *
104          * @param timeToLiveMillis The constant amount of time (in milliseconds)
105          *        an entry is available before it expires. A negative value
106          *        results in entries that NEVER expire. A zero value results in
107          *        entries that ALWAYS expire.
108          */
109         public ConstantTimeToLiveExpirationPolicy(final long timeToLiveMillis) {
110             this.timeToLiveMillis = timeToLiveMillis;
111         }
112 
113         /**
114          * Constructs a policy with the given time-to-live constant measured in
115          * the given time unit of measure.
116          *
117          * @param timeToLive The constant amount of time an entry is available
118          *        before it expires. A negative value results in entries that
119          *        NEVER expire. A zero value results in entries that ALWAYS
120          *        expire.
121          * @param timeUnit The unit of time for the {@code timeToLive}
122          *        parameter, must not be null.
123          * @throws NullPointerException if the time unit is null.
124          */
125         public ConstantTimeToLiveExpirationPolicy(final long timeToLive,
126                                                   final TimeUnit timeUnit) {
127             this(validateAndConvertToMillis(timeToLive, timeUnit));
128         }
129 
130         /**
131          * Determine the expiration time for the given key-value entry.
132          *
133          * @param key The key for the entry (ignored).
134          * @param value The value for the entry (ignored).
135          * @return if {@link #timeToLiveMillis} &ge; 0, an expiration time of
136          *         {@link #timeToLiveMillis} +
137          *         {@link System#currentTimeMillis()} is returned. Otherwise, -1
138          *         is returned indicating the entry never expires.
139          */
140         @Override
141         public long expirationTime(final K key, final V value) {
142             if (timeToLiveMillis >= 0L) {
143                 // avoid numerical overflow
144                 final long nowMillis = System.currentTimeMillis();
145                 if (nowMillis > Long.MAX_VALUE - timeToLiveMillis) {
146                     // expiration would be greater than Long.MAX_VALUE
147                     // never expire
148                     return -1;
149                 }
150 
151                 // timeToLiveMillis in the future
152                 return nowMillis + timeToLiveMillis;
153             }
154 
155             // never expire
156             return -1L;
157         }
158     }
159 
160     private final class EntrySet extends AbstractSetDecorator<Entry<K, V>> {
161 
162         /** Generated serial version ID. */
163         private static final long serialVersionUID = 1L;
164 
165         private EntrySet(final Set<Entry<K, V>> set) {
166             super(set);
167         }
168 
169         @Override
170         public void clear() {
171             PassiveExpiringMap.this.clear();
172         }
173 
174         @Override
175         public boolean contains(final Object object) {
176             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
177             return super.contains(object);
178         }
179 
180         @Override
181         public boolean containsAll(final Collection<?> coll) {
182             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
183             return super.containsAll(coll);
184         }
185 
186         @Override
187         public boolean isEmpty() {
188             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
189             return super.isEmpty();
190         }
191 
192         @Override
193         public Iterator<Entry<K, V>> iterator() {
194             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
195             return new EntrySetIterator(super.iterator());
196         }
197 
198         @Override
199         public boolean remove(final Object object) {
200             if (object instanceof Map.Entry) {
201                 final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
202                 final Object key = entry.getKey();
203                 if (PassiveExpiringMap.this.containsKey(key)) {
204                     final Object value = PassiveExpiringMap.this.get(key);
205                     if (Objects.equals(value, entry.getValue())) {
206                         PassiveExpiringMap.this.remove(key);
207                         return true;
208                     }
209                 }
210             }
211             return false;
212         }
213 
214         @Override
215         public boolean removeAll(final Collection<?> coll) {
216             Objects.requireNonNull(coll, "coll");
217             boolean changed = false;
218             if (size() > coll.size()) {
219                 for (final Object obj : coll) {
220                     changed |= remove(obj);
221                 }
222             } else {
223                 final Iterator<?> it = iterator();
224                 while (it.hasNext()) {
225                     if (coll.contains(it.next())) {
226                         it.remove();
227                         changed = true;
228                     }
229                 }
230             }
231             return changed;
232         }
233 
234         @Override
235         public boolean removeIf(final Predicate<? super Entry<K, V>> filter) {
236             Objects.requireNonNull(filter, "filter");
237             boolean changed = false;
238             final Iterator<Entry<K, V>> it = iterator();
239             while (it.hasNext()) {
240                 if (filter.test(it.next())) {
241                     it.remove();
242                     changed = true;
243                 }
244             }
245             return changed;
246         }
247 
248         @Override
249         public boolean retainAll(final Collection<?> coll) {
250             Objects.requireNonNull(coll, "coll");
251             boolean changed = false;
252             final Iterator<?> it = iterator();
253             while (it.hasNext()) {
254                 if (!coll.contains(it.next())) {
255                     it.remove();
256                     changed = true;
257                 }
258             }
259             return changed;
260         }
261 
262         @Override
263         public int size() {
264             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
265             return super.size();
266         }
267 
268         @Override
269         public Object[] toArray() {
270             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
271             return super.toArray();
272         }
273 
274         @Override
275         public <T> T[] toArray(final T[] array) {
276             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
277             return super.toArray(array);
278         }
279     }
280 
281     private final class EntrySetIterator extends AbstractIteratorDecorator<Entry<K, V>> {
282         private Entry<K, V> lastReturned;
283 
284         private EntrySetIterator(final Iterator<Entry<K, V>> iterator) {
285             super(iterator);
286         }
287 
288         @Override
289         public Entry<K, V> next() {
290             lastReturned = super.next();
291             return lastReturned;
292         }
293 
294         @Override
295         public void remove() {
296             super.remove();
297             if (lastReturned != null) {
298                 PassiveExpiringMap.this.expirationMap.remove(lastReturned.getKey());
299                 lastReturned = null;
300             }
301         }
302     }
303 
304     /**
305      * A policy to determine the expiration time for key-value entries.
306      *
307      * @param <K> The key object type.
308      * @param <V> The value object type
309      * @since 4.0
310      */
311     @FunctionalInterface
312     public interface ExpirationPolicy<K, V>
313         extends Serializable {
314 
315         /**
316          * Determine the expiration time for the given key-value entry.
317          *
318          * @param key The key for the entry.
319          * @param value The value for the entry.
320          * @return The expiration time value measured in milliseconds. A
321          *         negative return value indicates the entry never expires.
322          */
323         long expirationTime(K key, V value);
324     }
325 
326     private final class KeySet extends AbstractSetDecorator<K> {
327 
328         /** Generated serial version ID. */
329         private static final long serialVersionUID = 1L;
330 
331         private KeySet(final Set<K> set) {
332             super(set);
333         }
334 
335         @Override
336         public void clear() {
337             PassiveExpiringMap.this.clear();
338         }
339 
340         @Override
341         public boolean contains(final Object key) {
342             PassiveExpiringMap.this.removeIfExpired(key, PassiveExpiringMap.this.now());
343             return super.contains(key);
344         }
345 
346         @Override
347         public boolean containsAll(final Collection<?> coll) {
348             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
349             return super.containsAll(coll);
350         }
351 
352         @Override
353         public boolean isEmpty() {
354             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
355             return super.isEmpty();
356         }
357 
358         @Override
359         public Iterator<K> iterator() {
360             return new KeySetIterator(PassiveExpiringMap.this.entrySet().iterator());
361         }
362 
363         @Override
364         public boolean remove(final Object key) {
365             final boolean hasKey = contains(key);
366             if (hasKey) {
367                 PassiveExpiringMap.this.remove(key);
368             }
369             return hasKey;
370         }
371 
372         @Override
373         public boolean removeAll(final Collection<?> coll) {
374             Objects.requireNonNull(coll, "coll");
375             boolean changed = false;
376             if (size() > coll.size()) {
377                 for (final Object obj : coll) {
378                     changed |= remove(obj);
379                 }
380             } else {
381                 final Iterator<?> it = iterator();
382                 while (it.hasNext()) {
383                     if (coll.contains(it.next())) {
384                         it.remove();
385                         changed = true;
386                     }
387                 }
388             }
389             return changed;
390         }
391 
392         @Override
393         public boolean removeIf(final Predicate<? super K> filter) {
394             Objects.requireNonNull(filter, "filter");
395             boolean changed = false;
396             final Iterator<K> it = iterator();
397             while (it.hasNext()) {
398                 if (filter.test(it.next())) {
399                     it.remove();
400                     changed = true;
401                 }
402             }
403             return changed;
404         }
405 
406         @Override
407         public boolean retainAll(final Collection<?> coll) {
408             Objects.requireNonNull(coll, "coll");
409             boolean changed = false;
410             final Iterator<?> it = iterator();
411             while (it.hasNext()) {
412                 if (!coll.contains(it.next())) {
413                     it.remove();
414                     changed = true;
415                 }
416             }
417             return changed;
418         }
419 
420         @Override
421         public int size() {
422             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
423             return super.size();
424         }
425 
426         @Override
427         public Object[] toArray() {
428             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
429             return super.toArray();
430         }
431 
432         @Override
433         public <T> T[] toArray(final T[] array) {
434             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
435             return super.toArray(array);
436         }
437     }
438 
439     private final class KeySetIterator implements Iterator<K> {
440         private final Iterator<Entry<K, V>> iterator;
441 
442         private KeySetIterator(final Iterator<Entry<K, V>> iterator) {
443             this.iterator = iterator;
444         }
445 
446         @Override
447         public boolean hasNext() {
448             return iterator.hasNext();
449         }
450 
451         @Override
452         public K next() {
453             return iterator.next().getKey();
454         }
455 
456         @Override
457         public void remove() {
458             iterator.remove();
459         }
460     }
461 
462     private final class ValuesCollection extends AbstractCollectionDecorator<V> {
463 
464         /** Generated serial version ID. */
465         private static final long serialVersionUID = 1L;
466 
467         private ValuesCollection(final Collection<V> coll) {
468             super(coll);
469         }
470 
471         @Override
472         public void clear() {
473             PassiveExpiringMap.this.clear();
474         }
475 
476         @Override
477         public boolean contains(final Object value) {
478             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
479             return super.contains(value);
480         }
481 
482         @Override
483         public boolean containsAll(final Collection<?> coll) {
484             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
485             return super.containsAll(coll);
486         }
487 
488         @Override
489         public boolean isEmpty() {
490             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
491             return super.isEmpty();
492         }
493 
494         @Override
495         public Iterator<V> iterator() {
496             return new ValuesIterator(PassiveExpiringMap.this.entrySet().iterator());
497         }
498 
499         @Override
500         public boolean remove(final Object value) {
501             final Iterator<V> it = iterator();
502             while (it.hasNext()) {
503                 if (Objects.equals(it.next(), value)) {
504                     it.remove();
505                     return true;
506                 }
507             }
508             return false;
509         }
510 
511         @Override
512         public boolean removeAll(final Collection<?> coll) {
513             Objects.requireNonNull(coll, "coll");
514             boolean changed = false;
515             final Iterator<?> it = iterator();
516             while (it.hasNext()) {
517                 if (coll.contains(it.next())) {
518                     it.remove();
519                     changed = true;
520                 }
521             }
522             return changed;
523         }
524 
525         @Override
526         public boolean removeIf(final Predicate<? super V> filter) {
527             Objects.requireNonNull(filter, "filter");
528             boolean changed = false;
529             final Iterator<V> it = iterator();
530             while (it.hasNext()) {
531                 if (filter.test(it.next())) {
532                     it.remove();
533                     changed = true;
534                 }
535             }
536             return changed;
537         }
538 
539         @Override
540         public boolean retainAll(final Collection<?> coll) {
541             Objects.requireNonNull(coll, "coll");
542             boolean changed = false;
543             final Iterator<?> it = iterator();
544             while (it.hasNext()) {
545                 if (!coll.contains(it.next())) {
546                     it.remove();
547                     changed = true;
548                 }
549             }
550             return changed;
551         }
552 
553         @Override
554         public int size() {
555             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
556             return super.size();
557         }
558 
559         @Override
560         public Object[] toArray() {
561             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
562             return super.toArray();
563         }
564 
565         @Override
566         public <T> T[] toArray(final T[] array) {
567             PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
568             return super.toArray(array);
569         }
570     }
571 
572     private final class ValuesIterator implements Iterator<V> {
573         private final Iterator<Entry<K, V>> iterator;
574 
575         private ValuesIterator(final Iterator<Entry<K, V>> iterator) {
576             this.iterator = iterator;
577         }
578 
579         @Override
580         public boolean hasNext() {
581             return iterator.hasNext();
582         }
583 
584         @Override
585         public V next() {
586             return iterator.next().getValue();
587         }
588 
589         @Override
590         public void remove() {
591             iterator.remove();
592         }
593     }
594 
595     /** Serialization version */
596     private static final long serialVersionUID = 1L;
597 
598     /**
599      * First validate the input parameters. If the parameters are valid, convert
600      * the given time measured in the given units to the same time measured in
601      * milliseconds.
602      *
603      * @param timeToLive The constant amount of time an entry is available
604      *        before it expires. A negative value results in entries that NEVER
605      *        expire. A zero value results in entries that ALWAYS expire.
606      * @param timeUnit The unit of time for the {@code timeToLive}
607      *        parameter, must not be null.
608      * @throws NullPointerException if the time unit is null.
609      */
610     private static long validateAndConvertToMillis(final long timeToLive,
611                                                    final TimeUnit timeUnit) {
612         Objects.requireNonNull(timeUnit, "timeUnit");
613         return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
614     }
615 
616     /** Map used to manage expiration times for the actual map entries. */
617     private final Map<Object, Long> expirationMap = new HashMap<>();
618 
619     /** The policy used to determine time-to-live values for map entries. */
620     private final ExpirationPolicy<K, V> expiringPolicy;
621 
622     /**
623      * Default constructor. Constructs a map decorator that results in entries
624      * NEVER expiring.
625      */
626     public PassiveExpiringMap() {
627         this(-1L);
628     }
629 
630     /**
631      * Constructs a map decorator using the given expiration policy to determine
632      * expiration times.
633      *
634      * @param expiringPolicy The policy used to determine expiration times of
635      *        entries as they are added.
636      * @throws NullPointerException if expiringPolicy is null
637      */
638     public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
639         this(expiringPolicy, new HashMap<>());
640     }
641 
642     /**
643      * Constructs a map decorator that decorates the given map and uses the given
644      * expiration policy to determine expiration times. If there are any
645      * elements already in the map being decorated, they will NEVER expire
646      * unless they are replaced.
647      *
648      * @param expiringPolicy The policy used to determine expiration times of
649      *        entries as they are added.
650      * @param map The map to decorate, must not be null.
651      * @throws NullPointerException if the map or expiringPolicy is null.
652      */
653     public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
654                               final Map<K, V> map) {
655         super(map);
656         this.expiringPolicy = Objects.requireNonNull(expiringPolicy, "expiringPolicy");
657     }
658 
659     /**
660      * Constructs a map decorator that decorates the given map using the given
661      * time-to-live value measured in milliseconds to create and use a
662      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
663      *
664      * @param timeToLiveMillis The constant amount of time (in milliseconds) an
665      *        entry is available before it expires. A negative value results in
666      *        entries that NEVER expire. A zero value results in entries that
667      *        ALWAYS expire.
668      */
669     public PassiveExpiringMap(final long timeToLiveMillis) {
670         this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
671                 new HashMap<>());
672     }
673 
674     /**
675      * Constructs a map decorator using the given time-to-live value measured in
676      * milliseconds to create and use a
677      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
678      * are any elements already in the map being decorated, they will NEVER
679      * expire unless they are replaced.
680      *
681      * @param timeToLiveMillis The constant amount of time (in milliseconds) an
682      *        entry is available before it expires. A negative value results in
683      *        entries that NEVER expire. A zero value results in entries that
684      *        ALWAYS expire.
685      * @param map The map to decorate, must not be null.
686      * @throws NullPointerException if the map is null.
687      */
688     public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V> map) {
689         this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
690              map);
691     }
692 
693     /**
694      * Constructs a map decorator using the given time-to-live value measured in
695      * the given time units of measure to create and use a
696      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
697      *
698      * @param timeToLive The constant amount of time an entry is available
699      *        before it expires. A negative value results in entries that NEVER
700      *        expire. A zero value results in entries that ALWAYS expire.
701      * @param timeUnit The unit of time for the {@code timeToLive}
702      *        parameter, must not be null.
703      * @throws NullPointerException if the time unit is null.
704      */
705     public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
706         this(validateAndConvertToMillis(timeToLive, timeUnit));
707     }
708 
709     /**
710      * Constructs a map decorator that decorates the given map using the given
711      * time-to-live value measured in the given time units of measure to create
712      * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This policy
713      * is used to determine expiration times. If there are any elements already
714      * in the map being decorated, they will NEVER expire unless they are
715      * replaced.
716      *
717      * @param timeToLive The constant amount of time an entry is available
718      *        before it expires. A negative value results in entries that NEVER
719      *        expire. A zero value results in entries that ALWAYS expire.
720      * @param timeUnit The unit of time for the {@code timeToLive}
721      *        parameter, must not be null.
722      * @param map The map to decorate, must not be null.
723      * @throws NullPointerException if the map or time unit is null.
724      */
725     public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit, final Map<K, V> map) {
726         this(validateAndConvertToMillis(timeToLive, timeUnit), map);
727     }
728 
729     /**
730      * Constructs a map decorator that decorates the given map and results in
731      * entries NEVER expiring. If there are any elements already in the map
732      * being decorated, they also will NEVER expire.
733      *
734      * @param map The map to decorate, must not be null.
735      * @throws NullPointerException if the map is null.
736      */
737     public PassiveExpiringMap(final Map<K, V> map) {
738         this(-1L, map);
739     }
740 
741     /**
742      * Normal {@link Map#clear()} behavior with the addition of clearing all
743      * expiration entries as well.
744      */
745     @Override
746     public void clear() {
747         super.clear();
748         expirationMap.clear();
749     }
750 
751     /**
752      * All expired entries are removed from the map prior to determining the
753      * contains result.
754      * {@inheritDoc}
755      */
756     @Override
757     public boolean containsKey(final Object key) {
758         removeIfExpired(key, now());
759         return super.containsKey(key);
760     }
761 
762     /**
763      * All expired entries are removed from the map prior to determining the
764      * contains result.
765      * {@inheritDoc}
766      */
767     @Override
768     public boolean containsValue(final Object value) {
769         removeAllExpired(now());
770         return super.containsValue(value);
771     }
772 
773     /**
774      * All expired entries are removed from the map prior to returning the entry set.
775      * {@inheritDoc}
776      */
777     @Override
778     public Set<Entry<K, V>> entrySet() {
779         removeAllExpired(now());
780         return new EntrySet(super.entrySet());
781     }
782 
783     /**
784      * All expired entries are removed from the map prior to returning the entry value.
785      * {@inheritDoc}
786      */
787     @Override
788     public V get(final Object key) {
789         removeIfExpired(key, now());
790         return super.get(key);
791     }
792 
793     /**
794      * All expired entries are removed from the map prior to determining if it is empty.
795      * {@inheritDoc}
796      */
797     @Override
798     public boolean isEmpty() {
799         removeAllExpired(now());
800         return super.isEmpty();
801     }
802 
803     /**
804      * Determines if the given expiration time is less than {@code now}.
805      *
806      * @param now The time in milliseconds used to compare against the
807      *        expiration time.
808      * @param expirationTimeObject The expiration time value retrieved from
809      *        {@link #expirationMap}, can be null.
810      * @return {@code true} if {@code expirationTimeObject} is &ge; 0
811      *         and {@code expirationTimeObject} &lt; {@code now}.
812      *         {@code false} otherwise.
813      */
814     private boolean isExpired(final long now, final Long expirationTimeObject) {
815         if (expirationTimeObject != null) {
816             final long expirationTime = expirationTimeObject.longValue();
817             return expirationTime >= 0 && now >= expirationTime;
818         }
819         return false;
820     }
821 
822     /**
823      * All expired entries are removed from the map prior to returning the key set.
824      * {@inheritDoc}
825      */
826     @Override
827     public Set<K> keySet() {
828         removeAllExpired(now());
829         return new KeySet(super.keySet());
830     }
831 
832     /**
833      * The current time in milliseconds.
834      */
835     private long now() {
836         return System.currentTimeMillis();
837     }
838 
839     /**
840      * {@inheritDoc}
841      * <p>
842      * Add the given key-value pair to this map as well as recording the entry's expiration time based on the current time in milliseconds and this map's
843      * {@link #expiringPolicy}.
844      * </p>
845      */
846     @Override
847     public V put(final K key, final V value) {
848         // remove the previous record
849         removeIfExpired(key, now());
850 
851         // record expiration time of new entry
852         final long expirationTime = expiringPolicy.expirationTime(key, value);
853         expirationMap.put(key, Long.valueOf(expirationTime));
854 
855         return super.put(key, value);
856     }
857 
858     @Override
859     public void putAll(final Map<? extends K, ? extends V> mapToCopy) {
860         for (final Map.Entry<? extends K, ? extends V> entry : mapToCopy.entrySet()) {
861             put(entry.getKey(), entry.getValue());
862         }
863     }
864 
865     /**
866      * Deserializes the map in using a custom routine.
867      *
868      * @param in The input stream
869      * @throws IOException Thrown if an error occurs while reading from the stream
870      * @throws ClassNotFoundException if an object read from the stream cannot be loaded
871      */
872     @SuppressWarnings("unchecked")
873     // (1) should only fail if input stream is incorrect
874     private void readObject(final ObjectInputStream in)
875         throws IOException, ClassNotFoundException {
876         in.defaultReadObject();
877         map = (Map<K, V>) in.readObject(); // (1)
878     }
879 
880     /**
881      * Normal {@link Map#remove(Object)} behavior with the addition of removing
882      * any expiration entry as well.
883      * {@inheritDoc}
884      */
885     @Override
886     public V remove(final Object key) {
887         expirationMap.remove(key);
888         return super.remove(key);
889     }
890 
891     /**
892      * Removes all entries in the map whose expiration time is less than
893      * {@code now}. The exceptions are entries with negative expiration
894      * times; those entries are never removed.
895      *
896      * @see #isExpired(long, Long)
897      */
898     private void removeAllExpired(final long nowMillis) {
899         final Iterator<Map.Entry<Object, Long>> iter = expirationMap.entrySet().iterator();
900         while (iter.hasNext()) {
901             final Map.Entry<Object, Long> expirationEntry = iter.next();
902             if (isExpired(nowMillis, expirationEntry.getValue())) {
903                 // remove entry from collection
904                 super.remove(expirationEntry.getKey());
905                 // remove entry from expiration map
906                 iter.remove();
907             }
908         }
909     }
910 
911     /**
912      * Removes the entry with the given key if the entry's expiration time is
913      * less than {@code now}. If the entry has a negative expiration time,
914      * the entry is never removed.
915      */
916     private void removeIfExpired(final Object key, final long nowMillis) {
917         final Long expirationTimeObject = expirationMap.get(key);
918         if (isExpired(nowMillis, expirationTimeObject)) {
919             remove(key);
920         }
921     }
922 
923     /**
924      * All expired entries are removed from the map prior to returning the size.
925      * {@inheritDoc}
926      */
927     @Override
928     public int size() {
929         removeAllExpired(now());
930         return super.size();
931     }
932 
933     /**
934      * All expired entries are removed from the map prior to returning the value collection.
935      * {@inheritDoc}
936      */
937     @Override
938     public Collection<V> values() {
939         removeAllExpired(now());
940         return new ValuesCollection(super.values());
941     }
942 
943     /**
944      * Serializes this object to an ObjectOutputStream.
945      *
946      * @param out The target ObjectOutputStream.
947      * @throws IOException thrown when an I/O errors occur writing to the target stream.
948      */
949     private void writeObject(final ObjectOutputStream out)
950         throws IOException {
951         out.defaultWriteObject();
952         out.writeObject(map);
953     }
954 }