关于Java:浮点运算不能产生精确结果

Floating point arithmetic not producing exact results

本问题已经有最佳答案,请猛点这里访问。

我需要在Java中做一些浮点运算,如下面的代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class TestMain {
    private static Map<Integer, Double> ccc = new HashMap<Integer, Double>() {
      { put(1, 0.01); put(2, 0.02); put(3, 0.05); put(4, 0.1); put(6, 0.2);
        put(10, 0.5); put(20, 1.0); put(30, 2.0); put(50, 5.0); put(100, 10.0);
      }
    };

    Double increment(Double i, boolean up) {
        Double inc = null;

        while (inc == null) {
            inc = ccc.get(i.intValue());

            if (up)
                --i;
            else
                ++i;
        }
        return inc;
    }

    public static void main(String[] args) {
        TestMain tt = new TestMain();

        for (double i = 1; i < 1000; i += tt.increment(i, true)) {
            System.out.print(i +",");
        }
    }
}

这是为了模拟betfair spinner小部件输出的值的范围。

Java中的浮点运算似乎引入了一些意想不到的错误。例如,我得到的是2.180000000000001而不是2.18。浮点数有什么用?你不能相信它们的算术结果?我怎样才能解决这个问题?


如果需要精确的十进制值,应该使用java.math.BigDecimal。然后阅读"每一个计算机科学家应该知道的关于浮点运算的知识",了解你为什么得到这些结果。

(我有一篇以.NET为中心的文章,您可能会发现它更容易阅读,当然也更短。Java和.NET之间的差异与理解这个问题的目的无关。


浮点数使用的是二进制分数而不是十进制分数。也就是说,你习惯于用十分位、百分位、千分位等组成的小数。d1/10+d2/100+d3/1000…但浮点数是二进制的,所以它们有半位数、四分之一位数、八位数等。d1/2+d2/4+d3/8…

许多小数不能精确地用任何有限个二进制数字表示。例如,1/2不是问题:在十进制中是.5,在二进制中是.1。3/4是十进制的.75,二进制的.11。但1/10是一个干净的.1十进制,但在二进制中是.0001100110011…"0011"永远重复。由于计算机只能存储有限数量的数字,因此在某些情况下,必须将其切掉,因此答案并不精确。当我们将输出转换回十进制时,会得到一个看起来很奇怪的数字。

如乔恩·斯基特所说,如果你需要精确的十进制分数,使用bigdecimal。如果性能是一个问题,您可以滚动自己的小数。比如,如果你知道你总是想要精确的3个小数点,并且数字不会超过100万左右,你可以简单地用一个假定的3个小数点的int,当你做算术和写一个输出格式函数插入小数点在正确的位置时,做必要的调整。但99%的时间性能还不够大,不值得麻烦。


浮点数是不精确的,特别是因为它们是以二进制分数(1/2、1/4、1/8、1/16、1/32,…)而不是十进制分数(1/10、1/100、1/1000,…)工作的。只需定义你的感觉"足够接近",然后使用类似于Math.abs(a-b) < 0.000001的东西。


在哲学上,我想知道:现在大多数计算机CPU都内置了对整数算术和浮点算术的支持,但不支持十进制算术。为什么不?多年来我还没有写过一个应用程序,因为这个舍入问题,浮点数是可用的。您当然不能将其用于金额:没有人希望在销售收据上打印"42.3200003美元"的价格。没有会计会接受这样的说法:"我们可能会因为使用了二进制分数和舍入错误而在这里或那里少花一分钱。"

对于距离或温度这样的测量,浮球是很好的,因为没有"精确的答案",无论如何,你必须在某一点上精确到仪器的精度。我想对于那些在化学实验室里为计算机编程的人来说,浮点数是经常使用的。但是对于我们这些在商界的人来说,它们几乎是无用的。

早在我在大型机上编程的时候,IBM360系列的CPU就内置了对压缩十进制算法的支持。它们存储字符串,其中每个字节包含两个十进制数字,即前四位的值为0到9,第二个四位的值同上,CPU具有操作它们的算术功能。为什么英特尔不能这样做?然后Java可以添加一个"十进制"数据类型,我们不需要所有额外的垃圾。

当然,我不是说要废除彩车。只需加上小数。

哦,好吧,随着伟大的社会运动的进行,我不认为这是一个将产生许多流行的兴奋或在街上暴动。


通过使用格式化的输出,可以使程序的输出看起来更像您期望的那样。

http://java.sun.com/javase/6/docs/api/java/util/formatter.html

显然,底层浮点运算的工作原理仍然相同,但至少输出的可读性更高。

例如,要将结果四舍五入为两位小数:

1
System.out.print(String.format(".2f", i) +",");

您可以编写一些代码来计算机器上的epsilon。我相信STD::C++定义它,它根据你使用的其他方式来定义。

1
2
3
4
5
6
7
8
9
10
11
private static float calcEpsilonFloat() {
    float epsi = 1.0f;


    while ((float) (1.0 + (epsi / 2.0)) != 1.0)
    {
       epsi /= 2.0f;
    }

    return epsi;
}

我唯一一次担心的是Epsilon是在我比较阈值信号的时候。即使如此,我也不确定我是否真的需要担心它,根据我的经验,如果你关心epsilon,你可能需要先考虑一些其他的问题。


顺便说一句,您可以尝试使用这个函数来确保(不要过多的十进制数字)您的数字将被重新格式化,只保留您需要的小数。

http://pastebin.com/cacer0xk

n是一个有很多小数的数字(如Math.PI)。numberOfDecimals是您需要的最大小数位数(例如,2表示3.14,3表示3.151)。

从理论上讲,如果把一个负值放到numberOfDecmals上,它也会切断数字的下整数位。例如放置n=1588.22numberOfDecimals=-2,函数将返回1500.0

如果有什么问题,请告诉我。