为什么不能使用Double或Float来表示货币?

1217

我一直被告知不要使用 doublefloat 类型来表示货币,这次我向您提出问题:为什么?

我相信有很好的原因,只是我不知道是什么。


5
请查看此 Stack Overflow 问题:Rounding Errors? - Jeff Ogata
103
需要翻译的内容:Just to be clear, they shouldn't be used for anything that requires accuracy -- not just currency.为了明确起见,它们不应该被用于任何需要准确性的事情,不仅限于货币。 - Jeff
193
它们不应用于需要精确性的任何事情。但是双精度浮点数的53个有效位(约16个十进制数字)通常足够处理那些仅需要准确性的事情。 - dan04
39
@jeff你的评论完全误解了二进制浮点数的适用和不适用范围。请仔细阅读下面zneak的回答,并删除你误导性的评论。 - Pascal Cuoq
1
请明确一点,您所说的“exactness”(或“precision”)是指小数点位数的准确性。 - Jack Leow
16个回答

1266
因为浮点数和双精度数不能准确地表示我们用于货币的十进制倍数。这个问题不仅限于Java,而是适用于任何使用基于2进制的浮点类型的编程语言。
在十进制中,您可以将10.25写成1025 * 10-2(一个整数乘以10的幂)。IEEE-754浮点数是不同的,但是一种非常简单的方法是将其乘以二的幂次。例如,您可能正在查看164 * 2-4(一个整数乘以二的幂),它也等于10.25。这不是数字在内存中的表示方式,但数学影响是相同的。
即使在十进制中,这种表示法也不能准确地表示大多数简单分数。例如,您无法表示1/3:十进制表示形式是重复的(0.3333 ...),因此没有有限的整数可以乘以10的幂次来得到1/3。您可以选择一个长序列的3和一个小指数,例如333333333 * 10-10,但它不准确:如果您将其乘以3,则不会得到1。
然而,对于计算货币,至少对于货币价值与美元相差不超过一个数量级的国家,通常您只需要能够存储10-2的倍数,因此无论1/3是否能够被表示都并不重要。
浮点数和双精度数的问题在于,绝大部分类似货币的数字没有一个精确表示为2的幂次方乘以整数的表示法。实际上,在0到1之间的仅有的可以被表示为IEEE-754二进制浮点数的0.01的倍数(在处理货币时因为它们是整数美分而显著)是0、0.25、0.5、0.75和1。所有其他的都会有一点偏差。类比于0.333333的例子,如果你取0.01的浮点值并乘以10,你得到的不会是0.1。相反,你会得到像0.099999999786这样的结果...
将货币表示为double或float可能起初看起来很好,因为软件会四舍五入小误差,但是当你对不精确的数字进行更多的加减乘除运算时,误差会累积,最终你会得到明显不准确的值。这使得浮点数和双精度数在处理货币时不够精确,因为需要对10的幂次方的倍数进行完美的精确计算。

一种适用于任何语言的解决方案是使用整数来计算分数。例如,1025表示$10.25。许多语言也有内置类型来处理货币。其中,Java有BigDecimal类,Rust有rust_decimal crate,C#有decimal类型。


5
@Fran:你会遇到舍入误差,在一些大量使用货币的情况下,利率计算可能会极度失准。 - linuxuser27
7
大多数十进制小数(即基数为10的小数)在二进制浮点表示法中无法精确表示,例如,0.1无法被准确地表示成二进制浮点数。因此,1.0 / 10 * 10 的结果可能不等于1.0。请注意,这里所说的是二进制浮点数的精度问题。 - C. K. Young
8
@linuxuser27,我觉得Fran是在尝试幽默。无论如何,zneak的回答是我见过最好的,甚至比Bloch的经典版本还要好。 - Isaac Rabinovitch
5
当然,如果你知道精确度,你可以将结果四舍五入,避免整个问题。这比使用BigDecimal更快、更简单。另一个选择是使用固定精度的int或long。 - Peter Lawrey
8
@JoL 你是对的,float(0.1) * 10 ≠ 1的说法是错误的。在双精度浮点数中,0.1用0b0.00011001100110011001100110011001100110011001100110011010表示,而10用0b1010表示。如果你将这两个二进制数相乘,你会得到1.0000000000000000000000000000000000000000000000000000010,在它被舍入为53位可用的二进制数字后,你正好得到了1。浮点数的问题不在于它们总是出错,而在于它们有时会出错——就像0.1 + 0.2 ≠ 0.3的例子一样。 - Jim Danner
显示剩余34条评论

