/* This is an efficient implementation of a fixed size cache using Java generics. * It uses a least recently used eviction policy implemented via a combination hashtable/linked list data structure. * Written by Alrecenk October 2015 because I felt like playing with generics. */ import java.util.HashMap; import java.util.Random; public class Cache<KeyType, RecordType>{ // Example usage of generic cache. public static void main(String args[]){ //Initialize integer to String cache with maximum size of 500. Cache<Integer, String> cache = new Cache<Integer, String>(500); int hits = 0, misses = 0; for(int k=0;k<100000;k++){ // Caches perform well in uneven key distributions. float power = 1 ; for(int j=0;j<5;j++){ power*=Math.random(); } int key = (int)(power*10000); // Keys up to 9999 but more common in lower numbers. // Try to fetch key from cache. String value = cache.get(key); if(value == null){ // If it wasn't in the cache then do the full operation and cache it. value = expensiveDeterministicOperation(key); cache.put(key, value); misses++; }else{ hits++; } } System.out.println(cache); // Print out the keys in the cache System.out.println("Hits:" + hits +" Misses:" + misses); } // An expensive but deterministic operation such as retrieving a file from a remote server. public static String expensiveDeterministicOperation(Integer i){ Random r = new Random(i); String s = ""; for(int k=0;k<50;k++){ s+= (char)(r.nextInt(26) + (int)'a'); } return s ; } //------------Generic Cache Implementation Begins Here --------- // Hash table to fetch records for keys. // CacheValue includes record and pointer to queue position. HashMap<KeyType,CacheValue> table; // Doubly linked queue to keep track of least recently used item. QueueNode head, tail; // Allowed capacity of cache and how much is already full. int capacity, filled = 0 ; public Cache(int size) { capacity = size; table = new HashMap<KeyType,CacheValue>(2*size); // Double table size reduces hash collisions. } //Nodes for queue keeping track of item access order. private class QueueNode{ QueueNode next ; QueueNode previous ; KeyType key; public QueueNode(KeyType k){ key = k ; } void removeFromQueue(){ if(this==head)head = next; if(this==tail)tail = previous; if(previous!=null)previous.next = next; if(next!=null)next.previous = previous; next = null; previous = null; } void removeFromTable(){ table.remove(key); } } // Groups a queue node and a record, so they can be fetched from the hash table together. private class CacheValue{ public CacheValue(RecordType r){ record = r ; } QueueNode node; RecordType record; } // Fetch a Record from the cache or null if it is not present. // Keeps track of accesses for future evictions. public RecordType get(KeyType k){ CacheValue c = table.get(k); if(c == null)return null; // If found move to head of queue. QueueNode n = c.node; n.removeFromQueue(); n.next = head; if(head!=null)head.previous = n; head = n; return c.record; } // Puts a record in the cache. Replaces record with matching key if found. // If cache is full evicts least recently accessed item. public void put(KeyType k, RecordType r){ //If item is in the cache just update its value. CacheValue d = table.get(k); if(d!=null){ d.record = r; }else{ // If not in the cache then add it. CacheValue c = new CacheValue(r); QueueNode n = new QueueNode(k); c.node = n; table.put(k,c); // Put the item at the head of the queue. n.next = head; if(head!=null)head.previous = n; head = n; // First item in queue is also the tail if(tail==null)tail = head; filled++; //If the cache is full remove the item at the tail of the queue. if(filled > capacity){ QueueNode t = tail.previous; tail.removeFromTable(); tail.removeFromQueue(); tail = t; filled--; } } } // Prints the keys for the items currently in the cache. public String toString(){ QueueNode node = head; String s = "Keys:" + node.key; while(node != tail){ node = node.next; if(node==null){ break; } s+=", " + node.key; } return s ; } }