com.softicar.platform.common.container.cache.Cache Maven / Gradle / Ivy
Show all versions of platform-common Show documentation
package com.softicar.platform.common.container.cache;
import com.softicar.platform.common.core.annotations.Nullable;
import com.softicar.platform.common.core.logging.IntervalLog;
import com.softicar.platform.common.core.logging.Log;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* This structure can be used to cache the loading of objects.
*
* The cache works like a Map but it has a variable upper limit of its size.
* When the maximum size is reached, the least used item will be removed. This
* way, the hit/miss ratio should be optimal.
*
* @author Oliver Richers
*/
public class Cache {
private final Map> keyMap = new TreeMap<>();
private final int maxSize;
private final String name;
private Item newest;
private int queueSize;
private int hitCount = 0;
private int missCount = 0;
/**
* Creates the cache with the specified parameters.
*
* If max is less than 1, the cache will never remove any items.
*
* @param max
* the maximum size of the cache
* @param cacheName
* the name of the cache (only used for logging/debugging)
*/
public Cache(int max, String cacheName) {
this.maxSize = max;
this.name = cacheName;
}
/**
* Adds an new value to the cache.
*
* If the maximum size of the cache is reached, this will remove the least
* used item from the cache.
*
* If there is already a value with the specified key, that value will be
* overwritten with the new value.
*/
public void put(K key, V value) {
if (key == null) {
throw new IllegalArgumentException("The key for a cache entry may not be null.");
}
// remove old item with the same key
Item oldItem = keyMap.remove(key);
if (oldItem != null) {
removeFromQueue(oldItem);
}
// add new item
Item item = new Item<>(key, value);
keyMap.put(key, item);
insertIntoQueue(item);
// remove if too many entries
if (maxSize > 0) {
while (keyMap.size() > maxSize) {
Item oldest = newest.prev;
Log.verbose("Cache: Removing oldest item " + oldest.key + ".");
keyMap.remove(oldest.key);
removeFromQueue(oldest);
}
}
}
/**
* Returns true if a value with the specified key is in the cache.
*/
public boolean containsKey(K key) {
return getItem(key) != null;
}
/**
* Returns the value with the specified key from the cache.
*
* @param key
* the key of the value
* @return the value, or null if no such item can be found
*/
public @Nullable V get(K key) {
Item item = getItem(key);
return item == null? null : item.value;
}
/**
* Returns the keys of all cached items.
*
* @return collection of all keys
*/
public Set getKeys() {
return keyMap.keySet();
}
/**
* Returns the current number of cached items.
*/
public int size() {
return keyMap.size();
}
private static class Item {
public Item(K k, V v) {
key = k;
value = v;
}
public K key;
public V value;
public Item prev;
public Item next;
}
private void insertIntoQueue(Item item) {
if (newest == null) {
item.prev = item;
item.next = item;
} else {
item.next = newest;
item.prev = newest.prev;
item.next.prev = item;
item.prev.next = item;
}
newest = item;
++queueSize;
}
private void removeFromQueue(Item item) {
// special case for one entry
if (queueSize == 1) {
if (item != newest || item.next != item || item.prev != item) {
throw new IllegalStateException("Internal error in cache for item " + item.key + " -> " + item.value + ".");
}
newest = null;
} else {
item.next.prev = item.prev;
item.prev.next = item.next;
if (item == newest) {
newest = item.next;
}
}
--queueSize;
}
private Item getItem(K key) {
Item item = keyMap.get(key);
if (item != null) {
++hitCount;
removeFromQueue(item);
insertIntoQueue(item);
} else {
++missCount;
}
IntervalLog.intervalLog(Log.VERBOSE_LEVEL, name, "Hit/miss ratio: " + hitCount + "/" + missCount + ".");
return item;
}
}