A least recently used (LRU) cache.
//package org.j4me.collections; import java.util.*; /** * A least recently used (LRU) cache. Data is * stored internally in a hashtable, which maps keys to values. Once * the cache is full and a new entry is added, the least recently used * entry is discarded. Therefore a cache is like a hashtable except * it stops growing at a certain point. * <p> * Any non-null object can be used as a key or as a value. * To successfully store and retrieve objects from a cache, the objects * used as keys must implement the hashCode method and the equals method. * * @see java.util.Hashtable */ public class Cache { /** * The maximum number of objects that can be stored in the cache. * When adding a new item and the cache already has this many items, * the least recently used will be removed from the cache. */ private int max; /** * The LRU cache. The key is always a <code>Terrain</code> object and the * data is an <code>Item</code>. The <code>Item</code> data structure maintains * a list of the order in which they are used. */ private Hashtable cache; /** * The most recently used cached <code>Item</code>. This will be <code>null</code> * only when the cache is empty. */ private Item mru; /** * The least recently used cached <code>Item</code>. This will be <code>null</code> * only when the cache is empty. */ private Item lru; /** * A data structure for each object stored in <code>cache</code>. It contains * the <code>key</code> and <code>data</code> used in any map. It also has pointers * to keep a list in order of how recently each <code>Item</code> object has * been accessed. */ private static final class Item { /** * The key used to lookup this <code>Item</code> in the hash table. */ public Object key; /** * The cached data associated with the <code>key</code>. */ public Object data; /** * The next in the list which is less recently used than this. */ public Item next; /** * The previous in the list which is more recently used than this. */ public Item previous; } /** * Constructs the cache. * * @param maxCapacity is the number of key/value pairs that can be stored * before adding new entries ejects the least recently used ones. */ public Cache (int maxCapacity) { cache = new Hashtable( maxCapacity * 2 ); // Adjust for load factor mru = null; lru = null; setMaxCapacity( maxCapacity ); } /** * Clears this cache so that it contains no keys. */ public void clear () { cache.clear(); mru = null; lru = null; } /** * Returns the number of keys in this cache. * * @return The number of keys in this cache. */ public int size () { return cache.size(); } /** * Returns the maximum number of keys that can be stored in this * cache. The value of <code>size</code> can never be greater than this * number. * * @return The maximum number of keys that can be stored in this * cache. */ public int getMaxCapacity () { return max; } /** * Sets the maximum number of keys that can be stored in this * cache. * <p> * The value of <code>size</code> can never be greater than this * number. If the maximum capicity is shrinking and too many * elements are already in the cache, the least recently used * ones will be discarded until <code>size</code> is the same as * <code>maxCapacity</code>. * * @param maxCapacity is the total number of keys that can be * stored in the cache. */ public void setMaxCapacity (int maxCapacity) { if ( maxCapacity < 0 ) { // The cache cannot contain a negative number of elements. throw new IllegalArgumentException(); } // Remove entries so the cache size is no more than its capacity. for ( int i = cache.size() - maxCapacity; i > 0; i-- ) { // Kick out the least recently used element. cache.remove( lru.key ); lru.previous.next = null; lru = lru.previous; } // Record the new maximum cache size. max = maxCapacity; } /** * Adds an <code>Object</code> to the cache that is associated with <code>key</code>. * The new item will become the most recently used. If the cache is full it * will replace the least recently used entry. * * @param key is the indexing object. * @param data is the object to cache. */ public void add (Object key, Object data) { if ( key == null ) { // The key cannot be null. throw new IllegalArgumentException(); } if ( max > 0 ) { Item item = new Item(); int cacheSize = cache.size(); // Sanity check. if ( cacheSize > max ) { // This can only happen if access to the cache was not synchronized. // The cache itself does not synchronization to improve performance. // If you see this application you should add syncronized() blocks // around the cache. throw new IllegalStateException(); } // Is the item being added already cached? Object existing = get( key ); if ( existing != null ) { // The key has already been used. By calling get() we already promoted // it to the MRU spot. However, if the data has changed, we need to // update it in the hash table. if ( existing != data ) { Item i = (Item)cache.get( key ); i.data = data; } } else // cache miss { // Add the new data. // Is the cache is full? if ( cacheSize == max ) { // Kick out the least recently used element. cache.remove( lru.key ); if ( lru.previous != null ) { lru.previous.next = null; } lru = lru.previous; } // Store the new item as the most recently used. item.key = key; item.data = data; item.next = mru; item.previous = null; if ( cache.size() == 0 ) // then cache is empty { lru = item; } else { mru.previous = item; } mru = item; cache.put( key, item ); } } } /** * Gets a cached <code>Object</code> associated with <code>key</code>. * * @param key is the indexing object. * @return The <code>Object</code> associated with <code>key</code>; <code>null</code> if * <code>key</code> is not in the cache. */ public Object get (Object key) { if ( key == null ) { // The key cannot be null. throw new IllegalArgumentException(); } // Get the cached item. Object o = cache.get( key ); if ( o == null ) // Cache miss { return null; } else // Cache hit { // Make this the most recently used entry. Item item = (Item)o; if ( mru != item ) // then not already the MRU { if ( lru == item ) // I'm the least recently used { lru = item.previous; } // Remove myself from the LRU list. if ( item.next != null ) { item.next.previous = item.previous; } item.previous.next = item.next; // Add myself back in to the front. mru.previous = item; item.previous = null; item.next = mru; mru = item; } // Return the cached data. return item.data; } } } package org.j4me.collections; import j2meunit.framework.*; /** * Tests the <code>Cache</code> class. It is a hashtable with a * maximum capacity that removes the LRU element when adding * a new one and the cache has reached capacity. * * @see org.j4me.collections.Cache */ public class CacheTest extends TestCase { public CacheTest () { super(); } public CacheTest (String name, TestMethod method) { super( name, method ); } public Test suite () { TestSuite suite = new TestSuite(); suite.addTest(new CacheTest("testBasicAddAndGet", new TestMethod() { public void run(TestCase tc) {((CacheTest) tc).testBasicAddAndGet(); } })); suite.addTest(new CacheTest("testIllegalOperations", new TestMethod() { public void run(TestCase tc) {((CacheTest) tc).testIllegalOperations(); } })); suite.addTest(new CacheTest("testAddingTwice", new TestMethod() { public void run(TestCase tc) {((CacheTest) tc).testAddingTwice(); } })); suite.addTest(new CacheTest("testLRU", new TestMethod() { public void run(TestCase tc) {((CacheTest) tc).testLRU(); } })); suite.addTest(new CacheTest("testCapacityChange", new TestMethod() { public void run(TestCase tc) {((CacheTest) tc).testCapacityChange(); } })); suite.addTest(new CacheTest("testZeroCapacity", new TestMethod() { public void run(TestCase tc) {((CacheTest) tc).testZeroCapacity(); } })); suite.addTest(new CacheTest("testCapacityOfOne", new TestMethod() { public void run(TestCase tc) {((CacheTest) tc).testCapacityOfOne(); } })); return suite; } /** * Tests that an element can be added and retreived from the * cache. There is no fancy LRU stuff going on. */ public void testBasicAddAndGet () { Cache cache = new Cache(10); assertEquals("The maximum cache size should be set by the constructor.", 10, cache.getMaxCapacity()); assertEquals("The cache should initially be empty.", 0, cache.size()); // Add an element. int key = 13; Integer data = new Integer(42); cache.add( new Integer(key), data ); assertEquals("Cache should not be empty now that an element has been added.", 1, cache.size()); // Get the element back out. Object result = cache.get( new Integer(key) ); assertTrue("The key should return a reference to the same object that was put in the cache.", data == result); // Try getting an element that doesn't exit. result = cache.get( new Integer(key - 1) ); assertNull("The key should not return data since it has not been added to the cache.", result); // Make sure we can clear the cache. cache.clear(); assertEquals("Cache should be empty now that it has been cleared.", 0, cache.size()); result = cache.get( new Integer(key) ); assertNull("Cache should not contain our key now that is has been cleared.", result); } /** * Tests the cache guards against programming it cannot accept. This * keeps the cache in a valid state. */ public void testIllegalOperations () { // Test cannot create a cache with no capacity. boolean caughtException = false; try { new Cache( -1 ); } catch (IllegalArgumentException e) { caughtException = true; } catch (Throwable t) { String actualExceptionName = t.getClass().getName(); fail( "Expected exception 'IllegalArgumentException' and got '" + actualExceptionName + "'." ); } if ( caughtException == false ) { fail( "Expected exception 'IllegalArgumentException' but no exceptions caught." ); } // Test cannot change a cache to have have no capacity. caughtException = false; try { Cache cache = new Cache( 13 ); cache.setMaxCapacity( -1 ); } catch (IllegalArgumentException e) { caughtException = true; } catch (Throwable t) { String actualExceptionName = t.getClass().getName(); fail( "Expected exception 'IllegalArgumentException' and got '" + actualExceptionName + "'." ); } if ( caughtException == false ) { fail( "Expected exception 'IllegalArgumentException' but no exceptions caught." ); } // Test cannot add a null key to a cache. caughtException = false; try { Cache cache = new Cache( 5 ); cache.add( null, null ); } catch (IllegalArgumentException e) { caughtException = true; } catch (Throwable t) { String actualExceptionName = t.getClass().getName(); fail( "Expected exception 'IllegalArgumentException' and got '" + actualExceptionName + "'." ); } if ( caughtException == false ) { fail( "Expected exception 'IllegalArgumentException' but no exceptions caught." ); } // Test cannot get a null key from a cache. caughtException = false; try { Cache cache = new Cache( 5 ); cache.add( new Integer(5), new Integer(5) ); cache.get( null ); } catch (IllegalArgumentException e) { caughtException = true; } catch (Throwable t) { String actualExceptionName = t.getClass().getName(); fail( "Expected exception 'IllegalArgumentException' and got '" + actualExceptionName + "'." ); } if ( caughtException == false ) { fail( "Expected exception 'IllegalArgumentException' but no exceptions caught." ); } } /** * Tests adding the same element to the cache twice to make sure there isn't * a duplicate entry. */ public void testAddingTwice () { Integer one = new Integer( 1 ); Integer two = new Integer( 2 ); int cacheSize = 5; Cache cache = new Cache( cacheSize ); // Add "one" many times. for ( int i = 0; i < cacheSize * 2; i++ ) { cache.add( one, one ); } assertEquals("one is the only element", 1, cache.size()); // Change the data for "one". cache.add( one, two ); assertEquals("one is still the only element", 1, cache.size()); Integer data = (Integer)cache.get( one ); assertEquals("data is two", two, data); // Just to be sure, add another key. cache.add( two, two ); assertEquals("There are two elements", 2, cache.size()); data = (Integer)cache.get( one ); assertEquals("key=one and data=two", two, data); data = (Integer)cache.get( two ); assertEquals("key=two and data=two", two, data); } /** * Tests that the LRU policy of the cache works as expected. No resizing * of the maximum cache size is done in this test. */ public void testLRU () { int max = 3; // Maximum of 3 elements Cache cache = new Cache(max); // Fill the cache, but don't overfill yet. cache.add( new Integer(1), new Integer(1) ); cache.add( new Integer(2), new Integer(2) ); cache.add( new Integer(3), new Integer(3) ); assertEquals("Cache should be full.", max, cache.size()); // Add another entry and make sure the LRU was ejected. cache.add( new Integer(4), new Integer(4) ); assertEquals("Cache should still be full.", max, cache.size()); Object result = cache.get( new Integer(1) ); assertNull("1 should no longer be in the cache (it was LRU).", result); // Make sure the cache entries still exist that we expect. // Note must call these in order they were inserted to keep the // same LRU order for later. result = cache.get( new Integer(2) ); assertNotNull("2 should still be in the cache.", result); result = cache.get( new Integer(3) ); assertNotNull("3 should still be in the cache.", result); result = cache.get( new Integer(4) ); assertNotNull("4 should be in the cache.", result); // Now try reversing the LRU order and adding more entries. result = cache.get( new Integer(3) ); result = cache.get( new Integer(2) ); cache.add( new Integer(5), new Integer(5) ); // Should kick out 4 cache.add( new Integer(6), new Integer(6) ); // Should kick out 3 result = cache.get( new Integer(3) ); assertNull("3 should no longer be in the cache.", result); result = cache.get( new Integer(4) ); assertNull("4 should no longer be in the cache.", result); result = cache.get( new Integer(2) ); assertNotNull("2 should still be in the cache.", result); // Order is now: 5, 6, 2. Get 5, add something, check that 2 and 5 still exist (6 tossed). result = cache.get( new Integer(5) ); assertNotNull("5 should still be in the cache.", result); cache.add( new Integer(7), new Integer(7) ); result = cache.get( new Integer(2) ); assertNotNull("2, 5, and 7 should be in the cache.", result); result = cache.get( new Integer(5) ); assertNotNull("2, 5, and 7 should be in the cache.", result); result = cache.get( new Integer(7) ); assertNotNull("2, 5, and 7 should be in the cache.", result); // Clear the cache. cache.clear(); result = cache.get( new Integer(2) ); assertNull("2, 5, and 7 should no longer be in the cache.", result); result = cache.get( new Integer(5) ); assertNull("2, 5, and 7 should no longer be in the cache.", result); result = cache.get( new Integer(7) ); assertNull("2, 5, and 7 should no longer be in the cache.", result); // Add back in 4 numbers and make sure first is ejected. cache.add( new Integer(11), new Integer(11) ); cache.add( new Integer(12), new Integer(12) ); cache.add( new Integer(13), new Integer(13) ); cache.add( new Integer(14), new Integer(14) ); result = cache.get( new Integer(11) ); assertNull("11 should no longer be in the cache.", result); result = cache.get( new Integer(12) ); assertNotNull("12, 13, and 14 should be in the cache.", result); result = cache.get( new Integer(13) ); assertNotNull("12, 13, and 14 should be in the cache.", result); result = cache.get( new Integer(14) ); assertNotNull("12, 13, and 14 should be in the cache.", result); } /** * Tests the capacity of the cache can be changed dynamically after it is * created. */ public void testCapacityChange () { // Fill a cache. Cache cache = new Cache(3); cache.add( new Integer(1), new Integer(1) ); cache.add( new Integer(2), new Integer(2) ); cache.add( new Integer(3), new Integer(3) ); // Grow the cache. cache.setMaxCapacity(5); assertEquals("The cache capacity should have grown to 5.", 5, cache.getMaxCapacity()); cache.add( new Integer(4), new Integer(4) ); cache.add( new Integer(5), new Integer(5) ); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(1))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(2))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(3))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(4))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(5))); // Set the cache to the same size (integer.e. test no-op). cache.setMaxCapacity(5); assertEquals("The cache capacity should remain at 5.", 5, cache.getMaxCapacity()); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(1))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(2))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(3))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(4))); assertNotNull("1, 2, 3, 4, and 5 should be in the cache.", cache.get(new Integer(5))); // Shrink the cache, but not to size 0. cache.setMaxCapacity(2); // Should remove 5 - 2 = 3 elements assertEquals("The cache capacity should shrink to 2.", 2, cache.getMaxCapacity()); assertEquals("The cache size should have shrunk to 2.", 2, cache.size()); assertNull("1, 2, 3 should not be in the cache.", cache.get(new Integer(1))); assertNull("1, 2, 3 should not be in the cache.", cache.get(new Integer(2))); assertNull("1, 2, 3 should not be in the cache.", cache.get(new Integer(3))); assertNotNull("4 and 5 should still be in the cache.", cache.get(new Integer(4))); assertNotNull("4 and 5 should still be in the cache.", cache.get(new Integer(5))); } /** * Tests that a cache of size 0 doesn't actually cache anything, but lets all * calls execute as normal (i.e. doesn't crash). */ public void testZeroCapacity () { Integer one = new Integer( 1 ); // Create a zero-size cache. Cache cache = new Cache( 0 ); assertEquals("Cache size is 0", 0, cache.getMaxCapacity()); // Make sure add is called without a problem. cache.add( one, one ); assertEquals("one not stored", 0, cache.size()); // Try getting it out just to be sure. Object data = cache.get( one ); assertNull("No data should be in cache", data); } /** * Tests that a cache of size 1 doesn't crash. */ public void testCapacityOfOne () { Integer one = new Integer( 1 ); Integer two = new Integer( 2 ); // Create the cache. Cache cache = new Cache( 1 ); assertEquals("Cache size is 1", 1, cache.getMaxCapacity()); // Make sure an element can be added. cache.add( one, one ); assertEquals("one stored", 1, cache.size()); Integer data = (Integer)cache.get( one ); assertEquals("one's data", one, data); // Add another element. cache.add( two, two ); assertEquals("two only thing stored", 1, cache.size()); data = (Integer)cache.get( one ); assertNull("one can no longer be retreived", data); data = (Integer)cache.get( two ); assertEquals("two's data", two, data); } }