为什么不能使用“==”来比较两个浮点数或双精度数?

30
我正在阅读《Effective Java》一书,作者是Joshua Bloch。在第8条「覆盖equals时遵守通用约定」中,有这样一句话:

对于float类型的字段,请使用Float.compare方法;对于double类型的字段,请使用Double.compare。之所以对float和double类型的字段进行特殊处理,是因为存在Float.NaN、-0.0f以及类似的double常量。

有人能否举个例子解释一下为什么我们不能使用==来比较float或double呢?

2
一个 NaN 是否等于另一个 NaN?它是大于、小于还是等于其他任何数字?-0.0f 是否等于 0.0f? - Paul Tomblin
https://dev59.com/hmox5IYBdhLWcg3wOx5i#9341669 - MrLore
这取决于你想做什么。如果我没记错的话(不要完全相信我),NaN == NaN 是false。但是,compare 的官方规范并没有给出有关如何处理“奇怪”情况的详细信息。更重要的是要记住,在除了特殊情况之外,永远不要进行“相等”的比较,除非你知道自己在做什么。 - Hot Licks
3
@devnull,你的重复建议太糟糕了。这个问题是关于为什么“==”不足以作为等价关系,而“Float.compare(x,y)==0”更好,因为它是一个等价关系。你所建议的问题的被接受回答是关于构建一个“epsilon范围内相等”的概念,这甚至更不是一个等价关系。在建议将浮点数比较到epsilion或者说这是一个关于浮点数比较到epsilon的重复问题之前,请仔细阅读浮点数问题。 - Pascal Cuoq
有人应该添加一个例子,比如说将 2000000020000001 这两个浮点数进行比较,如果使用 == 来比较,它们是不相等的! - devnull
显示剩余2条评论
3个回答

20

从apidoc中了解到,Float.compare的作用是比较两个指定的浮点数值。返回的整数值与以下代码的返回值相同:

new Float(f1).compareTo(new Float(f2))

该整数值的符号与通过上述代码调用返回的整数的符号相同。

Float.compareTo:

比较两个浮点数对象的大小。当应用于原始的浮点值时,该方法执行的比较和Java语言中的数值比较运算符(<、<=、==、>=、>)有两种不同之处:
  • 该方法认为Float.NaN等于自身且大于所有其他浮点值(包括Float.POSITIVE_INFINITY)。
  • 该方法认为0.0f大于-0.0f。
这确保了该方法所施加的Float对象的自然排序与equals方法一致。 考虑以下代码:
    System.out.println(-0.0f == 0.0f); //true
    System.out.println(Float.compare(-0.0f, 0.0f) == 0 ? true : false); //false      
    System.out.println(Float.NaN == Float.NaN);//false
    System.out.println(Float.compare(Float.NaN, Float.NaN) == 0 ? true : false); //true
    System.out.println(-0.0d == 0.0d); //true
    System.out.println(Double.compare(-0.0d, 0.0d) == 0 ? true : false);//false     
    System.out.println(Double.NaN == Double.NaN);//false
    System.out.println(Double.compare(Double.NaN, Double.NaN) == 0 ? true : false);//true        

输出结果不正确,因为一些不是数字的东西就是不是数字,应该在数字比较的角度上视为相等。同时,很明显0=-0
让我们看看Float.compare做了什么:
public static int compare(float f1, float f2) {
   if (f1 < f2)
        return -1;           // Neither val is NaN, thisVal is smaller
    if (f1 > f2)
        return 1;            // Neither val is NaN, thisVal is larger

    int thisBits = Float.floatToIntBits(f1);
    int anotherBits = Float.floatToIntBits(f2);

    return (thisBits == anotherBits ?  0 : // Values are equal
            (thisBits < anotherBits ? -1 : // (-0.0, 0.0) or (!NaN, NaN)
             1));                          // (0.0, -0.0) or (NaN, !NaN)
}

Float.floatToIntBits:

根据IEEE 754浮点数“单格式”位布局的规定,返回指定浮点值的表达式。第31位(由掩码0x80000000选择的位)表示浮点数的符号。第30-23位(由掩码0x7f800000选择的位)表示指数。第22-0位(由掩码0x007fffff选择的位)表示浮点数的尾数(有时称为“曼蒂萨”)。如果参数是正无穷大,则结果为0x7f800000。如果参数是负无穷大,则结果为0xff800000。如果参数是NaN,则结果为0x7fc00000。在所有情况下,结果都是一个整数,当给intBitsToFloat(int)方法时,它将产生与floatToIntBits的参数相同的浮点值(除了所有NaN值均折叠为单个“规范”NaN值)。

