【问题标题】:Why this weird output with truncate and BigDecimal?为什么这个奇怪的输出带有 truncate 和 BigDecimal?
【发布时间】:2014-12-06 10:10:31
【问题描述】:

我正在调用一个截断方法来截断双精度值,以便小数点后应该有一个数字(不四舍五入),
对于前。 truncate(123.48574) = 123.4.

我的截断方法是这样的

public double truncate (double x) {
    long y = (long) (x * 10);
    double z = (double) (y / 10);
    return z;
}

除了这个奇怪的输出之外,它几乎适用于所有值。

double d = 0.787456;
d = truncate(d + 0.1); //gives 0.8 as expected. Okay.

但是,

double d = 0.7;
d = truncate(d + 0.1); //should also give 0.8. But its giving 0.7 only. 
                       //Strange I don't know why?

事实上它适用于所有其他 0.1、0.2、0.3、0.4、0.5、0.6、-、0.8、0.9
我的意思是,例如,

double d = 0.8;
d = truncate(d + 0.1); //gives 0.9 as expected

我也试过BigDecimal。但是一样。不用找了。 这是它的代码。

double d = 0.7;
BigDecimal a = new BigDecimal(d + 0.1);
BigDecimal floored = a.setScale(1, BigDecimal.ROUND_DOWN);
double d1 = floored.doubleValue();
System.out.println(d1); //Still gives 0.7

再一次,真正的事实是它与Math.round 配合得很好。

public double roundUp1d (double d) {
    return Math.round(d * 10.0) / 10.0;
}

所以如果我打电话给roundUp1d(0.7 + 0.1),它会按预期给出 0.8。但我不希望这些值被四舍五入,所以我不能使用它。

0.7 有什么问题?

【问题讨论】:

  • 我已经测试过了,如果你的输入是0.7+0.1,那么它会被错误计算到0.799999999999999
  • 0.7 不能准确地用双精度表示,因此转换为双精度的 BigDecimal 可能仍会出现相同的“问题”。你知道基本问题吗?必填链接:What every programmer should know about Floating Point
  • 当您执行new BigDecimal(0.7).add(new BigDecimal(0.1)) 时会发生什么?您当前的 BigDecimal 代码不起作用,因为 d + 0.1 仍然是常规的 double 加法,它会为您提供浮点异常。 顺便说一句,它的拼写很奇怪而不是奇怪。
  • 啊,是的,感谢@Tom。应该是BigDecimal a = BigDecimal.valueOf(d).add(BigDecimal.valueOf(0.1));。这将在内部将双打转换为字符串(您不需要按照 Tom 的建议进行外部转换)。
  • 这对所有人都有效吗...? 是的! 这确实是使用 BigDecimal 的全部意义 :) 但你需要使用它正确。简单地在您的代码中提及它并不构成正确的用法。我会发布一个答案。我在尝试教学时也学到了:D

标签: java double bigdecimal


【解决方案1】:

(如果您对理论不感兴趣,请滚动到最后,您的代码可以修复)

原因很简单:如你所知,二进制系统只支持0s和1s

那么,让我们看看你的值,以及它们在二进制表示中的含义:

0.1 - 0.0001100110011001100110011001100110011001100110011001101
0.2 - 0.001100110011001100110011001100110011001100110011001101
0.3 - 0.010011001100110011001100110011001100110011001100110011
0.4 - 0.01100110011001100110011001100110011001100110011001101
0.5 - 0.1
0.6 - 0.10011001100110011001100110011001100110011001100110011
0.7 - 0.1011001100110011001100110011001100110011001100110011
0.8 - 0.1100110011001100110011001100110011001100110011001101
0.9 - 0.11100110011001100110011001100110011001100110011001101

这是什么意思? 0.1 是 1 的十分之一。在 十进制 系统中没什么大不了的,只需将分隔符移一位即可。但是在二进制中,您不能表示 0.1 - 导致小数点符号的每次移位都等于 *2/2 - 取决于方向。 (而且 10 不能分成 2 的 X 位移)

对于您想要除以 2 的倍数的值 - 您会得到一个精确的结果:

1/2 - 0.1
1/4 - 0.01
1/8 - 0.001
1/16- 0.0001
and so on.

因此尝试计算/10 是一个无限长结果,当值用完位时会被截断。

这就是说,这是计算机工作方式的限制,这样的值永远不能完全精确地存储。

站点注释:Patriot System 忽略了这一“事实”,导致它在运行数小时后变得无法使用,请参见此处:http://sydney.edu.au/engineering/it/~alum/patriot_bug.html


但是为什么它对除了 0.7 + 0.1 之外的所有东西都有效 - 你可能会问

如果您使用 0.8 测试您的代码 - 它可以工作 - 但不适用于 0.7 + 0.1

同样,在二进制中,这两个值都已经不精确了。如果将两个值相加,则结果 更不精确,导致错误的结果:

如果将 0.7 和 0.1(小数点分隔符后)相加,您会得到:

  0.101100110011001100110011001100110011001100110011001 1000
+ 0.000110011001100110011001100110011001100110011001100 1101
  ---------------------------------------------------------
  0.110011001100110011001100110011001100110011001100110 0101     

但是 0.8 会是

  0.110011001100110011001100110011001100110011001100110 1000

比较最后 4 位并注意,ADDITION 的结果“0.8”小于将 0.8 直接转换为二进制的结果。

猜猜看:

System.out.println(0.7 + 0.1 == 0.8); //returns false

在处理数字时,您应该为自己设置一个精度限制 - 并且始终相应地对数字进行四舍五入以避免此类错误(而不是截断!):

 //compare doubles with 3 decimals
 System.out.println((lim(0.7, 3) + lim(0.1, 3)) == lim(0.8, 3)); //true

 public static long lim(double d, int t){
     return Math.round(d*10*t);
 }

