我总是被告知从不代表double
或float
类型的钱,而这次我向您提出问题:为什么?
我确信有一个很好的理由,我根本就不知道它是什么。
答案 0 :(得分:882)
因为浮点数和双打数不能准确地代表我们用于赚钱的基数10倍数。这个问题不仅适用于Java,也适用于任何使用base 2浮点类型的编程语言。
在基数10中,您可以将10.25写为1025 * 10 -2 (10的幂的整数倍)。 IEEE-754 floating-point numbers是不同的,但考虑它们的一种非常简单的方法是乘以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.01的倍数介于0和1之间(这在处理货币时非常重要,因为它们是整数分钱),可以完全表示为IEEE-754二进制浮点数,分别为0,0.25,0.5,0.75和1.所有其他的都关闭少量的。与0.333333示例类比,如果将浮点值设为0.1并将其乘以10,则不会得到1.
代表资金double
或float
一开始看起来可能会很好,因为软件可以完成微小的错误,但是当您对不精确的数字执行更多的加法,减法,乘法和除法时,错误会复合,你最终会得到明显不准确的价值观。这使浮动和双打不足以处理金钱,其中需要基本10倍数倍的完美准确度。
几乎适用于任何语言的解决方案是使用整数,并计算美分。例如,1025将是10.25美元。几种语言也有内置类型来处理钱。其中,Java具有BigDecimal
类,C#具有decimal
类型。
答案 1 :(得分:287)
来自Bloch,J.,Effective Java,2nd ed,Item 48:
float
和double
类型 特别不适合货币 计算,因为这是不可能的 表示0.1(或任何其他 负的权力为10)作为float
或 完全double
。例如,假设您有1.03美元 你花了42c。多少钱呢 你离开了吗?
System.out.println(1.03 - .42);
打印出
0.6100000000000001
。解决这个问题的正确方法是 使用
BigDecimal
,int
或long
用于货币计算。
虽然BigDecimal
有一些警告(请参阅当前接受的答案)。
答案 2 :(得分:68)
这不是准确性问题,也不是精确问题。这是一个满足使用基数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进行内部计算而不是双精度数,那么答案总是完全符合人类的预期,假设您的代码中没有其他错误。
答案 3 :(得分:46)
我对其中的一些反应感到不安。我认为双打和花车在财务计算中占有一席之地。当然,在添加和减去非小数货币金额时,使用整数类或BigDecimal类时不会有精度损失。但是,当执行更复杂的操作时,无论您如何存储数字,通常都会得到几个或多个小数位的结果。问题是你如何呈现结果。
如果你的结果是在向上舍入和向下舍入之间的边界,并且最后一分钱真正重要,你可能应该告诉观众答案几乎在中间 - 通过显示更多小数位。
双打的问题,以及浮点数更多的问题是,当它们用于组合大数和小数时。在java中,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
结果
1.1875
答案 4 :(得分:36)
浮动和双打是近似值。如果你创建一个BigDecimal并将一个浮点数传递给构造函数,你会看到浮点数实际上等于:
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
这可能不是你想要代表$ 1.01的方式。
问题在于,IEEE规范无法准确表示所有分数,其中一些最终会成为重复分数,因此最终会出现近似误差。会计师喜欢把事情准确地说出来一分钱,如果他们付账单就会对客户感到烦恼,而且付款处理完毕后他们欠他们.01他们会收取一定的费用或无法关闭账户,最好使用精确类型,如十进制(在C#中)或Java中的java.math.BigDecimal。
如果您舍入:see this article by Peter Lawrey,则错误不是可控制的。一开始就不容易绕圈。大多数处理资金的应用程序都不需要大量数学,操作包括添加内容或将数量分配给不同的存储桶。引入浮点和舍入只会使事情复杂化。
答案 5 :(得分:18)
我冒着被投票的风险,但我认为浮动点数不适合用于货币计算被高估了。只要你确保正确地进行分数舍入并且有足够的有效位数来处理由zneak解释的二进制十进制表示不匹配,就没有问题。
在Excel中使用货币计算的人一直使用双精度浮点数(Excel中没有货币类型),我还没有看到有人抱怨舍入错误。
当然,你必须保持理智;例如一个简单的网上商店可能永远不会遇到任何双精度浮动的问题,但如果你这样做,例如会计或任何其他需要添加大量(不受限制的)数字的东西,你不会想要触及10英尺极点的浮点数。
答案 6 :(得分:17)
虽然浮点类型只能表示近似的十进制数据,但如果在呈现数字之前将数字舍入到必要的精度,则可以获得正确的结果。一般
通常因为double类型的精度小于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;
}
}
答案 7 :(得分:16)
浮点数的结果并不准确,这使得它们不适合任何需要精确结果而非近似的财务计算。 float和double是为工程和科学计算而设计的,并且很多次都没有产生精确的结果,浮点计算的结果也可能因JVM到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
答案 8 :(得分:7)
正如之前所说的那样“首先将货币表示为双倍或浮动可能看起来很好,因为软件可以完成微小的错误,但是当你对不精确的数字进行更多的加法,减法,乘法和除法时,你会失去更多错误加起来更加精确。这使得浮动和双打不足以处理金钱,其中需要基本10倍数倍的完美准确度。“
最后,Java有一种标准的方式来处理货币和货币!
JSR 354:货币和货币API
JSR 354提供了一个API,用于使用货币和货币表示,传输和执行综合计算。您可以从以下链接下载:
JSR 354: Money and Currency API Download
该规范包含以下内容:
- 用于处理e的API。 G。货币金额和货币
- 支持可互换实施的API
- 用于创建实施类实例的工厂
- 货币金额的计算,转换和格式化功能
- 用于处理货币和货币的Java API,计划包含在Java 9中。
- 所有规范类和接口都位于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"));
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;
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 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
资源:
Handling money and currencies in Java with JSR 354
答案 9 :(得分:4)
如果你的计算涉及不同的步骤,任意精度算术都不会覆盖你100%。
使用完美结果表示的唯一可靠方法(使用自动分数数据类型将批次除法运算到最后一步)并且仅在最后一步中转换为十进制表示法。
任意精度都无济于事,因为总会有数字有这么多小数位,或者某些结果如0.6666666 ......任意表示都不会涵盖最后一个例子。因此,每一步都会出现小错误。
这个错误会加起来,最终可能不容易被忽视。这称为Error Propagation。
答案 10 :(得分:2)
我更喜欢使用Integer或Long来代表货币。 BigDecimal太多了源代码。
你只需要知道你所有的价值都是美分。或者您正在使用的任何货币的最低值。
答案 11 :(得分:2)
大多数答案都强调了为什么不应该使用双打进行货币和货币计算的原因。我完全赞同他们。
这并不意味着双打永远不会用于此目的。
我曾参与过许多gc要求非常低的项目,并且拥有BigDecimal对象是造成这种开销的重要因素。
缺乏对双重表征的理解,缺乏处理准确性和精确度的经验,这带来了明智的建议。
如果您能够处理项目的精度和准确度要求,则可以使其工作,这需要根据处理的双值范围来完成。
您可以参考番石榴的FuzzyCompare方法来获得更多想法。参数容差是关键。 我们为证券交易应用程序处理了这个问题,我们对不同范围内不同数值的公差进行了详尽的研究。
此外,在某些情况下,您可能会想要使用Double包装器作为地图键,并将哈希映射作为实现。这是非常危险的,因为Double.equals和哈希码例如值“0.5”&amp; “0.6 - 0.1”会造成很大的混乱。
答案 12 :(得分:1)
一些例子......这对几乎所有的编程语言都有效(实际上并没有按预期工作)......我已经尝试过使用Delphi,VBScript,Visual Basic,JavaScript和Java /机器人:
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!!!");
}
输出:
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!!!
答案 13 :(得分:1)
此问题的许多答案都讨论了IEEE和浮点运算的标准。
来自非计算机科学背景(物理和工程),我倾向于从不同的角度看待问题。对我来说,我不会在数学计算中使用double或float的原因是我会丢失太多信息。
有哪些替代方案?有很多(其中许多我不知道!)。
Java中的BigDecimal是Java语言的原生。 Apfloat是Java的另一个任意精度库。
C#中的十进制数据类型是Microsoft的替代28位有效数字。
SciPy(科学Python)可能也可以处理财务计算(我没有尝试过,但我怀疑是这样)。GNU多精度库(GMP)和GNU MFPR库是C和C ++的两个免费开源资源。
还有JavaScript(!)的数值精度库,我认为PHP可以处理财务计算。
对于许多计算机语言,还有专有(特别是,我认为,对于Fortran)和开源解决方案。
我不是通过培训的计算机科学家。但是,我倾向于倾向于Java中的BigDecimal或C#中的decimal。我还没有尝试过我列出的其他解决方案,但它们也可能非常好。
对我来说,我喜欢BigDecimal,因为它支持的方法。 C#的小数非常好,但我还没有机会像我一样喜欢它。我在业余时间对我感兴趣的科学计算,BigDecimal似乎工作得很好,因为我可以设置我的浮点数的精度。 BigDecimal的缺点是什么?它有时会很慢,特别是如果您使用除法方法。
为了提高速度,您可以查看C,C ++和Fortran中的免费和专有库。
答案 14 :(得分:1)
要补充先前的答案,除了处理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
答案 15 :(得分:0)
浮点数是十进制的二进制形式,具有不同的设计;他们是两个不同的东西。相互转换时,两种类型之间几乎没有错误。另外,float旨在代表科学的无限多个值。这意味着它被设计为在具有固定字节数的情况下将精度损失到极小和极大的数量。十进制不能代表无限数量的值,它只能与十进制数字的数量绑定。所以Float和Decimal是出于不同的目的。
有一些方法可以管理货币值错误:
使用长整数并以美分为单位。
使用双精度,仅将有效数字保持为15,以便可以精确模拟十进制。在展示价值之前四舍五入;计算时经常四舍五入。
使用Java BigDecimal之类的十进制库,因此您无需使用double来模拟十进制。
p.s。有趣的是,大多数品牌的手持式科学计算器都使用小数而不是浮点数。因此,没有人抱怨浮动转换错误。
答案 16 :(得分:-1)
美国货币可以轻松地用美元和美分金额表示。整数的精度为100%,而浮点二进制数与浮点小数不完全匹配。