来自JLS 15.20.1. 数值比较运算符 <, <=, >, 和 >=

根据IEEE 754标准的规定,浮点数比较的结果如下:

  • 如果任一操作数为NaN,则结果为false。

  • 所有非NaN值都是有序的,负无穷小于所有有限值,正无穷大于所有有限值。

  • 正零和负零被认为是相等的。例如,-0.0<0.0为false,但-0.0<=0.0为true。

  • 然而,请注意,方法Math.min和Math.max将负零视为严格小于正零。

对于严格比较,其中操作数为正零和负零,结果将是错误的。

来自JLS 15.21.1. 数值相等运算符 == 和 !=

根据IEEE 754标准的规定,浮点数比较的结果为:

浮点数相等性测试遵循IEEE 754标准的规则:

  • 如果任一操作数为NaN,则==的结果为false,但!=的结果为true。实际上,当且仅当x的值为NaN时,测试x!= x为true。方法Float.isNaN和Double.isNaN也可用于测试一个值是否为NaN。

  • 正零和负零被认为是相等的。例如,-0.0 == 0.0为true。

  • 否则,相等运算符将两个不同的浮点值视为不相等。特别地,有一个表示正无穷大和一个表示负无穷大的值;每个值只与自身相等,并且与所有其他值都不相等。

对于两个操作数都是 NaN 的相等比较,结果将会是错误的。

由于全序关系(=, <, >,<=, >=被许多重要的算法所使用(参见所有实现Comparable接口的类),因此最好使用compare方法,因为它会产生更一致的行为。

在 IEEE-754 标准下,全序关系的后果是正零和负零之间的差异。

例如,如果您使用等号运算符而非 compare 方法,并且有一些值的集合,您的代码逻辑基于元素的顺序做出一些决策,以某种方式开始获取 NaN 值的剩余部分,则它们将被视为不同的值而不是相同的值。

这可能会导致程序行为的错误与NaN值的数量/比例成正比。如果您有很多正零和负零,那么只有一对就足以通过错误影响您的逻辑。

Float 使用 IEEE-754 32位格式,Double 使用 IEEE-754 64位格式。


8

float(和double)具有一些特殊的位序列,用于保留非“数字”的特殊含义:

  • 负无穷大,内部表示为0xff800000
  • 正无穷大,内部表示为0x7f800000
  • 不是数字,内部表示为0x7fc00000

使用Float.compare()将它们与自身进行比较时,每个值都会返回0(表示它们“相同”),但是对于Float.NaN,以下使用==进行的比较则与此不同:

Float.NEGATIVE_INFINITY == Float.NEGATIVE_INFINITY // true
Float.POSITIVE_INFINITY == Float.POSITIVE_INFINITY // true
Float.NaN == Float.NaN // false

因此,当比较 float 值时,为了对所有值保持一致性,包括特殊的 Float.NaN 值,最好使用 Float.compare()

对于 double 同样适用。


@EricPostpischil的Float.compare()返回一个int,而不是"false"(假)或"true"(真)。所有后续对Float.compare()的调用都返回0:Float.compare(Float.POSITIVE_INFINITY, Float.POSITIVE_INFINITY)Float.compare(Float.NEGATIVE_INFINITY, Float.NEGATIVE_INFINITY)Float.compare(Float.NaN, Float.NaN)。我确定这一点,因为我刚刚执行了它。 - Bohemian
@EricPostpischil OK - 我编辑了答案,解释了零的含义(即使它已经在compare()的Javadoc中了)。 - Bohemian

2

比较浮点对象有两个原因:

  • 我在进行数学运算,所以我想比较它们的数值。从数值上看,-0等于+0,而NaN不等于任何东西,甚至不等于自己,因为“相等”是仅适用于数字的属性,而NaN不是数字。
  • 我正在使用计算机中的对象,所以我需要区分不同的对象并将它们排序。例如,在树或其他容器中对对象进行排序就是必要的。

== 运算符提供了数学比较。对于 NaN == NaN 返回 false,对于 -0.f == +0.f 返回 true。

comparecompareTo 程序提供了对象比较。当将 NaN 与自身进行比较时,它们表明它们是相同的(通过返回零)。当将 -0.f+0.f 进行比较时,它们表明它们是不同的(通过返回非零值)。


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