我一直被告知不要使用 double
或 float
类型来表示货币,这次我向您提出问题:为什么?
我相信有很好的原因,只是我不知道是什么。
我一直被告知不要使用 double
或 float
类型来表示货币,这次我向您提出问题:为什么?
我相信有很好的原因,只是我不知道是什么。
一种适用于任何语言的解决方案是使用整数来计算分数。例如,1025表示$10.25。许多语言也有内置类型来处理货币。其中,Java有BigDecimal
类,Rust有rust_decimal
crate,C#有decimal
类型。
1.0 / 10 * 10
的结果可能不等于1.0。请注意,这里所说的是二进制浮点数的精度问题。 - C. K. Young0b0.00011001100110011001100110011001100110011001100110011010
表示,而10用0b1010
表示。如果你将这两个二进制数相乘,你会得到1.0000000000000000000000000000000000000000000000000000010
,在它被舍入为53位可用的二进制数字后,你正好得到了1。浮点数的问题不在于它们总是出错,而在于它们有时会出错——就像0.1 + 0.2 ≠ 0.3的例子一样。 - Jim Danner摘自 Bloch, J. 的《Effective Java》(第二版,第48条;第三版,第60条):
float
和double
类型不适合进行货币计算,因为它们不能准确地表示 0.1(或其他任何负十次幂)。例如,假设你有 1.03 美元并花费了 42 美分。你还剩多少钱呢?
System.out.println(1.03 - .42);
输出结果为0.6100000000000001
。
解决此问题的正确方法是使用BigDecimal
、int
或long
进行货币计算。
虽然
BigDecimal
有一些注意事项(请参见当前已接受的答案)。
long a = 104
并将计数单位设为分而不是美元。 - zneakBigDecimal
。 - zneak这不是精度问题,也不是准确性问题。而是满足使用十进制计算而非二进制计算的人类期望的问题。例如,在财务计算中使用双精度浮点数并不会在数学意义上产生“错误”的答案,但可能会产生与财务预期不符的答案。
即使您在输出前最后一刻四舍五入结果,有时仍可能会得到使用双精度浮点数得出的结果与期望不符。
使用计算器或手动计算,1.40*165=231恰好成立。但是,在我的编译器/操作系统环境中,内部使用双精度浮点数将其存储为接近230.99999...的二进制数,因此如果截断该数字,则会得到230而不是231。您可能会认为舍入而不是截断会得到所需的231。这是正确的,但舍入始终涉及截断。无论使用何种舍入技术,仍然存在边界条件,例如此类条件将在您期望它向上舍入时向下舍入。它们很少发生,通常不会通过简单测试或观察发现。您可能需要编写一些代码来搜索表明结果不符合预期的示例。
假设您想将某个东西舍入到最接近的一分钱。因此,您将最终结果乘以100,加上0.5,截断,然后将结果除以100以回到一分钱。如果您存储的内部数字是3.46499999...而不是3.465,则在将数字舍入到最接近一分钱时,您将得到3.46而不是3.47。但是,您的十进制计算可能表明答案应该恰好是3.465,这显然应向上舍入为3.47而不是向下舍入为3.46。当您在财务计算中使用双精度浮点数时,这些情况偶尔会发生。它很少发生,因此通常被视为不是问题,但确实会发生。
如果您在内部计算中使用十进制而非双精度浮点数,则答案始终与人类期望完全相符,假设您的代码中没有其他错误。
我对其中一些回答感到困扰。我认为双精度和浮点数在金融计算中有其用处。当添加和减去非分数金额时,使用整数类或BigDecimal类不会丢失精度。但是,在执行更复杂的操作时,无论如何存储数字,您通常都会得到结果超出几个或多个小数位。问题在于如何呈现结果。
如果您的结果处于舍入和上取整之间,而最后一个便士真的很重要,您应该向观众表明答案接近中间-通过显示更多的小数位。
双精度和尤其是浮点数的问题在于它们被用于组合大数和小数。在Java中,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
导致结果为
1.1875
在 Excel 中进行货币计算的人一直使用双精度浮点数(Excel 中没有货币类型),我还没有看到有人抱怨舍入误差。
当然,你必须合理地控制;例如,一个简单的网店可能永远都不会遇到使用双精度浮点数的问题,但如果你需要做会计或任何需要添加大量(不受限制)数字的工作,你就不会碰浮点数。
浮点数和双精度浮点数都是近似值。如果你创建一个BigDecimal并将一个float传递给构造函数,你会看到这个float实际上等于多少:
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
这可能不是您想要表示1.01的方式。
问题在于IEEE规范没有一种能够精确表示所有分数的方法,其中一些分数最终会变成重复的分数,因此会出现近似误差。由于会计喜欢事情恰好相符,而客户如果支付账单后,在付款被处理后还欠0.01美元,他们将被收取费用或无法关闭账户,因此最好使用精确类型,如C#中的decimal或Java中的java.math.BigDecimal。
并不是说如果四舍五入可以控制误差:请参见Peter Lawrey的这篇文章。只是在第一次避免进行舍入更容易。大多数处理货币的应用程序都不需要进行大量的数学运算,操作包括添加物品或将金额分配给不同的桶。引入浮点数和舍入只会让事情变得更加复杂。
float
、double
和BigDecimal
表示_精确的_值。代码到对象的转换以及其他操作均为不精确。这些类型本身并不是不精确的。 - chux - Reinstate Monicanew BigDecimal("1.01")
代替new BigDecimal(1.01F)
,因为在你的例子中,问题并不是来自于BigDecimal
本身,而是1.01F
部分。这个1.01F
会给你带来1.0099999904632568359375
,而BigDecimal
可以正常工作。 - Sharkgroovy:000> new BigDecimal("1.01")
和那个输出。这将有助于澄清对新手来说实际上什么都不能比字符串(以及更大程度上的 BigDecimal)更准确地保存任何 N-ary 长十进制数 :D - Shark尽管浮点类型只能近似表示十进制数据是正确的,但如果在呈现它们之前将数字舍入到必要的精度,通常可以获得正确的结果。
通常是这样,因为双精度类型的精度小于16个数字。如果您需要更高的精度,则不适用于此类型。此外,近似值可能会累积。
必须说,即使使用定点算术,您仍然必须舍入数字,否则如果得到周期性小数,则BigInteger和BigDecimal会产生错误。因此这里也存在一些近似。
例如,历史上用于财务计算的COBOL具有最大18位数字的精度。因此通常会有一个隐式舍入。
总之,在我看来,双精度主要由于其16位数字的精度不足而不适用,这可能会导致不准确。
考虑下面程序的输出。它显示在精度为16的情况下,经过四舍五入后,双精度与BigDecimal给出相同的结果。
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;
}
}
浮点数的结果不是精确的,这使得它们不适合任何需要精确结果而非近似结果的财务计算。float和double设计用于工程和科学计算,很多情况下也无法产生精确结果。此外,浮点计算的结果可能因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)));
输出:
difference between 2.0 and 1.1 using double is: 0.8999999999999999
difference between 2.0 and 1.1 using BigDecimal is: 0.9
正如之前所说,"将货币表示为double或float可能一开始看起来很好,因为软件会四舍五入处理微小误差,但是随着对不精确数字进行更多的加减乘除运算,您会失去越来越多的精度,因为误差会累积。这使得浮点数和双精度数不适用于处理货币,而在货币交易中需要对10的幂次方的倍数进行完美的精确度。"
最后,Java有了一种标准的处理货币和金钱的方法!
JSR 354:Money and Currency API
JSR 354提供了一种API,用于表示、传输和执行与货币和货币有关的综合计算。您可以从此链接下载:
JSR 354: Money and Currency API Download
规范包括以下内容:
- 用于处理货币金额和货币等的API
- 支持可互换实现的API
- 创建实现类实例的工厂
- 对货币金额进行计算、转换和格式化的功能
- Java API可用于处理货币和货币,计划包括在Java 9中。
- 所有规范类和接口都位于javax.money.*包中。
JSR 354:Money and 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));
使用参考实现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"));
货币单位和货币金额
// 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
当处理货币金额的集合时,可以使用一些方便的实用方法来进行过滤、排序和分组。
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-货币和货币
MonetaryAmount
,值得称赞。 - omerhakanbilici