《开发实战》09 | 数值计算:注意精度、舍入和溢出问题

国外的计算程序使用的是单步计算法。在单步计算法中,a+b%代表的是 a*(1+b%)。所以,手机计算器计算 10%+10% 时,其实计算的是10%*(1+10%),所以得到的是 0.11 而不是 0.2。

Double
System.out.println(0.1+0.2);  0.30000000000000004
System.out.println(1.0-0.8);  0.19999999999999996
System.out.println(4.015*100);  401.49999999999994
System.out.println(123.3/100);  1.2329999999999999
double amount1 = 2.15;
double amount2 = 1.10;
if (amount1 - amount2 == 1.05) // 不相等
  System.out.println("OK");

出现上面的原因是因为计算机是以二进制存储数值的,浮点数也不例外
BigDecimal 类型,浮点数精确表达和运算的场景

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

可以看到,运算结果还是不精确,只不过是精度高了而已.
使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化BigDecimal

System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2"))); // 0.3
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8"))); // 0.2 
System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100"))); // 401.500
System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100"))); // 1.233

如果一定要用 Double 来初始化 BigDecimal 的话,可以使用 BigDecimal.valueOf 方法,以确保其表现和字符串形式的构造方法一致

System.out.println(new BigDecimal("4.015").multiply(BigDecimal.valueOf(100)));

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

首先用 double 和 float 初始化两个 3.35 的浮点数,然后通过String.format 使用 %.1f 来格式化这 2 个数字:

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

Formatter 类的相关源码,可以发现使用的舍入模式是 HALF_UP.
浮点数的字符串格式化也要通过BigDecimal 进行

用 equals 做判等,就一定是对的吗?
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1"))) // false

BigDecimal 的 equals 方法的注释中说明了原因,比较的是 BigDecimal 的 value 和 scale,1.0 的 scale 是 1,1 的scale 是 0,所以结果一定是 false.
如果我们希望只比较 BigDecimal 的 value,可以使用 compareTo 方法:
System.out.println(new BigDecimal(“1.0”).compareTo(new BigDecimal(“1”))==0);

你可能会意识到 BigDecimal 的 equals 和 hashCode 方法会同时考虑 value和 scale.
所以使用HashSet 或 HashMap就可能会有麻烦

Set<BigDecimal> hashSet1 = new HashSet<>();
hashSet1.add(new BigDecimal("1.0"));
System.out.println(hashSet1.contains(new BigDecimal("1")));// 返回 false

解决方法:

  1. 使用 TreeSet 替换 HashSet。TreeSet 不使用 hashCode 方法,也不使用 equals 比较元素,而是使用 compareTo 方法
  2. 把 BigDecimal 存入 HashSet 或 HashMap 前,先使用stripTrailingZeros 方法去掉尾部的零,比较的时候也去掉尾部的 0,确保 value 相同的BigDecimal,scale 也是一致的

小心数值溢出问题

不管是 int 还是 long,所有的基本数值类型都有超出表达范围的可能性

long l = Long.MAX_VALUE;
System.out.println(l + 1); // 出现负数 -9223372036854775808
System.out.println(l + 1 == Long.MIN_VALUE); // true

显然这是发生了溢出,而且是默默的溢出,并没有任何异常
解决方法:

  1. 考虑使用 Math 类的 addExact、subtractExact 等 xxExact 方法进行数值运算,这些方法可以在数值溢出时主动抛出异常。
    long l = Long.MAX_VALUE;

System.out.println(Math.addExact(l, 1));

  1. 使用大数类 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();
}

对于金融、科学计算等场景,请尽可能使用 BigDecimal 和 BigInteger

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值