为啥不使用 Double 或 Float 来表示货币?

Posted

技术标签:

【中文标题】为啥不使用 Double 或 Float 来表示货币?【英文标题】:Why not use Double or Float to represent currency?为什么不使用 Double 或 Float 来表示货币? 【发布时间】:2011-04-13 09:38:25 【问题描述】:

一直有人告诉我从不doublefloat 类型来表示金钱,这次我向你提出问题:为什么?

我确信有一个很好的理由,我只是不知道它是什么。

【问题讨论】:

查看这个 SO 问题:Rounding Errors? 明确一点,它们不应该用于任何需要准确性的事情——不仅仅是货币。 它们不应该用于任何需要精确度的事情。但是 double 的 53 个有效位(约 16 个十进制数字)通常对于只需要 准确度 的事情就足够了。 @jeff 您的评论完全歪曲了二进制浮点的好处和坏处。阅读下面 zneak 的答案,请删除您的误导性评论。 明确地说,“精确度”(或“精确度”)是指十进制。 【参考方案1】:

因为浮点数和双精度数无法准确表示我们用于货币的以 10 为底的倍数。这个问题不仅仅针对 Java,还针对任何使用基数 2 浮点类型的编程语言。

以 10 为底,您可以将 10.25 写为 1025 * 10-2(整数乘以 10 的幂)。 IEEE-754 floating-point numbers 是不同的,但是考虑它们的一个非常简单的方法是乘以 2 的幂。例如,您可以查看 164 * 2-4(整数乘以 2 的幂),它也等于 10.25。这不是数字在内存中的表示方式,但数学含义是相同的。

即使以 10 为底,这种表示法也不能准确地表示大多数简单的分数。例如,你不能表示 1/3:十进制表示是重复的 (0.3333...),所以没有有限的整数可以乘以 10 的幂得到 1/3。您可以确定一长串 3 和一个小指数,例如 333333333 * 10-10,但这并不准确:如果将其乘以 3,则不会得到 1。

但是,为了数钱,至少对于货币价值在美元数量级以内的国家来说,通常你只需要能够存储 10-2 sup>,所以 1/3 不能表示也没关系。

浮点数和双精度数的问题在于,绝大多数类似货币的数字没有整数乘以 2 的幂的精确表示。事实上,只有 0.01 的倍数可以精确表示为 IEEE-754 二进制浮点数的 0 和 1 之间(在处理货币时很重要,因为它们是整数美分)是 0、0.25、0.5、0.75 和 1。所有其他都关闭少量。与 0.333333 示例类似,如果您将浮点值 0.01 乘以 10,则不会得到 0.1。相反,你会得到类似 0.099999999786...

将钱表示为doublefloat 一开始可能看起来不错,因为软件会消除微小的错误,但当您对不精确的数字执行更多的加法、减法、乘法和除法时,错误会变得复杂,您'最终会得到明显不准确的值。这使得浮点数和双精度数不适用于处理金钱,在这种情况下需要以 10 为底的幂的倍数的完美精度。

几乎适用于任何语言的解决方案是改用整数并计算美分。例如,1025 将是 10.25 美元。几种语言也有处理金钱的内置类型。其中,Java 有BigDecimal 类,C# 有decimal 类型。

【讨论】:

@Fran 你会得到四舍五入的错误,在某些使用大量货币的情况下,利率计算可能会严重偏离 ...大多数以 10 为底的分数,也就是说。例如,0.1 没有精确的二进制浮点表示。所以,1.0 / 10 * 10 可能和 1.0 不一样。 @linuxuser27 我认为 Fran 是想搞笑。无论如何,zneak 的答案是我见过的最好的,甚至比 Bloch 的经典版本还要好。 当然,如果你知道精度,你总是可以对结果进行四舍五入,从而避免整个问题。这比使用 BigDecimal 更快、更简单。另一种选择是使用固定精度 int 或 long。 @JoL 你是对的,float(0.1) * 10 ≠ 1 的说法是错误的。在双精度浮点数中,0.1 表示为0b0.00011001100110011001100110011001100110011001100110011010,10 表示为0b1010。如果你将这两个二进制数相乘,你会得到1.0000000000000000000000000000000000000000000000000000010,然后四舍五入到可用的 53 个二进制数,你正好是 1。浮点数的问题不在于它们总是出错,但他们有时会这样做 - 例如 0.1 + 0.2 ≠ 0.3。【参考方案2】:

