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

我总是被告知从未doublefloat类型代表金钱,这次我向你提出问题:为什么?

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

385874 次浏览

因为浮点数和双精度不能准确地表示我们用于货币的基数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不能代表并不重要。

浮点数和双精度数的问题在于,类似货币的数字绝大多数不能精确地表示为整数乘以2的幂。事实上,0和1之间的0.01的唯一倍数(在处理货币时很重要,因为它们是整数美分)可以准确地表示为IEEE-754二进制浮点数的是0、0.25、0.5、0.75和1。所有其他的都有一点偏差。作为0.333333示例的类比,如果你将0.01的浮点值乘以10,你不会得到0.1。相反,你会得到类似0的东西。099999999786…

将货币表示为doublefloat,一开始可能看起来不错,因为软件会消除微小的错误。但是,当你对不精确的数字执行更多的加法、减法、乘法和除法时,错误会加剧,最终你会得到明显不准确的值。这使得浮点数和双打数不足以处理货币,在这种情况下,需要完美的精度来计算基数10的倍数。

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

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

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

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

问题是IEEE规范没有办法精确地表示所有的分数,其中一些最终成为重复的分数,所以你最终会产生近似误差。由于会计师喜欢精确到一分钱的东西,如果他们支付账单,并且在支付处理后他们欠了.01,客户会感到恼火,他们会被收取费用或无法关闭他们的账户,最好使用精确的类型,如decimal(在C#中)或java.math.BigDecimal在Java。

这并不是说如果你对:本文作者:Peter Lawrey进行舍入,错误就无法控制。只是一开始不必舍入会更容易。大多数处理金钱的应用程序不需要大量的数学运算,操作包括添加东西或将金额分配到不同的桶中。引入浮点和舍入只会使事情复杂化。

来自布洛赫,J.,有效Java(第2版,项目48.第3版,项目60):

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

例如,假设你有1.03美元你花了42c。多少钱你离开了吗?

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

打印0.6100000000000001

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

虽然BigDecimal有一些警告(请参阅当前接受的答案)。

这不是精确度的问题,也不是精确度的问题。这是一个满足以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进行内部计算而不是双精度计算,则答案始终与人类预期的完全相同,假设您的代码中没有其他错误。

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

通常是因为双精度类型的精度小于16位数字。如果您需要更高的精度,它不是一种合适的类型。近似值也可以累加。

必须指出的是,即使你使用定点算术,你仍然需要整数,如果不是因为如果你获得周期十进制数,BigInteger和BigDecimal会出错。所以这里也有一个近似值。

例如,COBOL历来用于金融计算,其最大精度为18位数字。因此通常存在隐式舍入。

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

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

Precision 14------------------------------------------------------BigDecimalNoRound             : 56789.012345 / 1111111111 = Non-terminating decimal expansion; no exact representable decimal result.DoubleNoRound                 : 56789.012345 / 1111111111 = 5.111011111561101E-5BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611Double                        : 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-5BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110Double                        : 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-5BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101Double                        : 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-5BigDecimal                    : 56789.012345 / 1111111111 = 0.000051110111115611011Double                        : 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-5BigDecimal                    : 56789.012345 / 1111111111 = 0.0000511101111156110111Double                        : 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-5BigDecimal                    : 56789.012345 / 1111111111 = 0.00005111011111561101111Double                        : 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) {// acceptancedouble amount0 = Double.parseDouble(amount);double quantity0 = Double.parseDouble(quantity);
//calculationdouble price0 = amount0 / quantity0;
// presentationString price = Double.toString(price0);return price;}
public static String divideUsingDouble(String amount, String quantity,int precision) {// acceptancedouble amount0 = Double.parseDouble(amount);double quantity0 = Double.parseDouble(quantity);
//calculationdouble price0 = amount0 / quantity0;
// presentationMathContext precision0 = new MathContext(precision);String price = new BigDecimal(price0, precision0).toString();return price;}
public static String divideUsingBigDecimal(String amount, String quantity,int precision) {// acceptanceBigDecimal amount0 = new BigDecimal(amount);BigDecimal quantity0 = new BigDecimal(quantity);MathContext precision0 = new MathContext(precision);
//calculationBigDecimal price0 = amount0.divide(quantity0, precision0);
// presentationString price = price0.toString();return price;}
public static String divideUsingBigDecimalNoRound(String amount, String quantity,int precision) {// acceptanceBigDecimal amount0 = new BigDecimal(amount);BigDecimal quantity0 = new BigDecimal(quantity);
//calculationBigDecimal price0 = amount0.divide(quantity0);
// presentationString price = price0.toString();return price;}}

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

如果你的结果是在四舍五入和四舍五入之间的边界,最后一分钱真的很重要,你应该告诉观众,答案几乎在中间-通过显示更多的小数位。

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

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

导致

1.1875

浮点数的结果不精确,这使得它们不适合任何需要精确结果而不是近似值的财务计算。浮点数和双精度是为工程和科学计算而设计的,很多时候不能产生精确的结果,浮点计算的结果可能因JVM而异。看看下面的例子BigDecimal和双基元用于表示货币价值,很明显浮点计算可能不精确,应该使用BigDecimal进行财务计算。

    // floating point calculationfinal 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 calculationfinal 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.8999999999999999difference between 2.0 and 1.1 using BigDecimal is: 0.9

如果您的计算涉及各种步骤,任意精度算术不会100%覆盖您。

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

任意精度没有帮助,因为总是可以有这么多小数位的数字,或者一些结果,如0.6666666…没有任意表示将涵盖最后一个例子。所以你在每一步都会有小错误。

这些错误会加起来,最终可能变得不再容易忽略。这称为错误传播

这个问题的许多答案都讨论了IEEE和围绕浮点运算的标准。

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

有什么替代方案?有很多(还有更多我不知道的!)。

Java中的BigDecimal是Java语言的原生语言。Apflow是另一个用于Java的任意精度库。

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

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

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

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

还有专有的(特别是,我认为,对于Fortran)和开源解决方案以及许多计算机语言。

我不是受过训练的计算机科学家。然而,我倾向于Java的BigDecimal或C#的decimal。我没有尝试过我列出的其他解决方案,但它们可能也很好。

对我来说,我喜欢BigDecimal是因为它支持的方法。C#的小数非常好,但我还没有机会像我想的那样使用它。我在业余时间做我感兴趣的科学计算,BigDecimal似乎工作得很好,因为我可以设置浮点数的精度。BigDecimal的缺点?它有时会很慢,特别是如果你使用除法。

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

看看这个简单的例子:它看起来逻辑上是正确的,但在现实世界中,如果威胁不正确,它可能会返回意想不到的结果:

0.1 x 10=1👍,所以:

double total = 0.0;
// adds 10 cents, 10 timesfor (int i = 0; i < 10; i++) {total += 0.1;  // adds 10 cents}
Log.d("result: ", "current total: " + total);
// looks like total equals to 1.0, don't?
// now, do reversefor (int i = 0; i < 10; i++) {total -= 0.1;  // removes 10 cents}
// total should be equals to 0.0, right?Log.d("result: ", "current total: " + total);if (total == 0.0) {Log.d("result: ", "is total equal to ZERO? YES, of course!!");} else {Log.d("result: ", "is total equal to ZERO? No...");// so be careful comparing equality in this cases!!!}

输出:

 result: current total: 0.9999999999999999result: current total: 2.7755575615628914E-17   🤔result: is total equal to ZERO? No... 😌

如前所述,“将货币表示为双精度或浮点数一开始可能看起来不错,因为软件会消除微小的错误,但随着您对不精确的数字执行更多的加法、减法、乘法和除法,随着错误的增加,您将失去越来越多的精度。这使得浮点数和双精度不足以处理货币,而在处理货币时需要完美的精度,以10为基数的倍数。”

最后Java有一个标准的方法来使用货币和金钱!

JSR 354:货币和货币API

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

JSR 354: Money and Money cy API下载

该规范包括以下内容:

  1. 用于处理例如货币金额和货币的API
  2. 支持可互换实现的API
  3. 用于创建实现类实例的工厂
  4. 计算、转换和格式化货币金额的功能
  5. Java用于处理金钱和货币的API,计划包含在Java9中。
  6. 所有规范类和接口都位于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 localeCurrencyUnit 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(); // 123double doubleValue = numberValue.doubleValue(); // 123.45long fractionDenominator = numberValue.getAmountFractionDenominator(); // 100long fractionNumerator = numberValue.getAmountFractionNumerator(); // 45int precision = numberValue.getPrecision(); // 5
// NumberValue extends java.lang.Number.// So we assign numberValue to a variable of type NumberNumber 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"));

自定义MonetaryAmount操作

// A monetary operator that returns 10% of the input MonetaryAmount// Implemented using Java 8 LambdasMonetaryOperator 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 MonetaryAmountMonetaryAmount tenPercentDollars = dollars.with(tenPercentOperator); // USD 1.234567

资源说明:

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

查看Java9货币和货币API(JSR 354)

另见:JSR 354-货币和金钱

我会冒着被否决的风险,但我认为浮点数不适合货币计算被高估了。只要你确保正确进行美分四舍五入,并有足够的有效数字来处理,以对抗zneak解释的二进制-十进制表示不匹配,就不会有问题。

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

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

大多数答案都强调了为什么不应该在货币和货币计算中使用双打。我完全同意他们的观点。

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

我曾参与过许多gc要求非常低的项目,而拥有BigDecimal对象是该开销的主要贡献者。

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

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

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

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

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

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

更准确地说,有一个编译时依赖项,但它不是需要

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

使用Joda Money的示例:

  // create a monetary valueMoney money = Money.parse("USD 23.87");  
// add another amount with safe double conversionCurrencyUnit usd = CurrencyUnit.of("USD");money = money.plus(Money.of(usd, 12.43d));  
// subtracts an amount in dollarsmoney = money.minusMajor(2);  
// multiplies by 3.5 with roundingmoney = money.multipliedBy(3.5d, RoundingMode.DOWN);  
// compare two amountsboolean bigAmount = money.isGreaterThan(dailyWage);  
// convert to GBP using a supplied rateBigDecimal conversionRate = ...;  // obtained from code outside Joda-MoneyMoney moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);  
// use a BigMoney for more complex calculations where scale mattersBigMoney 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

Float是Decimal的二进制形式,具有不同的设计;它们是两个不同的东西。当相互转换时,两种类型之间几乎没有错误。此外,浮点数被设计为代表无限大数量的科学值。这意味着它被设计为在固定字节数的情况下失去对极小和极大数字的精度。Decimal不能代表无限数量的值,它只限于十进制位数。所以Float和Decimal有不同的用途。

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

  1. 使用长整数并以美分为单位。

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

  3. 使用像JavaBigDecimal这样的十进制库,这样你就不需要使用双精度来模拟十进制。

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

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