本文首发于个人微信公众号《andyqian》,期待你的关注!
前言
在Java中,我们通常使用 BigDecimal 类型来表示金额,特别是在金融,财务系统中,使用的特别多。例如:转账金额,手续费等等。今天就一起来认识下BigDecimal。
为什么是BigDecimal ?
在此之前,我们先来讲讲为什么要使用 BigDecimal ?而不是Float,Double类型?其实光从表现形式来看,Float,Double,BigDecimal 类型都能表示小数。其区别在于精确计算时,Float 与 Double 类型都会损失精度,当然了,BigDecimal 使用不正确时,也会损失精度。在金融系统中,金额计算是最基本的运算,精度的丢失是绝对不能容忍的。接下来,我们来看看下面的例子:
Float 类型:
public void testFloat(){
float a = 1.1f;
float b = 0.8f;
System.out.println("a-b = "+(a-b));
System.out.println("a+b = "+(a+b));
System.out.println("a*b = "+(a*b));
System.out.println("a/b = "+(a/b));
}
结果如下:
a-b = 0.3
a+b = 1.9000001
a*b = 0.88000005
a/b = 1.375
Double 类型:
public void testDouble(){
double a = 1.1;
double b = 0.8;
System.out.println("a-b = "+(a-b));
System.out.println("a+b = "+(a+b));
System.out.println("a*b = "+(a*b));
System.out.println("a/b = "+(a/b));
}
结果如下:
a-b = 0.30000000000000004
a+b = 1.9000000000000001
a*b = 0.8800000000000001
a/b = 1.375
BigDecmial 错误使用:
public void testBigDecimal(){
BigDecimal a = new BigDecimal(1.1);
BigDecimal b = new BigDecimal(0.8);
System.out.println("a-b = "+(a.subtract(b)));
System.out.println("a+b = "+(a.add(b)));
System.out.println("a*b = "+(a.multiply(b)));
System.out.println("a/b = "+(a.divide(b)));
}
结果如下:
a-b = 0.3000000000000000444089209850062616169452667236328125
a+b = 1.9000000000000001332267629550187848508358001708984375
a*b = 0.8800000000000001199040866595169103100567462588676208086428264139311483660321755451150238513946533203125
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
正确使用方法:
public void testBigDecimalNormal(){
BigDecimal a = new BigDecimal("1.1");
BigDecimal b = new BigDecimal("0.8");
System.out.println("a-b = "+(a.subtract(b)));
System.out.println("a+b = "+(a.add(b)));
System.out.println("a*b = "+(a.multiply(b)));
System.out.println("a/b = "+(a.divide(b)));
}
结果如下:
a-b = 0.3
a+b = 1.9
a*b = 0.88
a/b = 1.375
通过上面的例子,我们可以清晰的看出。除了正确使用BigDecimal类型外,其余的在计算过程中,均损失精度。因此我们可以得出以下结论:
- 在需要精度计算数值时,不应该使用float,double 类型,进行计算。
- BigDecimal 应该使用 String 构造函数,禁止使用double构造函数。
使用细节
其实,在使用BigDecimal过程,也有许多需要注意的细节。
- 科学计数法问题
@Test
public void testBigDecimalResult(){
BigDecimal b = new BigDecimal("0.0000001");
System.out.println(b.toString());
System.out.println(b.toPlainString());
}
执行结果:
1E-7
0.0000001
结论:当 BigDecimal的值 小于一定值时(测试时发现:小于等于0.0000001)时,则会被记为科学计数法。可以使用 toPlainString()
方法显示原来的值。
2. 去除多余的 0
@Test
public void testBigDecimalStripZeros(){
BigDecimal b = new BigDecimal("0.000000100000000");
System.out.println(b.stripTrailingZeros().toString());
System.out.println(b.stripTrailingZeros().toPlainString());
}
使用场景:去除多余的0,当金额有小数位限制时,使用该方法能够去除掉无效的0,从而达到自动修复无效参数的目的。
结论:stripTrailingZeros() 方法的本质是去除掉多余的0,其返回数据类型是BigDecimal,同样的在使用时需要注意科学技术法的问题。
3. 保留小数位
@Test
public void testBigDecimalStripZeros(){
BigDecimal d = new BigDecimal("1.2222");
d.setScale(2);
System.out.println(d.toPlainString());
}
运行结果:
java.lang.ArithmeticException: Rounding necessary
原因:在setScale()方法中的roundingMode属性设置为了ROUND_UNNECESSARY,代码如下:
public BigDecimal setScale(int newScale) {
return setScale(newScale,);
}
而在:
java.math.BigDecimal.commonNeedIncrement(BigDecimal.java:4179)
中ROUND_UNNECESSARY 类型恰恰会抛出异常。代码显示如下:
private static boolean commonNeedIncrement(int roundingMode, int qsign,
int cmpFracHalf, boolean oddQuot) {
switch(roundingMode) {
case ROUND_UNNECESSARY:
throw new ArithmeticException("Rounding necessary");
case ROUND_UP: // Away from zero
return true;
...
小结
通过上面的例子,现在我们已经知道了BigDecimal的一些使用细节。其实呀,这些都是血淋淋的教训换来的经验,每一个小细节对应的都是一个个事故,记忆犹新。这里推荐大家都抽时间看看《Java开发手册》,就能避免掉很多坑。
上面的问题,在《Java开发手册》中同样有写到:
【强制】为了防止精度损失,禁止使用构造方法BigDecimal(double)的方式把double值转化为BigDecimal对象。
说明:BigDecimal(double) 存在精度损失风险,在精确计算或值比较的场景中可能会导致业务逻辑异常。如:BigDecimal g = new BigDecimal(0.1f); 实际的存储值为:0.10000000149
正例:优先推荐入参为String 的构造函数,或使用BigDecimal的valueOf方法。
相关阅读:
《软件之路 之 项目外包》
《ThreadPoolExecutor 原理解析》
《Java线程池ThreadPoolExecutor》
《再谈Java 生产神器 BTrace》