我总是被告知永远不要用双类型或浮点类型来表示货币,这一次我向你提出一个问题:为什么?

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


当前回答

我将冒着被否决的风险,但我认为浮点数在货币计算中的不适用性被高估了。只要确保正确地进行了舍入,并且有足够的有效数字来处理zneak解释的二进制十进制表示不匹配,就不会有问题。

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

当然,你必须在合理范围内;例如,一个简单的网络商店可能永远不会遇到双精度浮点数的任何问题,但如果你做会计或其他需要添加大量(无限制)数字的事情,你不会想要用十英尺的杆子触摸浮点数。

其他回答

如前所述,“把钱表示为双位数或浮点数,一开始可能看起来不错,因为软件会消除微小的错误,但当你对不精确的数字进行更多的加减乘除时,随着错误的增加,你会失去越来越多的精度。”这使得浮点数和双精度数不适用于处理货币,因为货币需要精确计算以10为底数的倍数。”

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

JSR 354:货币和货币API

JSR 354提供了一个API,用于表示、传输和执行Money和Currency的综合计算。你可以从以下连结下载:

JSR 354:货币和货币API下载

该规范包括以下内容:

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

JSR 354: Money and Currency API示例:

创建一个moneyaryamount并将其打印到控制台的示例如下:

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

CurrencyUnit和moneyaryamount

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

moneyaryamount有各种方法,允许访问指定的货币,数字金额,其精度和更多:

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;

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

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

当使用monearyamount的集合时,可以使用一些不错的实用程序方法进行过滤、排序和分组。

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

自定义monearyamount操作

// 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 -货币和货币

因为浮点数和双精度数不能准确地表示我们用来表示金钱的以10为底的倍数。这个问题不仅适用于Java,还适用于任何使用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不能表示并没有什么关系。

The problem with floats and doubles is that the vast majority of money-like numbers don't have an exact representation as an integer times a power of 2. In fact, the only multiples of 0.01 between 0 and 1 (which are significant when dealing with money because they're integer cents) that can be represented exactly as an IEEE-754 binary floating-point number are 0, 0.25, 0.5, 0.75 and 1. All the others are off by a small amount. As an analogy to the 0.333333 example, if you take the floating-point value for 0.01 and you multiply it by 10, you won't get 0.1. Instead you will get something like 0.099999999786...

把钱表示成双位数或浮点数一开始可能看起来不错,因为软件会消除微小的错误,但当你对不精确的数字进行更多的加减乘除运算时,错误就会加剧,最终你会得到明显不准确的数值。这使得浮点数和双精度数不适用于处理货币,因为货币需要精确计算以10为底数的倍数。

一种适用于任何语言的解决方案是使用整数,并计算美分。例如,1025就是10.25美元。一些语言也有内置的类型来处理钱。其中,Java有BigDecimal类,Rust有rust_decimal板条箱,c#有decimal类型。

The result of floating point number is not exact, which makes them unsuitable for any financial calculation which requires exact result and not approximation. float and double are designed for engineering and scientific calculation and many times doesn’t produce exact result also result of floating point calculation may vary from JVM to JVM. Look at below example of BigDecimal and double primitive which is used to represent money value, its quite clear that floating point calculation may not be exact and one should use BigDecimal for financial calculations.

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

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

这不是精确与否的问题,也不是精确与否的问题。这是一个满足以10为底而不是以2为底计算的人的期望的问题。例如,在财务计算中使用双精度值不会产生数学意义上的“错误”答案,但它可以产生财务意义上不期望的答案。

即使您在输出前的最后一分钟舍入结果,您仍然可以偶尔使用与期望不匹配的双精度结果。

Using a calculator, or calculating results by hand, 1.40 * 165 = 231 exactly. However, internally using doubles, on my compiler / operating system environment, it is stored as a binary number close to 230.99999... so if you truncate the number, you get 230 instead of 231. You may reason that rounding instead of truncating would have given the desired result of 231. That is true, but rounding always involves truncation. Whatever rounding technique you use, there are still boundary conditions like this one that will round down when you expect it to round up. They are rare enough that they often will not be found through casual testing or observation. You may have to write some code to search for examples that illustrate outcomes that do not behave as expected.

Assume you want to round something to the nearest penny. So you take your final result, multiply by 100, add 0.5, truncate, then divide the result by 100 to get back to pennies. If the internal number you stored was 3.46499999.... instead of 3.465, you are going to get 3.46 instead 3.47 when you round the number to the nearest penny. But your base 10 calculations may have indicated that the answer should be 3.465 exactly, which clearly should round up to 3.47, not down to 3.46. These kinds of things happen occasionally in real life when you use doubles for financial calculations. It is rare, so it often goes unnoticed as an issue, but it happens.

如果您使用以10为基数进行内部计算,而不是使用双数,则如果您的代码中没有其他错误,那么结果总是完全符合人类的预期。