📦 Section 4 · Question #13

What is TreeSet and how does it work

Unlike HashSet, where element iteration is chaotic and dictated by hash bucket distribution, TreeSet elements are ordered according to their Natural Ordering (classes implementi...


🟢 Junior Level

1. Purpose and Foundational Properties

TreeSet is a core implementation of the NavigableSet interface (which extends SortedSet and Set) in the Java Collections Framework. It guarantees that all stored elements are unique and automatically maintained in strictly sorted order.

Unlike HashSet, where element iteration is chaotic and dictated by hash bucket distribution, TreeSet elements are ordered according to their Natural Ordering (classes implementing Comparable<T>) or via a custom Comparator<T> provided at collection construction time:

// Natural Ordering (Ascending for Integer):
Set<Integer> numbers = new TreeSet<>();
numbers.add(42);
numbers.add(10);
numbers.add(5);
numbers.add(99);
numbers.add(10); // Duplicate silently ignored

System.out.println(numbers); // Always outputs: [5, 10, 42, 99]

2. Key Characteristics of TreeSet

  1. Automatic Continuous Sorting: Elements occupy their exact sorted position immediately upon insertion.
  2. Internal Implementation: Implemented as a lightweight adapter wrapper around a self-balancing Red-Black Tree (TreeMap).
  3. null Element Prohibition: Inserting null into a default TreeSet immediately throws NullPointerException, as null cannot be compared to other elements (null.compareTo(...)).
  4. Time Complexity: Core operations (add, remove, contains) execute in $O(\log N)$ time, which is asymptotically slower than HashSet’s average $O(1)$, but provides guaranteed order and range navigation.

🟡 Middle Level

1. Architectural Foundation: The Red-Black Tree (TreeMap)

TreeSet encapsulates an internal instance of NavigableMap<E, Object> m (specifically, an instance of TreeMap):

public class TreeSet<E> extends AbstractSet<E> implements NavigableSet<E>, Cloneable, java.io.Serializable {
    private transient NavigableMap<E, Object> m;
    private static final Object PRESENT = new Object();

    public TreeSet() {
        this(new TreeMap<>());
    }

    public boolean add(E e) {
        return m.put(e, PRESENT) == null;
    }
}

Underneath TreeMap lies a Red-Black Tree — a self-balancing binary search tree governed by five strict mathematical invariants:

  1. Every node is colored either Red or Black.
  2. The root node is always Black.
  3. All leaf nodes (NIL / null references) are treated as Black.
  4. If a node is Red, both of its children must be Black (two Red nodes cannot appear consecutively on any path).
  5. Every path from a given node to any of its descendant NIL leaves contains the exact same number of Black nodes (Black-Height invariant).
                 15 (B)           <- Root is always BLACK
                /      \
             9 (R)     23 (R)     <- Red nodes can have only Black children
            /     \        \
          3 (B)  11 (B)    30 (B) <- Balanced Black-Height across all paths

These invariants ensure that the longest path from the root to any leaf is never more than twice as long as the shortest path. Consequently, the tree height $h$ is strictly bounded by $h \le 2 \log_2(N + 1)$, guaranteeing $O(\log N)$ worst-case time complexity for lookup, insertion, and deletion. If an insertion or deletion violates these invariants, the tree restores balance via node recoloring and tree rotations (left and right rotations).


2. The Equality Paradox: compareTo() vs. equals()

[!WARNING] TreeSet completely disregards hashCode() and equals()! Element identity and uniqueness are governed exclusively by Comparable.compareTo() or Comparator.compare(). Two elements $a$ and $b$ are deemed identical if and only if: \(\text{compare}(a, b) == 0\)

This represents a classic departure from the standard java.util.Set interface contract, which specifies uniqueness via equals():

// Classic BigDecimal precision disparity:
Set<BigDecimal> hashSet = new HashSet<>();
Set<BigDecimal> treeSet = new TreeSet<>();

BigDecimal d1 = new BigDecimal("1.0");
BigDecimal d2 = new BigDecimal("1.00");

// equals() compares numeric value AND precision (scale):
System.out.println(d1.equals(d2));       // false (scale 1 != scale 2)

// compareTo() compares strictly numerical value:
System.out.println(d1.compareTo(d2));    // 0 (1.0 == 1.00)

hashSet.add(d1);
hashSet.add(d2);
System.out.println(hashSet.size());      // 2 (Both elements retained)

treeSet.add(d1);
treeSet.add(d2);
System.out.println(treeSet.size());      // 1 (d2 rejected as duplicate!)

If an entity’s compareTo() implementation is not consistent with equals() ((a.compareTo(b) == 0) != a.equals(b)), TreeSet will drop valid elements, introducing subtle, hard-to-detect data corruption bugs.


3. Navigational Capabilities (NavigableSet)

TreeSet provides rich closest-match and directional range queries:

NavigableSet<Integer> set = new TreeSet<>(List.of(10, 20, 30, 40, 50));

// Directional boundary lookup:
System.out.println(set.lower(30));    // 20 (Strictly greatest element < 30)
System.out.println(set.floor(30));    // 30 (Greatest element <= 30)
System.out.println(set.ceiling(35));  // 40 (Smallest element >= 35)
System.out.println(set.higher(40));   // 50 (Strictly smallest element > 40)

// Priority Queue / Double-Ended Queue operations:
System.out.println(set.pollFirst()); // 10 (Retrieves and removes lowest)
System.out.println(set.pollLast());  // 50 (Retrieves and removes highest)

🔴 Senior Level

1. Sub-Range Live Views & Range Traps

Methods such as subSet(), headSet(), and tailSet() do not copy data into a new collection! They return an $O(1)$ lightweight backed view (AscendingSubMap or DescendingSubMap) mapped directly over the backing Red-Black Tree:

TreeSet<Integer> set = new TreeSet<>();
for (int i = 1; i <= 10; i++) set.add(i);

NavigableSet<Integer> view = set.subSet(3, true, 7, true);
System.out.println(view); // [3, 4, 5, 6, 7]

// 1. Mutating the view mutates the backing TreeSet:
view.remove(5);
System.out.println(set.contains(5)); // false!

// 2. Modifying the original TreeSet immediately updates the view:
set.add(4);

// 3. Attempting to insert an element outside the bounded view window:
view.add(100); // ❌ Throws java.lang.IllegalArgumentException: key out of range

Because the view retains a hard reference to the root TreeSet, keeping a sub-range view in memory prevents the garbage collector from reclaiming the entire backing tree structure.


2. Memory Footprint Breakdown & Cache Line Misses

The internal node class TreeMap.Entry is among the heaviest node structures in the Java runtime:

static final class Entry<K,V> implements Map.Entry<K,V> {
    K key;
    V value;
    Entry<K,V> left;
    Entry<K,V> right;
    Entry<K,V> parent;
    boolean color = BLACK;
}

On a 64-bit JVM with Compressed OOPs (-XX:+UseCompressedOops):

  • Object Header: 12 bytes (8-byte Mark Word + 4-byte compressed Klass pointer).
  • Object References: 5 references (key, value, left, right, parent) $\times 4\text{ bytes} = 20\text{ bytes}$.
  • Primitive color: 1 byte (boolean).
  • 8-byte Alignment Padding: 7 bytes.
  • Total Size per Node: 40 bytes (vs. 32 bytes for HashMap.Node).
  • Without Compressed OOPs ($> 32\text{ GB}$ heap), each node balloons to 64 bytes!

Hardware Pipeline Impact: The Cache Miss Penalty

Nodes in a Red-Black Tree are allocated incrementally and scattered haphazardly across the heap. During a tree traversal, following node.left or node.right pointers almost invariably causes a CPU L1/L2 Cache Miss, forcing the core to stall for 100–300 clock cycles while fetching the node from main memory (RAM). Consequently, iterating through 1,000,000 elements in a TreeSet is $10\times\text{ to }50\times$ slower than iterating across a contiguous ArrayList.


3. Java 21 SequencedSet Integration (JEP 431)

In Java 21, TreeSet implements SequencedSet, providing standardized ordered accessors:

TreeSet<String> set = new TreeSet<>(List.of("alpha", "beta", "gamma"));

System.out.println(set.getFirst()); // "alpha" (Lowest sorted element)
System.out.println(set.getLast());  // "gamma" (Highest sorted element)

// Constant-time zero-copy reverse view:
SequencedSet<String> reversed = set.reversed();
System.out.println(reversed.getFirst()); // "gamma"

// Positional Mutators are FORBIDDEN:
set.addFirst("omega"); // ❌ Throws UnsupportedOperationException!
set.addLast("omega");  // ❌ Throws UnsupportedOperationException!

Why addFirst() / addLast() throw exceptions: In a TreeSet, element positions are strictly governed by their comparator/sort keys. Granting caller-defined explicit head/tail insertion would violate the sorted invariants of the binary search tree.


4 Tricky Questions

1. What happens if you insert new BigDecimal("2.0") and new BigDecimal("2.00") into HashSet vs. TreeSet?

Answer:

  • HashSet retains 2 elements: HashSet uses equals(). For BigDecimal, equals() compares both numerical value and scale. Because "2.0" has scale 1 and "2.00" has scale 2, equals() returns false, causing HashSet to store both.
  • TreeSet retains 1 element: TreeSet uses compareTo(). For BigDecimal, compareTo() compares only numerical magnitude ($2.0 == 2.00$), returning 0. TreeSet considers the second element a duplicate and discards it.

2. Can you ever store null in a TreeSet?

Answer: In standard operation, attempting to insert null throws NullPointerException because TreeSet dispatches null.compareTo(existing) or compares against existing elements. However, null can be stored if the TreeSet is constructed with an explicit null-aware comparator, such as:

Set<String> set = new TreeSet<>(Comparator.nullsFirst(Comparator.naturalOrder()));
set.add(null);   // ✅ Successfully stored as the lowest element!
set.add("Alfa"); // ✅ Stored after null

Because the comparator evaluates null == null as 0, only a single null can ever be added; subsequent null insertions are ignored as duplicates.


3. How does subSet() work internally, and does it create a separate copy of the data?

Answer: subSet() creates zero copies of the tree data ($O(1)$ memory and time). It returns an instance of an internal package-private class (TreeMap.AscendingSubMap or DescendingSubMap). This view holds a reference to the root TreeMap along with the low and high boundary keys. When operations like contains(), add(), or iterator traversal are performed, the sub-map verifies that the target key satisfies the boundary conditions (fromKey <= key < toKey) and delegates to the root tree. If code attempts to insert an element outside the specified range (view.add(outOfBoundsKey)), the view throws IllegalArgumentException: key out of range.


4. Why do addFirst() and addLast() throw UnsupportedOperationException in TreeSet under Java 21 SequencedSet?

Answer: Java 21 introduced the SequencedSet interface, which defines addFirst(E) and addLast(E) for collections with defined encounter orders. However, in sorted sets like TreeSet, element positioning is strictly deterministic: an element’s position is dictated solely by its comparison key relative to other nodes. Allowing a developer to arbitrarily insert an element at the beginning or end would corrupt the Red-Black Tree’s ordering invariant. Therefore, TreeSet explicitly overrides these methods to throw UnsupportedOperationException.


🎯 Interview Cheat Sheet

30-Second Elevator Pitch

“TreeSet is a navigable sorted set backed by a self-balancing Red-Black Tree (TreeMap). It guarantees $O(\log N)$ time complexity for insertions, deletions, and lookups while preventing duplicates. Crucially, TreeSet evaluates element uniqueness and ordering exclusively via Comparable.compareTo() or an explicit Comparator, ignoring equals() and hashCode(). By default, it rejects null (NullPointerException). It offers powerful $O(1)$ live window views via subSet(), floor(), and ceiling(). In Java 21, it implements SequencedSet, though explicit positional additions (addFirst/addLast) throw UnsupportedOperationException.”

High-Impact Architectural Facts

  • Memory Footprint: Each TreeSet element incurs 40 bytes of node overhead (TreeMap.Entry) on 64-bit JVMs with Compressed OOPs (64 bytes without), making it the heaviest standard collection node in Java.
  • Cache Locality Penalty: Pointer-chasing across the heap causes frequent CPU L1/L2 cache misses, making sequential iteration $10-50\times$ slower than array-backed collections.
  • Red-Black Tree Balance Invariant: The Black-Height property guarantees maximum tree height $h \le 2 \log_2(N + 1)$, preventing performance degradation to $O(N)$.
  • Live Range Views: Subsets returned by subSet() are zero-copy backed views; adding out-of-range elements throws IllegalArgumentException.

Common Pitfalls & Red Flags

  • ❌ Inconsistent compareTo() and equals(): If compareTo() returns 0 when equals() returns false, TreeSet silently drops distinct objects.
  • ❌ Mutating fields that determine sorting order: Modifying a sorted field while the object is in a TreeSet breaks tree search paths; the element becomes unfindable and cannot be removed.
  • ❌ Using TreeSet merely for insertion-order tracking: Demonstrates ignorance of LinkedHashSet, which preserves insertion order in $O(1)$ time without $O(\log N)$ tree balancing overhead.
  • ❌ Assuming TreeSet is thread-safe: It is completely unsynchronized; concurrent access requires Collections.synchronizedNavigableSet() or ConcurrentSkipListSet.