在Java中使用“==”比较浮点数有什么问题?

193
根据这个Java官方文档,在Java中==是浮点数的相等比较运算符。
然而,当我输入以下代码时:
if(sectionID == currentSectionID)

当我在编辑器中运行静态分析时,我收到了以下提示:"JAVA0078 浮点数使用“==”进行比较"

使用 == 比较浮点数有什么问题? 正确的方法是什么? 


32
因为使用“==”比较浮点数会存在问题,所以将其用作标识符是不明智的。您示例代码中的名称表明您正在这样做;长整型(longs)是首选,并且是事实上的标准ID。 - Carl Manaster
4
这只是一个随机的例子,还是您实际上使用浮点数作为 ID?这是有原因的吗? - Per Wiklander
7
对于浮点类型的字段,请使用Float.compare方法;对于双精度类型的字段,请使用Double.compare方法。由于存在Float.NaN、-0.0f以及类似的double常量,因此需要对float和double字段进行特殊处理;有关详细信息,请参阅Float.equals文档。 - lbalazscs
@nos 旧评论,我知道,但链接现在似乎已经失效了(至少在我的浏览器中是这样)。更新后的链接应该是这个:https://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html - Per Lundberg
如果您的浮点数是由其他浮点数计算得出的,可能会出现舍入误差。因此,如果某个值应该计算为2.0,实际可能是1.999992341,与2.0进行比较将返回false。即使是Float.compare方法也会受到这种影响。 - undefined
21个回答

220

测试浮点数“相等”的正确方法是:

if(Math.abs(sectionID - currentSectionID) < epsilon)

epsilon是一个非常小的数,例如0.00000001,具体取决于所需的精度。


