双包装类的 .equals() 方法是否可以查找浮点数的相等性?

Hat*_*Hat 0 java floating-point

我知道对于原始浮点类型(浮点数和双精度数),您不应该直接通过==. 但是如果您使用 double 的包装类怎么办?会像

Double a = 5.05;
Double b = 5.05; 
boolean test = a.equals(b);
Run Code Online (Sandbox Code Playgroud)

正确比较两个值?

rzw*_*oot 6

== 您需要充分理解比较不是一个好主意的原因。如果不理解,你就只能在黑暗中摸索。

让我们谈谈计算机(和替身)如何工作。

想象你进入一个房间;它有 3 个灯开关,否则它是裸露的。你将进入房间,你可以摆弄开关,但随后你必须离开。我稍后进入房间,可以查看开关。

你能传达多少信息?

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

当计算机存储双精度数时,计算机的工作原理与此完全相同。除了 3 个开关之外,您还有 64 个开关。这意味着2^64您可以用一个 来传达不同的状态double,这就是大量的状态:这是一个 19 位数字。

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

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

所以双打实际上并不是这样工作的。相反,有人花了一些功夫,在一个大房间里挂了一条巨大的数字线,从负无穷到正无穷,并向2^64这条线扔飞镖。他们落在的数字是“祝福数字”——这些数字可以用一个double值来表示。这确实意味着任意两支飞镖之间存在无限数量的数字,因此无法表示。飞镖并不是完全随机的:越接近 0,飞镖就越密集。一旦超过大约2^52左右,2 个省道之间的距离甚至会超过 1.0。

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

这就是问题所在:你可以进行数学运算:

double x = 0.1 + 0.2;
Run Code Online (Sandbox Code Playgroud)

然后做:

double y = 0.9 - 0.8 + 0.15 + 0.05;
Run Code Online (Sandbox Code Playgroud)

我们人类会立即注意到这一点,x并且y自然是相同的。但对于计算机来说却并非如此——因为四舍五入到最接近的神圣数字,有可能是x0.29999999999999999785并且y0.300000000000000000012

因此,我们在使用时会遇到四个关键方面double(或者float在任何方面都更糟糕,永远不要使用这些):

  • 如果您需要绝对精度,则根本不要使用它们。
  • 打印它们时,始终将它们向下舍入。System.out.println 可以开箱即用地执行此操作,但您实际上应该使用.printf("%.5f")或类似的方法:选择您需要的数字位数。
  • 请注意,错误会复合,并且随着距离 1.0 越来越远,情况会变得更糟。
  • 不要与 进行比较==,而是始终使用 delta-compare:“如果两个数字彼此相差在 0.0000000001 以内,则认为它们相等”的概念。

不存在通用的魔法增量值;这取决于你的精度需求,你离 1.0 有多远,等等。因此,只问计算机:嘿,把这个东西弄清楚,我只是想知道这两个双精度数是否相等是不可能的。唯一不需要您输入它们“有多接近”的可用定义是纯粹完美的概念:只有当它们完全相同时它们才是相等的。在上面那个简单的例子中,这个定义会让你失败。Double.equals如果您使用代替double == double或任何其他实用程序类,则没有丝毫区别。

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

注意:当表示金钱时,您不希望出现不可预测的舍入,因此切勿使用双精度数。相反,要弄清楚原子银行单位是什么(美元、欧分、日元、比特币的聪等),并将其存储为长整型。您存储 4.52 美元long x = 452;,而不是作为double x = 4.52;.