Advanced HashMap, TreeMap & LinkedHashMap
Go beyond put and get : count and group in one line with merge / computeIfAbsent , navigate sorted keys with TreeMap , build an LRU cache from LinkedHashMap , and update safely across threads with ConcurrentHashMap .
Learn Advanced HashMap, TreeMap & LinkedHashMap in our free Java course — a beginner-friendly interactive lesson with worked examples, a practice exercise…
Part of the free Java course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Before You Start
You should already be comfortable with the Collections internals and the hashCode() / equals() contract. This lesson assumes you can create a basic HashMap<K, V> and iterate it — here you'll learn the methods that make Maps genuinely powerful.
What You'll Learn
🗄️ A Real-World Analogy
Think of the three core Map types as three ways of organising a filing cabinet:
- HashMap is a cabinet where folders are thrown into whichever drawer is free. Finding any one folder is instant, but the order is chaos.
- LinkedHashMap threads a string through every folder in the order you filed them — fast lookup and a predictable walk from first to last (or, with access order, "most recently touched" last — exactly what an LRU cache needs).
- TreeMap is an alphabetised cabinet. Filing takes a moment longer, but you can ask "the folder just before M" or "everything from D to H" — the navigation methods you'll use below.
The modern methods — merge , computeIfAbsent , getOrDefault — are like a clerk who handles "add to the folder, or start a new one if it doesn't exist" in a single instruction, instead of you checking the drawer first every time.
1️⃣ The Methods That Replace "check, then put"
The old way to count things was a three-line dance: get the value, test for null , then put back the incremented number. Java 8 collapsed all of that into a handful of methods. Learn these five and most of your Map code becomes one line.
- getOrDefault(key, fallback) — read a key, but return fallback instead of null when it's missing.
- putIfAbsent(key, value) — insert only when the key isn't already there; returns the value that ended up staying.
- merge(key, value, fn) — if absent, store value ; if present, store fn(oldValue, value) . The counting workhorse.
- computeIfAbsent(key, fn) — if the key is missing, run fn(key) , store the result, and return it. Perfect for "get-or-create a List".
- computeIfPresent(key, fn) — only transform a value that already exists; does nothing for missing keys.
🎯 Your Turn #1 — Word Frequency
Fill in the blanks so merge counts each word and getOrDefault reads a count safely. The expected output is in the comments — match it exactly.
2️⃣ Ordering: LinkedHashMap, TreeMap & EnumMap
A plain HashMap gives you no order at all — iteration can appear in any sequence and may change between runs. When order matters, pick a different implementation:
Iterates in insertion order by default. Pass accessOrder=true to instead order by "most recently used" — the basis of an LRU cache.
Keeps keys sorted (natural order or a Comparator ) and adds navigation: floorKey , ceilingKey , subMap , firstKey , lastKey .
A tiny, very fast map whose keys are an enum . Backed by an array, it iterates in the enum's declaration order. Use it whenever your keys are an enum.
TreeMap navigation, in plain English: floorKey(x) = the largest key ≤ x ; ceilingKey(x) = the smallest key ≥ x ; subMap(from, to) = a live view of keys in the range [from, to) (from inclusive, to exclusive). These turn "find the price tier for a spend" into a single call.
🎯 Your Turn #2 — Price Tiers
Use TreeMap navigation to find which discount tier a spend qualifies for, then show every tier from 100 upward. Decide carefully between floorKey and ceilingKey .
3️⃣ Caches & Concurrency: LinkedHashMap LRU + ConcurrentHashMap
Two production patterns pull all of this together.
LRU cache. A cache with a size limit must drop something when it fills up; "least recently used" is the classic policy. Construct a LinkedHashMap with access order on, then override removeEldestEntry to return true once the size passes your capacity. Because access order moves every touched entry to the end, the eldest entry is always the least-recently-used one — eviction is automatic.
Thread safety. A normal HashMap is corrupted by concurrent writes. ConcurrentHashMap lets many threads read and write at once, and its merge , compute , and computeIfAbsent methods run atomically — so incrementing a counter from many threads is correct without you writing any locks.
🧩 Mini-Challenge — Group People by City
Support is faded now: only an outline is given. Build a TreeMap<String, List<String>> grouping each person under their city using computeIfAbsent , then print it. The expected output is in the comments.
Common Errors & Fixes
Pro Tips
💡 One-line counter: map.merge(key, 1, Integer::sum) is the idiomatic way to count occurrences.
💡 get-or-create: map.computeIfAbsent(key, k -> new ArrayList<>()).add(item) builds a multi-map without any null checks.
💡 Prefer EnumMap for enum keys: it's faster and uses less memory than a HashMap, and iterates in enum order for free.
💡 Need both order kinds? Sorted snapshot of a HashMap: wrap it once — new TreeMap <> (hashMap) — to print or scan in sorted order without changing the original.
📋 Quick Reference
Method / Type
Purpose
Example
getOrDefault(k, d)
Read with a fallback
map.getOrDefault("x", 0)
putIfAbsent(k, v)
Insert only if missing
map.putIfAbsent("x", 1)
merge(k, v, fn)
Accumulate (count/sum)
map.merge("x", 1, Integer::sum)
computeIfAbsent(k, fn)
Get-or-create a value
m.computeIfAbsent(k, x -> new ArrayList<>())
computeIfPresent(k, fn)
Transform if present
m.computeIfPresent(k, (key, v) -> v - 1)
floorKey / ceilingKey
Largest ≤ / smallest ≥
tree.floorKey(65)
subMap(from, to)
Range view [from, to)
tree.subMap(60, 80)
LinkedHashMap(…, true)
Access order (LRU)
new LinkedHashMap(16, .75f, true)
ConcurrentHashMap
Thread-safe, atomic ops
chm.merge(k, 1, Integer::sum)
❓ Frequently Asked Questions
🎉 Lesson Complete!
You can now count and group in one line with merge and computeIfAbsent , pick the right Map for the ordering you need, navigate sorted keys with TreeMap , build an LRU cache from LinkedHashMap , and update maps safely across threads with ConcurrentHashMap .
Next: Multithreading — create and manage threads, and use thread pools with ExecutorService .
Practice quiz
What does map.merge(word, 1, Integer::sum) do the FIRST time a word is seen?
- Throws a NullPointerException
- Stores the value 1
- Stores 0
- Does nothing
Answer: Stores the value 1. If the key is absent, merge stores the given value (1). If present, it applies the function to (old, new).
After counting 'red blue red green red blue' with merge, what does new TreeMap<>(counts) print?
- {red=3, blue=2, green=1}
- {blue=2, green=1, red=3}
- {green=1, red=3, blue=2}
- {blue=2, red=3, green=1}
Answer: {blue=2, green=1, red=3}. A TreeMap sorts keys alphabetically: blue=2, green=1, red=3.
For a TreeMap with keys 50,60,70,80, what does floorKey(65) return?
- 70
- 65
- 60
- 50
Answer: 60. floorKey(x) returns the largest key ≤ x. The largest key ≤ 65 is 60.
Which method runs its function ONLY when the key is missing, ideal for get-or-create a List?
- putIfAbsent
- computeIfPresent
- computeIfAbsent
- merge
Answer: computeIfAbsent. computeIfAbsent(k, fn) only runs fn when the key is absent, so the container is built lazily and reused.
Which Map implementation iterates in INSERTION order by default?
- HashMap
- TreeMap
- LinkedHashMap
- EnumMap
Answer: LinkedHashMap. LinkedHashMap remembers insertion order. HashMap has no order; TreeMap sorts; EnumMap uses enum declaration order.
To build an LRU cache from a LinkedHashMap you construct it with access order on and then:
- Override toString()
- Override removeEldestEntry to return true once size exceeds capacity
- Call clear() periodically
- Set the load factor to 1.0
Answer: Override removeEldestEntry to return true once size exceeds capacity. Access order moves touched entries to the end; removeEldestEntry evicts the least-recently-used entry automatically.
Which statement about null keys/values is correct?
- TreeMap allows a null key
- ConcurrentHashMap allows null values
- HashMap allows one null key and null values
- All three forbid null entirely
Answer: HashMap allows one null key and null values. HashMap permits one null key and null values; TreeMap forbids null keys; ConcurrentHashMap forbids both.
stock has {apple=5}. After stock.computeIfPresent("pear", (k,v) -> v-2), stock.get("pear") is:
- 3
- -2
- 0
- null
Answer: null. computeIfPresent does nothing when the key is absent, so 'pear' is never added — get returns null.
When should you prefer TreeMap over HashMap?
- When you never iterate in order
- When you need sorted keys or navigation like floorKey/subMap
- When you want the fastest possible average lookup
- When keys can be null
Answer: When you need sorted keys or navigation like floorKey/subMap. TreeMap gives sorted order and navigation at O(log n). HashMap is faster (avg O(1)) when order is not needed.
Why is ConcurrentHashMap preferred over Collections.synchronizedMap for concurrency?
- It locks the whole map on every call
- Its merge/compute methods run atomically and it scales better under many threads
- It allows null values
- It keeps keys sorted
Answer: Its merge/compute methods run atomically and it scales better under many threads. ConcurrentHashMap offers atomic update methods and finer-grained concurrency; synchronizedMap locks the entire map.
Continue this course
- Previous: Previous Lesson
- Next: Next Lesson