应避免用==或equals()直接比较浮点数属性,因其二进制精度缺陷被对象封装隐藏,导致缓存失效、去重失败等业务问题;正确做法是用long/bigdecimal建模或引入误差容忍比较。

在面向对象编程中,对浮点数属性直接用 == 或 equals() 做等值判断,极易导致逻辑结果失真——不是代码写错了,而是浮点数本身的二进制表示缺陷被对象封装“隐藏”后,在比较环节集中爆发。
对象封装放大了精度误差的不可见性
当浮点数作为类的属性(如 Product.price、User.score)被封装后,开发者容易误以为“属性值就是它看起来的样子”。但实际:
- 构造时可能来自不同源头:JSON 字符串解析、数据库 DECIMAL 字段转 double、前端传参再反序列化,每条路径引入的舍入误差都不同
- getter 方法返回的仍是原始 double/float,但调用方看不到中间转换过程,误以为“读出来就该是精确值”
- 两个逻辑上相同的对象(如同一商品两次加载),因加载时机、JVM 优化或 CPU 指令差异,price 属性内存值可能差一个最低有效位
equals() 方法无法绕过底层比特差异
Java 中 Double.equals() 和 Float.equals() 虽然做了 null 安全处理,但本质仍是比较内存中的比特位。只要存在微小舍入差异,就返回 false:
-
new Double(0.1 + 0.2).equals(new Double(0.3))→false - 若在
equals()重写中直接写this.price == other.price,同样失效 - 哪怕所有字段其他都相同,仅 price 差
1e-16,整个对象就被判为“不等”
业务逻辑链路因此断裂
面向对象常依赖相等性支撑关键行为,一旦失真,后果层层传导:
- 缓存失效:相同业务对象因 price 微差生成不同 hashcode,无法命中同一缓存 key
-
集合去重失败:
HashSet<product></product>把本应合并的两个价格近似的商品当作不同元素保留 -
状态机跳转错误:订单状态根据
paymentAmount == total判断是否付清,结果因误差始终卡在“待支付” -
DTO 比对误报:前后端传输对象 diff 时,把
0.9500000000000001和0.95当作变更,触发不必要的同步
正确做法:在对象设计阶段就规避风险
不要等出问题再修 equals,而应在建模时切断失真根源:
- 金额、比例、阈值类属性,优先用
long(单位“分”)、int(百分比数值)或BigDecimal(必须用字符串构造,如new BigDecimal("0.95")) - 若必须保留 double,重写
equals()时改用误差容忍:例如Math.abs(this.confidence - other.confidence) - 提供语义化比较方法,如
isPriceEqual(Product other),而非依赖通用equals() - 在 setter 中预标准化:如
this.price = Math.round(price * 100.0) / 100.0,统一截断到分位











