bigywater.tech
Java 内部原理 · 99

compareTo与equals不一致真实验证:TreeSet真实把BigDecimal("2.0")和("2.00")当成同一个值

BigDecimal 的 compareTo 和 equals 不一致是文档里明确警告过的"陷阱",这篇真实用同一批数据分别喂给 HashSet 和 TreeSet,验证这句警告到底会造成多大的真实分歧。

同一批数据两种Set给出不同size
真实验证:含"2.0/2.00/2.000/3.0"四个值的列表,HashSet去重后真实size是4,TreeSet真实size是2
TreeMap能查到HashMap查不到的key
真实验证:treeMap.containsKey("1.00")真实为true(能查到"1.0"存的值),hashMap.containsKey("1.00")真实为false

上一篇:线程未捕获异常处理器真实验证

真实验证:同一对BigDecimal,equals()说不等,compareTo()说相等

系列前面验证过BigDecimal("1.0").equals("1.00")为false——这次专门验证compareTo()给出的真实是相反答案。

CompareToEqualsInconsistencyDemo.java BigDecimal a = new BigDecimal("1.0"); BigDecimal b = new BigDecimal("1.00"); a.equals(b); a.compareTo(b);
真实实测
a.equals(b) 真实结果: false(equals()真实认为scale不同就不相等)
a.compareTo(b) 真实结果: 0(compareTo()真实只比较数值,认为相等)

真实结果印证了 BigDecimal 官方文档明确写出的警告:equals() 把小数位数(scale)也纳入判等标准,"1.0" 和 "1.00" 因为 scale 不同(分别是1和2)真实被认为是不同的对象;而 compareTo() 遵循的是数学意义上的数值比较,两者数值相同,真实返回 0(相等)。这是 JDK 里少数几个明确"违反" Comparable 接口文档建议("compareTo的结果最好和equals保持一致")的类之一。

真实验证:HashSet 和 TreeSet 对同一批数据给出不同去重结果

HashSet依赖equals/hashCode判等,TreeSet依赖compareTo判等——用同一批含"重复值"的数据分别喂给两者。

CompareToEqualsInconsistencyDemo.java List<BigDecimal> values = List.of( new BigDecimal("2.0"), new BigDecimal("2.00"), new BigDecimal("2.000"), new BigDecimal("3.0") ); new HashSet<>(values); new TreeSet<>(values);
真实实测
同一批4个值(含3个数值相同但scale不同),HashSet去重后真实size: 4
同一批4个值,TreeSet去重后真实size: 2(真实把2.0/2.00/2.000当成同一个值)
真实分歧的后果有多严重。这4个值里 "2.0"、"2.00"、"2.000" 在数学上是同一个数,只有 "3.0" 是真正不同的值。真实运行 HashSet 去重完全没有识别出这个"重复"——因为它用 equals()/hashCode() 判断,三个 scale 不同的 "2" 被当成三个独立元素,最终 size 是 4;而 TreeSet 用 compareTo() 判断,真实认出了三者是"同一个值",只保留了最先插入的那个,加上 "3.0",size 真实是 2。如果一段代码从 HashSet 换成 TreeSet(比如为了获得有序遍历),而数据里恰好存在这种 scale 不同的等值 BigDecimal,去重结果会真实、悄无声息地发生变化。

真实验证:TreeMap 能用不同scale的key查到值,HashMap不能

同样的判等标准差异,在TreeMap/HashMap上会如何体现。

真实实测
treeMap.containsKey(b) 真实结果: true(TreeMap真实用compareTo判断key是否存在,b和a数值相同)
treeMap.get(b) 真实结果: 存的是a对应的值(真实能用b查到a存的值)
hashMap.containsKey(b) 真实结果: false(HashMap真实用equals判断,b和a的equals()为false)

真实结果再次印证了同一套原理:用 a("1.0")存入的值,TreeMap 真实允许用数值相同但 scale 不同的 b("1.00")查到,而 HashMap 真实查不到——两种 Map 实现对"key是否相同"这个基本问题,因为底层依赖的判等接口不同,真实给出了完全相反的答案。这也是官方文档给出的明确建议:涉及 BigDecimal 的场景,如果要用作集合的元素或 key,需要提前想清楚业务上到底需要"数值相等"(用 TreeSet/TreeMap,或者先用 stripTrailingZeros() 统一 scale 再用 HashSet/HashMap)还是"完全一致"(HashSet/HashMap 默认语义)。

分步演示:两种 Set 是从哪一步开始分道扬镳的

每一步的 size 和内容都是真实运行这段代码得到的,不是按规则手工推的。

同一批 4 个值,逐个插入 HashSet 和 TreeSet:size 是怎么从相同变成 4 对 2 的
这一步刚插入/被拒绝的值

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,HashSet/TreeSet/TreeMap/HashMap的判等结果重复运行完全一致。
    • 和BigDecimal 精度那篇一起看,能更完整理解BigDecimal这个"精确计算"的标准工具类,在集合语义上反而藏着一个需要格外小心的真实陷阱。
    • 没有涉及:BigDecimal.stripTrailingZeros()统一scale后是否能让equals()和compareTo()语义重新一致(结论是能,但有0的特殊场景需要额外注意)、其他JDK内置类型是否也存在类似的compareTo/equals不一致设计——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号