双精度浮点数 vs BigDecimal?

416

我需要计算一些浮点变量,我的同事建议我使用 BigDecimal 而不是 double,因为它会更精确。但我想知道它是什么以及如何充分利用 BigDecimal


看看这个;https://dev59.com/W3RC5IYBdhLWcg3wW_tk - Espen Schulstad
8个回答

593
BigDecimal是一种精确表示数字的方式。而Double则具有一定的精度。当使用不同量级的double(例如:d1 = 1000.0d2 = 0.001)时,由于差异较大,可能会导致0.001在求和时被完全舍去。但是使用BigDecimal就不会出现这种情况。 BigDecimal的缺点是速度较慢,编写算法也稍微有些困难(因为没有重载+-*/)。如果您需要处理货币或者必须保证精度,则应使用BigDecimal。否则,Double通常已经足够了。
我建议阅读BigDecimaljavadoc,因为它们比我在这里解释的更好 :)

3
是的,我正在计算股票价格,所以我相信在这种情况下 BigDecimal 很有用。 - Truong Ha
6
在处理价格时,您应该使用 BigDecimal。如果您将它们存储在数据库中,您需要类似的东西。 - extraneon
150
说“BigDecimal是精确表示数字的一种方式”是误导性的。 1/3和1/7不能在十进制(BigDecimal)或二进制(float或double)中被精确表示。 1/3可以在基数为3、6、9、12等情况下被精确表示,而1/7可以在基数为7、14、21等情况下被精确表示。BigDecimal的优点是任意精度以及人们习惯于十进制中得到的舍入误差。 - procrastinate_later
4
关于速度较慢的观点很有帮助,这让我理解了Netflix Ribbon负载均衡器代码中为什么涉及到双精度,并且有像这样的语句:if (Math.abs(loadPerServer - maxLoadPerServer) < 0.000001d) { - michaelok
@extraneon 我想你的意思是说“如果精度很重要,使用BigDecimal”,Double会有更多的“精度”(更多的数字)。 - jspinella
显示剩余2条评论

251

我的英文不好,所以我只会写一个简单的例子。

double a = 0.02;
double b = 0.03;
double c = b - a;
System.out.println(c);

BigDecimal _a = new BigDecimal("0.02");
BigDecimal _b = new BigDecimal("0.03");
BigDecimal _c = _b.subtract(_a);
System.out.println(_c);

程序输出:

0.009999999999999998
0.01

还有人想要使用双倍符号吗? ;)


12
@eldjon 这不是真的,看这个例子:BigDecimal two = new BigDecimal("2"); BigDecimal eight = new BigDecimal("8"); System.out.println(two.divide(eight)); 这将打印出0.25。 - Ludvig W
然而,如果您使用浮点数,您将在这种情况下获得与BigDecimal相同的精度,但性能更好。 - EliuX
4
@EliuX 浮点数可能可以处理0.03-0.02,但其他值仍然不精确:"System.out.println(0.003f - 0.002f);" BigDecimal是精确的:"System.out.println(new BigDecimal("0.003").subtract(new BigDecimal("0.002")));" - Martin
但这是因为您没有正确打印浮点数。想法是使用double进行计算。一旦获得结果,将其转换为BigDecimal。设置您的精度和舍入设置并打印它。或者您可以使用Formatter。 - Joaquim Perez
例如,将 0.00999999999998 四舍五入后可以得到准确的 0.01。 - Joaquim Perez
有人能解释一下为什么会发生这种情况吗? - mattsmith5

81

相对于 double 类型,BigDecimal 类型有两个主要区别:

  • 任意精度,类似 BigInteger,BigDecimal 可以包含任意大小和精度的数字(而 double 有固定数量的位数)
  • 使用 Base 10 而不是 Base 2,BigDecimal 表示为 n * 10^(-scale),其中 n 是任意大的有符号整数,scale 可以被视为将小数点向左或向右移动的位数。

