Junior 10 min · March 05, 2026

Java TreeSet Comparator — How It Silently Drops Duplicates

TreeSet comparator defined only on score caused 950 leaderboard entries to be silently dropped.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • TreeMap/TreeSet maintain automatic sorted order via Red-Black Tree
  • Keys/elements must implement Comparable or receive a Comparator
  • Range queries (subMap, headSet) are O(log n) - the killer feature
  • Two elements are equal if compare() returns 0, not if equals() says so
  • Null keys throw NullPointerException; null values are allowed
  • Mutation of key fields after insertion silently corrupts the tree
Plain-English First

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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.
Production Insight
If you need range queries, TreeSet is the only Set implementation that provides them in O(log n).
But if you only iterate the set once, consider sorting a List instead — it avoids the overhead of preserving order on every insert.
Rule: use TreeSet when you query ranges more than you insert.
Key Takeaway
TreeSet wraps a TreeMap and uses NavigableSet for range slicing.
Live views (headSet, subSet) reflect changes to the backing set.
Don't use TreeSet unless you actually call first(), last(), or range methods.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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.
Production Insight
TreeMap's sorted order adds ~40% overhead vs HashMap for single-key operations.
But for range queries, it's orders of magnitude faster than scanning a HashMap.
Rule: if you never use floorKey(), headMap(), or subMap(), you're paying for sorting you don't need.
Key Takeaway
TreeMap provides O(log n) range queries via NavigableMap.
descendingMap() gives reverse order without extra cost.
Use TreeMap when your primary access pattern is sorted iteration or range lookups.

The true power of TreeMap and TreeSet lies in their Navigable interfaces — not just sorted iteration, but the ability to slice, dice, and query your data in logarithmic time. These methods don't copy data; they return live views that reflect ongoing mutations.

  • floorKey(k) / ceilingKey(k): Find the nearest key less than/equal or greater than/equal to a target.
  • lowerKey(k) / higherKey(k): Strict versions of above (exclusive).
  • headMap(k, inclusive): All keys less than (or ≤) k.
  • tailMap(k, inclusive): All keys greater than (or ≥) k.
  • subMap(from, fromInclusive, to, toInclusive): A contiguous slice.
  • descendingMap() / descendingSet(): Reverse-order views without sorting copy.
  • pollFirstEntry() / pollLastEntry(): Retrieve and remove boundary entries.

Use these when you need to answer 'what's the next available slot?', 'find all orders within a date range', or 'get the top 5 scores' without ever calling sort().

NavigableApiDemo.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package io.thecodeforge.collections;

import java.util.NavigableMap;
import java.util.TreeMap;

public class NavigableApiDemo {
    public static void main(String[] args) {
        // io.thecodeforge: Using NavigableMap for real-time slicing
        TreeMap<Integer, String> map = new TreeMap<>();
        map.put(10, "ten");
        map.put(20, "twenty");
        map.put(30, "thirty");
        map.put(40, "forty");
        map.put(50, "fifty");

        // Closest key >= 25
        System.out.println("Ceiling of 25: " + map.ceilingKey(25)); // 30

        // Closest key < 30
        System.out.println("Lower of 30: " + map.lowerKey(30)); // 20

        // Entries between 20 (inclusive) and 40 (exclusive)
        NavigableMap<Integer, String> slice = map.subMap(20, true, 40, false);
        System.out.println("Submap [20,40): " + slice);

        // Reverse order view
        System.out.println("Descending: " + map.descendingMap());
    }
}
Output
Ceiling of 25: 30
Lower of 30: 20
Submap [20,40): {20=twenty, 30=thirty}
Descending: {50=fifty, 40=forty, 30=thirty, 20=twenty, 10=ten}
Mental Model: Tree as a Sorted Index
  • floor/lower find the page before or at the topic
  • headMap/tailMap give you the entire chapter before or after
  • subMap is the exact page range you're looking for
  • descendingMap flips the book upside down without reprinting it