来自 Bloch, J.,Effective Java,(第 2 版,第 48 条。第 3 版,第 60 条):

floatdouble 类型是 特别不适合货币 计算,因为这是不可能的 表示 0.1(或任何其他 十的负幂)作为floatdouble 完全正确。

例如,假设您有 1.03 美元 你花了42c。多少钱做 你走了?

System.out.println(1.03 - .42);

打印出0.6100000000000001

解决这个问题的正确方法是 使用BigDecimalintlong 用于货币计算。

虽然BigDecimal 有一些注意事项(请参阅当前接受的答案)。

【讨论】:

我对使用 int 或 long 进行货币计算的建议有点困惑。您如何将 1.03 表示为 int 或 long?我试过“long a = 1.04;”和“长 a = 104/100;”无济于事。 @Peter,你用long a = 104 计算美分而不是美元。 @zneak 什么时候需要应用百分比,例如复利或类似? @trusktr,我会选择你平台的十进制类型。在 Java 中,这是 BigDecimal @maaartinus ...您不认为对此类事情使用 double 容易出错吗?我已经看到浮点舍入问题影响到实际系统hard。即使在银行业。请不要推荐它,或者如果您这样做,请将其作为单独的答案提供(这样我们就可以否决它:P)【参考方案3】:

这不是准确度的问题,也不是精确度的问题。这是为了满足使用基数 10 而不是基数 2 进行计算的人们的期望。例如,使用双精度数进行金融计算不会产生数学意义上的“错误”答案,但它可以产生的答案是不是财务意义上的预期。

即使您在输出前的最后一分钟对结果进行四舍五入,您仍然偶尔会使用与预期不符的双打得到结果。

使用计算器或手动计算结果,1.40 * 165 = 231 正好。但是,在内部使用双打,在我的编译器/操作系统环境中,它被存储为接近 230.99999 的二进制数......所以如果你截断这个数字,你会得到 230 而不是 231。你可能会认为舍入而不是截断会已经给出了 231 的预期结果。这是真的,但舍入总是涉及截断。无论您使用哪种舍入技术,仍然存在像这样的边界条件,当您期望它向上舍入时,它会向下舍入。它们非常罕见,以至于通常不会通过随意的测试或观察发现。您可能需要编写一些代码来搜索说明未按预期运行的结果的示例。

假设您想将某物四舍五入到最接近的一美分。所以你得到你的最终结果,乘以 100,加 0.5,截断,然后将结果除以 100 得到便士。如果您存储的内部数字是 3.46499999.... 而不是 3.465,那么当您将数字四舍五入到最接近的美分时,您将得到 3.46 而不是 3.47。但是您的以 10 为底的计算可能表明答案应该是 3.465,显然应该向上取整到 3.47,而不是向下取整到 3.46。当您使用双精度数进行财务计算时,这些事情在现实生活中偶尔会发生。这种情况很少见,因此通常会被忽视,但它确实会发生。

如果您使用以 10 为底的内部计算而不是双精度数,则答案总是与人类预期的完全一致,假设您的代码中没有其他错误。

【讨论】:

相关,有趣:在我的 chrome js 控制台中:Math.round(.4999999999999999): 0 Math.round(.49999999999999999): 1 这个答案具有误导性。 1.40 * 165 = 231。除了 231 之外的任何数字在数学意义上(以及所有其他意义上)都是错误的。 @Karu 我认为这就是 Randy 说浮点数不好的原因......我的 Chrome JS 控制台显示 230.99999999999997 结果。这错的,这是答案中的重点。 @Karu:恕我直言,答案在数学上并没有错。只是有 2 个问题被回答,而不是被问到的问题。您的编译器回答的问题是 1.39999999 * 164.99999999 等等,数学上正确的等于 230.99999.... 显然,这不是首先提出的问题.... @CurtisYallop 因为关闭双精度值到 0.49999999999999999 是 0.5 Why does Math.round(0.49999999999999994) return 1?【参考方案4】:

我对其中一些回复感到困扰。我认为双打和浮点数在财务计算中占有一席之地。当然,在使用整数类或 BigDecimal 类时,在加减非小数货币金额时不会损失精度。但是,在执行更复杂的操作时,无论您如何存储数字,您最终都会得到小数点后几位或多位的结果。问题在于你如何呈现结果。

如果您的结果介于四舍五入和四舍五入之间,并且最后一分钱真的很重要,那么您可能应该通过显示更多小数位来告诉观众答案几乎在中间。

双精度数的问题,尤其是浮点数的问题在于,它们用于组合大数和小数。在java中,

System.out.println(1000000.0f + 1.2f - 1000000.0f);

结果

1.1875

【讨论】:

这个!!!!我正在搜索所有答案以找到这个相关的事实!!!在正常计算中,没有人关心你是否只有几分之一,但在这里,如果数字很大,每笔交易很容易损失一些美元! 现在想象有人从他的 100 万美元中获得 0.01% 的每日收入 - 他每天将一无所获 - 一年后他没有得到 1000 美元,这很重要 问题不在于准确性,而是浮动并没有告诉您它变得不准确。一个整数最多只能容纳 10 位数字,而浮点数最多可以容纳 6 位而不会变得不准确(当您相应地削减它时)。它确实允许这样做,而整数溢出并且像 java 这样的语言会警告您或不允许它。当您使用双精度时,最多可以达到 16 位,这对于许多用例来说已经足够了。 @Klaws 谢谢你提供的细节。我觉得我开始明白了。但我不熟悉欧洲税法,因此感到困惑。价格通常显示为“最终用户价格”(含税)并且卖方应将最终用户价格 0.02 欧元(包括卖方的 0.017 欧元和 0.003 欧元的税)乘以 1000,这对吗?为卖方获得 17.00 欧元和 3.00 欧元的税款?这感觉很奇怪(从美国的情况来看,税收总是在最后计算并且从不包含在广告价格中),感觉 17.00 欧元 @19% 的税应该是 3.23 欧元。谢谢! @Josiah Yoder 欧盟的增值税法......很复杂。自引入欧元以来,小数点后三位是强制性的,这意味着应用程序通常使用 4 位小数来确保正确舍入。 显示的价格通常是最终用户价格,但通常存储为净价(不含增值税)。在德国,增值税是在每次交货结束时计算的,而不是针对单个商品。然而,我认为荷兰允许计算每个项目的税款并在最后加起来。对于德国的增值税预付款,适用不同的规则(甚至一次四舍五入到零位)。【参考方案5】:

我可能会被否决,但我认为浮点数不适合货币计算被高估了。只要您确保正确地进行了四舍五入并有足够的有效数字来处理 zneak 解释的二进制十进制表示不匹配,就不会有问题。

在 Excel 中使用货币计算的人一直使用双精度浮点数(Excel 中没有货币类型),我还没有看到有人抱怨舍入错误。

当然,你必须保持理智;例如一个简单的网上商店可能永远不会遇到双精度浮点数的任何问题,但如果你这样做,例如会计或其他需要添加大量(无限制)数字的东西,你不会想用 10 英尺的柱子接触浮点数。

【讨论】:

