Skip to content
Home Java Java TreeMap and TreeSet Explained — Sorted, Navigable, and Powerful

Java TreeMap and TreeSet Explained — Sorted, Navigable, and Powerful

Where developers are forged. · Structured learning · Free forever.
📍 Part of: Collections → Topic 6 of 21
TreeMap and TreeSet in Java keep your data automatically sorted.
⚙️ Intermediate — basic Java knowledge assumed
In this tutorial, you'll learn
TreeMap and TreeSet in Java keep your data automatically sorted.
  • 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 what equals() 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.
✦ Plain-English analogy ✦ Real code with output ✦ Interview questions
Quick Answer

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.

LeaderboardTreeSet.java · JAVA
12345678910111213141516171819202122232425262728293031
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);
    }
}
▶ Output
All scores (always sorted): [1500, 3300, 4200, 8750, 9100]
Lowest score : 1500
Highest score: 9100
Highest score at or below 4000: 3300
Main bracket [3000, 9000): [3300, 4200, 8750]
💡Pro Tip: Views Are Live
headSet(), tailSet(), and subSet() don't copy data — they return a live view backed by the original TreeSet. If you add a score that falls within the range after creating the view, it appears in the view automatically. This is great for efficiency but can bite you if you mutate the set while iterating the view.

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.

TaskSchedulerTreeMap.java · JAVA
12345678910111213141516171819202122232425262728
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());
    }
}
▶ Output
Next task: 2025-07-04 - Deploy v2.0 to production
Overdue count: 1
🔥Interview Gold: Why Red-Black Tree?
TreeMap uses a Red-Black Tree specifically because it guarantees O(log n) worst-case for insert, delete, and lookup. A plain binary search tree can degrade to O(n) if elements are inserted in sorted order. Red-Black Trees self-balance on every mutation, so worst-case is always logarithmic — that's why Java's designers chose it.

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.

CustomComparatorDemo.java · JAVA
12345678910111213141516171819202122
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()));
    }
}
▶ Output
USB-C Cable: $15.0
Wireless Mouse: $45.0
Mechanical Keyboard: $120.0
⚠ Watch Out: Comparator IS the Equality Check
If your Comparator returns 0 for two objects that 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.

ChoosingTheRightCollection.java · JAVA
1234567891011121314151617181920
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);
    }
}
▶ Output
TreeSet Order: [apple, banana, cherry]
🔥Pro Tip: Cheap Conversion
new TreeMap<>(existingHashMap) creates a sorted copy in O(n log n). This is a common pattern: build fast with HashMap during heavy write phases, then wrap in a TreeMap for a read-heavy, sorted-output phase. Just remember it's a copy — mutations to one don't affect the other.
Feature / AspectTreeMap / TreeSetHashMap / HashSet
Internal structureRed-Black TreeHash Table (array of buckets)
Iteration orderAlways sorted (natural or Comparator)Unpredictable (effectively random)
get / containsO(log n) worst caseO(1) average, O(n) worst case
put / addO(log n) worst caseO(1) average, O(n) worst case
Range queries (subMap, headSet…)Built-in, O(log n)Not supported — requires full scan
Null keys allowedNo — throws NullPointerExceptionYes (one null key for HashMap)
Null values allowed (Map only)YesYes
Thread safetyNot thread-safeNot thread-safe
Memory overheadHigher (tree node pointers)Lower (flat array)
Best used whenSorted iteration or range queries neededMaximum 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 what equals() 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

    Storing objects in TreeSet without Comparable or a Comparator — When you add a custom class like `Employee` to a TreeSet without implementing Comparable or supplying a Comparator, you'll get a ClassCastException at runtime (not compile time) on the second insertion: 'Employee cannot be cast to Comparable'. Fix: implement Comparable<Employee> in your class, or pass a Comparator to the TreeSet constructor. The error appears at the second add() because that's when TreeSet first tries to compare two elements.
    Fix

    implement Comparable<Employee> in your class, or pass a Comparator to the TreeSet constructor. The error appears at the second add() because that's when TreeSet first tries to compare two elements.

    Assuming comparator.compare(a, b) == 0 means the element is rejected as a duplicate when you didn't intend it — A developer creates a TreeSet<Product> with a Comparator that sorts only by price. Two products with the same price but different names are treated as the same element; the second one is silently discarded. The set ends up with fewer entries than expected and no exception is thrown. Fix: always include a tiebreaker in your Comparator (e.g., .thenComparing(Product::name)) so that compare() returns 0 only when the objects are genuinely the same.
    Fix

    always include a tiebreaker in your Comparator (e.g., .thenComparing(Product::name)) so that compare() returns 0 only when the objects are genuinely the same.

    Mutating a key's fields after inserting it into a TreeMap or TreeSet — TreeMap and TreeSet don't re-sort entries when an existing object's state changes. If you insert a Product keyed by price, then change that product's price field externally, the tree structure becomes corrupted — lookups may fail or return wrong results, and the collection's sorted invariant is silently broken. Fix: treat keys as immutable after insertion, or use immutable value types (records, Strings, primitives). If you must change a key, remove it, mutate it, then re-insert.
    Fix

    treat keys as immutable after insertion, or use immutable value types (records, Strings, primitives). If you must change a key, remove it, mutate it, then re-insert.

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) == 0 but a.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.

🔥
Naren Founder & Author

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.

← PreviousHashSet in JavaNext →Stack and Queue in Java
Forged with 🔥 at TheCodeForge.io — Where Developers Are Forged