双倍资金:我的框架使用双精度浮点数表示货币金额。

9
我接手了一个使用double类型表示货币金额的项目。
更糟糕的是,它使用的框架及其自身的类也将double用于货币。框架ORM还处理从数据库检索(和存储)值。在数据库中,货币值的类型为number(19, 7),但框架ORM将其映射为double。
除了完全绕过框架类和ORM之外,我是否有办法精确计算货币值?
编辑:是的,我知道应该使用BigDecimal。问题在于,我与一个框架紧密耦合,例如,framework.commerce.pricing.ItemPriceInfo类具有成员double mRawTotalPrice;和double mListPrice。我的公司应用程序的代码扩展了这个ItemPriceInfoClass。
实际上,我不能对我的公司说:“因为舍入误差而废弃两年的工作和数十万美元的支出,基于这个框架编写的代码”。

10
标题“翻一番”获得一个+1。 - Marko
5
我理解您的处境。但是:现金和适当的会计对公司来说往往非常重要,疏忽诉讼也是如此。您处于困境之中:一方面,建议公司付出多年的开发成本去修复一个看起来并不是问题的问题。另一方面,这可能会在未来带来巨大的问题。如果我是您,我会向我的管理层这样表述,并让他们做出决定。您面临着一个责任问题,这就是您的上司存在的目的。记录任何已做出的决定。 - Michael Petrotta
2
根据@Michael Petrotta所说,他是100%正确的。根据您正在做的事情,这个决定可能会带来巨大的责任问题。此外,这个框架——它是商业框架吗?你能说服他们改用BigDecimal吗?或者有没有一个版本可以改用BigDecimal?否则,你将需要走渐进重构的路线,这是一种可怕的情况,你会处于其中——我为你感到难过。 - aperkins
如果您必须使用双精度浮点数,请查看http://docs.sun.com/source/806-3568/ncg_goldberg.html,其中详细解释了浮点数的工作原理,其中包含一些数学知识,但并不可怕。 - teto
5个回答

10

如果可以的话,将货币类型视为整体。换句话说,如果您在美国工作,请跟踪分(cents)而不是美元,如果分提供了您需要的细度。双精度浮点数可以准确地表示整数,最大值可达非常大的值(2^53)(直到该值没有舍入误差)。

但是,实际上,正确的事情是完全绕过框架并使用更合理的东西。这对于框架来说是如此业余的错误 - 谁知道还有什么其他的问题潜藏呢?


6

我没有看到你提到重构。我认为这是你最好的选择。不要仅仅为了暂时让事情变得更好而匆忙地拼凑一些东西,为什么不用正确的方法来解决呢?

关于 double 和 BigDecimal 的比较,这篇文章建议使用 BigDecimal,尽管它速度较慢。


1
《Effective Java》(第二版,第48项)也建议这样做。 - Péter Török
2
我希望它建议使用BigDecimal... 如果由于double而导致数字和计算不正确,那么速度并没有太大意义。 - ColinD
从OP的描述来看,他好像无法进行重构,因为该框架(我假设他没有源代码)使用FP算术。 - gustafc

4
很多人会建议使用BigDecimal,如果您不知道如何在项目中使用四舍五入,则应该这样做。如果您知道如何正确地使用十进制舍入,请使用double。它的速度快得多,更加清晰简单,因此在我看来容错率更低。如果您使用美元和美分(或需要两位小数),则可以获得高达70万亿美元的准确结果。基本上,只要使用适当的舍入进行纠正,就不会出现舍入误差。顺便说一句,许多开发人员听到舍入误差就感到恐惧,但它们并不是随机错误,您可以相对容易地管理它们。编辑:考虑以下简单示例的舍入误差。
    double a = 100000000.01;
    double b = 100000000.09;
    System.out.println(a+b); // prints 2.0000000010000002E8

有许多可能的四舍五入策略。您可以在打印/显示时对结果进行四舍五入,例如:

    System.out.printf("%.2f%n", a+b); // prints 200000000.10

或者在数学上对结果进行四舍五入。
    double c = a + b;
    double r= (double)((long)(c * 100 + 0.5))/100;
    System.out.println(r); // prints 2.000000001E8

