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

Fra*_*ick 887 floating-point currency

我总是被告知永远不要代表钱doublefloat类型,这次我向你提出问题:为什么?

我确信有一个很好的理由,我根本不知道它是什么.

zne*_*eak 929

因为浮点数和双打数不能准确地代表我们用于赚钱的基数10倍数.这个问题不仅适用于Java,也适用于任何使用base 2浮点类型的编程语言.

在基数10中,您可以将10.25写为1025*10 -2(10 的幂的整数倍).IEEE-754浮点数是不同的,但考虑它们的一种非常简单的方法是乘以2的幂.例如,你可能会看到164*2 -4(2 的幂的整数倍),也等于10.25.这不是数字在记忆中的表现方式,但数学含义是相同的.

即使在基数10中,这种表示法也不能准确地表示最简单的分数.例如,你不能代表1/3:十进制表示重复(0.3333 ...),因此没有有限整数可以乘以10的幂来得到1/3.你可以选择一个3的长序列和一个小指数,如333333333*10 -10,但它不准确:如果你乘以3,你就不会得到1.

但是,为了计算货币,至少对于货币价值在美元数量级范围内的国家来说,通常你所需要的就是能够存储10 -2的倍数,所以它并不重要1/3无法表示.

浮点数和双打数的问题在于,绝大多数类似货币的数字没有精确表示为2的幂的整数倍.实际上,0到1之间的唯一倍数为0.01(这在交易时很重要)有钱,因为它们是整数分钱),可以完全表示为IEEE-754二进制浮点数,分别是0,0.25,0.5,0.75和1.所有其他的都是少量的.与0.333333示例类似,如果将浮点值设为0.1并将其乘以10,则不会得到1.

当软件完成微小错误时,代表钱作为double或者float看起来可能看起来很好,但是当您对不精确的数字执行更多的加法,减法,乘法和除法时,错误将会复合并且您最终会得到明显的值不准确.这使浮动和双打不足以处理金钱,其中需要基本10倍数的倍数的完美精确度.

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

  • @ linuxuser27我觉得弗兰试图搞笑.无论如何,zneak的回答是我见过的最好的,甚至比Bloch的经典版更好. (6认同)
  • ...大多数基数为10的分数,即.例如,0.1没有精确的二进制浮点表示.因此,`1.0/10*10`可能与1.0不同. (5认同)
  • @JoL 你是对的, float(0.1) * 10 ≠ 1 的说法是错误的。在双精度浮点数中,0.1 表示为“0b0.00011001100110011001100110011001100110011001100110011010”,10 表示为“0b1010”。如果你将这两个二进制数相乘,你会得到“1.0000000000000000000000000000000000000000000000000000000000000000000010”,然后四舍五入到可用的53个二进制数字,你正好是1。问题不是他们*,而是浮点数有时*做 - 就像 0.1 + 0.2 ≠ 0.3 的例子。 (5认同)
  • @Fran你会得到四舍五入的错误,在某些情况下使用大量货币,利率计算可能会大幅下降 (3认同)
  • 当然,如果您知道精度,您可以始终对结果进行舍入,从而避免整个问题.这比使用BigDecimal更快更简单.另一种方法是使用固定精度int或long. (3认同)
  • 我知道准确性在金融中很重要。我只是看不出如果使用例如“double”,舍入误差有何意义。在此之前,在它可见之前,您需要进行多少次操作?另一件事是,如果您进行像 $0.01 + $0.02 这样的操作,是的,您最终会出现舍入误差,但是如果您对每个操作进行舍入,这样您就不会传播舍入误差,但最终会保持在精度范围内,该怎么办?那行得通吗? (3认同)
  • 除非您正在构建一个汇率经常达到小数点后 5 位的外汇系统,否则您必须将所有内容相乘,这样您最终就会以微微美元或其他方式工作,以避免舍入错误。 (2认同)
  • @zneak 不,我是 BigDecimal 的忠实粉丝(尽管我通常用 Amount 类包装它,然后将它包装在 Money (Amount + Currency) 对象中)。我指出,只要您要执行 FX 计算,整数就不是很好。 (2认同)
  • @zneak 虽然我们很迂腐,但 IEEE-754 浮点数不一定以 2 为基数。该标准还定义了以 10 为基数的浮点数,尽管它们不太常见。 (2认同)
  • @zneak你知道有理数是实数的_subset_对吗?IEEE-754实数**是**实数.他们只是碰巧**也是理性的. (2认同)
  • 那么@DavidWallace,不要让自己退缩.单独呼吁存在问题不会使它们消失. (2认同)
  • 哇,谢谢你认真对待我的批评.你所做的是一个巨大的进步.我删除了我的downvote并用upvote替换了它.我对最后一句话仍然感到不安,但是我理解你所说的关于错误累积的内容,所以我看到了留下它的智慧.再次感谢你改进这个答案. (2认同)
  • “如果你取 0.1 的浮点值并将它乘以 10,你不会得到 1” 奇怪的是,你确实得到 1,至少在 Javascript、Python、Ruby 和 C(双精度和浮点数)中。也许,它四舍五入到 1。不过,你的观点可以用 `0.1 + 0.2 != 0.3` 来表示。 (2认同)

