This is an automated email from the ASF dual-hosted git repository.

garydgregory pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/commons-collections.git


The following commit(s) were added to refs/heads/master by this push:
     new b219ccbe7 [COLLECTIONS-776] Fix expiration bypass in 
PassiveExpiringMap when wrapped in SynchronizedMap (#694).
b219ccbe7 is described below

commit b219ccbe7b95250abd3ba3143edf340b7fad1943
Author: Gary Gregory <[email protected]>
AuthorDate: Sun Jul 5 15:54:07 2026 -0400

    [COLLECTIONS-776] Fix expiration bypass in PassiveExpiringMap when
    wrapped in SynchronizedMap (#694).
    
    Sort members.
---
 src/changes/changes.xml                            |    1 +
 .../collections4/map/PassiveExpiringMap.java       | 1158 ++++++++++----------
 .../collections4/map/PassiveExpiringMapTest.java   |  342 +++---
 3 files changed, 751 insertions(+), 750 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 24535388a..53f8208db 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -69,6 +69,7 @@
     <action type="fix" dev="ggregory" due-to="Dexter.k, Gary Gregory">Fix 
stale size in AbstractMapMultiSet view iterator remove (#701).</action>
     <action type="fix" dev="ggregory" due-to="Javid Khan, Gary Gregory">Stop 
ExtendedMessageFormat seekNonWs reading past the pattern end (#1740).</action>
     <action type="fix" dev="ggregory" due-to="Dexter.k, Gary Gregory">Remove 
stale keys in OrderedProperties compute and merge (#702).</action>
+    <action type="fix" dev="ggregory" due-to="sankalp suman, Gary Gregory" 
issue="COLLECTIONS-776">Fix expiration bypass in PassiveExpiringMap when 
wrapped in SynchronizedMap (#694).</action>
     <!-- ADD -->
     <action type="add" dev="ggregory" due-to="Gary Gregory">Add generics to 
UnmodifiableIterator for the wrapped type.</action>
     <action type="add" dev="ggregory" due-to="Gary Gregory">Add a Maven 
benchmark profile for JMH.</action>
diff --git 
a/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java 
b/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java
index eb1eb1c7f..feacaf26c 100644
--- a/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java
+++ b/src/main/java/org/apache/commons/collections4/map/PassiveExpiringMap.java
@@ -157,6 +157,150 @@ public class PassiveExpiringMap<K, V>
         }
     }
 
+    private final class EntrySet extends AbstractSetDecorator<Entry<K, V>> {
+
+        /** Generated serial version ID. */
+        private static final long serialVersionUID = 1L;
+
+        private EntrySet(final Set<Entry<K, V>> set) {
+            super(set);
+        }
+
+        @Override
+        public void clear() {
+            PassiveExpiringMap.this.clear();
+        }
+
+        @Override
+        public boolean contains(final Object object) {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.contains(object);
+        }
+
+        @Override
+        public boolean containsAll(final Collection<?> coll) {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.containsAll(coll);
+        }
+
+        @Override
+        public boolean isEmpty() {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.isEmpty();
+        }
+
+        @Override
+        public Iterator<Entry<K, V>> iterator() {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return new EntrySetIterator(super.iterator());
+        }
+
+        @Override
+        public boolean remove(final Object object) {
+            if (object instanceof Map.Entry) {
+                final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
+                final Object key = entry.getKey();
+                if (PassiveExpiringMap.this.containsKey(key)) {
+                    final Object value = PassiveExpiringMap.this.get(key);
+                    if (Objects.equals(value, entry.getValue())) {
+                        PassiveExpiringMap.this.remove(key);
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public boolean removeAll(final Collection<?> coll) {
+            Objects.requireNonNull(coll);
+            boolean changed = false;
+            if (size() > coll.size()) {
+                for (final Object obj : coll) {
+                    changed |= remove(obj);
+                }
+            } else {
+                final Iterator<?> it = iterator();
+                while (it.hasNext()) {
+                    if (coll.contains(it.next())) {
+                        it.remove();
+                        changed = true;
+                    }
+                }
+            }
+            return changed;
+        }
+
+        @Override
+        public boolean removeIf(final Predicate<? super Entry<K, V>> filter) {
+            Objects.requireNonNull(filter);
+            boolean changed = false;
+            final Iterator<Entry<K, V>> it = iterator();
+            while (it.hasNext()) {
+                if (filter.test(it.next())) {
+                    it.remove();
+                    changed = true;
+                }
+            }
+            return changed;
+        }
+
+        @Override
+        public boolean retainAll(final Collection<?> coll) {
+            Objects.requireNonNull(coll);
+            boolean changed = false;
+            final Iterator<?> it = iterator();
+            while (it.hasNext()) {
+                if (!coll.contains(it.next())) {
+                    it.remove();
+                    changed = true;
+                }
+            }
+            return changed;
+        }
+
+        @Override
+        public int size() {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.size();
+        }
+
+        @Override
+        public Object[] toArray() {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.toArray();
+        }
+
+        @Override
+        public <T> T[] toArray(final T[] array) {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.toArray(array);
+        }
+    }
+
+    private final class EntrySetIterator extends 
AbstractIteratorDecorator<Entry<K, V>> {
+        private Entry<K, V> lastReturned;
+
+        private EntrySetIterator(final Iterator<Entry<K, V>> iterator) {
+            super(iterator);
+        }
+
+        @Override
+        public Entry<K, V> next() {
+            lastReturned = super.next();
+            return lastReturned;
+        }
+
+        @Override
+        public void remove() {
+            super.remove();
+            if (lastReturned != null) {
+                
PassiveExpiringMap.this.expirationMap.remove(lastReturned.getKey());
+                lastReturned = null;
+            }
+        }
+    }
+
     /**
      * A policy to determine the expiration time for key-value entries.
      *
@@ -179,413 +323,50 @@ public class PassiveExpiringMap<K, V>
         long expirationTime(K key, V value);
     }
 
-    /** Serialization version */
-    private static final long serialVersionUID = 1L;
+    private final class KeySet extends AbstractSetDecorator<K> {
 
-    /**
-     * First validate the input parameters. If the parameters are valid, 
convert
-     * the given time measured in the given units to the same time measured in
-     * milliseconds.
-     *
-     * @param timeToLive the constant amount of time an entry is available
-     *        before it expires. A negative value results in entries that NEVER
-     *        expire. A zero value results in entries that ALWAYS expire.
-     * @param timeUnit the unit of time for the {@code timeToLive}
-     *        parameter, must not be null.
-     * @throws NullPointerException if the time unit is null.
-     */
-    private static long validateAndConvertToMillis(final long timeToLive,
-                                                   final TimeUnit timeUnit) {
-        Objects.requireNonNull(timeUnit, "timeUnit");
-        return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
-    }
+        /** Generated serial version ID. */
+        private static final long serialVersionUID = 1L;
 
-    /** Map used to manage expiration times for the actual map entries. */
-    private final Map<Object, Long> expirationMap = new HashMap<>();
+        private KeySet(final Set<K> set) {
+            super(set);
+        }
 
-    /** The policy used to determine time-to-live values for map entries. */
-    private final ExpirationPolicy<K, V> expiringPolicy;
+        @Override
+        public void clear() {
+            PassiveExpiringMap.this.clear();
+        }
 
-    /**
-     * Default constructor. Constructs a map decorator that results in entries
-     * NEVER expiring.
-     */
-    public PassiveExpiringMap() {
-        this(-1L);
-    }
+        @Override
+        public boolean contains(final Object key) {
+            PassiveExpiringMap.this.removeIfExpired(key, 
PassiveExpiringMap.this.now());
+            return super.contains(key);
+        }
 
-    /**
-     * Constructs a map decorator using the given expiration policy to 
determine
-     * expiration times.
-     *
-     * @param expiringPolicy the policy used to determine expiration times of
-     *        entries as they are added.
-     * @throws NullPointerException if expiringPolicy is null
-     */
-    public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
-        this(expiringPolicy, new HashMap<>());
-    }
+        @Override
+        public boolean containsAll(final Collection<?> coll) {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.containsAll(coll);
+        }
 
-    /**
-     * Constructs a map decorator that decorates the given map and uses the 
given
-     * expiration policy to determine expiration times. If there are any
-     * elements already in the map being decorated, they will NEVER expire
-     * unless they are replaced.
-     *
-     * @param expiringPolicy the policy used to determine expiration times of
-     *        entries as they are added.
-     * @param map the map to decorate, must not be null.
-     * @throws NullPointerException if the map or expiringPolicy is null.
-     */
-    public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
-                              final Map<K, V> map) {
-        super(map);
-        this.expiringPolicy = Objects.requireNonNull(expiringPolicy, 
"expiringPolicy");
-    }
+        @Override
+        public boolean isEmpty() {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.isEmpty();
+        }
 
-    /**
-     * Constructs a map decorator that decorates the given map using the given
-     * time-to-live value measured in milliseconds to create and use a
-     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
-     *
-     * @param timeToLiveMillis the constant amount of time (in milliseconds) an
-     *        entry is available before it expires. A negative value results in
-     *        entries that NEVER expire. A zero value results in entries that
-     *        ALWAYS expire.
-     */
-    public PassiveExpiringMap(final long timeToLiveMillis) {
-        this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
-                new HashMap<>());
-    }
-
-    /**
-     * Constructs a map decorator using the given time-to-live value measured 
in
-     * milliseconds to create and use a
-     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
-     * are any elements already in the map being decorated, they will NEVER
-     * expire unless they are replaced.
-     *
-     * @param timeToLiveMillis the constant amount of time (in milliseconds) an
-     *        entry is available before it expires. A negative value results in
-     *        entries that NEVER expire. A zero value results in entries that
-     *        ALWAYS expire.
-     * @param map the map to decorate, must not be null.
-     * @throws NullPointerException if the map is null.
-     */
-    public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V> 
map) {
-        this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
-             map);
-    }
-
-    /**
-     * Constructs a map decorator using the given time-to-live value measured 
in
-     * the given time units of measure to create and use a
-     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
-     *
-     * @param timeToLive the constant amount of time an entry is available
-     *        before it expires. A negative value results in entries that NEVER
-     *        expire. A zero value results in entries that ALWAYS expire.
-     * @param timeUnit the unit of time for the {@code timeToLive}
-     *        parameter, must not be null.
-     * @throws NullPointerException if the time unit is null.
-     */
-    public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
-        this(validateAndConvertToMillis(timeToLive, timeUnit));
-    }
-
-    /**
-     * Constructs a map decorator that decorates the given map using the given
-     * time-to-live value measured in the given time units of measure to create
-     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This 
policy
-     * is used to determine expiration times. If there are any elements already
-     * in the map being decorated, they will NEVER expire unless they are
-     * replaced.
-     *
-     * @param timeToLive the constant amount of time an entry is available
-     *        before it expires. A negative value results in entries that NEVER
-     *        expire. A zero value results in entries that ALWAYS expire.
-     * @param timeUnit the unit of time for the {@code timeToLive}
-     *        parameter, must not be null.
-     * @param map the map to decorate, must not be null.
-     * @throws NullPointerException if the map or time unit is null.
-     */
-    public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit, 
final Map<K, V> map) {
-        this(validateAndConvertToMillis(timeToLive, timeUnit), map);
-    }
-
-    /**
-     * Constructs a map decorator that decorates the given map and results in
-     * entries NEVER expiring. If there are any elements already in the map
-     * being decorated, they also will NEVER expire.
-     *
-     * @param map the map to decorate, must not be null.
-     * @throws NullPointerException if the map is null.
-     */
-    public PassiveExpiringMap(final Map<K, V> map) {
-        this(-1L, map);
-    }
-
-    /**
-     * Normal {@link Map#clear()} behavior with the addition of clearing all
-     * expiration entries as well.
-     */
-    @Override
-    public void clear() {
-        super.clear();
-        expirationMap.clear();
-    }
-
-    /**
-     * All expired entries are removed from the map prior to determining the
-     * contains result.
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean containsKey(final Object key) {
-        removeIfExpired(key, now());
-        return super.containsKey(key);
-    }
-
-    /**
-     * All expired entries are removed from the map prior to determining the
-     * contains result.
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean containsValue(final Object value) {
-        removeAllExpired(now());
-        return super.containsValue(value);
-    }
-
-    /**
-     * All expired entries are removed from the map prior to returning the 
entry set.
-     * {@inheritDoc}
-     */
-    @Override
-    public Set<Entry<K, V>> entrySet() {
-        removeAllExpired(now());
-        return new EntrySet(super.entrySet());
-    }
-
-    /**
-     * All expired entries are removed from the map prior to returning the 
entry value.
-     * {@inheritDoc}
-     */
-    @Override
-    public V get(final Object key) {
-        removeIfExpired(key, now());
-        return super.get(key);
-    }
-
-    /**
-     * All expired entries are removed from the map prior to determining if it 
is empty.
-     * {@inheritDoc}
-     */
-    @Override
-    public boolean isEmpty() {
-        removeAllExpired(now());
-        return super.isEmpty();
-    }
-
-    /**
-     * Determines if the given expiration time is less than {@code now}.
-     *
-     * @param now the time in milliseconds used to compare against the
-     *        expiration time.
-     * @param expirationTimeObject the expiration time value retrieved from
-     *        {@link #expirationMap}, can be null.
-     * @return {@code true} if {@code expirationTimeObject} is &ge; 0
-     *         and {@code expirationTimeObject} &lt; {@code now}.
-     *         {@code false} otherwise.
-     */
-    private boolean isExpired(final long now, final Long expirationTimeObject) 
{
-        if (expirationTimeObject != null) {
-            final long expirationTime = expirationTimeObject.longValue();
-            return expirationTime >= 0 && now >= expirationTime;
-        }
-        return false;
-    }
-
-    /**
-     * All expired entries are removed from the map prior to returning the key 
set.
-     * {@inheritDoc}
-     */
-    @Override
-    public Set<K> keySet() {
-        removeAllExpired(now());
-        return new KeySet(super.keySet());
-    }
-
-    /**
-     * The current time in milliseconds.
-     */
-    private long now() {
-        return System.currentTimeMillis();
-    }
-
-    /**
-     * {@inheritDoc}
-     * <p>
-     * 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
-     * {@link #expiringPolicy}.
-     * </p>
-     */
-    @Override
-    public V put(final K key, final V value) {
-        // remove the previous record
-        removeIfExpired(key, now());
-
-        // record expiration time of new entry
-        final long expirationTime = expiringPolicy.expirationTime(key, value);
-        expirationMap.put(key, Long.valueOf(expirationTime));
-
-        return super.put(key, value);
-    }
-
-    @Override
-    public void putAll(final Map<? extends K, ? extends V> mapToCopy) {
-        for (final Map.Entry<? extends K, ? extends V> entry : 
mapToCopy.entrySet()) {
-            put(entry.getKey(), entry.getValue());
-        }
-    }
-
-    /**
-     * Deserializes the map in using a custom routine.
-     *
-     * @param in the input stream
-     * @throws IOException if an error occurs while reading from the stream
-     * @throws ClassNotFoundException if an object read from the stream cannot 
be loaded
-     */
-    @SuppressWarnings("unchecked")
-    // (1) should only fail if input stream is incorrect
-    private void readObject(final ObjectInputStream in)
-        throws IOException, ClassNotFoundException {
-        in.defaultReadObject();
-        map = (Map<K, V>) in.readObject(); // (1)
-    }
-
-    /**
-     * Normal {@link Map#remove(Object)} behavior with the addition of removing
-     * any expiration entry as well.
-     * {@inheritDoc}
-     */
-    @Override
-    public V remove(final Object key) {
-        expirationMap.remove(key);
-        return super.remove(key);
-    }
-
-    /**
-     * Removes all entries in the map whose expiration time is less than
-     * {@code now}. The exceptions are entries with negative expiration
-     * times; those entries are never removed.
-     *
-     * @see #isExpired(long, Long)
-     */
-    private void removeAllExpired(final long nowMillis) {
-        final Iterator<Map.Entry<Object, Long>> iter = 
expirationMap.entrySet().iterator();
-        while (iter.hasNext()) {
-            final Map.Entry<Object, Long> expirationEntry = iter.next();
-            if (isExpired(nowMillis, expirationEntry.getValue())) {
-                // remove entry from collection
-                super.remove(expirationEntry.getKey());
-                // remove entry from expiration map
-                iter.remove();
-            }
-        }
-    }
-
-    /**
-     * Removes the entry with the given key if the entry's expiration time is
-     * less than {@code now}. If the entry has a negative expiration time,
-     * the entry is never removed.
-     */
-    private void removeIfExpired(final Object key, final long nowMillis) {
-        final Long expirationTimeObject = expirationMap.get(key);
-        if (isExpired(nowMillis, expirationTimeObject)) {
-            remove(key);
-        }
-    }
-
-    /**
-     * All expired entries are removed from the map prior to returning the 
size.
-     * {@inheritDoc}
-     */
-    @Override
-    public int size() {
-        removeAllExpired(now());
-        return super.size();
-    }
-
-    /**
-     * All expired entries are removed from the map prior to returning the 
value collection.
-     * {@inheritDoc}
-     */
-    @Override
-    public Collection<V> values() {
-        removeAllExpired(now());
-        return new ValuesCollection(super.values());
-    }
-
-    /**
-     * Serializes this object to an ObjectOutputStream.
-     *
-     * @param out the target ObjectOutputStream.
-     * @throws IOException thrown when an I/O errors occur writing to the 
target stream.
-     */
-    private void writeObject(final ObjectOutputStream out)
-        throws IOException {
-        out.defaultWriteObject();
-        out.writeObject(map);
-    }
-
-    private final class EntrySet extends AbstractSetDecorator<Entry<K, V>> {
-
-        /** Generated serial version ID. */
-        private static final long serialVersionUID = 1L;
-
-        private EntrySet(final Set<Entry<K, V>> set) {
-            super(set);
-        }
-
-        @Override
-        public Iterator<Entry<K, V>> iterator() {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return new EntrySetIterator(super.iterator());
-        }
-
-        @Override
-        public int size() {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.size();
-        }
+        @Override
+        public Iterator<K> iterator() {
+            return new 
KeySetIterator(PassiveExpiringMap.this.entrySet().iterator());
+        }
 
         @Override
-        public boolean isEmpty() {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.isEmpty();
-        }
-
-        @Override
-        public boolean contains(final Object object) {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.contains(object);
-        }
-
-        @Override
-        public boolean remove(final Object object) {
-            if (object instanceof Map.Entry) {
-                final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) object;
-                final Object key = entry.getKey();
-                if (PassiveExpiringMap.this.containsKey(key)) {
-                    final Object value = PassiveExpiringMap.this.get(key);
-                    if (Objects.equals(value, entry.getValue())) {
-                        PassiveExpiringMap.this.remove(key);
-                        return true;
-                    }
-                }
+        public boolean remove(final Object key) {
+            final boolean hasKey = contains(key);
+            if (hasKey) {
+                PassiveExpiringMap.this.remove(key);
             }
-            return false;
+            return hasKey;
         }
 
         @Override
@@ -609,12 +390,12 @@ public class PassiveExpiringMap<K, V>
         }
 
         @Override
-        public boolean retainAll(final Collection<?> coll) {
-            Objects.requireNonNull(coll);
+        public boolean removeIf(final Predicate<? super K> filter) {
+            Objects.requireNonNull(filter);
             boolean changed = false;
-            final Iterator<?> it = iterator();
+            final Iterator<K> it = iterator();
             while (it.hasNext()) {
-                if (!coll.contains(it.next())) {
+                if (filter.test(it.next())) {
                     it.remove();
                     changed = true;
                 }
@@ -623,12 +404,12 @@ public class PassiveExpiringMap<K, V>
         }
 
         @Override
-        public boolean removeIf(final Predicate<? super Entry<K, V>> filter) {
-            Objects.requireNonNull(filter);
+        public boolean retainAll(final Collection<?> coll) {
+            Objects.requireNonNull(coll);
             boolean changed = false;
-            final Iterator<Entry<K, V>> it = iterator();
+            final Iterator<?> it = iterator();
             while (it.hasNext()) {
-                if (filter.test(it.next())) {
+                if (!coll.contains(it.next())) {
                     it.remove();
                     changed = true;
                 }
@@ -637,8 +418,9 @@ public class PassiveExpiringMap<K, V>
         }
 
         @Override
-        public void clear() {
-            PassiveExpiringMap.this.clear();
+        public int size() {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.size();
         }
 
         @Override
@@ -652,55 +434,55 @@ public class PassiveExpiringMap<K, V>
             
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
             return super.toArray(array);
         }
-
-        @Override
-        public boolean containsAll(final Collection<?> coll) {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.containsAll(coll);
-        }
     }
 
-    private final class EntrySetIterator extends 
AbstractIteratorDecorator<Entry<K, V>> {
-        private Entry<K, V> lastReturned;
+    private final class KeySetIterator implements Iterator<K> {
+        private final Iterator<Entry<K, V>> iterator;
 
-        private EntrySetIterator(final Iterator<Entry<K, V>> iterator) {
-            super(iterator);
+        private KeySetIterator(final Iterator<Entry<K, V>> iterator) {
+            this.iterator = iterator;
         }
 
         @Override
-        public Entry<K, V> next() {
-            lastReturned = super.next();
-            return lastReturned;
+        public boolean hasNext() {
+            return iterator.hasNext();
+        }
+
+        @Override
+        public K next() {
+            return iterator.next().getKey();
         }
 
         @Override
         public void remove() {
-            super.remove();
-            if (lastReturned != null) {
-                
PassiveExpiringMap.this.expirationMap.remove(lastReturned.getKey());
-                lastReturned = null;
-            }
+            iterator.remove();
         }
     }
 
-    private final class KeySet extends AbstractSetDecorator<K> {
+    private final class ValuesCollection extends 
AbstractCollectionDecorator<V> {
 
         /** Generated serial version ID. */
         private static final long serialVersionUID = 1L;
 
-        private KeySet(final Set<K> set) {
-            super(set);
+        private ValuesCollection(final Collection<V> coll) {
+            super(coll);
         }
 
         @Override
-        public Iterator<K> iterator() {
-            return new 
KeySetIterator(PassiveExpiringMap.this.entrySet().iterator());
+        public void clear() {
+            PassiveExpiringMap.this.clear();
         }
 
         @Override
-        public int size() {
+        public boolean contains(final Object value) {
             
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.size();
+            return super.contains(value);
+        }
+
+        @Override
+        public boolean containsAll(final Collection<?> coll) {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.containsAll(coll);
         }
 
         @Override
@@ -710,47 +492,43 @@ public class PassiveExpiringMap<K, V>
         }
 
         @Override
-        public boolean contains(final Object key) {
-            PassiveExpiringMap.this.removeIfExpired(key, 
PassiveExpiringMap.this.now());
-            return super.contains(key);
+        public Iterator<V> iterator() {
+            return new 
ValuesIterator(PassiveExpiringMap.this.entrySet().iterator());
         }
 
         @Override
-        public boolean remove(final Object key) {
-            final boolean hasKey = contains(key);
-            if (hasKey) {
-                PassiveExpiringMap.this.remove(key);
+        public boolean remove(final Object value) {
+            final Iterator<V> it = iterator();
+            while (it.hasNext()) {
+                if (Objects.equals(it.next(), value)) {
+                    it.remove();
+                    return true;
+                }
             }
-            return hasKey;
+            return false;
         }
 
         @Override
         public boolean removeAll(final Collection<?> coll) {
             Objects.requireNonNull(coll);
             boolean changed = false;
-            if (size() > coll.size()) {
-                for (final Object obj : coll) {
-                    changed |= remove(obj);
-                }
-            } else {
-                final Iterator<?> it = iterator();
-                while (it.hasNext()) {
-                    if (coll.contains(it.next())) {
-                        it.remove();
-                        changed = true;
-                    }
+            final Iterator<?> it = iterator();
+            while (it.hasNext()) {
+                if (coll.contains(it.next())) {
+                    it.remove();
+                    changed = true;
                 }
             }
             return changed;
         }
 
         @Override
-        public boolean retainAll(final Collection<?> coll) {
-            Objects.requireNonNull(coll);
+        public boolean removeIf(final Predicate<? super V> filter) {
+            Objects.requireNonNull(filter);
             boolean changed = false;
-            final Iterator<?> it = iterator();
+            final Iterator<V> it = iterator();
             while (it.hasNext()) {
-                if (!coll.contains(it.next())) {
+                if (filter.test(it.next())) {
                     it.remove();
                     changed = true;
                 }
@@ -759,12 +537,12 @@ public class PassiveExpiringMap<K, V>
         }
 
         @Override
-        public boolean removeIf(final Predicate<? super K> filter) {
-            Objects.requireNonNull(filter);
+        public boolean retainAll(final Collection<?> coll) {
+            Objects.requireNonNull(coll);
             boolean changed = false;
-            final Iterator<K> it = iterator();
+            final Iterator<?> it = iterator();
             while (it.hasNext()) {
-                if (filter.test(it.next())) {
+                if (!coll.contains(it.next())) {
                     it.remove();
                     changed = true;
                 }
@@ -773,8 +551,9 @@ public class PassiveExpiringMap<K, V>
         }
 
         @Override
-        public void clear() {
-            PassiveExpiringMap.this.clear();
+        public int size() {
+            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
+            return super.size();
         }
 
         @Override
@@ -788,18 +567,12 @@ public class PassiveExpiringMap<K, V>
             
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
             return super.toArray(array);
         }
-
-        @Override
-        public boolean containsAll(final Collection<?> coll) {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.containsAll(coll);
-        }
     }
 
-    private final class KeySetIterator implements Iterator<K> {
+    private final class ValuesIterator implements Iterator<V> {
         private final Iterator<Entry<K, V>> iterator;
 
-        private KeySetIterator(final Iterator<Entry<K, V>> iterator) {
+        private ValuesIterator(final Iterator<Entry<K, V>> iterator) {
             this.iterator = iterator;
         }
 
@@ -809,8 +582,8 @@ public class PassiveExpiringMap<K, V>
         }
 
         @Override
-        public K next() {
-            return iterator.next().getKey();
+        public V next() {
+            return iterator.next().getValue();
         }
 
         @Override
@@ -819,136 +592,363 @@ public class PassiveExpiringMap<K, V>
         }
     }
 
-    private final class ValuesCollection extends 
AbstractCollectionDecorator<V> {
+    /** Serialization version */
+    private static final long serialVersionUID = 1L;
 
-        /** Generated serial version ID. */
-        private static final long serialVersionUID = 1L;
+    /**
+     * First validate the input parameters. If the parameters are valid, 
convert
+     * the given time measured in the given units to the same time measured in
+     * milliseconds.
+     *
+     * @param timeToLive the constant amount of time an entry is available
+     *        before it expires. A negative value results in entries that NEVER
+     *        expire. A zero value results in entries that ALWAYS expire.
+     * @param timeUnit the unit of time for the {@code timeToLive}
+     *        parameter, must not be null.
+     * @throws NullPointerException if the time unit is null.
+     */
+    private static long validateAndConvertToMillis(final long timeToLive,
+                                                   final TimeUnit timeUnit) {
+        Objects.requireNonNull(timeUnit, "timeUnit");
+        return TimeUnit.MILLISECONDS.convert(timeToLive, timeUnit);
+    }
 
-        private ValuesCollection(final Collection<V> coll) {
-            super(coll);
-        }
+    /** Map used to manage expiration times for the actual map entries. */
+    private final Map<Object, Long> expirationMap = new HashMap<>();
+
+    /** The policy used to determine time-to-live values for map entries. */
+    private final ExpirationPolicy<K, V> expiringPolicy;
+
+    /**
+     * Default constructor. Constructs a map decorator that results in entries
+     * NEVER expiring.
+     */
+    public PassiveExpiringMap() {
+        this(-1L);
+    }
+
+    /**
+     * Constructs a map decorator using the given expiration policy to 
determine
+     * expiration times.
+     *
+     * @param expiringPolicy the policy used to determine expiration times of
+     *        entries as they are added.
+     * @throws NullPointerException if expiringPolicy is null
+     */
+    public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy) {
+        this(expiringPolicy, new HashMap<>());
+    }
+
+    /**
+     * Constructs a map decorator that decorates the given map and uses the 
given
+     * expiration policy to determine expiration times. If there are any
+     * elements already in the map being decorated, they will NEVER expire
+     * unless they are replaced.
+     *
+     * @param expiringPolicy the policy used to determine expiration times of
+     *        entries as they are added.
+     * @param map the map to decorate, must not be null.
+     * @throws NullPointerException if the map or expiringPolicy is null.
+     */
+    public PassiveExpiringMap(final ExpirationPolicy<K, V> expiringPolicy,
+                              final Map<K, V> map) {
+        super(map);
+        this.expiringPolicy = Objects.requireNonNull(expiringPolicy, 
"expiringPolicy");
+    }
+
+    /**
+     * Constructs a map decorator that decorates the given map using the given
+     * time-to-live value measured in milliseconds to create and use a
+     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
+     *
+     * @param timeToLiveMillis the constant amount of time (in milliseconds) an
+     *        entry is available before it expires. A negative value results in
+     *        entries that NEVER expire. A zero value results in entries that
+     *        ALWAYS expire.
+     */
+    public PassiveExpiringMap(final long timeToLiveMillis) {
+        this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
+                new HashMap<>());
+    }
+
+    /**
+     * Constructs a map decorator using the given time-to-live value measured 
in
+     * milliseconds to create and use a
+     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. If there
+     * are any elements already in the map being decorated, they will NEVER
+     * expire unless they are replaced.
+     *
+     * @param timeToLiveMillis the constant amount of time (in milliseconds) an
+     *        entry is available before it expires. A negative value results in
+     *        entries that NEVER expire. A zero value results in entries that
+     *        ALWAYS expire.
+     * @param map the map to decorate, must not be null.
+     * @throws NullPointerException if the map is null.
+     */
+    public PassiveExpiringMap(final long timeToLiveMillis, final Map<K, V> 
map) {
+        this(new ConstantTimeToLiveExpirationPolicy<>(timeToLiveMillis),
+             map);
+    }
+
+    /**
+     * Constructs a map decorator using the given time-to-live value measured 
in
+     * the given time units of measure to create and use a
+     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy.
+     *
+     * @param timeToLive the constant amount of time an entry is available
+     *        before it expires. A negative value results in entries that NEVER
+     *        expire. A zero value results in entries that ALWAYS expire.
+     * @param timeUnit the unit of time for the {@code timeToLive}
+     *        parameter, must not be null.
+     * @throws NullPointerException if the time unit is null.
+     */
+    public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit) {
+        this(validateAndConvertToMillis(timeToLive, timeUnit));
+    }
+
+    /**
+     * Constructs a map decorator that decorates the given map using the given
+     * time-to-live value measured in the given time units of measure to create
+     * {@link ConstantTimeToLiveExpirationPolicy} expiration policy. This 
policy
+     * is used to determine expiration times. If there are any elements already
+     * in the map being decorated, they will NEVER expire unless they are
+     * replaced.
+     *
+     * @param timeToLive the constant amount of time an entry is available
+     *        before it expires. A negative value results in entries that NEVER
+     *        expire. A zero value results in entries that ALWAYS expire.
+     * @param timeUnit the unit of time for the {@code timeToLive}
+     *        parameter, must not be null.
+     * @param map the map to decorate, must not be null.
+     * @throws NullPointerException if the map or time unit is null.
+     */
+    public PassiveExpiringMap(final long timeToLive, final TimeUnit timeUnit, 
final Map<K, V> map) {
+        this(validateAndConvertToMillis(timeToLive, timeUnit), map);
+    }
+
+    /**
+     * Constructs a map decorator that decorates the given map and results in
+     * entries NEVER expiring. If there are any elements already in the map
+     * being decorated, they also will NEVER expire.
+     *
+     * @param map the map to decorate, must not be null.
+     * @throws NullPointerException if the map is null.
+     */
+    public PassiveExpiringMap(final Map<K, V> map) {
+        this(-1L, map);
+    }
+
+    /**
+     * Normal {@link Map#clear()} behavior with the addition of clearing all
+     * expiration entries as well.
+     */
+    @Override
+    public void clear() {
+        super.clear();
+        expirationMap.clear();
+    }
+
+    /**
+     * All expired entries are removed from the map prior to determining the
+     * contains result.
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean containsKey(final Object key) {
+        removeIfExpired(key, now());
+        return super.containsKey(key);
+    }
+
+    /**
+     * All expired entries are removed from the map prior to determining the
+     * contains result.
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean containsValue(final Object value) {
+        removeAllExpired(now());
+        return super.containsValue(value);
+    }
+
+    /**
+     * All expired entries are removed from the map prior to returning the 
entry set.
+     * {@inheritDoc}
+     */
+    @Override
+    public Set<Entry<K, V>> entrySet() {
+        removeAllExpired(now());
+        return new EntrySet(super.entrySet());
+    }
 
-        @Override
-        public Iterator<V> iterator() {
-            return new 
ValuesIterator(PassiveExpiringMap.this.entrySet().iterator());
-        }
+    /**
+     * All expired entries are removed from the map prior to returning the 
entry value.
+     * {@inheritDoc}
+     */
+    @Override
+    public V get(final Object key) {
+        removeIfExpired(key, now());
+        return super.get(key);
+    }
 
-        @Override
-        public int size() {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.size();
-        }
+    /**
+     * All expired entries are removed from the map prior to determining if it 
is empty.
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean isEmpty() {
+        removeAllExpired(now());
+        return super.isEmpty();
+    }
 
-        @Override
-        public boolean isEmpty() {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.isEmpty();
+    /**
+     * Determines if the given expiration time is less than {@code now}.
+     *
+     * @param now the time in milliseconds used to compare against the
+     *        expiration time.
+     * @param expirationTimeObject the expiration time value retrieved from
+     *        {@link #expirationMap}, can be null.
+     * @return {@code true} if {@code expirationTimeObject} is &ge; 0
+     *         and {@code expirationTimeObject} &lt; {@code now}.
+     *         {@code false} otherwise.
+     */
+    private boolean isExpired(final long now, final Long expirationTimeObject) 
{
+        if (expirationTimeObject != null) {
+            final long expirationTime = expirationTimeObject.longValue();
+            return expirationTime >= 0 && now >= expirationTime;
         }
+        return false;
+    }
 
-        @Override
-        public boolean contains(final Object value) {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.contains(value);
-        }
+    /**
+     * All expired entries are removed from the map prior to returning the key 
set.
+     * {@inheritDoc}
+     */
+    @Override
+    public Set<K> keySet() {
+        removeAllExpired(now());
+        return new KeySet(super.keySet());
+    }
 
-        @Override
-        public boolean remove(final Object value) {
-            final Iterator<V> it = iterator();
-            while (it.hasNext()) {
-                if (Objects.equals(it.next(), value)) {
-                    it.remove();
-                    return true;
-                }
-            }
-            return false;
-        }
+    /**
+     * The current time in milliseconds.
+     */
+    private long now() {
+        return System.currentTimeMillis();
+    }
 
-        @Override
-        public boolean removeAll(final Collection<?> coll) {
-            Objects.requireNonNull(coll);
-            boolean changed = false;
-            final Iterator<?> it = iterator();
-            while (it.hasNext()) {
-                if (coll.contains(it.next())) {
-                    it.remove();
-                    changed = true;
-                }
-            }
-            return changed;
-        }
+    /**
+     * {@inheritDoc}
+     * <p>
+     * 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
+     * {@link #expiringPolicy}.
+     * </p>
+     */
+    @Override
+    public V put(final K key, final V value) {
+        // remove the previous record
+        removeIfExpired(key, now());
 
-        @Override
-        public boolean retainAll(final Collection<?> coll) {
-            Objects.requireNonNull(coll);
-            boolean changed = false;
-            final Iterator<?> it = iterator();
-            while (it.hasNext()) {
-                if (!coll.contains(it.next())) {
-                    it.remove();
-                    changed = true;
-                }
-            }
-            return changed;
-        }
+        // record expiration time of new entry
+        final long expirationTime = expiringPolicy.expirationTime(key, value);
+        expirationMap.put(key, Long.valueOf(expirationTime));
 
-        @Override
-        public boolean removeIf(final Predicate<? super V> filter) {
-            Objects.requireNonNull(filter);
-            boolean changed = false;
-            final Iterator<V> it = iterator();
-            while (it.hasNext()) {
-                if (filter.test(it.next())) {
-                    it.remove();
-                    changed = true;
-                }
-            }
-            return changed;
-        }
+        return super.put(key, value);
+    }
 
-        @Override
-        public void clear() {
-            PassiveExpiringMap.this.clear();
+    @Override
+    public void putAll(final Map<? extends K, ? extends V> mapToCopy) {
+        for (final Map.Entry<? extends K, ? extends V> entry : 
mapToCopy.entrySet()) {
+            put(entry.getKey(), entry.getValue());
         }
+    }
 
-        @Override
-        public Object[] toArray() {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.toArray();
-        }
+    /**
+     * Deserializes the map in using a custom routine.
+     *
+     * @param in the input stream
+     * @throws IOException if an error occurs while reading from the stream
+     * @throws ClassNotFoundException if an object read from the stream cannot 
be loaded
+     */
+    @SuppressWarnings("unchecked")
+    // (1) should only fail if input stream is incorrect
+    private void readObject(final ObjectInputStream in)
+        throws IOException, ClassNotFoundException {
+        in.defaultReadObject();
+        map = (Map<K, V>) in.readObject(); // (1)
+    }
 
-        @Override
-        public <T> T[] toArray(final T[] array) {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.toArray(array);
-        }
+    /**
+     * Normal {@link Map#remove(Object)} behavior with the addition of removing
+     * any expiration entry as well.
+     * {@inheritDoc}
+     */
+    @Override
+    public V remove(final Object key) {
+        expirationMap.remove(key);
+        return super.remove(key);
+    }
 
-        @Override
-        public boolean containsAll(final Collection<?> coll) {
-            
PassiveExpiringMap.this.removeAllExpired(PassiveExpiringMap.this.now());
-            return super.containsAll(coll);
+    /**
+     * Removes all entries in the map whose expiration time is less than
+     * {@code now}. The exceptions are entries with negative expiration
+     * times; those entries are never removed.
+     *
+     * @see #isExpired(long, Long)
+     */
+    private void removeAllExpired(final long nowMillis) {
+        final Iterator<Map.Entry<Object, Long>> iter = 
expirationMap.entrySet().iterator();
+        while (iter.hasNext()) {
+            final Map.Entry<Object, Long> expirationEntry = iter.next();
+            if (isExpired(nowMillis, expirationEntry.getValue())) {
+                // remove entry from collection
+                super.remove(expirationEntry.getKey());
+                // remove entry from expiration map
+                iter.remove();
+            }
         }
     }
 
-    private final class ValuesIterator implements Iterator<V> {
-        private final Iterator<Entry<K, V>> iterator;
-
-        private ValuesIterator(final Iterator<Entry<K, V>> iterator) {
-            this.iterator = iterator;
+    /**
+     * Removes the entry with the given key if the entry's expiration time is
+     * less than {@code now}. If the entry has a negative expiration time,
+     * the entry is never removed.
+     */
+    private void removeIfExpired(final Object key, final long nowMillis) {
+        final Long expirationTimeObject = expirationMap.get(key);
+        if (isExpired(nowMillis, expirationTimeObject)) {
+            remove(key);
         }
+    }
 
-        @Override
-        public boolean hasNext() {
-            return iterator.hasNext();
-        }
+    /**
+     * All expired entries are removed from the map prior to returning the 
size.
+     * {@inheritDoc}
+     */
+    @Override
+    public int size() {
+        removeAllExpired(now());
+        return super.size();
+    }
 
-        @Override
-        public V next() {
-            return iterator.next().getValue();
-        }
+    /**
+     * All expired entries are removed from the map prior to returning the 
value collection.
+     * {@inheritDoc}
+     */
+    @Override
+    public Collection<V> values() {
+        removeAllExpired(now());
+        return new ValuesCollection(super.values());
+    }
 
-        @Override
-        public void remove() {
-            iterator.remove();
-        }
+    /**
+     * Serializes this object to an ObjectOutputStream.
+     *
+     * @param out the target ObjectOutputStream.
+     * @throws IOException thrown when an I/O errors occur writing to the 
target stream.
+     */
+    private void writeObject(final ObjectOutputStream out)
+        throws IOException {
+        out.defaultWriteObject();
+        out.writeObject(map);
     }
 }
diff --git 
a/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java 
b/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java
index 436d6cfdc..e366964bc 100644
--- 
a/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java
+++ 
b/src/test/java/org/apache/commons/collections4/map/PassiveExpiringMapTest.java
@@ -75,6 +75,17 @@ public class PassiveExpiringMapTest<K, V> extends 
AbstractMapTest<PassiveExpirin
         return "4";
     }
 
+    @SuppressWarnings("unchecked")
+    private Map<Object, Long> getExpirationMap(final PassiveExpiringMap<?, ?> 
map) {
+        try {
+            final java.lang.reflect.Field field = 
PassiveExpiringMap.class.getDeclaredField("expirationMap");
+            field.setAccessible(true);
+            return (Map<Object, Long>) field.get(map);
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
     @Override
     protected int getIterationBehaviour() {
         return AbstractCollectionTest.UNORDERED;
@@ -108,6 +119,104 @@ public class PassiveExpiringMapTest<K, V> extends 
AbstractMapTest<PassiveExpirin
         return m;
     }
 
+    @Test
+    void testCollectionsSynchronizedMapExpiration() throws 
InterruptedException {
+        final Map<String, String> map = Collections.synchronizedMap(new 
PassiveExpiringMap<>(50L));
+        map.put("a", "b");
+        map.put("c", "d");
+        assertEquals(2, map.size());
+        // Cache the views in SynchronizedMap before they expire
+        final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
+        final Collection<String> keySet = map.keySet();
+        final Collection<String> values = map.values();
+        Thread.sleep(100L);
+        // entrySet view access triggers expiration
+        synchronized (map) {
+            assertTrue(entrySet.isEmpty());
+            assertTrue(keySet.isEmpty());
+            assertTrue(values.isEmpty());
+        }
+        map.put("a", "b");
+        map.put("c", "d");
+        assertEquals(2, map.size());
+        Thread.sleep(100L);
+        // keySet view access triggers expiration
+        synchronized (map) {
+            assertTrue(entrySet.isEmpty());
+            assertTrue(keySet.isEmpty());
+            assertTrue(values.isEmpty());
+        }
+        map.put("a", "b");
+        map.put("c", "d");
+        assertEquals(2, map.size());
+        Thread.sleep(100L);
+        // values view access triggers expiration
+        synchronized (map) {
+            assertTrue(entrySet.isEmpty());
+            assertTrue(keySet.isEmpty());
+            assertTrue(values.isEmpty());
+        }
+    }
+
+    @Test
+    void testCollectionViewIteratorExpiration() throws InterruptedException {
+        final PassiveExpiringMap<String, String> map = new 
PassiveExpiringMap<>(50L);
+        map.put("a", "b");
+
+        final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
+        final Collection<String> keySet = map.keySet();
+        final Collection<String> values = map.values();
+
+        Thread.sleep(100L);
+
+        // The iterators should trigger expiration and not return any elements
+        assertFalse(entrySet.iterator().hasNext());
+        assertFalse(keySet.iterator().hasNext());
+        assertFalse(values.iterator().hasNext());
+    }
+
+    @Test
+    void testCollectionViewNullInputs() {
+        final PassiveExpiringMap<String, String> map = new 
PassiveExpiringMap<>(10000L);
+        map.put("a", "b");
+        // entrySet
+        assertThrows(NullPointerException.class, () -> 
map.entrySet().removeAll(null));
+        assertThrows(NullPointerException.class, () -> 
map.entrySet().retainAll(null));
+        // keySet
+        assertThrows(NullPointerException.class, () -> 
map.keySet().removeAll(null));
+        assertThrows(NullPointerException.class, () -> 
map.keySet().retainAll(null));
+        // values
+        assertThrows(NullPointerException.class, () -> 
map.values().removeAll(null));
+        assertThrows(NullPointerException.class, () -> 
map.values().retainAll(null));
+    }
+
+    @Test
+    void testCollectionViewRemoval() {
+        final PassiveExpiringMap<String, String> map = new 
PassiveExpiringMap<>(10000L);
+        map.put("a", "b");
+        map.put("c", "d");
+        map.put("e", "f");
+        // Remove via entrySet iterator
+        final Iterator<Map.Entry<String, String>> entryIter = 
map.entrySet().iterator();
+        assertTrue(entryIter.hasNext());
+        final Map.Entry<String, String> entry = entryIter.next();
+        final String removedKey = entry.getKey();
+        entryIter.remove();
+        assertFalse(map.containsKey(removedKey));
+        // Remove via keySet iterator
+        final Iterator<String> keyIter = map.keySet().iterator();
+        assertTrue(keyIter.hasNext());
+        final String key = keyIter.next();
+        keyIter.remove();
+        assertFalse(map.containsKey(key));
+        // Remove via values iterator
+        final Iterator<String> valIter = map.values().iterator();
+        assertTrue(valIter.hasNext());
+        final String val = valIter.next();
+        valIter.remove();
+        assertFalse(map.containsValue(val));
+    }
+
     @Test
     void testConstructors() {
         assertThrows(NullPointerException.class, () -> {
@@ -190,166 +299,6 @@ public class PassiveExpiringMapTest<K, V> extends 
AbstractMapTest<PassiveExpirin
         validateExpiration(new PassiveExpiringMap<>(new 
PassiveExpiringMap.ConstantTimeToLiveExpirationPolicy<>(1, TimeUnit.SECONDS)), 
1000);
     }
 
-    @Test
-    void testGet() {
-        final Map<Integer, String> m = makeTestMap();
-        assertNull(m.get(Integer.valueOf(1)));
-        assertEquals("two", m.get(Integer.valueOf(2)));
-        assertNull(m.get(Integer.valueOf(3)));
-        assertEquals("four", m.get(Integer.valueOf(4)));
-        assertNull(m.get(Integer.valueOf(5)));
-        assertEquals("six", m.get(Integer.valueOf(6)));
-    }
-
-    @Test
-    void testIsEmpty() {
-        Map<Integer, String> m = makeTestMap();
-        assertFalse(m.isEmpty());
-
-        // remove just evens
-        m = makeTestMap();
-        m.remove(Integer.valueOf(2));
-        m.remove(Integer.valueOf(4));
-        m.remove(Integer.valueOf(6));
-        assertTrue(m.isEmpty());
-    }
-
-    @Test
-    void testKeySet() {
-        final Map<Integer, String> m = makeTestMap();
-        assertEquals(3, m.size());
-    }
-
-    @Test
-    void testPut() {
-        final Map<Integer, String> m = makeTestMap();
-        assertNull(m.put(Integer.valueOf(1), "ONE"));
-        assertEquals("two", m.put(Integer.valueOf(2), "TWO"));
-        assertNull(m.put(Integer.valueOf(3), "THREE"));
-        assertEquals("four", m.put(Integer.valueOf(4), "FOUR"));
-        assertNull(m.put(Integer.valueOf(5), "FIVE"));
-        assertEquals("six", m.put(Integer.valueOf(6), "SIX"));
-    }
-
-    @Test
-    void testSize() {
-        final Map<Integer, String> m = makeTestMap();
-        assertEquals(3, m.size());
-    }
-
-    @Test
-    void testValues() {
-        final Map<Integer, String> m = makeTestMap();
-        assertEquals(3, m.size());
-    }
-
-    @Test
-    void testZeroTimeToLive() {
-        // item should not be available
-        final PassiveExpiringMap<String, String> m = new 
PassiveExpiringMap<>(0L);
-        m.put("a", "b");
-        assertNull(m.get("a"));
-    }
-
-    private void validateExpiration(final Map<String, String> map, final long 
timeout) throws InterruptedException {
-        map.put("a", "b");
-        assertNotNull(map.get("a"));
-        Thread.sleep(2 * timeout);
-        assertNull(map.get("a"));
-    }
-
-    @Test
-    void testCollectionsSynchronizedMapExpiration() throws 
InterruptedException {
-        final Map<String, String> map = Collections.synchronizedMap(new 
PassiveExpiringMap<>(50L));
-        map.put("a", "b");
-        map.put("c", "d");
-        assertEquals(2, map.size());
-        // Cache the views in SynchronizedMap before they expire
-        final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
-        final Collection<String> keySet = map.keySet();
-        final Collection<String> values = map.values();
-        Thread.sleep(100L);
-        // entrySet view access triggers expiration
-        synchronized (map) {
-            assertTrue(entrySet.isEmpty());
-            assertTrue(keySet.isEmpty());
-            assertTrue(values.isEmpty());
-        }
-        map.put("a", "b");
-        map.put("c", "d");
-        assertEquals(2, map.size());
-        Thread.sleep(100L);
-        // keySet view access triggers expiration
-        synchronized (map) {
-            assertTrue(entrySet.isEmpty());
-            assertTrue(keySet.isEmpty());
-            assertTrue(values.isEmpty());
-        }
-        map.put("a", "b");
-        map.put("c", "d");
-        assertEquals(2, map.size());
-        Thread.sleep(100L);
-        // values view access triggers expiration
-        synchronized (map) {
-            assertTrue(entrySet.isEmpty());
-            assertTrue(keySet.isEmpty());
-            assertTrue(values.isEmpty());
-        }
-    }
-
-    @Test
-    void testCollectionViewRemoval() {
-        final PassiveExpiringMap<String, String> map = new 
PassiveExpiringMap<>(10000L);
-        map.put("a", "b");
-        map.put("c", "d");
-        map.put("e", "f");
-        // Remove via entrySet iterator
-        final Iterator<Map.Entry<String, String>> entryIter = 
map.entrySet().iterator();
-        assertTrue(entryIter.hasNext());
-        final Map.Entry<String, String> entry = entryIter.next();
-        final String removedKey = entry.getKey();
-        entryIter.remove();
-        assertFalse(map.containsKey(removedKey));
-        // Remove via keySet iterator
-        final Iterator<String> keyIter = map.keySet().iterator();
-        assertTrue(keyIter.hasNext());
-        final String key = keyIter.next();
-        keyIter.remove();
-        assertFalse(map.containsKey(key));
-        // Remove via values iterator
-        final Iterator<String> valIter = map.values().iterator();
-        assertTrue(valIter.hasNext());
-        final String val = valIter.next();
-        valIter.remove();
-        assertFalse(map.containsValue(val));
-    }
-
-    @Test
-    void testCollectionViewNullInputs() {
-        final PassiveExpiringMap<String, String> map = new 
PassiveExpiringMap<>(10000L);
-        map.put("a", "b");
-        // entrySet
-        assertThrows(NullPointerException.class, () -> 
map.entrySet().removeAll(null));
-        assertThrows(NullPointerException.class, () -> 
map.entrySet().retainAll(null));
-        // keySet
-        assertThrows(NullPointerException.class, () -> 
map.keySet().removeAll(null));
-        assertThrows(NullPointerException.class, () -> 
map.keySet().retainAll(null));
-        // values
-        assertThrows(NullPointerException.class, () -> 
map.values().removeAll(null));
-        assertThrows(NullPointerException.class, () -> 
map.values().retainAll(null));
-    }
-
-    @SuppressWarnings("unchecked")
-    private Map<Object, Long> getExpirationMap(final PassiveExpiringMap<?, ?> 
map) {
-        try {
-            final java.lang.reflect.Field field = 
PassiveExpiringMap.class.getDeclaredField("expirationMap");
-            field.setAccessible(true);
-            return (Map<Object, Long>) field.get(map);
-        } catch (final Exception e) {
-            throw new RuntimeException(e);
-        }
-    }
-
     @Test
     void testExpirationMapCleanup() {
         final PassiveExpiringMap<String, String> map = new 
PassiveExpiringMap<>(10000L);
@@ -423,20 +372,71 @@ public class PassiveExpiringMapTest<K, V> extends 
AbstractMapTest<PassiveExpirin
     }
 
     @Test
-    void testCollectionViewIteratorExpiration() throws InterruptedException {
-        final PassiveExpiringMap<String, String> map = new 
PassiveExpiringMap<>(50L);
-        map.put("a", "b");
+    void testGet() {
+        final Map<Integer, String> m = makeTestMap();
+        assertNull(m.get(Integer.valueOf(1)));
+        assertEquals("two", m.get(Integer.valueOf(2)));
+        assertNull(m.get(Integer.valueOf(3)));
+        assertEquals("four", m.get(Integer.valueOf(4)));
+        assertNull(m.get(Integer.valueOf(5)));
+        assertEquals("six", m.get(Integer.valueOf(6)));
+    }
 
-        final Collection<Map.Entry<String, String>> entrySet = map.entrySet();
-        final Collection<String> keySet = map.keySet();
-        final Collection<String> values = map.values();
+    @Test
+    void testIsEmpty() {
+        Map<Integer, String> m = makeTestMap();
+        assertFalse(m.isEmpty());
 
-        Thread.sleep(100L);
+        // remove just evens
+        m = makeTestMap();
+        m.remove(Integer.valueOf(2));
+        m.remove(Integer.valueOf(4));
+        m.remove(Integer.valueOf(6));
+        assertTrue(m.isEmpty());
+    }
 
-        // The iterators should trigger expiration and not return any elements
-        assertFalse(entrySet.iterator().hasNext());
-        assertFalse(keySet.iterator().hasNext());
-        assertFalse(values.iterator().hasNext());
+    @Test
+    void testKeySet() {
+        final Map<Integer, String> m = makeTestMap();
+        assertEquals(3, m.size());
+    }
+
+    @Test
+    void testPut() {
+        final Map<Integer, String> m = makeTestMap();
+        assertNull(m.put(Integer.valueOf(1), "ONE"));
+        assertEquals("two", m.put(Integer.valueOf(2), "TWO"));
+        assertNull(m.put(Integer.valueOf(3), "THREE"));
+        assertEquals("four", m.put(Integer.valueOf(4), "FOUR"));
+        assertNull(m.put(Integer.valueOf(5), "FIVE"));
+        assertEquals("six", m.put(Integer.valueOf(6), "SIX"));
+    }
+
+    @Test
+    void testSize() {
+        final Map<Integer, String> m = makeTestMap();
+        assertEquals(3, m.size());
+    }
+
+    @Test
+    void testValues() {
+        final Map<Integer, String> m = makeTestMap();
+        assertEquals(3, m.size());
+    }
+
+    @Test
+    void testZeroTimeToLive() {
+        // item should not be available
+        final PassiveExpiringMap<String, String> m = new 
PassiveExpiringMap<>(0L);
+        m.put("a", "b");
+        assertNull(m.get("a"));
+    }
+
+    private void validateExpiration(final Map<String, String> map, final long 
timeout) throws InterruptedException {
+        map.put("a", "b");
+        assertNotNull(map.get("a"));
+        Thread.sleep(2 * timeout);
+        assertNull(map.get("a"));
     }
 
 }

Reply via email to