Production Insight
Live views are efficient but dangerous: modifying the parent collection while iterating a view throws ConcurrentModificationException.
If you need a snapshot, copy the view into a new ArrayList or HashSet.
Rule: use views for read-heavy patterns; copy when you need to mutate independently.
Key Takeaway
NavigableMap's range methods are O(log n) and return live views.
Ceiling/floor are for 'nearest neighbor' queries.
Descending views cost no extra memory or time.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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).
Production Insight
A Comparator that is inconsistent with equals() is a common source of data loss.
Test with duplicate sort keys: write a unit test that inserts two objects with the same sort value and verify both are present if they have different identities.
Rule: always add a tiebreaker (thenComparing) that uses a unique identifier.
Key Takeaway
Comparator overrides natural order and defines equality.
Inconsistent comparator vs equals() causes silent data loss.
Always use .thenComparing() with a unique field to distinguish objects.

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.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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.
Production Insight
Using TreeMap when HashMap suffices wastes memory (tree node pointers) and time (log vs 1).
But retrofitting sorting onto a HashMap later is expensive: you'll either sort on every read or maintain a separate index.
Rule: choose sorted collection at design time if you need range queries, never as a default.
Key Takeaway
TreeMap/TreeSet for sorted iteration and range queries.
HashMap/HashSet for maximum speed on point queries.
LinkedHashMap/LinkedHashSet for insertion-order without sorting overhead.

TreeMap vs HashMap — Detailed Comparison

While both implement Map, TreeMap and HashMap serve fundamentally different purposes. The table below highlights the key differences you need to consider when choosing.

FeatureTreeMapHashMap
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 queriesBuilt-in O(log n)Not supported
Null keysNot allowed (NPE)One null key allowed
Null valuesAllowedAllowed
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
Use case exampleTask scheduler, leaderboard, price range queriesCaching, ID lookups, configuration maps

This table is a quick reference for code reviews and design decisions. When in doubt, ask: "Do I need to answer 'what's between X and Y'?" If yes, TreeMap is the answer.

Production Insight
Many teams default to HashMap and retrofit sorting later. That's often acceptable if the data volume is small, but for large datasets the performance cost of sorting on every read is substantial. Use TreeMap from the start when range queries are a first-class requirement.
Key Takeaway
TreeMap offers sorted iteration and range queries at O(log n); HashMap offers O(1) average point operations. Choose based on whether you need sorted access.

TreeSet vs HashSet — Detailed Comparison

The same trade-offs apply to the Set variants. Here's a direct comparison.

FeatureTreeSetHashSet
Internal structureRed-Black Tree (backed by TreeMap)Hash Table
Iteration orderAlways sortedUnpredictable
add / contains / removeO(log n)O(1) average, O(n) worst
Range queriesBuilt-in (subSet, headSet) O(log n)Not supported
Null elementsNot allowed (NPE)One null allowed
Thread safetyNot thread-safeNot thread-safe
Memory overheadHigher (tree node pointers)Lower
Best used whenNeed sorted elements or range searchesMaximum performance, order irrelevant
Use case exampleUnique sorted scores, auto-complete listsDeduplication, presence checks

Like the Map counterparts, the decision hinges on whether you ever need ordered iteration or range queries. If you only need uniqueness and fast membership tests, HashSet is the better choice.

Production Insight
TreeSet is often mistakenly used for 'unique list that needs to be sorted'. If the collection is only iterated once (e.g., at the end of a batch), consider collecting into a HashSet first and then sorting into a list — that can be faster than maintaining a TreeSet throughout the operation.
Key Takeaway
TreeSet provides sorted iteration and range queries; HashSet provides faster point operations. Choose TreeSet only when you need sorted access.

Advantages and Disadvantages of TreeMap and TreeSet

Both TreeMap and TreeSet share the same underlying data structure (Red-Black Tree), so their strengths and weaknesses are nearly identical.

Advantages

AdvantageExplanation
Automatic sortingElements are always in order without manual sorting.
Rich Navigable APIfloor, ceiling, headSet, subSet, descendingMap — all O(log n).
Live viewsRange views reflect mutations to the backing collection instantly.
Guaranteed log(n) performanceAll operations are O(log n) worst-case, no performance cliffs.
Comparator injectionCustom ordering without modifying the element class.
Natural ordering fallbackWorks with any Comparable type out of the box.

