<audio id="audio" title="09 | 数值计算：注意精度、舍入和溢出问题" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/48/33/48b41f8dda454ee64d25947bebef7233.mp3"></audio>

你好，我是朱晔。今天，我要和你说说数值计算的精度、舍入和溢出问题。

之所以要单独分享数值计算，是因为很多时候我们习惯的或者说认为理所当然的计算，在计算器或计算机看来并不是那么回事儿。就比如前段时间爆出的一条新闻，说是手机计算器把10%+10%算成了0.11而不是0.2。

出现这种问题的原因在于，国外的计算程序使用的是单步计算法。在单步计算法中，a+b%代表的是a*(1+b%)。所以，手机计算器计算10%+10%时，其实计算的是10%*（1+10%），所以得到的是0.11而不是0.2。

在我看来，计算器或计算机会得到反直觉的计算结果的原因，可以归结为：

- 在人看来，浮点数只是具有小数点的数字，0.1和1都是一样精确的数字。但，计算机其实无法精确保存浮点数，因此浮点数的计算结果也不可能精确。
- 在人看来，一个超大的数字只是位数多一点而已，多写几个1并不会让大脑死机。但，计算机是把数值保存在了变量中，不同类型的数值变量能保存的数值范围不同，当数值超过类型能表达的数值上限则会发生溢出问题。

接下来，我们就具体看看这些问题吧。

## “危险”的Double

我们先从简单的反直觉的四则运算看起。对几个简单的浮点数进行加减乘除运算：

```
System.out.println(0.1+0.2);
System.out.println(1.0-0.8);
System.out.println(4.015*100);
System.out.println(123.3/100);

double amount1 = 2.15;
double amount2 = 1.10;
if (amount1 - amount2 == 1.05)
    System.out.println(&quot;OK&quot;);

```

输出结果如下：

```
0.30000000000000004
0.19999999999999996
401.49999999999994
1.2329999999999999

```

可以看到，输出结果和我们预期的很不一样。比如，0.1+0.2输出的不是0.3而是0.30000000000000004；再比如，对2.15-1.10和1.05判等，结果判等不成立。

出现这种问题的主要原因是，计算机是以二进制存储数值的，浮点数也不例外。Java采用了[IEEE 754标准](https://en.wikipedia.org/wiki/IEEE_754)实现浮点数的表达和运算，你可以通过[这里](http://www.binaryconvert.com/)查看数值转化为二进制的结果。

比如，0.1的二进制表示为0.0 0011 0011 0011… （0011 无限循环)，再转换为十进制就是0.1000000000000000055511151231257827021181583404541015625。**对于计算机而言，0.1无法精确表达，这是浮点数计算造成精度损失的根源。**

你可能会说，以0.1为例，其十进制和二进制间转换后相差非常小，不会对计算产生什么影响。但，所谓积土成山，如果大量使用double来作大量的金钱计算，最终损失的精度就是大量的资金出入。比如，每天有一百万次交易，每次交易都差一分钱，一个月下来就差30万。这就不是小事儿了。那，如何解决这个问题呢？

我们大都听说过BigDecimal类型，浮点数精确表达和运算的场景，一定要使用这个类型。不过，在使用BigDecimal时有几个坑需要避开。我们用BigDecimal把之前的四则运算改一下：

```
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));
System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));
System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));

```

输出如下：

```
0.3000000000000000166533453693773481063544750213623046875
0.1999999999999999555910790149937383830547332763671875
401.49999999999996802557689079549163579940795898437500
1.232999999999999971578290569595992565155029296875

```

可以看到，运算结果还是不精确，只不过是精度高了而已。这里给出浮点数运算避坑第一原则：**使用BigDecimal表示和计算浮点数，且务必使用字符串的构造方法来初始化BigDecimal**：

```
System.out.println(new BigDecimal(&quot;0.1&quot;).add(new BigDecimal(&quot;0.2&quot;)));
System.out.println(new BigDecimal(&quot;1.0&quot;).subtract(new BigDecimal(&quot;0.8&quot;)));
System.out.println(new BigDecimal(&quot;4.015&quot;).multiply(new BigDecimal(&quot;100&quot;)));
System.out.println(new BigDecimal(&quot;123.3&quot;).divide(new BigDecimal(&quot;100&quot;)));

```

改进后，就能得到我们想要的输出了：

```
0.3
0.2
401.500
1.233

```

