An in-memory LRU cache is so often needed that I figured it would make 
sense to have a dedicated implementation. Combining the CacheStore and 
ReplacementPolicy allows for better optimized implementations for 
special cases, such as this.

Feel free to include it if you find it useful.

(: A ;)

-- 
Antti Koivunen (Mr.) <[EMAIL PROTECTED]>
---------------------------------------------
The song of life might not be a simple one,
but there's plenty of room for improvisation.
---------------------------------------------

/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included  with this distribution in
 * the LICENSE.txt file.
 */

package org.apache.avalon.excalibur.cache;

import java.util.HashMap;

/**
 * An in-memory LRU (least recently used) cache implementation that allows its
 * capacity to be dynamically adjusted. Two alternative methods for ordering the
 * entries are supported, <code>BUBBLE</code> and <code>MOVE_TO_TOP</code>. In
 * the former, each successful call to the method {@link #get get(Object key)} 
 * will cause the object to 'bubble' up one entry away from the 'drop zone', and
 * in the latter, the object will be literally moved to the top.
 * <p>
 * This implementation uses a custom linked list for maintaining the element
 * order, backed by a HashMap for quick lookups. The ordering is performed 
 * entirely by passing variable references, so the performance should be fairly
 * close to that of HashMap (although put operations include an extra object 
 * instantiation).
 * <p>
 * <b>This implementation is not synchronized.</b> If multiple threads
 * access the same instance concurrently, it <b>must</b> be externally 
 * synchronized, e.g. by wrapping it inside a {@link SynchronizedCache}.
 *
 * @author <a href="mailto:[EMAIL PROTECTED]";>Antti Koivunen</a>
 * @version $Revision: $ $Date: $
 */
public class LRUMemoryCache extends AbstractCache
{
    /**
     * The constant indicating that the method 'bubble' should be used for 
     * element ordering.
     */
    public static final int BUBBLE = 1;

    /**
     * The constant indicating that the method 'move to top' should be used for 
     * element ordering.
     */
    public static final int MOVE_TO_TOP = 2;
    
    private HashMap m_entries;
    
    private int m_capacity;
    
    private int m_mode;
    
    private Entry m_head = new Entry( null, null, null, null );

    /**
     * Creates a new instance of <code>LRUMemoryCache</code> with the specified
     * (initial) capacity and mode (either BUBBLE or MOVE_TO_TOP).
     *
     * @throws IllegalArgumentException if <code>capacity</code> is less than
     *     zero or if <code>mode</code> is not BUBBLE or MOVE_TO_TOP.
     */
    public LRUMemoryCache( final int capacity, final int mode )
    {
        if ( capacity < 0 )
        {
            throw new IllegalArgumentException("The capacity cannot be negative.");
        }
        if ( mode != BUBBLE && mode != MOVE_TO_TOP )
        {
            throw new IllegalArgumentException("Unsupported mode.");
        }
        m_capacity = capacity;
        m_entries = new HashMap( capacity );
        m_mode = mode;
        m_head.prev = m_head.next = m_head;
    }

    /**
     * Stores the specified object in the cache and associates it with the
     * specified key.
     *
     * @param key The key to be associated with the specified object.
     * @param value The object to be cached.
     * @return Previous value associated with the specified key, or
     *     <code>null</code> if not found.
     */
    public Object put( final Object key, final Object value )
    {
        final Entry entry = addEntryBefore( key, value, m_head.next );
        final Entry old = (Entry) m_entries.put( key, entry );
        trimToCapacity();
        notifyAdded( key, value );
        if ( old != null )
        {
            return old.value;
        }
        return null;
    }
    
    /**
     * Returns the object associated with the specified key.
     *
     * @param key The key.
     * @return The cached object or <code>null</code> if not found.
     */
    public Object get( final Object key )
    {
        final Entry entry = (Entry) m_entries.get( key );
        if ( entry == null )
        {
            return null;
        }
        recordHit( entry );
        return entry.value;
    }
    