在我的情况下,当从服务器发送(写入套接字和文件)时,我会对结果进行四舍五入,但使用自己的程序避免任何对象创建。
以下是更通用的四舍五入功能,但如果您可以使用 printf 或 DecimalFormat,则可能更简单。
private static long TENS[] = new long[19]; static { 
    TENS[0] = 1; 
    for (int i = 1; i < TENS.length; i++) TENS[i] = 10 * TENS[i - 1]; 
} 

public static double round(double v, int precision) { 
    assert precision >= 0 && precision < TENS.length; 
    double unscaled = v * TENS[precision]; 
    assert unscaled > Long.MIN_VALUE && unscaled < Long.MAX_VALUE; 
    long unscaledLong = (long) (unscaled + (v < 0 ? -0.5 : 0.5)); 
    return (double) unscaledLong / TENS[precision]; 
}

注意:如果您需要特定的舍入方法,可以使用BigDecimal进行最终舍入。

2
我没有点踩,但是如果没有提供一些关于如何处理这些舍入误差的指导,那么这个答案就不太有帮助。 - Yishai
如果你正在处理大量的资金,使用浮点数是不切实际的(例如,数百万或数十亿美元的加减操作等)。此外,由于硬件的不同,舍入误差可能会有所不同,这将导致潜在的痛点和不可预测的行为。如果需要精度,就不要使用浮点数估算数据结构(double/float),而应该专注于精度。 - aperkins
1
Java在所有JSE平台上执行相同的舍入和精度,您可能会发现JME平台有所不同,我没有任何相关经验。 - Peter Lawrey
@Peter Lawrey:请注意(自JDK1.2以来),只有在使用strictfp时才适用;否则,即使该硬件不完全与Java自己的FP例程相同,JVM也可以自由地使用合适的硬件FP单元来加速处理。 - sleske
@sleske,我同意这可能与平台和硬件有关。例如,我没有尝试过在JavaME或Android上运行。我知道它可以在x86 / x64平台上的Oracle JVM上工作(这往往更严格)。我有单元测试来确认这一点。我建议任何代表货币/价格的人都应该为其组件编写单元测试,以检查舍入误差是否按预期处理。 - Peter Lawrey

1

实际上,你并没有太多选择:

你可以重构项目,使用 BigDecimal(或其他更适合其需求的东西)来表示货币。

要非常小心溢出 / 下溢和精度丢失,这意味着添加大量检查,并以不必要的方式重构系统的更大部分。更不用说如果你打算这样做需要多少研究。

保持现状,希望没有人注意到(这是个玩笑)。

在我看来,最好的解决方案是将其简单地重构出去。这可能需要进行一些复杂的重构,但是已经造成的问题是现成的,我认为这应该是你最好的选择。

最好, 瓦西尔

P.S. 噢,而且你可以将货币视为整数(计算分),但如果你要进行货币转换、计算利息等,这听起来不是一个好主意。


精度可能会有所损失,但“溢出/下溢”你是认真的吗? - Peter Lawrey
双精度浮点数在超过10^308的值时会溢出,在小于10^-308的值时会下溢。哪些货币具有这种排序方式? - Peter Lawrey
好的,溢出情况非常不可预测,但你无法确定这些数字将进行哪些操作,而在某些情况下下溢是可能发生的。 - vstoyanov
1
软件工程也是关于风险管理的,如果你可以接受性能损失,那么你应该使用BigDecimal,因为它更少出错。哦,而查找浮点算法错误与同步问题一样恶心。 - vstoyanov

0

我认为对于你的代码来说,这种情况至少是可以挽救的。你可以通过ORM框架以double类型获取值。然后,在进行任何数学/计算操作之前,使用静态的valueOf方法将其转换为BigDecimal(请参见此处了解原因),然后仅在存储时将其转换回double。

由于你已经在扩展这些类,所以当需要时,你可以添加getter方法来获取double值并将其作为BigDecimal返回。

虽然这可能无法覆盖100%的情况(我特别担心ORM或JDBC驱动程序将double转换回Number类型时会发生什么),但它比直接对原始double进行数学运算要好得多。

然而,我远不确定这种方法实际上是否更便宜,从长远来看,对公司更有利。


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