365

摘自 Bloch, J. 的《Effective Java》(第二版,第48条;第三版,第60条):

floatdouble 类型不适合进行货币计算,因为它们不能准确地表示 0.1(或其他任何负十次幂)。

例如,假设你有 1.03 美元并花费了 42 美分。你还剩多少钱呢?

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

输出结果为0.6100000000000001

解决此问题的正确方法是使用BigDecimalintlong进行货币计算。

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


10
我对使用int或long进行货币计算的建议有些困惑。你如何将1.03表示为int或long?我尝试过"long a = 1.04;"和"long a = 104/100;",但都无法实现。 - Peter
70
@Peter,你使用 long a = 104 并将计数单位设为分而不是美元。 - zneak
4
当需要应用比例(如复利或类似情况)时该怎么办? - trusktr
5
@trusktr,我会选择你的平台上的十进制类型。在Java中,这就是BigDecimal - zneak
17
@maaartinus,您不觉得在这种情况下使用双精度浮点数容易出错吗?我见过浮点舍入问题对真实系统造成的严重影响,甚至包括银行系统。请不要推荐它,或者如果您坚持推荐,请作为单独的答案提供(这样我们就可以踩一下了:P)。 - eis
显示剩余7条评论

94

这不是精度问题,也不是准确性问题。而是满足使用十进制计算而非二进制计算的人类期望的问题。例如,在财务计算中使用双精度浮点数并不会在数学意义上产生“错误”的答案,但可能会产生与财务预期不符的答案。

即使您在输出前最后一刻四舍五入结果,有时仍可能会得到使用双精度浮点数得出的结果与期望不符。

使用计算器或手动计算,1.40*165=231恰好成立。但是,在我的编译器/操作系统环境中,内部使用双精度浮点数将其存储为接近230.99999...的二进制数,因此如果截断该数字,则会得到230而不是231。您可能会认为舍入而不是截断会得到所需的231。这是正确的,但舍入始终涉及截断。无论使用何种舍入技术,仍然存在边界条件,例如此类条件将在您期望它向上舍入时向下舍入。它们很少发生,通常不会通过简单测试或观察发现。您可能需要编写一些代码来搜索表明结果不符合预期的示例。

假设您想将某个东西舍入到最接近的一分钱。因此,您将最终结果乘以100,加上0.5,截断,然后将结果除以100以回到一分钱。如果您存储的内部数字是3.46499999...而不是3.465,则在将数字舍入到最接近一分钱时,您将得到3.46而不是3.47。但是,您的十进制计算可能表明答案应该恰好是3.465,这显然应向上舍入为3.47而不是向下舍入为3.46。当您在财务计算中使用双精度浮点数时,这些情况偶尔会发生。它很少发生,因此通常被视为不是问题,但确实会发生。

如果您在内部计算中使用十进制而非双精度浮点数,则答案始终与人类期望完全相符,假设您的代码中没有其他错误。