到这里，你可能会继续问，不能调用BigDecimal传入Double的构造方法，但手头只有一个Double，如何转换为精确表达的BigDecimal呢？

我们试试用Double.toString把double转换为字符串，看看行不行？

```
System.out.println(new BigDecimal(&quot;4.015&quot;).multiply(new BigDecimal(Double.toString(100))));

```

输出为401.5000。与上面字符串初始化100和4.015相乘得到的结果401.500相比，这里为什么多了1个0呢？原因就是，BigDecimal有scale和precision的概念，scale表示小数点右边的位数，而precision表示精度，也就是有效数字的长度。

调试一下可以发现，new BigDecimal(Double.toString(100))得到的BigDecimal的scale=1、precision=4；而new BigDecimal(“100”)得到的BigDecimal的scale=0、precision=3。对于BigDecimal乘法操作，返回值的scale是两个数的scale相加。所以，初始化100的两种不同方式，导致最后结果的scale分别是4和3：

```
private static void testScale() {
    BigDecimal bigDecimal1 = new BigDecimal(&quot;100&quot;);
    BigDecimal bigDecimal2 = new BigDecimal(String.valueOf(100d));
    BigDecimal bigDecimal3 = new BigDecimal(String.valueOf(100));
    BigDecimal bigDecimal4 = BigDecimal.valueOf(100d);
    BigDecimal bigDecimal5 = new BigDecimal(Double.toString(100));

    print(bigDecimal1); //scale 0 precision 3 result 401.500
    print(bigDecimal2); //scale 1 precision 4 result 401.5000
    print(bigDecimal3); //scale 0 precision 3 result 401.500
    print(bigDecimal4); //scale 1 precision 4 result 401.5000
    print(bigDecimal5); //scale 1 precision 4 result 401.5000
}

private static void print(BigDecimal bigDecimal) {
    log.info(&quot;scale {} precision {} result {}&quot;, bigDecimal.scale(), bigDecimal.precision(), bigDecimal.multiply(new BigDecimal(&quot;4.015&quot;)));
}

```

BigDecimal的toString方法得到的字符串和scale相关，又会引出了另一个问题：对于浮点数的字符串形式输出和格式化，我们应该考虑显式进行，通过格式化表达式或格式化工具来明确小数位数和舍入方式。接下来，我们就聊聊浮点数舍入和格式化。

## 考虑浮点数舍入和格式化的方式

除了使用Double保存浮点数可能带来精度问题外，更匪夷所思的是这种精度问题，加上String.format的格式化舍入方式，可能得到让人摸不着头脑的结果。

我们看一个例子吧。首先用double和float初始化两个3.35的浮点数，然后通过String.format使用%.1f来格式化这2个数字：

```
double num1 = 3.35;
float num2 = 3.35f;
System.out.println(String.format(&quot;%.1f&quot;, num1));//四舍五入
System.out.println(String.format(&quot;%.1f&quot;, num2));

```

得到的结果居然是3.4和3.3。

这就是由精度问题和舍入方式共同导致的，double和float的3.35其实相当于3.350xxx和3.349xxx：

```
3.350000000000000088817841970012523233890533447265625
3.349999904632568359375

```

String.format采用四舍五入的方式进行舍入，取1位小数，double的3.350四舍五入为3.4，而float的3.349四舍五入为3.3。

**我们看一下Formatter类的相关源码，可以发现使用的舍入模式是HALF_UP**（代码第11行）：

```
else if (c == Conversion.DECIMAL_FLOAT) {
    // Create a new BigDecimal with the desired precision.
    int prec = (precision == -1 ? 6 : precision);
    int scale = value.scale();

    if (scale &gt; prec) {
        // more &quot;scale&quot; digits than the requested &quot;precision&quot;
        int compPrec = value.precision();
        if (compPrec &lt;= scale) {
            // case of 0.xxxxxx
            value = value.setScale(prec, RoundingMode.HALF_UP);
        } else {
            compPrec -= (scale - prec);
            value = new BigDecimal(value.unscaledValue(),
                                   scale,
                                   new MathContext(compPrec));
        }
    }

```

如果我们希望使用其他舍入方式来格式化字符串的话，可以设置DecimalFormat，如下代码所示：

```
double num1 = 3.35;
float num2 = 3.35f;
DecimalFormat format = new DecimalFormat(&quot;#.##&quot;);
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num1));
format.setRoundingMode(RoundingMode.DOWN);
System.out.println(format.format(num2));

```

