本文主要阐述Java中的浮点数计算需要注意的点。
浮点数
运算
测试用例
1 | double a = 0.1+0.2;//0.30000000000000004 |
分析
计算机是以二进制存储数值的,浮点数也不例外。Java 采用了IEEE 754 标准实现浮点数的表达和运算。如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。
对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。
结论
虽然二进制数转换成十进制数的差值不大,但当涉及大数值时,这个数目就不小,因此不能被忽略。可以用BigDecimal 类型,用于浮点数精确表达和运算的场景。
格式化
测试用例
1 | double num1 = 3.35; |
分析
String.format 采用四舍五入的方式进行舍入,取 1 位小数,double 的 3.350 四舍五入为 3.4,而 float 的 3.349 四舍五入为 3.3。
结论
浮点数的字符串格式化要通过 BigDecimal 进行。配置参数ROUND_DOWN表示向下舍去,ROUND_HALF_UP表示四舍五入。
1 | BigDecimal num1 = new BigDecimal("3.35"); |
BigDecimal
注意
BigDecimal 的使用有什么注意的点?
- 使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal。如,使用
new BigDecimal("number"),而不是new BigDecimal(number)。 - 如果一定要用 Double 来初始化 BigDecimal 的话,可以使用 BigDecimal.valueOf 方法。如,使用
BigDecimal.valueOf(doublenum),而不是new BigDecimal(Double.toString(doublenum))。
精确度和位数
BigDecimal 用 scale 表示小数点右边的位数, precision表示精度,即有效位数的长度。
如,用 new BigDecimal(Double.toString(100)) 得到的 BigDecimal 的 scale=1、precision=4;而 new BigDecimal(“100”) 得到的 BigDecimal 的 scale=0、precision=3。
判等
查看 BigDecimal 里的 equals 和 hashCode 源码,可以发现,它们会同时考虑 value 和 scale,就是会区分数值的精确度,即 0.1 与 0.10 是不同的。
- 如果要比较值的话,可以用 compareTo 方法。
- 由于 hashCode 的值不同,不能使用HashSet 或 HashMap。
- 使用 TreeSet 替换 HashSet。TreeSet 不使用 hashCode 方法,也不使用 equals 比较元素,而是使用 compareTo 方法。
- 把 BigDecimal 存入 HashSet 或 HashMap 前,先使用 stripTrailingZeros 方法去掉尾部的零,比较的时候也去掉尾部的 0,确保 value 相同的 BigDecimal,scale 也是一致的。如,
new BigDecimal("1.0").stripTrailingZeros()。
大数值
测试用例
1 | long l = Long.MAX_VALUE; |
分析
long 的最大值加一变成了最小值。这是发生了溢出,而且没有任何异常。使用 Math 类的 addExact 方法,可以捕获到 ArithmeticException 异常,为 long overflow。
BigInteger
可以使用 BigInteger 对大数值进行计算。
1 | BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE)); |
结论
对于金融、科学计算等场景,尽可能使用 BigDecimal 和 BigInteger,避免由精度和溢出问题引发难以发现,但影响重大的 Bug。