双包装器 class 的 .equals() 方法是否适用于查找浮点数的相等性?

Does the .equals() method of the double wrapper class work for finding equality of floating point numbers?

我知道对于原始浮点类型(浮点数和双精度数),您不应该通过 == 直接比较它们。但是,如果您将包装器 class 用于 double 怎么办?会不会像

Double a = 5.05;
Double b = 5.05; 
boolean test = a.equals(b);

正确比较两个值?

您需要完全理解为什么 == 比较不是一个好主意。不理解,就是在黑暗中摸索。

让我们谈谈计算机(和双打)的工作原理。

假设你进入了一个房间;它有 3 个电灯开关,否则它是光秃秃的。你会进入房间,你可以 fiddle 使用开关,但你必须离开。我稍后进入房间,可以看看开关。

你能传达多少信息?

答案是:你可以传达8种不同的状态:DDD、DDU、DUD、DUU、UDD、UDU、UUD和UUU。就是这样。

计算机在存储双精度值时的工作方式与此完全相同。除了不是 3 个开关,你得到 64 个开关。这意味着 2^64 个不同的状态,您可以用一个 double 来表达,这是大量的状态:这是一个 19 位数字。

但这仍然是有限数量的状态,这是有问题的:在 0 和 1 之间有无限数量的数字。更不用说在 -infinity 和 +infinity 之间了,double 敢覆盖。当您只能表示 2^64 个状态时,您如何存储无穷无尽的选择之一?当这个 19 位数字的任务是从无穷无尽的可能性中区分出来时,它开始看起来很小,不是吗?

答案当然是这是完全不可能的

所以双打实际上并不是那样工作的。相反,有人花了一些力气,在一个大房间里挂了一个巨大的数字线,从负无限到正无限,并向这条线投掷 2^64 飞镖。他们登陆的数字是 'blessed numbers' - 这些由 double 值表示。这确实意味着任意 2 个飞镖之间有无限数量的数字,因此 不能 表示。飞镖不是很随机:越接近 0,飞镖越密集。一旦超过 2^52 左右,2 镖之间的距离甚至超过 1.0。

这是一个无法表示的数字的简单示例:0.3。太棒了,不是吗?就这么简单。这意味着 计算机实际上无法使用 double 计算 0.1 + 0.2。那么当你尝试时会发生什么?规则规定任何计算的结果总是四舍五入到最接近的祝福数字。

问题就在这里:你可以 运行 数学:

double x = 0.1 + 0.2;

以后做:

double y = 0.9 - 0.8 + 0.15 + 0.05;

而我们人类会立即注意到 xy 自然是相同的。但是 对于计算机来说并非如此 - 由于无声四舍五入到最接近的祝福数字,x 可能是 0.29999999999999999785,而 y0.300000000000000000012.

因此我们在使用 double(或 float 时有四个关键方面,这在所有方面都差不多,永远不要使用它们):

  • 如果您需要绝对精度,请完全不要使用它们。
  • 打印时,始终向下舍入。 System.out.println 开箱即用,但你真的应该使用 .printf("%.5f") 或类似的:选择你需要的数字。
  • 请注意,错误会加剧,并且随着您离 1.0 越来越远,情况会变得更糟。
  • 永远不要与 == 进行比较,而是始终使用增量比较:“如果 2 个数字彼此相差在 0.0000000001 以内,则我们认为它们相等”的概念。

没有通用的魔法增量值;这取决于你的精度需求,你离 1.0 有多远,等等。因此,只要问计算机:嘿,把这些东西弄清楚我只想知道这两个双打是否相等 不可能.关于 'how close' 它们可能是什么,唯一不需要您输入的可用定义是绝对完美的概念:仅当它们完全相同时它们才相等。在上面那个简单的例子中,这个定义会让你失败。如果您使用 Double.equals 而不是 double == double 或任何其他实用程序 class,这不会有任何区别。

所以,不,Double.equals不合适。您将不得不比较 Math.abs(d1 - d2) < epsilon,其中 epsilon 是您的选择。大多数情况下,如果平等很重要,那么您已经做错了,一开始就不应该使用 double

注意:在表示金钱时,您不希望四舍五入,因此切勿对这些使用双精度值。相反,弄清楚原子银行单位是什么(美元、欧分、日元、比特币的 satoshis 等),并将其存储为 long。您存储 $4.52 和 long x = 452;,而不是 double x = 4.52;.