A compareTo is consistent with equals when a.compareTo(b) == 0 if and only if a.equals(b). The contract recommends this but doesn't require it. BigDecimal is the canonical violator: new BigDecimal("2.0").equals(new BigDecimal("2.00")) is false (different scale), but compareTo returns 0 (same numeric value).
The BigDecimal mismatch
var a = new BigDecimal("2.0");
var b = new BigDecimal("2.00");
a.equals(b); // false — scale differs (1 vs 2)
a.compareTo(b); // 0 — numeric value is identical
a.hashCode() == b.hashCode(); // false (consistent with equals)
BigDecimal deliberately keeps scale in equals to distinguish 2.0 from 2.00 (they have different "precision"), but compareTo ignores scale because numerically they're the same.
Why this breaks sorted collections
Sorted collections (TreeSet, TreeMap) treat elements as equal when compareTo returns 0, not when equals returns true. Hash collections do the opposite.
var hashSet = new HashSet<BigDecimal>();
hashSet.add(new BigDecimal("2.0"));
hashSet.add(new BigDecimal("2.00"));
hashSet.size(); // 2 — equals + hashCode say "different"
var treeSet = new TreeSet<BigDecimal>();
treeSet.add(new BigDecimal("2.0"));
treeSet.add(new BigDecimal("2.00"));
treeSet.size(); // 1 — compareTo says "same"
Same elements, same code path, different collection types, different sizes. Move from HashSet to TreeSet (or back) during a refactor and you can lose data without any warning.
The Set interface violation
Set is contractually defined in terms of equals. TreeSet is a Set but uses compareTo for membership. So TreeSet technically violates the Set interface contract when its element type's compareTo is inconsistent with equals. The Javadoc warns about exactly this:
A sorted set [...] is well-behaved even when its ordering is inconsistent with equals; it just fails to obey the general contract of the
Setinterface.
Other types with the same trap
BigDecimal— scale-sensitive equals, scale-insensitive compareTo (described above).- Case-insensitive
Stringcomparators —Comparator.CASE_INSENSITIVE_ORDERsays"hello".compareTo("HELLO") == 0but"hello".equals("HELLO") == false. - Most user-defined
Comparators — most comparators sort by a subset of fields, so two objects with different "other" fields compare as 0.
How to be safe
- Document inconsistency — if your type has
compareToinconsistent withequals, say so in the Javadoc. Effective Java Item 14. - Prefer
HashSet/HashMapwhen membership matters — they useequals, which is what programmers usually expect. - Use
TreeSet(Comparator)explicitly when you want compareTo-based deduplication. Then it's an intentional design choice. - Normalize before insertion — call
bigDecimal.stripTrailingZeros()before adding to aHashSetif you want2.0and2.00treated as duplicates.