我总是被告知永远不要用双类型或浮点类型来表示货币,这一次我向你提出一个问题:为什么?
我相信有一个很好的理由,我只是不知道是什么。
我总是被告知永远不要用双类型或浮点类型来表示货币,这一次我向你提出一个问题:为什么?
我相信有一个很好的理由,我只是不知道是什么。
当前回答
为了补充前面的答案,在处理问题中解决的问题时,除了BigDecimal之外,还可以选择在Java中实现Joda-Money。Java模块名称为org.joda.money。
它需要Java SE 8或更高版本,并且没有依赖关系。
更准确地说,存在编译时依赖关系,但它不是 必需的。
<dependency>
<groupId>org.joda</groupId>
<artifactId>joda-money</artifactId>
<version>1.0.1</version>
</dependency>
使用Joda Money的例子:
// create a monetary value
Money money = Money.parse("USD 23.87");
// add another amount with safe double conversion
CurrencyUnit usd = CurrencyUnit.of("USD");
money = money.plus(Money.of(usd, 12.43d));
// subtracts an amount in dollars
money = money.minusMajor(2);
// multiplies by 3.5 with rounding
money = money.multipliedBy(3.5d, RoundingMode.DOWN);
// compare two amounts
boolean bigAmount = money.isGreaterThan(dailyWage);
// convert to GBP using a supplied rate
BigDecimal conversionRate = ...; // obtained from code outside Joda-Money
Money moneyGBP = money.convertedTo(CurrencyUnit.GBP, conversionRate, RoundingMode.HALF_UP);
// use a BigMoney for more complex calculations where scale matters
BigMoney 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
其他回答
大多数回答都强调了为什么不应该使用替身来计算金钱和货币。我完全同意他们的观点。
但这并不是说,double永远不能用于这个目的。
我曾经参与过许多gc需求非常低的项目,BigDecimal对象是造成这种开销的一个重要因素。
正是由于缺乏对双重表示的理解,以及缺乏处理准确性和精确性的经验,才产生了这个明智的建议。
如果您能够处理项目的精度和准确性要求,则可以使其工作,这必须基于处理的双精度值的范围来完成。
你可以参考番石榴的FuzzyCompare方法来获得更多的信息。参数公差是关键。 我们为一个证券交易应用程序处理了这个问题,并对在不同范围内对不同数值使用什么公差做了详尽的研究。
此外,在某些情况下,您可能会试图使用Double包装器作为映射键,并将哈希映射作为实现。这是非常危险的,因为双重。等号和哈希码,例如值“0.5”和“0.6 - 0.1”将导致一个大混乱。
摘自Bloch, J., Effective Java,(第二版,第48项。第3版,项目60):
float和double类型是 尤其不适用于货币 因为这是不可能的 表示0.1(或任何其他。 10的负次方)作为浮点数或 完全的两倍。 例如,假设您有1.03美元 你花了42c。多少钱? 你走了? System.out.println(1.03 - .42); 输出0.6100000000000001。 解决这个问题的正确方法是 使用BigDecimal, int或long 用于货币计算。
虽然BigDecimal有一些警告(请参阅当前接受的答案)。
我对其中一些回答感到困扰。我认为双数和浮点数在财务计算中占有一席之地。当然,在使用整数类或BigDecimal类时,在加减非分数货币金额时,不会损失精度。但是,当执行更复杂的操作时,无论您如何存储这些数字,您经常会得到小数点后几位或许多位的结果。问题在于你如何呈现结果。
如果你的结果是在四舍五入和四舍五入之间的边缘,最后一分真的很重要,你可能应该告诉观众答案几乎在中间——通过显示更多的小数点后数位。
双精度浮点数的问题是,当它们被用来组合大数和小数时。在java中,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
结果
1.1875
浮点数和双精度数是近似的。如果你创建了一个BigDecimal并将一个float传递给构造函数,你会看到float实际等于什么:
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
这可能不是您想要的表示1.01美元的方式。
问题是IEEE规范没有一种方法来精确地表示所有的分数,其中一些分数最终是重复的分数,所以你最终会得到近似错误。由于会计人员喜欢精确到每一分钱,如果客户支付账单,在付款处理后他们欠0.01,他们会被收取费用或无法关闭他们的帐户,那么最好使用精确的类型,如decimal(在c#中)或Java. math. bigdecimal。
这并不是说如果你四舍五入,误差就无法控制:请参阅Peter Lawrey的这篇文章。只是从一开始就不用四舍五入更容易。大多数处理资金的应用程序不需要大量的数学运算,操作包括添加东西或将金额分配到不同的存储空间。引入浮点数和舍入只会使事情复杂化。
如前所述,“把钱表示为双位数或浮点数,一开始可能看起来不错,因为软件会消除微小的错误,但当你对不精确的数字进行更多的加减乘除时,随着错误的增加,你会失去越来越多的精度。”这使得浮点数和双精度数不适用于处理货币,因为货币需要精确计算以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 -货币和货币