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

我一直被告知永远不要doublefloattypes代表金钱,这次我向你提出这个问题:为什么?

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

因为漂浮和双打不能准确地代表我们用来赚钱的基数的10倍。 这个问题不仅适用于Java,它适用于任何使用本机浮点types的编程语言,因为它源于默认情况下计算机如何处理浮点数。

这就是IEEE-754浮点数的工作原理:它为符号贡献了一点,为基数存储了一些指数,剩下的则用于该基数的倍数。 这导致像10.25这样的数字以类似于1025 * 10 -2的forms表示; 除了基数是10,对于float s和double s,它是2,所以这将是164 * 2 -4 。 (这还不完全如何用硬件来表示,但这很简单,math也是一样。)

即使在基数10,这个记号也不能准确地表示最简单的分数。 例如,对于大多数计算器来说,1/3会导致0.333333333333的重复,而数字显示允许的数量是3,因为只能用十进制表示1/3。 但是,为了资金的目的(至less对于货币价值在美元数量级以内的国家),在大多数情况下,您所需要的只是能够存储10 -2的倍数,所以我们不真正关心的是1/3是否具有整数乘以10的整数倍的精确表示,甚至最便宜的计算器也能处理好。

浮动和双打的问题是绝大多数类似于钱的数字没有一个整数次幂的整数。 实际上,0/100和100/100之间的百分之几(这对于处理货币是重要的,因为它们是整数美分),可以精确地表示为一个IEEE-754二进制浮点数,它们是0, 0.25,0.5,0.75和1.所有其他的都是less量的。

将货币表示为doublefloat ,首先可能会看起来不错,因为软件会消除这些微小的错误,但是当您对不精确的数字执行更多的加法,减法,乘法和除法时,错误会增加向上。 这使浮动和双打不足以处理金钱,要求完美的准确性的基数10倍数的倍数。

一种适用于任何语言的解决scheme是使用整数来代替美分。 例如,1025将是10.25美元。 几种语言也有内置的types来处理金钱。 其中,Java有BigDecimal类,C#有decimaltypes。

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

floatdoubletypes特别不适合进行货币计算,因为不可能将0.1(或10的任何其他负数)表示为floatdouble

例如,假设你有$ 1.03,你花了42c。 你剩下多less钱?

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

打印出0.6100000000000001

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

这不是一个准确的问题,也不是一个精确的问题。 这是满足人类使用基数10来计算而不是基数2的期望的问题。例如,使用双打进行财务计算不会产生在math意义上是“错误”的答案,但它可以产生答案而不是财务意义上的预期。

即使您在输出前的最后一刻将结果四舍五入,仍然可以偶尔使用与预期不符的双打结果。

使用计算器或手工计算结果,1.40 * 165 = 231。 然而,在我的编译器/操作系统环境中,内部使用双精度,它被存储为一个接近230.99999的二进制数…所以如果你截断数字,你得到230而不是231.你可能会导致舍入而不是截断已经给出了期望的结果231.这是真的,但四舍五入总是涉及截断。 无论你使用的是什么四舍五入技术,当你期待它圆整时,仍然会有这样的边界条件。 它们非常罕见,通常不会通过临时testing或观察发现。 您可能需要编写一些代码来search示例,说明结果不像预期的那样。

假设你想将某物转到最近的一分钱。 所以你把最后的结果乘以100,加0.5,截断,然后把结果除以100得到硬币。 如果您存储的内部号码是3.46499999 ….而不是3.465,那么当您将号码四舍五入到最接近的一分钱时,您将得到3.46而不是3.47。 但是你的基数10的计算可能已经表明答案应该是3.465,显然应该是3.47,而不是3.46。 当您使用双打进行财务计算时,这些事情偶尔会在现实生活中发生。 这是罕见的,所以它经常被忽视作为一个问题,但它发生。

如果你用内部计算而不是双打来使用base 10,那么假设你的代码中没有其他的错误,那么答案总是和人类预期的一样。

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

如果你的结果处于四舍五入状态,而最后一分钱真的很重要,那么你应该告诉观众,答案几乎在中间 – 通过显示更多的小数位。

双打的问题,以及浮动的问题更多的是当他们被用来结合大量和小数目。 在java中,

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

结果是

 1.1875 

浮动和双打是近似的。 如果你创build一个BigDecimal并将一个float传递给构造函数,你会看到float实际上等于什么:

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

这可能不是你想要代表的$ 1.01。

问题是IEEE规范没有办法精确地表示所有的分数,其中一些最终重复分数,所以你最终得到近似误差。 由于会计师喜欢的东西就是一分钱出来的,客户如果支付账单,处理付款之后就会感到恼火,他们要支付一定的费用或者不能closures账户,所以最好使用精确的types,如十进制(在C#中)或Java中的java.math.BigDecimal。

这并不是说你的错误是无法控制的: 见Peter Lawrey的这篇文章 。 第一个地方不用轮回就更简单了。 大多数处理金钱的应用程序并不需要大量的math运算,这些操作包括添加事物或将金额分配给不同的存储桶。 引入浮点和舍入只是使事情复杂化。

浮点数的结果是不准确的,这使得它们不适合于需要精确结果而不是近似的任何财务计算。 float和double是为工程和科学计算而devise的,很多时候不会产生精确的结果,浮点计算的结果也可能因JVM而不同。 看下面BigDecimal和用来表示货币价值的双重原型的例子,很明显浮点计算可能不准确,应该使用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 

虽然浮点types只能表示十进制数据的近似值,但是如果在提供数字之前将数字舍入到必要的精度,那么也可以得到正确的结果。 通常。

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

必须说,即使你使用定点算术,你仍然需要整数,如果你得到周期性的十进制数,那么不是因为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; } } 

正如前面所说的“当货币代表双重或浮动时,首先可能看起来不错,因为软件会消除微小的错误,但是当您对不精确的数字进行更多的加法,减法,乘法和除法,将会失去越来越多的精度因为错误加起来,这使浮动和加倍不足以处理金钱,要求完全准确的基数10倍数的倍数。

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

JSR 354:货币和货币API

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

JSR 354:货币和货币API下载

规范包含以下内容:

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

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

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

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

CurrencyUnit和MonetaryAmount

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

在处理MonetaryAmounts集合时,可以使用一些很好的实用方法进行过滤,sorting和分组。

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

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

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

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

如果你的计算涉及各个步骤,任意的精确算术不会100%覆盖你。

使用完美表示结果的唯一可靠方法(使用自定义分数数据types,将划分操作分批到最后一步),并只在上一步中转换为十进制表示法。

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

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

一些例子…这工作(实际上不能按预期工作),在几乎所有的编程语言…我已经试过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... thats why you should not use Double for some math!!!"); } 

OUTPUT:

round problems?: current total: 0.9999999999999999 round problems?: current total: 2.7755575615628914E-17 round problems?: is total equal to ZERO? NO... thats why you should not use Double for some math!!!

我更喜欢使用Integer或Long来表示货币。 BigDecimal太多吞噬了源代码。

你只需要知道你所有的价值都在美分。 或者是您使用的任何币种的最低价值。

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

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

有什么select? 有很多(还有更多我不知道!)。

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

C#中的十进制数据types是Microsoft的.NET替代品,用于28位有效数字。

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

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

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

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

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

对我而言,我喜欢BigDecimal,因为它支持的方法。 C#的十进制是非常好的,但我没有机会像它那样多处理它。 我在业余时间对科学计算感兴趣,BigDecimal似乎工作得很好,因为我可以设置我的浮点数的精度。 BigDecimal的缺点? 有时可能会很慢,特别是如果您使用除法。

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