一、问题背景
在Java
中,用来表示浮点数的数据类型有 float
、double
。在涉及到金钱相关的开发需求当中,它的计算结果会怎样。先看几个简单的浮点数四则运算:
public static void main(String[] args) {
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
System.out.println((2.15 - 1.10) == 1.05); // false
}
从程序计算结果来看,和我们的预期是有很大差距的。比如 0.1 + 0.2
的结果并不是 0.3
,造成这种现象的主要原因是:计算机是以二进制来存储数值的,浮点数也不例外。0.1
的二进制表示为: 0.0 0011 0011 0011 ....
,转换为十进制为: 0.1000000000000000055511151231257827021181583404541015625
。可以看到的 0.1
的二进制表示是个无限循环的,那么这个无限循环对计算机来讲是没法精确表示,这就造成了浮点数的精度损失的原因。为了尽可能的降低精度损失带来的计算误差,Java 中为我们提供了BigDecimal
来提高精度。
public static void main(String[] args) {
// 0.3000000000000000166533453693773481063544750213623046875
System.out.println(new BigDecimal(0.1).add(new BigDecimal(0.2)));
// 0.1999999999999999555910790149937383830547332763671875
System.out.println(new BigDecimal(1.0).subtract(new BigDecimal(0.8)));
// 401.49999999999996802557689079549163579940795898437500
System.out.println(new BigDecimal(4.015).multiply(new BigDecimal(100)));
// 1.232999999999999971578290569595992565155029296875
System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100)));
}
可以看到,0.1 + 0.2
和 new BigDecimal(0.1).add(new BigDecimal(0.2))
的计算结果还是存在误差,只是 BigDecimal
的结果更加精确,那么 BigDecimal
为什么没有达到我们想要的预期呢?这就要了解BigDecimal
的一些使用技巧。
二、BigDecimal
使用技巧
2.1 必须使用字符串的构造方法来初始化BigDecimal
对象
public static void main(String[] args) {
// 0.3
System.out.println(new BigDecimal("0.1").add(new BigDecimal("0.2")));
// 0.2
System.out.println(new BigDecimal("1.0").subtract(new BigDecimal("0.8")));
// 401.500
System.out.println(new BigDecimal("4.015").multiply(new BigDecimal("100")));
// 1.233
System.out.println(new BigDecimal("123.3").divide(new BigDecimal("100")));
}
为什么在使用了 public BigDecimal(String val)
之后,计算结果就符合我们的预期了呢?BigDecimal
的两个重要属性
public class BigDecimal extends Number implements Comparable<BigDecimal> {
// 表示小数点后的的位数,也就是保留几位小数
private final int scale;
/**
* 表示精度,也就是有效数字的长度,什么是有效数字长度?举个例子:
* 12345 / 100000 = 0.12345 则 scale = 5 precision = 5
* 1 / 100000 = 0.00001 则 scale = 5 precision = 1
*/
private transient int precision;
}
所以在 public BigDecimal(String val)
字符的构造函数中,根据字符串的长度解析得到 scale
和 precision
。
2.2 统一使用compareTo
方法来比较BigDecimal
的大小
public static void main(String[] args) {
BigDecimal a = new BigDecimal("0.01");
BigDecimal b = new BigDecimal("0.010");
System.out.println(a.equals(b)); // false
System.out.println(a.compareTo(b) == 0); // true
}
因为BigDecimal
的equals
方法会同时比较 value
和 scale
2.3 BigDecimal
的加减乘除运算时最好都设置保留小数位数和舍入模式
- 舍入模式参考
java.math.RoundingMode
类,比如
public static void main(String[] args) {
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("3.0");
System.out.println(a.divide(b));
}
运行时报 ```java.lang.ArithmeticException````
Exception in thread "main" java.lang.ArithmeticException:
Non-terminating decimal expansion; no exact representable decimal result.
at java.math.BigDecimal.divide(BigDecimal.java:1690)
所以为了避免这种错误,建议按照如下方式使用:
- 针对
divide
方法,运算过程中一定要设置好小数位数和舍入模
public static void main(String[] args) {
BigDecimal divide = new BigDecimal("1.0").divide(new BigDecimal("3.0"), 2, RoundingMode.UP);
System.out.println(divide); //0.34
}
- 针对
add
、subtract
和multiply
加减乘的方法,对运算结果一定要调用setScale
方法设置好小数位数和舍入模式
public static void main(String[] args) {
// 0.3000000000000000166533453693773481063544750213623046875
BigDecimal add = new BigDecimal(0.1).add(new BigDecimal(0.2)).setScale(2, RoundingMode.UP);
System.out.println(add); // 0.31
// 0.1999999999999999555910790149937383830547332763671875
BigDecimal sub = new BigDecimal(1.0).subtract(new BigDecimal(0.8)).setScale(2, RoundingMode.UP);
System.out.println(sub); // 0.20
// 401.49999999999996802557689079549163579940795898437500
BigDecimal mul = new BigDecimal(4.015).multiply(new BigDecimal(100)).setScale(2, RoundingMode.UP);
System.out.println(mul); // 401.50
}
2.4 统一使用toPlainString()
方法来打印 BigDecimal
结果
public static void main(String[] args) {
BigDecimal a = new BigDecimal("4536785E10");
// 4.536785E+16
System.out.println(a.toString());
// 45.36785E+15
System.out.println(a.toEngineeringString());
// 45367850000000000
System.out.println(a.toPlainString());
}
toString()
和toEngineeringString()
会使用科学计数法来输出结果toPlainString()
会真正的输出实际结果值
2.5 BigDecimal
计算结果格式化
数值格式化主要有:NumberFormat 和 DecimalFormat,这里我就不再叙述,详情可参考博文: www.cnblogs.com/miniSimple/…
三、数值计算防止溢出
数值计算还有一个要小心的点是溢出,不管是 int 还是 long,都有超出表达范围的可能性。如下代码案例:
public static void main(String[] args) {
long l = Long.MAX_VALUE;
// -9223372036854775808
System.out.println(l + 1);
}
显然这个计算结果是不对的,但是没有任何异常信息,在实际开发中这个是很危险的!
- 使用
Math
类的addExact
、subtractExact
等xxExact
方法进行数值运算,这些方法可以在数值溢出时主动抛出异常
public static void main(String[] args) {
long l = Long.MAX_VALUE;
System.out.println(Math.addExact(l, 1));
}
- 使用
BigInteger
BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。
public static void main(String[] args) {
BigInteger big = new BigInteger(String.valueOf(Long.MAX_VALUE));
// 9223372036854775808
System.out.println(big.add(BigInteger.valueOf(1)).toString());
System.out.println(big.add(BigInteger.valueOf(1)).longValueExact());
}
四、总结
总之,对于金融、支付等场景,业务需要时请尽可能使用 BigDecimal 或者 BigInteger,避免由精度和溢出引起重大的 Bug。