Disadvantages

DisadvantageExplanation
Slower than hash-based counterpartsO(log n) vs O(1) for get/put/add. 40% more overhead for single-key ops.
No null keys/elementsNullPointerException on null keys/elements.
Higher memory consumptionTree nodes store pointers to left, right, parent, and color (Red/Black).
Comparator inconsistency riskSilent data loss if compare() returns 0 for non-identical objects.
Not thread-safeRequires external synchronization or ConcurrentSkipListMap for concurrent access.
Key mutation dangerMutating key fields after insertion breaks the tree without warning.

These trade-offs are fundamental to the data structure. Use the advantages when you need sorted ordering; avoid the disadvantages by carefully designing keys and comparators.

Production Insight
In high-throughput systems, the 40% overhead of TreeMap vs HashMap can be significant. Profile your application: if range queries are rare, consider using a HashMap and sorting only when required (e.g., at endpoint level). If range queries are frequent, the TreeMap's O(log n) range slicing will vastly outperform sorting a HashMap on each request.
Key Takeaway
TreeMap/TreeSet offer automatic sorting and powerful range queries at the cost of slower single-key operations, no null keys, and higher memory usage.

Advanced NavigableMap Operations: descendingMap, pollFirstEntry, pollLastEntry

Beyond the basic floor/ceiling and sub-range views, NavigableMap offers three powerful methods for boundary manipulation: descendingMap(), pollFirstEntry(), and pollLastEntry(). These are especially useful for leaderboards, job queues, and any scenario where you need to process items in order while mutating the collection.

  • descendingMap(): Returns a reverse-order view of the map. No new data is created; iteration over the descending map is just as fast and memory-cheap as iterating forward. Combined with subMap(), you can easily get slices in descending order.
  • pollFirstEntry(): Retrieves and removes the entry with the smallest key. This is a single O(log n) operation that both reads and deletes the first entry. Equivalent to firstEntry() + remove(firstKey()) but atomic and more efficient.
  • pollLastEntry(): Retrieves and removes the entry with the largest key. Same benefits as pollFirstEntry for the other end.

These methods are commonly used in priority queues where you want to serve items in order while draining the structure. They are also great for implementing round-robin or timeout processing without additional locking or iteration.

AdvancedNavigableMap.javaJAVA
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package io.thecodeforge.collections;

import java.util.TreeMap;
import java.util.NavigableMap;

public class AdvancedNavigableMap {
    public static void main(String[] args) {
        TreeMap<String, Integer> scores = new TreeMap<>();
        scores.put("Alice", 2500);
        scores.put("Bob", 1800);
        scores.put("Charlie", 3200);
        scores.put("Diana", 1500);
        scores.put("Eve", 2900);

        // Descending order view
        NavigableMap<String, Integer> descending = scores.descendingMap();
        System.out.println("Descending order: " + descending);
        // Output: Descending order: {Eve=2900, Charlie=3200, Alice=2500, Bob=1800, Diana=1500}

        // Poll first (lowest) entry
        System.out.println("Poll first (lowest score): " + scores.pollFirstEntry());
        // Output: Poll first (lowest score): Diana=1500
        System.out.println("After pollFirstEntry: " + scores);
        // Output: After pollFirstEntry: {Alice=2500, Bob=1800, Charlie=3200, Eve=2900}

        // Poll last (highest) entry
        System.out.println("Poll last (highest score): " + scores.pollLastEntry());
        // Output: Poll last (highest score): Eve=2900
        System.out.println("After pollLastEntry: " + scores);
        // Output: After pollLastEntry: {Alice=2500, Bob=1800, Charlie=3200}
    }
}
Output
Descending order: {Eve=2900, Charlie=3200, Alice=2500, Bob=1800, Diana=1500}
Poll first (lowest score): Diana=1500
After pollFirstEntry: {Alice=2500, Bob=1800, Charlie=3200, Eve=2900}
Poll last (highest score): Eve=2900
After pollLastEntry: {Alice=2500, Bob=1800, Charlie=3200}
Polling for Job Queues
pollFirstEntry() is ideal for implementing a priority queue where you want to process items in order and remove them from the queue simultaneously. Use TreeMap with a custom Comparator for priority (e.g., deadline, urgency) and pollFirstEntry() in a loop to drain the queue safely.
Production Insight
descendingMap() is particularly useful for leaderboards that need both ascending and descending views. You can keep one TreeMap and call descendingMap() for the 'top scores' view — no separate reverse-sorted structure needed. This halves memory usage and avoids consistency problems.
pollFirstEntry() and pollLastEntry() are atomic for single-threaded use. In concurrent scenarios, wrap them in synchronized blocks or use ConcurrentSkipListMap.
Key Takeaway
pollFirstEntry/pollLastEntry combine retrieval and removal in one O(log n) operation — ideal for draining a sorted map.
descendingMap() provides reverse order at zero extra cost.

