From 1e6d49414b7b7c1abecd354defce3adf5bc6f14d Mon Sep 17 00:00:00 2001 From: mdb Date: Fri, 17 Jan 2003 00:40:45 +0000 Subject: [PATCH] Added ability for LRU hash map to support computed "sizes" for items which will be accounted for when keeping the map under its target "size". Had to rearchitect it to delegate to the LinkedHashMap because HashMap doesn't expose the necessary helper methods to allow us to accomplish what we need as a derived class. Additionally, the keySet(), values() and entrySet() methods return read-only collections because supporting modification through those interfaces would dramatically increase complexity. git-svn-id: https://samskivert.googlecode.com/svn/trunk@1026 6335cc39-0255-0410-8fd6-9bcaacd3b74c --- .../java/com/samskivert/util/LRUHashMap.java | 166 ++++++++++++++++-- .../com/samskivert/util/LRUHashMapTest.java | 57 ++++++ 2 files changed, 212 insertions(+), 11 deletions(-) create mode 100644 projects/samskivert/tests/src/java/com/samskivert/util/LRUHashMapTest.java 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(); + } +}