这实际上是一个相当不错的答案。在大多数情况下,使用它们完全没问题。 需要注意的是,大多数投资银行都像大多数 C++ 程序一样使用 double。有些使用时间长,但因此存在跟踪规模的问题。 我觉得这个答案很有趣。我假设您和@PeterLawrey 从经验中发言。是否可以找到引用/网络链接来支持您的主张?根据我自己的经验,我知道公司一直在 Excel 中使用财务信息。但是使用 double 的投资银行呢? @JosiahYoder 交易系统传统上是用 C++ 编写的,通常使用双精度或固定精度。即没有 BigDecimal。固定精度的问题是任何潜在错误的成本。即使在 10 亿美元的交易中,double 也可能不到 1 美分,但对于固定精度,您可能会损失 10 倍或更多。 多年前我第一次接触到这个问题,当时一位会计师告诉他们不能接受账面上的一分钱差额。【参考方案6】:

浮点数和双精度数是近似值。如果你创建一个 BigDecimal 并将一个浮点数传递给构造函数,你会看到浮点数实际上等于:

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375

这可能不是您想要的表示 1.01 美元的方式。

问题在于 IEEE 规范没有办法准确表示所有分数,其中一些最终成为重复分数,因此您最终会出现近似错误。由于会计师喜欢事情一分钱一分货,如果客户支付账单并且在处理付款后他们欠 0.01 并且他们被收取费用或无法关闭他们的账户,那么最好使用确切的类型,例如小数(在 C# 中)或 Java 中的 java.math.BigDecimal。

四舍五入并不是错误不可控:see this article by Peter Lawrey。一开始就不必四舍五入更容易。大多数处理金钱的应用程序不需要大量的数学运算,操作包括添加东西或将金额分配到不同的桶。引入浮点和舍入只会使事情复杂化。

【讨论】:

floatdoubleBigDecimal 代表精确值。代码到对象的转换和其他操作一样不精确。类型本身并不是不准确的。 @chux:重读这篇文章,我认为您的观点是我的措辞可以改进。我将对此进行编辑并改写。【参考方案7】:

虽然浮点类型确实只能表示近似十进制数据,但如果在呈现数字之前将数字四舍五入到必要的精度,则可以获得正确的结果。通常。

通常是因为 double 类型的精度小于 16 位。如果您需要更高的精度,则它不是合适的类型。近似值也可以累积。

必须说,即使你使用定点算法,你仍然需要对数字进行四舍五入,如果不是因为 BigInteger 和 BigDecimal 如果你获得周期十进制数会出错。所以这里也有一个近似值。

例如,历史上用于财务计算的 COBOL 的最大精度为 18 位数字。所以经常有一个隐式的舍入。

最后,在我看来,double 不适合它的 16 位精度,这可能是不够的,而不是因为它是近似的。

考虑后续程序的以下输出。它表明,在将 double 舍入后给出与 BigDecimal 相同的结果,直到精度为 16。

Precision 14
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611

Precision 15
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110

Precision 16
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101

Precision 17
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011
Double                        : 56789.012345 / 1111111111 = 0.000051110111115611013

Precision 18
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111
Double                        : 56789.012345 / 1111111111 = 0.0000511101111156110125

Precision 19
------------------------------------------------------
BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.
DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5
BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111
Double                        : 56789.012345 / 1111111111 = 0.00005111011111561101252

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.MathContext;

public class Exercise 
    public static void main(String[] args) throws IllegalArgumentException,
            SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException 
        String amount = "56789.012345";
        String quantity = "1111111111";
        int [] precisions = new int [] 14, 15, 16, 17, 18, 19;
        for (int i = 0; i < precisions.length; i++) 
            int precision = precisions[i];
            System.out.println(String.format("Precision %d", precision));
            System.out.println("------------------------------------------------------");
            execute("BigDecimalNoRound", amount, quantity, precision);
            execute("DoubleNoRound", amount, quantity, precision);
            execute("BigDecimal", amount, quantity, precision);
            execute("Double", amount, quantity, precision);
            System.out.println();
        
    

    private static void execute(String test, String amount, String quantity,
            int precision) throws IllegalArgumentException, SecurityException,
            IllegalAccessException, InvocationTargetException,
            NoSuchMethodException 
        Method impl = Exercise.class.getMethod("divideUsing" + test, String.class,
                String.class, int.class);
        String price;
        try 
            price = (String) impl.invoke(null, amount, quantity, precision);
         catch (InvocationTargetException e) 
            price = e.getTargetException().getMessage();
        
        System.out.println(String.format("%-30s: %s / %s = %s", test, amount,
                quantity, price));
    

    public static String divideUsingDoubleNoRound(String amount,
            String quantity, int precision) 
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        String price = Double.toString(price0);
        return price;
    

    public static String divideUsingDouble(String amount, String quantity,
            int precision) 
        // acceptance
        double amount0 = Double.parseDouble(amount);
        double quantity0 = Double.parseDouble(quantity);

        //calculation
        double price0 = amount0 / quantity0;

        // presentation
        MathContext precision0 = new MathContext(precision);
        String price = new BigDecimal(price0, precision0)
                .toString();
        return price;
    

    public static String divideUsingBigDecimal(String amount, String quantity,
            int precision) 
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);
        MathContext precision0 = new MathContext(precision);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0, precision0);

        // presentation
        String price = price0.toString();
        return price;
    

    public static String divideUsingBigDecimalNoRound(String amount, String quantity,
            int precision) 
        // acceptance
        BigDecimal amount0 = new BigDecimal(amount);
        BigDecimal quantity0 = new BigDecimal(quantity);

        //calculation
        BigDecimal price0 = amount0.divide(quantity0);

        // presentation
        String price = price0.toString();
        return price;
    