尽管 BigDecimal 仍然不能表示所有数字,但在货币计算中使用 BigDecimal 的两个原因是:

  • 它可以表示所有可以用十进制表示法表示的数字,这包括货币世界中的几乎所有数字(你不会将 1/3 美元转移到某人账户上)。
  • 可以控制精度以避免累积误差。使用 double 类型,随着值的增加,其精度会降低,从而可能导致结果中出现重大误差。

9
这个答案真正解释了使用BigDecimal而不是double的区别和原因,性能问题是次要的。 - Vortex
@Meros - 你能详细解释一下“任意精度”吗? - vaibhav.g
https://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html - Meros

46
如果你把一个分数值,比如 1 / 7,写成小数形式,你会得到
1/7 = 0.142857142857142857142857142857142857142857...

以无限重复的数字142857为例。由于你只能写下有限数量的数字,你将不可避免地引入一个舍入(或截断)误差。
像1/10或1/100这样的数字,以二进制数表示并带有小数部分,在小数点后也有无限数量的数字。
1/10 = binary 0.0001100110011001100110011001100110...

双精度浮点数以二进制形式存储数值,因此仅仅将十进制数转换为二进制数就可能引入错误,甚至不进行任何算术运算。
另一方面,十进制数通常将每个十进制数字按原样存储(二进制编码,但每个十进制数字独立存储),或者在BigDecimal的情况下,以x*10y的形式存储为两个带符号的二进制整数(感谢@AlexSalauyou的指出)。参见:Class BigDecimal。这意味着十进制类型在一般意义上并不比二进制浮点数或定点数更精确(即不能存储1/7而不丢失精度),但对于具有有限十进制位数的数字,它更准确,而这在货币计算中经常发生。
Java的BigDecimal还具有额外的优势,它可以在小数点两侧拥有任意(但有限)数量的数字,仅受可用内存的限制。

十进制数(如BigDecimal),将每个十进制数字按原样存储(二进制编码,但每个十进制数字独立存储)---这是不正确的,BigDecimalx*10^y的形式存储数字,xy都以有符号的二进制整数形式存储。 - undefined
1
@AlexSalauyou:谢谢你的指正。回答已更新。 - undefined

26

如果你处理计算,那么有关于如何计算以及使用何种精度的法律。如果你违反了这些规定,你将会从事非法行为。 唯一的真正原因是十进制数字的位表示不是精确的。就像 Basil 所说的,举个例子是最好的解释。为了补充他的例子,这里是发生的情况:

static void theDoubleProblem1() {
    double d1 = 0.3;
    double d2 = 0.2;
    System.out.println("Double:\t 0,3 - 0,2 = " + (d1 - d2));

    float f1 = 0.3f;
    float f2 = 0.2f;
    System.out.println("Float:\t 0,3 - 0,2 = " + (f1 - f2));

    BigDecimal bd1 = new BigDecimal("0.3");
    BigDecimal bd2 = new BigDecimal("0.2");
    System.out.println("BigDec:\t 0,3 - 0,2 = " + (bd1.subtract(bd2)));
}

输出:

Double:  0,3 - 0,2 = 0.09999999999999998
Float:   0,3 - 0,2 = 0.10000001
BigDec:  0,3 - 0,2 = 0.1

此外,我们还有以下内容:
static void theDoubleProblem2() {
    double d1 = 10;
    double d2 = 3;
    System.out.println("Double:\t 10 / 3 = " + (d1 / d2));

    float f1 = 10f;
    float f2 = 3f;
    System.out.println("Float:\t 10 / 3 = " + (f1 / f2));

    // Exception! 
    BigDecimal bd3 = new BigDecimal("10");
    BigDecimal bd4 = new BigDecimal("3");
    System.out.println("BigDec:\t 10 / 3 = " + (bd3.divide(bd4)));
}

给我们输出结果:
Double:  10 / 3 = 3.3333333333333335
Float:   10 / 3 = 3.3333333
Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion

但是:
static void theDoubleProblem2() {
    BigDecimal bd3 = new BigDecimal("10");
    BigDecimal bd4 = new BigDecimal("3");
    System.out.println("BigDec:\t 10 / 3 = " + (bd3.divide(bd4, 4, BigDecimal.ROUND_HALF_UP)));
}

输出如下:

BigDec:  10 / 3 = 3.3333 

