
org.divxdede.collection.Cache Maven / Gradle / Ivy
/*
* Copyright (c) 2010 ANDRE S?bastien (divxdede). All rights reserved.
* Cache.java is a part of this Commons library
* ====================================================================
*
* Commons library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or any later version.
*
* This 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, see .
*/
package org.divxdede.collection;
import org.divxdede.commons.Disposable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.NoSuchElementException;
/** Create a cache instance that hold like a Map a pair of Key/Value.
* A Cache has a maximum size and hold a Removal Entry Policy that decide which entry to remove when the cache reach it's maximum size.
*
* This implementation can allow 3 Removal entry policies
*
* - LeastRecentlyUsed: The least recently used entry will be removed when the cache reach it's maximum size
* - LeastFrequentlyUsed: The least frequently used entry will be removed when the cache reach it's maximum size
* - OldestInsertion: The oldest inserted entry will be removed when the cache reach it's maximum size
*
*
* Exemple:
*
* Cache cache = new Cache( 2 , RemovalEntryPolicy.LeastRecentlyUsed );
* cache.put("sophie" , Color.RED );
* cache.put("tom" , Color.BLUE );
*
* cache.get("tom");
* cache.get("sophie");
*
* cache.put("robin" , Color.GREEN); // sophie is accessed more recently than "tom", then robin will be remove "tom" entry
*
*
* This cache implementation is thread-safe by default because we thinks that these objects are often build for be used in a concurrent architecture.
*
* @author Andr? S?bastien - INFASS Syst?mes (http://www.infass.com)
*/
public class Cache implements Disposable , Iterable {
/** Removal entry policy applicable to a Cache when it's size reach it's maximum size.
*/
public enum RemovalEntryPolicy {
/** This removal policy remove the least recently used entry when the cache reach it's maximum size.
* Any new entry is considered as the most recently entry used
*/
LeastRecentlyUsed ,
/** This removal policy remove the least frequently used entry when the cache reach it's maximum size.
* If the cache is full, any new entry will be sucessfully inserted to let it a chance to be frequently used :P
*/
LeastFrequentlyUsed ,
/** This removal policy remove the oldest inserted entry when the cache reach it's maximum size
*/
OldestInsertion
};
/** Internal implementation of this map regarding to it's cache policy
*/
private final CacheHelper helper;
private final RemovalEntryPolicy policy;
public Cache(int maximumSize , RemovalEntryPolicy policy ) {
this.policy = policy;
switch(this.policy) {
case LeastFrequentlyUsed : helper = new FrequencyMap(maximumSize);
break;
case LeastRecentlyUsed : helper = new FixedMap(maximumSize,true);
break;
case OldestInsertion : helper = new FixedMap(maximumSize,false);
break;
default : throw new IllegalArgumentException("policy can't be null");
}
}
/** Return the Removal entry policy of this cache when it's size reach it's maximum size
* @return Removal entry policy of this cache
*/
public RemovalEntryPolicy getRemovalEntryPolicy() {
return policy;
}
/**
* Associates the specified value with the specified key in this cache.
* If the cache previously contained a mapping for this key, the old value is replaced.
*
* This method record an access to the specified entry in order to respect this cache policy when it's size reach it's maximum size.
* That mean if the specified entry already exists, this method don't reset previous record accesses for the existing key.
*
* @param key key with which the specified value is to be associated.
* @param value value to be associated with the specified key.
* @return previous value associated with specified key, or null if there was no mapping for key.
* A null return can also indicate that the HashMap previously associated null with the specified key.
*/
public synchronized V put(K key,V value) {
return helper.put(key,value);
}
/**
* Returns the value to which the specified key is mapped in this cache or null if the cache contains no mapping for this key.
* A return value of null does not necessarily indicate that the cache contains no mapping for the key; it is also possible that the cache explicitly maps the key to null.
* The containsKey method may be used to distinguish these two cases.
*
* This method record an access to the specified entry in order to respect this cache policy when it's size reach it's maximum size.
* You can have a cache respecting the Least Recently Used order or Least Frequently Used order or simply Insertion order
*
* @param key the key whose associated value is to be returned.
* @return the value to which this map maps the specified key, or
* null if the map contains no mapping for this key.
* @see #put(Object, Object)
*/
public synchronized V get(K key) {
return helper.get(key);
}
/**
* Returns true if this cache contains a mapping for the specified key.
*
* This method don't record any access to the specified entry and do not participate to help the cache's policy
*
* @param key The key whose presence in this map is to be tested
* @return true if this map contains a mapping for the specified
* key.
*/
public synchronized boolean containsKey(K key) {
return helper.containsKey(key);
}
/**
* Returns an iterator over the values contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public synchronized Iterator iterator() {
return helper.iterator();
}
/**
* Returns an iterator over the keys contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public synchronized Iterator iteratorKeys() {
return helper.iteratorKeys();
}
/** Return the maximum size of this cache.
*
* When you reach the maximum size, the removal entry policy is used to determine which entry must be removed to preserve the maximum size.
*
* @return Maximum size of this cache
*/
public int getMaximumSize() {
return helper.getMaximumSize();
}
/** Clear the cache contents.
* This cache can be reuse at any time even after a dispose invocation.
*/
public synchronized void dispose() {
helper.dispose();
}
/** Return the current size of this cache.
* @return Current size of this cache
*/
public synchronized int getSize() {
return helper.getSize();
}
/** Interface describing the helper class responsible to hold cache entry.
* This help must respect the requested policy when the cache reach it's maximum size
*
* @author Andr? S?bastien
*/
public static interface CacheHelper extends Iterable , Disposable {
/** Return the maximum size of this cache
* @return Maximum size of this cache
*/
public int getMaximumSize();
/** Return the current size of this cache.
* @return Current size of this cache
*/
public int getSize();
/**
* Associates the specified value with the specified key in this cache.
* If the map previously contained a mapping for this key, the old value is replaced.
*
* This method record an access to the specified entry in order to respect this cache policy when it's size reach it's maximum size.
* That mean if the specified entry already exists, this method don't reset previous record accesses for the existing key.
*
* @param key key with which the specified value is to be associated.
* @param value value to be associated with the specified key.
* @return previous value associated with specified key, or null if there was no mapping for key.
* A null return can also indicate that the HashMap previously associated null with the specified key.
*/
public V put(K key,V value);
/**
* Returns the value to which the specified key is mapped in this cache or null if the cache contains no mapping for this key.
* A return value of null does not necessarily indicate that the cache contains no mapping for the key; it is also possible that the cache explicitly maps the key to null.
* The containsKey method may be used to distinguish these two cases.
*
* This method record an access to the specified entry in order to respect this cache policy when it's size reach it's maximum size.
* You can have a cache respecting the Least Recently Used order or Least Frequently Used order or simply Insertion order
*
* @param key the key whose associated value is to be returned.
* @return the value to which this map maps the specified key, or
* null if the map contains no mapping for this key.
* @see #put(Object, Object)
*/
public V get(K key);
/**
* Returns true if this cache contains a mapping for the specified key.
*
* This method don't record any access to the specified entry and do not participate to help the cache's policy
*
* @param key The key whose presence in this map is to be tested
* @return true if this map contains a mapping for the specified
* key.
*/
public boolean containsKey(K key);
/**
* Returns an iterator over the values contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public Iterator iterator();
/**
* Returns an iterator over the keys contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public Iterator iteratorKeys();
}
/** LinkedHashMap implementation with a maximum size that can manage cache policy in 2 ways:
*
* - false : Insertion order (Oldest entries removed first)
* - true : Access order (Least Recently Used removed first)
*
*/
private static class FixedMap extends LinkedHashMap implements CacheHelper {
/** Maximum size
*/
private final int maximumSize;
/** Constructor with a specified order
* @param maximumSize Maximum size of this map
* @param accessOrder false for an insertion order, true for a least recently used order
*/
public FixedMap(int maximumSize , boolean accessOrder) {
super( 16 , 0.75f , accessOrder ); // default INITIAL_CAPACITY and LOAD_FACTOR
this.maximumSize = maximumSize;
}
/**
* Returns an iterator over the values contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public Iterator iterator() {
return super.values().iterator();
}
/**
* Returns an iterator over the keys contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public Iterator iteratorKeys() {
return super.keySet().iterator();
}
@Override
protected boolean removeEldestEntry(java.util.Map.Entry eldest) {
return size() > getMaximumSize();
}
/* Return the maximum size allowed in this cache map
*/
public int getMaximumSize() {
return maximumSize;
}
public void dispose() {
this.clear();
}
public int getSize() {
return size();
}
}
/** CacheHelper implementation for Least Frequently Used removal policy.
*
* This cache use an HashMap
* but maintain each entries linked by "frequency access order" for determine which entry to remove when this cache is full.
*
*
*/
private static class FrequencyMap implements CacheHelper {
/** The first bullet (less frequently usage bullet) of this map
*/
private FrequencyBullet firstBullet = null;
private int maximumSize = 0;
private HashMap> map = null;
/** Constructor
*/
public FrequencyMap(int maximumSize) {
this.maximumSize = maximumSize;
this.map = new HashMap>();
}
/* Return the maximum size allowed in this cache map
*/
public int getMaximumSize() {
return maximumSize;
}
/**
* Associates the specified value with the specified key in this cache.
* If the map previously contained a mapping for this key, the old value is replaced.
*
* This method record an access to the specified entry in order to respect this cache policy when it's size reach it's maximum size.
* That mean if the specified entry already exists, this method don't reset previous record accesses for the existing key.
*
* @param key key with which the specified value is to be associated.
* @param value value to be associated with the specified key.
* @return previous value associated with specified key, or null if there was no mapping for key.
* A null return can also indicate that the HashMap previously associated null with the specified key.
*/
public V put(K key,V value) {
FrequencyEntry entry = map.get(key);
if( entry != null ) {
V oldValue = entry.getValue();
entry.setValue(value);
recordAccess( entry );
return oldValue;
}
else {
if( getSize() == getMaximumSize() ) {
removeEldestEntry();
}
entry = new FrequencyEntry(key,value);
if( firstBullet == null || firstBullet.getFrequency() > 1 ) {
// we must create a new bullet for contains our new entry
FrequencyBullet newBullet = new FrequencyBullet(1);
if( firstBullet != null ) newBullet.insertBefore(firstBullet);
newBullet.add( entry );
// Now the new bullet is the first bullet
firstBullet = newBullet;
}
else {
// we can reuse the first bullet because it is a 0-frequency bullet
firstBullet.add( entry );
}
map.put(key,entry);
return null;
}
}
/**
* Returns an iterator over the values contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public Iterator iterator() {
if( firstBullet == null ) return (Iterator)Collections.EMPTY_LIST.iterator();
return new Iterator() {
Iterator> i = firstBullet.iterator();
Iterator j = map.keySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public V next() {
if( ! hasNext() ) throw new NoSuchElementException();
try {
return i.next().getValue();
}
finally {
j.next(); // just for manage ConcurrentException
}
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns an iterator over the keys contents of this cache .
* This iterator order begin from the eldest entry to the newest entry regarding the current policy of this cache.
*
* @return an Iterator.
*/
public Iterator iteratorKeys() {
if( firstBullet == null ) return (Iterator)Collections.EMPTY_LIST.iterator();
return new Iterator() {
Iterator> i = firstBullet.iterator();
Iterator j = map.keySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public K next() {
if( ! hasNext() ) throw new NoSuchElementException();
try {
return i.next().getKey();
}
finally {
j.next(); // just for manage ConcurrentException
}
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
/**
* Returns true if this cache contains a mapping for the specified key.
*
* This method don't record any access to the specified entry and do not participate to help the cache's policy
*
* @param key The key whose presence in this map is to be tested
* @return true if this map contains a mapping for the specified
* key.
*/
public boolean containsKey(K key) {
return map.containsKey(key);
}
/**
* Returns the value to which the specified key is mapped in this cache or null if the cache contains no mapping for this key.
* A return value of null does not necessarily indicate that the cache contains no mapping for the key; it is also possible that the cache explicitly maps the key to null.
* The containsKey method may be used to distinguish these two cases.
*
* This method record an access to the specified entry in order to respect this cache policy when it's size reach it's maximum size.
* You can have a cache respecting the Least Recently Used order or Least Frequently Used order or simply Insertion order
*
* @param key the key whose associated value is to be returned.
* @return the value to which this map maps the specified key, or
* null if the map contains no mapping for this key.
* @see #put(Object, Object)
*/
public V get(K key) {
FrequencyEntry entry = map.get(key);
if( entry != null ) {
recordAccess( entry );
return entry.getValue();
}
return null;
}
/** dispose
*/
public void dispose() {
this.map.clear();
this.firstBullet = null;
}
/** Current Size
*/
public int getSize() {
return this.map.size();
}
/** Record an access on a FrequencyEntry
*/
private void recordAccess(FrequencyEntry entry) {
FrequencyBullet suggestedFirstBullet = entry.recordAccess();
if( suggestedFirstBullet != null ) firstBullet = suggestedFirstBullet;
}
/** EldestEntryRemover implementation for removing the Least Frequently Used entry
*/
private void removeEldestEntry() {
FrequencyEntry eldest = this.firstBullet.firstEntry;
this.firstBullet = this.firstBullet.remove(eldest);
map.remove( eldest.getKey() );
}
}
/** FrequencyEntry represent a Key/Value pair like a Map.Entry do.
* But in addition, a FrequencyEntry is doubly-linked with all other entries with the same frequency.
* This set of FrequencyEntry is called FrequencyBullet.
*
* The double link don't refer any special order of theses entries since all of them has the same frequency.
*
* This class has a recordAccess() method incrementing it's access count by 1 and moving this entry to the right bullet.
*
* @author Andr? S?bastien
*/
private static class FrequencyEntry {
private K key = null;
private V value = null;
// linked
FrequencyBullet bullet = null;
FrequencyEntry previous = null;
FrequencyEntry next = null;
/** Create a new FrequencyEntry without bound to any bullet
* @param key user-key
* @param value user-value
*/
public FrequencyEntry(K key , V value) {
this.key = key;
this.value = value;
}
/** Retrieve the user-key
*/
public K getKey() {
return key;
}
/** Retrieve the access frequency of this key.
* Note that this frequency value is deducted from it's bullet container.
* @throws NullPointerException if this entry is not bound to any bullet
*/
public int getFrequency() {
return bullet.getFrequency();
}
/** Retrieve the user-value
*/
public V getValue() {
return this.value;
}
/** Set the user-value
*/
public void setValue(V value) {
this.value = value;
}
/** Record an access to this entry.
* This implementation move when it's required this entry to another bullet in order to reflet the new frequency count value.
*
* @return The first-bullet in the whole-bullet-chain if it change, null otherwise
*/
public FrequencyBullet recordAccess() {
FrequencyBullet eldest = null;
int newFrequency = bullet.getFrequency() + 1;
if( bullet.nextBullet != null && bullet.nextBullet.getFrequency() == newFrequency ) {
// Create a new bullet and move to it this entry
FrequencyBullet newBullet = bullet.nextBullet;
// remove from the old bullet
eldest = bullet.remove(this);
if( eldest != null && eldest.previousBullet != null ) eldest = null;
// add to the new bullet
newBullet.add(this);
}
else {
// If the bullet contains only this entry, we can increment the frequency of this bullet container directly
if( bullet.firstEntry == this && this.next == null ) {
bullet.frequency++;
}
else {
// We create a new bullet and move this entry to it
FrequencyBullet newBullet = new FrequencyBullet( newFrequency );
// Insert the new bullet
newBullet.insertAfter( bullet );
// remove this entry from the old bullet
eldest = bullet.remove(this);
if( eldest != null && eldest.previousBullet != null ) eldest = null;
// add it to the new bullet
newBullet.add(this);
}
}
return eldest;
}
// /** Return an human readable interpretation of this entry
// */
// public String toString() {
// return SimpleFormatter.format("Entry[@%%,key=%%,value=%%,bullet=%%]", Integer.toHexString( System.identityHashCode(this) ) , getKey() , getValue() , this.bullet );
// }
}
/** A FrequencyBullet is a container that holds all FrequencyEntries for the same specified frequency.
*
* Bullets are doubly-linked in an defined order.
* Each bullet refer to the previous bullet (less frequently used) and it's next bullet (more frequently used).
*
* When a FrequencyEntry record an access, it update it's frequency count and change it's FrequencyBullet container.
* This move-on can create a new bullet if this one is not already used by another entry.
*
* Theses bullet are designed to make a recordAccess time constant over the contents of the cache.
* Since each recordAccess() re-order entries from the less used to the more used.
* Without bullets, If you have thousands entries with the same frequency and one of them is accessed. we should iterate over all
* equally entries for found it's new place. This bullet-design allow to safe-fast throw out all theses equally entries.
*
* @author Andr? S?bastien
*/
private static class FrequencyBullet implements Iterable> {
int frequency;
FrequencyBullet previousBullet = null;
FrequencyBullet nextBullet = null;
FrequencyEntry firstEntry = null;
public FrequencyBullet(int frequency) {
this.frequency = frequency;
}
public int getFrequency() {
return frequency;
}
/** Insert a THIS bullet BEFORE the specified one
*/
public void insertBefore( FrequencyBullet postBullet ) {
this.previousBullet = postBullet.previousBullet;
this.nextBullet = postBullet;
if( postBullet.previousBullet != null ) postBullet.previousBullet.nextBullet = this;
postBullet.previousBullet = this;
}
/** Insert a THIS bullet AFTER the specified one
*/
public void insertAfter( FrequencyBullet precedingBullet ) {
this.previousBullet = precedingBullet;
this.nextBullet = precedingBullet.nextBullet;
if( precedingBullet.nextBullet != null ) precedingBullet.nextBullet.previousBullet = this;
precedingBullet.nextBullet = this;
}
/** Remove this bullet from the bullet's chain
*/
public void remove() {
if( firstEntry != null ) throw new IllegalStateException("can't remove a non-empty bullet from the bullet's chain");
if( this.previousBullet != null ) this.previousBullet.nextBullet = this.nextBullet;
if( this.nextBullet != null ) this.nextBullet.previousBullet = this.previousBullet;
this.previousBullet = null;
this.nextBullet = null;
}
/** add an entry to this bullet
* @param entry Entry to add to this bullet
*/
public void add( FrequencyEntry entry ) {
if( entry.bullet != null ) throw new IllegalStateException("Can't add an already linked-entry to a bullet");
entry.bullet = this;
entry.previous = null;
entry.next = this.firstEntry;
if( this.firstEntry != null ) this.firstEntry.previous = entry;
this.firstEntry = entry;
}
/** Remove an entry from this bullet
* @return this bullet if not empty or the next bullet if this one is removed
*/
private FrequencyBullet remove(FrequencyEntry entry) {
if( entry.bullet != this ) {
throw new IllegalStateException("Can't remove a bullet from it's non-owner bullet");
}
FrequencyBullet result = this;
FrequencyEntry oldPreviousEntry = entry.previous;
FrequencyEntry oldNextEntry = entry.next;
if( this.firstEntry == entry )
this.firstEntry = entry.next;
// re-link entries from the bullet
if( oldPreviousEntry != null ) oldPreviousEntry.next = oldNextEntry;
if( oldNextEntry != null ) oldNextEntry.previous = oldPreviousEntry;
if( this.firstEntry == null ) {
// this bullet is now empty, we can remove it from the bullet chains
result = nextBullet;
this.remove();
}
// entry is now unlinked
entry.previous = null;
entry.next = null;
entry.bullet = null;
return result;
}
/** Return an iterator over all entries of this bullet container and next ones
* Be careful, this iterator don't stop at the and of this bullet and throw out to another bullets
*/
public Iterator> iterator() {
return new Iterator>() {
FrequencyBullet currentBullet = Cache.FrequencyBullet.this;
FrequencyEntry currentEntry = currentBullet.firstEntry;
public boolean hasNext() {
if( currentEntry != null ) return true;
currentBullet = currentBullet.nextBullet;
if( currentBullet == null ) return false;
currentEntry = currentBullet.firstEntry;
return currentEntry != null;
}
public FrequencyEntry next() {
if( ! hasNext() ) throw new NoSuchElementException();
try {
return currentEntry;
}
finally {
currentEntry = currentEntry.next;
}
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
// /** Return an human readable interpretation of this entry
// */
// public String toString() {
// return SimpleFormatter.format("Bullet[@%%,frequency=%%]", Integer.toHexString( System.identityHashCode(this) ) , getFrequency() );
// }
}
}