【讨论】:

COBOL 具有定点的原生十进制类型。这可以准确地引用最多 18 位的所有十进制类型。这与浮点数不同,无论位数如何,因为它是本机十进制类型。 0.1 永远是 0.1,有时不是 0.99999999999999【参考方案8】:

浮点数的结果是不精确的,这使得它们不适合任何需要精确结果而不是近似值的金融计算。 float 和 double 是为工程和科学计算而设计的,很多时候不会产生精确的结果,浮点计算的结果也可能因 JVM 不同而不同。看下面 BigDecimal 和 double 原语的例子,它用来表示货币价值,很明显浮点计算可能不精确,应该使用 BigDecimal 进行金融计算。

    // floating point calculation
    final double amount1 = 2.0;
    final double amount2 = 1.1;
    System.out.println("difference between 2.0 and 1.1 using double is: " + (amount1 - amount2));

    // Use BigDecimal for financial calculation
    final BigDecimal amount3 = new BigDecimal("2.0");
    final BigDecimal amount4 = new BigDecimal("1.1");
    System.out.println("difference between 2.0 and 1.1 using BigDecimal is: " + (amount3.subtract(amount4)));

输出:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9

【讨论】:

让我们尝试一些除了微不足道的加法/减法和整数乘法以外的方法,如果代码计算了 7% 贷款的月利率,这两种类型都需要无法提供准确的值并需要四舍五入到最接近的值0.01。四舍五入到最低货币单位是货币计算的一部分,使用小数类型可以避免加/减的需要——但其他不多。 @chux-ReinstateMonica:如果利息应该按月计算,则通过将每日余额相加,乘以 7(利率)来计算每个月的利息,然后除以四舍五入到最接近的值一分钱,按一年中的天数计算。除了在最后一步每月一次之外,任何地方都没有四舍五入。 @supercat 我的comment 强调使用最小货币单位的二进制 FP 或十进制 FP 都会导致类似的舍入问题 - 就像在您的评论中使用“除以,四舍五入到最接近的一分钱”一样。在您的场景中,使用 base 2 或 base 10 FP 不会提供任何优势。 @chux-ReinstateMonica:在上面的场景中,如果计算出利息应该精确地等于 0.5 美分,那么正确的财务计划必须以精确指定的方式四舍五入。如果浮点计算产生的利息值为例如1.23499941 美元,但舍入前的数学精确值应该是 1.235 美元,并且舍入指定为“最接近的偶数”,使用这种浮点计算不会导致结果偏离 0.000059 美元,而是整整 0.01 美元,出于会计目的,这完全是错误的。 正确进行财务/会计计算所需的是仅使用数学上精确的运算,除非在精确指定舍入的地方。正确除数时,必须指定舍入,必须同时计算商和余数,或者商和除数的乘积必须精确等于被除数。除以 7 而不指定舍入或余数通常是错误的。【参考方案9】:

