Java BigDecimal 数值相同,equals 却返回 false
BigDecimal 的 equals 同时检查数值和 scale。按数值比较使用 compareTo,集合查找则需要一致的键规范化规则,并保留原来的精度与展示约定。
两个十进制数分别从 "3.0" 和 "3.00" 构造,equals() 返回 false,compareTo() 却返回 0。把其中一个放进 HashSet 后,拿另一个查找也可能找不到。
这里要区分数值相等与表示方式相等。BigDecimal 的 equals() 同时检查数值和 scale;compareTo() 按数值比较,不要求 scale 相同。下面的示例适用于 Java 8 及以上。
打印 scale,而不是只看格式化后的金额
import java.math.BigDecimal;
public class DecimalEquality {
public static void main(String[] args) {
BigDecimal left = new BigDecimal("3.0");
BigDecimal right = new BigDecimal("3.00");
System.out.println(left.scale());
System.out.println(right.scale());
System.out.println(left.equals(right));
System.out.println(left.compareTo(right));
}
}
预期输出依次为 1、2、false、0。界面可能把两者都显示成两位小数,但显示格式并没有修改对象中保存的 scale。
这个差异与浮点近似误差无关。示例使用字符串构造,差异就在表示精度。不要先转成 double 再比较,这会额外引入精度丢失,反而放弃了 BigDecimal 原有的精确十进制表示。
只关心数值时使用 compareTo
boolean sameAmount = left.compareTo(right) == 0;
boolean smaller = left.compareTo(right) < 0;
把这段判断放在两个非 null 的 BigDecimal 上。业务若允许缺失值,应先明确缺失值是否可比较;不要把 null 自动当成零。
如果业务确实需要区分 3.0 与 3.00 的表示,则保留 equals()。不要仅为了让一个判断通过,统一把所有字段改成固定小数位;改变 scale 可能需要舍入,也可能改变后续计算和展示约定。
HashSet 和键查找需要一致的规范化规则
在普通集合查找中,单独改一处条件为 compareTo() 不会改变集合的等价规则。HashSet 的 contains() 使用对象相等语义;BigDecimal 的哈希计算也包含 scale。
如果集合明确以“相同数值视为同一项”为契约,可以在插入和查找两端都规范化:
import java.math.BigDecimal;
import java.util.HashSet;
import java.util.Set;
public class DecimalSet {
public static void main(String[] args) {
Set<BigDecimal> amounts = new HashSet<>();
amounts.add(new BigDecimal("3.0").stripTrailingZeros());
boolean found = amounts.contains(
new BigDecimal("3.00").stripTrailingZeros()
);
System.out.println(found);
}
}
这段代码的预期输出是 true。只规范化查询参数,而集合仍存着原来的不同 scale 对象,不能作为完整修复。已有集合需要按同一规则重新构建,并评估原来不同的项合并后是否会丢掉业务信息。
stripTrailingZeros() 返回新对象,不改原对象;结果的 scale 可能是负数,字符串显示也可能变为科学计数法。因此适合规范化比较键,不应未经评估就替换所有展示值或持久化金额。
验证时分别覆盖尾随零不同、真实数值不同、零,以及集合插入后查找。回退时恢复原比较或键规范化规则;若已对持久化数据做了合并,仅回退代码无法恢复合并前的区别,应先保留原始数据,避免把这一修复扩大成不可逆的数据清洗。