5
相关、有趣:在我的 Chrome JS 控制台中: Math.round(.4999999999999999): 0 Math.round(.49999999999999999): 1 - Curtis Yallop
23
这个答案是误导性的。1.40 * 165 = 231。除了恰好是231的数字,其他数字在数学意义上(以及其他任何意义上)都是错误的。 - Karu
3
@Karu 我认为这就是Randy说浮点数不好的原因...我的Chrome JS控制台显示230.99999999999997作为结果。那确实是错误的,这也是答案中提到的要点。 - trusktr
6
@Karu:在我看来,这个答案在数学上并没有错误。只是有两个问题,其中一个被回答了,而这不是问题的实际问法。你编译器所回答的问题是1.39999999 * 164.99999999 等等,这个在数学上是正确的,相当于 230.99999... 显然,这不是最初提出的问题... - markus
3
@CurtisYallop 因为离0.49999999999999999最近的double值是0.5。为什么Math.round(0.49999999999999994)返回1? - phuclv
显示剩余8条评论

69

我对其中一些回答感到困扰。我认为双精度和浮点数在金融计算中有其用处。当添加和减去非分数金额时,使用整数类或BigDecimal类不会丢失精度。但是,在执行更复杂的操作时,无论如何存储数字,您通常都会得到结果超出几个或多个小数位。问题在于如何呈现结果。

如果您的结果处于舍入和上取整之间,而最后一个便士真的很重要,您应该向观众表明答案接近中间-通过显示更多的小数位。

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

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

导致结果为

1.1875

24
这个!!!我一直在寻找答案,找到了这个相关的事实!!!在普通的计算中,如果你偏离一分钱,没人会在意,但是在这里,由于涉及高额数字,每次交易可能会损失几美元! - Falco
26
现在想象一下,如果有人每天从他的一百万美元中获得0.01%的收益率,他每天将获得零收益。一年之后,他仍然没有获得1000美元,这将是很重要的事情。 - Falco
9
问题不在于准确性,而是浮点数无法告诉你它何时变得不准确。整数只能保留10个数字,而浮点数最多可以保留6个数字而不失精度(当你相应地截取它时)。虽然它允许这样做,但整数会出现溢出错误,例如Java语言会发出警告或不允许这种情况发生。使用双精度浮点数时,您可以达到16个数字,这已足够满足许多用例的需求。 - sigi
1
@Klaws 谢谢你提供了具体信息。我感觉我开始理解了。但是我对欧洲税法不熟悉,因此感到困惑。卖方应该以“最终用户价格”(包括税)显示价格,然后取€0.02的最终用户价格,其中包括卖方的€0.017和€0.003的税,将其乘以1000得到卖方的€17.00和€3.00的税,这样正确吗?这感觉很奇怪(从美国的角度来看,税收总是在最后计算,从不包含在广告价格中),在这种情况下,€17.00 @19% 的税费应该是€3.23。谢谢! - Josiah Yoder
1
@Josiah Yoder 欧盟的增值税法律非常复杂。自欧元引入以来,三位小数是强制性的,这意味着应用程序通常使用4位小数以确保正确的舍入。显示的价格通常是最终用户价格,但通常存储为净价格(不含增值税)。在德国,增值税是按交货结束时计算的,而不是按单个物品计算。我认为荷兰允许为每个项目计算税款并在最后加总。对于德国的增值税预付款,适用不同的规则(甚至在某一点上向下舍入到零位)。 - Klaws
显示剩余6条评论

53
我会冒被踩的风险,但我认为浮点数在货币计算中不适用的问题被高估了。只要确保你正确地进行分转换单位,并有足够的有效数字来解决由 zneak 解释的二进制和十进制表示不匹配的问题,就没有问题。

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

当然,你必须合理地控制;例如,一个简单的网店可能永远都不会遇到使用双精度浮点数的问题,但如果你需要做会计或任何需要添加大量(不受限制)数字的工作,你就不会碰浮点数。


7
这实际上是一个相当不错的回答。在大多数情况下,使用它们是完全可以的。 - Vahid Amiri
3
值得注意的是,大多数投资银行和大多数C++程序都使用双精度浮点数。一些使用长整型,但这会带来跟踪数据范围的问题。 - Peter Lawrey
2
我觉得这个答案很有趣。我猜你和@PeterLawrey是从经验中说出来的。能否找到引用/网页链接来支持你们的说法呢?根据我的经验,我知道公司经常在Excel中使用财务信息。但投资银行是否也使用double呢? - Josiah Yoder
4
很多年前,当一位会计师说他们无法接受账簿中有一分钱的差异时,我第一次接触到这个问题。 - aled
1
在Excel中使用货币计算的人一直使用双精度浮点数……我还没有看到有人抱怨舍入误差。这是因为Excel对其显示的数字进行了各种显然是特别定制的额外处理,以消除大多数人会抱怨的异常情况。因此,Excel实际上不能用作这个问题的论据。 - Steve Summit
显示剩余9条评论

