【问题标题】:Unpredictable double [duplicate]不可预测的双重[重复]
【发布时间】:2012-03-28 20:49:20
【问题描述】:

可能重复:
Double precision problems on .NET
Double calculation producing odd result

我知道双值 0.2 的内部表示类似于 0.199999。但是下面的代码仍然让我感到困惑。

代码:

public static void main(String[] args) {
    double d= 0.3d;
    double f= 0.1d;
    System.out.println(d+f);
    System.out.println(d*f);
    System.out.println(d);
    System.out.println(f);
    System.out.println(d-f);
    System.out.println(d/f);
    System.out.println((d-f)*(d-f));
}

输出:

0.4
0.03
0.3
0.1
0.19999999999999998
2.9999999999999996
0.039999999999999994

究竟发生了什么?加法,乘法很好,但减法,除法不是。谁能详细说明为什么加法不同于减法

【问题讨论】:

  • 必须是计算中被问和回答最多的问题之一。 :P
  • ... 在 Stackoverflow 上
  • 看来你问题的第一句话就是答案!
  • @assylias 我的问题是为什么加法不同于减法? :(

标签: java math double operation


【解决方案1】:

如果您迫切需要精确,请使用 BigDecimal。

public static void main(String[] args) {
    BigDecimal d = BigDecimal.valueOf(0.3d);
    BigDecimal f = BigDecimal.valueOf(0.1d);
    System.out.println(d.add(f));
    System.out.println(d.multiply(f));
    System.out.println(d);
    System.out.println(f);
    System.out.println(d.subtract(f));
    System.out.println(d.divide(f));
    System.out.println((d.subtract(f)).multiply(d.subtract(f)));
}

输出

0.4
0.03
0.3
0.1
0.2
3
0.04

或者对你的结果进行四舍五入,DecimalFormat 会很好地使用 # 符号表示仅在必要时显示小数

    double d = 0.3d;
    double f = 0.1d;
    DecimalFormat format = new DecimalFormat("#.##");
    System.out.println(format.format(d + f));
    System.out.println(format.format(d * f));
    System.out.println(format.format(d));
    System.out.println(format.format(f));
    System.out.println(format.format(d - f));
    System.out.println(format.format(d / f));
    System.out.println(format.format((d - f) * (d - f)));

输出

0.4
0.03
0.3
0.1
0.2
3
0.04

【讨论】:

    【解决方案2】:

    简短的回答是浮点运算存在表示错误和舍入错误。 toString()“知道”表示错误,因此如果没有舍入错误,您将看不到它。但是,如果舍入误差太大,你会这样做。

    解决方案是使用 BigDecimal 或对结果进行四舍五入。


    如果您使用 BigDecimal,它将显示您真正拥有的确切值。

    double d = 0.3d;
    double f = 0.1d;
    System.out.println("d= " + new BigDecimal(d));
    System.out.println("f= " + new BigDecimal(f));
    System.out.println("d+f= " + new BigDecimal(d + f));
    System.out.println("0.4= " + new BigDecimal(0.4));
    System.out.println("d*f= " + new BigDecimal(d * f));
    System.out.println("0.03= " + new BigDecimal(0.03));
    System.out.println("d-f= " + new BigDecimal(d - f));
    System.out.println("0.2= " + new BigDecimal(0.2));
    System.out.println("d/f= " + new BigDecimal(d / f));
    System.out.println("(d-f)*(d-f)= " + new BigDecimal((d - f) * (d - f)));
    

    打印

    d= 0.299999999999999988897769753748434595763683319091796875
    f= 0.1000000000000000055511151231257827021181583404541015625
    d+f= 0.40000000000000002220446049250313080847263336181640625
    0.4= 0.40000000000000002220446049250313080847263336181640625
    d*f= 0.0299999999999999988897769753748434595763683319091796875
    0.03= 0.0299999999999999988897769753748434595763683319091796875
    d-f= 0.1999999999999999833466546306226518936455249786376953125
    0.2= 0.200000000000000011102230246251565404236316680908203125
    d/f= 2.999999999999999555910790149937383830547332763671875
    (d-f)*(d-f)= 0.03999999999999999389377336456163902767002582550048828125
    

    您会注意到0.1 有点太大,0.3 有点太小。这意味着当您将它们相加或相乘时,您会得到一个大约正确的数字。但是,如果您使用减法或除法,则错误会累积,并且您会得到一个与表示的数字相差太远的数字。

    即您可以看到 0.1 和 0.3 产生与 0.4 相同的值,而 0.3 - 0.1 不会产生与 0.2 相同的值


    顺便说一句,在不使用 BigDecimal 的情况下对答案进行四舍五入

    System.out.printf("d-f= %.2f%n", d - f);
    System.out.printf("d/f= %.2f%n", d / f);
    System.out.printf("(d-f)*(d-f)= %.2f%n", (d - f) * (d - f));
    

    打印

    d-f= 0.20
    d/f= 3.00
    (d-f)*(d-f)= 0.04
    

    System.out.println("d-f= " +  roundTo6Places(d - f));
    System.out.println("d/f= " +  roundTo6Places(d / f));
    System.out.println("(d-f)*(d-f)= " +  roundTo6Places((d - f) * (d - f)));
    
    public static double roundTo6Places(double d) {
        return (long)(d * 1e6 + (d > 0 ? 0.5 : -0.5)) / 1e6;
    }
    

    打印

    System.out.println("d-f= " +  roundTo6Places(d - f));
    System.out.println("d/f= " +  roundTo6Places(d / f));
    System.out.println("(d-f)*(d-f)= " +  roundTo6Places((d - f) * (d - f)));
    

    舍入消除了舍入误差(只留下了 toString 旨在处理的表示错误)


    0.1前后可以表示的值可以计算为

    double before_f = Double.longBitsToDouble(Double.doubleToLongBits(f) - 1);
    System.out.println("The value before 0.1 is " + new BigDecimal(before_f) + " error= " + BigDecimal.valueOf(0.1).subtract(new BigDecimal(before_f)));
    System.out.println("The value after 0.1 is  " + new BigDecimal(f) + " error= " + new BigDecimal(f).subtract(BigDecimal.valueOf(0.1)));
    

    打印

    The value before 0.1 is 0.09999999999999999167332731531132594682276248931884765625 
        error= 8.32667268468867405317723751068115234375E-18
    The value after 0.1 is  0.1000000000000000055511151231257827021181583404541015625 
        error= 5.5511151231257827021181583404541015625E-18
    

    【讨论】:

    • 非常感谢 :) 为什么 0.1 比实际值多而少 0.3?
    • 0.1 稍微多一些,因为这是最接近的可表示值。对于0.3,最接近的可表示值略小。
    • 天啊!为什么会这样:(最接近实际上是什么意思?
    • 查看我上次编辑的最接近 0.1 double 可以表示的两个值。你可以看到它使用的是最接近的。即它的错误最少。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-07-10
    • 1970-01-01
    • 1970-01-01
    • 2018-03-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多