Java Tutorial
🔍

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

OperationHashMapLinkedHashMapTreeMap
put(k, v)O(1) avgO(1) avgO(log n)
get(k)O(1) avgO(1) avgO(log n)
remove(k)O(1) avgO(1) avgO(log n)
firstKey / lastKeyN/AN/AO(log n)
floorKey / ceilingKeyN/AN/AO(log n)
headMap / tailMap viewN/AN/AO(1) view creation
IterationO(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 stored

Mistake 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 view
Output:
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