41

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

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

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

问题在于IEEE规范没有一种能够精确表示所有分数的方法,其中一些分数最终会变成重复的分数,因此会出现近似误差。由于会计喜欢事情恰好相符,而客户如果支付账单后,在付款被处理后还欠0.01美元,他们将被收取费用或无法关闭账户,因此最好使用精确类型,如C#中的decimal或Java中的java.math.BigDecimal。

并不是说如果四舍五入可以控制误差:请参见Peter Lawrey的这篇文章。只是在第一次避免进行舍入更容易。大多数处理货币的应用程序都不需要进行大量的数学运算,操作包括添加物品或将金额分配给不同的桶。引入浮点数和舍入只会让事情变得更加复杂。


9
floatdoubleBigDecimal表示_精确的_值。代码到对象的转换以及其他操作均为不精确。这些类型本身并不是不精确的。 - chux - Reinstate Monica
1
@chux:重新阅读了一遍,我认为你说得对,我的措辞可以改进。我会编辑并重新表述。 - Nathan Hughes
尝试使用new BigDecimal("1.01")代替new BigDecimal(1.01F),因为在你的例子中,问题并不是来自于BigDecimal本身,而是1.01F部分。这个1.01F会给你带来1.0099999904632568359375,而BigDecimal可以正常工作。 - Shark
@Shark:我不禁想你是否理解错了我的意思?当然BigDecimal可以正常工作。我从未想鼓励人们将浮点数传递给BigDecimal,这只是一种练习。如果这不清楚,并且您有改进的建议,则欢迎提出。 - Nathan Hughes
@NathanHughes 不,我没有错过重点,对于在10年后回复这个帖子感到抱歉,只是今天从其他不相关的地方遇到它,看到了有些误导但正确的 Groovy 输出。为了改进它,请尝试添加另一件事情: groovy:000> new BigDecimal("1.01") 和那个输出。这将有助于澄清对新手来说实际上什么都不能比字符串(以及更大程度上的 BigDecimal)更准确地保存任何 N-ary 长十进制数 :D - Shark
@Shark:我认为6hats给出了很好的建议。如果你等不及我来处理它,那么请随意编辑。 - Nathan Hughes

24

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

通常是这样,因为双精度类型的精度小于16个数字。如果您需要更高的精度,则不适用于此类型。此外,近似值可能会累积。

必须说,即使使用定点算术,您仍然必须舍入数字,否则如果得到周期性小数,则BigInteger和BigDecimal会产生错误。因此这里也存在一些近似。

例如,历史上用于财务计算的COBOL具有最大18位数字的精度。因此通常会有一个隐式舍入。

总之,在我看来,双精度主要由于其16位数字的精度不足而不适用,这可能会导致不准确。

考虑下面程序的输出。它显示在精度为16的情况下,经过四舍五入后,双精度与BigDecimal给出相同的结果。

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;
    }
}

3
COBOL有一种本地的十进制类型,它是固定点的。这可以精确地引用所有小数类型高达18位数字。这与浮点数不同,无论数字有多少位,因为它是一种本地的十进制类型。0.1始终是0.1,而不是有时候是0.99999999999999。 - lukevp

18

浮点数的结果不是精确的,这使得它们不适合任何需要精确结果而非近似结果的财务计算。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