dog*_*ane 297

来自Bloch,J.,Effective Java,2nd ed,Item 48:

这些floatdouble类型特别不适合进行货币计算,因为不可能将0.1(或任何其他10的负幂)表示为a floatdouble精确.

例如,假设您有1.03美元,而您花费42c.你还剩多少钱?

System.out.println(1.03 - .42);
Run Code Online (Sandbox Code Playgroud)

打印出来0.6100000000000001.

解决这个问题的正确方法是使用BigDecimal,intlong 货币计算.

  • @Peter,你使用`long a = 104`并以美分而不是美元计算. (46认同)
  • @maaartinus ......你不认为使用double这样的东西很容易出错吗?我已经看到浮动舍入问题触及真实系统*难*.甚至在银行业.请不要推荐,或者如果你这样做,请提供单独的答案(因此我们可以将其投票:P) (11认同)
  • 我对使用int或long进行货币计算的建议感到有些困惑.你如何将1.03表示为int或long?我试过"长a = 1.04;" 并且"长a = 104/100;" 无济于事. (5认同)
  • @zneak 什么时候需要应用百分比,比如复利或类似的? (2认同)
  • @trusktr,我会使用你平台的十进制类型.在Java中,那是'BigDecimal`. (2认同)
  • 请问`0.6100000000000001`有什么问题吗?我们可以将其四舍五入为 0.61,对吗?而且,如果我们使用整数和美分,我想知道当我们在计算中得到小于一美分的值时会发生什么?我们是否将它们四舍五入?例如,当有分数时,我们可以将一美元(100美分)除以3,得到33.3333...。现在保存33安全吗?当这些四舍五入的值累积起来时会发生什么?三笔交易几乎损失一分钱!我哪里错了? (2认同)
  • @aderchox `0.6100000000000001` 的问题在于,由于其自​​身的基数为 2,所以存在错误,而不是在进行的计算中,而是在数据类型本身中。从数学上来说,结果是错误的。当然,您可以截断或舍入结果以正确显示结果,但它只会隐藏初始错误。在您的示例中,您可以使用希望以整数数据类型存储的小数金额。如果您不想在每笔交易中损失一分钱,您可以使用 33333 来节省 33.333(多一位小数)。但它变得不切实际,并且从程序上来说,每笔交易的花费超过一美分。 (2认同)
  • @aderchox `0.6100000000000001` 在**货币计算**的背景下完全是错误的。如果你在学校的考试中做这样的数学题,你会得到 F。而且不可能除以 100 克拉。3(将 100 克拉平均分配给 3 方)。对于这种情况,(必须)存在必须考虑的规则,例如“按照给定顺序将除法余数的最小单位分配给各方 Px”,使得 P1 获得 34 cts。P2/P3 获得 33 克拉。我也不同意@Andrés Morales:用 FPN 抽象货币内容并不是数据类型的错误,这只是设计上的错误。 (2认同)

小智 71

