Java TreeSet and exception handling

Recently, we used TreeSet in a backend service to handle user-generated tags and recommendation data. The initial goal was simple: to automatically sort the data while ensuring its uniqueness.

At first glance, TreeSet seemed to perfectly meet our needs. It eliminated duplicates, maintained element order, and provided efficient lookup operations. However, to our surprise, some seemingly harmless changes during development triggered runtime exceptions that didn’t immediately appear during code review.

This article summarizes some of the problems we encountered while using TreeSet, the debugging process, and the final solution.

Why We Chose TreeSet

Internally, TreeSet is implemented using a Red-Black Tree. Unlike HashSet, which focuses on fast hashing, TreeSet maintains elements in sorted order automatically.

The operations we relied on most frequently were:

  • add() — Add an element if it does not already exist
  • remove() — Remove a specific element
  • first() / last() — Retrieve the minimum or maximum element
  • ceiling() / floor() — Find the nearest matching value
  • iterator() — Traverse elements in sorted order

In our case, the automatic sorting was the main reason for choosing it. We initially replaced several manual sorting operations with a single TreeSet, and the code became much cleaner.

However, after deployment, some extreme cases began to appear in the logs.

The most common exceptions were:

  • NullPointerException
  • ClassCastException
  • ConcurrentModificationException

And in one particularly annoying case, an incorrectly implemented comparator caused unexpected behavior during insertion.


1. NullPointerException

The first issue appeared after integrating data from a third-party API.

Under normal circumstances, every record should contain a valid string value. However, one day the monitoring dashboard showed a sudden spike in failures. After tracing the request chain, we found that one API response occasionally returned a missing field, which became null.

The exception stack trace pointed directly to TreeSet.add().

Example

import java.util.TreeSet;

public class TreeSetNullPointerExample {
    public static void main(String[] args) {
        TreeSet<String> set = new TreeSet<>();

        try {
            set.add(null); // Attempt to add null element
        } catch (NullPointerException e) {
            System.out.println("Caught NullPointerException: TreeSet does not allow null elements.");
        }

        set.add("Apple");
        set.add("Banana");
        System.out.println("TreeSet after adding elements: " + set);
    }
}

What We Learned

Initially, a colleague thought the problem was caused by serialization, since the exception wouldn’t occur every time.

After reproducing the problem locally, it became clear that the TreeSet must compare each element to maintain order. If a null value is encountered during the comparison, Java cannot determine its relative position and therefore throws a NullPointerException.

In our service, the fix was not adding a try-catch block. Instead, we validated incoming data before insertion:

if (value != null) {
    set.add(value);
}

This reduced several hundred daily exception logs to zero.


2. ClassCastException

This one took longer to diagnose because the error appeared only after a new feature was released.

Originally, our collection contained only strings. Later, another module accidentally inserted numeric IDs into the same structure. The compiler didn’t complain because the collection was declared as TreeSet<Object>.

Everything looked fine until runtime.

Example

import java.util.TreeSet;

public class TreeSetClassCastExample {
    public static void main(String[] args) {
        TreeSet<Object> set = new TreeSet<>();

        try {
            set.add("Apple");
            set.add(10); // Mixing String and Integer
        } catch (ClassCastException e) {
            System.out.println("Caught ClassCastException: Elements must be mutually comparable.");
        }

        System.out.println("TreeSet contents: " + set);
    }
}

Debugging Notes

The first clue came from the stack trace:

java.lang.ClassCastException:
class java.lang.Integer cannot be cast to class java.lang.String

Initially, we suspected a deserialization issue, as the values ​​originated from different services.

After adding temporary logs around each insertion point, we discovered that both String and Integer values ​​were being inserted into the same TreeSet.

The root cause is straightforward:

  • TreeSet relies on compareTo() or a provided Comparator
  • Every element must participate in the same comparison logic
  • String and Integer have no meaningful way to compare against each other

The long-term solution was to enforce stronger typing and replace:

TreeSet<Object>

with:

TreeSet<String>

Once that change was merged, the exception disappeared completely.


3. ConcurrentModificationException

This is probably the most common TreeSet issue I’ve seen during code reviews.

The bug appeared in a cleanup task that removed invalid entries while iterating through a collection.

The implementation looked harmless:

