为什么 Math.floor(1.23456789 * 1e8) / 1e8) return 1.23456788

Why does Math.floor(1.23456789 * 1e8) / 1e8) return 1.23456788

Math.floor(1.23456789 * 1e8) / 1e8)

returns:

1.23456788

考虑到这一点很奇怪:

Math.floor(1.23456789 * 1e9) / 1e9)

returns:

1.23456789

还有

Math.floor(1.23456799 * 1e8) / 1e8)

returns:

1.23456799

知道为什么会发生这种情况以及如何避免这种情况吗?

浮点运算不精确。除了浮点精度有限这一事实之外,转换为二进制再返回十进制可能会导致问题(因为二进制不能完美地表示小数,反之亦然)。

您可以使用 BigDecimal 来获得完美的数学,但速度要慢得多。只有在进行大量计算时才会注意到这一点。

编辑: Here's a BigDecimal tutorial.

正如 Daniel Centore 的回答,双精度值不精确。以下是双精度数字的实际值列表(至 20 位)。显示的是 1.23456789 的两个最接近的编码;第一个更近。乘以 1e8 时,乘法不会向上舍入。乘以 1e9 时,四舍五入为一个精确的整数值。

1.23456789     => 1.2345678899999998901 hex 3ff3c0ca4283de1b
1.23456789     => 1.2345678900000001121 hex 3ff3c0ca4283de1c
1.23456789*1e8 => 123456788.99999998510 hex 419d6f3453ffffff
1.23456789*1e9 => 1234567890.0000000000 hex 41d26580b4800000

Java double 类型(几乎)符合称为 IEEE-754 的浮点数国际标准。可以用这种类型表示的数字都有一个共同点——它们的二进制表示最多在 53 位有效数字后终止。

大多数具有终止十进制表示的数字没有终止二进制表示,这意味着没有 double 可以准确地存储它们。当您在 Java 中写入 double 文字时,存储在 double 中的值通常不是您在文字中写入的数字 - 相反,它将是最接近的可用 double.

文字1.23456789也不例外。 It falls neatly between two numbers that can be stored as double, and the exact values of those two double numbers are 1.2345678899999998900938180668163113296031951904296875 and 1.23456789000000011213842299184761941432952880859375。规则是选择这两个数字中较接近的数字,因此文字 1.23456789 存储为 1.2345678899999998900938180668163113296031951904296875.

文字 1E8 可以完全存储为 double,因此第一个示例中的乘法是 1.2345678899999998900938180668163113296031951904296875 乘以 100000000 ,当然是 123456788.99999998900938180668163113296031951904296875。此数字不能完全存储为 double。它下面最近的 double123456788.99999998509883880615234375,它上面最近的 double123456789。但是,它下面的double更接近,所以Java表达式1.23456789 * 1E8的值实际上是123456788.99999998509883880615234375。当您对这个数字应用 Math.floor 方法时,结果正好是 123456788.

文字 1E9 可以完全存储为 double,因此第二个示例中的乘法是 1.2345678899999998900938180668163113296031951904296875 乘以 1000000000 ,当然是 1234567889.9999998900938180668163113296031951904296875。此数字不能完全存储为 double。它下面最近的 double1234567889.9999997615814208984375,它上面最近的双精度是 1234567890。但是这次,它上面的double更接近了,所以Java表达式1.23456789 * 1E9的值恰好是1234567890,它没有被Math.floor 方法。

你问题的第二部分是如何避免这种情况。好吧,如果您想对带有终止十进制表示的数字进行精确计算,则不得将它们存储在 double 变量中。相反,你可以使用 BigDecimal class,它可以让你做这样的事情

BigDecimal a = new BigDecimal("1.23456789");
BigDecimal b = new BigDecimal("100000000");
BigDecimal product = a.multiply(b);

并且数字准确无误。