如前所述,“将钱表示为双精度或浮点数一开始可能看起来不错,因为软件会消除微小的错误,但随着您对不精确的数字执行更多的加法、减法、乘法和除法,您将损失更多随着误差的增加,精度更高。这使得浮点数和双精度数不足以处理金钱,因为需要以 10 次方为底的倍数的完美精度。"

Java 终于有了一种处理货币和金钱的标准方法!

JSR 354:货币和货币 API

JSR 354 提供了一个 API,用于表示、传输和执行货币和货币的综合计算。你可以从这个链接下载它:

JSR 354: Money and Currency API Download

规范包含以下内容:

    用于处理 e 的 API。 G。货币金额和货币 支持可互换实现的 API 用于创建实现类实例的工厂 计算、转换和格式化货币金额的功能 用于处理货币和货币的 Java API,计划包含在 Java 9 中。 所有规范类和接口都位于 javax.money.* 包中。

JSR 354 示例:货币和货币 API:

创建 MonetaryAmount 并将其打印到控制台的示例如下所示:

MonetaryAmountFactory<?> amountFactory = Monetary.getDefaultAmountFactory();
MonetaryAmount monetaryAmount = amountFactory.setCurrency(Monetary.getCurrency("EUR")).setNumber(12345.67).create();
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

使用参考实现 API 时,必要的代码要简单得多:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmountFormat format = MonetaryFormats.getAmountFormat(Locale.getDefault());
System.out.println(format.format(monetaryAmount));

API 还支持使用 MonetaryAmounts 进行计算:

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));

货币单位和货币金额

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);

MonetaryAmount 有多种方法可以访问指定的货币、数字金额、其精度等:

MonetaryAmount monetaryAmount = Money.of(123.45, euro);
CurrencyUnit currency = monetaryAmount.getCurrency();
NumberValue numberValue = monetaryAmount.getNumber();

int intValue = numberValue.intValue(); // 123
double doubleValue = numberValue.doubleValue(); // 123.45
long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100
long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45
int precision = numberValue.getPrecision(); // 5

// NumberValue extends java.lang.Number.
// So we assign numberValue to a variable of type Number
Number number = numberValue;

MonetaryAmounts 可以使用舍入运算符进行舍入:

CurrencyUnit usd = MonetaryCurrencies.getCurrency("USD");
MonetaryAmount dollars = Money.of(12.34567, usd);
MonetaryOperator roundingOperator = MonetaryRoundings.getRounding(usd);
MonetaryAmount roundedDollars = dollars.with(roundingOperator); // USD 12.35

在使用 MonetaryAmounts 集合时,可以使用一些不错的实用方法来过滤、排序和分组。

List<MonetaryAmount> amounts = new ArrayList<>();
amounts.add(Money.of(2, "EUR"));
amounts.add(Money.of(42, "USD"));
amounts.add(Money.of(7, "USD"));
amounts.add(Money.of(13.37, "JPY"));
amounts.add(Money.of(18, "USD"));

自定义货币金额操作

// A monetary operator that returns 10% of the input MonetaryAmount
// Implemented using Java 8 Lambdas
MonetaryOperator tenPercentOperator = (MonetaryAmount amount) -> 
    BigDecimal baseAmount = amount.getNumber().numberValue(BigDecimal.class);
    BigDecimal tenPercent = baseAmount.multiply(new BigDecimal("0.1"));
    return Money.of(tenPercent, amount.getCurrency());
;

MonetaryAmount dollars = Money.of(12.34567, "USD");

// apply tenPercentOperator to MonetaryAmount
MonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

资源:

Handling money and currencies in Java with JSR 354

Looking into the Java 9 Money and Currency API (JSR 354)

另请参阅:JSR 354 - Currency and Money

【讨论】:

感谢在 Java 9 中提及 MonetaryAmount【参考方案10】:

大多数答案都强调了为什么不应该使用双精度数来计算货币和货币的原因。我完全同意他们的看法。

这并不意味着双打永远不能用于此目的。

我参与过许多对 gc 要求非常低的项目,而 BigDecimal 对象是导致开销的一大原因。

正是对双重表示缺乏理解,缺乏处理准确性和精确性的经验,才产生了这个明智的建议。

如果您能够处理项目的精度和准确度要求,您就可以让它工作,这必须根据处理的双精度值的范围来完成。

你可以参考番石榴的 FuzzyCompare 方法来获得更多的想法。参数容差是关键。 我们在一个证券交易应用程序中处理了这个问题,并对不同范围内的不同数值使用什么容差进行了详尽的研究。

此外,在某些情况下,您可能会尝试使用 Double 包装器作为映射键,而哈希映射是实现。这是非常危险的,因为 Double.equals 和哈希码(例如值“0.5”和“0.6 - 0.1”)会造成很大的混乱。

【讨论】:

【参考方案11】:

如果您的计算涉及多个步骤,任意精度算术将无法 100% 覆盖您。

使用完美表示结果的唯一可靠方法(使用自定义 Fraction 数据类型,将批量除法运算到最后一步)并且仅在最后一步转换为十进制表示法。

任意精度无济于事,因为总是有很多小数位的数字,或者某些结果,例如0.6666666... 没有任意表示将涵盖最后一个示例。所以每一步都会有小错误。

这些错误会累积起来,最终可能不再容易被忽略。这称为Error Propagation。

【讨论】:

【参考方案12】:

针对此问题发布的许多答案都讨论了 IEEE 和围绕浮点运算的标准。

来自非计算机科学背景(物理和工程),我倾向于从不同的角度看待问题。对我来说,我不会在数学计算中使用 double 或 float 的原因是我会丢失太多信息。

有哪些选择?有很多(还有很多我不知道的!)。

Java 中的 BigDecimal 是 Java 语言的本机。 Apfloat 是另一个用于 Java 的任意精度库。

C# 中的十进制数据类型是 Microsoft 的 .NET 替代 28 位有效数字。

SciPy(科学 Python)可能也可以处理财务计算(我没有尝试过,但我怀疑是这样)。

GNU 多精度库 (GMP) 和 GNU MFPR 库是用于 C 和 C++ 的两个免费和开源资源。

还有用于 javascript(!) 的数值精度库,我认为 php 可以处理财务计算。

还有许多计算机语言的专有(我认为尤其是 Fortran)和开源解决方案。

我不是受过训练的计算机科学家。但是,我倾向于使用 Java 中的 BigDecimal 或 C# 中的小数。我没有尝试过我列出的其他解决方案,但它们可能也非常好。

对我来说,我喜欢 BigDecimal,因为它支持的方法。 C# 的小数非常好,但我没有机会尽可能多地使用它。我在业余时间做我感兴趣的科学计算,BigDecimal 似乎工作得很好,因为我可以设置浮点数的精度。 BigDecimal 的缺点?有时它可能会很慢,尤其是在您使用除法时。

为了加快速度,您可以查看 C、C++ 和 Fortran 中的免费和专有库。

【讨论】:

关于 SciPy/Numpy,不支持固定精度(即 Python 的 decimal.Decimal)(docs.scipy.org/doc/numpy-dev/user/basics.types.html)。某些函数不能与 Decimal 一起正常工作(例如 isnan)。 Pandas 基于 Numpy,由主要的量化对冲基金 AQR 发起。所以你有关于财务计算的答案(不是杂货会计)。【参考方案13】:

要补充以前的答案,在处理问题中解决的问题时,除了 BigDecimal 之外,还可以选择在 Java 中实现 Joda-Money。 Java 模块名称是 org.joda.money。

它需要 Java SE 8 或更高版本,并且没有依赖项。

更准确地说,存在编译时依赖,但不是 必填。