这不是准确性问题,也不是精确问题.这是一个满足使用基数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进行内部计算而不是双精度数,那么答案总是完全符合人类的预期,假设您的代码中没有其他错误.

  • 这个答案有误导性.1.40*165 = 231.除了231*之外的任何数字在数学意义上都是错误的(以及所有其他意义). (15认同)
  • @Karu:Imho答案在数学上不是错误的.只是有2个问题被回答,这不是被问到的问题.您的编译器回答的问题是1.39999999*164.99999999等等,数学上正确等于230.99999 ....显然,这不是首先要问的问题.... (5认同)
  • 相关的,有趣的:在我的chrome js控制台:Math.round(.4999999999999999):0 Math.round(.49999999999999999):1 (2认同)
  • @Karu 我认为这就是 Randy 说浮点数不好的原因......我的 Chrome JS 控制台显示 230.999999999999997 作为结果。这是*是*错误的,这是答案中提出的观点。 (2认同)
  • @CurtisYallop,因为与0.49999999999999999的闭合双精度值为0.5 [为什么`Math.round(0.49999999999999994)`返回1?](/sf/ask/693207791/) (2认同)

小智 50

我对其中的一些反应感到不安.我认为双打和花车在财务计算中占有一席之地.当然,在添加和减去非小数货币金额时,使用整数类或BigDecimal类时不会有精度损失.但是,当执行更复杂的操作时,无论您如何存储数字,通常都会得到几个或多个小数位的结果.问题是你如何呈现结果.

如果你的结果是在向上舍入和向下舍入之间的边界,并且最后一分钱真正重要,你可能应该告诉观众答案几乎在中间 - 通过显示更多小数位.

双精度问题,以及浮点数更多的问题是,当它们用于组合大数和小数时.在java中,

System.out.println(1000000.0f + 1.2f - 1000000.0f);
Run Code Online (Sandbox Code Playgroud)

结果是

1.1875
Run Code Online (Sandbox Code Playgroud)

  • 而现在想象一下,有人在他的100万美元上获得0.01%的每日收入 - 他每天都得不到任何东西 - 一年之后他还没有获得1000美元,这一点至关重要 (18认同)
  • 这个!!!!我正在搜索所有答案,以找到这个相关的事实!在正常的计算中,没有人关心你是否只有一分钱,但在这里有很高的数字,每笔交易很容易损失一些美元! (13认同)
  • 问题不是准确性,而是浮点数并不告诉您它变得不准确。整数最多只能容纳10位数字,而浮点数最多可以容纳6位而不会变得不准确(当您相应地削减它时)。当整数溢出时,它确实允许这样做,而Java之类的语言会警告您或不允许这样做。使用双精度字时,最多可以输入16位数字,这足以满足许多使用情况。 (4认同)

Nat*_*hes 38

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

groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
Run Code Online (Sandbox Code Playgroud)

这可能不是你想要代表1.01美元的方式.

问题是IEEE规范没有办法准确地表示所有分数,其中一些最终成为重复分数,因此最终得出近似误差.会计师喜欢把事情准确地说出来一分钱,如果他们付账单就会对客户感到烦恼,而且付款处理完毕后他们欠他们.01他们会收取一定的费用或无法关闭账户,最好使用确切的类型,如十进制(在C#中)或Java中的java.math.BigDecimal.

如果你是圆的,那并不是错误是不可控制的:参见Peter Lawrey的这篇文章.一开始就不容易绕圈.大多数处理资金的应用程序都不需要大量数学,操作包括添加内容或将数量分配给不同的存储桶.引入浮点和舍入只会使事情复杂化.

  • `float`,`double`和`BigDecimal`代表_exact_值.代码到对象转换是不精确的以及其他操作.类型本身并不是不精确的. (2认同)

esc*_*ram 24

我会冒被低估的风险,但我认为浮点数不适合用于货币计算被高估了.只要你确保正确地进行分数舍入并且有足够的有效数字来处理zneak解释的二进制十进制表示不匹配,就没有问题.

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

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

  • 很多年前,我第一次遇到这个问题,当时一位会计师告诉我,他们不能接受账簿上一分钱的差异。 (4认同)
  • 这实际上是一个相当不错的答案。在大多数情况下,使用它们是完全可以的。 (2认同)
  • 应该注意的是,大多数投资银行使用的费用是大多数C ++程序的两倍。有些使用时间很长,但是它有跟踪规模的问题。 (2认同)
  • 我觉得这个答案很有趣。我假设你和@PeterLawrey 都是根据经验说话的。是否可以找到引文/网络链接来支持您的主张?根据我自己的经验,我知道公司一直在使用 Excel 中的财务信息。但投资银行使用双倍又如何呢? (2认同)

use*_*165 20

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

通常因为double类型的精度小于16位数.如果你需要更好的精度,它不是一个合适的类型.也可以累积近似值.

必须要说的是,即使你使用定点算法,你仍然需要舍入数字,如果你得到周期性的十进制数,那么BigInteger和BigDecimal就不会出错.所以这里也有一个近似值.

例如,历史上用于财务计算的COBOL的最大精度为18位数.所以通常会有一个隐含的舍入.

最后,在我看来,双重不适合主要是因为它的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
Run Code Online (Sandbox Code Playgroud)
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;
    }
}
Run Code Online (Sandbox Code Playgroud)

  • COBOL 具有定点的本机十进制类型。这可以准确引用最多 18 位数字的所有十进制类型。无论位数有多少,这与浮点数不同,因为它是本机十进制类型。0.1 永远是 0.1,有时不是 0.99999999999999 (2认同)