7
该警察在凌晨2点破门而入,你能想象吗?“先生,这是您的代码吗?你知道你在用错误的精度除以这两个数字吗?!现在靠墙站好。” - Tarek
3
@Tarek7 这确实是银行、市场、电信等任何与货币相关的领域都面临的法律问题。如果你观看过《超人》电影,你就知道精度的微小变化可以让你成为百万富翁! :) - jfajunior

11

BigDecimal 是 Oracle 的任意精度数字库,是 Java 语言的一部分,适用于各种应用程序,从金融到科学(那就是我所在的领域)。

对于某些计算使用 double 没有问题。但是,假设您想计算 Math.Pi * Math.Pi / 6,也就是针对实数参数为二的黎曼ζ函数的值(这是我目前正在进行的项目)。浮点除法会带来一个痛苦的问题,即舍入误差。

另一方面,BigDecimal 包括许多选项以任意精度计算表达式。如下所述的 add、multiply 和 divide 方法在 BigDecimal Java World 中“取代”了 +、* 和 /:

http://docs.oracle.com/javase/7/docs/api/java/math/BigDecimal.html

compareTo 方法在 while 和 for 循环中尤其有用。

然而,在使用 BigDecimal 的构造函数时要小心。字符串构造函数在许多情况下非常有用。例如,代码

BigDecimal onethird = new BigDecimal("0.33333333333");

利用了表示1/3的字符串表示形式,以指定的精度表示该无限重复的数字。舍入误差很可能深藏在 JVM 中,不会干扰大多数实际计算。然而,从个人经验来看,我见过舍入误差的问题。setScale 方法在这些方面很重要,如 Oracle 文档所示。


BigDecimal是Java的任意精度数字库的一部分。在这个上下文中,“内部”的意义不大,特别是它是由IBM编写的。 - user207421
@EJP:我研究了BigDecimal类,并了解到其中只有一部分是由IBM编写的。版权注释如下:`/*
  • 版权所有 IBM 公司,2001年。保留所有权利。 */`
- realPK
舍入误差很可能在JVM的深处。这个错误不是“深藏在JVM中”,而是在假设1/3=0.3333的前提下产生的。当你使用new BigDecimal("0.3333")时,小数0.3333被精确地存储。 - undefined

0
你的同事只是想简单化。
几乎每个以小数形式表示的值x在二进制的double类型中无法精确存储,因此,代替x,会存储一些x + delta。这本身并不是问题,因为在大多数情况下,转换回十进制会正确处理它(例如:Double.toString(0.1) -> "0.1")。但是当进行算术计算时,参数的delta会在结果中累积,你需要注意处理它。

常常有人说,累积的不准确性不足以影响结果,因为双精度浮点数的精度为1e-16(即:|delta| < 1e-16)。这对于x约为1的情况是正确的。对于x约为1M,精度为1e-10;对于100B(这是印尼盾中相当典型的金额),精度降至1e-5。现在,您有一个限制,即保证在安全区域内进行100K个简单算术运算。

即使您绝对确定累积的不准确性不会传播到结果的重要部分,您仍然必须实施一些规则来抑制它:四舍五入,从分转换等等,每当结果应被视为十进制时,即进行比较,打印,保存到数据库,发送到下游等等。而且,您的同事必须审查所有这些。

使用BigDecimal,其中十进制小数被准确地存储,鉴于现代世界中所有货币金额都是以小数计算的,您和您的同事根本不必担心这个问题。


0
如果需要在算术运算中使用除法,需要使用double而不是BigDecimal。 BigDecimal 中的除法(divide(BigDecimal) method)对于处理重复小数有些无用,即除数为非循环小数会抛出java.lang.ArithmeticException: Non-terminating decimal expansion;没有确切可表示的十进制结果。

只需尝试BigDecimal.ONE.divide(new BigDecimal("3"));

另一方面,double会正确处理除法(具有约15个有效数字的预期精度)


2
divide(BigDecimal divisor, int scale, int roundingMode) 有何作用?它可以让我们指定除法结果的精确度,以满足我们的需求。 - ivan.ukr

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