    /**
     * Removes the object associated with the specified key.
     *
     * @param key The cache key.
     * @return Previous value associated with the specified key, or
     *     <code>null</code> if not found.
     */
    public Object remove( final Object key )
    {
        final Entry entry = (Entry) m_entries.remove( key );
        if ( entry == null )
        {
            return null;
        }
        removeEntry( entry );
        return entry.value;
    }
    
    /**
     * Returns <code>true</code> if this cache contains an object associated
     * with the specified key.
     *
     * @param key The key.
     * @return <code>true</code> if this cache contains an object associated
     *     with the specified key.
     */
    public boolean containsKey( final Object key )
    {
        return m_entries.containsKey( key );
    }
    
    /**
     * Returns the number of objects currently cached.
     */
    public int size()
    {
        return m_entries.size();
    }
    
    /**
     * Returns the maximum number of objects to be cached.
     */
    public int capacity()
    {
        return m_capacity;
    }
    
    /**
     * Removes all cache entries, but does not modify the capacity.
     */
    public void clear()
    {
        m_head.next = m_head.prev = m_head;
        m_entries.clear();
    }
    
    /**
     * Sets the maximum number of objects to be cached. The cache size will be
     * trimmed down to the new capacity, if necessary.
     *
     * @param key The new capacity.
     * @throws IllegalArgumentException if <code>capacity < 0</code>.
     */
    public void setCapacity( final int capacity )
    {
        if ( capacity < 0 )
        {
            throw new IllegalArgumentException("The capacity cannot be negative.");
        }
        m_capacity = capacity;
        trimToCapacity();
    }
    
    /**
     * Returns a string representation of this cache.
     */
    public String toString()
    {
        StringBuffer sb = new StringBuffer("[")
            .append(getClass().getName()).append(": ")
            .append("size = ").append(size()).append(", ")
            .append("capacity = ").append(capacity())
            .append("]");
        return sb.toString();
    }
    
    private void trimToCapacity()
    {
        while ( size() > capacity() )
        {
            removeLRU();
        }
    }
    
    /**
     * Removes the least recently used cache entry.
     */
    private void removeLRU()
    {
        if ( m_head == m_head.prev )
        {
            return; // Cache empty
        }
        m_entries.remove( m_head.prev.key );
        removeEntry( m_head.prev );
    }
    
    private void recordHit( final Entry entry )
    {
        if ( m_mode == MOVE_TO_TOP )
        {
            moveToTop( entry );
        }
        else
        {
            bubble( entry );
        }
    }
    
    private void moveToTop( final Entry entry )
    {
        removeEntry( entry );
        insertEntryBefore( entry, m_head.next );
    }
    
    private void bubble( final Entry entry )
    {
        removeEntry( entry );
        insertEntryBefore( entry, entry.prev );
    }
    
    private Entry addEntryBefore( final Object key,
                                  final Object value,
                                  final Entry refEntry )
    {
        final Entry entry = new Entry( key, value, refEntry.prev, refEntry );
        entry.prev.next = entry;
        entry.next.prev = entry;
        return entry;
    }
    
    private void insertEntryBefore( final Entry entry, final Entry refEntry )
    {
        entry.prev = refEntry.prev;
        entry.next = refEntry;
        entry.prev.next = entry;
        entry.next.prev = entry;
    }
    
    private void removeEntry( final Entry entry )
    {
        entry.prev.next = entry.next;
        entry.next.prev = entry.prev;
        notifyRemoved( entry.key, entry.value );
    }
    
    /**
     * Entry in the linked list that maintains the LRU element order.
     */
    private static class Entry
    {
        Object key;
        Object value;
        Entry prev;
        Entry next;
        
        Entry( final Object key, final Object value,
               final Entry prev, final Entry next )
        {
            this.key = key;
            this.value = value;
            this.prev = prev;
            this.next = next;
        }
    }
}

--
To unsubscribe, e-mail:   <mailto:[EMAIL PROTECTED]>
For additional commands, e-mail: <mailto:[EMAIL PROTECTED]>

Reply via email to