4
让我们尝试一些不同于简单加减和整数乘法的东西。如果代码计算了7%贷款的月利率,两种类型都无法提供精确值,并需要四舍五入到最接近0.01的数字。将货币单位舍入到最低单位是货币计算的一部分。使用十进制类型可以避免在加减运算时出现这种情况,但并不能解决其他问题。 - chux - Reinstate Monica
@chux-ReinstateMonica:如果利息应该按月复利,则每个月通过将每日余额相加,乘以7(利率),然后除以一年的天数,四舍五入到最近的一分钱来计算利息。 除了在最后一步每月进行一次四舍五入之外,任何地方都不进行四舍五入。 - supercat
1
@supercat 我的评论强调使用最小货币单位的二进制浮点数或十进制浮点数都会遇到类似的舍入问题,就像你在评论中提到的“除法并四舍五入至最接近一分钱”。在你的情况下,使用基于2或基于10的浮点数都没有优劣之分。 - chux - Reinstate Monica
在上述情况下,如果数学计算出利息应该恰好等于一些半分数,正确的财务程序必须以精确指定的方式四舍五入。如果浮点计算产生了例如$1.23499941的利息值,但在舍入之前数学上精确的值应该是$1.235,并且舍入被指定为“最接近偶数”,使用这样的浮点计算不会导致结果偏差$0.000059,而是整个$0.01,这对于会计目的来说是完全错误的。 - supercat
1
正确进行财务/会计计算所需的是仅使用数学精确运算,除非在明确定义舍入的地方。当正确地进行数字除法时,必须指定舍入、计算商和余数,或商与除数的乘积必须精确等于被除数。未指定舍入或余数而进行7的除法通常是错误的。 - supercat
显示剩余2条评论

18

正如之前所说,"将货币表示为double或float可能一开始看起来很好,因为软件会四舍五入处理微小误差,但是随着对不精确数字进行更多的加减乘除运算,您会失去越来越多的精度,因为误差会累积。这使得浮点数和双精度数不适用于处理货币,而在货币交易中需要对10的幂次方的倍数进行完美的精确度。"

最后,Java有了一种标准的处理货币和金钱的方法!

JSR 354:Money and Currency API

JSR 354提供了一种API,用于表示、传输和执行与货币和货币有关的综合计算。您可以从此链接下载:

JSR 354: Money and Currency API Download

规范包括以下内容:

  1. 用于处理货币金额和货币等的API
  2. 支持可互换实现的API
  3. 创建实现类实例的工厂
  4. 对货币金额进行计算、转换和格式化的功能
  5. Java API可用于处理货币和货币,计划包括在Java 9中。
  6. 所有规范类和接口都位于javax.money.*包中。

JSR 354:Money and Currency 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还支持使用MonetaryAmount进行计算:

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;

可以使用四舍五入运算符对货币金额进行舍入:

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

当处理货币金额的集合时,可以使用一些方便的实用方法来进行过滤、排序和分组。

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"));

自定义MonetaryAmount操作

// 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

资源:

使用JSR 354在Java中处理货币和货币

了解Java 9货币和货币API(JSR 354)

另请参阅:JSR 354-货币和货币


Java 9中提到MonetaryAmount,值得称赞。 - omerhakanbilici

7
大多数答案都强调了为什么不应该使用double进行货币和货币计算的原因。我完全同意他们的观点。
但这并不意味着double永远不能用于此目的。
我曾在许多需要极低gc的项目上工作,而拥有BigDecimal对象是导致gc开销大的重要因素之一。
这是由于缺乏对double表示和精度处理的理解和经验,才会出现这个明智的建议。
如果您能够处理项目的精度和准确性要求,并且根据所处理的double值范围来完成这项工作,则可以使其正常工作。
您可以参考Guava的FuzzyCompare方法来获取更多想法。容差参数是关键。
我们曾为证券交易应用程序解决过这个问题,并对在不同范围内使用哪些公差进行了详尽的研究。
此外,可能会出现诱惑您使用Double包装器作为哈希图的映射键的情况。这非常危险,因为例如值“0.5”和“0.6-0.1”的Double.equals和哈希码将引起混乱。

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