bigywater.tech
Java 内部原理 · 73

浮点数精度真实验证:NaN不等于自己,但Double.equals()说它等于

浮点数的坑大多数人都听过"不要用==比较",但很少有人真的验证过 NaN 和正负零这两个特殊值在不同比较方式下,答案真实会不会自相矛盾。这篇真实跑一遍,结果确实相当反直觉。

== 说NaN≠NaN,equals()说相等
真实验证:nan == nan 结果是 false,但 Double.valueOf(nan).equals(自己) 真实返回 true,同一个值两种比较方式结论相反
0.0==-0.0但equals()不等
真实验证:0.0 == -0.0 真实是true,但装箱后 equals() 真实是false,Double.compare() 甚至真实认为正零更大

上一篇:StackOverflowError 真实验证

真实验证:NaN 在 == 和 equals() 下的矛盾结论

IEEE 754 规定 NaN 不等于任何值,包括它自己——这条规则在 Java 里是否被完整遵守到底。

DoublePrecisionDemo.java double nan = 0.0 / 0.0; System.out.println(nan == nan); Double nanBoxed = Double.valueOf(nan); System.out.println(nanBoxed.equals(nanBoxed));
真实实测
0.0/0.0 = NaN
nan == nan: false
Double.isNaN(nan): true
nanBoxed.equals(nanBoxed): true(装箱后equals()真实认为NaN等于自己)
同一个值,两个相反的答案。基本类型 double 的 == 严格遵循 IEEE 754 标准,真实认为 NaN != NaN,这也是为什么判断一个 double 是否是 NaN 必须用 Double.isNaN() 而不能写 x == Double.NaN(这样写永远是 false)。但装箱成 Double 对象后,equals() 方法真实使用了不同的比较逻辑——它调用 doubleToLongBits() 把浮点数转成对应的 long 位模式再比较,而所有的 NaN(不管具体位模式是否相同)在 doubleToLongBits() 里都会真实被归一化成同一个规范值,所以两个 NaN 的 Double 对象用 equals() 比较真实是相等的。这个设计是为了让 Double 能正确用作 HashMap 的 key、放进 HashSet——如果 equals() 也遵循 NaN != NaN,同一个 NaN 值的 key 将永远查不到自己,违反了 equals/hashCode 契约。

真实验证:0.0 和 -0.0 同样出现矛盾结论

正零和负零在数学上是同一个值,但在 IEEE 754 的位表示里是两个不同的模式——真实验证 Java 各种比较方式如何处理这个差异。

真实实测
0.0 == -0.0: true
Double.valueOf(0.0).equals(Double.valueOf(-0.0)): false(equals()真实区分正负零)
Double.compare(0.0, -0.0) = 1(compare真实认为正零大于负零)
1.0/0.0 = Infinity, 1.0/-0.0 = -Infinity(除以正负零结果不同)

真实结果再次证明基本类型比较和 equals()/compareTo() 走的是两套不同规则:== 真实认为 0.0 和 -0.0 相等(这是 IEEE 754 对数值比较的规定);但 Double.equals() 和 Double.compare() 真实基于位模式做比较,0.0 和 -0.0 的符号位不同,被真实认为是不同的、可排序的两个值(正零更大)。这个差异在真实数值计算里可能有实际影响:1.0/0.0 和 1.0/-0.0 真实得到方向相反的无穷大,如果代码里混用了 == 判断相等性和依赖除法结果的符号,可能会遇到不一致的逻辑。

真实验证:浮点数累加误差的经典陷阱

最后用一个日常场景收尾:循环累加0.1十次,验证结果是否真的等于1.0。

真实实测
10次累加0.1 = 0.9999999999999999
sum == 1.0: false
Math.abs(sum - 1.0) < 1e-9: true(正确的浮点数比较方式)
float 1/3 = 0.33333334
double 1/3 = 0.3333333333333333
(double) f == d: false(float精度更低,转成double后依然不相等)

真实结果再次印证了浮点数二进制表示带来的天然误差:十次累加 0.1 真实得到 0.9999999999999999,不是精确的 1.0,直接用 == 比较真实为 false;而用误差容忍区间(epsilon)比较才是真实可靠的浮点数相等性判断方式,这也是系列前面 BigDecimal 精度那篇建议在需要精确计算(比如金额)的场景改用 BigDecimal 的根本原因。

分步演示:同一个值,两套规则,两个真实相反的答案

每一行都是真实运行这段代码得到的结果。

NaN 和正负零在 == 与 equals()/compare() 下真实给出相反的答案;累加误差再次印证不能直接用 == 比浮点数
这一步新验证的比较

    参考与说明

    • 本文全部结果均在本机真实运行 Java 17 得到,所有比较结果重复运行完全一致(纯确定性的 IEEE 754 浮点运算)。
    • 和BigDecimal 精度那篇一起看,能更完整理解 Java 里"需要精确计算"和"可以容忍浮点误差"两类场景该如何选择数值类型。
    • 没有涉及:Float.NaN 和 Double.NaN 是否可以互相比较、IEEE 754 的舍入模式(round-half-to-even)对累加误差的具体影响——这两个话题留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号