浮点运算无法产生精确结果

37

我需要在Java中执行一些浮点数运算,代码如下所示:

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微件输出的值范围。

在Java中浮点算术似乎会引入一些意外的误差。例如,我得到的是2.180000000000001而不是2.18。如果你不能信任对它们执行的算术运算的结果,浮点数有什么用处?我该如何解决这个问题?


45
欢迎来到计算机科学领域。 :) - BobbyShaftoe
1
查看此问题,虽然用的措辞不同,但得出的答案是相同的。https://dev59.com/y3NA5IYBdhLWcg3wHp6B - Yishai
问题可以重新表述为:不精确的算术无法产生精确的值。 当然没错! - Ingo
不是要不公平,但程序员应该多了解一些类型表示方面的知识,因为这在每个严肃的计算机科学课程中都很重要。在我的大学里,第一次考试就是手动计算IEEE数字... :) - ingconti
7个回答

38

6
请记住,BigDecimal 比普通浮点运算要慢很多。 - Anthony Mills
24
@Anthony:没错,但如果你确实需要准确的小数值,那么慢而正确比快而错误更好。 - Jon Skeet
4
在这种情况下,您也可以使用整数来表示分。您需要记住218代表2.18,并且需要进行额外的打印工作。 - starblue
4
我热爱数学,但是我自己也没能读完那篇文章。我希望人们停止分享它——除非你想读一本小书来理解浮点数的工作原理,否则它并不是一个好的参考资料。 - BlueRaja - Danny Pflughoeft
1
http://floating-point-gui.de/ 对于解释浮点数问题有一个相对简单和更实用的方法。 - Joachim Sauer
显示剩余4条评论

11

浮点数使用二进制小数而不是十进制小数。也就是说,你习惯了由十分位,百分位,千分位等组成的十进制小数,如 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" 会永远重复。由于计算机只能存储有限数量的数字,因此在某些时候必须将其截断,所以答案不精确。当我们在输出时转换回十进制时,会得到一个奇怪的数字。

正如Jon Skeet所说,如果您需要精确的十进制小数,请使用BigDecimal。如果性能是个问题,您可以自己编写十进制小数。例如,如果您知道您始终需要恰好3个小数位,并且数字不会超过一百万左右,您可以简单地使用 int 假定有三位小数,并在进行算术运算和编写输出格式函数时进行必要的调整以插入小数点。但99%的情况下性能并不值得费这个劲。


3

浮点数不够精确,特别是因为它们使用二进制分数(1/2、1/4、1/8、1/16、1/32等),而不是十进制分数(1/10、1/100、1/1000等)。只需定义您认为“足够接近”的内容,并使用类似于Math.abs(a-b) < 0.000001的东西。


如需更全面的处理(其中之一),请参见http://prokutfaq.byethost15.com/FloatCompare。 - Anthony Mills

3
在哲学层面上,我想知道:大多数计算机CPU今天都内置支持整数算术和浮点算术,但没有支持十进制算术。为什么呢?我已经好几年没有写过使用浮点数的应用程序了,因为存在舍入问题。你肯定不能将它们用于货币金额:没有人想在销售收据上打印出“42.3200003美元”的价格。没有会计师会接受“我们可能会有一两分钱的差错,因为我们使用二进制小数并且存在舍入误差”的说法。
对于测量,如距离或温度,浮点数是可以的,因为不存在“精确答案”,你必须在某个时候四舍五入到你的仪器的精度。我想对于在化学实验室中编程的人来说,浮点数是例行使用的。但对于我们商界的人来说,它们基本上是无用的。
回到那些远古时代,当我在大型机上编程时,IBM 360系列CPU内置支持压缩十进制算术。它们存储字符串,其中每个字节包含两个十进制数字,即第一个四位表示从0到9的值,第二个四位也是如此,并且CPU有算术函数来操作它们。为什么英特尔不能做类似的事情呢?然后Java可以添加“十进制”数据类型,我们就不需要所有这些额外的东西了。
当然,我并不是说要废除浮点数。只需添加十进制数即可。
哦,好吧,作为重大社会运动,我不认为这是一个会激起很多普通人兴奋或在街头骚乱的问题。

1
顺带一提,为了速度,现代十进制支持不再使用BCD码。现在64位和甚至128位的寄存器已经很普遍了,因此它可能会用到定长的十进制浮点数(DFP),整个值可以放入一个寄存器中。IEEE-754现在已经包括了DFP,英特尔十进制浮点数数学库和IBM的POWER6都有专门支持它的硬件。 (而定点小数可以高效地在现有的整数上实现。)由于AMD和英特尔是大众市场,与詹姆斯列出的东西相比,它们可能不认为这是一个具有市场价值的功能。 - Andrew Janke
@AndrewJanke 我并不是说BCD字符串是实现它的最佳或唯一方法,只是要注意这是旧CPU上存在的一种方法。也许在整数之上在软件中实现十进制,然后利用芯片空间进行改进缓存或其他操作,会比使用芯片空间来实现十进制算术得到更好的结果。我不敢自称了解足够的CPU设计知识来进行智能讨论。我只想能够编写使用小数与使用整数和浮点数一样简单和自然的程序,无论你如何帮助我! - Jay
不是想引发争论,只是觉得你会对当前十进制支持的情况感兴趣,并且需要注意在这个方向上已经取得了一些进展,所以你的梦想并没有完全破灭。我也希望Java有decimal原语(或者至少有运算符重载和非引用对象类型,这样你就可以构建自己的类型);我会一直使用它们,硬件DFP支持对我来说将是一个巨大的胜利。(在C++中,这并不重要:你现在可以在Intel DFP库的基础上自己编写十进制类型,从用户的角度来看,它看起来与float一样自然。) - Andrew Janke
1
@Jay 只是顺便说一下,C# 中已经内置了这个功能,通过 Decimal 类型来实现。你会遇到使用 Double 和 Float 时的常见问题,但 Decimal 类型不会出现舍入误差。为什么不是每种编程语言都有类似 Decimal 的功能呢?这个问题我也说不准确。 - kingfrito_5005
因为你可以只计算便士,并在正确的位置上用小数点显示它们? - Will Crawford
显示剩余6条评论

1

通过使用格式化输出,您可以使程序的输出看起来更符合您的预期。

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

显然底层浮点运算仍然是相同的,但至少输出将更易读。

例如,要将结果舍入到小数点后两位:

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

0
你可以编写一些代码在你的计算机上计算 Epsilon。我相信标准的 C++ 定义了它,而根据你使用的方式,它也可以用其他的方式定义。
private static float calcEpsilonFloat() {
    float epsi = 1.0f;


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

    return epsi;
}

我唯一担心 Epsilon 的时候是在比较信号进行阈值处理时。即使那时,我也不确定我真的需要担心它。根据我的经验,如果你关注 Epsilon,可能首先需要处理其他问题。

-1

顺便说一下,你可以尝试使用这个函数来确保(对于不太多的小数位数),你的数字将被重新格式化以仅保留你需要的小数位数。

http://pastebin.com/CACER0xK

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

理论上,将负值放入 numberOfDecimals 中,它也会截断数字的较低整数位。例如,将 n=1588.22numberOfDecimals=-2 放入函数中,它将返回 1500.0

如果有任何问题,请告诉我。


“Doesn't work”,并且代码应该已经发布在这里了。 - user207421

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接