【问题标题】:How to resolve a Java Rounding Double issue [duplicate]如何解决 Java Rounding Double 问题 [重复]
【发布时间】:2010-09-15 19:38:06
【问题描述】:

似乎减法触发了某种问题,结果值是错误的。

double tempCommission = targetPremium.doubleValue()*rate.doubleValue()/100d;

78.75 = 787.5 * 10.0/100d

double netToCompany = targetPremium.doubleValue() - tempCommission;

708.75 = 787.5 - 78.75

double dCommission = request.getPremium().doubleValue() - netToCompany;

877.8499999999999 = 1586.6 - 708.75

得到的预期值为 877.85。

应该怎样做才能保证计算正确?

【问题讨论】:

    标签: java math rounding


    【解决方案1】:

    查看对this question 的回复。本质上,您所看到的是使用浮点运算的自然结果。

    如果您觉得这样做很舒服,您可以选择一些任意精度(输入的有效数字?)并将结果四舍五入。

    【讨论】:

      【解决方案2】:

      任何时候你用双打进行计算,都可能发生这种情况。此代码将为您提供 877.85:

      双重答案 = Math.round(dCommission * 100000) / 100000.0;

      【讨论】:

      • 最好除以 100000.0 而不是 100000; Math.round 返回一个 long,因此您将使用整数除法。
      【解决方案3】:

      要控制浮点运算的精度,您应该使用java.math.BigDecimal。阅读 John Zukowski 的The need for BigDecimal 了解更多信息。

      根据您的示例,最后一行使用 BigDecimal 如下所示。

      import java.math.BigDecimal;
      
      BigDecimal premium = BigDecimal.valueOf("1586.6");
      BigDecimal netToCompany = BigDecimal.valueOf("708.75");
      BigDecimal commission = premium.subtract(netToCompany);
      System.out.println(commission + " = " + premium + " - " + netToCompany);
      

      这会产生以下输出。

      877.85 = 1586.6 - 708.75
      

      【讨论】:

      • 您无法完全“避免”浮点算术错误。用于表示数字的位数总是有限的。您所能做的就是使用精度更高(位)的数据类型。
      • 确实如此。我将编辑我的答案以更准确地反映 BigDecimal 的使用。
      • 我将添加一个注释,BigDecimal division 需要与 +,-,* 稍有不同,因为默认情况下,如果它无法返回,它会抛出异常一个精确的值(例如 1 / 3)。在类似的情况下,我使用了:BigDecimal.valueOf(a).divide(BigDecimal.valueOf(b), 25, RoundingMode.HALF_UP).doubleValue(),如果精度的最大位数为 25(超过 double 结果所需的精度) )。
      【解决方案4】:

      保存美分而不是美元,输出时只需将其格式化为美元。这样您就可以使用不受精度问题影响的整数。

      【讨论】:

      • 这里唯一的问题是,在这个过程的早期可能会损失一小部分 - 这可能对金融应用程序不利。如果这是一个问题,您可以节省 1/10 美分或任何您需要的精度。
      • 如果我能回到过去,给过去的自己一个“窍门”,就是这样。价格一分钱! (或 1e-3 或 1e-6 —— 作为 int 仍然适合 2+ 百万美元)
      【解决方案5】:

      另一个例子:

      double d = 0;
      for (int i = 1; i <= 10; i++) {
          d += 0.1;
      }
      System.out.println(d);    // prints 0.9999999999999999 not 1.0
      

      改用 BigDecimal。

      编辑:

      另外,只是指出这不是“Java”舍入问题。其他语言展览 类似(尽管不一定一致)的行为。 Java 至少保证了这方面的一致行为。

      【讨论】:

        【解决方案6】:

        正如前面的答案所述,这是进行浮点运算的结果。

        正如之前的海报所建议的,当您进行数字计算时,请使用java.math.BigDecimal

        但是,使用BigDecimal 有一个问题。当您从双精度值转换为BigDecimal 时,您可以选择使用新的BigDecimal(double) 构造函数或BigDecimal.valueOf(double) 静态工厂方法。使用静态工厂方法。

        double 构造函数将double 的整个精度转换为BigDecimal,而静态工厂有效地将其转换为String,然后将其转换为BigDecimal

        当您遇到那些细微的舍入错误时,这变得相关。一个数字可能显示为 0.585,但在内部它的值是 '0.58499999999999996447286321199499070644378662109375'。如果你使用BigDecimal 构造函数,你会得到不等于 0.585 的数字,而静态方法会给你一个等于 0.585 的值。

        双值 = 0.585; System.out.println(new BigDecimal(value)); System.out.println(BigDecimal.valueOf(value));

        在我的系统上给出

        0.58499999999999996447286321199499070644378662109375 0.585

        【讨论】:

        • 这个问题我遇到过很多次了,真的很烦!
        • 请注意,原因是静态方法使用 UNLIMITED MathContext 实际上意味着:new BigDecimal(value, new MathContext(0, RoundingMode.HALF_UP)) ;)
        【解决方案7】:

        我将上面的例子修改如下:

        import java.math.BigDecimal;
        
        BigDecimal premium = new BigDecimal("1586.6");
        BigDecimal netToCompany = new BigDecimal("708.75");
        BigDecimal commission = premium.subtract(netToCompany);
        System.out.println(commission + " = " + premium + " - " + netToCompany);
        

        这样你就避免了使用字符串开头的陷阱。 另一种选择:

        import java.math.BigDecimal;
        
        BigDecimal premium = BigDecimal.valueOf(158660, 2);
        BigDecimal netToCompany = BigDecimal.valueOf(70875, 2);
        BigDecimal commission = premium.subtract(netToCompany);
        System.out.println(commission + " = " + premium + " - " + netToCompany);
        

        我认为这些选项比使用双打更好。在 webapps 中,数字无论如何都是以字符串开头的。

        【讨论】:

          【解决方案8】:

          到目前为止,Java 中最优雅、最有效的方式:

          double newNum = Math.floor(num * 100 + 0.5) / 100;
          

          【讨论】:

            【解决方案9】:
            double rounded = Math.rint(toround * 100) / 100;
            

            【讨论】:

              【解决方案10】:

              虽然您不应该使用双精度数进行精确计算,但如果您无论如何都要对结果进行四舍五入,以下技巧对我有帮助。

              public static int round(Double i) {
                  return (int) Math.round(i + ((i > 0.0) ? 0.00000001 : -0.00000001));
              }
              

              例子:

                  Double foo = 0.0;
                  for (int i = 1; i <= 150; i++) {
                      foo += 0.00010;
                  }
                  System.out.println(foo);
                  System.out.println(Math.round(foo * 100.0) / 100.0);
                  System.out.println(round(foo*100.0) / 100.0);
              

              哪些打印:

              0.014999999999999965
              0.01
              0.02
              

              更多信息:http://en.wikipedia.org/wiki/Double_precision

              【讨论】:

              • 由于 .5 可以精确地表示为二进制分数,唯一会产生影响的情况是值合法不同。
              • @Michael:我的例子有缺陷。然而问题依然存在。因此我添加了一个示例。
              • 问题仍然存在,当值真的类似于 0.01499999999 时,您的方法将返回错误的结果 - 从根本上说,这是解决此类问题的错误方法。
              【解决方案11】:

              这很简单。

              使用 %.2f 运算符进行输出。问题解决了!

              例如:

              int a = 877.8499999999999;
              System.out.printf("Formatted Output is: %.2f", a);
              

              以上代码的打印输出为: 877.85

              %.2f 运算符定义只应使用两位小数。

              【讨论】:

              • 这不能解决尾数逼近和舍入问题!你看到的不是你所做的!
              • 如果您四舍五入到小数点后 1 位,那么您将得到 877.8。这可能不是你想要的。
              【解决方案12】:

              这是一个有趣的问题。

              Timons 回复背后的想法是您指定一个 epsilon,它表示合法双精度可以达到的最小精度。如果您在应用程序中知道您永远不需要低于 0.00000001 的精度,那么他的建议足以获得更接近事实的更精确结果。在他们预先知道其最大精度的应用程序中很有用(例如货币精度的金融等)

              但是,试图将其舍入的根本问题是,当您除以一个因子来重新调整它时,实际上会引入另一种精度问题的可能性。任何对双精度数的操作都可能引入频率不同的不精确问题。特别是如果您尝试以非常有效的数字进行舍入(因此您的操作数

              System.out.println(round((1515476.0) * 0.00001) / 0.00001);
              

              将产生1499999.9999999998,这里的目标是以 500000 为单位进行舍入(即我们想要 1500000)

              事实上,完全确定您已经消除了不精确性的唯一方法是通过 BigDecimal 进行缩减。例如

              System.out.println(BigDecimal.valueOf(1515476.0).setScale(-5, RoundingMode.HALF_UP).doubleValue());
              

              混合使用 epsilon 策略和 BigDecimal 策略可以让您更好地控制精度。 epsilon 的想法让您非常接近,然后 BigDecimal 将消除由之后重新缩放引起的任何不精确性。虽然使用 BigDecimal 会降低应用程序的预期性能。

              有人向我指出,在某些用例中,当您可以确定没有最终除法可以重新引入错误的输入值时,使用 BigDecimal 重新调整它的最后一步并不总是必要的。目前我不知道如何正确确定这一点,所以如果有人知道如何确定,我会很高兴听到它。

              【讨论】:

                【解决方案13】:

                最好使用JScience,因为 BigDecimal 相当有限(例如,没有 sqrt 函数)

                double dCommission = 1586.6 - 708.75;
                System.out.println(dCommission);
                > 877.8499999999999
                
                Real dCommissionR = Real.valueOf(1586.6 - 708.75);
                System.out.println(dCommissionR);
                > 877.850000000000
                

                【讨论】:

                • 感谢库!但是对于简单的事情是不是有点超载了?
                • @Benj 你可以对番石榴说同样的话;)
                猜你喜欢
                • 2023-03-23
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2022-01-11
                • 2019-06-07
                相关资源
                最近更新 更多