日常开发踩坑:你的数值计算真的如你所愿吗?

读者在日常业务开发中或多或少会涉及到一些数值计算逻辑,尤其是金融行业需要特别严谨,通常由数值引起的问题都是潜移默化的且难以发现,拿“数值偏差”来说,初期的微小偏差是很难察觉的,当这种偏差累积成量级的数值错误而引起生产事故则为时已晚。

本篇就针对这一部分踩过的坑为读者做科普和预警。

1

浮点数的精度偏差:1减0.8真的等于0.2吗?

测试一下:

代码语言:javascript
复制
System.out.println(1 - 0.8);

控制台输出:

代码语言:javascript
复制
0.19999999999999996

显然结果并非我们所想,原因是十进制数在计算机底层是以二进制数保存的(0、1),浮点数的精度偏差就产生于这个进制转换过程

既然浮点数会产生精度偏差,那么使用Java提供的精度运算类BigDecimal就能避免偏差吗?

尝试修改代码:

代码语言:javascript
复制
System.out.println(new BigDecimal(1).subtract(new BigDecimal(0.8)));

控制台输出:

代码语言:javascript
复制
0.1999999999999999555910790149937383830547332763671875

是否又出你所料,BigDecimal确实在精度上有所提升,不过并不是想要的正确结果,所以再次修改代码:

代码语言:javascript
复制
System.out.println(new BigDecimal("1").subtract(new BigDecimal("0.8")))

控制台输出:

代码语言:javascript
复制
0.2

脱坑指南一

想要避免浮点数运算的精度偏差,需使用java.math包下提供的精度运算类BigDecimal,且必须使用字符串类型参数初始化BigDecimal。

2

浮点数的判断:用equals判断真的奏效了吗?

对于浮点数的判断通常有两种情况:1、值相等,2、值相等且精度相等。下面用equals分别测试一下:

代码语言:javascript
复制
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1")))
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1.0")))

控制台输出:

代码语言:javascript
复制
false
true

结果很直观,对于浮点数来说,equals的判定条件是值相等且精度也要相等,否则无法奏效。

那么如果只想判断值是否相等应该怎么做呢?使用compareTo方法:

代码语言:javascript
复制
System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1"))==0);

控制台输出:

代码语言:javascript
复制
true

脱坑指南二

想要仅对浮点数的值判等用compareTo方法,值+精度判等用equals方法。

3

数值溢出:运算时数值超过类型最大值不会报错吗?

通过代码测试一下,第二行数值溢出了,首先没有编译错误:

代码语言:javascript
复制
Integer integer = Integer.MAX_VALUE;
System.out.println(integer + 1);
System.out.println(integer + 1 == Integer.MIN_VALUE);

控制台输出,也没有任何异常:

代码语言:javascript
复制
-2147483648
true

所以内存溢出不会报错,数值将回到对应数据类型的最小值重新轮回。既然问题已经产生,应该如何解决呢?

脱坑指南三

Plan A:使用Math类提供的带有溢出异常捕获的运算方法xxExact

方法源码,对溢出主动捕获异常

Plan B:使用范围更大的数据类型BigInteger,可能有读者会问“如果这个类型也不够用溢出了呢”,只能说这样问下去的话问题永远也没有答案了。

4

总结:三个坑点

1、浮点数的精度偏差

方案: 使用BigDecimal做浮点数运算,且构造入参要使用字符串类型

2、浮点数的判等问题

方案:仅对浮点数的值判等用compareTo方法,值+精度判等用equals方法

3、隐性的数值溢出问题

方案1:使用Math类提供的带有溢出异常捕获的运算方法xxExact做数值运算

方案2:使用范围更大的数据类型BigInteger