Java Tutorial
🔍

Java TreeSet

Java TreeSet

java.util.TreeSet<E> is the Set implementation you reach for when sorted order matters. Every element you add is automatically placed in its correct sorted position — first() always returns the smallest, last() always returns the largest, and you can query ranges like "all elements between 100 and 500" in a single method call. The price is O(log n) per operation instead of O(1), but for sorted unique data with range navigation, nothing else in the standard library comes close.

What Is Java TreeSet?

TreeSet<E> is a concrete class in java.util that implements NavigableSet<E>. It stores elements in a self-balancing Red-Black tree, which means elements are always maintained in sorted ascending order. It rejects duplicates, requires elements to be comparable, and provides powerful range navigation methods unavailable on HashSet or LinkedHashSet.

The diagram below shows where TreeSet sits in the full Set hierarchy.

java.lang.Iterable<E>
    └── java.util.Collection<E>
            └── java.util.Set<E>                   ← uniqueness contract
                    ├── HashSet<E>                  ← O(1), no order
                    │       └── LinkedHashSet<E>    ← O(1), insertion order
                    └── java.util.SortedSet<E>      ← adds first(), last(), headSet(), tailSet()
                            └── java.util.NavigableSet<E>  ← adds floor(), ceiling(), lower(), higher()
                                    └── java.util.TreeSet<E>  ← Red-Black tree, sorted order

KEY FACTS:
  Package    : java.util
  Since      : Java 1.2
  Backed by  : TreeMap<E, Object> internally
  Ordering   : natural (Comparable) or supplied Comparator — always ascending
  Null       : NOT allowed — compareTo(null) throws NullPointerException
  Duplicates : rejected — two elements where compare returns 0 are the same
  Thread     : NOT thread-safe
  Operations : O(log n) for add, remove, contains — guaranteed by Red-Black tree

The Three Set Implementations at a Glance

FEATURE COMPARISON:

  Feature                HashSet          LinkedHashSet       TreeSet
  ---------------------- ---------------- ------------------- --------------------
  Ordering               None             Insertion order     Sorted (asc)
  add/remove/contains    O(1) average     O(1) average        O(log n) guaranteed
  Null elements          Allowed (1)      Allowed (1)         NOT allowed
  Backed by              HashMap          LinkedHashMap        TreeMap (Red-Black)
  first() / last()       No               No                  O(log n)
  floor() / ceiling()    No               No                  O(log n)
  headSet() / tailSet()  No               No                  O(1) view creation
  Comparator support     No               No                  Yes
  Best for               Fast lookup      Ordered display      Sorted + range ops

When to Use TreeSet

TreeSet is the right choice when sorted unique elements and range-based access are both required. The cost is O(log n) per operation — always, not just on average — because the Red-Black tree must be traversed and rebalanced.

USE TreeSet WHEN:
  1. Elements must be in sorted order for iteration
     — leaderboard scores, alphabetically sorted tags, sorted event timestamps
  2. Range queries are needed:
     — "all scores between 80 and 100": subSet(80, true, 100, true)
     — "top 3 scores": descendingSet() + iteration
     — "all cities alphabetically before Pune": headSet("Pune")
  3. Nearest-element navigation is needed:
     — floor(x)   → largest element <= x
     — ceiling(x) → smallest element >= x
     — lower(x)   → largest element strictly < x
     — higher(x)  → smallest element strictly > x
  4. A sorted view of a dataset needs to be maintained as elements are added

CHOOSE HashSet INSTEAD WHEN:
  - Order is irrelevant and O(1) speed is the priority
  - O(log n) vs O(1) matters at scale (millions of elements, millions of lookups)

CHOOSE LinkedHashSet INSTEAD WHEN:
  - Insertion order (not sorted order) is what matters
  - O(1) operations are required alongside ordered iteration

CHOOSE TreeMap INSTEAD WHEN:
  - Sorted order is needed AND each element has an associated value to look up
  - Range-query results need both key and value, not just the key

NEVER USE TreeSet WHEN:
  - Elements do not implement Comparable AND no Comparator is provided
    → ClassCastException on the second add() call
  - Null elements are needed
    → NullPointerException on any add(null) call

How TreeSet Works Internally

The Red-Black Tree Structure

TreeSet is a thin wrapper around TreeMap<E, Object>, exactly as HashSet wraps HashMap. Every element becomes a key in the backing TreeMap, with a shared static PRESENT dummy object as the value.

TreeSet<Integer> set = new TreeSet<>();
set.add(50); set.add(20); set.add(80); set.add(10); set.add(60);