29
请查看被接受的答案中的链接(http://www.cygnus-software.com/papers/comparingfloats/comparingfloats.htm),了解为什么固定的epsilon不总是一个好主意。具体而言,当要比较的浮点数值变得很大(或很小)时,epsilon就不再合适。(如果您知道浮点数值都相对合理,则使用epsilon是可以的。) - P.T.
1
@P.T 他能否将epsilon与一个数字相乘,并将函数更改为if(Math.abs(sectionID - currentSectionID) < epsilon*sectionID来解决这个问题? - enthusiasticgeek
3
这可能是迄今为止最好的答案,但仍有缺陷。你从哪里得到ε呢? - Michael Piefel
1
@MichaelPiefel,它已经说了:“根据所需的精度”。浮点数本质上就像物理值:您只对某些有限的位置感兴趣,这取决于总不准确性,超出这个范围的任何差异都被认为是无关紧要的。 - ivan_pozdeev
但是OP实际上只想测试相等性,而且由于这被认为是不可靠的,所以必须使用不同的方法。尽管如此,我并不明白他知道他的“期望精度”是什么;因此,如果你想要一个更可靠的相等性测试,问题仍然存在:你从哪里得到epsilon?我在回答这个问题时建议使用Math.ulp() - Michael Piefel
显示剩余3条评论

57

浮点数值可能存在微小误差,因此它们可能不会报告为完全相等。例如,将一个浮点型设置为“6.1”,然后再次打印出来,可能会得到一个报告值,如“6.099999904632568359375”。这是浮点数工作的基础,因此,您不希望使用相等性进行比较,而是在范围内进行比较,也就是说,如果浮点数与要比较的数字之间的差异小于某个绝对值。

注册表上的文章提供了关于为什么要这样做的很好概述,并且是有用和有趣的阅读材料。


@kevindtimm:那么你将这样进行相等性测试,如果(number == 6.099999904632568359375),每当你想知道number是否等于6.1时...是的,你是正确的...计算机中的所有内容都是严格确定的,只是在使用浮点数时所用的近似值在解决数学问题时是违反直觉的。 - Newtopian
浮点数仅在非常特定的硬件上是不确定精确的。 - Stuart P. Bentley
1
@Stuart 我可能错了,但我认为FDIV漏洞并不是非确定性的。硬件给出的答案不符合规格,但它们是确定性的,因为相同的计算总是产生相同的错误结果。 - Gravity
@Gravity 你可以争辩说,只要给定一组特定的限制条件,任何行为都是确定性的。 - Stuart P. Bentley
1
浮点数值并不是不精确的。每个浮点数值都恰好是它本身。可能不准确的是浮点计算结果。但要注意!当你在程序中看到类似于0.1这样的内容时,那不是浮点数值。那是一个浮点字面量---编译器通过计算将其转换为浮点数值的字符串。 - Solomon Slow

23

为了解释大家所说的原因。

浮点数的二进制表示有点麻烦。

在二进制中,大多数程序员知道1b=1d、10b=2d、100b=4d、1000b=8d之间的关联。

同样的道理也适用于另一面。

.1b=.5d、.01b=.25d、.001b=.125等等。

问题在于,没有一种精确的方式可以表示像.1、.2、.3等大部分小数。我们只能用二进制进行近似。系统在数字打印时进行一些虚假舍入,以便显示.1而不是.10000000000001或.999999999999(这两者与存储表示中的.1差不多)。

编辑自评论:问题在于我们的期望值。我们完全预计2/3在转换为十进制时会进行一些虚假舍入,例如.7或.67或.666667... 但我们并不会自动预期会像2/3一样虚假舍入.1——但事实上就是这样。

顺便提一下,如果你好奇,它内部存储的数字是使用二进制“科学记数法”表示的纯二进制表示形式。因此,如果您告诉它存储10.75d的十进制数,它将为10存储1010b,对于小数点则存储.11b。因此它会存储.101011然后在末尾保存一些位以指示:将小数点向右移四个位置。

(尽管从技术上讲,它已经不再是十进制点了,现在是一个二进制点,但这种术语对大多数找到本答案有用的人来说不会使事情更易懂。)


1
@Matt K - 嗯,不是定点;如果你“在末尾保存一些位数以将小数点[N]向右移动”,那就是浮点数。定点则将基数点的位置固定。此外,通常情况下,由于移动二进制小数点始终可以使左侧最左边的位置为“1”,因此您会发现一些系统省略了前导的“1”,将这样释放的空间(1位!)用于扩展指数的范围。 - JustJeff
这个问题与二进制和十进制表示无关。使用十进制浮点数,你仍然会遇到像(1/3)* 3 == 0.9999999999999999999999999999这样的情况。 - dan04
2
@dan04 是的,因为1/3没有十进制或二进制表示,它确实有三进制表示,并且以这种方式转换是正确的 :). 我列出的数字(.1、.25等)都有完美的十进制表示,但没有二进制表示——人们习惯于这些数字具有“精确”的表示。BCD可以完美处理它们。这就是区别所在。 - Bill K
1
这篇文章应该得到更多的赞,因为它描述了问题背后的真正原因。 - Levite

19
使用“==”比较浮点数有什么问题吗? 因为“0.1 + 0.2 == 0.3”并不成立。

8
那么 Float.compare(0.1f+0.2f, 0.3f) == 0 怎么样? - Aquarius Power
0.1f + 0.2f == 0.3f,但是0.1d + 0.2d != 0.3d。默认情况下,0.1 + 0.2为double类型。0.3同样为double类型。 - burnabyRails

18

截至今日,快速方便的方法是:

if (Float.compare(sectionID, currentSectionID) == 0) {...}

然而,文档并没有明确指定在浮点数计算中始终存在的边界差值(来自@Victor答案中的epsilon),但它应该是合理的,因为它是标准语言库的一部分。
但是,如果需要更高或定制的精度,则需要使用:
float epsilon = Float.MIN_NORMAL;  
if(Math.abs(sectionID - currentSectionID) < epsilon){...}

还有另一种解决方案可供选择。


2
你链接的文档中指出:“如果f1在数值上等于f2,则返回0”,这使得它与执行(sectionId == currentSectionId)相同,但对于浮点数来说并不准确。epsilon方法是更好的方法,可以参考这个答案:https://dev59.com/y3NA5IYBdhLWcg3wHp6B#1088271 - typoerrpr

14

我认为在浮点数(和双精度浮点数)方面存在很多混淆,有必要澄清。

  1. 在标准兼容的 JVM 中使用浮点数作为 ID 是没有任何本质问题的[*]。如果你只是将浮点数 ID 设置为 x,不对其进行任何操作(即不进行算术运算),然后测试 y 是否等于 x,那么就没问题了。同样,将它们用作 HashMap 中的键也没有问题。但是你不能假设等式成立,例如 x == (x - y) + y 等等。尽管如此,人们通常使用整数类型作为 ID,你可以观察到这里的大多数人对此代码不感冒,因此出于实际原因,最好遵循惯例。请注意,与 long 值一样,有许多不同的 double 值,因此使用 double 没有任何更多的好处。此外,使用双精度浮点数生成“下一个可用 ID”可能会很棘手,并需要一些关于浮点数算术的知识。得不偿失。

  2. 另一方面,依靠两个在数学上等价的计算结果的数值相等性是有风险的。这是由于从十进制转换为二进制表示时出现的舍入误差和精度损失。这在 SO 上已经讨论过很多次了。

[*] 当我说“标准兼容的 JVM”时,我想排除某些有缺陷的 JVM 实现。请参见 这里


在讨论什么是“浮点数”?如果尝试构建一个唯一的float值表并使用==进行比较,可怕的IEEE-754比较规则将导致表中充满了NaN值。 - supercat
啊——我不是指一个equals实例方法,而是指比较两个float类型值的静态方法(我认为在Float类中)。 - supercat
如果您使用==,则会得到相同的结果。您必须自己拒绝NaN。 - quant_dev
1
当我们谈论使用浮点数作为ID列时,这似乎是正确的答案。然而,我想知道数据库的“浮点行为”如何影响这个方程。所以,这可能会让我们回到Epsilon方法。最后,使用浮点数作为ID可能是个不好的主意。 - undefined
1
@Teddy 你说得对,数据库处理浮点数的方式在这里增加了额外的复杂性,可能不值得麻烦。 - undefined
显示剩余4条评论

8
这不是仅适用于Java的问题。使用“==”比较两个浮点数/双精度数/任何十进制类型的数字可能会导致问题,因为它们存储方式的原因。
根据IEEE标准754,单精度浮点数有32位,分配如下: 1位-符号(0=正,1=负) 8位-指数(2^x中x的特殊(偏移127)表示) 23位-尾数。实际存储的数字。
尾数是引起问题的原因。它有点像科学计数法,只是以二进制(基数2)显示,类似于1.110011 x 2^5。 但在二进制中,第一个1始终是1(除了表示0的情况)。
因此,为了节省一些内存空间(双关语),IEEE决定应该假设1。例如,尾数为1011的实际上是1.1011。
这可能会导致某些比较问题,特别是对于0,因为0在浮点数中无法完全表示。 这就是为什么“==”被反对的主要原因,除了其他答案所描述的浮点数数学问题。
Java具有独特的问题,因为该语言在许多不同的平台上都是通用的,每个平台都可能具有自己独特的浮点格式。这使得避免“==”更加重要。
比较两个浮点数(不特定于语言)是否相等的正确方法如下:
if(ABS(float1 - float2) < ACCEPTABLE_ERROR)
    //they are approximately equal

其中ACCEPTABLE_ERROR被定义为0.000000001或其他所需的精度常量,正如Victor已经提到的。

有些语言内置了此功能或常量,但通常养成这种习惯是很好的。


4
Java对浮点数有明确定义的行为,不会受平台影响。 - Yishai
1
IEEE-754标准中使用的术语是“尾数”,而不是“幂”。如果指数字段为1-254,则尾数的最高位为1。如果指数字段为0,则尾数的最高位为0。语句“浮点数中无法精确表示0”是错误的;0用所有位都为零的方式表示(并且最高位设置为1,这被区分为-0,等于+0)。这不会导致任何比较问题,也不是“==被不鼓励的主要原因”。 - Eric Postpischil
关于“比较两个浮点数的正确方法”:没有一般性的解决方案来比较包含先前操作错误的浮点数。 - Eric Postpischil

8

浮点数值不可靠,因为存在舍入误差。

因此,它们可能不适合用作关键值,例如sectionID。请改用整数,如果int不能包含足够的可能值,则使用long。


2
同意。鉴于这些是ID,没有必要用浮点算术复杂化事情。 - Yohnny
2
或者是一个长整型。根据未来生成的唯一ID数量,int类型可能不够大。 - Wayne Hartman
双精度浮点数相对于单精度浮点数有多精确? - Arvindh Mani
1
@ArvindhMani double类型更加精确,但它们也是浮点数值,因此我的答案旨在包括floatdouble两者。 - Eric Wilson

8

7

除了之前的答案,你应该知道与-0.0f+0.0f相关的奇怪行为(它们是==但不是equals),以及Float.NaN(它是equals但不是==)(希望我没搞错 - 哎呀,不要这样做!)。

编辑:让我们检查一下!

import static java.lang.Float.NaN;
public class Fl {
    public static void main(String[] args) {
        System.err.println(          -0.0f   ==              0.0f);   // true
        System.err.println(new Float(-0.0f).equals(new Float(0.0f))); // false
        System.err.println(            NaN   ==               NaN);   // false
        System.err.println(new Float(  NaN).equals(new Float( NaN))); // true
    }
} 

欢迎来到IEEE/754。

如果两个东西是“==”,那么它们在位级别上是相同的。它们怎么可能不相等()?也许你搞反了? - Matt K
@Matt NaN是特殊的。在Java中,Double.isNaN(double x)实际上是通过 { return x!= x; } 实现的... - quant_dev
1
使用浮点数时,== 不意味着数字是“位相同的”(尽管同一个数字可以用不同的位模式表示,但只有其中一个是规范化形式)。此外,-0.0f0.0f 由不同的位模式表示(符号位不同),但使用 == 比较相等(但不使用 equals)。一般来说,您认为 == 是按位比较的假设是错误的。 - Pavel Minaev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接