LinkedHashMap LRU Cache - Missing removeEldestEntry OOM
- LinkedHashMap maintains insertion order; HashMap does not.
- Pass accessOrder=true to LinkedHashMap constructor for LRU semantics.
- Override removeEldestEntry() to implement a bounded LRU cache in 10 lines.
LinkedHashMap and LinkedHashSet maintain insertion order — iteration yields elements in the order they were added. LinkedHashMap also supports access-order mode, where recently accessed entries move to the tail — the foundation of an LRU cache. They have the same O(1) average complexity as HashMap and HashSet with a small memory overhead for the doubly-linked list.
Quick Debug Steps for LinkedHashMap Issues
LRU cache not evicting
Add a debug log in removeEldestEntry: System.out.println("Eldest: " + eldest + " size=" + size());Use jmap -histo:live <pid> | head -20 to see number of entries in the map.LinkedHashSet iteration order unexpected
Print the set after each insertion to trace order changes.Check hashCode() of elements — if mutable, hash can change and affect order.Production Incident
size() > capacity. Also set accessOrder=true to get LRU semantics. Example: new LinkedHashMap<K,V>(capacity, 0.75f, true) and override removeEldestEntry.Production Debug GuideCommon symptoms and their root causes
get() is called on access – otherwise the entry order is not updated.HashMap and HashSet are fast but their iteration order is undefined — you get a different order on different JVM runs. LinkedHashMap and LinkedHashSet solve this by maintaining a doubly-linked list through their entries in addition to the hash table.
The most practical consequence: if you need a Map that remembers insertion order (for example, building JSON, preserving request parameter order, or implementing an LRU cache), LinkedHashMap is the right tool.
LinkedHashMap — Insertion Order
LinkedHashMap extends HashMap and adds a doubly-linked list that runs through all entries. The list defines the iteration ordering, which by default is insertion-order: the order in which keys were inserted into the map. This is guaranteed as long as no rehash operations disrupt the list. Rehashing does not change the order because the list is maintained separately from the bucket array.
When you put a key that already exists, the mapping is updated but the entry stays in its original position in the list. This is the key behavior that differs from access-order mode.
package io.thecodeforge.java.collections; import java.util.LinkedHashMap; import java.util.Map; public class LinkedHashMapDemo { public static void main(String[] args) { // Insertion-order map Map<String, Integer> scores = new LinkedHashMap<>(); scores.put("Alice", 95); scores.put("Charlie", 88); scores.put("Bob", 92); // Iteration order = insertion order — always scores.forEach((name, score) -> System.out.println(name + ": " + score)); // Alice: 95 // Charlie: 88 // Bob: 92 // HashMap would print in some arbitrary order — not predictable } }
Charlie: 88
Bob: 92
LRU Cache Using Access-Order LinkedHashMap
Pass true as the third constructor argument (accessOrder=true) and override removeEldestEntry to cap the size. This is a textbook LRU cache.
In access-order mode, every get or put moves the accessed entry to the tail of the list. The head of the list is the least recently used entry. When removeEldestEntry returns true (typically when size() exceeds a threshold), that eldest (head) entry is removed. This gives you O(1) get, put, and eviction — but the eviction is lazy, occurring only on new inserts after the threshold is passed.
This implementation is not thread-safe. For a concurrent LRU cache, use Caffeine or Guava's CacheBuilder.
package io.thecodeforge.java.collections; import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends LinkedHashMap<K, V> { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); // accessOrder = true this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; // evict when over capacity } public static void main(String[] args) { LRUCache<Integer, String> cache = new LRUCache<>(3); cache.put(1, "one"); cache.put(2, "two"); cache.put(3, "three"); System.out.println(cache.keySet()); // [1, 2, 3] cache.get(1); // access 1 — moves to tail (most recently used) System.out.println(cache.keySet()); // [2, 3, 1] cache.put(4, "four"); // capacity exceeded — removes 2 (least recently used) System.out.println(cache.keySet()); // [3, 1, 4] System.out.println(cache.containsKey(2)); // false — evicted } }
[2, 3, 1]
[3, 1, 4]
false
get(), entries never move. So a put-only pattern makes it behave like insertion-order with eviction of the first inserted.get() on every access.LinkedHashSet — Insertion-Order Set
LinkedHashSet is backed by a LinkedHashMap (with dummy values). It inherits all properties of HashSet plus a guaranteed iteration order — the order in which elements were inserted.
This is incredibly useful when you need to deduplicate a stream of data while preserving the order of first occurrence. Common use cases include building a list of unique recent actions, maintaining a priority order for a job queue, or generating a unique list of visited URLs in a web crawler.
Like LinkedHashMap, the order is unaffected by rehashing and is consistent over time if no modifications are made.
package io.thecodeforge.java.collections; import java.util.LinkedHashSet; import java.util.Set; public class LinkedHashSetDemo { public static void main(String[] args) { Set<String> visited = new LinkedHashSet<>(); visited.add("/home"); visited.add("/about"); visited.add("/home"); // duplicate — ignored visited.add("/contact"); System.out.println(visited); // [/home, /about, /contact] // Order preserved, duplicates removed // Deduplication while preserving order String[] input = {"banana", "apple", "banana", "cherry", "apple"}; Set<String> deduped = new LinkedHashSet<>(java.util.Arrays.asList(input)); System.out.println(deduped); // [banana, apple, cherry] } }
[banana, apple, cherry]
Performance and Memory Overhead
LinkedHashMap and LinkedHashSet have the same O(1) average complexity for add, remove, contains, and get as their parent classes. The only difference is the linked list, which adds two references (prev, next) per entry. On a 64-bit JVM with compressed OOPs, that's about 16 bytes per entry extra.
Iteration over a LinkedHashMap is actually slightly faster than over a HashMap because it traverses the linked list rather than the entire bucket array (which can be sparse). But for very large maps, the extra memory can be significant.
Access-order mode does not add computational overhead on get/put beyond the O(1) operation to move the entry to the tail (which is a constant-time pointer update).
Thread Safety and Synchronization
Like HashMap and HashSet, LinkedHashMap and LinkedHashSet are not thread-safe. If multiple threads access them concurrently and at least one modifies the structure, external synchronization is required.
You can wrap them with Collections.synchronizedMap(new LinkedHashMap(...)), but that only protects individual method calls. Iteration still requires manual synchronization. For an LRU cache that needs concurrency, use java.util.concurrent.ConcurrentHashMap with additional logic, or better, use a dedicated library like Caffeine.
One common pattern is to use a synchronized block around the entire put-get-evict sequence in an LRU cache, but that quickly becomes a bottleneck under high concurrency.
package io.thecodeforge.java.collections; import java.util.*; public class SynchronizedLRU<K, V> { private final Map<K, V> cache; private final int capacity; public SynchronizedLRU(int capacity) { this.cache = Collections.synchronizedMap( new LinkedHashMap<K, V>(capacity, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } } ); this.capacity = capacity; } // But iteration is not thread-safe; you must synchronize on cache public List<Map.Entry<K, V>> snapshot() { synchronized (cache) { return new ArrayList<>(cache.entrySet()); } } }
snapshot() method shows the correct way to iterate safely: synchronize on the map and copy to a list.Collections.synchronizedMap() works but synchronizes every operation.| Property | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| Iteration order | Undefined (depends on bucket structure) | Insertion-order or access-order | Sorted according to natural ordering or Comparator |
| Time complexity (get/put) | O(1) average | O(1) average | O(log n) |
| Memory overhead | Base | + ~16 bytes per entry (prev/next pointers) | + ~32 bytes per entry (color, parent, left, right, key, value) |
| Access-order LRU support | No | Yes (accessOrder=true + override removeEldestEntry) | No |
| Thread-safe | No | No | No |
| Often used for | General-purpose mapping | Caches, ordered JSON, preserving param order | Range queries, sorted data |
🎯 Key Takeaways
- LinkedHashMap maintains insertion order; HashMap does not.
- Pass accessOrder=true to LinkedHashMap constructor for LRU semantics.
- Override removeEldestEntry() to implement a bounded LRU cache in 10 lines.
- LinkedHashSet deduplicates while preserving insertion order.
- Memory overhead vs HashMap/HashSet: one extra prev/next pointer per entry.
- Neither is thread-safe;
Collections.synchronizedMap()does not protect iteration.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QHow does LinkedHashMap differ from HashMap?JuniorReveal
- QHow would you implement an LRU cache using Java's built-in classes?Mid-levelReveal
- QWhen is LinkedHashSet preferable to HashSet?JuniorReveal
- QWhat is the difference between insertion-order and access-order in LinkedHashMap?Mid-levelReveal
- QHow does removeEldestEntry work? What would happen if you forget to override it?Mid-levelReveal
Frequently Asked Questions
When should I use LinkedHashMap instead of HashMap?
When iteration order matters — building an ordered JSON response, preserving URL parameter order, displaying results in insertion order, or testing where you need predictable output. If order does not matter, HashMap is slightly faster.
Is LinkedHashMap thread-safe?
No, like HashMap. Wrap it with Collections.synchronizedMap() for basic thread safety, but this does not protect iteration. For a thread-safe LRU cache in production, use Caffeine or Guava's Cache instead.
Can I use LinkedHashMap as a FIFO queue?
Yes, with insertion-order and a fixed capacity via removeEldestEntry, you get a simple FIFO eviction policy (oldest inserted is evicted first). For a true FIFO queue, however, a LinkedList or ArrayDeque is more appropriate.
What is the initial capacity parameter in LinkedHashMap constructor?
It sets the initial size of the hash table (the number of buckets). It does NOT set a maximum size. The capacity you pass is only a tuning parameter for resizing; the map can grow beyond it if you do not override removeEldestEntry.
Why does my LinkedHashMap iteration order change after rehashing?
It doesn't. The linked list ensures ordering is preserved across rehashes. If you see order changes, you might have removed and reinserted entries, or used a constructor that copies from another map (which may have a different order).
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.