INTERNALLY → TreeMap<Integer, Object>:

  Red-Black tree after all 5 insertions:

              50 (Black)
            /             \
        20 (Red)         80 (Red)
        /
    10 (Black)   60 (Black — actually between 20 and 80 in BST, shown simplified)

  FULL TREE (actual BST structure):
              50 (B)
            /        \
        20 (R)       80 (R)
       /    \        /
    10 (B) 30(B)  60 (B)

  In-order traversal: 10, 20, 30, 50, 60, 80  ← always sorted ascending
  Height bounded at:  2 × log₂(n+1)            ← Red-Black invariant

  FIVE RED-BLACK INVARIANTS:
  1. Every node is Red or Black
  2. Root is always Black
  3. No two consecutive Red nodes (Red node's parent must be Black)
  4. Every path from a node to its descendant null leaves has the same Black-node count
  5. Null leaves are considered Black

  These invariants guarantee the tree height stays O(log n), preventing degeneration
  to O(n) that would happen with an unbalanced BST on sorted input.

How Comparisons Drive Everything

TreeSet uses compareTo() (natural ordering) or a Comparator (custom ordering) for ALL operations — add(), remove(), contains(), floor(), ceiling(). It never calls equals() or hashCode(). Two elements where compare(a, b) == 0 are considered identical and the duplicate is rejected.

TREESET COMPARISON LOGIC for add(element):

  1. Start at root
  2. compare(element, currentNode.key):
       < 0  → go left
       > 0  → go right
       == 0 → DUPLICATE — reject add(), return false
  3. Repeat from step 2 at the new node
  4. If null is reached → insert new node here
  5. Rebalance to maintain Red-Black invariants — at most O(log n) rotations

  KEY INSIGHT:
  compare() == 0 means "same element" in TreeSet.
  This is independent of equals(). If compare returns 0 but equals returns false,
  TreeSet treats them as duplicates — equals() is never consulted.

  This is why TreeSet's consistency-with-equals contract matters:
  Ideally: (a.compareTo(b) == 0) == (a.equals(b))
  BigDecimal violates this intentionally: 2.0.compareTo(2.00) == 0 but equals returns false
  In a TreeSet<BigDecimal>, 2.0 and 2.00 are treated as the same element.

Core Operations with Examples

Basic add(), contains(), remove() and Size

1// File: TreeSetBasicsDemo.java 2 3import java.util.TreeSet; 4 5public class TreeSetBasicsDemo { 6 7 public static void main(String[] args) { 8 9 TreeSet<Integer> examScores = new TreeSet<>(); 10 11 // add() returns true for new elements, false for duplicates 12 System.out.println("=== add() ==="); 13 System.out.println("add(85) : " + examScores.add(85)); // true 14 System.out.println("add(92) : " + examScores.add(92)); // true 15 System.out.println("add(78) : " + examScores.add(78)); // true 16 System.out.println("add(95) : " + examScores.add(95)); // true 17 System.out.println("add(85) : " + examScores.add(85)); // false — duplicate 18 System.out.println("add(63) : " + examScores.add(63)); // true 19 System.out.println("add(100): " + examScores.add(100)); // true 20 System.out.println("TreeSet : " + examScores); // always sorted! 21 System.out.println("Size : " + examScores.size()); // 6, not 7 22 23 System.out.println(); 24 25 // contains() — O(log n), never scans all elements 26 System.out.println("=== contains() ==="); 27 System.out.println("contains(85) : " + examScores.contains(85)); // true 28 System.out.println("contains(50) : " + examScores.contains(50)); // false 29 30 // remove() — O(log n), rebalances tree after removal 31 System.out.println("\n=== remove() ==="); 32 System.out.println("remove(78) : " + examScores.remove(78)); // true 33 System.out.println("remove(50) : " + examScores.remove(50)); // false 34 System.out.println("After : " + examScores); 35 36 // first() and last() — always O(log n) — leftmost and rightmost nodes 37 System.out.println("\n=== first() and last() ==="); 38 System.out.println("first() : " + examScores.first()); // 63 — minimum 39 System.out.println("last() : " + examScores.last()); // 100 — maximum 40 } 41}
Output:
=== add() ===
add(85) : true
add(92) : true
add(78) : true
add(95) : true
add(85) : false — duplicate
add(63) : true
add(100): true
TreeSet : [63, 78, 85, 92, 95, 100]
Size    : 6

=== contains() ===
contains(85)  : true
contains(50)  : false

=== remove() ===
remove(78) : true
remove(50) : false
After      : [63, 85, 92, 95, 100]

=== first() and last() ===
first() : 63
last()  : 100

NavigableSet Methods — Floor, Ceiling, Lower, Higher

These four methods make TreeSet uniquely powerful for range-based lookups. None of them are available on HashSet or LinkedHashSet.

1// File: TreeSetNavigationDemo.java 2 3import java.util.TreeSet; 4 5public class TreeSetNavigationDemo { 6 7 public static void main(String[] args) { 8 9 TreeSet<Integer> prices = new TreeSet<>(); 10 for (int p : new int[]{199, 499, 799, 1199, 1999, 2999, 4999, 7999}) { 11 prices.add(p); 12 } 13 System.out.println("Price tiers: " + prices); 14 System.out.println(); 15 16 int budget = 1500; 17 18 // floor(e) — largest element <= e 19 System.out.printf("floor(%d) : %d (most expensive within budget)%n", 20 budget, prices.floor(budget)); 21 22 // ceiling(e) — smallest element >= e 23 System.out.printf("ceiling(%d) : %d (cheapest at or above budget)%n", 24 budget, prices.ceiling(budget)); 25 26 // lower(e) — largest element strictly < e 27 System.out.printf("lower(%d) : %d (strictly below budget)%n", 28 budget, prices.lower(budget)); 29 30 // higher(e) — smallest element strictly > e 31 System.out.printf("higher(%d) : %d (just above budget)%n", 32 budget, prices.higher(budget)); 33 34 System.out.println(); 35 36 // Edge cases 37 System.out.println("=== Edge cases ==="); 38 System.out.println("floor(199) : " + prices.floor(199)); // 199 — exact match 39 System.out.println("floor(100) : " + prices.floor(100)); // null — nothing below 40 System.out.println("ceiling(9999) : " + prices.ceiling(9999)); // null — nothing above 41 System.out.println("lower(199) : " + prices.lower(199)); // null — nothing strictly less 42 System.out.println("higher(7999) : " + prices.higher(7999)); // null — nothing above max 43 44 System.out.println(); 45 46 // pollFirst() and pollLast() — remove and return 47 System.out.println("=== pollFirst() and pollLast() ==="); 48 System.out.println("pollFirst() : " + prices.pollFirst()); // removes and returns 199 49 System.out.println("pollLast() : " + prices.pollLast()); // removes and returns 7999 50 System.out.println("After polls : " + prices); 51 } 52}
Output:
Price tiers: [199, 499, 799, 1199, 1999, 2999, 4999, 7999]

floor(1500)   : 1199  (most expensive within budget)
ceiling(1500) : 1999  (cheapest at or above budget)
lower(1500)   : 1199  (strictly below budget)
higher(1500)  : 1999  (just above budget)

=== Edge cases ===
floor(199)    : 199
floor(100)    : null
ceiling(9999) : null
lower(199)    : null
higher(7999)  : null

=== pollFirst() and pollLast() ===
pollFirst()   : 199
pollLast()    : 7999
After polls   : [499, 799, 1199, 1999, 2999, 4999]

Range Views — headSet, tailSet, subSet

Range views return live, navigable sub-views of the TreeSet. Modifications through the view affect the backing set, and changes to the backing set are visible through the view.

1// File: TreeSetRangeViewDemo.java 2 3import java.util.TreeSet; 4 5public class TreeSetRangeViewDemo { 6 7 public static void main(String[] args) { 8 9 TreeSet<Integer> scores = new TreeSet<>(); 10 for (int s : new int[]{45, 55, 62, 70, 78, 84, 88, 91, 95, 100}) { 11 scores.add(s); 12 } 13 System.out.println("All scores: " + scores); 14 System.out.println(); 15 16 // headSet(toElement) — elements STRICTLY LESS than toElement 17 System.out.println("=== headSet() — exclusive by default ==="); 18 System.out.println("headSet(70) : " + scores.headSet(70)); // < 70 19 System.out.println("headSet(70, true) : " + scores.headSet(70, true)); // <= 70 (inclusive) 20 21 // tailSet(fromElement) — elements >= fromElement (inclusive by default) 22 System.out.println("\n=== tailSet() — inclusive by default ==="); 23 System.out.println("tailSet(84) : " + scores.tailSet(84)); // >= 84 24 System.out.println("tailSet(84, false) : " + scores.tailSet(84, false)); // > 84 (exclusive) 25 26 // subSet(from, to) — from inclusive, to exclusive by default 27 System.out.println("\n=== subSet() ==="); 28 System.out.println("subSet(70, 90) : " + scores.subSet(70, 90)); // [70, 90) 29 System.out.println("subSet(70, true, 90, true): " + scores.subSet(70, true, 90, true)); // [70, 90] 30 31 // Range views are LIVE — modification propagates back to original set 32 System.out.println("\n=== Live view — removal propagates ==="); 33 java.util.NavigableSet<Integer> failRange = scores.headSet(60, true); 34 System.out.println("Failing scores (<=60): " + failRange); 35 failRange.clear(); // removes from the BACKING TreeSet 36 System.out.println("After clearing fail range, all scores: " + scores); 37 38 // descendingSet() — reverse-order view 39 System.out.println("\n=== descendingSet() — highest first ==="); 40 System.out.println("Descending: " + scores.descendingSet()); 41 } 42}
Output:
All scores: [45, 55, 62, 70, 78, 84, 88, 91, 95, 100]

=== headSet() — exclusive by default ===
headSet(70)           : [45, 55, 62]
headSet(70, true)     : [45, 55, 62, 70]

=== tailSet() — inclusive by default ===
tailSet(84)           : [84, 88, 91, 95, 100]
tailSet(84, false)    : [88, 91, 95, 100]

=== subSet() ===
subSet(70, 90)           : [70, 78, 84, 88]
subSet(70, true, 90, true): [70, 78, 84, 88, 90]

=== Live view — removal propagates ===
Failing scores (<=60): [45, 55]
After clearing fail range, all scores: [62, 70, 78, 84, 88, 91, 95, 100]

=== Descending: [100, 95, 91, 88, 84, 78, 70, 62]

Custom Ordering with Comparator

When elements do not implement Comparable, or when a non-natural ordering is needed, pass a Comparator to the TreeSet constructor.

1// File: TreeSetComparatorDemo.java 2 3import java.util.Comparator; 4import java.util.Objects; 5import java.util.TreeSet; 6 7public class TreeSetComparatorDemo { 8 9 record Product(String sku, String name, double price, int stock) {} 10 11 public static void main(String[] args) { 12 13 // Sort by price ascending, then by name for ties 14 Comparator<Product> byPriceThenName = 15 Comparator.comparingDouble(Product::price) 16 .thenComparing(Product::name); 17 18 TreeSet<Product> catalogue = new TreeSet<>(byPriceThenName); 19 catalogue.add(new Product("P001", "Laptop Stand", 599.0, 45)); 20 catalogue.add(new Product("P002", "Keyboard Cover", 299.0, 80)); 21 catalogue.add(new Product("P003", "USB-C Hub", 1299.0, 32)); 22 catalogue.add(new Product("P004", "Mouse Pad", 199.0, 100)); 23 catalogue.add(new Product("P005", "Monitor Riser", 799.0, 20)); 24 25 // Sorted by price ascending 26 System.out.println("=== Sorted by price ==="); 27 catalogue.forEach(p -> 28 System.out.printf(" %-20s Rs.%7.2f (stock: %d)%n", 29 p.name(), p.price(), p.stock())); 30 31 // floor, ceiling work with the Comparator 32 Product probe = new Product("", "", 600.0, 0); // probe for price 600 33 System.out.println("\nfloor (<=600 by price) : " 34 + Objects.requireNonNullElse(catalogue.floor(probe), "none")); 35 System.out.println("ceiling (>=600 by price): " 36 + Objects.requireNonNullElse(catalogue.ceiling(probe), "none")); 37 38 // Reverse order — most expensive first 39 System.out.println("\n=== Descending (most expensive first) ==="); 40 catalogue.descendingSet().forEach(p -> 41 System.out.printf(" %-20s Rs.%7.2f%n", p.name(), p.price())); 42 } 43}
Output:
=== Sorted by price ===
  Mouse Pad            Rs.   199.00  (stock: 100)
  Keyboard Cover       Rs.   299.00  (stock: 80)
  Laptop Stand         Rs.   599.00  (stock: 45)
  Monitor Riser        Rs.   799.00  (stock: 20)
  USB-C Hub            Rs.  1299.00  (stock: 32)

floor (<=600 by price) : Product[sku=P001, name=Laptop Stand, price=599.0, stock=45]
ceiling (>=600 by price): Product[sku=P005, name=Monitor Riser, price=799.0, stock=20]

=== Descending (most expensive first) ===
  USB-C Hub            Rs.  1299.00
  Monitor Riser        Rs.   799.00
  Laptop Stand         Rs.   599.00
  Keyboard Cover       Rs.   299.00
  Mouse Pad            Rs.   199.00

Real-World Example — PhonePe Transaction History with Range Queries

A payments platform like PhonePe stores transaction amounts for a user and needs to answer range-based analytics queries in real time — total spend in a price band, finding the nearest transaction to a given amount, and filtering transactions above a threshold. TreeSet handles all of these in a single data structure without any sorting step.

1// File: TransactionRecord.java 2 3import java.util.Objects; 4 5public class TransactionRecord implements Comparable<TransactionRecord> { 6 7 private final String txnId; 8 private final double amount; 9 private final String category; 10 private final String timestamp; 11 12 public TransactionRecord(String txnId, double amount, 13 String category, String timestamp) { 14 this.txnId = txnId; 15 this.amount = amount; 16 this.category = category; 17 this.timestamp = timestamp; 18 } 19 20 public String getTxnId() { return txnId; } 21 public double getAmount() { return amount; } 22 public String getCategory() { return category; } 23 24 // Primary sort by amount ascending; txnId as tiebreaker to ensure no duplicates are lost 25 @Override 26 public int compareTo(TransactionRecord other) { 27 int cmp = Double.compare(this.amount, other.amount); 28 if (cmp != 0) return cmp; 29 return this.txnId.compareTo(other.txnId); // tiebreaker 30 } 31 32 @Override 33 public boolean equals(Object obj) { 34 if (!(obj instanceof TransactionRecord other)) return false; 35 return Objects.equals(this.txnId, other.txnId); 36 } 37 38 @Override 39 public int hashCode() { return Objects.hash(txnId); } 40 41 @Override 42 public String toString() { 43 return String.format("[%s] Rs.%8.2f %-14s %s", 44 txnId, amount, category, timestamp); 45 } 46}
1// File: TransactionAnalytics.java 2 3import java.util.NavigableSet; 4import java.util.TreeSet; 5 6public class TransactionAnalytics { 7 8 private final TreeSet<TransactionRecord> transactions = new TreeSet<>(); 9 10 public void record(TransactionRecord txn) { 11 transactions.add(txn); 12 } 13 14 // Range query — all transactions between minAmount and maxAmount (inclusive) 15 public NavigableSet<TransactionRecord> inRange(double minAmount, double maxAmount) { 16 TransactionRecord low = new TransactionRecord("", minAmount, "", ""); 17 TransactionRecord high = new TransactionRecord("\uFFFF", maxAmount, "", ""); // high tiebreaker 18 return transactions.subSet(low, true, high, true); 19 } 20 21 // Find the nearest transaction to a given amount 22 public TransactionRecord nearestTo(double targetAmount) { 23 TransactionRecord probe = new TransactionRecord("", targetAmount, "", ""); 24 TransactionRecord below = transactions.floor(probe); 25 TransactionRecord above = transactions.ceiling(probe); 26 27 if (below == null) return above; 28 if (above == null) return below; 29 30 double diffBelow = Math.abs(below.getAmount() - targetAmount); 31 double diffAbove = Math.abs(above.getAmount() - targetAmount); 32 return diffBelow <= diffAbove ? below : above; 33 } 34 35 public void printReport() { 36 System.out.println("=".repeat(64)); 37 System.out.println(" TRANSACTION HISTORY (sorted by amount)"); 38 System.out.println("=".repeat(64)); 39 transactions.forEach(t -> System.out.println(" " + t)); 40 System.out.printf("%n Total transactions : %d%n", transactions.size()); 41 System.out.printf(" Minimum amount : Rs.%.2f%n", transactions.first().getAmount()); 42 System.out.printf(" Maximum amount : Rs.%.2f%n", transactions.last().getAmount()); 43 System.out.println("=".repeat(64)); 44 } 45 46 public static void main(String[] args) { 47 48 TransactionAnalytics analytics = new TransactionAnalytics(); 49 50 analytics.record(new TransactionRecord("T001", 149.00, "Recharge", "2024-01-05")); 51 analytics.record(new TransactionRecord("T002", 1299.00, "Shopping", "2024-01-07")); 52 analytics.record(new TransactionRecord("T003", 49.00, "Food", "2024-01-08")); 53 analytics.record(new TransactionRecord("T004", 5499.00, "Electronics", "2024-01-10")); 54 analytics.record(new TransactionRecord("T005", 749.00, "Travel", "2024-01-11")); 55 analytics.record(new TransactionRecord("T006", 299.00, "Food", "2024-01-12")); 56 analytics.record(new TransactionRecord("T007", 3999.00, "Shopping", "2024-01-14")); 57 analytics.record(new TransactionRecord("T008", 89.00, "Recharge", "2024-01-15")); 58 59 analytics.printReport(); 60 61 System.out.println("\n=== Range query: Rs.200 — Rs.1500 ==="); 62 analytics.inRange(200, 1500) 63 .forEach(t -> System.out.println(" " + t)); 64 65 System.out.println("\n=== Nearest transaction to Rs.400 ==="); 66 System.out.println(" " + analytics.nearestTo(400)); 67 68 System.out.println("\n=== Transactions above Rs.1000 (tailSet) ==="); 69 TransactionRecord threshold = new TransactionRecord("", 1000.0, "", ""); 70 analytics.transactions.tailSet(threshold) 71 .forEach(t -> System.out.println(" " + t)); 72 73 double highSpendTotal = analytics.transactions.tailSet(threshold) 74 .stream().mapToDouble(TransactionRecord::getAmount).sum(); 75 System.out.printf("%n Total above Rs.1000: Rs.%.2f%n", highSpendTotal); 76 } 77}
Output:
================================================================
  TRANSACTION HISTORY (sorted by amount)
================================================================
  [T003] Rs.    49.00  Food            2024-01-08
  [T008] Rs.    89.00  Recharge        2024-01-15
  [T001] Rs.   149.00  Recharge        2024-01-05
  [T006] Rs.   299.00  Food            2024-01-12
  [T005] Rs.   749.00  Travel          2024-01-11
  [T002] Rs.  1299.00  Shopping        2024-01-07
  [T007] Rs.  3999.00  Shopping        2024-01-14
  [T004] Rs.  5499.00  Electronics     2024-01-10

  Total transactions : 8
  Minimum amount     : Rs.49.00
  Maximum amount     : Rs.5499.00
================================================================

=== Range query: Rs.200 — Rs.1500 ===
  [T006] Rs.   299.00  Food            2024-01-12
  [T005] Rs.   749.00  Travel          2024-01-11
  [T002] Rs.  1299.00  Shopping        2024-01-07

=== Nearest transaction to Rs.400 ===
  [T006] Rs.   299.00  Food            2024-01-12

=== Transactions above Rs.1000 (tailSet) ===
  [T002] Rs.  1299.00  Shopping        2024-01-07
  [T007] Rs.  3999.00  Shopping        2024-01-14
  [T004] Rs.  5499.00  Electronics     2024-01-10

  Total above Rs.1000: Rs.10797.00

Performance Considerations

OperationHashSetLinkedHashSetTreeSet
add(e)O(1) avgO(1) avgO(log n) guaranteed
remove(e)O(1) avgO(1) avgO(log n) guaranteed
contains(e)O(1) avgO(1) avgO(log n) guaranteed
first() / last()N/AN/AO(log n)
floor() / ceiling()N/AN/AO(log n)
headSet() / tailSet() viewN/AN/AO(1) view creation
IterationO(n + capacity)O(n)O(n) — in-order
Memory per element~48 bytes~64 bytes~56 bytes (tree node)

O(log n) is always bounded: Unlike HashMap's O(1) average that degrades to O(n) on collision attacks, TreeSet's O(log n) is guaranteed regardless of input order. The Red-Black rebalancing invariants bound tree height at 2 × log₂(n+1), so a tree of 1,000,000 elements has a maximum height of ~40.

Range view cost: headSet(), tailSet(), and subSet() return live view objects in O(1) — they do not copy elements. Iteration over the view is O(k) where k is the number of elements in the range. This makes range queries very efficient: view creation is instant; only the elements actually accessed are traversed.

Thread safety: TreeSet is not thread-safe. For concurrent sorted set access, use ConcurrentSkipListSet from java.util.concurrent — it provides O(log n) operations with full concurrency guarantees.

Best Practices

Always include a tiebreaker in compareTo() or Comparator to avoid silent duplicate loss. If two distinct objects compare as equal (return 0), TreeSet treats them as the same element and rejects the second. For TransactionRecord sorted by amount, two different transactions with the same amount would be considered duplicates — only one would be stored. Include a secondary comparison field like a unique ID to break ties: Comparator.comparingDouble(T::amount).thenComparing(T::id).

Declare the variable as NavigableSet<E> rather than TreeSet<E> when range methods are needed. NavigableSet<Integer> prices = new TreeSet<>() exposes floor(), ceiling(), headSet(), tailSet(), and subSet() — the full navigation API. Set<Integer> prices = new TreeSet<>() hides them. SortedSet<Integer> exposes headSet()/tailSet() but not floor()/ceiling(). Choose the declaration type based on which methods callers need.

Use Comparator.comparing() chains instead of manual compareTo() logic. Java 8's Comparator.comparing(keyExtractor).thenComparing(secondary) is cleaner, null-safe with Comparator.nullsFirst(), and reversible with reversed(). Manual compareTo() implementations are error-prone — the classic bug is returning a.field - b.field for integer comparison, which overflows for large negative values.

Pre-build a HashSet for fast loading, convert to TreeSet when sorted output is needed. Building a TreeSet from 100,000 elements is O(n log n). Building a HashSet from the same elements is O(n), then new TreeSet<>(hashSet) is O(n log n). The total cost is the same, but if sorted access is only needed once (for a sorted report), the HashSet approach is more memory-efficient during the accumulation phase.

Common Mistakes

Mistake 1 — Missing Tiebreaker Causes Silent Duplicate Loss

1// Product sorted by price only — two products with the same price collide 2Comparator<Product> byPriceOnly = Comparator.comparingDouble(Product::getPrice); 3TreeSet<Product> products = new TreeSet<>(byPriceOnly); 4 5products.add(new Product("P001", "USB Hub", 599.0)); 6products.add(new Product("P002", "Laptop Stand", 599.0)); // same price → compare returns 0 → DUPLICATE 7 8System.out.println(products.size()); // 1 — P002 silently dropped! 9 10// CORRECT — add a tiebreaker to distinguish equal-price products 11Comparator<Product> byPriceThenId = 12 Comparator.comparingDouble(Product::getPrice) 13 .thenComparing(Product::getId); 14TreeSet<Product> safeProducts = new TreeSet<>(byPriceThenId); 15safeProducts.add(new Product("P001", "USB Hub", 599.0)); 16safeProducts.add(new Product("P002", "Laptop Stand", 599.0)); // different ID — stored 17System.out.println(safeProducts.size()); // 2

Mistake 2 — Adding Null Elements

1TreeSet<String> cities = new TreeSet<>(); 2cities.add("Mumbai"); 3cities.add("Delhi"); 4 5// WRONG — TreeSet calls compareTo() on the null element 6cities.add(null); // throws NullPointerException — compareTo(null) throws 7 8// CORRECT — either avoid null, or use a null-tolerant Comparator 9TreeSet<String> withNullSupport = new TreeSet<>( 10 Comparator.nullsFirst(Comparator.naturalOrder()) 11); 12withNullSupport.add(null); // null placed before all non-null elements 13withNullSupport.add("Mumbai"); 14withNullSupport.add("Delhi"); 15System.out.println(withNullSupport); // [null, Delhi, Mumbai]

Mistake 3 — Using compareTo() Returns for Numeric Subtraction

1// WRONG — integer subtraction can overflow for large negative values 2class Score implements Comparable<Score> { 3 int value; 4 5 @Override 6 public int compareTo(Score other) { 7 return this.value - other.value; // overflow: Integer.MIN_VALUE - 1 wraps to positive 8 } 9} 10 11// CORRECT — use Integer.compare() or Double.compare() 12@Override 13public int compareTo(Score other) { 14 return Integer.compare(this.value, other.value); // safe, no overflow 15}

Mistake 4 — Modifying an Element's Comparison Field After Insertion

1// WRONG — mutating the field used in compareTo() after insertion corrupts the tree 2class Task implements Comparable<Task> { 3 int priority; 4 String name; 5 6 @Override 7 public int compareTo(Task other) { 8 return Integer.compare(this.priority, other.priority); 9 } 10} 11 12TreeSet<Task> queue = new TreeSet<>(); 13Task task = new Task(5, "Deploy"); 14queue.add(task); 15 16task.priority = 1; // BST property violated — task is in wrong position in tree 17 18queue.contains(task); // may return false — tree traversal goes wrong way 19queue.remove(task); // may fail — cannot find the element via BST path 20// Always use immutable comparison fields in TreeSet elements

Interview Questions

Q1. What is TreeSet in Java and what makes it different from HashSet and LinkedHashSet?

TreeSet is a NavigableSet implementation backed by a TreeMap (Red-Black self-balancing binary search tree). It stores elements in sorted ascending order — either natural ordering via Comparable or a supplied Comparator. Unlike HashSet (O(1) average, no order) and LinkedHashSet (O(1) average, insertion order), TreeSet guarantees O(log n) for all operations due to the tree structure, and elements are always iterated in sorted order. It also provides navigation methods — floor(), ceiling(), lower(), higher(), headSet(), tailSet(), subSet() — that the hash-based implementations do not offer.

Q2. How does TreeSet determine duplicate elements?

TreeSet uses compareTo() (natural ordering) or the supplied Comparator for all comparisons — it never calls equals() or hashCode(). Two elements where compare(a, b) == 0 are considered identical, and the second is rejected. This means a class can have equals() returning false for two objects while compareTo() returns 0 — TreeSet treats them as duplicates and only stores one. This is why TreeSet's consistency-with-equals contract recommends that (a.compareTo(b) == 0) == a.equals(b), even though it is not enforced.

Q3. What is the time complexity of TreeSet operations and why is it always O(log n)?

All TreeSet operations — add(), remove(), contains(), first(), last(), floor(), ceiling() — 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. Contrast this with HashMap's O(1) average that can degrade to O(n) on hash collisions (mitigated but not eliminated by Java 8 treeification). TreeSet trades the speed advantage of hashing for the guarantee of sorted order and bounded worst-case performance.

Q4. What happens if you add an element to a TreeSet whose class does not implement Comparable?

The first add() succeeds — the tree is empty, no comparison is needed. The second add() throws ClassCastException at runtime because the tree must compare the new element with the existing one using compareTo(), which does not exist on the class. The compiler does not catch this — generics allow TreeSet<MyClass> even without Comparable. The fix is to either implement Comparable on MyClass or pass a Comparator to the TreeSet constructor.

Q5. What is the difference between headSet(), tailSet(), and subSet()?

All three return live view objects — they do not copy elements. headSet(toElement) returns all elements strictly less than toElement (exclusive by default); the overloaded form headSet(toElement, inclusive) controls the boundary. tailSet(fromElement) returns all elements greater than or equal to fromElement (inclusive by default). subSet(from, to) returns elements from from (inclusive by default) to to (exclusive by default); the four-argument overload allows full control over both boundaries. Modifying the view modifies the backing TreeSet, and changes to the backing set are reflected in all views. Adding an element outside a view's range throws IllegalArgumentException.

Q6. Why can TreeSet not contain null elements?

TreeSet determines element position using compareTo() or a Comparator. Comparing any non-null object with null using compareTo() throws NullPointerException — the Java specification requires compareTo() to throw when called with a null argument. Since TreeSet must compare every new element with existing elements to find its position in the tree, null cannot be placed anywhere in the tree. HashSet can store one null because it uses hashCode() (mapped to bucket 0 for null) and skips compareTo() entirely.

FAQs

Does TreeSet allow duplicate elements?

No. TreeSet rejects any element where compareTo() or the Comparator returns 0 when compared to an existing element. add() returns false for such elements. Note that "duplicate" for TreeSet means compare returns 0 — not necessarily that equals() returns true. Two objects can be logically different (by equals()) but considered duplicates by TreeSet if their comparison fields are identical without a tiebreaker.

Can TreeSet work with a custom sort order?

Yes. Pass a Comparator<E> to the constructor: new TreeSet<>(Comparator.comparing(Product::getPrice)). The Comparator overrides any Comparable ordering the class may have. Java 8 Comparator.comparing() chains are the idiomatic approach: Comparator.comparing(Product::getCategory).thenComparing(Product::getPrice).thenComparing(Product::getId) for multi-level sorting with a tiebreaker.

What is the difference between TreeSet and a sorted ArrayList?

A sorted ArrayList is O(n) for contains() and O(n) for remove() by value. TreeSet is O(log n) for both. Inserting in sorted position in an ArrayList is O(n) (binary search finds position in O(log n), but shifting elements is O(n)). TreeSet.add() is O(log n) with no shifting. TreeSet also provides floor(), ceiling(), and range views — none of which ArrayList supports natively. For sets that require frequent membership checks and range queries, TreeSet is significantly more efficient than a maintained sorted ArrayList.

Is TreeSet iteration always in ascending order?

Yes — for-each, iterators, and stream() on a TreeSet always produce elements in ascending sorted order (or the Comparator order if one was provided). Use descendingSet() to get a descending-order view, or descendingIterator() to iterate in reverse without creating a view object.

What is ConcurrentSkipListSet and when should it replace TreeSet?

ConcurrentSkipListSet from java.util.concurrent is a thread-safe sorted set backed by a skip list (a probabilistic data structure that provides O(log n) expected performance). It supports all NavigableSet operations including floor(), ceiling(), and range views, without any external synchronisation. Use it when multiple threads need to read from and write to a sorted set concurrently. TreeSet with Collections.synchronizedSortedSet() exists but requires manual synchronisation during iteration and provides no atomicity guarantees for compound operations.

How does TreeSet compare to using a sorted list for leaderboards?

A leaderboard backed by TreeSet automatically maintains sorted order on every insertion and removal — no sort() call is ever needed. contains() and remove() by value are O(log n). first() and last() for top and bottom scores are O(log n). subSet() for score ranges is O(1) view creation plus O(k) iteration. A sorted ArrayList requires Collections.sort() after each insertion (O(n log n) or O(n) with binary search + insert) and O(n) for removal by value. For a leaderboard that updates frequently, TreeSet is the correct data structure.

Summary

TreeSet<E> is Java's sorted unique-element collection. Backed by a TreeMap Red-Black tree, every element is always in its correct sorted position — O(log n) for every operation, guaranteed. The real power is in the NavigableSet methods: floor(), ceiling(), lower(), higher(), headSet(), tailSet(), and subSet() enable range queries and nearest-element lookups that no other standard Set implementation provides.

Two rules govern TreeSet correctness: elements must implement Comparable or a Comparator must be supplied, and every comparison must include a tiebreaker field when logically distinct elements can have equal primary comparison values. Violating the second rule causes silent duplicate loss — the most confusing TreeSet bug in production code.

For interviews: explain the Red-Black tree backing and the O(log n) guarantee, describe how duplicates are detected via compareTo() not equals(), walk through the NavigableSet API methods, and explain why null elements cause NullPointerException. These questions appear consistently in Java collections interviews at all levels.

What to Read Next