使用Double进行财务软件

时间:2013-11-14 13:00:30

标签: java floating-point double bigdecimal ieee-754

我知道这个问题已经多次讨论过,但我对答案并不完全满意。请不要回答"双打是不准确的,你不能代表0.1!你必须使用BigDecimal" ...

基本上我正在做一个财务软件,我们需要在内存中存储很多价格。 BigDecimal太大了,无法容纳缓存,因此我们决定切换到double。 到目前为止,我们没有因为正当理由而遇到任何错误,我们需要12位数的准确度。 12位数的估算是基于这样一个事实:即使我们以百万谈话,我们仍然可以处理美分。

double给出15位有效十进制数字的精度。如果你必须显示/比较它们时你的双打,可能会出现什么问题?

我猜问题是不准确的积累,但它有多糟糕?在它影响第12位之前需要多少次操作?

你看到双打还有其他问题吗?

编辑:很长一段时间,这绝对是我们所想到的。我们正在进行大量的除法乘法,并且很久不会很好地处理(丢失小数和溢出),或者至少你必须非常小心你所做的事情。我的问题更多的是关于双打理论,基本上有多糟糕,是否可以接受不准确?

EDIT2:不要试图解决我的软件,我不准确:)。我重新提出这样的问题:如果你只需要12个数字并且在显示/比较时你会翻倍,会出现不准确的可能性吗?

8 个答案:

答案 0 :(得分:12)

