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);
}
}
Related examples in the same category