Java TreeMap
Java TreeMap
java.util.TreeMap<K, V> is the sorted map — every key is automatically placed in its correct sorted position, and firstKey(), lastKey(), floorKey(), ceilingKey(), headMap(), tailMap(), and subMap() let you query ranges without any manual scanning. The price over HashMap is O(log n) per operation — always, not just worst case — because every operation traverses a Red-Black tree. When your data needs to be both keyed and sorted, TreeMap handles both requirements in one structure.
What Is Java TreeMap?
TreeMap<K, V> is a concrete class in java.util that implements NavigableMap<K, V>. It stores entries in a self-balancing Red-Black tree keyed on the natural ordering of keys (via Comparable) or a supplied Comparator. Every traversal visits nodes in ascending key order.
The diagram below shows where TreeMap fits in the Map hierarchy.
java.util.Map<K, V> ← key-value contract
├── HashMap<K, V> ← O(1) avg, no order
│ └── LinkedHashMap<K, V> ← insertion/access order
└── java.util.SortedMap<K, V> ← adds sorted key view
└── java.util.NavigableMap<K, V> ← adds floor, ceiling, lower, higher
└── java.util.TreeMap<K,V> ← THIS CLASS: Red-Black tree
KEY FACTS:
Package : java.util
Since : Java 1.2
Implements : NavigableMap<K,V>, Cloneable, Serializable
Backed by : Red-Black tree (self-balancing BST)
Key order : natural (Comparable) or supplied Comparator — always ascending
Null keys : NOT allowed — compareTo(null) throws NullPointerException
Null values: allowed (unlimited)
Thread-safe: NO — use Collections.synchronizedSortedMap() or ConcurrentSkipListMap
Operations : O(log n) guaranteed — tree height is bounded by 2 × log₂(n+1)
Basic Overview — TreeMap Method Groups
TreeMap inherits put(), get(), remove() from Map, then adds two layers of navigation through SortedMap and NavigableMap.
TREEMAP COMPLETE METHOD REFERENCE:
MAP OPERATIONS (O(log n)):
put(key, value) ← insert or update
get(key) ← lookup
remove(key) ← delete
containsKey(key) ← O(log n) — traverses tree
containsValue(value) ← O(n) — full scan (no tree ordering on values)
SORTED MAP ADDITIONS — SortedMap<K,V>:
firstKey() ← smallest key (leftmost node)
lastKey() ← largest key (rightmost node)
headMap(toKey) ← live view of keys strictly less than toKey
tailMap(fromKey) ← live view of keys >= fromKey
subMap(from, to) ← live view of keys in [from, to)
comparator() ← returns Comparator or null for natural ordering
NAVIGABLE MAP ADDITIONS — NavigableMap<K,V>:
floorKey(key) ← largest key <= key (null if none)
ceilingKey(key) ← smallest key >= key (null if none)
lowerKey(key) ← largest key strictly < key (null if none)
higherKey(key) ← smallest key strictly > key (null if none)
floorEntry(key) ← Entry version of floorKey
ceilingEntry(key) ← Entry version of ceilingKey
firstEntry() ← Entry with smallest key
lastEntry() ← Entry with largest key
pollFirstEntry() ← remove and return smallest entry
pollLastEntry() ← remove and return largest entry
descendingMap() ← reverse-order view of entire map
descendingKeySet() ← reverse-order NavigableSet of keys
headMap(to, inclusive)← overloaded with inclusive boundary
tailMap(from, inclusive)← overloaded with inclusive boundary
subMap(from,fi,to,ti) ← overloaded with inclusive boundaries
ALL range views are LIVE — modifications propagate back to the backing TreeMap.
When to Use TreeMap
USE TreeMap WHEN:
1. Keys must be iterated in sorted order
— leaderboards, alphabetical catalogues, sorted event logs
2. Range queries are needed
— all keys between 100 and 500: subMap(100, true, 500, true)
— all keys alphabetically before "P": headMap("P")
— all keys from a given timestamp: tailMap(timestamp)
3. Nearest-key navigation is needed
— "what is the largest configured timeout <= my request timeout?"
→ floorKey(requestTimeout)
— "what is the next scheduled event >= now?"
→ ceilingKey(System.currentTimeMillis())
4. A sorted unique-key view of a dataset must be maintained live
— price levels, time slots, route segments
CHOOSE HashMap INSTEAD WHEN:
- Key order is irrelevant
- O(1) is required: HashMap put/get/remove are faster than TreeMap
- The map holds millions of entries and the O(log n) cost accumulates
CHOOSE LinkedHashMap INSTEAD WHEN:
- Insertion order (not key-sorted order) matters
- O(1) operations are needed alongside ordered iteration
CHOOSE ConcurrentSkipListMap INSTEAD WHEN:
- Sorted map with concurrent multi-thread access is needed
- ConcurrentSkipListMap provides O(log n) operations with full thread safety
NEVER USE TreeMap WHEN:
- Keys do not implement Comparable AND no Comparator is provided
→ ClassCastException on the second put() call
- Null keys are needed → NullPointerException on any put(null, value)
How TreeMap Works Internally
Red-Black Tree Structure
Every entry in TreeMap is stored as an Entry<K, V> node in a Red-Black tree — a self-balancing binary search tree with five invariants that bound the tree height at 2 × log₂(n+1).
TREEMAP ENTRY NODE:
K key
V value
Entry left, right ← BST children
Entry parent ← parent reference (for upward traversal)
boolean color ← RED or BLACK (Red-Black invariant)
RED-BLACK TREE INVARIANTS:
1. Every node is RED or BLACK
2. The root is always BLACK
3. No two consecutive RED nodes (RED node's parent must be BLACK)
4. Every path from a node to a null leaf has the same number of BLACK nodes
5. Null leaves are considered BLACK
THESE INVARIANTS GUARANTEE:
Height <= 2 × log₂(n + 1)
For n = 1,000,000 entries: height <= ~40 comparisons per operation
TREEMAP AFTER: put("D",4), put("B",2), put("F",6), put("A",1), put("C",3):
In-order traversal always produces keys in ascending order.
Tree structure (simplified, colours omitted):
D
/ \
B F
/ \
A C
put() steps for "E":
1. Compare "E" with root "D" → "E" > "D" → go right
2. Compare "E" with "F" → "E" < "F" → go left
3. Null slot found → insert new node here
4. Rebalance: check Red-Black invariants, rotate if needed
O(log n) comparisons + O(log n) rotations (bounded)
get("C"):
1. Compare "C" with "D" → "C" < "D" → go left
2. Compare "C" with "B" → "C" > "B" → go right
3. Compare "C" with "C" → match → return value
O(log n) comparisons — no collision chains, no hash computation
compareTo() Drives Everything
TreeMap uses compareTo() (natural ordering) or the provided Comparator for all operations. It never calls equals() or hashCode(). Two keys where compare(a, b) == 0 are treated as identical — the second put() replaces the value rather than adding a new entry.
DUPLICATE DETECTION in TreeMap:
compare(key1, key2) == 0 → same key — value replaced, no new node
compare(key1, key2) != 0 → different keys — separate nodes
This is independent of equals(). If compare() returns 0 but equals()
returns false, TreeMap treats them as the same key (value replaced).
CONSISTENCY-WITH-EQUALS CONTRACT (recommended, not enforced):
(a.compareTo(b) == 0) should equal (a.equals(b))
BigDecimal violates this: 2.0.compareTo(2.00) == 0 but equals is false.
In a TreeMap<BigDecimal, ...>, put(new BigDecimal("2.0"), X) followed by
put(new BigDecimal("2.00"), Y) results in one entry with value Y.
Core Operations with Examples
put(), get(), Basic SortedMap Methods
1// File: TreeMapBasicsDemo.java
2
3import java.util.TreeMap;
4
5public class TreeMapBasicsDemo {
6
7 public static void main(String[] args) {
8
9 TreeMap<String, Integer> cityRank = new TreeMap<>();
10
11 // put() — always maintains sorted key order
12 cityRank.put("Mumbai", 1);
13 cityRank.put("Delhi", 2);
14 cityRank.put("Bengaluru", 3);
15 cityRank.put("Hyderabad", 4);
16 cityRank.put("Chennai", 5);
17 cityRank.put("Ahmedabad", 6);
18
19 System.out.println("=== TreeMap iterates in sorted key order ===");
20 cityRank.forEach((city, rank) ->
21 System.out.printf(" %-12s rank %d%n", city, rank));
22
23 System.out.println("\n=== SortedMap boundary access ===");
24 System.out.println("firstKey() : " + cityRank.firstKey()); // Ahmedabad (alphabetically first)
25 System.out.println("lastKey() : " + cityRank.lastKey()); // Mumbai (alphabetically last)
26 System.out.println("firstEntry(): " + cityRank.firstEntry()); // Ahmedabad=6
27 System.out.println("lastEntry() : " + cityRank.lastEntry()); // Mumbai=1
28
29 System.out.println("\n=== pollFirstEntry / pollLastEntry ===");
30 System.out.println("pollFirstEntry(): " + cityRank.pollFirstEntry()); // removes Ahmedabad
31 System.out.println("pollLastEntry() : " + cityRank.pollLastEntry()); // removes Mumbai
32 System.out.println("After polls : " + cityRank.keySet());
33 }
34}Output:
=== TreeMap iterates in sorted key order ===
Ahmedabad rank 6
Bengaluru rank 3
Chennai rank 5
Delhi rank 2
Hyderabad rank 4
Mumbai rank 1
=== SortedMap boundary access ===
firstKey() : Ahmedabad
lastKey() : Mumbai
firstEntry(): Ahmedabad=6
lastEntry() : Mumbai=1
=== pollFirstEntry / pollLastEntry ===
pollFirstEntry(): Ahmedabad=6
pollLastEntry() : Mumbai=1
After polls : [Bengaluru, Chennai, Delhi, Hyderabad]
NavigableMap — Floor, Ceiling, Lower, Higher
1// File: TreeMapNavigationDemo.java
2
3import java.util.TreeMap;
4
5public class TreeMapNavigationDemo {
6
7 public static void main(String[] args) {
8
9 // Price tier map — nearest-price queries
10 TreeMap<Integer, String> priceTiers = new TreeMap<>();
11 priceTiers.put(199, "Basic");
12 priceTiers.put(499, "Standard");
13 priceTiers.put(999, "Premium");
14 priceTiers.put(1999, "Pro");
15 priceTiers.put(4999, "Enterprise");
16
17 System.out.println("Price tiers: " + priceTiers);
18 System.out.println();
19
20 int userBudget = 750;
21
22 // floorKey — largest key <= budget (best affordable tier)
23 System.out.printf("Budget: Rs.%d%n", userBudget);
24 System.out.printf("floorKey(%d) : %s (best tier within budget)%n",
25 userBudget, priceTiers.floorKey(userBudget));
26
27 // ceilingKey — smallest key >= budget (cheapest tier at or above budget)
28 System.out.printf("ceilingKey(%d) : %s (cheapest tier at or above budget)%n",
29 userBudget, priceTiers.ceilingKey(userBudget));
30
31 // lowerKey — largest key strictly < budget
32 System.out.printf("lowerKey(%d) : %s (strictly below budget)%n",
33 userBudget, priceTiers.lowerKey(userBudget));
34
35 // higherKey — smallest key strictly > budget
36 System.out.printf("higherKey(%d) : %s (just above budget)%n",
37 userBudget, priceTiers.higherKey(userBudget));
38
39 System.out.println();
40
41 // Edge cases — returns null when no key satisfies the condition
42 System.out.println("=== Edge cases ===");
43 System.out.println("floorKey(100) : " + priceTiers.floorKey(100)); // null — below all tiers
44 System.out.println("ceilingKey(6000) : " + priceTiers.ceilingKey(6000)); // null — above all tiers
45 System.out.println("floorKey(999) : " + priceTiers.floorKey(999)); // 999 — exact match
46 System.out.println("lowerKey(199) : " + priceTiers.lowerKey(199)); // null — nothing below minimum
47 System.out.println("floorEntry(750) : " + priceTiers.floorEntry(750)); // full entry
48 }
49}Output:
Price tiers: {199=Basic, 499=Standard, 999=Premium, 1999=Pro, 4999=Enterprise}
Budget: Rs.750
floorKey(750) : 499 (best tier within budget)
ceilingKey(750) : 999 (cheapest tier at or above budget)
lowerKey(750) : 499 (strictly below budget)
higherKey(750) : 999 (just above budget)
=== Edge cases ===
floorKey(100) : null
ceilingKey(6000) : null
floorKey(999) : 999
lowerKey(199) : null
floorEntry(750) : 499=Standard
Range Views — headMap, tailMap, subMap
1// File: TreeMapRangeDemo.java
2
3import java.util.Map;
4import java.util.NavigableMap;
5import java.util.TreeMap;
6
7public class TreeMapRangeDemo {
8
9 public static void main(String[] args) {
10
11 TreeMap<Long, String> eventLog = new TreeMap<>();
12 eventLog.put(1_000L, "UserLogin");
13 eventLog.put(2_000L, "CartUpdated");
14 eventLog.put(3_000L, "PaymentInitiated");
15 eventLog.put(4_000L, "PaymentConfirmed");
16 eventLog.put(5_000L, "OrderPlaced");
17 eventLog.put(6_000L, "OrderDispatched");
18 eventLog.put(7_000L, "UserLogout");
19
20 System.out.println("All events: " + eventLog.values());
21 System.out.println();
22
23 // headMap — strictly before timestamp 4000
24 System.out.println("=== headMap(4000) — before payment confirmation ===");
25 Map<Long, String> before = eventLog.headMap(4_000L);
26 before.forEach((ts, event) -> System.out.println(" " + ts + ": " + event));
27
28 // headMap with inclusive boundary
29 System.out.println("\n=== headMap(4000, inclusive=true) ===");
30 eventLog.headMap(4_000L, true)
31 .forEach((ts, event) -> System.out.println(" " + ts + ": " + event));
32
33 // tailMap — from timestamp 5000 (inclusive by default)
34 System.out.println("\n=== tailMap(5000) — from order placed onwards ===");
35 eventLog.tailMap(5_000L)
36 .forEach((ts, event) -> System.out.println(" " + ts + ": " + event));
37
38 // subMap — range [3000, 6000) exclusive
39 System.out.println("\n=== subMap(3000, 6000) — payment + order events ===");
40 eventLog.subMap(3_000L, 6_000L)
41 .forEach((ts, event) -> System.out.println(" " + ts + ": " + event));
42
43 // subMap with inclusive both ends
44 System.out.println("\n=== subMap(3000, true, 6000, true) — inclusive both ===");
45 eventLog.subMap(3_000L, true, 6_000L, true)
46 .forEach((ts, event) -> System.out.println(" " + ts + ": " + event));
47
48 // Range views are LIVE — modifications propagate to backing TreeMap
49 System.out.println("\n=== Live view — remove through headMap ===");
50 NavigableMap<Long, String> loginPhase = eventLog.headMap(3_000L, true);
51 loginPhase.clear(); // removes events 1000, 2000, 3000 from backing TreeMap
52 System.out.println("Backing TreeMap after clear: " + eventLog.values());
53
54 // descendingMap — reverse order view
55 System.out.println("\n=== descendingMap() — most recent first ===");
56 eventLog.descendingMap()
57 .forEach((ts, event) -> System.out.println(" " + ts + ": " + event));
58 }
59}Output:
All events: [UserLogin, CartUpdated, PaymentInitiated, PaymentConfirmed, OrderPlaced, OrderDispatched, UserLogout]
=== headMap(4000) — before payment confirmation ===
1000: UserLogin
2000: CartUpdated
3000: PaymentInitiated
=== headMap(4000, inclusive=true) ===
1000: UserLogin
2000: CartUpdated
3000: PaymentInitiated
4000: PaymentConfirmed
=== tailMap(5000) — from order placed onwards ===
5000: OrderPlaced
6000: OrderDispatched
7000: UserLogout
=== subMap(3000, 6000) — payment + order events ===
3000: PaymentInitiated
4000: PaymentConfirmed
5000: OrderPlaced
=== subMap(3000, true, 6000, true) — inclusive both ===
3000: PaymentInitiated
4000: PaymentConfirmed
5000: OrderPlaced
6000: OrderDispatched
=== Live view — remove through headMap ===
Backing TreeMap after clear: [PaymentConfirmed, OrderPlaced, OrderDispatched, UserLogout]
=== descendingMap() — most recent first ===
7000: UserLogout
6000: OrderDispatched
5000: OrderPlaced
4000: PaymentConfirmed
Custom Ordering with Comparator
1// File: TreeMapComparatorDemo.java
2
3import java.util.Comparator;
4import java.util.TreeMap;
5
6public class TreeMapComparatorDemo {
7
8 record Product(String id, String name, double price, int stock) {}
9
10 public static void main(String[] args) {
11
12 // Sort by price ascending, then by id as tiebreaker
13 // Tiebreaker is CRITICAL — without it, equal-price products overwrite each other
14 Comparator<Product> byPriceThenId =
15 Comparator.comparingDouble(Product::price)
16 .thenComparing(Product::id);
17
18 TreeMap<Product, Integer> catalogue = new TreeMap<>(byPriceThenId);
19 catalogue.put(new Product("P003", "USB Hub", 799.0, 42), 42);
20 catalogue.put(new Product("P001", "Mouse Pad", 199.0, 100), 100);
21 catalogue.put(new Product("P005", "HDMI Cable", 399.0, 65), 65);
22 catalogue.put(new Product("P002", "Keyboard Cover", 299.0, 80), 80);
23 catalogue.put(new Product("P004", "Laptop Stand", 799.0, 30), 30); // same price as P003
24
25 System.out.println("=== Sorted by price ascending (tiebreak: id) ===");
26 catalogue.forEach((product, qty) ->
27 System.out.printf(" Rs.%5.2f %-18s (stock: %d)%n",
28 product.price(), product.name(), qty));
29
30 // floorKey, ceilingKey work with the Comparator
31 Product probe = new Product("", "", 400.0, 0);
32 System.out.println("\nfloor (<=Rs.400) : "
33 + (catalogue.floorKey(probe) != null
34 ? catalogue.floorKey(probe).name() : "none"));
35 System.out.println("ceiling (>=Rs.400) : "
36 + (catalogue.ceilingKey(probe) != null
37 ? catalogue.ceilingKey(probe).name() : "none"));
38
39 // descendingMap — most expensive first
40 System.out.println("\n=== Descending (most expensive first) ===");
41 catalogue.descendingMap().forEach((product, qty) ->
42 System.out.printf(" Rs.%5.2f %s%n", product.price(), product.name()));
43 }
44}Output:
=== Sorted by price ascending (tiebreak: id) ===
Rs.199.00 Mouse Pad (stock: 100)
Rs.299.00 Keyboard Cover (stock: 80)
Rs.399.00 HDMI Cable (stock: 65)
Rs.799.00 USB Hub (stock: 42)
Rs.799.00 Laptop Stand (stock: 30)
floor (<=Rs.400) : HDMI Cable
ceiling (>=Rs.400) : USB Hub
=== Descending (most expensive first) ===
Rs.799.00 Laptop Stand
Rs.799.00 USB Hub
Rs.399.00 HDMI Cable
Rs.299.00 Keyboard Cover
Rs.199.00 Mouse Pad
Real-World Example — Razorpay Transaction Range Analytics
A payments analytics service at Razorpay needs to answer range-based queries on transaction amounts: total revenue in a price band, nearest transaction amount to a given threshold, transactions above a cutoff, and the highest and lowest recorded amounts. TreeMap handles all of these in one structure without any sorting step or external index.
1// File: TransactionBucket.java
2
3public record TransactionBucket(
4 double minAmount,
5 double maxAmount,
6 long count,
7 double totalRevenue) {
8
9 @Override
10 public String toString() {
11 return String.format("Rs.%7.2f - Rs.%7.2f | %3d txns | total Rs.%10.2f",
12 minAmount, maxAmount, count, totalRevenue);
13 }
14}1// File: TransactionAnalytics.java
2
3import java.util.Map;
4import java.util.NavigableMap;
5import java.util.TreeMap;
6
7public class TransactionAnalytics {
8
9 // Key = bucket lower bound, Value = bucket stats
10 private final TreeMap<Double, TransactionBucket> buckets = new TreeMap<>();
11
12 public void addBucket(TransactionBucket bucket) {
13 buckets.put(bucket.minAmount(), bucket);
14 }
15
16 // Find the bucket that contains a given amount
17 public TransactionBucket bucketFor(double amount) {
18 Map.Entry<Double, TransactionBucket> entry = buckets.floorEntry(amount);
19 if (entry == null) return null;
20 TransactionBucket bucket = entry.getValue();
21 return amount <= bucket.maxAmount() ? bucket : null;
22 }
23
24 // Revenue in a price range [from, to] (inclusive)
25 public double revenueInRange(double from, double to) {
26 NavigableMap<Double, TransactionBucket> range =
27 buckets.subMap(from, true, to, true);
28 return range.values().stream()
29 .mapToDouble(TransactionBucket::totalRevenue)
30 .sum();
31 }
32
33 // Buckets from a threshold amount upward
34 public void printHighValueBuckets(double threshold) {
35 System.out.println(" Buckets from Rs." + threshold + " upward:");
36 buckets.tailMap(threshold).values()
37 .forEach(b -> System.out.println(" " + b));
38 }
39
40 public void printReport() {
41 System.out.println("=".repeat(72));
42 System.out.println(" TRANSACTION BUCKET ANALYSIS");
43 System.out.println("=".repeat(72));
44 buckets.values().forEach(b -> System.out.println(" " + b));
45 System.out.println("-".repeat(72));
46 double grandTotal = buckets.values().stream()
47 .mapToDouble(TransactionBucket::totalRevenue).sum();
48 long totalTxns = buckets.values().stream()
49 .mapToLong(TransactionBucket::count).sum();
50 System.out.printf(" TOTAL: %d transactions | Rs.%.2f%n", totalTxns, grandTotal);
51 System.out.println("=".repeat(72));
52 }
53
54 public static void main(String[] args) {
55
56 TransactionAnalytics analytics = new TransactionAnalytics();
57
58 analytics.addBucket(new TransactionBucket( 0, 500, 2841, 712_650.0));
59 analytics.addBucket(new TransactionBucket( 501, 2000, 1520, 1_824_000.0));
60 analytics.addBucket(new TransactionBucket( 2001, 5000, 892, 3_123_400.0));
61 analytics.addBucket(new TransactionBucket( 5001, 15000, 437, 4_371_000.0));
62 analytics.addBucket(new TransactionBucket(15001, 50000, 128, 3_584_000.0));
63 analytics.addBucket(new TransactionBucket(50001,100000, 29, 1_885_000.0));
64
65 analytics.printReport();
66
67 System.out.println("\n--- Range queries ---");
68
69 // Revenue between Rs.500 and Rs.15,000
70 double rangeRevenue = analytics.revenueInRange(501, 15000);
71 System.out.printf("Revenue Rs.500-15000 : Rs.%.2f%n", rangeRevenue);
72
73 // Which bucket handles a Rs.3,200 transaction
74 TransactionBucket b = analytics.bucketFor(3200);
75 System.out.println("Bucket for Rs.3200 : " + (b != null ? b : "none"));
76
77 // Nearest bucket boundaries around Rs.1,800
78 Double floor = analytics.buckets.floorKey(1800.0);
79 Double ceiling = analytics.buckets.ceilingKey(1800.0);
80 System.out.println("floorKey(1800) : Rs." + floor);
81 System.out.println("ceilingKey(1800) : Rs." + ceiling);
82
83 System.out.println();
84 analytics.printHighValueBuckets(5001.0);
85
86 System.out.println("\n--- Boundary entries ---");
87 System.out.println("Lowest bucket : " + analytics.buckets.firstEntry().getValue());
88 System.out.println("Highest bucket : " + analytics.buckets.lastEntry().getValue());
89 }
90}Output:
========================================================================
TRANSACTION BUCKET ANALYSIS
========================================================================
Rs. 0.00 - Rs. 500.00 | 2841 txns | total Rs. 712650.00
Rs.501.00 - Rs. 2000.00 | 1520 txns | total Rs.1824000.00
Rs.2001.00 - Rs. 5000.00 | 892 txns | total Rs.3123400.00
Rs.5001.00 - Rs.15000.00 | 437 txns | total Rs.4371000.00
Rs.15001.00 - Rs.50000.00 | 128 txns | total Rs.3584000.00
Rs.50001.00 - Rs.100000.00 | 29 txns | total Rs.1885000.00
------------------------------------------------------------------------
TOTAL: 5847 transactions | Rs.15500050.00
========================================================================
--- Range queries ---
Revenue Rs.500-15000 : Rs.9318400.00
Bucket for Rs.3200 : Rs.2001.00 - Rs. 5000.00 | 892 txns | total Rs.3123400.00
floorKey(1800) : Rs.501.0
ceilingKey(1800) : Rs.2001.0
Buckets from Rs.5001.0 upward:
Rs.5001.00 - Rs.15000.00 | 437 txns | total Rs.4371000.00
Rs.15001.00 - Rs.50000.00 | 128 txns | total Rs.3584000.00
Rs.50001.00 - Rs.100000.00 | 29 txns | total Rs.1885000.00
--- Boundary entries ---
Lowest bucket : Rs. 0.00 - Rs. 500.00 | 2841 txns | total Rs. 712650.00
Highest bucket : Rs.50001.00 - Rs.100000.00 | 29 txns | total Rs.1885000.00
Performance Considerations
| Operation | HashMap | LinkedHashMap | TreeMap |
|---|---|---|---|
| put(k, v) | O(1) avg | O(1) avg | O(log n) |
| get(k) | O(1) avg | O(1) avg | O(log n) |
| remove(k) | O(1) avg | O(1) avg | O(log n) |
| firstKey / lastKey | N/A | N/A | O(log n) |
| floorKey / ceilingKey | N/A | N/A | O(log n) |
| headMap / tailMap view | N/A | N/A | O(1) view creation |
| Iteration | O(n + capacity) | O(n) | O(n) in-order |
| Memory per entry | ~48 bytes | ~64 bytes | ~56 bytes (tree node) |
O(log n) is worst-case bounded: Unlike HashMap where O(1) average can degrade to O(n) on hash collisions (mitigated by Java 8 treeification), TreeMap's O(log n) is an absolute guarantee. For n = 1,000,000 entries, worst-case height is ~40. For n = 1,000,000,000 entries, worst-case is ~60. The Red-Black invariants make this unconditional.
Range view cost: headMap(), tailMap(), and subMap() return SubMap view objects in O(1) — no copying. Iterating k elements within a range is O(k). This is extremely efficient for analytics queries that touch only a fraction of the map.
Thread safety: TreeMap is not thread-safe. For concurrent sorted map access, use ConcurrentSkipListMap from java.util.concurrent — it provides the same NavigableMap interface with O(log n) thread-safe operations and no global lock.
Best Practices
Declare the variable as NavigableMap<K, V> when range methods are needed. NavigableMap<String, Integer> prices = new TreeMap<>() exposes floor(), ceiling(), headMap(), tailMap(), subMap(), and descendingMap(). SortedMap<String, Integer> exposes only headMap(), tailMap(), subMap(), firstKey(), and lastKey(). Map<String, Integer> hides all navigation methods. Declare the narrowest interface that still exposes the methods callers need.
Always include a tiebreaker in the Comparator to prevent silent key collisions. Two keys where compare(a, b) == 0 are treated as the same key — the second put() replaces the first's value. For a TreeMap sorted by price where two products have the same price, only one is stored. Fix this by appending a secondary comparison field — typically a unique ID: Comparator.comparingDouble(Product::price).thenComparing(Product::id). This is the most common TreeMap correctness bug in production code.
Use range views instead of streaming the entire map. map.subMap(from, to) creates a live view in O(1) and iterates only the k matching entries. map.entrySet().stream().filter(e -> e.getKey() >= from && e.getKey() <= to) iterates all n entries. For large maps with narrow range queries, the subMap approach is O(k + log n) versus O(n).
Pre-build as HashMap then convert to TreeMap for one-time sorted output. Building a TreeMap from n individual inserts is O(n log n). Building a HashMap is O(n), then new TreeMap<>(hashMap) sorts all at once — also O(n log n) total, but with lower constant factors because bulk construction can use more efficient tree-building. If sorted output is only needed once, this pattern keeps the accumulation phase at O(1) per entry.
Common Mistakes
Mistake 1 — Missing Tiebreaker Causes Silent Value Overwrite
1// WRONG — two products at the same price: second overwrites first
2Comparator<Product> byPriceOnly = Comparator.comparingDouble(Product::getPrice);
3TreeMap<Product, String> map = new TreeMap<>(byPriceOnly);
4
5map.put(new Product("A", 599.0), "ProductA");
6map.put(new Product("B", 599.0), "ProductB"); // compare returns 0 — treated as same key!
7
8System.out.println(map.size()); // 1 — ProductA is gone!
9
10// CORRECT — always add a tiebreaker
11Comparator<Product> byPriceThenId =
12 Comparator.comparingDouble(Product::getPrice)
13 .thenComparing(Product::getId);
14TreeMap<Product, String> safeMap = new TreeMap<>(byPriceThenId);
15safeMap.put(new Product("A", 599.0), "ProductA");
16safeMap.put(new Product("B", 599.0), "ProductB");
17System.out.println(safeMap.size()); // 2 — both storedMistake 2 — Inserting null Keys
1TreeMap<String, Integer> map = new TreeMap<>();
2map.put("Alpha", 1);
3
4// WRONG — TreeMap calls compareTo() which throws on null
5map.put(null, 2); // NullPointerException
6
7// CORRECT — for null-tolerant sorted maps, use a null-accepting Comparator
8TreeMap<String, Integer> nullable = new TreeMap<>(
9 Comparator.nullsFirst(Comparator.naturalOrder())
10);
11nullable.put(null, 2); // null placed before all non-null keys
12nullable.put("Alpha", 1);
13System.out.println(nullable); // {null=2, Alpha=1}Mistake 3 — Iterating a Range View While Modifying Its Bounds
1TreeMap<Integer, String> scores = new TreeMap<>();
2for (int i = 1; i <= 10; i++) scores.put(i * 100, "Score-" + i);
3
4// Range view [300, 700]:
5java.util.SortedMap<Integer, String> view = scores.subMap(300, 700);
6
7// WRONG — inserting a key outside the view's range through the view throws
8// IllegalArgumentException. Inserting through the BACKING map is fine but
9// may or may not appear in the view depending on whether the key is in range.
10try {
11 view.put(800, "Score-Out"); // 800 is outside [300, 700) — throws
12} catch (IllegalArgumentException e) {
13 System.out.println("Exception: key outside subMap bounds");
14}
15
16// CORRECT — insert through backing map; appears in view only if in range
17scores.put(500, "Score-5-updated"); // within range — visible through view
18scores.put(800, "Score-8"); // outside range — not visible through viewOutput:
Exception: key outside subMap bounds
Mistake 4 — Using TreeMap When HashMap Performance Is Needed
1// WRONG for high-throughput lookups — O(log n) TreeMap instead of O(1) HashMap
2// Processing 10 million events with a map lookup per event:
3// HashMap: ~10ms; TreeMap: ~200ms — 20x difference at scale
4TreeMap<String, UserProfile> userCache = new TreeMap<>();
5
6// CORRECT — HashMap for pure key-value lookup at scale
7Map<String, UserProfile> userCache2 = new HashMap<>();
8
9// Use TreeMap ONLY when sorted iteration or range navigation is genuinely needed.
10// If you only call get() and put(), HashMap is always the right choice.Interview Questions
Q1. What is TreeMap in Java and how does it differ from HashMap?
TreeMap is a NavigableMap implementation backed by a Red-Black self-balancing binary search tree. It stores entries in ascending key order — natural ordering via Comparable or a supplied Comparator. HashMap uses a hash table, provides O(1) average put() and get(), but guarantees no iteration order. TreeMap provides O(log n) guaranteed for all operations and always iterates in sorted key order. TreeMap also adds the full NavigableMap API — floorKey(), ceilingKey(), headMap(), tailMap(), subMap() — which HashMap has no equivalent for.
Q2. How does TreeMap determine duplicate keys?
TreeMap uses compareTo() (natural ordering) or the Comparator for all comparisons — it never calls equals() or hashCode(). Two keys where compare(a, b) == 0 are treated as identical: the second put() replaces the value without creating a new tree node. This means a class can have equals() returning false for two objects while compareTo() returns 0 — TreeMap still treats them as the same key. This is why the Comparator must include a tiebreaker field (unique ID, timestamp) when the primary sort field is not unique.
Q3. What is the time complexity of TreeMap operations and why is it always O(log n)?
All TreeMap operations — put(), get(), remove(), firstKey(), lastKey(), floorKey(), ceilingKey() — are O(log n) in both average and worst case. This is guaranteed by the Red-Black tree's self-balancing invariants, which bound the tree height at 2 × log₂(n+1) regardless of insertion order. For 1,000,000 entries, every operation traverses at most ~40 nodes. This contrasts with HashMap's O(1) average that can degrade to O(n) on hash collisions (mitigated in Java 8 by treeification, but not eliminated).
Q4. What is the difference between headMap(), tailMap(), and subMap()?
All three return live NavigableMap view objects — no copying. headMap(toKey) returns all entries with keys strictly less than toKey (exclusive); the overloaded headMap(toKey, inclusive) controls the boundary. tailMap(fromKey) returns all entries with keys greater than or equal to fromKey (inclusive by default). subMap(from, to) returns entries with keys in [from, to) — from inclusive, to exclusive by default; the four-argument overload controls both boundaries. Modifications through any view are immediately reflected in the backing TreeMap. Adding a key outside a view's bounds throws IllegalArgumentException.
Q5. Why can't TreeMap have null keys?
TreeMap determines every key's position using compareTo() or the Comparator. Calling compareTo(null) on any object throws NullPointerException — the Java specification requires this. Since TreeMap must compare every new key with existing keys to find its tree position, null cannot be placed anywhere. HashMap can store one null key by placing it in bucket 0 and bypassing hashCode(). If null-tolerant sorted maps are needed, pass Comparator.nullsFirst(Comparator.naturalOrder()) to the TreeMap constructor.
Q6. When would you choose TreeMap over a sorted ArrayList of Map.Entry objects?
TreeMap maintains sorted order on every put() and remove() in O(log n). A sorted ArrayList requires Collections.sort() after each insertion — O(n log n) per sort — and O(n) for remove() by key (binary search finds position, shifting fills the gap). containsKey() on ArrayList is O(n); TreeMap.get() is O(log n). TreeMap also provides floorKey(), ceilingKey(), and range views — none of which ArrayList supports natively. For any use case with frequent insertions, deletions, and key-based lookups, TreeMap is strictly superior to a manually maintained sorted list.
FAQs
Does TreeMap allow duplicate keys?
No. When compare(a, b) == 0, TreeMap treats a and b as the same key and replaces the existing value. This is determined purely by the Comparator or compareTo() — not by equals(). Two objects can be logically different (by equals()) but treated as duplicates by TreeMap if the comparison returns 0 without a tiebreaker field.
What is the difference between TreeMap and TreeSet?
TreeSet is backed by a TreeMap where elements become keys and a shared static PRESENT object is the value. Everything about TreeMap's Red-Black tree, O(log n) operations, null rejection, and NavigableSet API (which parallels NavigableMap) applies directly to TreeSet. Use TreeMap when each key has an associated value to retrieve; use TreeSet for pure membership testing with sorted unique elements.
How do I create a TreeMap in reverse sorted order?
Pass Comparator.reverseOrder() for natural reverse ordering: new TreeMap<>(Comparator.reverseOrder()). For custom reverse ordering: myComparator.reversed(). This creates a max-first tree where firstKey() returns the largest key and lastKey() returns the smallest. descendingMap() provides a reverse-order view of an existing TreeMap without creating a new map.
Is TreeMap thread-safe?
No. Concurrent modifications from multiple threads can corrupt the tree structure. For concurrent sorted maps, use ConcurrentSkipListMap from java.util.concurrent, which implements ConcurrentNavigableMap — the full NavigableMap interface with O(log n) thread-safe operations including floorKey(), ceilingKey(), and all range views.
What is ConcurrentSkipListMap and when should it replace TreeMap?
ConcurrentSkipListMap is a thread-safe sorted map backed by a skip list (probabilistic data structure providing O(log n) expected performance). It supports the full NavigableMap interface without any external synchronisation. Use it when multiple threads need concurrent read and write access to a sorted map. TreeMap with Collections.synchronizedSortedMap() exists but holds a single lock — only one thread accesses the map at a time. ConcurrentSkipListMap allows fine-grained concurrent access at much higher throughput.
What happens if the compareTo() method is inconsistent with equals()?
TreeMap's behaviour becomes inconsistent with the Map contract. The general Map contract defines key uniqueness by equals(). If compareTo() returns 0 for two keys where equals() returns false, TreeMap treats them as one key while a HashMap would treat them as two. BigDecimal is the standard Java example: new BigDecimal("2.0").compareTo(new BigDecimal("2.00")) == 0 but equals() returns false. In a TreeMap<BigDecimal, V>, only one of them can be stored. The Java documentation recommends making compareTo() consistent with equals() for SortedMap keys.
Summary
TreeMap<K, V> is Java's sorted map — O(log n) for every operation, guaranteed by Red-Black tree invariants that bound height at 2 × log₂(n+1). Keys are always maintained in ascending order, and the full NavigableMap API — floorKey(), ceilingKey(), headMap(), tailMap(), subMap(), descendingMap() — enables range queries and nearest-key navigation that no hash-based collection provides.
Two rules prevent the most common TreeMap bugs: always include a tiebreaker in the Comparator when the primary sort field is not unique (equal keys silently overwrite), and never insert null keys (compareTo throws). Range views are live and O(1) to create — use subMap() for range queries instead of streaming and filtering the entire map.
For interviews: explain the Red-Black tree structure and O(log n) guarantee, describe how duplicates are detected via compareTo() not equals(), walk through the NavigableMap method set, explain why null keys throw, and distinguish TreeMap from ConcurrentSkipListMap for concurrent use. These questions appear consistently from fresher campus rounds through senior engineering interviews.
What to Read Next
Learn a HashMap that's safe to use across multiple threads.