Home of The JavaSpecialists' Newsletter

246LRU Cache From LinkedHashMap

Posted: 2017-02-28Category: Tips and TricksJava Version: 1.4+Dr. Heinz M. Kabutz
 

Abstract: The LinkedHashMap has an interesting feature, where we can order elements in "access order", rather than the default "insertion order". This allows us to build a light-weight LRU cache.

 

Welcome to the 246th edition of The Java(tm) Specialists' Newsletter. I felt like a celebrity last week. I was recognized by someone on the flight from Chania to Athens and then by someone else from Athens to Zurich. As I arrived at Voxxed Zurich, I was recognized again. And this continued the whole day long. Java programmers would come up to me and tell me that they love my newsletter or that they are coming to JCrete this year. The next day we travelled to CERN, where we were taken on a tour of CMS. Nope, not the CMS you're thinking of, but a huge machine that detects subatomic particles when protons collide at close to the speed of light. It was a great honour to see this and I felt even more like royalty. This continued the next day, when I gave a lecture at Voxxed CERN to an almost empty main auditorium. Stephen Hawking had spoken in the same lecture theatre. Now some speakers would have been disappointed with the low turnout, but I was blown away and honoured beyond imagination. Next door, my long-time friend Joshua Long was speaking at the same time. He is a real rock star speaker and sucks in crowds like a black hole. It would have been obvious that he should've been given the main room. But they gave it to me instead. Afterwards one of the other speakers told me: "I enjoyed your talk. I tried going to Josh's talk, but it was standing room only, so I came to yours instead." ha ha :-)

My only regret was booking my flight home on the same day, which meant having to leave CERN before lunch. I seriously considered booking new flights, but unfortunately stuck to my original schedule. Getting to my flight was a challenge. I got completely lost trying to find my way out of the CERN buildings. There are so many corridors and parts that are closed. A bit like navigating your car through an old European town with a hundred one-way roads. And on my flight from Geneva to Athens, a Java friend sat in the seat in front of me. What an intoxicating three days of fun :-)

NEW: Refactoring to Java 8 Lambdas and Streams Workshop Are you currently using Java 6 or 7 and would like to see how Java 8 can improve your code base? Are you tired of courses that teach you a whole bunch of techniques that you cannot apply in your world? Check out our one day intensive Refactoring to Java 8 Lambdas and Streams Workshop.

LRU Cache From LinkedHashMap

Least-recently-used. LRU. Meaning that elements are removed when they have been used the least recently. As opposed to most recently. It's very confusing, I admit.

LinkedHashMap was already in Java 1.4, but I keep on meeting Java programmers who have never heard of it. Granted, there are a lot of collection classes in the JDK, not to speak of all the third party libraries, such as from Google, Apache and Eclipse. LinkedHashMap is a blend of a linked list and a hash map. By default when you iterate over it, you get back the insertion order. This is useful when you want to build up a dictionary of entries that should be fast to access, but also have a very specific order. This is different to a sorted map such as TreeMap.

An oft overlooked feature of LinkedHashMap is the ability to also retrieve elements in the order in which they were last accessed. A special constructor is used to create this type of LinkedHashMap: new LinkedHashMap(initialCapacity, loadFactor, true) - the true means "accessOrder" instead of the default, which is "insertionOrder". For example, consider this code:

import java.util.*;

public class AccessOrderTest {
  public static void main(String... args) {
    test(new LinkedHashMap<>()); //insertion order
    System.out.println();
    test(new LinkedHashMap<>(16, 0.75f, true)); //access order
    System.out.println();
    test(new TreeMap<>()); //sorted order
    System.out.println();
    test(new HashMap<>()); //undefined order
  }

  private static void test(Map<Integer, String> map) {
    System.out.println(map.getClass().getSimpleName());
    map.put(42, "Meaning");
    map.put(7, "Days Per Week");
    map.put(86400, "Seconds");
    map.put(1, "Highlander");

    System.out.println("map = " + map);
    System.out.println("map.get(7) = " + map.get(7));
    System.out.println("map = " + map);
  }
}
  

When we run this, we can see that by default, the insertion order is maintained in the first LinkedHashMap. In the second LinkedHashMap, entry 7 goes to the end after being accessed. TreeMap stores entries sorted. And HashMap in an undefined order:

