SelfExpiringHashMap - a Java Map which entries expire automatically after a given time; it uses a DelayQueue internally.
Original from: https://gist.github.com/pcan/16faf4e59942678377e0
SelfExpiringHashMap - a Java Map which entries expire automatically after a given time; it uses a DelayQueue internally.
Original from: https://gist.github.com/pcan/16faf4e59942678377e0
/* | |
* Copyright (c) 2017 Pierantonio Cangianiello | |
* | |
* MIT License | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
import java.util.Collection; | |
import java.util.Map; | |
import java.util.Map.Entry; | |
import java.util.Set; | |
import java.util.WeakHashMap; | |
import java.util.concurrent.ConcurrentHashMap; | |
import java.util.concurrent.DelayQueue; | |
import java.util.concurrent.Delayed; | |
import java.util.concurrent.TimeUnit; | |
/** | |
* A thread-safe implementation of a HashMap which entries expires after the specified life time. | |
* The life-time can be defined on a per-key basis, or using a default one, that is passed to the | |
* constructor. | |
* | |
* @author Pierantonio Cangianiello | |
* @param <K> the Key type | |
* @param <V> the Value type | |
*/ | |
public class SelfExpiringHashMap<K, V> implements SelfExpiringMap<K, V> { | |
private final Map<K, V> internalMap; | |
private final Map<K, ExpiringKey<K>> expiringKeys; | |
/** | |
* Holds the map keys using the given life time for expiration. | |
*/ | |
private final DelayQueue<ExpiringKey> delayQueue = new DelayQueue<ExpiringKey>(); | |
/** | |
* The default max life time in milliseconds. | |
*/ | |
private final long maxLifeTimeMillis; | |
public SelfExpiringHashMap() { | |
internalMap = new ConcurrentHashMap<K, V>(); | |
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>(); | |
this.maxLifeTimeMillis = Long.MAX_VALUE; | |
} | |
public SelfExpiringHashMap(long defaultMaxLifeTimeMillis) { | |
internalMap = new ConcurrentHashMap<K, V>(); | |
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>(); | |
this.maxLifeTimeMillis = defaultMaxLifeTimeMillis; | |
} | |
public SelfExpiringHashMap(long defaultMaxLifeTimeMillis, int initialCapacity) { | |
internalMap = new ConcurrentHashMap<K, V>(initialCapacity); | |
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>(initialCapacity); | |
this.maxLifeTimeMillis = defaultMaxLifeTimeMillis; | |
} | |
public SelfExpiringHashMap(long defaultMaxLifeTimeMillis, int initialCapacity, float loadFactor) { | |
internalMap = new ConcurrentHashMap<K, V>(initialCapacity, loadFactor); | |
expiringKeys = new WeakHashMap<K, ExpiringKey<K>>(initialCapacity, loadFactor); | |
this.maxLifeTimeMillis = defaultMaxLifeTimeMillis; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public int size() { | |
cleanup(); | |
return internalMap.size(); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public boolean isEmpty() { | |
cleanup(); | |
return internalMap.isEmpty(); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public boolean containsKey(Object key) { | |
cleanup(); | |
return internalMap.containsKey((K) key); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public boolean containsValue(Object value) { | |
cleanup(); | |
return internalMap.containsValue((V) value); | |
} | |
@Override | |
public V get(Object key) { | |
cleanup(); | |
renewKey((K) key); | |
return internalMap.get((K) key); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public V put(K key, V value) { | |
return this.put(key, value, maxLifeTimeMillis); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public V put(K key, V value, long lifeTimeMillis) { | |
cleanup(); | |
ExpiringKey delayedKey = new ExpiringKey(key, lifeTimeMillis); | |
ExpiringKey oldKey = expiringKeys.put((K) key, delayedKey); | |
if(oldKey != null) { | |
expireKey(oldKey); | |
expiringKeys.put((K) key, delayedKey); | |
} | |
delayQueue.offer(delayedKey); | |
return internalMap.put(key, value); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public V remove(Object key) { | |
V removedValue = internalMap.remove((K) key); | |
expireKey(expiringKeys.remove((K) key)); | |
return removedValue; | |
} | |
/** | |
* Not supported. | |
*/ | |
@Override | |
public void putAll(Map<? extends K, ? extends V> m) { | |
throw new UnsupportedOperationException(); | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public boolean renewKey(K key) { | |
ExpiringKey<K> delayedKey = expiringKeys.get((K) key); | |
if (delayedKey != null) { | |
delayedKey.renew(); | |
return true; | |
} | |
return false; | |
} | |
private void expireKey(ExpiringKey<K> delayedKey) { | |
if (delayedKey != null) { | |
delayedKey.expire(); | |
cleanup(); | |
} | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public void clear() { | |
delayQueue.clear(); | |
expiringKeys.clear(); | |
internalMap.clear(); | |
} | |
/** | |
* Not supported. | |
*/ | |
@Override | |
public Set<K> keySet() { | |
throw new UnsupportedOperationException(); | |
} | |
/** | |
* Not supported. | |
*/ | |
@Override | |
public Collection<V> values() { | |
throw new UnsupportedOperationException(); | |
} | |
/** | |
* Not supported. | |
*/ | |
@Override | |
public Set<Entry<K, V>> entrySet() { | |
throw new UnsupportedOperationException(); | |
} | |
private void cleanup() { | |
ExpiringKey<K> delayedKey = delayQueue.poll(); | |
while (delayedKey != null) { | |
internalMap.remove(delayedKey.getKey()); | |
expiringKeys.remove(delayedKey.getKey()); | |
delayedKey = delayQueue.poll(); | |
} | |
} | |
private class ExpiringKey<K> implements Delayed { | |
private long startTime = System.currentTimeMillis(); | |
private final long maxLifeTimeMillis; | |
private final K key; | |
public ExpiringKey(K key, long maxLifeTimeMillis) { | |
this.maxLifeTimeMillis = maxLifeTimeMillis; | |
this.key = key; | |
} | |
public K getKey() { | |
return key; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public boolean equals(Object obj) { | |
if (obj == null) { | |
return false; | |
} | |
if (getClass() != obj.getClass()) { | |
return false; | |
} | |
final ExpiringKey<K> other = (ExpiringKey<K>) obj; | |
if (this.key != other.key && (this.key == null || !this.key.equals(other.key))) { | |
return false; | |
} | |
return true; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public int hashCode() { | |
int hash = 7; | |
hash = 31 * hash + (this.key != null ? this.key.hashCode() : 0); | |
return hash; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public long getDelay(TimeUnit unit) { | |
return unit.convert(getDelayMillis(), TimeUnit.MILLISECONDS); | |
} | |
private long getDelayMillis() { | |
return (startTime + maxLifeTimeMillis) - System.currentTimeMillis(); | |
} | |
public void renew() { | |
startTime = System.currentTimeMillis(); | |
} | |
public void expire() { | |
startTime = System.currentTimeMillis() - maxLifeTimeMillis - 1; | |
} | |
/** | |
* {@inheritDoc} | |
*/ | |
@Override | |
public int compareTo(Delayed that) { | |
return Long.compare(this.getDelayMillis(), ((ExpiringKey) that).getDelayMillis()); | |
} | |
} | |
} |
/* | |
* Copyright (c) 2017 Pierantonio Cangianiello | |
* | |
* MIT License | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
import it.pcan.util.map.SelfExpiringHashMap; | |
import it.pcan.util.map.SelfExpiringMap; | |
import static org.junit.Assert.*; | |
import org.junit.Test; | |
/** | |
* | |
* @author Pierantonio Cangianiello | |
*/ | |
public class SelfExpiringHashMapTests { | |
private final static int SLEEP_MULTIPLIER = 10; | |
@Test | |
public void basicGetTest() throws InterruptedException { | |
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>(); | |
map.put("a", "b", 2 * SLEEP_MULTIPLIER); | |
Thread.sleep(1 * SLEEP_MULTIPLIER); | |
assertEquals(map.get("a"), "b"); | |
} | |
@Test | |
public void basicExpireTest() throws InterruptedException { | |
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>(); | |
map.put("a", "b", 2 * SLEEP_MULTIPLIER); | |
Thread.sleep(3 * SLEEP_MULTIPLIER); | |
assertNull(map.get("a")); | |
} | |
@Test | |
public void basicRenewTest() throws InterruptedException { | |
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>(); | |
map.put("a", "b", 3 * SLEEP_MULTIPLIER); | |
Thread.sleep(2 * SLEEP_MULTIPLIER); | |
map.renewKey("a"); | |
Thread.sleep(2 * SLEEP_MULTIPLIER); | |
assertEquals(map.get("a"), "b"); | |
} | |
@Test | |
public void getRenewTest() throws InterruptedException { | |
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>(); | |
map.put("a", "b", 3 * SLEEP_MULTIPLIER); | |
Thread.sleep(2 * SLEEP_MULTIPLIER); | |
assertEquals(map.get("a"), "b"); | |
Thread.sleep(2 * SLEEP_MULTIPLIER); | |
assertEquals(map.get("a"), "b"); | |
} | |
@Test | |
public void multiplePutThenRemoveTest() throws InterruptedException { | |
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>(); | |
map.put("a", "b", 2 * SLEEP_MULTIPLIER); | |
Thread.sleep(1 * SLEEP_MULTIPLIER); | |
map.put("a", "c", 2 * SLEEP_MULTIPLIER); | |
Thread.sleep(1 * SLEEP_MULTIPLIER); | |
map.put("a", "d", 400 * SLEEP_MULTIPLIER); | |
Thread.sleep(2 * SLEEP_MULTIPLIER); | |
assertEquals(map.remove("a"), "d"); | |
} | |
@Test | |
public void multiplePutThenGetTest() throws InterruptedException { | |
SelfExpiringMap<String, String> map = new SelfExpiringHashMap<String, String>(); | |
map.put("a", "b", 2 * SLEEP_MULTIPLIER); | |
Thread.sleep(1 * SLEEP_MULTIPLIER); | |
map.put("a", "c", 2 * SLEEP_MULTIPLIER); | |
Thread.sleep(1 * SLEEP_MULTIPLIER); | |
map.put("a", "d", 400 * SLEEP_MULTIPLIER); | |
Thread.sleep(2 * SLEEP_MULTIPLIER); | |
assertEquals(map.get("a"), "d"); | |
} | |
} |
/* | |
* Copyright (c) 2017 Pierantonio Cangianiello | |
* | |
* MIT License | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ | |
import java.util.Map; | |
/** | |
* | |
* @author Pierantonio Cangianiello | |
* @param <K> the Key type | |
* @param <V> the Value type | |
*/ | |
public interface SelfExpiringMap<K, V> extends Map<K, V> { | |
/** | |
* Renews the specified key, setting the life time to the initial value. | |
* | |
* @param key | |
* @return true if the key is found, false otherwise | |
*/ | |
public boolean renewKey(K key); | |
/** | |
* Associates the given key to the given value in this map, with the specified life | |
* times in milliseconds. | |
* | |
* @param key | |
* @param value | |
* @param lifeTimeMillis | |
* @return a previously associated object for the given key (if exists). | |
*/ | |
public V put(K key, V value, long lifeTimeMillis); | |
} |