diff --git a/projects/samskivert/src/java/com/samskivert/util/LRUHashMap.java b/projects/samskivert/src/java/com/samskivert/util/LRUHashMap.java index b465cf37..bd7893f3 100644 --- a/projects/samskivert/src/java/com/samskivert/util/LRUHashMap.java +++ b/projects/samskivert/src/java/com/samskivert/util/LRUHashMap.java @@ -1,25 +1,56 @@ // -// $Id: LRUHashMap.java,v 1.1 2002/10/18 01:43:46 ray Exp $ +// $Id: LRUHashMap.java,v 1.2 2003/01/17 00:40:45 mdb Exp $ package com.samskivert.util; +import java.util.Collection; +import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; +import java.util.Set; /** * A HashMap with LRU functionality and rudimentary performance tracking * facilities. */ -public class LRUHashMap extends LinkedHashMap +public class LRUHashMap implements Map { /** - * Construct a LRUHashMap with the specified maximum size. + * Used to return the "size" of a cache item for systems that wish to + * differentiate cache items based on memory footprint or other + * metric. The size will be used to scale up the size of a cache entry + * such that when the specified number of units is exceeded, the least + * recently used items will be flushed until the cache is back below + * its target size. + */ + public static interface ItemSizer + { + /** Returns the "size" of the specified object. */ + public int computeSize (Object item); + } + + /** + * Construct a LRUHashMap with the specified maximum size. All items + * in the cache will be considered to have a size of one. */ public LRUHashMap (int maxSize) { - super(Math.min(1024, Math.max(16, maxSize)), .75f, true); + this(maxSize, null); + } + + /** + * Construct a LRUHashMap with the specified maximum total size and + * the supplied item sizer which will be used to compute the size of + * each item. + */ + public LRUHashMap (int maxSize, ItemSizer sizer) + { + _delegate = new LinkedHashMap( + Math.min(1024, Math.max(16, maxSize)), .75f, true); _maxSize = maxSize; + _sizer = (sizer == null) ? _unitSizer : sizer; } /** @@ -52,10 +83,34 @@ public class LRUHashMap extends LinkedHashMap return new int[] {_hits, _misses}; } - // documentation inherited + // documentation inherited from interface + public int size () + { + return _delegate.size(); + } + + // documentation inherited from interface + public boolean isEmpty () + { + return _delegate.isEmpty(); + } + + // documentation inherited from interface + public boolean containsKey (Object key) + { + return _delegate.containsKey(key); + } + + // documentation inherited from interface + public boolean containsValue (Object value) + { + return _delegate.containsValue(value); + } + + // documentation inherited from interface public Object get (Object key) { - Object result = super.get(key); + Object result = _delegate.get(key); if (_tracking) { if (result == null) { @@ -71,29 +126,118 @@ public class LRUHashMap extends LinkedHashMap return result; } - // documentation inherited + // documentation inherited from interface public Object put (Object key, Object value) { - Object result = super.put(key, value); + Object result = _delegate.put(key, value); if (_tracking) { _seenKeys.add(key); } + // updated our computed "size" + _size += _sizer.computeSize(value); + if (result != null) { + _size -= _sizer.computeSize(result); + } +// System.out.println("Added " + value + ": " + _size); + + // if we've exceeded our size, remove things until we're back + // under the required size; but don't freak out if we have one + // *really* big item + while (_size > _maxSize && size() > 1) { + key = keySet().iterator().next(); + remove(key); +// System.out.println("Removed " + key + ": " + _size); + } + return result; } - // documentation inherited - protected boolean removeEldestEntry (Map.Entry eldest) + // documentation inherited from interface + public Object remove (Object key) { - return size() > _maxSize; + Object removed = _delegate.remove(key); + if (removed != null) { + _size -= _sizer.computeSize(removed); + } + return removed; } + // documentation inherited from interface + public void putAll (Map t) + { + Iterator i = t.keySet().iterator(); + while (i.hasNext()) { + Object key = i.next(); + put(key, t.get(key)); + } + } + + // documentation inherited from interface + public void clear () + { + _delegate.clear(); + _size = 0; + } + + // documentation inherited from interface + public Set keySet () + { + // no modifying except through put() and remove() + return Collections.unmodifiableSet(_delegate.keySet()); + } + + // documentation inherited from interface + public Collection values () + { + // no modifying except through put() and remove() + return Collections.unmodifiableCollection(_delegate.values()); + } + + // documentation inherited from interface + public Set entrySet () + { + // no modifying except through put() and remove() + return Collections.unmodifiableSet(_delegate.entrySet()); + } + + // documentation inherited from interface + public boolean equals (Object o) + { + return _delegate.equals(o); + } + + // documentation inherited from interface + public int hashCode () + { + return _delegate.hashCode(); + } + + /** Since we can't override addEntry and removeEntryForKey in Sun's + * lovely collection classes, we have to delegate to a HashMap and + * reimplement a crapload of stuff so that we can provide our required + * size tracking support. Yay! I wish we could support code reuse as + * well as Sun does. */ + protected LinkedHashMap _delegate; + /** The maximum size of this cache. */ protected int _maxSize; + /** The current size of this cache. */ + protected int _size; + + /** Used to compute the size of items in this cache. */ + protected ItemSizer _sizer; /** Tracking info. */ protected boolean _tracking; protected HashSet _seenKeys; protected int _hits, _misses; + + /** Used for caches with no item sizer. */ + protected static final ItemSizer _unitSizer = new ItemSizer() { + public int computeSize (Object item) { + return 1; + } + }; } diff --git a/projects/samskivert/tests/src/java/com/samskivert/util/LRUHashMapTest.java b/projects/samskivert/tests/src/java/com/samskivert/util/LRUHashMapTest.java new file mode 100644 index 00000000..4fbf4133 --- /dev/null +++ b/projects/samskivert/tests/src/java/com/samskivert/util/LRUHashMapTest.java @@ -0,0 +1,57 @@ +// +// $Id: LRUHashMapTest.java,v 1.1 2003/01/17 00:40:45 mdb Exp $ + +package com.samskivert.util; + +import junit.framework.Test; +import junit.framework.TestCase; + +/** + * Tests the {@link LRUHashMap} class. + */ +public class LRUHashMapTest extends TestCase +{ + public LRUHashMapTest () + { + super(LRUHashMapTest.class.getName()); + } + + public void runTest () + { + LRUHashMap map = new LRUHashMap(10, new LRUHashMap.ItemSizer() { + public int computeSize (Object item) { + return ((Integer)item).intValue(); + } + }); + + map.put("one.1", new Integer(1)); + assertTrue("size == 1", map.size() == 1); + map.put("one.2", new Integer(1)); + assertTrue("size == 2", map.size() == 2); + map.put("one.3", new Integer(1)); + assertTrue("size == 3", map.size() == 3); + map.put("one.4", new Integer(1)); + assertTrue("size == 4", map.size() == 4); + map.put("one.5", new Integer(1)); + assertTrue("size == 5", map.size() == 5); + map.put("three.1", new Integer(3)); + assertTrue("size == 6", map.size() == 6); + map.put("five.1", new Integer(5)); + assertTrue("size == 4", map.size() == 4); + map.put("three.2", new Integer(3)); + assertTrue("size == 2", map.size() == 2); + map.put("three.3", new Integer(3)); + assertTrue("size == 2", map.size() == 2); + } + + public static Test suite () + { + return new LRUHashMapTest(); + } + + public static void main (String[] args) + { + LRUHashMapTest test = new LRUHashMapTest(); + test.runTest(); + } +}