Dhe*_*ora 16

浮点数的结果不准确,这使得它们不适用于任何需要精确结果而非近似的财务计算.float和double是为工程和科学计算而设计的,并且很多次都没有产生精确的结果,浮点计算的结果也可能因JVM到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)));
Run Code Online (Sandbox Code Playgroud)

输出:

difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
Run Code Online (Sandbox Code Playgroud)

  • 让我们尝试一些琐碎的加/减和整数乘法操作。如果代码计算了7%贷款的月利率,则这两种类型都将无法提供确切值,并且需要四舍五入到最接近的0.01。四舍五入到最低的货币单位是货币计算的一部分,使用十进制类型可以避免加/减的需求-但没有其他好处。 (3认同)

Vir*_*ual 8

正如之前所说的那样"首先将货币表示为双重或浮动可能看起来很好,因为软件可以完成微小的错误,但是当您对不精确的数字执行更多的加法,减法,乘法和除法时,您将失去越来越多的精度因为错误加起来.这使浮动和双打不足以处理金钱,其中需要基本10倍数倍的完美准确性."

最后,Java有一种标准的方式来处理货币和货币!

JSR 354:货币和货币API

JSR 354提供了一个API,用于使用货币和货币表示,传输和执行综合计算.您可以从以下链接下载:

JSR 354:货币和货币API下载

该规范包括以下内容:

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

JSR 354示例:Money和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));
Run Code Online (Sandbox Code Playgroud)

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

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

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

MonetaryAmount monetaryAmount = Money.of(12345.67, "EUR");
MonetaryAmount otherMonetaryAmount = monetaryAmount.divide(2).add(Money.of(5, "EUR"));
Run Code Online (Sandbox Code Playgroud)

CurrencyUnit和MonetaryAmount

// getting CurrencyUnits by locale
CurrencyUnit yen = MonetaryCurrencies.getCurrency(Locale.JAPAN);
CurrencyUnit canadianDollar = MonetaryCurrencies.getCurrency(Locale.CANADA);
Run Code Online (Sandbox Code Playgroud)

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;
Run Code Online (Sandbox Code Playgroud)

可以使用舍入运算符舍入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
Run Code Online (Sandbox Code Playgroud)

使用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"));
Run Code Online (Sandbox Code Playgroud)

自定义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
Run Code Online (Sandbox Code Playgroud)

资源:

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

研究Java 9货币和货币API(JSR 354)

另见:JSR 354 - 货币和货币


小智 6

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

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

我曾参与过许多对 gc 要求非常低的项目,并且拥有 BigDecimal 对象是造成这种开销的重要因素。

正是由于对双重表示缺乏理解以及在处理准确性和精确性方面缺乏经验,才导致了这一明智的建议。

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

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

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


Kem*_*Dağ 5

如果您的计算涉及多个步骤,那么任意精度的算术都不会100%覆盖您。

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

任意精度将无济于事,因为总会有小数位那么多的数字,或者诸如0.6666666之类的结果。因此,您在每个步骤中都会有一些小错误。

该错误将累加,最终可能变得不容易被忽略。这称为错误传播