我一直被告知从不用 double
或 float
类型来表示钱,这次我向您提出问题:为什么?
我确信有一个很好的理由,我根本不知道它是什么。
因为浮点数和双精度数不能准确地表示我们用于货币的以 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 和 1 之间的唯一倍数 0.01(这在交易时很重要)用钱,因为它们是整数美分),可以精确地表示为 IEEE-754 二进制浮点数是 0、0.25、0.5、0.75 和 1。所有其他的都是少量的。与 0.333333 示例类似,如果您将浮点值设为 0.01,然后将其乘以 10,则不会得到 0.1。相反,你会得到类似 0.099999999786 的东西......
将钱表示为 double
或 float
起初可能看起来不错,因为软件会消除微小的错误,但当您对不精确的数字执行更多的加法、减法、乘法和除法时,错误会复合,你会结束加上明显不准确的值。这使得浮点数和双精度数不足以处理金钱,其中需要以 10 为底的幂的倍数的完美准确性。
几乎适用于任何语言的解决方案是使用整数,并计算美分。例如,1025 将是 10.25 美元。几种语言也有处理金钱的内置类型。其中,Java 有 BigDecimal
类,C# 有 decimal
类型。
来自 Bloch, J.,Effective Java,(第 2 版,第 48 条。第 3 版,第 60 条):
float 和 double 类型特别不适合货币计算,因为不可能将 0.1(或任何其他 10 的负幂)精确地表示为 float 或 double。例如,假设你有 1.03 美元,你花了 42 美分。你还剩多少钱? System.out.println(1.03 - .42);打印出 0.6100000000000001。解决这个问题的正确方法是使用 BigDecimal、int 或 long 进行货币计算。
虽然 BigDecimal
有一些警告(请参阅当前接受的答案)。
long a = 104
并以美分而不是美元计数。
BigDecimal
。
这不是准确度的问题,也不是精确度的问题。这是为了满足使用基数 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 为底的内部计算而不是双精度数,则答案总是完全符合人类的预期,假设您的代码中没有其他错误。
Math.round(0.49999999999999994)
return 1?
我对其中一些回应感到不安。我认为双打和浮点数在财务计算中占有一席之地。当然,在使用整数类或 BigDecimal 类时,在加减非小数货币金额时不会损失精度。但是,在执行更复杂的操作时,无论您如何存储数字,您最终都会得到小数点后几位或多位的结果。问题是你如何呈现结果。
如果您的结果处于向上舍入和向下舍入之间的边界,并且最后一分钱真的很重要,那么您可能应该告诉观众答案几乎在中间 - 通过显示更多小数位。
双精度数的问题,尤其是浮点数的问题在于,当它们用于组合大数和小数时。在java中,
System.out.println(1000000.0f + 1.2f - 1000000.0f);
结果是
1.1875
我会冒被否决的风险,但我认为浮点数不适合货币计算被高估了。只要您确保正确地进行了四舍五入并有足够的有效数字来处理 zneak 解释的二进制十进制表示不匹配,就不会有问题。
在 Excel 中使用货币计算的人一直使用双精度浮点数(Excel 中没有货币类型),我还没有看到有人抱怨舍入错误。
当然,你必须保持理智;例如,一个简单的网上商店可能永远不会遇到双精度浮点数的任何问题,但如果你做会计或其他需要添加大量(无限制)数字的事情,你不会想用 10 英尺触摸浮点数极。
浮点数和双精度数是近似值。如果您创建一个 BigDecimal 并将一个浮点数传递给构造函数,您会看到浮点数实际等于:
groovy:000> new BigDecimal(1.0F)
===> 1
groovy:000> new BigDecimal(1.01F)
===> 1.0099999904632568359375
这可能不是您想要表示 1.01 美元的方式。
问题是 IEEE 规范没有办法准确地表示所有分数,其中一些最终成为重复分数,因此您最终会出现近似错误。由于会计师喜欢事情一分钱一分货,如果客户支付账单并且在处理付款后他们欠 0.01 并且他们被收取费用或无法关闭他们的账户,那么最好使用确切的类型,如十进制(在 C# 中)或 Java 中的 java.math.BigDecimal。
如果您四舍五入:see this article by Peter Lawrey,并不是错误不可控。一开始就不必四舍五入更容易。大多数处理金钱的应用程序不需要大量的数学运算,这些操作包括添加事物或将金额分配到不同的存储桶。引入浮点和舍入只会使事情复杂化。
float
、double
和 BigDecimal
代表 精确 值。代码到对象的转换和其他操作一样不精确。类型本身并不是不准确的。
虽然浮点类型确实只能表示近似十进制数据,但如果在呈现数字之前将数字四舍五入到必要的精度,则可以获得正确的结果。通常。
通常是因为 double 类型的精度小于 16 位。如果您需要更高的精度,则它不是合适的类型。近似值也可以累积。
必须说,即使您使用定点算术,您仍然需要对数字进行四舍五入,如果不是因为 BigInteger 和 BigDecimal 如果您获得周期性十进制数会出错。所以这里也有一个近似值。
例如,历史上用于财务计算的 COBOL 的最大精度为 18 位。所以经常有一个隐式的舍入。
最后,在我看来,双精度主要不适合其 16 位精度,这可能是不够的,而不是因为它是近似的。
考虑后续程序的以下输出。它表明,在四舍五入后,与 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;
}
}
浮点数的结果是不精确的,这使得它们不适合任何需要精确结果而不是近似值的金融计算。 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
如前所述,“将钱表示为双精度或浮点数一开始可能看起来不错,因为软件会消除微小的错误,但随着您对不精确的数字执行更多的加法、减法、乘法和除法,您将失去越来越多的精度随着错误加起来。这使得浮点数和双精度数不足以处理金钱,需要以 10 为底的幂的倍数的完美准确性。
最后,Java 有了一种处理货币和金钱的标准方法!
JSR 354:货币和货币 API
JSR 354 提供了一个 API,用于表示、传输和执行货币和货币的综合计算。您可以从此链接下载它:
JSR 354: Money and Currency API Download
该规范由以下内容组成:
用于处理例如货币金额和货币的 API 支持可互换实现的 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"));
货币单位和货币金额
// 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"));
自定义货币金额操作
// 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
Looking into the Java 9 Money and Currency API (JSR 354)
另请参阅:JSR 354 - Currency and Money
MonetaryAmount
大多数答案都强调了为什么不应该使用双精度来计算货币和货币的原因。我完全同意他们的看法。
这并不意味着双打永远不能用于此目的。
我从事过许多 gc 要求非常低的项目,而拥有 BigDecimal 对象是造成这种开销的重要因素。
正是对双重表示缺乏理解,缺乏处理准确性和精确性的经验,才产生了这个明智的建议。
如果您能够处理项目的精度和准确性要求,则可以使其工作,这必须根据处理的双精度值范围来完成。
你可以参考 guava 的 FuzzyCompare 方法来获得更多的想法。参数容差是关键。我们在证券交易应用程序中处理了这个问题,并对不同范围内的不同数值使用哪些容差进行了详尽的研究。
此外,在某些情况下,您可能很想将 Double 包装器用作映射键,而哈希映射是实现。这是非常危险的,因为 Double.equals 和哈希码(例如值“0.5”和“0.6 - 0.1”)会造成很大的混乱。
如果您的计算涉及多个步骤,那么任意精度算术将无法 100% 覆盖您。
使用结果的完美表示的唯一可靠方法(使用自定义分数数据类型,将批量除法运算到最后一步)并且仅在最后一步转换为十进制表示法。
任意精度无济于事,因为总是有很多小数位的数字,或者诸如 0.6666666
之类的某些结果......没有任意表示将涵盖最后一个示例。所以你在每一步都会有小错误。
这些错误会累积起来,最终可能变得不再容易被忽略。这称为 Error Propagation。
对此问题发布的许多答案都讨论了 IEEE 和围绕浮点运算的标准。
我来自非计算机科学背景(物理和工程),我倾向于从不同的角度看待问题。对我来说,我不会在数学计算中使用双精度或浮点数的原因是我会丢失太多信息。
有哪些替代方案?有很多(还有更多我不知道的!)。
Java 中的 BigDecimal 是 Java 语言的本机。 Apfloat 是另一个用于 Java 的任意精度库。
C# 中的十进制数据类型是 Microsoft 的 .NET 替代 28 位有效数字。
SciPy(Scientific Python)可能也可以处理财务计算(我没有尝试过,但我怀疑是这样)。
GNU 多精度库 (GMP) 和 GNU MFPR 库是 C 和 C++ 的两个免费和开源资源。
还有用于 JavaScript(!)的数值精度库,我认为 PHP 可以处理财务计算。
还有许多计算机语言的专有(我认为尤其是 Fortran)和开源解决方案。
我不是受过训练的计算机科学家。但是,我倾向于使用 Java 中的 BigDecimal 或 C# 中的小数。我还没有尝试过我列出的其他解决方案,但它们可能也非常好。
对我来说,我喜欢 BigDecimal 因为它支持的方法。 C# 的小数非常好,但我没有机会尽可能多地使用它。我在业余时间做我感兴趣的科学计算,BigDecimal 似乎工作得很好,因为我可以设置浮点数的精度。 BigDecimal 的缺点?有时它可能会很慢,尤其是在您使用除法时。
为了速度,您可以查看 C、C++ 和 Fortran 中的免费和专有库。
为了补充以前的答案,在处理问题中解决的问题时,除了 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 。钱钱
看看这个简单的例子:它看起来在逻辑上是正确的,但在现实世界中,如果没有正确地受到威胁,这可能会返回意想不到的结果:
0.1 x 10 = 1 👍 ,所以:
double total = 0.0;
// adds 10 cents, 10 times
for (int i = 0; i < 10; i++) {
total += 0.1; // adds 10 cents
}
Log.d("result: ", "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
}
// total should be equals to 0.0, right?
Log.d("result: ", "current total: " + total);
if (total == 0.0) {
Log.d("result: ", "is total equal to ZERO? YES, of course!!");
} else {
Log.d("result: ", "is total equal to ZERO? No...");
// so be careful comparing equality in this cases!!!
}
输出:
result: current total: 0.9999999999999999
result: current total: 2.7755575615628914E-17 🤔
result: is total equal to ZERO? No... 😌
Float 是 Decimal 的二进制形式,具有不同的设计;他们是两个不同的东西。两种类型相互转换时几乎没有错误。此外,float 旨在表示无限大量的科学值。这意味着它的设计目的是在固定字节数的情况下将精度降低到极小和极大的数字。十进制不能表示无限数量的值,它仅限于该数量的十进制数字。所以 Float 和 Decimal 有不同的目的。
有一些方法可以管理货币价值的错误:
改用长整数并以美分计数。使用双精度,仅将有效数字保持为 15,以便可以精确模拟小数。在呈现值之前进行四舍五入;计算时经常四舍五入。使用像 Java BigDecimal 这样的十进制库,因此您不需要使用 double 来模拟十进制。
ps 有趣的是,大多数品牌的手持科学计算器都使用十进制而不是浮点数。所以没有人抱怨浮动转换错误。
美国货币可以很容易地用美元和美分来表示。整数是 100% 精确的,而浮点二进制数与浮点小数不完全匹配。
1.0 / 10 * 10
可能与 1.0 不同。0b0.00011001100110011001100110011001100110011001100110011010
,10 表示为0b1010
。如果将这两个二进制数相乘,得到1.0000000000000000000000000000000000000000000000000000010
,然后四舍五入到可用的 53 个二进制数字,则正好是 1。浮点数的问题不在于它们总是出错, 但他们有时会这样做 - 就像 0.1 + 0.2 ≠ 0.3 的例子一样。