Back to Blog
Java

Java TreeSet Null Handling: Allow or Reject Nulls

A TreeSet throws NullPointerException when adding null without a comparator that handles it. This article explains why, and how to allow nulls with Comparator.nullsFirst or nullsLast.

TreeSetNullPointerExceptionComparatorJava CollectionsSortingNull Handling
A TreeSet diagram showing null elements being rejected by default and allowed with a custom comparator.

By default, a TreeSet rejects null elements: calling add(null) without a custom comparator throws a NullPointerException. This is not a bug; it follows from how TreeSet maintains sorted order. The set relies on the compareTo method of its elements (or on a provided Comparator) to place each element in a red-black tree. Because null has no natural ordering, the default comparator cannot compare it, so the insertion fails.

Default Behavior: NullPointerException on Insert

The common way to hit this behavior is calling add(null) on a TreeSet without an explicit comparator. Consider this minimal example:

TreeSet<String> set = new TreeSet<>(); set.add("apple"); set.add(null); // throws NullPointerException

The exception comes from TreeMap.put, which TreeSet delegates to. TreeMap calls the comparator or the natural compareTo method both to locate the new key and to type-check it. Adding null to an empty TreeSet fails for the same reason. With natural ordering, comparing null in that call raises a NullPointerException.

Why TreeSet Rejects Null by Default

TreeSet is backed by a TreeMap, which is a red-black tree implementation. The tree's invariants require that every element be comparable to every other element. The default comparator is the natural ordering of the elements, which relies on the Comparable interface. null does not implement Comparable, and there is no sensible natural ordering for it. Therefore, the only way to allow null is to supply a Comparator that explicitly defines how null relates to non-null values.

In the standard Java implementation, TreeMap does not accept null keys, and TreeSet does not accept null elements, without a custom comparator that handles null. If you need to store null, you must take control of the ordering logic.

Allowing Nulls with a Custom Comparator

To permit null elements in a TreeSet, provide a Comparator that handles null. The Comparator interface includes two static factory methods designed for this purpose: nullsFirst and nullsLast. These methods wrap an existing comparator and define a total order that treats null as either less than or greater than every non-null value.

TreeSet<String> set = new TreeSet<>(Comparator.nullsFirst(String::compareTo)); set.add("banana"); set.add(null); set.add("apple"); System.out.println(set); // [null, apple, banana]

In this example, null is considered smaller than any String, so it appears first in iteration order. The comparator is applied consistently for all operations, including contains, remove, and first/last.

If you prefer null to be treated as the largest element, use nullsLast:

TreeSet<String> set = new TreeSet<>(Comparator.nullsLast(String::compareTo)); set.add("banana"); set.add(null); set.add("apple"); System.out.println(set); // [apple, banana, null]

Both wrappers handle null before delegating to the supplied comparator. They work with any existing Comparator, not just natural ordering.

Ordering Nulls with nullsFirst and nullsLast

The choice between nullsFirst and nullsLast affects not only iteration order but also the behavior of boundary operations. For example, first() returns the smallest element according to the comparator. With nullsFirst, first() returns null if the set contains a null. With nullsLast, last() returns null if present.

MethodnullsFirstnullsLast
first()Returns null if presentReturns smallest non-null
last()Returns largest non-nullReturns null if present
add(null)Allowed, placed at beginningAllowed, placed at end

This distinction is important when you rely on first() or last() to obtain a meaningful boundary value. If your application treats null as a sentinel that should not be returned as a real element, you may need to guard against it explicitly.

Operational Considerations When Nulls Are Allowed

Allowing null in a TreeSet introduces a few operational concerns. First, the comparator should be consistent with equals. If the comparator returns 0 for two non-null elements that are not equal, the set will treat them as duplicates and discard one. The nullsFirst and nullsLast wrappers preserve the underlying comparator's consistency, so this is only a risk if you write a custom comparator from scratch.

Second, methods like subSet, headSet, and tailSet require bounds that the comparator can handle. If you allow null, you must ensure that the bounds are non-null or that your comparator handles null in the bound as well. For example, set.headSet(null) compares null with the elements. With nullsFirst, null is smaller than everything, so headSet(null) returns an empty set. This may be surprising.

Third, changing the comparator after elements have been added is not supported by the TreeSet API. If a comparator object is mutable and you change how it orders elements, the set's behavior becomes undefined. You would need to recreate the set with the new comparator.

Performance and Runtime Cost of Null Handling

From a performance perspective, allowing null with nullsFirst or nullsLast adds negligible overhead. The comparator performs a null check on each comparison. In a red-black tree, each insertion or lookup performs O(log n) comparisons, so the extra null check is constant-time per comparison. There is no additional memory allocation or tree restructuring specifically for null.

However, there is a subtle runtime consideration: if a comparator does not handle null consistently, it may cause intermittent failures that are hard to debug. The nullsFirst and nullsLast wrappers reduce this risk by centralizing null handling.

Another point is that TreeSet does not allow duplicate elements. If you add multiple null values, only one is stored, because the comparator returns 0 when comparing null to null. This is usually the desired behavior for a set, but it means you cannot use TreeSet to count occurrences of null.

When to Avoid Nulls in a TreeSet

While custom comparators make it possible to store null, doing so often indicates a design issue. A TreeSet is meant to maintain a sorted collection of non-null elements. If you find yourself adding null frequently, consider whether a different collection better fits your needs.

  • If order is not required, a HashSet allows null without any special handling.
  • If you need order and must store null, a List with a null-safe comparator and manual duplicate checks may be simpler.
  • If null represents an absence of value, consider using an Optional or a sentinel object that implements Comparable.

Allowing null also complicates the API for consumers of your code. Every caller must remember that first() might return null, and that contains(null) is a valid query. This increases the cognitive load and the chance of a NullPointerException elsewhere in the codebase. Weigh the convenience of allowing null against the long-term maintainability of the collection.

If you do decide to allow null, document the comparator's behavior clearly and use Comparator.nullsFirst or nullsLast rather than a hand-written comparator. This keeps the null handling explicit and reduces the risk of subtle ordering bugs.

Java TreeSet Null Handling: Default Rejection and Custom Comparators | RYUSLOG DEV