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 existremove()— Remove a specific elementfirst()/last()— Retrieve the minimum or maximum elementceiling()/floor()— Find the nearest matching valueiterator()— 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:
NullPointerExceptionClassCastExceptionConcurrentModificationException
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:
TreeSetrelies oncompareTo()or a providedComparator- Every element must participate in the same comparison logic
StringandIntegerhave 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.