LinkedHashMap
map = {42=Meaning, 7=Days Per Week, 86400=Seconds, 1=Highlander}
map.get(7) = Days Per Week
map = {42=Meaning, 7=Days Per Week, 86400=Seconds, 1=Highlander}

LinkedHashMap
map = {42=Meaning, 7=Days Per Week, 86400=Seconds, 1=Highlander}
map.get(7) = Days Per Week
map = {42=Meaning, 86400=Seconds, 1=Highlander, 7=Days Per Week}

TreeMap
map = {1=Highlander, 7=Days Per Week, 42=Meaning, 86400=Seconds}
map.get(7) = Days Per Week
map = {1=Highlander, 7=Days Per Week, 42=Meaning, 86400=Seconds}

HashMap
map = {86400=Seconds, 1=Highlander, 7=Days Per Week, 42=Meaning}
map.get(7) = Days Per Week
map = {86400=Seconds, 1=Highlander, 7=Days Per Week, 42=Meaning}
  

So far we have seen that we can store entries in the order in which they were last accessed. But we can also automatically remove the eldest entries if we have exceeded our maximum number. To do that we subclass LinkedHashMap and add a maxEntries field. We also override the removeEldestEntry(Map.Entry<K, V> eldest) method from LinkedHashMap to return true when we have reached our maximum size.

import java.util.*;

public class LRUCache<K, V> extends LinkedHashMap<K, V> {
  private final int maxEntries;
  private static final int DEFAULT_INITIAL_CAPACITY = 16;
  private static final float DEFAULT_LOAD_FACTOR = 0.75f;

  public LRUCache(int initialCapacity,
                  float loadFactor,
                  int maxEntries) {
    super(initialCapacity, loadFactor, true);
    this.maxEntries = maxEntries;
  }

  public LRUCache(int initialCapacity,
                  int maxEntries) {
    this(initialCapacity, DEFAULT_LOAD_FACTOR, maxEntries);
  }

  public LRUCache(int maxEntries) {
    this(DEFAULT_INITIAL_CAPACITY, maxEntries);
  }

  // not very useful constructor
  public LRUCache(Map<? extends K, ? extends V> m,
                  int maxEntries) {
    this(m.size(), maxEntries);
    putAll(m);
  }

  protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
    return size() > maxEntries;
  }
}
  

Here's an example of how it can be used. Look at the test code and try to imagine what the output will be before scrolling down:

import java.util.*;

public class LRUCacheTest {
  public static void main(String... args) {
    Map<Integer, String> cache = new LRUCache<>(5);
    for (int i = 0; i < 10; i++) {
      cache.put(i, "hi");
    }
    // entries 0-4 have already been removed
    // entries 5-9 are ordered
    System.out.println("cache = " + cache);

    System.out.println(cache.get(7));
    // entry 7 has moved to the end
    System.out.println("cache = " + cache);

    for (int i = 10; i < 14; i++) {
      cache.put(i, "hi");
    }
    // entries 5,6,8,9 have been removed (eldest entries)
    // entry 7 is at the beginning now
    System.out.println("cache = " + cache);

    cache.put(42, "meaning of life");
    // entry 7 is gone too
    System.out.println("cache = " + cache);
  }
}
  

Here is the output now. You can see that entry 7 jumps around a bit, due to it being accessed directly:

cache = {5=hi, 6=hi, 7=hi, 8=hi, 9=hi}
hi
cache = {5=hi, 6=hi, 8=hi, 9=hi, 7=hi}
cache = {7=hi, 10=hi, 11=hi, 12=hi, 13=hi}
cache = {10=hi, 11=hi, 12=hi, 13=hi, 42=meaning of life}
  

I hope you found this useful. I was certainly quite surprised when I discovered that you could do this with the LinkedHashMap.

Kind regards

Heinz

 

Related Articles

Browse the Newsletter Archive

About the Author

demo

Java Champion, author of the Javaspecialists Newsletter, conference speaking regular... About Heinz

Java Training

We deliver relevant courses, by top Java developers to produce more resourceful and efficient programmers within their organisations.

Java Consulting

Nobody ever wants to call a Java performance consultant, but with first-hand experience repairing and improving commercial Java applications - JavaSpecialists are a good place to start...

Threading Emergency?

If your system is down, we will review it for 15 minutes and give you our findings for just 1 € without any obligation.