<dependency>
  <groupId>org.joda</groupId>
  <artifactId>joda-money</artifactId>
  <version>1.0.1</version>
</dependency>

使用 Joda Money 的示例:

  // create a monetary value
  Money money = Money.parse("USD 23.87");
  
  // add another amount with safe double conversion
  CurrencyUnit usd = CurrencyUnit.of("USD");
  money = money.plus(Money.of(usd, 12.43d));
  
  // subtracts an amount in dollars
  money = money.minusMajor(2);
  
  // multiplies by 3.5 with rounding
  money = money.multipliedBy(3.5d, RoundingMode.DOWN);
  
  // compare two amounts
  boolean bigAmount = money.isGreaterThan(dailyWage);
  
  // convert to GBP using a supplied rate
  BigDecimal conversionRate = ...;  // obtained from code outside Joda-Money
  Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);
  
  // use a BigMoney for more complex calculations where scale matters
  BigMoney moneyCalc = money.toBigMoney();

文档: http://joda-money.sourceforge.net/apidocs/org/joda/money/Money.html

实现示例: https://www.programcreek.com/java-api-examples/?api=org.joda.money.Money

【讨论】:

【参考方案14】:

Float 是 Decimal 的二进制形式,具有不同的设计;他们是两个不同的东西。两种类型相互转换时几乎没有错误。此外,float 旨在表示无限大量的科学值。这意味着它的设计目的是在固定字节数的情况下将精度降低到极小和极大的数字。十进制不能表示无限数量的值,它仅限于该数量的十进制数字。所以 Float 和 Decimal 的用途不同。

有一些方法可以管理货币价值的错误:

    改用长整数并以美分计数。

    使用双精度,仅将有效数字保留为 15,以便可以精确模拟小数。在呈现值之前进行四舍五入;计算时经常四舍五入。

    使用 Java BigDecimal 之类的十进制库,因此无需使用 double 来模拟十进制。

附言有趣的是,大多数品牌的手持科学计算器都使用十进制而不是浮点数。所以没有人抱怨浮动转换错误。

【讨论】:

【参考方案15】:

这里有一些使用浮点数和小数的技巧。

0.1 x 10 = 1。看起来很合理,但在计算机级别,您作为开发人员应该处理:

在任何编程语言中(使用 Delphi、VBScript、Visual Basic、JavaScript 进行了测试,现在使用 Java/android 进行了测试):

    double total = 0.0;

    // do 10 adds of 10 cents
    for (int i = 0; i < 10; i++) 
        total += 0.1;  // adds 10 cents
    

    Log.d("round problems?", "current total: " + total);

    // looks like total equals to 1.0, don't?

    // now, do reverse
    for (int i = 0; i < 10; i++) 
        total -= 0.1;  // removes 10 cents
    

    // looks like total equals to 0.0, don't?
    Log.d("round problems?", "current total: " + total);
    if (total == 0.0) 
        Log.d("round problems?", "is total equal to ZERO? YES, of course!!");
     else 
        Log.d("round problems?", "is total equal to ZERO? No...");
        // so be careful comparing equality in this cases!!!
    

输出:

 round problems?: current total: 0.9999999999999999
 round problems?: current total: 2.7755575615628914E-17
 round problems?: is total equal to ZERO? No... 

【讨论】:

问题不在于发生舍入错误,而在于您没有处理它。将结果四舍五入到小数点后两位(如果你想要美分),你就完成了。【参考方案16】:

美国货币可以很容易地用美元和美分来表示。整数是 100% 精确的,而浮点二进制数与浮点小数不完全匹配。

【讨论】:

错了。整数不是 100% 精确的。精度需要小数或分数。 它们对于货币等整数值是精确的。

以上是关于为啥不使用 Double 或 Float 来表示货币?的主要内容,如果未能解决你的问题,请参考以下文章

java float double精度为啥会丢失

float和double有啥区别?

float,double类型小数计算结果为啥不一样

java float double精度为啥会丢失

为啥 double 和 float 存在? [复制]

double精确到几位小数