A least recently used (LRU) cache. : Cache « Development Class « Java






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

1.A LRU (Least Recently Used) cache replacement policy
2.A Map that is size-limited using an LRU algorithm
3.A random cache replacement policy
4.A second chance FIFO (First In First Out) cache replacement policy
5.An LRU (Least Recently Used) cache replacement policy
6.Async LRU List
7.FIFO First In First Out cache replacement policy
8.Implementation of a Least Recently Used cache policy
9.Generic LRU Cache
10.LRU Cache
11.A Least Recently Used Cache
12.The class that implements a simple LRU cache
13.Map implementation for cache usage
14.Weak Cache Map
15.Provider for the application cache directories.
16.Fixed length cache with a LRU replacement policy.
17.A small LRU object cache.
18.LRU Cache 2
19.A cache that purges values according to their frequency and recency of use and other qualitative values.
20.A thread-safe cache that keeps its values as java.lang.ref.SoftReference so that the cache is, in effect, managed by the JVM and kept as small as is required
21.Cache LRU
22.A FastCache is a map implemented with soft references, optimistic copy-on-write updates, and approximate count-based pruning.
23.A HardFastCache is a map implemented with hard references, optimistic copy-on-write updates, and approximate count-based pruning.