/*
* This file is part of muCommander, http://www.mucommander.com
* Copyright (C) 2002-2016 Maxence Bernard
*
* muCommander is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* muCommander is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.mucommander.cache;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An abstract LRU cache.
*
* <p>An LRU (Least Recently Used) cache can contain a fixed number of items (the capacity). When capacity is reached,
* the least recently used is removed. Each object retrieved with the {@link #get(Object) get()} method
* makes the requested item the most recently used one. Similarly, each object inserted using the
* {@link #add(Object, Object) add()} method makes the added item the most recently used one.</p>
*
* <p>This LRUCache provides an optional feature : the ability to assign a time-to-live for each or part of the
* items added. When the time-to-live of an item expires, the item is automatically removed from the cache and won't
* be returned by the {@link #get(Object) get()} method anymore.</p>
*
* <p><b>Implementation note:</b> checking for expired items can be an expensive operation so it doesn't have
* to be done as soon as the item has expired, the expired items can live a bit longer in the cache if necessary.
* <br>The LRUCache implementation must however guarantee two things :
* <ul>
* <li>as soon as an item has expired, it cannot be returned by {@link #get(Object) get()}.
* <li>when cache capacity is reached (cache is full) and a new item needs to be added, any expired item must be
* immediately removed. This prevents least recently used items from being removed unnecessarily.
* </ul></p>
*
* @author Maxence Bernard
*/
public abstract class LRUCache<K, V> {
private static final Logger LOGGER = LoggerFactory.getLogger(LRUCache.class);
/** Cache capacity: maximum number of items this cache can contain */
protected int capacity;
/** Current eldest expiration date amongst all items */
protected long eldestExpirationDate = Long.MAX_VALUE;
/** Specifies whether cache hit/miss counters should be updated (should be enabled for Debug purposes only) */
protected final static boolean UPDATE_CACHE_COUNTERS = false;
/** Number of cache hits since this LRUCache was created */
protected int nbHits;
/** Number of cache misses since this LRUCache was created */
protected int nbMisses;
/**
* Creates an initially empty LRUCache with the specified maximum capacity.
*/
public LRUCache(int capacity) {
this.capacity = capacity;
}
/**
* Returns the maximum number of items this cache can contain.
*/
public int getCapacity() {
return capacity;
}
/**
* Returns the number of cache hits since this LRUCache was created.
*
* @return the number of cache hits since this LRUCache was created
*/
public int getHitCount() {
return nbHits;
}
/**
* Returns the number of cache misses since this LRUCache was created.
*
* @return the number of cache misses since this LRUCache was created
*/
public int getMissCount() {
return nbMisses;
}
///////////////////////
// Absctract methods //
///////////////////////
/**
* Returns the cached object value corresponding to the given key and marks the cached item as the most
* recently used one.
*
* <p>This method will return <code>null</code> if:
* <ul>
* <li>the given key doesn't exist
* <li>the cached value corresponding to the key has expired
* <ul></p>
*
* @param key the cached item's key
* @return the cached value corresponding to the specified key, or <code>null</code> if a value could not
* found or has expired
*/
public abstract V get(K key);
/**
* Adds a new key/value pair to the cache and marks it as the most recently used.
*
* <p>If the cache's capacity has been reached (cache is full):
* <ul>
* <li>any object with a past expiration date will be removed<li>
* <li>if no expired item could be removed, the least recently used item will be removed
* <ul></p>
*
* @param key the key for the object to store
* @param value the value to cache
* @param timeToLive the time-to-live of the object in the cache in milliseconds, or -1 for no time-to-live,
* the object will just be removed when it becomes the least recently used one.
*/
public abstract void add(K key, V value, long timeToLive);
/**
* Convenience method, equivalent to add(key, value, -1).
*/
public synchronized void add(K key, V value) {
add(key, value, -1);
}
/**
* Removes all items from this cache, leaving the cache in the same state as when it was just created.
*/
public abstract void clearAll();
/**
* Returns the current size of this cache, i.e. the number of cached elements it contains.
* <br><b>Note: </b>Some items that have expired and have not yet been removed might be accounted for
* in the returned size.
*/
public abstract int size();
//////////////////
// Test methods //
//////////////////
/**
* Tests this LRUCache for corruption and throws a RuntimeException if something is wrong.
*/
protected abstract void testCorruption() throws RuntimeException;
/**
* Test method : simple test case + stress/sanity test
*/
public static void main(String args[]) {
LRUCache<Integer, Integer> cache;
/*
// Simple test case
cache = new FastLRUCache(3);
cache.add("orange", "ORANGE");
System.out.println(cache.toString());
cache.add("apple", "APPLE");
System.out.println(cache.toString());
System.out.println("get(orange)= "+cache.get("orange"));
System.out.println(cache.toString());
cache.add("apricot", "APRICOT");
System.out.println(cache.toString());
cache.add("banana", "BANANA", 1000);
System.out.println(cache.toString());
System.out.println("waiting for banana expiration");
try { Thread.sleep(1050); } catch(InterruptedException e) {}
System.out.println(cache.toString());
System.out.println("get(banana)= "+cache.get("banana"));
System.out.println(cache.toString());
*/
long timeStamp = System.currentTimeMillis();
// Stress test to see if everything looks OK after a few thousand iterations
int capacity = 1000;
cache = new FastLRUCache<Integer, Integer>(capacity);
java.util.Random random = new java.util.Random();
for(int i=0; i<100000; i++) {
// 50% chance to add a new element with a random value and expiration date (50% chance for no expiration date)
if(cache.size()==0 || random.nextBoolean()) {
// System.out.println("cache.add()");
cache.add(random.nextInt(capacity), random.nextInt(), random.nextBoolean()?-1:random.nextInt(10));
}
// 50% chance to retrieve a random existing element
else {
// System.out.println("cache.get()");
cache.get(random.nextInt(capacity));
}
try {
// Test the cache for corruption
cache.testCorruption();
}
catch(RuntimeException e) {
LOGGER.debug("Cache corrupted after "+i+" iterations, cache state="+cache);
return;
}
// // Print the cache's state
// System.out.println(cache.toString());
}
LOGGER.debug("Stress test took "+(System.currentTimeMillis()-timeStamp)+" ms.\n");
// Print the cache's state
System.out.println(cache.toString());
}
}