Back to Blog
Java

Getting First and Last Elements from a Java TreeSet

java treeset first last: Learn how to retrieve the first and last elements from a Java TreeSet using first() and last(), handle empty sets, and understand ordering and...

TreeSetJava CollectionsNavigableSetSortedSetJava
Editorial illustration of a balanced binary search tree with the leftmost and rightmost nodes highlighted, representing the first and last elements of a Java TreeSet

The java treeset first last pattern is straightforward: TreeSet exposes first() and last() to return the smallest and largest elements in the set according to its ordering. Both methods are one-liners:

TreeSet<Integer> numbers = new TreeSet<>(Set.of(5, 1, 8, 3)); int smallest = numbers.first(); // 1 int largest = numbers.last(); // 8

The ordering is determined either by the natural ordering of the elements (via Comparable) or by a Comparator supplied at construction time. Whatever ordering rule the set uses, first() returns the element that would come first in iteration order, and last() returns the element that would come last.

What Ordering Rule Applies

Before relying on first() and last(), you need to know which ordering the set is using. If you construct a TreeSet with no arguments, it uses natural ordering:

TreeSet<String> words = new TreeSet<>(); words.add("banana"); words.add("apple"); words.add("cherry"); words.first(); // "apple" words.last(); // "cherry"

Natural ordering for String is lexicographic, so "apple" is first and "cherry" is last. For a custom type, the class must implement Comparable. If you need a different order, supply a Comparator:

TreeSet<String> byLength = new TreeSet<>(Comparator.comparingInt(String::length)); byLength.add("a"); byLength.add("ccc"); byLength.add("bb"); byLength.first(); // "a" byLength.last(); // "ccc"

When a comparator is provided, it completely replaces natural ordering. The set's first() and last() reflect the comparator's ordering, not the natural ordering of the elements.

What Happens When the Set Is Empty

Both first() and last() throw NoSuchElementException when the set is empty. This is a runtime exception, so the compiler will not warn you. A common failure pattern is calling these methods on a set that may legitimately be empty:

TreeSet<Integer> empty = new TreeSet<>(); empty.first(); // throws NoSuchElementException

If an empty set is a valid state in your application, check isEmpty() before calling first() or last(), or use pollFirst() and pollLast() instead, which return null on an empty set.

Null Elements and Ordering Dependencies

A TreeSet with natural ordering does not allow null elements. The internal compareTo call would throw NullPointerException during insertion. With a custom comparator, null handling depends entirely on that comparator: if the comparator can handle null, the set can store it. This affects first() and last() because a null element could appear at either end of the ordering. If you are not certain whether a comparator accepts null, treat the set as null-hostile and validate inputs before insertion.

Performance: first() and last() Are Not O(1)

A common misconception is that first() and last() are constant-time operations like get(0) on an ArrayList. They are not. TreeSet is backed by a red-black tree, and retrieving the minimum or maximum element requires walking from the root down to the leftmost or rightmost node. This is O(log n) in the number of elements. For most applications this cost is negligible, but if you are calling first() or last() inside a hot loop over a very large set, the logarithmic cost adds up.

If you need constant-time access to the smallest or largest element and the set is frequently modified, consider whether a different data structure, such as a priority queue, better matches your access pattern.

Removing the Extremes: pollFirst() and pollLast()

When you need to retrieve and remove the extreme element in one step, pollFirst() and pollLast() are the right tools. Unlike first() and last(), they return null instead of throwing when the set is empty:

TreeSet<Integer> tasks = new TreeSet<>(Set.of(10, 20, 30)); Integer next = tasks.pollFirst(); // 10, removed from the set Integer nextAgain = tasks.pollFirst(); // 20, removed from the set Integer fromEmpty = new TreeSet<Integer>().pollFirst(); // null

This makes pollFirst() and pollLast() convenient for queue-like or stack-like processing where you drain the set one element at a time.

Choosing Between first() and pollFirst()

The choice between first() and pollFirst() depends on whether you need to keep the element in the set. If you are only reading the minimum value, for example to compare it against a threshold, first() is correct. If you are consuming elements in sorted order, pollFirst() avoids a separate remove() call and handles the empty case gracefully with a null return.

The same logic applies to last() and pollLast().

Custom Comparators and Inconsistent Ordering

When you provide a custom comparator, first() and last() reflect that comparator's ordering. If the comparator is inconsistent with equals() — meaning two elements that compare as equal are not equals() to each other — the set can contain both elements. In that case, first() and last() still work, but the set's behavior with respect to contains(), remove(), and iteration may surprise you. Keep comparators consistent with equals() unless you have a specific reason not to.

Thread Safety and Concurrent Access

TreeSet is not thread-safe. If multiple threads read and write the same set, first() and last() can observe a partially modified tree. Wrap the set with Collections.synchronizedSortedSet() for simple synchronization, or use a concurrent navigable set implementation when concurrent modification is frequent. The choice depends on your concurrency requirements; for read-heavy workloads with occasional writes, a synchronized wrapper is often sufficient.

java treeset first last: Practical Usage and Code Examples | RYUSLOG DEV