All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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() ); // } } }