我总是被告知永远不要用双类型或浮点类型来表示货币,这一次我向你提出一个问题:为什么?
我相信有一个很好的理由,我只是不知道是什么。
我总是被告知永远不要用双类型或浮点类型来表示货币,这一次我向你提出一个问题:为什么?
我相信有一个很好的理由,我只是不知道是什么。
当前回答
如果你的计算涉及到不同的步骤,任意的精度算法都不能100%覆盖你。
使用完美的结果表示(使用自定义Fraction数据类型,将除法操作批处理到最后一步)并且仅在最后一步转换为十进制的唯一可靠方法。
任意精度不会有帮助,因为总有可能有很多小数点后的数字,或者一些结果,如0.6666666……最后一个例子没有任意的表示法。所以每一步都会有小误差。
这些错误会累积起来,最终可能变得不再容易被忽视。这被称为错误传播。
其他回答
Float is binary form of Decimal with different design; they are two different things. There are little errors between two types when converted to each other. Also, float is designed to represent infinite large number of values for scientific. That means it is designed to lost precision to extreme small and extreme large number with that fixed number of bytes. Decimal can't represent infinite number of values, it bounds to just that number of decimal digits. So Float and Decimal are for different purpose.
有一些方法可以管理货币值的错误:
使用长整数,以分计算。 使用双精度,保持你的有效数字为15,这样小数可以精确模拟。在显示值之前舍入;做计算时经常四舍五入。 使用像Java BigDecimal这样的十进制库,这样就不需要使用double来模拟十进制。
附注:有趣的是,大多数品牌的手持科学计算器工作在十进制而不是浮点数。所以没有人抱怨浮点数转换错误。
因为浮点数和双精度数不能准确地表示我们用来表示金钱的以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类型。
如前所述,“把钱表示为双位数或浮点数,一开始可能看起来不错,因为软件会消除微小的错误,但当你对不精确的数字进行更多的加减乘除时,随着错误的增加,你会失去越来越多的精度。”这使得浮点数和双精度数不适用于处理货币,因为货币需要精确计算以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 -货币和货币
虽然浮点类型确实只能表示近似的十进制数据,但如果在表示数字之前将数字舍入到必要的精度,则可以获得正确的结果。通常。
通常是因为双排精度小于16位。如果你要求更高的精度,这不是一个合适的类型。近似也可以累积。
必须指出的是,即使您使用定点算术,您仍然必须对数字进行四舍五入,如果不是因为BigInteger和BigDecimal在获得周期性小数时会给出错误。所以这里也有一个近似。
例如,历史上用于财务计算的COBOL的最大精度为18位数字。所以通常会有一个隐含的舍入。
总之,在我看来,双精度主要不适合它的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;
}
}
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