当我们把这2个浮点数向下舍入取2位小数时，输出分别是3.35和3.34，还是我们之前说的浮点数无法精确存储的问题。

因此，即使通过DecimalFormat来精确控制舍入方式，double和float的问题也可能产生意想不到的结果，所以浮点数避坑第二原则：**浮点数的字符串格式化也要通过BigDecimal进行。**

比如下面这段代码，使用BigDecimal来格式化数字3.35，分别使用向下舍入和四舍五入方式取1位小数进行格式化：

```
BigDecimal num1 = new BigDecimal(&quot;3.35&quot;);
BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN);
System.out.println(num2);
BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP);
System.out.println(num3);

```

这次得到的结果是3.3和3.4，符合预期。

## 用equals做判等，就一定是对的吗？

现在我们知道了，应该使用BigDecimal来进行浮点数的表示、计算、格式化。在上一讲介绍[判等问题](https://time.geekbang.org/column/article/213604)时，我提到一个原则：包装类的比较要通过equals进行，而不能使用==。那么，使用equals方法对两个BigDecimal判等，一定能得到我们想要的结果吗？

我们来看下面的例子。使用equals方法比较1.0和1这两个BigDecimal：

```
System.out.println(new BigDecimal(&quot;1.0&quot;).equals(new BigDecimal(&quot;1&quot;)))

```

你可能已经猜到我要说什么了，结果当然是false。BigDecimal的equals方法的注释中说明了原因，equals比较的是BigDecimal的value和scale，1.0的scale是1，1的scale是0，所以结果一定是false：

```
/**
 * Compares this {@code BigDecimal} with the specified
 * {@code Object} for equality.  Unlike {@link
 * #compareTo(BigDecimal) compareTo}, this method considers two
 * {@code BigDecimal} objects equal only if they are equal in
 * value and scale (thus 2.0 is not equal to 2.00 when compared by
 * this method).
 *
 * @param  x {@code Object} to which this {@code BigDecimal} is
 *         to be compared.
 * @return {@code true} if and only if the specified {@code Object} is a
 *         {@code BigDecimal} whose value and scale are equal to this
 *         {@code BigDecimal}'s.
 * @see    #compareTo(java.math.BigDecimal)
 * @see    #hashCode
 */
@Override
public boolean equals(Object x)

```

**如果我们希望只比较BigDecimal的value，可以使用compareTo方法**，修改后代码如下：

```
System.out.println(new BigDecimal(&quot;1.0&quot;).compareTo(new BigDecimal(&quot;1&quot;))==0);

```

学过上一讲，你可能会意识到BigDecimal的equals和hashCode方法会同时考虑value和scale，如果结合HashSet或HashMap使用的话就可能会出现麻烦。比如，我们把值为1.0的BigDecimal加入HashSet，然后判断其是否存在值为1的BigDecimal，得到的结果是false：

```
Set&lt;BigDecimal&gt; hashSet1 = new HashSet&lt;&gt;();
hashSet1.add(new BigDecimal(&quot;1.0&quot;));
System.out.println(hashSet1.contains(new BigDecimal(&quot;1&quot;)));//返回false

```

解决这个问题的办法有两个：

- 第一个方法是，使用TreeSet替换HashSet。TreeSet不使用hashCode方法，也不使用equals比较元素，而是使用compareTo方法，所以不会有问题。

```
Set&lt;BigDecimal&gt; treeSet = new TreeSet&lt;&gt;();
treeSet.add(new BigDecimal(&quot;1.0&quot;));
System.out.println(treeSet.contains(new BigDecimal(&quot;1&quot;)));//返回true

```

- 第二个方法是，把BigDecimal存入HashSet或HashMap前，先使用stripTrailingZeros方法去掉尾部的零，比较的时候也去掉尾部的0，确保value相同的BigDecimal，scale也是一致的：

```
Set&lt;BigDecimal&gt; hashSet2 = new HashSet&lt;&gt;();
hashSet2.add(new BigDecimal(&quot;1.0&quot;).stripTrailingZeros());
System.out.println(hashSet2.contains(new BigDecimal(&quot;1.000&quot;).stripTrailingZeros()));//返回true

```

## 小心数值溢出问题

数值计算还有一个要小心的点是溢出，不管是int还是long，所有的基本数值类型都有超出表达范围的可能性。

比如，对Long的最大值进行+1操作：

```
long l = Long.MAX_VALUE;
System.out.println(l + 1);
System.out.println(l + 1 == Long.MIN_VALUE);

```

输出结果是一个负数，因为Long的最大值+1变为了Long的最小值：

```
-9223372036854775808
true

```

**显然这是发生了溢出，而且是默默地溢出，并没有任何异常**。这类问题非常容易被忽略，改进方式有下面2种。

方法一是，考虑使用Math类的addExact、subtractExact等xxExact方法进行数值运算，这些方法可以在数值溢出时主动抛出异常。我们来测试一下，使用Math.addExact对Long最大值做+1操作：

```
try {
    long l = Long.MAX_VALUE;
    System.out.println(Math.addExact(l, 1));
} catch (Exception ex) {
    ex.printStackTrace();
}

```

执行后，可以得到ArithmeticException，这是一个RuntimeException：

```
java.lang.ArithmeticException: long overflow
	at java.lang.Math.addExact(Math.java:809)
	at org.geekbang.time.commonmistakes.numeralcalculations.demo3.CommonMistakesApplication.right2(CommonMistakesApplication.java:25)
	at org.geekbang.time.commonmistakes.numeralcalculations.demo3.CommonMistakesApplication.main(CommonMistakesApplication.java:13)

```

方法二是，使用大数类BigInteger。BigDecimal是处理浮点数的专家，而BigInteger则是对大数进行科学计算的专家。

如下代码，使用BigInteger对Long最大值进行+1操作；如果希望把计算结果转换一个Long变量的话，可以使用BigInteger的longValueExact方法，在转换出现溢出时，同样会抛出ArithmeticException：

```
BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE));
System.out.println(i.add(BigInteger.ONE).toString());

try {
    long l = i.add(BigInteger.ONE).longValueExact();
} catch (Exception ex) {
    ex.printStackTrace();
}

```

输出结果如下：

```
9223372036854775808
java.lang.ArithmeticException: BigInteger out of long range
	at java.math.BigInteger.longValueExact(BigInteger.java:4632)
	at org.geekbang.time.commonmistakes.numeralcalculations.demo3.CommonMistakesApplication.right1(CommonMistakesApplication.java:37)
	at org.geekbang.time.commonmistakes.numeralcalculations.demo3.CommonMistakesApplication.main(CommonMistakesApplication.java:11)

```

可以看到，通过BigInteger对Long的最大值加1一点问题都没有，当尝试把结果转换为Long类型时，则会提示BigInteger out of long range。

## 重点回顾

今天，我与你分享了浮点数的表示、计算、舍入和格式化、溢出等涉及的一些坑。

第一，切记，要精确表示浮点数应该使用BigDecimal。并且，使用BigDecimal的Double入参的构造方法同样存在精度丢失问题，应该使用String入参的构造方法或者BigDecimal.valueOf方法来初始化。

第二，对浮点数做精确计算，参与计算的各种数值应该始终使用BigDecimal，所有的计算都要通过BigDecimal的方法进行，切勿只是让BigDecimal来走过场。任何一个环节出现精度损失，最后的计算结果可能都会出现误差。

第三，对于浮点数的格式化，如果使用String.format的话，需要认识到它使用的是四舍五入，可以考虑使用DecimalFormat来明确指定舍入方式。但考虑到精度问题，我更建议使用BigDecimal来表示浮点数，并使用其setScale方法指定舍入的位数和方式。

第四，进行数值运算时要小心溢出问题，虽然溢出后不会出现异常，但得到的计算结果是完全错误的。我们考虑使用Math.xxxExact方法来进行运算，在溢出时能抛出异常，更建议对于可能会出现溢出的大数运算使用BigInteger类。

总之，对于金融、科学计算等场景，请尽可能使用BigDecimal和BigInteger，避免由精度和溢出问题引发难以发现，但影响重大的Bug。

今天用到的代码，我都放在了GitHub上，你可以点击[这个链接](https://github.com/JosephZhu1983/java-common-mistakes)查看。

## 思考与讨论

1. [BigDecimal](https://docs.oracle.com/javase/8/docs/api/java/math/BigDecimal.html)提供了8种舍入模式，你能通过一些例子说说它们的区别吗？
1. 数据库（比如MySQL）中的浮点数和整型数字，你知道应该怎样定义吗？又如何实现浮点数的准确计算呢？

针对数值运算，你还遇到过什么坑吗？我是朱晔，欢迎在评论区与我留言分享你的想法，也欢迎你把这篇文章分享给你的朋友或同事，一起交流。
