
org.clapper.util.misc.LRUMap Maven / Gradle / Ivy
Show all versions of javautil Show documentation
package org.clapper.util.misc;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.io.Serializable;
import java.util.Collection;
/**
* An LRUMap implements a Map of a fixed maximum size
* that enforces a least recently used discard policy. When the
* LRUMap is full (i.e., contains the maximum number of entries),
* any attempt to insert a new entry causes one of the least recently used
* entries to be discarded.
*
* Note:
*
*
* - The put() method "touches" (or "refreshes") an object,
* making it "new" again, even if it simply replaces the value for
* an existing key.
*
- The get() method also refreshes the retrieved object,
* but the iterator(), containsValue() and
* containsKey() methods do not refresh the objects in the
* cache.
*
- This implementation is not synchronized. If multiple threads
* access this map concurrently, and at least one of the threads
* modifies the map structurally, it must be synchronized externally.
* Unlike other Map implementations, even a simple
* get() operation structurally modifies an LRUMap.
* Synchronization can be accomplished by synchronizing on some
* object that naturally encapsulates the map. If no such object
* exists, the map should be "wrapped" using the
* Collections.synchronizedMap() method. This is best done
* at creation time, to prevent accidental unsynchronized access to
* the map:
*
Map m = Collections.synchronizedMap (new LRUMap (...));
*
*
* There are other, similar implementations. For instance, see the
* LRUMap
* class in the
* Apache Jakarta Commons Collection
* API. (This leads to the obvious question: Why write another one? The primary
* answer is that I did not want to add another third-party library dependency.
* Plus, I wanted to experiment with this algorithm.)
*/
public class LRUMap
extends AbstractMap
implements Cloneable, Serializable
{
/*----------------------------------------------------------------------*\
Public Constants
\*----------------------------------------------------------------------*/
/**
* The default load factor, if one isn't specified to the constructor
*/
public static final float DEFAULT_LOAD_FACTOR = 0.75f;
/**
* The default initial capacity, if one isn't specified to the
* constructor
*/
public static final int DEFAULT_INITIAL_CAPACITY = 16;
/*----------------------------------------------------------------------*\
Inner Classes
\*----------------------------------------------------------------------*/
/**
* Set of Map.Entry (really, LRULinkedListEntry) objects returned by
* the LRUMap.entrySet() method.
*/
private class EntrySet extends AbstractSet>
{
private EntrySet()
{
// Nothing to do
}
public Iterator> iterator()
{
return new Iterator>()
{
EntryIterator it = new EntryIterator();
public Map.Entry next()
{
return (Map.Entry) it.next();
}
public boolean hasNext()
{
return it.hasNext();
}
public void remove()
{
it.remove();
}
};
}
public boolean contains (Object o)
{
boolean has = false;
if (o instanceof Map.Entry)
{
Map.Entry e = (Map.Entry) o;
Object key = e.getKey();
has = LRUMap.this.containsKey (key);
}
return has;
}
public boolean remove (Object o)
{
return (LRUMap.this.remove (o) != null);
}
public int size()
{
return LRUMap.this.size();
}
public void clear()
{
LRUMap.this.clear();
}
}
/**
* Iterator returned by EntrySet.iterator()
*/
private class EntryIterator implements Iterator
{
private LRULinkedListEntry current;
EntryIterator()
{
current = lruQueue.head;
}
public LRULinkedListEntry next()
{
LRULinkedListEntry result = current;
current = current.next;
return result;
}
public boolean hasNext()
{
return (current != null);
}
public void remove()
{
throw new UnsupportedOperationException();
}
}
/**
* Set of key objects returned by the LRUMap.keySet() method.
*/
private class KeySet extends AbstractSet
{
private KeySet()
{
// Nothing to do
}
public Iterator iterator()
{
return new KeySetIterator();
}
public boolean contains (Object key)
{
return LRUMap.this.containsKey (key);
}
public boolean remove (Object key)
{
return (LRUMap.this.remove (key) != null);
}
public int size()
{
return LRUMap.this.size();
}
public void clear()
{
LRUMap.this.clear();
}
}
/**
* Iterator returned by KeySet.iterator()
*/
private class KeySetIterator implements Iterator
{
private LRULinkedListEntry current;
KeySetIterator()
{
current = lruQueue.head;
}
public K next()
{
LRULinkedListEntry result = current;
current = current.next;
return result.key;
}
public boolean hasNext()
{
return (current != null);
}
public void remove()
{
throw new UnsupportedOperationException();
}
}
/**
* Shallow set that implements a set of values backed by the map.
*/
private class ValueSet extends AbstractSet
{
private ValueSet()
{
// Nothing to do
}
public void clear()
{
throw new UnsupportedOperationException();
}
public boolean contains (Object o)
{
return LRUMap.this.containsValue(o);
}
public boolean containsAll (Collection c)
{
boolean contains = true;
for (Object o : c)
{
if (! contains(o))
{
contains = false;
break;
}
}
return contains;
}
public boolean isEmpty()
{
return LRUMap.this.isEmpty();
}
public Iterator iterator()
{
return new ValueSetIterator();
}
public boolean remove (Object o)
{
throw new UnsupportedOperationException();
}
public int size()
{
return LRUMap.this.size();
}
}
/**
* Iterator returned by ValueSet.iterator()
*/
private class ValueSetIterator implements Iterator
{
private LRULinkedListEntry current;
ValueSetIterator()
{
current = lruQueue.head;
}
public V next()
{
LRULinkedListEntry result = current;
current = current.next;
return result.value;
}
public boolean hasNext()
{
return (current != null);
}
public void remove()
{
throw new UnsupportedOperationException();
}
}
/**
* Entry in the internal linked list (queue) of LRU entries. Implements
* Map.Entry for convenience.
*/
private final class LRULinkedListEntry implements Map.Entry
{
LRULinkedListEntry previous = null;
LRULinkedListEntry next = null;
K key = null;
V value = null;
LRULinkedListEntry (K key, V value)
{
setKeyValue (key, value);
}
public boolean equals (Object o)
{
return LRULinkedListEntry.class.isInstance (o);
}
public int hashCode()
{
return key.hashCode();
}
public K getKey()
{
return key;
}
public V getValue()
{
return value;
}
void setKeyValue (K key, V value)
{
this.key = key;
this.value = value;
}
public V setValue (V value)
{
V oldValue = this.value;
this.value = value;
return oldValue;
}
}
/**
* Internal linked list that implements the LRU queue. Each entry in the
* hash map of objects also points to one of these, allowing the hash map
* to remain untouched even as the linked list entries get reordered.
*/
private class LRULinkedList
{
LRULinkedListEntry head = null;
LRULinkedListEntry tail = null;
int size = 0;
private LRULinkedList()
{
// Nothing to do
}
protected void finalize()
throws Throwable
{
clear();
super.finalize();
}
void addToTail (LRULinkedListEntry entry)
{
entry.next = null;
entry.previous = tail;
if (head == null)
{
head = entry;
tail = entry;
}
else
{
entry.previous = tail;
tail.next = entry;
}
size++;
}
void addToHead (LRULinkedListEntry entry)
{
entry.next = null;
entry.previous = null;
if (head == null)
{
assert (tail == null);
head = entry;
tail = entry;
}
else
{
entry.next = head;
head.previous = entry;
head = entry;
}
size++;
}
void remove (LRULinkedListEntry entry)
{
if (entry.next != null)
entry.next.previous = entry.previous;
if (entry.previous != null)
entry.previous.next = entry.next;
if (entry == head) // NOPMD (legal reference comparison)
head = entry.next;
if (entry == tail) // NOPMD (legal reference comparison)
tail = entry.previous;
entry.next = null;
entry.previous = null;
size--;
assert (size >= 0);
}
LRULinkedListEntry removeTail()
{
LRULinkedListEntry result = tail;
if (result != null)
remove (result);
return result;
}
void moveToHead (LRULinkedListEntry entry)
{
remove (entry);
addToHead (entry);
}
void clear()
{
while (head != null)
{
LRULinkedListEntry next = head.next;
head.next = null;
head.previous = null;
head.key = null;
head.value = null;
head = next;
}
tail = null;
size = 0;
}
}
/**
* Wraps any ObjectRemovalListener passed into addRemovalListener().
* Keeps track of both the listener and its "automaticOnly" status
*/
private static class RemovalListenerWrapper
implements ObjectRemovalListener
{
boolean automaticOnly;
ObjectRemovalListener realListener;
RemovalListenerWrapper (ObjectRemovalListener realListener,
boolean automaticOnly)
{
this.realListener = realListener;
this.automaticOnly = automaticOnly;
}
public void objectRemoved (ObjectRemovalEvent event)
{
realListener.objectRemoved (event);
}
}
/*----------------------------------------------------------------------*\
Private Static Variables
\*----------------------------------------------------------------------*/
/**
* See JDK 1.5 version of java.io.Serializable
*/
private static final long serialVersionUID = 1L;
/*----------------------------------------------------------------------*\
Type Aliases
\*----------------------------------------------------------------------*/
/**
* Type alias
*/
private class ListenerMap extends HashMap
{
ListenerMap()
{
super();
}
}
/**
* Type alias for actual hash table
*/
private class EntryMap extends HashMap
{
EntryMap (int initialCapacity, float loadFactor)
{
super (initialCapacity, loadFactor);
}
}
/*----------------------------------------------------------------------*\
Private Variables
\*----------------------------------------------------------------------*/
private int maxCapacity;
private float loadFactor;
private int initialCapacity;
private EntryMap hash;
private LRULinkedList lruQueue;
private ListenerMap removalListeners = null;
/*----------------------------------------------------------------------*\
Constructors
\*----------------------------------------------------------------------*/
/**
* Construct a new empty map with a default capacity and load factor,
* and the specified maximum capacity.
*
* @param maxCapacity the maximum number of entries permitted in the
* map. Must not be negative.
*/
public LRUMap (int maxCapacity)
{
this (DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, maxCapacity);
}
/**
* Construct a new empty map with the specified initial capacity,
* a default load factor, and the specified maximum capacity.
*
* @param initialCapacity the initial capacity
* @param maxCapacity the maximum number of entries permitted in the
* map. Must not be negative.
*/
public LRUMap (int initialCapacity, int maxCapacity)
{
this (initialCapacity, DEFAULT_LOAD_FACTOR, maxCapacity);
}
/**
* Constructs a new, empty map with the specified initial capacity,
* load factor, and maximum capacity.
*
* @param initialCapacity the initial capacity
* @param loadFactor the load factor
* @param maxCapacity the maximum number of entries permitted in the
* map. Must not be negative.
*/
public LRUMap (int initialCapacity, float loadFactor, int maxCapacity)
{
assert (maxCapacity > 0);
assert (loadFactor > 0.0);
assert (initialCapacity > 0);
if (initialCapacity > maxCapacity)
initialCapacity = maxCapacity;
this.maxCapacity = maxCapacity;
this.loadFactor = loadFactor;
this.initialCapacity = initialCapacity;
this.hash = new EntryMap (initialCapacity, loadFactor);
this.lruQueue = new LRULinkedList();
}
/**
* Constructs a new map with the same mappings and parameters as the
* given LRUMap. The initial capacity and load factor is
* the same as for the parent HashMap class. The insertion
* order of the keys is preserved.
*
* @param map the map whose mappings are to be copied
*/
public LRUMap (LRUMap map)
{
this (map.initialCapacity, map.loadFactor, map.maxCapacity);
doPutAll (map);
}
/*----------------------------------------------------------------------*\
Public Methods
\*----------------------------------------------------------------------*/
/**
* Add an EventListener that will be called whenever an
* object is removed from the cache. If automaticOnly is
* true, then the listener is only notified for objects that
* are removed automatically when the cache needs to be cleared to make
* room for new objects. If automaticOnly is false,
* then the listener is notified whenever an object is removed for any
* reason, include a call to the {@link #remove remove()} method.
*
* Note that when this map announces the removal of an object, it
* passes an {@link ObjectRemovalEvent} that contains a
* java.util.Map.Entry object that wraps the actual object
* that was removed. That way, both the key and the value are available
* for the removed object.
*
* @param listener the listener to add
* @param automaticOnly see above
*
* @see #removeRemovalListener
*/
public synchronized void
addRemovalListener (ObjectRemovalListener listener, boolean automaticOnly)
{
if (removalListeners == null)
removalListeners = new ListenerMap();
removalListeners.put (listener,
new RemovalListenerWrapper (listener,
automaticOnly));
}
/**
* Remove an EventListener from the set of listeners to be invoked
* when an object is removed from the cache.
*
* @param listener the listener to add
*
* @return true if the listener was in the list and was removed,
* false otherwise
*
* @see #addRemovalListener
*/
public synchronized boolean
removeRemovalListener (ObjectRemovalListener listener)
{
boolean removed = false;
if ((removalListeners != null) &&
(removalListeners.remove(listener) != null))
{
removed = true;
}
return removed;
}
/**
* Remove all mappings from this map.
*/
public void clear()
{
hash.clear();
lruQueue.clear();
}
/**
* Determine whether this map contains a mapping for a given key. Note
* that this implementation of containsKey() does not refresh
* the object in the cache.
*
* @param key the key to find
*
* @return true if the key is in the map, false if not
*/
public boolean containsKey (Object key)
{
return hash.containsKey (key);
}
/**
* Determine whether this map contains a given value. Note that this
* implementation of containsValue() does not refresh the
* objects in the cache.
*
* @param value the value to find
*
* @return true if the value is in the map, false if not
*/
public boolean containsValue (Object value)
{
boolean contains = false;
for (LRULinkedListEntry entry : hash.values())
{
if (entry.getValue().equals(value))
{
contains = true;
break;
}
}
return contains;
}
/**
* Get a set view of the mappings in this map. Each element in this set
* is a Map.Entry. The collection is backed by the map, so
* changes to the map are reflected in the collection, and vice-versa.
* The collection supports element removal, which removes the
* corresponding mapping from the map, via the
* Iterator.remove, Collection.remove,
* removeAll, retainAll, and clear
* operations. It does not support the add or addAll
* operations.
*
* @return the entry set
*/
public Set> entrySet()
{
return new EntrySet();
}
/**
* Retrieve an object from the map. Retrieving an object from an
* LRU map "refreshes" the object so that it is among the most recently
* used objects.
*
* @param key the object's key in the map.
*
* @return the associated object, or null if not found
*/
public V get (Object key)
{
V value = null;
LRULinkedListEntry entry = (LRULinkedListEntry) hash.get (key);
if (entry != null)
{
// It's there. It's just been accessed, so move it to the
// top of the linked list.
assert (entry.key.equals (key)) :
"entry.key=" + entry.key + ", key=" + key;
lruQueue.moveToHead (entry);
value = entry.value;
}
return value;
}
/**
* Get the initial capacity of this LRUMap.
*
* @return the initial capacity, as passed to the constructor
*
* @see #getLoadFactor
* @see #getMaximumCapacity
*/
public int getInitialCapacity()
{
return initialCapacity;
}
/**
* Get the load factor for this LRUMap.
*
* @return the load factor, as passed to the constructor
*
* @see #getInitialCapacity
* @see #getMaximumCapacity
*/
public float getLoadFactor()
{
return loadFactor;
}
/**
* Get the maximum capacity of this LRUMap.
*
* @return the maximum capacity, as passed to the constructor
*
* @see #setMaximumCapacity
* @see #getLoadFactor
* @see #getInitialCapacity
*/
public int getMaximumCapacity()
{
return maxCapacity;
}
/**
* Determine whether this map is empty or not.
*
* @return true if the map has no mappings, false
* otherwise
*/
public boolean isEmpty()
{
return hash.isEmpty();
}
/**
* Return a Set view of the keys in the map. The set is
* backed by the map, so changes to the map are reflected in the set,
* and vice-versa. The set does supports element removal, which removes
* the corresponding mapping from this map; however, the
* Iterator returned by the set currently does not
* support element removal. The set does not support the add
* or addAll operations.
*
* @return the set of keys in this map
*/
public Set keySet()
{
return new KeySet();
}
/**
* Associates the specified value with the specified key in this map.
* If the key already has a value in this map, the existing value is
* replaced by the new value, and the old value is replaced. If the key
* already exists in the map, it is moved to the end of the key
* insertion order list.
*
* @param key the key with which the specified value is to be associated
* @param value the value to associate with the specified key
*
* @return the previous value associated with the key, or null if (a) there
* was no previous value, or (b) the previous value was a null
*/
public V put (K key, V value)
{
return doPut (key, value);
}
/**
* Copies all of the mappings from a specified map to this one. These
* mappings replace any mappings that this map had for any of the keys.
*
* @param map the map whose mappings are to be copied
*/
public void putAll (Map map)
{
doPutAll(map);
}
/**
* Removes the mapping for a key, if there is one.
*
* @param key the key to remove
*
* @return the previous value associated with the key, or null if (a) there
* was no previous value, or (b) the previous value was a null
*/
public V remove (Object key)
{
V value = null;
LRULinkedListEntry entry = (LRULinkedListEntry) hash.remove (key);
if (entry != null)
{
value = entry.value;
lruQueue.remove (entry);
callRemovalListeners (key, value, false);
}
assert (hash.size() == lruQueue.size);
return value;
}
/**
* Set or change the maximum capacity of this LRUMap. If the
* maximum capacity is reduced to less than the map's current size,
* then the map is reduced in size by discarding the oldest entries.
*
* @param newCapacity the new maximum capacity
*
* @return the old maximum capacity
*
* @see #getMaximumCapacity
*/
public int setMaximumCapacity (int newCapacity)
{
assert (newCapacity > 0);
int oldCapacity = this.maxCapacity;
clearTo (newCapacity);
this.maxCapacity = newCapacity;
return oldCapacity;
}
/**
* Get the number of entries in the map. Note that this value can
* temporarily exceed the maximum capacity of the map. See the class
* documentation for details.
*
* @return the number of entries in the map
*/
public int size()
{
return lruQueue.size;
}
/**
*
Returns a collection view of the values contained in this map. The
* returned collection is a "thin" view of the values contained in
* this map. The collection contains proxies for the actual disk-resident
* values; the values themselves are not loaded until a
* Collection method such as contains() is called.
*
* The collection is backed by the map, so changes to the map are
* reflected in the set. If the map is modified while an iteration over
* the set is in progress, the results of the iteration are undefined.
* The set does not support any of the add() methods.
*
* Warning:: The toArray() methods can be dangerous,
* since they will attempt to load every value from the data file into
* an in-memory array.
*
* @return a collection view of the values contained in this map.
*
* @see #keySet
* @see #values
*/
public Collection values()
{
return new ValueSet();
}
/*----------------------------------------------------------------------*\
Protected Methods
\*----------------------------------------------------------------------*/
/**
* Returns a shallow copy of this instance. The keys and values themselves
* are not cloned.
*
* @return a shallow copy of this map
*
* @throws CloneNotSupportedException not cloneable
*/
protected Object clone()
throws CloneNotSupportedException
{
return new LRUMap (this);
}
/*----------------------------------------------------------------------*\
Private Methods
\*----------------------------------------------------------------------*/
private LRULinkedListEntry clearTo (int size)
{
assert (hash.size() == lruQueue.size);
LRULinkedListEntry oldTail = null;
while (lruQueue.size > size)
{
oldTail = lruQueue.removeTail();
assert (oldTail != null);
Object key = oldTail.key;
LRULinkedListEntry rem = (LRULinkedListEntry) hash.remove (key);
assert (rem != null);
assert (rem.key == key);
callRemovalListeners (key, rem.value, true);
}
assert (lruQueue.size <= size);
assert (hash.size() == lruQueue.size);
return oldTail;
}
private synchronized void callRemovalListeners (final Object key,
final Object value,
boolean automatic)
{
if (removalListeners != null)
{
for (Iterator it = removalListeners.values().iterator();
it.hasNext(); )
{
RemovalListenerWrapper l = (RemovalListenerWrapper) it.next();
if ((! automatic) && (l.automaticOnly))
continue;
Map.Entry entry = new Map.Entry()
{
public boolean equals (Object o)
{
return false;
}
public Object getKey()
{
return key;
}
public Object getValue()
{
return value;
}
public int hashCode()
{
return key.hashCode();
}
public Object setValue (Object val)
{
return null;
}
};
l.objectRemoved (new ObjectRemovalEvent (entry));
}
}
}
/**
* Actual implementation of putAll(). Extracted to a private method
* so it can be called from the constructor.
*
* @param map the map from which to extract values
*/
private void doPutAll(final Map map)
{
for (Iterator it = map.keySet().iterator();
it.hasNext(); )
{
K key = it.next();
V value = map.get (key);
doPut (key, value);
}
}
private V doPut(final K key, final V value)
{
// If the total number of entries is at capacity, then we need to
// remove one of them to make room. The linked list is a priority
// queue, of sorts, with least recently used items at the end. So
// remove the tail entries.
V oldValue = null;
LRULinkedListEntry entry = (LRULinkedListEntry) hash.get (key);
if (entry == null)
{
// Must add a new one. Clear out the cruft. Reuse the last
// cleared entry, though, rather than allocate a new object.
entry = clearTo (this.maxCapacity - 1);
if (entry == null)
entry = new LRULinkedListEntry (key, value);
else
entry.setKeyValue (key, value);
lruQueue.addToHead (entry);
hash.put (key, entry);
}
else
{
// We're replacing the value with a new one. Move the entry to
// the head of the list.
oldValue = entry.value;
entry.value = value;
lruQueue.moveToHead (entry);
}
return oldValue;
}
}