Java TreeMap and TreeSet Explained — Sorted, Navigable, and Powerful
- TreeMap and TreeSet use a Red-Black Tree internally, guaranteeing O(log n) for every insert, delete, and lookup — but also for range queries that HashMap/HashSet can't even do efficiently.
- The Comparator you pass to a TreeMap or TreeSet is the equality definition —
compare()returning 0 means 'same element', regardless of whatequals()says. Always add a tiebreaker or you'll silently lose data. - The NavigableMap and NavigableSet APIs (floor, ceiling, headMap, tailSet, subMap) are the real reason to choose TreeMap/TreeSet. If you're not using at least one of those methods, you probably want a HashMap/HashSet instead.
Imagine a library where books are always kept in alphabetical order on the shelves — no matter when a new book arrives, the librarian slots it into exactly the right spot. That's a TreeSet or TreeMap. Unlike a pile of books on the floor (a HashMap), every insertion maintains perfect order automatically. You pay a small price in speed, but you gain the superpower of always knowing what's first, last, or nearest to any value.
Every non-trivial Java application eventually needs sorted data — a leaderboard ranked by score, a task scheduler ordered by deadline, a word-frequency map listed alphabetically. You could dump everything into an ArrayList and call Collections.sort() every time, but that's like re-alphabetising your entire library every time a new book arrives. Java's TreeMap and TreeSet solve this permanently by keeping data sorted at all times, automatically, on every insert and delete.
The problem with HashMap and HashSet is that they trade order for raw speed. That's a great deal when you only need to answer 'does this key exist?' or 'give me this value'. But the moment a user asks 'show me the top 10 scores' or 'find everything between Tuesday and Friday', a HashMap forces you to scan the entire structure. A TreeMap answers those range and boundary questions in O(log n) time because it's backed by a Red-Black Tree — a self-balancing binary search tree that keeps everything sorted without you lifting a finger.
By the end of this article you'll know exactly when to reach for a TreeMap versus a HashMap, how to exploit the NavigableMap and NavigableSet APIs to slice your data in powerful ways, and which subtle mistakes cause ClassCastExceptions and incorrect ordering in production. You'll also have concrete, real-world patterns you can copy directly into your own codebase.
TreeSet — When Order Is Non-Negotiable
A TreeSet is a Set — so it never stores duplicates — but unlike HashSet it keeps every element in its natural sorted order at all times. 'Natural order' means whatever Comparable defines for that type: alphabetical for Strings, ascending for numbers, chronological for dates.
The real payoff isn't just iteration order. It's the NavigableSet API: methods like first(), last(), floor(), ceiling(), headSet(), and tailSet() let you slice and query your sorted data without loops. Want every username that comes after 'mike' alphabetically? One line. Want the five closest scores below a threshold? One line.
Internally, TreeSet wraps a TreeMap and stores your elements as keys. Every add(), remove(), and contains() runs in O(log n) — slower than HashSet's O(1) average, but a fair trade when you genuinely need sorted access. Don't use TreeSet by default; use it when the ordering itself is part of the feature you're building.
package io.thecodeforge.collections; import java.util.TreeSet; import java.util.NavigableSet; public class LeaderboardTreeSet { public static void main(String[] args) { // io.thecodeforge: Scores are kept sorted (ascending) automatically TreeSet<Integer> scores = new TreeSet<>(); scores.add(4200); scores.add(1500); scores.add(8750); scores.add(3300); scores.add(9100); scores.add(4200); // duplicate — silently ignored System.out.println("All scores (always sorted): " + scores); // Lowest and highest score in O(log n) System.out.println("Lowest score : " + scores.first()); System.out.println("Highest score: " + scores.last()); // floor() returns the greatest element <= 4000 System.out.println("Highest score at or below 4000: " + scores.floor(4000)); // subSet with booleans for inclusivity NavigableSet<Integer> mainBracket = scores.subSet(3000, true, 9000, false); System.out.println("Main bracket [3000, 9000): " + mainBracket); } }
Lowest score : 1500
Highest score: 9100
Highest score at or below 4000: 3300
Main bracket [3000, 9000): [3300, 4200, 8750]
TreeMap — Sorted Keys With All the Power of a Map
TreeMap is to HashMap what TreeSet is to HashSet: it keeps all its keys in sorted order at all times, and it exposes a rich NavigableMap API for range queries, boundary lookups, and sliced views.
The classic use case is any kind of timeline, schedule, or rank-ordered dataset. Imagine you're building a task scheduler. Each task has a deadline (the key) and a description (the value). A TreeMap lets you instantly find the next task due, all tasks due before a certain date, or every task in a given window — without scanning the entire map.
TreeMap also implements SortedMap and NavigableMap, which means you get firstKey(), lastKey(), floorKey(), ceilingKey(), lowerKey(), higherKey(), headMap(), tailMap(), and subMap() — all backed by the same O(log n) Red-Black Tree. You can even get a descending view of the entire map with descendingMap(). These aren't convenience extras; they're the whole reason to choose TreeMap over HashMap.
package io.thecodeforge.collections; import java.time.LocalDate; import java.util.Map; import java.util.NavigableMap; import java.util.TreeMap; public class TaskSchedulerTreeMap { public static void main(String[] args) { // io.thecodeforge: Standardizing chronologically sorted task maps TreeMap<LocalDate, String> schedule = new TreeMap<>(); schedule.put(LocalDate.of(2025, 7, 15), "Submit tax return"); schedule.put(LocalDate.of(2025, 6, 30), "Renew software licences"); schedule.put(LocalDate.of(2025, 8, 1), "Team performance reviews"); schedule.put(LocalDate.of(2025, 7, 4), "Deploy v2.0 to production"); LocalDate today = LocalDate.of(2025, 7, 1); // ceilingKey returns the smallest key >= today LocalDate nextDeadline = schedule.ceilingKey(today); System.out.println("Next task: " + nextDeadline + " - " + schedule.get(nextDeadline)); // headMap(key, inclusive) provides an efficient view of past entries NavigableMap<LocalDate, String> overdue = schedule.headMap(today, false); System.out.println("Overdue count: " + overdue.size()); } }
Overdue count: 1
Custom Ordering With Comparator — When Natural Order Isn't Enough
Natural order works great for Strings and numbers, but what about your own objects? And what if you want reverse order, or a case-insensitive sort? That's where a Comparator comes in.
Both TreeMap and TreeSet accept a Comparator in their constructor. Once you supply one, it completely overrides natural order — even if your objects implement Comparable. This is powerful, but there's a critical rule: the Comparator you use for ordering becomes the equality definition for the collection. Two elements are considered duplicates if comparator.compare(a, b) == 0, not if a.equals(b). This surprises many developers.
A practical pattern: pass Comparator.reverseOrder() to get a descending TreeMap, which is perfect for leaderboards where you want the highest value first. Or pass String.CASE_INSENSITIVE_ORDER to a TreeSet of strings so 'Apple' and 'apple' are treated as the same entry. The Comparator is your formatting rule for the entire collection.
package io.thecodeforge.collections; import java.util.Comparator; import java.util.TreeMap; public class CustomComparatorDemo { record Product(String name, double price) {} public static void main(String[] args) { // io.thecodeforge: Custom Comparator for secondary sorting Comparator<Product> byPriceThenName = Comparator .comparingDouble(Product::price) .thenComparing(Product::name); TreeMap<Product, Integer> inventory = new TreeMap<>(byPriceThenName); inventory.put(new Product("Mechanical Keyboard", 120.00), 15); inventory.put(new Product("Wireless Mouse", 45.00), 22); inventory.put(new Product("USB-C Cable", 15.00), 100); inventory.forEach((p, qty) -> System.out.println(p.name() + ": $" + p.price())); } }
Wireless Mouse: $45.0
Mechanical Keyboard: $120.0
equals() considers different, the TreeSet or TreeMap will treat them as the same element and silently discard the second one. Always make your Comparator consistent with equals() — or at least be deliberate when it isn't (like the case-insensitive example above, where the behaviour is intentional).TreeMap vs HashMap vs TreeSet vs HashSet — Choosing the Right Tool
The most common mistake is defaulting to HashMap or HashSet out of habit and then discovering you need sorted access later — at which point you're either converting structures or adding sort calls everywhere. Choosing right from the start costs nothing.
The decision is straightforward: if you need to ask range questions ('everything between X and Y'), boundary questions ('what's the closest element to Z?'), or if iteration order must be predictable and sorted, use the Tree variant. If you only ever ask point questions ('does this key exist?', 'give me this value') and don't care about order, stick with the Hash variant for its faster O(1) average operations.
LinkedHashMap and LinkedHashSet are the middle ground — they maintain insertion order but don't sort. Use them when you want predictable iteration without the overhead of sorting. The table below captures the key trade-offs so you can settle this question in seconds during a code review.
package io.thecodeforge.collections; import java.util.*; public class ChoosingTheRightCollection { public static void main(String[] args) { List<String> rawData = List.of("cherry", "apple", "banana"); // O(1) speed, random order Set<String> unordered = new HashSet<>(rawData); // O(1) speed, maintains "cherry", "apple", "banana" order Set<String> insertionOrdered = new LinkedHashSet<>(rawData); // O(log n) speed, maintains "apple", "banana", "cherry" order Set<String> sorted = new TreeSet<>(rawData); System.out.println("TreeSet Order: " + sorted); } }
| Feature / Aspect | TreeMap / TreeSet | HashMap / HashSet |
|---|---|---|
| Internal structure | Red-Black Tree | Hash Table (array of buckets) |
| Iteration order | Always sorted (natural or Comparator) | Unpredictable (effectively random) |
| get / contains | O(log n) worst case | O(1) average, O(n) worst case |
| put / add | O(log n) worst case | O(1) average, O(n) worst case |
| Range queries (subMap, headSet…) | Built-in, O(log n) | Not supported — requires full scan |
| Null keys allowed | No — throws NullPointerException | Yes (one null key for HashMap) |
| Null values allowed (Map only) | Yes | Yes |
| Thread safety | Not thread-safe | Not thread-safe |
| Memory overhead | Higher (tree node pointers) | Lower (flat array) |
| Best used when | Sorted iteration or range queries needed | Maximum speed, order doesn't matter |
🎯 Key Takeaways
- TreeMap and TreeSet use a Red-Black Tree internally, guaranteeing O(log n) for every insert, delete, and lookup — but also for range queries that HashMap/HashSet can't even do efficiently.
- The Comparator you pass to a TreeMap or TreeSet is the equality definition —
compare()returning 0 means 'same element', regardless of whatequals()says. Always add a tiebreaker or you'll silently lose data. - The NavigableMap and NavigableSet APIs (floor, ceiling, headMap, tailSet, subMap) are the real reason to choose TreeMap/TreeSet. If you're not using at least one of those methods, you probably want a HashMap/HashSet instead.
- Never mutate an object's fields that affect its sort order after inserting it as a key or element — the tree won't re-sort itself and the collection's integrity breaks silently with no exception thrown.
⚠ Common Mistakes to Avoid
Interview Questions on This Topic
- QHow does TreeMap maintain its balance, and what is the time complexity of a re-balancing operation?
- QWhy does TreeMap throw NullPointerException for null keys while HashMap allows one null key?
- QExplain the 'consistency with equals' requirement for Comparators used in TreeSets. What happens if
compare(a, b) == 0buta.equals(b)is false? - QHow does ConcurrentSkipListMap serve as a thread-safe alternative to TreeMap, and what are its performance trade-offs?
Frequently Asked Questions
Does TreeMap allow null keys in Java?
No. TreeMap throws a NullPointerException if you try to insert a null key, because it must call compareTo() or the Comparator on every key during insertion, and invoking a method on null causes the exception. Null values (not keys) are perfectly fine. If you need null key support with sorted iteration, you'd have to handle the null case manually in a custom Comparator — but it's usually cleaner to use a sentinel value instead.
Is TreeMap thread-safe?
No, TreeMap is not thread-safe. Concurrent reads and writes from multiple threads can corrupt the internal Red-Black Tree structure. Wrap it with Collections.synchronizedSortedMap(new TreeMap<>()) for basic synchronisation, or use ConcurrentSkipListMap from java.util.concurrent, which provides thread-safe sorted map operations with better concurrent throughput than a synchronised TreeMap.
What is the difference between floorKey() and lowerKey() in TreeMap?
floorKey(k) returns the greatest key that is less than or equal to k (inclusive). lowerKey(k) returns the greatest key strictly less than k (exclusive). Similarly, ceilingKey(k) is >= k, while higherKey(k) is strictly > k. Use floor/ceiling when the exact match is a valid result for your query.
Developer and founder of TheCodeForge. I built this site because I was tired of tutorials that explain what to type without explaining why it works. Every article here is written to make concepts actually click.