BigDecimal与Double的区别和使用场景
背景
在项目中发现开发小组成员在写程序时,对于Oracle
数据类型为Number
的字段(经纬度),实体映射类型有的人用Double
有的人用BigDecimal
,没有一个统一规范,为此我在这里总结记录一下。
一般说到BigDecimal
与Double
,绕不开的就是金融或电商行业,毕竟涉及到了钱的问题,数据的敏感程度很高,对数据精度要求也很高。
BigDecimal
与Double
于两种类型在使用上都有一些缺点。
Double的问题
- 在计算时会出现不精确的问题
public static void main(String[] args) {System.out.println(12.3 + 45.6); // 57.900000000000006System.out.println(12.3 / 100); // 0.12300000000000001
}
-
小数部分无法使用二进制准确的表示
-
等于判断在使用时需要注意
public static void main(String[] args) {double a = 2.111111111111111111111111112;double b = 2.111111111111111111111111113;// duoble超过15位以后就会不对了System.out.println(a == b); // true
}
BigDecimal的问题
- 使用除法时除不尽会报
ArithmeticException
异常
public static void main(String[] args) {// 报异常:Exception in thread "main" java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.System.out.println(BigDecimal.valueOf(121).divide(BigDecimal.valueOf(3)));
}
public static void main(String[] args) {// 需要指定精度和舍入方式,当除不尽时也不会报异常 // 运行结果为 40.33System.out.println(BigDecimal.valueOf(121).divide(BigDecimal.valueOf(3), 2, RoundingMode.HALF_UP));
}
new BigDecimal(double)
结果或许预料不到。
public static void main(String[] args) {BigDecimal a = new BigDecimal(12.3);BigDecimal b = BigDecimal.valueOf(12.3);// Double的小数位其实无法被精确表示,所以传入的.3被精度扩展之后精度丢失,展示出来的并不是精确的.3// 结果为12.300000000000000710542735760100185871124267578125System.out.println(a);// 从底层代码可以看出来,BigDecimal.valueOf 会先转换为字符串之后再调用new BigDecimal,不会造成精度丢失// 结果为12.3System.out.println(b);
}
所以一般情况下会使用BigDecimal.valueOf()
而不是new BigDecimal()
。
另:
BigDecimal.valueOf()
是静态工厂类,永远优先于构造函数。(摘自《Effective java》)
BigDecimal
是不可变类
不可变类代表着,任何针对BigDecimal
的修改都将产生新对象。所以每次对BigDecimal
的修改都要重新指向一个新的对象。
public static void main(String[] args) {BigDecimal a = BigDecimal.valueOf(12.3);a.add(BigDecimal.valueOf(2.1));System.out.println(a); // 12.3,值未修改BigDecimal b = BigDecimal.valueOf(12.3);// 重新赋值给新对象BigDecimal c = b.add(BigDecimal.valueOf(2.1));System.out.println(c); // 14.4
}
- 比较大小不方便
BigDecimal
大小的比较都需要使用compareTo
,如果需要返回更大的数或更小的数可以使用max
、min
。还要注意在BigDecimal
中慎用equals
。
public static void main(String[] args) {BigDecimal a = BigDecimal.valueOf(12.3);BigDecimal b = BigDecimal.valueOf(12.32);System.out.println(a.compareTo(b)); // -1System.out.println(b.compareTo(a)); //1System.out.println(a.max(b)); // 12.32System.out.println(a.min(b)); // 12.3System.out.println(b.max(a)); // 12.32System.out.println(b.min(a)); // 12.3System.out.println(BigDecimal.valueOf(1).equals(BigDecimal.valueOf(1.0))); //false
}
BigDecimal
重写了equals
方法,在equals
方法里比较了小数位数,在方法注释上也有说明,所以 BigDecimal.valueOf(1).equals(BigDecimal.valueOf(1.0))
为什么结果为false
就可以理解了。在附录中会贴出 BigDecimal
中的 equals
方法的源码。
通过源码可以知道 if (scale != xDec.scale)
这句代码就是比较了小数位数,不等则直接返回false
。
Double与BigDecimal数值操作效率比较
做了一个测试,从1累加到1000000,Double
与BigDecimal
的效率比:
public static void main(String[] args) {double a = 0;long startLong = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {a += i;}// 打印结果:double耗时:9System.out.println("double耗时:" + (System.currentTimeMillis() - startLong));BigDecimal b = new BigDecimal(0);long startLong2 = System.currentTimeMillis();for (int i = 0; i < 1000000; i++) {b = b.add(BigDecimal.valueOf(i));}// 打印结果:BigDecimal耗时:83System.out.println("BigDecimal耗时:" + (System.currentTimeMillis() - startLong2));
}
可以看到Double
比BigDecimal
的效率高了快10倍。
总结
-
使用
Double
:在计算过程中会出现精度丢失问题,但使用方便,计算效率高,在对精度要求不高的情况下建议使用。
-
使用
BigDecimal
:高精度,但做除法的时候要注意除不尽异常,且因为是不可变类和对象类型,做计算时没那么方便,效率比
Double
低。
所以可以知道,在使用场景上,如果涉及到精确的数值计算,比如典型的金额,一定要使用BigDecimal
进行计算,对精准度要求没那么高的可以不使用BigDecimal
。需要进行频繁计算的可以使用Double
。
其实Double
或者Float
这种浮点类型如果不参与计算只是传值的话,其实没有精度问题,如果要计算就得注意一下。
那么回到我们自己的项目上,经纬度其实一般业务代码也不会去进行计算,大多是用于传参定位记录用,涉及到计算的比如距离,如果是保留到6位小数时已经是1米级别了,可以满足绝大多数场景了,所以在经纬度上使用Double
就足够了。
附录
BigDecimal 的 equals方法源码。
/*** Compares this {@code BigDecimal} with the specified* {@code Object} for equality. Unlike {@link* #compareTo(BigDecimal) compareTo}, this method considers two* {@code BigDecimal} objects equal only if they are equal in* value and scale (thus 2.0 is not equal to 2.00 when compared by* this method).* 该方法认为两个BigDecimal对象只有在值和比例相等时才相等,所以当使用该方法比较2.0与2.00时,二者不相等。** @param x {@code Object} to which this {@code BigDecimal} is* to be compared.* @return {@code true} if and only if the specified {@code Object} is a* {@code BigDecimal} whose value and scale are equal to this* {@code BigDecimal}'s.* @see #compareTo(java.math.BigDecimal)* @see #hashCode*/
@Override
public boolean equals(Object x) {// 比较对象是否为 BigDecimal 数据类型,不是直接返回falseif (!(x instanceof BigDecimal))return false;BigDecimal xDec = (BigDecimal) x;if (x == this)return true;// 比较 scale 值是否相等。在这里比较了小数位数,不等返回false。// scale 是BigDecimal 的标度。如果为零或正数,则标度是小数点后的位数。// 如果为负数,则将该数的非标度值乘以 10 的负 scale 次幂。例如,-3 标度是指非标度值乘以 1000。if (scale != xDec.scale)return false;long s = this.intCompact;long xs = xDec.intCompact;if (s != INFLATED) {if (xs == INFLATED)xs = compactValFor(xDec.intVal);return xs == s;} else if (xs != INFLATED)return xs == compactValFor(this.intVal);return this.inflated().equals(xDec.inflated());
}
关于MySql中如何选用这两种类型
在查资料的时候还看到了关于MySql
中如何选用这两种类型的问题,也在此记录一下。
在数据库中除了指定数据类型之外还需要指定精度,所以在MySql
里Double
的计算精度丢失比在Java
里要高很多,Java
的默认精度到了15-16位。
在阿里的编码规范中也强调统一带小数类型的一律要使用Decimal
类型而不是Double
,使用Decimal
可以大大减少计算采坑的概率。
所以在选用类型时,与Java
同样,在精度要求不高的情况下可以使用Double
,比如经纬度,但是有需要计算、金融金额等优先使用Decimal
。