本文主要阐述Java中的浮点数计算需要注意的点。

浮点数

运算

测试用例

1
2
3
4
5
6
7
double a = 0.1+0.2;//0.30000000000000004
double b = 1.0-0.8;//0.19999999999999996
double c = 4.015*100;//401.49999999999994
double d = 123.3/100;//1.2329999999999999

double amount1 = 2.15,amount2 = 1.10;
amount1 - amount2 == 1.05;//false

分析

计算机是以二进制存储数值的,浮点数也不例外。Java 采用了IEEE 754 标准实现浮点数的表达和运算。如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。

对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。

结论

虽然二进制数转换成十进制数的差值不大,但当涉及大数值时,这个数目就不小,因此不能被忽略。可以用BigDecimal 类型,用于浮点数精确表达和运算的场景

格式化

测试用例

1
2
3
4
5
double num1 = 3.35;
float num2 = 3.35f;
//四舍五入
String.format("%.1f", num1); //3.350000000000000088817841970012523233890533447265625
String.format("%.1f", num2); //3.349999904632568359375

分析

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

结论

浮点数的字符串格式化要通过 BigDecimal 进行。配置参数ROUND_DOWN表示向下舍去,ROUND_HALF_UP表示四舍五入。

1
2
3
BigDecimal num1 = new BigDecimal("3.35");
BigDecimal num2 = num1.setScale(1, BigDecimal.ROUND_DOWN); //3.3
BigDecimal num3 = num1.setScale(1, BigDecimal.ROUND_HALF_UP); //3.4

BigDecimal

注意

BigDecimal 的使用有什么注意的点?

  1. 使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal。如,使用new BigDecimal("number"),而不是new BigDecimal(number)
  2. 如果一定要用 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
2
3
long l = Long.MAX_VALUE; 
l + 1; //-9223372036854775808
l + 1 == Long.MIN_VALUE; //true

分析

long 的最大值加一变成了最小值。这是发生了溢出,而且没有任何异常。使用 Math 类的 addExact 方法,可以捕获到 ArithmeticException 异常,为 long overflow。

BigInteger

可以使用 BigInteger 对大数值进行计算。

1
2
BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE));
i.add(BigInteger.ONE).toString(); //不会抛出异常

结论

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