如果您绝对无法使用BigDecimal并且不想使用double,请使用longfixed-point arithmetic(因此每个long例如,值将代表分数。这将使你代表18位有效数字。

我会说使用joda-money,但这会使用BigDecimal


编辑(因为上面没有真正回答这个问题):

免责声明:如果准确性对您很重要,请don't use double to represent money。但似乎海报并不需要准确的准确性(这似乎是关于金融定价模型,可能有超过10 ** - 12内置的不确定性),并且更关心性能。假设是这种情况,使用double是可以原谅的。

通常,double不能精确地表示小数。那么,double有多么不精确?对此没有简短的答案。

double可能能够很好地表示一个数字,您可以将数字读入double,然后再将其写回,保留精度的十五位小数。但由于它是二进制而不是小数,它不可能是精确的 - 它是我们希望表示的值,加上或减去一些错误。当执行涉及不精确double s的许多算术运算时,该错误的量可能随着时间的推移而累积,使得最终产品的精度小于十五位数。少了几个?这取决于。

考虑以下函数,该函数采用1000的n根,然后将其自身乘以n次:

private static double errorDemo(int n) {
    double r = Math.pow(1000.0, 1.0/n);
    double result = 1.0;
    for (int i = 0; i < n; i++) {
        result *= r;
    }
    return 1000.0 - result;
}

结果如下:

errorDemo(     10) = -7.958078640513122E-13
errorDemo(     31) = 9.094947017729282E-13
errorDemo(    100) = 3.410605131648481E-13
errorDemo(    310) = -1.4210854715202004E-11
errorDemo(   1000) = -1.6370904631912708E-11
errorDemo(   3100) = 1.1107204045401886E-10
errorDemo(  10000) = -1.2255441106390208E-10
errorDemo(  31000) = 1.3799308362649754E-9
errorDemo( 100000) = 4.00075350626139E-9
errorDemo( 310000) = -3.100740286754444E-8
errorDemo(1000000) = -9.706695891509298E-9

请注意,累积不准确度的大小与中间步骤数的比例并不完全相同(实际上,它并非单调递增)。给定一系列已知的中间操作,我们可以确定不准确度的概率分布;虽然这将有更广泛的范围,但是更多的操作,确切的数量将取决于输入计算的数字。不确定性本身就不确定了!

根据您正在执行的计算类型,您可以通过在中间步骤之后舍入到整个单位/整分来控制此错误。 (考虑一个银行账户持有100美元,每月6%的年利息,每月0.5%的利息。在第三个月的利息被记入后,你想要余额是101.50美元还是101.51美元?)拥有你的{{ 1}}表示分数单位的数量(即分数),而不是整数单位的数量会使这更容易 - 但如果你这样做,你也可以只使用上面我建议的double

免责声明,再次:浮点错误的积累使得使用long s的金额可能非常混乱。作为一个Java开发人员,他曾经使用double来表达多年来对他进行过任何操作的十进制表示,我会使用十进制而不是浮点运算来计算任何涉及金钱的重要计算。

答案 1 :(得分:5)

Martin Fowler在这个主题上写了一些东西。他建议使用具有内部长表示的Money类和小数因子。 http://martinfowler.com/eaaCatalog/money.html

答案 2 :(得分:5)

不使用定点(整数)算术,您无法确定计算是否始终正确。这是因为IEEE 754浮点表示的工作方式,某些十进制数不能表示为有限长度的二进制分数。但是,所有定点数都可以表示为有限长度整数;因此,它们可以存储为精确的二进制值。

请考虑以下事项:

public static void main(String[] args) {
    double d = 0.1;
    for (int i = 0; i < 1000; i++) {
        d += 0.1;
    }
    System.out.println(d);
}

这会打印100.09999999999859。使用double WILL 任何资金实施失败。

要获得更直观的解释,请点击decimal to binary converter并尝试将0.1转换为二进制。你最终得到0.00011001100110011001100110011001(0011重复),将它转换回小数你得到0.0999999998603016138。

因此0.1 == 0.0999999998603016138


作为旁注,BigDecimal只是一个带有int小数位的BigInteger。 BigInteger依赖于底层的int []来保持其数字,因此提供了定点精度。

public static void main(String[] args) {
    double d = 0;
    BigDecimal b = new BigDecimal(0);
    for (long i = 0; i < 100000000; i++) {
        d += 0.1;
        b = b.add(new BigDecimal("0.1"));
    }
    System.out.println(d);
    System.out.println(b);
}

输出:
9999999.98112945(10 ^ 8次加成后损失一分钱)
10000000.0

答案 3 :(得分:1)

如果BigDecimal的大小对于您的缓存来说太大,那么在将数据写入缓存时应将金额转换为long值,并在它们转换为BigDecimal时将其转换为BigDecimal正在阅读这将为您的缓存提供更小的内存占用空间,并在您的应用程序中进行准确的计算。

即使您能够使用双精度正确表示您对计算的输入,但这并不意味着您将始终获得准确的结果。您仍然可能会遇到cancellation等事情。

如果您拒绝使用BigDecimal作为应用程序逻辑,那么您将重写{{1}}已经提供的许多功能。

答案 4 :(得分:1)

从历史上看,使用浮点类型对整数进行精确计算通常是合理的,这可能会大于2 ^ 32,但不会大于2 ^ 52 [或者,在具有适当“长双”类型的机器上,2 ^ 64]。将52位数字除以32位数以产生20位商需要在8088上进行相当冗长的绘制过程,但8087处理器可以相对快速,轻松地完成。如果所有需要精确的值始终用整数表示,那么使用小数进行财务计算是完全合理的。

如今,计算机能够更有效地处理更大的整数值,因此使用整数来处理将由整数表示的数量通常更有意义。对于像分数除法这样的事情,浮点似乎很方便,但正确的代码必须处理将事物舍入到整数的效果,无论它做什么。如果三个人需要支付100美元的费用,那么每个人支付33.333333333333就无法实现一分钱一分钱的会计。使事情保持平衡的唯一方法就是让人民支付不平等的金额。

答案 5 :(得分:0)

我将通过解决问题的不同部分来回答问题。请接受我试图解决根本问题,而不是状态问题。你有没有看过减少内存的所有选项?

  1. 例如,你是如何缓存的?
  2. 您是否使用Fly Weight模式来减少重复数字的存储?
  3. 您是否考虑过以某种方式代表公共号码? 示例零是一个常数,ZERO。
  4. 某种数字范围压缩或数字层次结构如何,例如主要数字的哈希映射?将32位存储在标志或某种类型的多个
  5. 提示一种很酷的差异方法,http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.65.2643
  6. 您的工厂缓存是否会降低效率?
  7. 指针不是免费的,想过阵列组?取决于你的问题。
  8. 您是否也将对象存储在缓存中,它们不小,您也可以将它们序列化为结构等。
  9. 查看存储问题并停止寻找避免潜在的数学问题。通常,在您不得不担心数字之前,Java中有很多过剩的东西。甚至有些人可以通过上述想法解决这些问题。

答案 6 :(得分:-1)

你不能相信财务软件中的双打。它们可能在简单的情况下工作得很好,但由于四舍五入,呈现某些值等不准确,你会遇到问题。

您别无选择,只能使用BigDecimal。否则你会说“我正在编写几乎有效的财务代码。你几乎不会注意到任何差异。”而那不会让你看起来值得信赖。

固定点在某些情况下有效,但您能确定现在和将来1美分的准确度是否足够?

答案 7 :(得分:-1)

我希望你读过Joshua Bloch Java Puzzlers Traps陷阱。这就是他在谜题2中所说的:改变的时候。

  

二进制   浮点数特别不适合货币计算,因为它无法代表   0.1-或任何其他10的负幂 - 正如有限长度二进制分数[EJ项31]。