Example

import java.util.TreeSet;
import java.util.Iterator;
import java.util.ConcurrentModificationException;

public class TreeSetConcurrentModification {
    public static void main(String[] args) {
        TreeSet<String> fruits = new TreeSet<>();
        fruits.add("Apple");
        fruits.add("Banana");
        fruits.add("Orange");

        try {
            for (String fruit : fruits) {
                if (fruit.equals("Banana")) {
                    fruits.remove(fruit); // Unsafe modification
                }
            }
        } catch (ConcurrentModificationException e) {
            System.out.println("Caught ConcurrentModificationException: Cannot modify TreeSet during iteration.");
        }

        // Safe removal using Iterator
        Iterator<String> iterator = fruits.iterator();
        while (iterator.hasNext()) {
            if (iterator.next().equals("Banana")) {
                iterator.remove();
            }
        }

        System.out.println("TreeSet after safe removal: " + fruits);
    }
}

What Happened During Debugging

Interestingly, this bug passed local testing several times because the dataset was tiny.

Once it ran against production-sized data, exceptions started appearing.

The stack trace showed:

java.util.ConcurrentModificationException

Many developers initially assume this means multiple threads are involved.

In our case, there was only one thread.

The real issue was that the collection structure changed while the iterator was still traversing it. Java’s fail-fast mechanism detected the modification and immediately threw an exception.

After switching to Iterator.remove(), the cleanup job became stable.

As a side effect, the execution time of the cleanup process became more predictable because we no longer had failed iterations that required retries.


4. IllegalArgumentException Caused by a Custom Comparator

This one was rare, but it caused the most confusion.

We had a custom sorting rule designed to prioritize certain records. During testing, some elements appeared in the wrong order, and occasionally insertion failed.

The problem turned out to be the comparator itself.

Example

import java.util.TreeSet;
import java.util.Comparator;

public class TreeSetComparatorExample {
    public static void main(String[] args) {
        Comparator<String> reverseComparator = (a, b) -> {
            if (a.equals("Apple") && b.equals("Banana")) return -1;
            return a.compareTo(b);
        };

        TreeSet<String> set = new TreeSet<>(reverseComparator);

        try {
            set.add("Apple");
            set.add("Banana"); // May break comparator logic
        } catch (IllegalArgumentException e) {
            System.out.println("Caught IllegalArgumentException: Check your Comparator logic.");
        }

        System.out.println("TreeSet contents: " + set);
    }
}

Why This Is Dangerous

A comparator must follow several consistency rules:

  • Comparison results should be symmetric
  • Ordering should be transitive
  • Results should remain stable for identical inputs

When those rules are violated, TreeSet may behave unpredictably.

The tricky part is that problems do not always appear immediately.

In one test environment, everything seemed normal. Under larger datasets, however, records started disappearing because the tree structure could no longer maintain a consistent ordering relationship.

The lesson here was simple:

Whenever we write a custom comparator now, we create unit tests that validate sorting behavior against hundreds or thousands of randomized inputs before deploying it.

That extra testing has saved us multiple times.


Practical Recommendations After Using TreeSet in Real Projects

After dealing with these issues repeatedly, our team eventually settled on a few rules:

1. Never Assume Input Data Is Clean

Even if upstream services claim a field is mandatory, add validation before inserting values into a TreeSet.

2. Avoid Generic Object Collections

If a collection is supposed to hold strings, declare it as:

TreeSet<String>

rather than:

TreeSet<Object>

Strong typing prevents many runtime surprises.

3. Never Remove Elements Inside a for-each Loop

Use an Iterator whenever deletion is required during traversal.

4. Treat Comparator Logic as Business Logic

Comparator bugs can be surprisingly expensive to diagnose. Test them with real datasets, not just a handful of hardcoded examples.

5. Don’t Rely on try-catch as the Primary Solution

During our early implementation, we wrapped several operations with exception handling and considered the issue solved.

In reality, the better approach was to eliminate invalid data and invalid operations before they reached the TreeSet.


If you’re going to introduce TreeSet into a production system, it’s worth taking the time to carefully check null value handling, comparator consistency, and iteration mode beforehand. This will save you a lot of debugging work later.

Leave a Reply

Your email address will not be published. Required fields are marked *