skip to content

What do headSet, tailSet, and subSet return, and what does it mean that they are 'views'?

level: seniorimportance: should knowfreq 50%

answer

  1. headSet < to ; tailSet >= from ; subSet [from,to)
  2. NavigableSet overloads add inclusive flags
  3. Views share storage — changes flow both ways
  4. Out-of-range add throws IllegalArgumentException
  5. subSet(a,b).clear() = cheap range delete on the original

basics

~20 s

They return a portion of the TreeSet within a range: headSet is everything below a value, tailSet everything from a value up, subSet a range between two. They are live views, so changes to either the view or the original affect both.

solid answer

~40 s

headSet, tailSet, and subSet return range-restricted views of a TreeSet: headSet(to) gives elements less than 'to', tailSet(from) gives elements >= 'from', and subSet(from, to) gives the half-open range [from, to). The NavigableSet overloads add boolean inclusive flags to control each boundary precisely. Crucially these are backed views, not copies — they share storage with the original set, so mutations flow both ways and are O(1) to obtain (plus O(log n) to locate boundaries). A view is range-restricted: adding an element outside its bounds throws IllegalArgumentException, and the view reflects later changes to the backing set. This makes range deletion cheap: subSet(a, b).clear() removes that whole range from the original in one call. Because the SortedSet overloads use the set's ordering, the from/to arguments are compared with the same Comparable/Comparator the set uses.

code

java · 10 lines
java
TreeSet<Integer> s = new TreeSet<>(List.of(10,20,30,40,50));

System.out.println(s.headSet(30));            // [10, 20]      (< 30)
System.out.println(s.tailSet(30));            // [30, 40, 50]  (>= 30)
System.out.println(s.subSet(20, 40));         // [20, 30]      ([20,40))
System.out.println(s.subSet(20, true, 40, true)); // [20, 30, 40]

// Live view: range delete on the original in one call
s.subSet(20, true, 40, true).clear();
System.out.println(s);                        // [10, 50]

go deeper

for a junior

Knows these return a slice of the set by range.

for a middle

States the exact bounds (headSet exclusive, tailSet inclusive, subSet half-open) and the inclusive overloads.

for a senior

Explains the backed-view semantics, bidirectional mutation, IllegalArgumentException on out-of-range writes, and uses subSet().clear() for range deletion.

for a principal

Reasons about views as a zero-copy abstraction, their lifecycle/CME risks under concurrent structural change, and when a defensive copy is warranted over a live view.

## What these methods are for These three methods let you work with a **contiguous slice** of a sorted set defined by value bounds, rather than scanning the whole thing. ## The basic (SortedSet) signatures For a TreeSet `s`: - **`s.headSet(to)`** — all elements **strictly less than** `to`. - **`s.tailSet(from)`** — all elements **greater than or equal to** `from`. - **`s.subSet(from, to)`** — the **half-open** range `[from, to)`: from-inclusive, to-exclusive. On `{10,20,30,40,50}`: `headSet(30)` = `{10,20}`, `tailSet(30)` = `{30,40,50}`, `subSet(20,40)` = `{20,30}`. ## The NavigableSet overloads — explicit inclusivity NavigableSet adds boolean flags so you control each boundary: - `headSet(to, inclusive)` - `tailSet(from, inclusive)` - `subSet(from, fromInclusive, to, toInclusive)` So `subSet(20, true, 40, true)` = `{20,30,40}`. Use these when the default inclusivity isn't what you want. ## The critical concept: a 'view', not a copy A **view** is a lightweight object that **shares the underlying storage** with the original set instead of duplicating the elements. Consequences: 1. **Cheap to create** — no copying; just records the bounds (locating boundaries is O(log n)). 2. **Live / bidirectional** — changes to the backing set appear in the view, and changes through the view modify the backing set. `s.subSet(10,30).clear()` deletes those elements **from `s`**. This makes bulk range deletion a one-liner. 3. **Range-restricted writes** — you may only insert elements that fall within the view's bounds. `headSet(30).add(40)` throws **`IllegalArgumentException`** ('key out of range'). Reads/iteration outside the range simply aren't visible. ## Ordering and the bounds The `from`/`to` arguments are compared using the **same ordering** the set uses (natural `Comparable` or the supplied `Comparator`). The bound values themselves need not be present in the set — `headSet(25)` works even though 25 isn't there. ## Practical patterns - **Range query:** `prices.subSet(min, true, max, true)` — all prices in a band. - **Range delete:** `events.subSet(start, end).clear()` — drop a time window. - **Sliding window / pruning:** `timestamps.headSet(cutoff).clear()` — evict everything older than a cutoff. ## Pitfalls - Holding a view while structurally modifying the backing set through *another* path can throw `ConcurrentModificationException` during the view's iteration. - Forgetting half-open semantics: `subSet(a,b)` excludes `b`. If you need `b`, use the inclusive overload. - Adding out-of-range elements to a view → IllegalArgumentException, not a silent no-op. ## Complexity Obtaining a view is O(log n) (boundary lookup). Iterating it costs O(k) for k elements in range. Range `clear()` is O(k log n) but expressed in one call.

  • How would you delete every element in a TreeSet between 100 and 200 inclusive in one statement?
    Use the NavigableSet overload: set.subSet(100, true, 200, true).clear(); the view shares storage, so clearing it removes those elements from the original set.
  • What happens if you call headSet(30).add(50) on a TreeSet?
    It throws IllegalArgumentException because 50 lies outside the view's range [..,30); a view only accepts inserts within its bounds.

saying these in an interview costs you the question

  • Thinking these return independent copies
  • Assuming subSet's upper bound is inclusive by default (it's exclusive)
  • Believing you can add any element to a view
  • Expecting tailSet to be exclusive of 'from' (it's inclusive)
  • Not realizing range clear() mutates the backing set

context