Practice Problems: Interval Scheduling, Range Queries, Sliding Window

The best way to internalize the power of TreeMap and TreeSet is to solve problems that naturally require sorted order and range queries. Below are five practice problems you can implement to sharpen your skills.

1. Interval Scheduling with TreeSet Given a list of intervals (start, end), find the maximum number of non-overlapping intervals you can schedule. Use a TreeSet to store intervals sorted by end time. Iterate through sorted intervals, and when you find one that doesn't overlap with the last scheduled, add it. Demonstrates floor() or last() for boundary checks.

2. Range Sum Queries on a Dynamic Array Maintain a TreeMap of cumulative sums or directly a TreeMap<Integer, Integer> where keys are indices. Support operations: add value at index, query sum between indices. Use subMap() to get entries in the range and compute the sum. Simulates a Fenwick tree without the complexity.

3. Sliding Window Median Given a stream of integers and a window size k, find the median of each window. Use two TreeSets (or one TreeSet with order statistics) to maintain left and right halves. Use last() and first() to get the max of left and min of right. Demonstrates maintaining balance and using boundary methods.

4. Nearest Neighbor in 1D Given a set of points on a line (integers), support insert(x) and query(x) that returns the closest point to x. Use TreeSet and its floor() and ceiling() methods. Compare distances to find nearest. Classic example of why NavigableSet exists.

5. Task Scheduler with Deadline Constraints Given tasks with deadlines and durations, find if all tasks can be scheduled. Use a TreeMap to store tasks sorted by deadline. Iterate from earliest deadline, subtract duration from current time, and if you ever go negative, impossible. Uses firstKey() and pollFirstEntry() efficiently.

Each problem can be solved in < 50 lines of Java using TreeMap/TreeSet. Implement them to gain confidence with the APIs discussed in this article.

Production Insight
These problems mirror real production scenarios: the interval scheduling problem is essentially the same as booking meeting rooms; range sum queries are used in real-time analytics dashboards; sliding window median appears in stock trading volatility calculations. Master these patterns and you'll immediately see opportunities to apply TreeMap/TreeSet in your day job.
Key Takeaway
Practice problems solidify your understanding of TreeMap/TreeSet's range query capabilities. Focus on problems that require floor/ceiling or sub-range views.
● Production incidentPOST-MORTEMseverity: high

The Case of the Missing Leaderboard Entries

Symptom
Player scores were being lost. The leaderboard had ~1000 expected entries but only showed 50 unique score values.
Assumption
The TreeSet would handle duplicate scores because each player has a different name. The team assumed the Comparator only looked at scores, not player ID.
Root cause
The Comparator for the TreeSet was defined solely on the score field. Two players with the same score were treated as the same element — the second insertion was silently ignored.
Fix
Changed the Comparator to include a tiebreaker: .thenComparing(Player::playerId). This made compare() return 0 only for truly identical players.
Key lesson
  • Comparator IS the equality definition in TreeSets and TreeMaps
  • Always add a tiebreaker unless you intentionally want duplicates suppressed
  • Test with deliberately equal sort keys to verify behavior
