In Java, standard sets (like HashSet) enforce uniqueness using the equals() and hashCode() methods. However, sorted sets (like TreeSet) ignore those methods entirely! Instead, they enforce uniqueness and ordering using compareTo(). If your comparison logic is incorrect, you can easily break the Set's uniqueness guarantees.
In this guide, we will trace a common bug where a custom class implements Comparable but returns 0 from compareTo(), causing different objects to overwrite each other inside a TreeSet.
Imagine registering students for a class in alphabetical order:
- The registrar has a rule: "If two names compare as identical, they are the same student, and we refuse duplicate registration."
- A student named **Coffee** registers first. The registrar writes their name down.
- A student named **Tea** arrives. The registrar, being lazy, evaluates their alphabetical comparison as: "They are the same priority level" (returns 0).
- The registrar says: "Tea, we already have Coffee registered, and since you are the same, we won't write you down." Tea is rejected because the sorting comparison said they are equal.
1. The Broken Comparable Class
Consider this class definition representing a drink. It implements Comparable, but its implementation always claims objects are equal:
class Drink implements Comparable {
public String name;
// TRAP: Always returns 0 (indicating elements are equal)
public int compareTo(Object o) {
return 0;
}
@Override
public String toString() {
return "Drink{name='" + name + "'}";
}
}
2. Tracing the TreeSet Execution
Now let's trace what happens when we attempt to add multiple distinct drinks to a TreeSet:
public static void main(String[] args) {
Drink one = new Drink();
Drink two = new Drink();
one.name = "Coffee";
two.name = "Tea";
TreeSet set = new TreeSet();
set.add(one); // Added successfully. Set size becomes 1.
set.add(two); // TreeSet calls two.compareTo(one), which returns 0!
System.out.println("Set size: " + set.size()); // PRINTS: Set size: 1
set.stream().forEach(System.out::println); // PRINTS: Drink{name='Coffee'}
}
When set.add(two) was called, the TreeSet traversed its binary tree to find where to place two. It called two.compareTo(one).
Because our method returned 0, the TreeSet concluded that two is a duplicate key and ignored the insertion.
As a result, "Tea" is never added, and the set's size remains 1.
Conclusion
When design custom types for sorted collections:
- Remember that
TreeSetandTreeMapusecompareTo()or aComparatorto enforce uniqueness, **not**equals(). - Ensure that if two objects are not equal, your
compareTo()method never returns0. Use field-by-field string or integer comparisons to break ties.