Java TreeMap Internal Working
Java TreeMap Internal Working
TreeMap<K, V> stores every key-value entry in a self-balancing Red-Black tree. Every put(), get(), and remove() is a tree traversal — comparing keys at each node to decide left or right, guaranteed to complete in O(log n) regardless of insertion order. The sorted key order that TreeMap delivers is a direct consequence of how a binary search tree works: in-order traversal of the tree always yields keys in ascending order. This is what separates TreeMap from HashMap — HashMap is faster on average, but TreeMap is the only standard Map that lets you ask "what is the key closest to X?" or "give me all entries with keys between A and B?" in O(log n).
What Is Inside a TreeMap?
TreeMap<K, V> is backed by a single Red-Black tree. The class holds a reference to the root node, a comparator (or null for natural ordering), the entry count, and a modification counter for fail-fast iteration.
TREEMAP INTERNAL FIELDS (simplified from JDK source):
private transient Entry<K,V> root; ← root of the Red-Black tree
private transient int size; ← number of key-value entries
private final Comparator<? super K> comparator; ← null means natural ordering
private transient int modCount; ← fail-fast iterator support
private static final boolean RED = false;
private static final boolean BLACK = true;
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left; ← left child (keys < this.key)
Entry<K,V> right; ← right child (keys > this.key)
Entry<K,V> parent; ← parent node (null for root)
boolean color; ← RED or BLACK
}
EMPTY TREEMAP:
root = null
size = 0
comparator = null (uses Comparable.compareTo())
modCount = 0
AFTER put("banana",2), put("apple",1), put("cherry",3):
root
|
[banana/BLACK]
/ \
[apple/RED] [cherry/RED]
left=null left=null
right=null right=null
in-order traversal: apple → banana → cherry (alphabetical order)
This is what entrySet() and keySet() iteration always returns.
Basic Overview — How Each Core Method Works
OPERATION INTERNAL MECHANISM COMPLEXITY
───────────────────────────────────────────────────────────────────────────────
put(k, v) BST insert: compare at each node, go left/right O(log n)
then fix Red-Black properties (rotations/recolor)
get(k) BST search: compare at each node, go left/right O(log n)
until match or null
remove(k) BST delete: find node, splice out, fix RB props O(log n)
containsKey(k) BST search, same as get O(log n)
firstKey() leftmost node of tree O(log n)
lastKey() rightmost node of tree O(log n)
floorKey(k) largest key <= k O(log n)
ceilingKey(k) smallest key >= k O(log n)
lowerKey(k) largest key strictly < k O(log n)
higherKey(k) smallest key strictly > k O(log n)
headMap(toKey) view of entries with keys < toKey O(log n)
tailMap(fromKey) view of entries with keys >= fromKey O(log n)
subMap(from, to) view of entries with keys in [from, to) O(log n)
size() return size field O(1)
isEmpty() return size == 0 O(1)
in-order iteration all keys in ascending order O(n)
(entrySet, keySet, values — all return sorted views)
KEY RULE:
All single-key operations are O(log n) — no O(1) average like HashMap.
In exchange: keys are always sorted, range queries work, no hashCode needed.
When to Use TreeMap
USE TreeMap WHEN:
1. Sorted key iteration is required
— entrySet(), keySet(), values() always return entries in key order
— event logs sorted by timestamp, leaderboard sorted by score
2. Range queries are needed
— "all orders placed between 9 AM and 5 PM"
— headMap(toKey), tailMap(fromKey), subMap(from, to)
— these views are live backed maps, not copies
3. Closest-match lookup is needed
— floorKey(k): largest key ≤ k (last event at or before timestamp)
— ceilingKey(k): smallest key ≥ k (next scheduled slot)
— lowerKey(k) / higherKey(k): strict variants
4. Keys implement Comparable or a Comparator is provided
— String, Integer, Long, LocalDate all implement Comparable
— Custom sort order: new TreeMap<>(Comparator.reverseOrder())
DO NOT USE TreeMap WHEN:
— Pure key-value lookup speed is the priority: use HashMap (O(1) avg)
— Insertion order iteration is needed: use LinkedHashMap
— Keys are null: TreeMap NEVER allows null keys (NullPointerException)
— Keys do not implement Comparable and no Comparator is provided:
ClassCastException at runtime on first put()
TREEMAP vs HASHMAP vs LINKEDHASHMAP:
HashMap: O(1) avg get/put, no order guarantee, allows null key
LinkedHashMap: O(1) avg get/put, insertion order, allows null key
TreeMap: O(log n) get/put, sorted key order, null key rejected
How TreeMap Works Internally
The Red-Black Tree
A Red-Black tree is a binary search tree with four structural rules that guarantee the tree stays balanced after every insert and delete. These rules bound the height to O(log n), which is why every TreeMap operation is O(log n) regardless of insertion sequence.
RED-BLACK TREE RULES:
1. Every node is either RED or BLACK
2. The root is always BLACK
3. Every null leaf (sentinel) is BLACK
4. A RED node's children are always BLACK
(no two consecutive RED nodes on any path)
5. Every path from any node to its descendant null leaves
has the same number of BLACK nodes (Black-Height property)
WHY RULES 4 AND 5 TOGETHER GUARANTEE BALANCE:
Rule 5: equal black nodes on all paths means no path can be
more than twice as long as any other path.
Rule 4: red nodes cannot be consecutive, so the longest
possible path alternates red/black: 2 × (black height).
Result: height ≤ 2 × log₂(n+1) → O(log n) guaranteed.
Compare to a plain BST:
put(1), put(2), put(3), put(4) ... put(n) in order
→ degrades into a linear chain → O(n) get/put
TreeMap NEVER degrades regardless of insertion order.
TREE ROTATIONS — used after insert/delete to restore balance:
Left Rotation (x becomes right child of its old left child):
x y
/ \ / \
A y → x C
/ \ / \
B C A B
Right Rotation (mirror of left):
x y
/ \ / \
y C → A x
/ \ / \
A B B C
Rotations preserve BST ordering while restructuring the tree shape.
A put() may require at most O(log n) recoloring and 2 rotations.
A remove() may require at most O(log n) recoloring and 3 rotations.
put() — BST Insert and Red-Black Fix
put("grape", 4) into tree containing apple/banana/cherry/date:
CURRENT TREE (simplified):
banana (BLACK)
/ \
apple (BLACK) date (BLACK)
/ \
cherry (RED) grape? ← to be inserted
STEP 1 — BST DESCENT (compare keys at each node):
Start at root (banana):
"grape" > "banana" → go RIGHT → reach date
At date:
"grape" > "date" → go RIGHT → reach null
Insert new Entry("grape", 4) as RED node at date.right
STEP 2 — RED-BLACK FIX (fixAfterInsertion):
New node is RED. Parent (date) is BLACK.
Rule 4: RED parent would violate the rule — but parent is BLACK, so OK.
No rotation needed in this case. Tree remains valid.
CASES THAT REQUIRE ROTATIONS:
Case 1 — Parent RED, Uncle RED:
Recolor: parent → BLACK, uncle → BLACK, grandparent → RED
Continue fix up toward root
Case 2 — Parent RED, Uncle BLACK, node is inner grandchild:
Rotate parent to make node an outer grandchild
Then handle as Case 3
Case 3 — Parent RED, Uncle BLACK, node is outer grandchild:
Rotate grandparent, swap colors of parent and grandparent
Tree balanced — done
get() — BST Binary Search
get("date"):
STEP 1: Start at root (banana)
compare("date", "banana") → "date" > "banana" → go right
STEP 2: At node date
compare("date", "date") → 0 → MATCH
return node.value
STEP 3 (if not found): reach null child → return null
COMPARISON METHOD:
if (comparator != null):
comparator.compare(key, nodeKey) ← use provided Comparator
else:
((Comparable<K>) key).compareTo(nodeKey) ← use natural ordering
This is why null keys are rejected:
null.compareTo(x) → NullPointerException
comparator may also reject null depending on implementation
firstKey(), lastKey(), and NavigableMap Operations
firstKey() — leftmost node:
Entry<K,V> p = root;
while (p.left != null) p = p.left;
return p.key;
This finds the minimum key by always going left.
lastKey() — rightmost node:
Entry<K,V> p = root;
while (p.right != null) p = p.right;
return p.key;
floorKey(k) — largest key ≤ k:
Traverse tree. At each node:
if node.key <= k: this is a candidate floor; go right to find larger valid key
if node.key > k: go left (current node is too large)
Return best candidate found.
ceilingKey(k) — smallest key ≥ k:
Mirror of floorKey: node.key >= k → candidate, go left for smaller valid key
node.key < k → go right
subMap(fromKey, toKey) — live view of entries in [fromKey, toKey):
Returns a SubMap that wraps the TreeMap.
Reads and writes through to the backing TreeMap.
Any put() into the SubMap with a key outside [from, to) throws IllegalArgumentException.
Internally uses comparisons against fromKey and toKey as bounds during iteration.
headMap(toKey) — live view of entries with keys < toKey
tailMap(fromKey) — live view of entries with keys >= fromKey
Core Operations with Examples
Sorted Order, Navigation, and Range Views
1// File: TreeMapBasicsDemo.java
2
3import java.util.Map;
4import java.util.TreeMap;
5
6public class TreeMapBasicsDemo {
7
8 public static void main(String[] args) {
9
10 // TreeMap maintains keys in natural (alphabetical) order automatically
11 TreeMap<String, Integer> scores = new TreeMap<>();
12 scores.put("Meera", 88);
13 scores.put("Arjun", 95);
14 scores.put("Priya", 91);
15 scores.put("Rohan", 79);
16 scores.put("Ananya", 97);
17 scores.put("Karan", 84);
18
19 System.out.println("=== Sorted iteration (always ascending) ===");
20 scores.forEach((name, score) ->
21 System.out.printf(" %-10s %d%n", name, score));
22
23 System.out.println();
24
25 // firstKey / lastKey — O(log n) leftmost / rightmost
26 System.out.println("=== Boundary access ===");
27 System.out.println("firstKey() : " + scores.firstKey());
28 System.out.println("lastKey() : " + scores.lastKey());
29 System.out.println("firstEntry() : " + scores.firstEntry());
30 System.out.println("lastEntry() : " + scores.lastEntry());
31
32 System.out.println();
33
34 // Closest-match navigation — O(log n) each
35 System.out.println("=== Closest-match navigation ===");
36 System.out.println("floorKey(Karan) : " + scores.floorKey("Karan")); // Karan (exact)
37 System.out.println("floorKey(Kavya) : " + scores.floorKey("Kavya")); // Karan (largest <= Kavya)
38 System.out.println("ceilingKey(Karan) : " + scores.ceilingKey("Karan")); // Karan (exact)
39 System.out.println("ceilingKey(Kavya) : " + scores.ceilingKey("Kavya")); // Meera (smallest >= Kavya)
40 System.out.println("lowerKey(Meera) : " + scores.lowerKey("Meera")); // Karan (strictly <)
41 System.out.println("higherKey(Meera) : " + scores.higherKey("Meera")); // Priya (strictly >)
42
43 System.out.println();
44
45 // Range views — live backed maps (not copies)
46 System.out.println("=== Range views (backed by the same TreeMap) ===");
47 System.out.println("headMap(Meera) keys < Meera : " + scores.headMap("Meera").keySet());
48 System.out.println("tailMap(Meera) keys >= Meera : " + scores.tailMap("Meera").keySet());
49 System.out.println("subMap(Karan, Priya) [Karan, Priya): " +
50 scores.subMap("Karan", "Priya").keySet());
51
52 System.out.println();
53
54 // Descending order — descendingMap() returns a reversed view
55 System.out.println("=== Descending iteration ===");
56 scores.descendingMap().forEach((name, score) ->
57 System.out.printf(" %-10s %d%n", name, score));
58 }
59}Output:
=== Sorted iteration (always ascending) ===
Ananya 97
Arjun 95
Karan 84
Meera 88
Priya 91
Rohan 79
=== Boundary access ===
firstKey() : Ananya
lastKey() : Rohan
firstEntry() : Ananya=97
lastEntry() : Rohan=79
=== Closest-match navigation ===
floorKey(Karan) : Karan
floorKey(Kavya) : Karan
ceilingKey(Karan) : Karan
ceilingKey(Kavya) : Meera
lowerKey(Meera) : Karan
higherKey(Meera) : Priya
=== Range views (backed by the same TreeMap) ===
headMap(Meera) keys < Meera : [Ananya, Arjun, Karan]
tailMap(Meera) keys >= Meera : [Meera, Priya, Rohan]
subMap(Karan, Priya) [Karan, Priya): [Karan, Meera]
=== Descending iteration ===
Rohan 79
Priya 91
Meera 88
Karan 84
Arjun 95
Ananya 97
Custom Comparator and Reverse Ordering
1// File: TreeMapComparatorDemo.java
2
3import java.util.Comparator;
4import java.util.TreeMap;
5
6public class TreeMapComparatorDemo {
7
8 public static void main(String[] args) {
9
10 // Reverse alphabetical order using Comparator.reverseOrder()
11 System.out.println("=== Reverse alphabetical order ===");
12 TreeMap<String, Integer> reverseMap =
13 new TreeMap<>(Comparator.reverseOrder());
14 reverseMap.put("Spring", 3);
15 reverseMap.put("Hibernate", 2);
16 reverseMap.put("Kafka", 4);
17 reverseMap.put("Docker", 1);
18 reverseMap.put("Redis", 5);
19 reverseMap.forEach((k, v) -> System.out.printf(" %-12s %d%n", k, v));
20
21 System.out.println();
22
23 // Multi-field comparator: sort by string length then alphabetically
24 System.out.println("=== Sort by length, then alphabetical ===");
25 Comparator<String> byLengthThenAlpha =
26 Comparator.comparingInt(String::length)
27 .thenComparing(Comparator.naturalOrder());
28
29 TreeMap<String, String> techMap = new TreeMap<>(byLengthThenAlpha);
30 techMap.put("Go", "compiled");
31 techMap.put("Java", "compiled");
32 techMap.put("Rust", "compiled");
33 techMap.put("Python", "interpreted");
34 techMap.put("JavaScript", "interpreted");
35 techMap.put("C", "compiled");
36 techMap.forEach((lang, type) ->
37 System.out.printf(" %-14s (%d chars) %s%n", lang, lang.length(), type));
38
39 System.out.println();
40
41 // Case-insensitive TreeMap — String.CASE_INSENSITIVE_ORDER
42 System.out.println("=== Case-insensitive key comparison ===");
43 TreeMap<String, String> caseInsensitive =
44 new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
45 caseInsensitive.put("Admin", "role-admin");
46 caseInsensitive.put("developer","role-dev");
47 caseInsensitive.put("VIEWER", "role-viewer");
48 caseInsensitive.put("manager", "role-mgr");
49
50 System.out.println("get(ADMIN) : " + caseInsensitive.get("ADMIN")); // role-admin
51 System.out.println("get(admin) : " + caseInsensitive.get("admin")); // role-admin
52 System.out.println("get(Admin) : " + caseInsensitive.get("Admin")); // role-admin
53 System.out.println("Sorted keys : " + caseInsensitive.keySet());
54
55 System.out.println();
56
57 // Null key throws immediately regardless of Comparator
58 System.out.println("=== Null key rejection ===");
59 TreeMap<String, Integer> noNullKeys = new TreeMap<>();
60 try {
61 noNullKeys.put(null, 1);
62 } catch (NullPointerException e) {
63 System.out.println("put(null, 1) throws NullPointerException");
64 }
65 }
66}Output:
=== Reverse alphabetical order ===
Spring 3
Redis 5
Kafka 4
Hibernate 2
Docker 1
=== Sort by length, then alphabetical ===
C (1 chars) compiled
Go (2 chars) compiled
Java (4 chars) compiled
Rust (4 chars) compiled
Python (6 chars) interpreted
JavaScript (10 chars) interpreted
=== Case-insensitive key comparison ===
get(ADMIN) : role-admin
get(admin) : role-admin
get(Admin) : role-admin
Sorted keys : [Admin, developer, manager, VIEWER]
=== Null key rejection ===
put(null, 1) throws NullPointerException
pollFirstEntry, pollLastEntry, and Live Submap Mutations
1// File: TreeMapNavigableDemo.java
2
3import java.util.Map;
4import java.util.TreeMap;
5
6public class TreeMapNavigableDemo {
7
8 public static void main(String[] args) {
9
10 // pollFirstEntry / pollLastEntry — remove and return boundary entries
11 System.out.println("=== pollFirstEntry / pollLastEntry ===");
12 TreeMap<Integer, String> timeline = new TreeMap<>();
13 timeline.put(900, "market-open");
14 timeline.put(1030, "lunch-session");
15 timeline.put(1200, "midday-break");
16 timeline.put(1400, "afternoon-session");
17 timeline.put(1530, "market-close");
18
19 System.out.println("Before: " + timeline.keySet());
20 Map.Entry<Integer, String> first = timeline.pollFirstEntry(); // removes min key
21 Map.Entry<Integer, String> last = timeline.pollLastEntry(); // removes max key
22 System.out.println("pollFirstEntry: " + first);
23 System.out.println("pollLastEntry : " + last);
24 System.out.println("Remaining : " + timeline.keySet());
25
26 System.out.println();
27
28 // subMap with both bounds inclusive using subMap(from, true, to, true)
29 System.out.println("=== subMap with inclusive/exclusive bounds ===");
30 TreeMap<Integer, String> priceRange = new TreeMap<>();
31 for (int price = 100; price <= 1000; price += 100) {
32 priceRange.put(price, "product-" + price);
33 }
34 System.out.println("All keys: " + priceRange.keySet());
35
36 // [300, 600] inclusive on both ends
37 Map<Integer, String> midRange =
38 priceRange.subMap(300, true, 600, true);
39 System.out.println("subMap[300, 600]: " + midRange.keySet());
40
41 // headMap with inclusive upper bound
42 Map<Integer, String> cheap = priceRange.headMap(400, true);
43 System.out.println("headMap(400, inclusive): " + cheap.keySet());
44
45 // tailMap with exclusive lower bound
46 Map<Integer, String> expensive = priceRange.tailMap(700, false);
47 System.out.println("tailMap(700, exclusive): " + expensive.keySet());
48
49 System.out.println();
50
51 // Live view — modifications to submap write through to parent
52 System.out.println("=== Live subMap — mutations write through ===");
53 TreeMap<String, Integer> inventory = new TreeMap<>();
54 inventory.put("Apple", 100);
55 inventory.put("Banana", 50);
56 inventory.put("Cherry", 30);
57 inventory.put("Date", 80);
58 inventory.put("Elderberry", 20);
59
60 Map<String, Integer> subView = inventory.subMap("Banana", "Date");
61 System.out.println("SubView before: " + subView.keySet());
62 subView.put("Coconut", 60); // writes through to inventory
63 System.out.println("SubView after put(Coconut): " + subView.keySet());
64 System.out.println("Inventory now: " + inventory.keySet()); // Coconut present
65
66 try {
67 subView.put("Elderberry", 25); // outside subMap range → rejected
68 } catch (IllegalArgumentException e) {
69 System.out.println("put(Elderberry) outside range → IllegalArgumentException");
70 }
71 }
72}Output:
=== pollFirstEntry / pollLastEntry ===
Before: [900, 1030, 1200, 1400, 1530]
pollFirstEntry: 900=market-open
pollLastEntry : 1530=market-close
Remaining : [1030, 1200, 1400]
=== subMap with inclusive/exclusive bounds ===
All keys: [100, 200, 300, 400, 500, 600, 700, 800, 900, 1000]
subMap[300, 600]: [300, 400, 500, 600]
headMap(400, inclusive): [100, 200, 300, 400]
tailMap(700, exclusive): [800, 900, 1000]
=== Live subMap — mutations write through ===
SubView before: [Banana, Cherry]
SubView after put(Coconut): [Banana, Cherry, Coconut]
Inventory now: [Apple, Banana, Cherry, Coconut, Date, Elderberry]
put(Elderberry) outside range → IllegalArgumentException
Real-World Example — Zepto Inventory Slot Scheduler
Zepto's 10-minute delivery service assigns incoming orders to delivery slot windows based on estimated preparation time. A TreeMap<LocalTime, DeliverySlot> keeps slots sorted by time automatically. The scheduler queries ceilingKey(requestedTime) to find the next available slot in O(log n), uses subMap() to fetch all slots in a delivery window, and uses pollFirstEntry() to dispatch the earliest pending slot.
1// File: DeliverySlot.java
2
3import java.time.LocalTime;
4import java.util.ArrayList;
5import java.util.List;
6
7public class DeliverySlot {
8
9 private final LocalTime startTime;
10 private final int capacityLimit;
11 private final List<String> assignedOrders = new ArrayList<>();
12
13 public DeliverySlot(LocalTime startTime, int capacityLimit) {
14 this.startTime = startTime;
15 this.capacityLimit = capacityLimit;
16 }
17
18 public boolean hasCapacity() {
19 return assignedOrders.size() < capacityLimit;
20 }
21
22 public boolean assign(String orderId) {
23 if (!hasCapacity()) return false;
24 assignedOrders.add(orderId);
25 return true;
26 }
27
28 public LocalTime getStartTime() { return startTime; }
29 public int getCapacityLimit() { return capacityLimit;}
30 public int getAssignedCount() { return assignedOrders.size(); }
31 public List<String> getAssignedOrders(){ return List.copyOf(assignedOrders); }
32
33 @Override
34 public String toString() {
35 return String.format("%s [%d/%d orders]",
36 startTime, assignedOrders.size(), capacityLimit);
37 }
38}1// File: SlotScheduler.java
2
3import java.time.LocalTime;
4import java.util.Map;
5import java.util.TreeMap;
6
7public class SlotScheduler {
8
9 // TreeMap<LocalTime, DeliverySlot>:
10 // LocalTime implements Comparable — natural order = chronological
11 // All slot lookups are O(log n) tree traversals
12 private final TreeMap<LocalTime, DeliverySlot> slots = new TreeMap<>();
13
14 public void registerSlot(LocalTime startTime, int capacity) {
15 slots.put(startTime, new DeliverySlot(startTime, capacity));
16 }
17
18 // O(log n): find nearest available slot at or after requested time
19 public DeliverySlot findNextAvailable(LocalTime requestedTime) {
20 Map.Entry<LocalTime, DeliverySlot> entry =
21 slots.ceilingEntry(requestedTime); // smallest key >= requestedTime
22
23 while (entry != null) {
24 if (entry.getValue().hasCapacity()) {
25 return entry.getValue();
26 }
27 // This slot is full — look for the next one
28 entry = slots.higherEntry(entry.getKey()); // strictly > current
29 }
30 return null; // no available slot found
31 }
32
33 // Assign order to next available slot at or after requestedTime
34 public boolean assignOrder(String orderId, LocalTime requestedTime) {
35 DeliverySlot slot = findNextAvailable(requestedTime);
36 if (slot == null) {
37 System.out.printf(" [REJECTED] %s — no slot available from %s%n",
38 orderId, requestedTime);
39 return false;
40 }
41 slot.assign(orderId);
42 System.out.printf(" [ASSIGNED] %s → slot %s%n", orderId, slot.getStartTime());
43 return true;
44 }
45
46 // O(log n) per boundary + O(k) for k entries: return all slots in a window
47 public Map<LocalTime, DeliverySlot> getSlotsInWindow(
48 LocalTime windowStart, LocalTime windowEnd) {
49 return slots.subMap(windowStart, true, windowEnd, true); // live view
50 }
51
52 // Dispatch earliest pending slot — O(log n) pollFirstEntry
53 public Map.Entry<LocalTime, DeliverySlot> dispatchNext() {
54 return slots.pollFirstEntry(); // removes and returns the earliest slot
55 }
56
57 public void printAllSlots() {
58 System.out.println("=".repeat(52));
59 System.out.println(" All registered slots (TreeMap — sorted by time):");
60 System.out.println("=".repeat(52));
61 slots.forEach((time, slot) ->
62 System.out.printf(" %s → %s%n", time, slot));
63 System.out.println("=".repeat(52));
64 }
65
66 public static void main(String[] args) {
67
68 SlotScheduler scheduler = new SlotScheduler();
69
70 System.out.println("--- Registering delivery slots ---");
71 scheduler.registerSlot(LocalTime.of(9, 0), 3);
72 scheduler.registerSlot(LocalTime.of(9, 10), 3);
73 scheduler.registerSlot(LocalTime.of(9, 20), 2);
74 scheduler.registerSlot(LocalTime.of(9, 30), 3);
75 scheduler.registerSlot(LocalTime.of(9, 40), 2);
76 scheduler.registerSlot(LocalTime.of(9, 50), 3);
77 scheduler.registerSlot(LocalTime.of(10, 0), 4);
78 scheduler.printAllSlots();
79
80 System.out.println("\n--- Assigning orders ---");
81 scheduler.assignOrder("ORD-001", LocalTime.of(9, 0));
82 scheduler.assignOrder("ORD-002", LocalTime.of(9, 0));
83 scheduler.assignOrder("ORD-003", LocalTime.of(9, 5)); // no exact slot → ceiling 09:10
84 scheduler.assignOrder("ORD-004", LocalTime.of(9, 10));
85 scheduler.assignOrder("ORD-005", LocalTime.of(9, 15)); // ceiling → 09:20
86 scheduler.assignOrder("ORD-006", LocalTime.of(9, 15)); // ceiling → 09:20
87 scheduler.assignOrder("ORD-007", LocalTime.of(9, 15)); // 09:20 full → 09:30
88 scheduler.assignOrder("ORD-008", LocalTime.of(9, 0));
89 scheduler.assignOrder("ORD-009", LocalTime.of(9, 0)); // 09:00 full → 09:10
90 scheduler.assignOrder("ORD-010", LocalTime.of(9, 0)); // 09:10 full → 09:30
91
92 System.out.println();
93 scheduler.printAllSlots();
94
95 System.out.println("\n--- Slots in 09:20 to 09:40 window (subMap) ---");
96 LocalTime windowStart = LocalTime.of(9, 20);
97 LocalTime windowEnd = LocalTime.of(9, 40);
98 scheduler.getSlotsInWindow(windowStart, windowEnd)
99 .forEach((time, slot) ->
100 System.out.printf(" %s → %s%n", time, slot));
101
102 System.out.println("\n--- Dispatching earliest slot (pollFirstEntry) ---");
103 Map.Entry<LocalTime, DeliverySlot> dispatched = scheduler.dispatchNext();
104 System.out.println("Dispatched: " + dispatched.getKey()
105 + " — orders: " + dispatched.getValue().getAssignedOrders());
106 System.out.println("Remaining slots: " + scheduler.slots.keySet());
107 }
108}Output:
--- Registering delivery slots ---
====================================================
All registered slots (TreeMap — sorted by time):
====================================================
09:00 → 09:00 [0/3 orders]
09:10 → 09:10 [0/3 orders]
09:20 → 09:20 [0/2 orders]
09:30 → 09:30 [0/3 orders]
09:40 → 09:40 [0/2 orders]
09:50 → 09:50 [0/3 orders]
10:00 → 10:00 [0/4 orders]
====================================================
--- Assigning orders ---
[ASSIGNED] ORD-001 → slot 09:00
[ASSIGNED] ORD-002 → slot 09:00
[ASSIGNED] ORD-003 → slot 09:10
[ASSIGNED] ORD-004 → slot 09:10
[ASSIGNED] ORD-005 → slot 09:20
[ASSIGNED] ORD-006 → slot 09:20
[ASSIGNED] ORD-007 → slot 09:30
[ASSIGNED] ORD-008 → slot 09:00
[ASSIGNED] ORD-009 → slot 09:10
[ASSIGNED] ORD-010 → slot 09:30
====================================================
All registered slots (TreeMap — sorted by time):
====================================================
09:00 → 09:00 [3/3 orders]
09:10 → 09:10 [3/3 orders]
09:20 → 09:20 [2/2 orders]
09:30 → 09:30 [2/3 orders]
09:40 → 09:40 [0/2 orders]
09:50 → 09:50 [0/3 orders]
10:00 → 10:00 [0/4 orders]
====================================================
--- Slots in 09:20 to 09:40 window (subMap) ---
09:20 → 09:20 [2/2 orders]
09:30 → 09:30 [2/3 orders]
09:40 → 09:40 [0/2 orders]
--- Dispatching earliest slot (pollFirstEntry) ---
Dispatched: 09:00 — orders: [ORD-001, ORD-002, ORD-008]
Remaining slots: [09:10, 09:20, 09:30, 09:40, 09:50, 10:00]
Performance Considerations
| Operation | TreeMap | HashMap | Notes |
|---|---|---|---|
| put(k, v) | O(log n) | O(1) avg | TreeMap: BST descent + RB fix |
| get(k) | O(log n) | O(1) avg | TreeMap: BST descent; HashMap: hash + equals |
| remove(k) | O(log n) | O(1) avg | TreeMap: BST delete + RB fix |
| containsKey(k) | O(log n) | O(1) avg | Same as get |
| firstKey() / lastKey() | O(log n) | N/A | HashMap has no ordered boundary |
| floorKey / ceilingKey | O(log n) | N/A | NavigableMap — no HashMap equivalent |
| subMap / headMap / tailMap | O(log n) setup | N/A | Live views, O(k) iteration for k entries |
| Iteration (all entries) | O(n) | O(n + capacity) | TreeMap in sorted order; HashMap visits all buckets |
| Memory per entry | ~5 refs/node | ~1 ref + Node | TreeMap: key/value/left/right/parent/color |
TreeMap iteration is more memory-predictable than HashMap iteration. HashMap's entrySet() traversal visits every bucket slot — a map with 16 entries and capacity 256 scans 256 slots. TreeMap's in-order traversal visits exactly n nodes. For sparse maps with frequent full iteration, TreeMap is more efficient.
No resize penalty. TreeMap never resizes. The Red-Black tree grows incrementally by adding one Entry node per put(). HashMap triggers O(n) resizes when size > threshold. For maps that receive entries gradually over time, TreeMap has more predictable per-operation latency with no periodic O(n) spikes.
Best Practices
Ensure key objects implement Comparable correctly, or provide a Comparator at construction. Without either, the first put() throws ClassCastException at runtime — not a compile error. The Comparator must be consistent with equals(): if comparator.compare(a, b) == 0, then a.equals(b) should also be true. Violations cause silent data loss: two keys that compare as equal are treated as the same key, so the second put() overwrites the first without warning.
Pass the Comparator to the constructor — not as a sort step after population. TreeMap uses its Comparator to maintain order during every put(). Sorting a separate list and then inserting does not give you a Comparator-aware TreeMap; insertion order is not preserved. If custom ordering is needed, always: new TreeMap<>(myComparator).
Use subMap(), headMap(), and tailMap() for range queries instead of filtering with streams. treeMap.subMap(low, high) is O(log n) to set up and O(k) to iterate k results. The equivalent stream pipeline — treeMap.entrySet().stream().filter(e -> e.getKey() >= low && e.getKey() < high) — is O(n) because it scans all entries. For large maps with range-query workloads, the TreeMap-native approach is significantly faster.
Prefer TreeMap over sorting an ArrayList when entries are inserted and queried interleaved. Building an ArrayList, sorting it O(n log n), then searching O(log n) is correct once. But if entries keep arriving and range queries happen throughout, maintaining a TreeMap — O(log n) per insert, O(log n) per range query setup — is more efficient than repeated sort cycles.
Common Mistakes
Mistake 1 — Key Class That Does Not Implement Comparable
1class Product {
2 String sku;
3 double price;
4 Product(String sku, double price) { this.sku = sku; this.price = price; }
5 // Comparable NOT implemented
6}
7
8// WRONG — ClassCastException on first put() with two entries
9TreeMap<Product, Integer> catalog = new TreeMap<>();
10catalog.put(new Product("P001", 499.0), 10);
11catalog.put(new Product("P002", 799.0), 5);
12// ClassCastException: Product cannot be cast to Comparable
13
14// CORRECT — provide a Comparator at construction
15TreeMap<Product, Integer> catalogFixed = new TreeMap<>(
16 Comparator.comparingDouble(p -> p.price)
17);
18catalogFixed.put(new Product("P001", 499.0), 10);
19catalogFixed.put(new Product("P002", 799.0), 5);
20System.out.println("Sorted by price: " + catalogFixed.size()); // 2Output:
Sorted by price: 2
Mistake 2 — Comparator Inconsistent With equals()
1// WRONG — Comparator says length("ab") == length("cd") → same key
2// Second put() silently overwrites the first
3TreeMap<String, String> map = new TreeMap<>(
4 Comparator.comparingInt(String::length)
5 // No tiebreaker! "ab" and "cd" both have length 2 → compare = 0
6);
7map.put("ab", "first");
8map.put("cd", "second"); // compare("cd","ab")=0 → TreeMap treats as same key!
9System.out.println("size: " + map.size()); // 1, not 2
10System.out.println("value: " + map.get("ab")); // second — overwritten!
11
12// CORRECT — always add a unique tiebreaker
13TreeMap<String, String> mapFixed = new TreeMap<>(
14 Comparator.comparingInt(String::length)
15 .thenComparing(Comparator.naturalOrder()) // tiebreaker preserves uniqueness
16);
17mapFixed.put("ab", "first");
18mapFixed.put("cd", "second");
19System.out.println("size: " + mapFixed.size()); // 2Mistake 3 — Using TreeMap When O(1) Lookup Is All That Is Needed
1// WRONG — TreeMap used just for key-value lookup; sorted order never used
2// O(log n) per lookup when HashMap gives O(1)
3Map<String, UserProfile> userCache = new TreeMap<>();
4for (UserProfile user : loadFromDatabase()) {
5 userCache.put(user.getId(), user); // no sorted iteration ever done
6}
7UserProfile profile = userCache.get("user-12345"); // O(log n) unnecessarily
8
9// CORRECT — use HashMap for pure lookup workloads
10Map<String, UserProfile> userCacheFixed = new HashMap<>();
11for (UserProfile user : loadFromDatabase()) {
12 userCacheFixed.put(user.getId(), user);
13}
14UserProfile profileFixed = userCacheFixed.get("user-12345"); // O(1)Mistake 4 — Modifying subMap Keys Outside the Allowed Range
1TreeMap<Integer, String> prices = new TreeMap<>();
2for (int i = 100; i <= 1000; i += 100) prices.put(i, "item-" + i);
3
4Map<Integer, String> midRange = prices.subMap(300, 700); // [300, 700)
5
6// WRONG — putting a key outside the subMap range throws immediately
7try {
8 midRange.put(800, "item-800"); // 800 >= 700 → outside range
9} catch (IllegalArgumentException e) {
10 System.out.println("put(800) → IllegalArgumentException (outside subMap range)");
11}
12
13// CORRECT — only keys within [fromKey, toKey) are valid for the subMap
14midRange.put(350, "item-350"); // valid — 300 <= 350 < 700
15System.out.println("midRange after valid put: " + midRange.keySet());Output:
put(800) → IllegalArgumentException (outside subMap range)
midRange after valid put: [300, 350, 400, 500, 600]
Interview Questions
Q1. What data structure backs TreeMap internally and what guarantees does it provide?
TreeMap is backed by a Red-Black tree — a self-balancing binary search tree. Every node is an Entry<K,V> holding the key, value, left and right child references, a parent reference, and a boolean color field (RED or BLACK). The five Red-Black properties guarantee the tree height stays at O(log n) regardless of insertion order. This means put(), get(), remove(), and all navigation operations are O(log n) guaranteed — unlike HashMap, which is O(1) average but O(n) worst case when hash collisions cluster entries in one bucket.
Q2. How does TreeMap maintain sorted key order?
TreeMap maintains the binary search tree invariant on every put() and remove(): left child keys are always less than the parent key, right child keys are always greater. This means in-order traversal of the tree — left subtree, node, right subtree — always yields keys in ascending order. entrySet(), keySet(), and values() all return sorted views by performing in-order traversal internally. No sorting happens at read time; the tree is kept in order at write time.
Q3. What is the difference between floorKey(), ceilingKey(), lowerKey(), and higherKey()?
All four are O(log n) NavigableMap operations that find the closest key to a given value. floorKey(k) returns the largest key less than or equal to k — or null if none exists. ceilingKey(k) returns the smallest key greater than or equal to k. lowerKey(k) is the strict version of floorKey — it returns the largest key strictly less than k. higherKey(k) is the strict version of ceilingKey — strictly greater than k. These are used for nearest-match queries that HashMap cannot support at all.
Q4. Why does TreeMap reject null keys while HashMap allows them?
TreeMap compares keys at every tree traversal step using either Comparable.compareTo() or the provided Comparator. Calling null.compareTo(x) throws NullPointerException. Even with a Comparator, the standard Comparator implementations throw NullPointerException for null inputs. The prohibition is structural — the binary search descent requires ordering every key at every node. HashMap handles null specially by routing it to bucket 0 with an explicit key == null check, which TreeMap cannot do.
Q5. What is the difference between subMap(), headMap(), and tailMap() in TreeMap?
All three return live views backed by the same TreeMap. subMap(fromKey, toKey) returns entries with keys in the range [fromKey, toKey) — inclusive from, exclusive to. The inclusive/exclusive bounds can be flipped using the four-argument form subMap(from, fromInclusive, to, toInclusive). headMap(toKey) returns all entries with keys strictly less than toKey. tailMap(fromKey) returns all entries with keys greater than or equal to fromKey. Live view means reads and writes through the submap operate on the original TreeMap. Writes with keys outside the defined range throw IllegalArgumentException.
Q6. When would you choose TreeMap over HashMap?
TreeMap is the right choice when sorted key order or range queries are needed. Specifically: when iterating entries in key order, when finding the nearest key (floor/ceiling), when querying a key range (subMap), or when dispatching the minimum or maximum key (pollFirstEntry/pollLastEntry). If none of these apply — if the only operation is get/put by exact key — HashMap's O(1) average is faster than TreeMap's O(log n). For 1 million entries, O(log n) is about 20 comparisons per operation compared to O(1) for HashMap. The sorted ordering is the premium paid for the O(log n) cost.
FAQs
What is the time complexity of all TreeMap operations?
Every single-key operation — put(), get(), remove(), containsKey(), firstKey(), lastKey(), floorKey(), ceilingKey(), lowerKey(), higherKey() — is O(log n). subMap(), headMap(), and tailMap() are O(log n) to create the view and O(k) to iterate k entries. Full iteration via entrySet() or keySet() is O(n). size() and isEmpty() are O(1).
Does TreeMap allow duplicate keys?
No. Like all Map implementations, TreeMap stores at most one value per key. A second put() with an existing key replaces the old value and returns it. What makes TreeMap uniquely dangerous for duplicates is the Comparator path: if comparator.compare(k1, k2) == 0, TreeMap treats k1 and k2 as the same key regardless of what equals() says. A Comparator without a tiebreaker that sorts distinct-but-equal-length strings will silently overwrite entries.
Is TreeMap thread-safe?
No. TreeMap is not synchronised. Concurrent reads and writes without external locking corrupt the tree structure — the Red-Black rebalancing modifies multiple node references atomically in a single put(), and concurrent threads can see partial states. For thread-safe sorted map access, use Collections.synchronizedSortedMap(new TreeMap<>()) for a global-lock wrapper, or ConcurrentSkipListMap for a lock-free concurrent sorted map with the same NavigableMap operations.
Can TreeMap store a custom class as a key without implementing Comparable?
Yes, by providing a Comparator at construction: new TreeMap<>(comparator). The Comparator is then used for all key comparisons instead of compareTo(). The class does not need to implement Comparable. The Comparator must be consistent with equals() to avoid silent data loss — comparator.compare(a, b) == 0 implies a.equals(b) should also be true.
What is ConcurrentSkipListMap and how does it relate to TreeMap?
ConcurrentSkipListMap<K, V> in java.util.concurrent is a thread-safe sorted map that implements the same NavigableMap interface as TreeMap. It uses a probabilistic skip list structure instead of a Red-Black tree, allowing lock-free concurrent reads and lock-free concurrent writes at different key ranges. For single-threaded sorted map use, TreeMap is faster. For concurrent workloads where sorted order matters, ConcurrentSkipListMap is the correct replacement — not Collections.synchronizedSortedMap(new TreeMap<>()), which serialises all operations through a single lock.
Does TreeMap preserve insertion order?
No. TreeMap always maintains key order based on the Comparator or natural ordering — not insertion order. Inserting "banana", "apple", "cherry" produces iteration order "apple", "banana", "cherry" regardless of insertion sequence. For insertion-order preservation with O(1) average get/put, use LinkedHashMap.
Summary
TreeMap<K, V> is a Red-Black tree where every node is an Entry holding key, value, left, right, parent, and color. The five Red-Black properties bound tree height to O(log n), making put(), get(), remove(), and all navigation operations O(log n) guaranteed. In-order traversal of the tree — what entrySet(), keySet(), and values() perform — always yields keys in ascending order.
TreeMap earns its O(log n) cost when sorted order or range queries matter: floorKey(), ceilingKey() for closest-match lookups, subMap() / headMap() / tailMap() for range views, firstKey() / lastKey() for boundary access, and pollFirstEntry() / pollLastEntry() for priority-queue-like dispatch. For pure key-value lookup with no ordering requirement, HashMap is the right choice.
Two implementation details answer most TreeMap interview questions: the Comparator consistency rule (inconsistency causes silent key overwrites) and null key rejection (BST descent requires comparing every key — null cannot be compared).
What to Read Next
| Topic | Link |
|---|---|
| How HashMap's hash table offers O(1) average get/put as an alternative to TreeMap | Java HashMap → |
| How TreeSet uses a TreeMap internally to maintain a sorted set of unique elements | Java HashSet → |
| How LinkedHashMap preserves insertion order as a middle ground between HashMap and TreeMap | Java LinkedList → |
| How the Collections Framework organises the SortedMap and NavigableMap hierarchy | Java Collections Framework → |
| How Generics make TreeMap type-safe with bounded wildcards and Comparator type parameters | Java Generics → |