Production debug guideSymptom → Action guide for common ordered collection problems5 entries
Symptom · 01
ClassCastException on second insertion
Fix
Verify the element class implements Comparable, or that you passed a Comparator to the constructor. The error appears at runtime not compile time.
Symptom · 02
TreeSet/TreeMap has fewer elements than expected
Fix
Review your Comparator — compare(a,b) == 0 means duplicate. Use a tiebreaker like .thenComparing() to ensure equality only for genuine duplicates.
Symptom · 03
Contains() or get() returns unexpected results after mutation
Fix
Key objects that were changed after insertion break the tree structure. Use immutable keys or remove/re-insert after mutation.
Symptom · 04
NullPointerException on put/Add
Fix
TreeMap does not allow null keys. Check for null before inserting, or use a sentinel value. Null values are fine.
Symptom · 05
Iteration order is not sorted
Fix
TreeMap and TreeSet iterate in sorted order. If you see random order, you may have used a HashMap/HashSet by mistake. Check the collection type.
★ TreeMap/TreeSet Quick Debug Cheat SheetHands-on commands and fixes for the three most common production issues.
ClassCastException when adding second element
Immediate action
Check if the class implements Comparable or if a Comparator was provided.
Commands
Add Comparable definition: class MyClass implements Comparable<MyClass> { ... }
Or pass a Comparator: new TreeSet<>(Comparator.comparing(MyClass::getField))
Fix now
If the class is third-party, wrap in a Comparator-based TreeSet immediately.
Silent data loss — fewer entries than expected+
Immediate action
List all keys being added and compare them using the Comparator logic.
Commands
Print the comparator: System.out.println(treeSet.comparator());
Check equals() vs compare(): add a temporary custom comparator that logs compare() calls.
Fix now
Add a secondary sort key: Comparator.comparing(MyClass::primary).thenComparing(MyClass::id)
Lookups returning wrong results after mutation+
Immediate action
Stop mutating key fields after insertion. Treat keys as immutable.
Commands
Consider using records: record Key(String name, int id) implements Comparable<Key> { ... }
If mutation is unavoidable, remove the key, mutate, then re-insert: treeSet.remove(key); key.mutate(); treeSet.add(key);
Fix now
Revert the mutation and create a new entry instead.
TreeMap vs HashMap — Feature Comparison
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

1
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.
2
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.
3
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.
4
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

3 patterns
×

Storing objects in TreeSet without Comparable or a Comparator

Symptom
You add a custom class like Employee to a TreeSet. On the second insertion you get a runtime ClassCastException: 'Employee cannot be cast to Comparable'. The first insert works because the tree is empty and no comparison is needed.
Fix
Implement Comparable<Employee> in your class, or supply a Comparator via the TreeSet constructor. The error appears at the second add() because that's when TreeSet first needs to compare two elements.
×

Comparator.compare(a,b) == 0 causes silent data loss

Symptom
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.
×

Mutating a key's fields after inserting it into a TreeMap or TreeSet

Symptom
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.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does TreeMap maintain its balance, and what is the time complexity o...
Q02SENIOR
Why does TreeMap throw NullPointerException for null keys while HashMap ...
Q03SENIOR
Explain the 'consistency with equals' requirement for Comparators used i...
Q04SENIOR
How does ConcurrentSkipListMap serve as a thread-safe alternative to Tre...
Q05JUNIOR
What happens if you call floorKey() and there is no key less than or equ...
Q01 of 05SENIOR

How does TreeMap maintain its balance, and what is the time complexity of a re-balancing operation?

ANSWER
TreeMap uses a Red-Black Tree, a self-balancing binary search tree. After every insert or delete, it performs at most O(log n) recoloring and rotation operations (at most two rotations for insertion, three for deletion). Each rotation is O(1), so the overall worst-case complexity for mutation remains O(log n). The tree guarantees O(log n) height, preventing worst-case O(n) degradation seen in plain BSTs.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Does TreeMap allow null keys in Java?
02
Is TreeMap thread-safe?
03
What is the difference between floorKey() and lowerKey() in TreeMap?
04
Can I store duplicate keys in a TreeMap?
05
What is the time complexity of constructing a TreeMap from an existing collection?
🔥

That's Collections. Mark it forged?

10 min read · try the examples if you haven't

Previous
HashSet in Java
6 / 21 · Collections
Next
Stack and Queue in Java