要修复您的代码:将其四舍五入到 4 位,然后在第一个数字之后截断:

public static double truncate(double x){
   long y = (long)((Math.round(x*10000)/10000.0)*10);
   double z = (double)y/10;
   return z;
}

System.out.println(truncate(0.7+0.1)); //0.8
System.out.println(truncate(0.8)); //0.8

这仍会根据需要截断,但可以确保 0.69999 在截断之前四舍五入为 0.7。您可以根据应用程序的需要设置精度。 10、20、30、40位?

其他值仍将保持正确,因为像 0.58999 这样的值只会四舍五入为 0.59 - 所以仍会截断为 0.5 而不会四舍五入为 0.6

【讨论】:

  • 固定代码是否适用于所有此类浮点数异常?因为如果它可以发生在 0.7 上,那么它可以发生在任意浮点数上。我的意思是它的不可预测性。
  • @user1612078 它将根据您选择的精度解决所有数字的问题。如果您决定忽略小数点分隔符后面 20 位的任何内容,那么如果从分隔符后面 21 位开始的数字与另一个数字不同,则会产生错误的结果。
【解决方案2】:

这不是由您的程序或 Java 引起的。简单地说,浮点数在设计上是不准确的。您不知道哪些数字会不准确,但有些会(在您的情况下为 0.7)。关于这个主题的众多文章之一:http://effbot.org/pyfaq/why-are-floating-point-calculations-so-inaccurate.htm

底线:永远不要相信双 0.7 真的是 0.7。

【讨论】:

  • 但我对Math.round 进行了同样的尝试,它给了我预期的结果。查看我更新的帖子。
  • 在最后一次尝试中,您仍然得到0.699999..,但println 也会对输出进行四舍五入。
  • 这是一个非常普遍的问题,我从来没有找到最终的解决方案。还有一些可能对您有帮助的建议:mydevelopedworld.wordpress.com/2013/05/19/…stackoverflow.com/questions/16133520/…
  • 你的意思是 0.7,而不是 7.0?
【解决方案3】:

浮点数的设计本质上是不准确的。这里的其他答案已经解释了这种不准确性背后的理论。强烈建议您改用BigDecimalBigInteger

在我的回答中,我想详细说明您如何错误地使用BigDecimal 以及如何正确使用它。不要错误地将这些类简单地用作浮点计算的包装器。在您当前的代码中:

BigDecimal a = new BigDecimal(d + 0.1);

即使您在此处尝试使用BigDecimal,您仍然在使用常规浮点计算执行加法。这和做的完全一样:

double d_a = d + 0.1; //0.799999999 ad infinitum
BigDecimal a = new BigDecimal(d_a);

要利用BigX类的准确性,必须使用它们自己的计算方法,以及valueOf静态方法(不是构造函数):

BigDecimal a = BigDecimal.valueOf(d).add( BigDecimal.valueOf(0.1) );

这里,创建了两个BigDecimal对象来精确匹配0.70.1,然后使用add方法计算它们的和并产生第三个@ 987654332@ 对象(确切地说是 0.8)。

使用valueOf 静态方法而不是构造函数可确保创建的BigDecimal 对象代表精确的double 值,因为它在转换为字符串时显示(0.7 作为字符串是“0.7”),而不是计算机存储来表示它的近似值(计算机将 0.7 存储为 0.699999999 ad infinitum)。

【讨论】:

    【解决方案4】:

    “问题”在于使用像doublefloat 这样的浮点数。这些数字在内部使用以 2 为底的分数来近似大范围的数字,从非常小到非常大。

    例如,0.5 可以表示为1/20.75 可以表示为1\2 + 1\4

    但是,正如您发现的那样,您不能总是轻松地在以 10 为底的分数和以 2 为底的分数之间进行转换。

    以 10 为底的 0.7 等于 7/10,在以 2 为底的分数中,这变得非常困难。

    这就像试图用以 10 为底的十进制数准确表示 1/3,这在以 3 为底的小数中非常容易,只要有足够的小数位可用,您就可以获得非常接近的近似值,但是您无法以 10 进制数字准确表示 1/3

    【讨论】:

      【解决方案5】:

      关键点是 float 和 double 旨在使用舍入原理。如果计算的结果不能准确表示,则结果将尽可能接近准确答案,无论这是否使其小于或大于精确。

      0.7 + 0.1的问题是,0.79999999999999993338661852249060757458209991455078125,最接近的表示值,以的0.6999999999999999555910790149937383830547332763671875和0.1000000000000000055511151231257827021181583404541015625,最接近于0.7和0.1表示数的总和,大于0.8略小。 P>

      有几种可能的解决方案。如果小数截断是问题的核心,并且比性能和空间更重要,请使用 BigDecimal。如果不是,请考虑在截断之前添加一个小的调整以解决此影响。实际上,将稍微小于 0.8 的数字视为大于或等于 0.8。这是可行的,因为double 算法引入的差异通常比现实世界中出现的差异要小得多。

      【讨论】:

        【解决方案6】:

        Java 使用 IEEE 754 格式对双精度值进行编码。

        这意味着每个数字都是尽可能准确的近似值。

        0.7 最好近似为 0.6999999999999999999999999

        看看这个:http://www.binaryconvert.com/result_double.html?decimal=048046055

        要解决您的问题,您可以尝试乘以 10.0 并相应地处理您的值吗?

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2015-05-31
          • 1970-01-01
          • 2010-11-12
          • 1970-01-01
          • 2020-04-11
          • 1970-01-01
          • 1970-01-01
          • 2011-04-07
          相关资源
          最近更新 更多