在电商与金融场景中,对数据的敏感程度较高,一旦出现了精度的问题,涉及到钱,精度问题造成了误差,会造成一定的经济损失,这是大家都不希望看到的。
所以今天,来学习一下,如何正确的使用double、Double与BigDecimal
double与Double
double是一种基本数据类型,创建引用,存放在栈中,8字节,不可为NULL
Double是一个类,创建对象,存放在堆中,可以为NULL
Double是对double类型的封装(也可以说是包装类),内置很多方法可以实现String到double的转换,以及获取各种double类型的属性值(MAX_VALUE,SIZE等)
double可以自动拆箱和自动装箱,把double类型数据转换成Double,那么这个数据就可以使用Double中的所有方法
另外,由于Double是包装类型(可为NULL),在接口中使用的时候,也只能使用包装类型,不能使用基本数据类型,例如:ThreadLocal<Double>、List<Double>、Map<Double,Object>等
double在使用中,还会有一些隐藏的坑
1.计算时出现不准确的问题
System.out.println(12.3 + 45.6); // 57.900000000000006 System.out.println(1.14 / 100); // 0.011399999999999999
double的小数部分容易出现使用二进制无法准确表示
如十进制的0.1,0.2,0.3,0.4 都不能准确表示成二进制
2.判断double是否相等需要注意
在使用 == 判断两个double是否相等时,如果double超过了16位,会出现问题。
16位,不相等
double a = 1.1234567890123451; double b = 1.1234567890123452; System.out.println(a == b); // false
17位,相等
double a = 1.12345678901234561; double b = 1.12345678901234567; System.out.println(a == b); // true
如果在使用场景中,对数据的敏感程度不高,可以容忍精度损失的情况,例如:计算占比,百分比等情况,本身对数据的结果是未知的,且数据不敏感,推荐使用double,节省了时间和空间
BigDecimal
与Double相同,BigDecimal也是一个类,创建对象,存放在堆中
上文我们说过,double超过了16位就会有一些精度损失的问题,那么BigDecimal就是用来对超过16位有效位的数进行精确的运算,并且BigDecimal可以控制舍入方式和保留位数,可以说非常贴心
因为BigDecimal是一个类,所以在运算中无法直接进行 + – x ÷ 等算术运算符的的操作,而要调用对应的方法
加 add 减 subtract 乘 multiply 除 divide
同样在BigDecimal在使用中也要注意一些问题
1.new BigDecimal(double) 与 BigDecimal.valueOf(double)
BigDecimal bd1 = BigDecimal.valueOf(12.3); // 12.3 BigDecimal bd2 = new BigDecimal(12.3); // 12.300000000000000710542735760100185871124267578125
通过上面的例子可以看出,new BigDecimal的方式,可能不是你想要的结果,这种问题,早在在源码中 BigDecimal(double) 构造函数的注释中就有说明,说明如下:
①参数类型为double的构造方法的结果有一定的不可预知性
有人可能认为在Java中写入newBigDecimal(0.1)所创建的BigDecimal正好等于 0.1(非标度值 1,其标度为 1),但是它实际上等于
0.1000000000000000055511151231257827021181583404541015625,这是因为0.1无法准确地表示为 double(或者说对于该情况,不能表示为任何有限长度的二进制小数)
这样,传入到构造方法的值不会正好等于 0.1(虽然表面上等于该值)
②String 构造方法是完全可预知的:写入 newBigDecimal(“0.1”) 将创建一个 BigDecimal,它正好等于预期的 0.1,因此,比较而言, 通常建议优先使用String构造方法
③当double必须用作BigDecimal的源时,请注意,此构造方法提供了一个准确转换;它不提供与以下操作相同的结果:先使用Double.toString(double)方法,然后使用BigDecimal(String)构造方法,将double转换为String。要获取该结果,请使用static valueOf(double)方法
另外,BigDecimal.valueOf() 是静态工厂类,永远优先于构造函数,所以,在使用的时候,推荐使用BigDecimal.valueOf(xxx)
2.divide除法
1)除不尽
BigDecimal bd1 = BigDecimal.valueOf(100); BigDecimal bd2 = BigDecimal.valueOf(3); System.out.println(bd1.divide(bd2));
除不尽时,会抛出异常:java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.
发生这样的问题,是使用除法divide时,没有指定精度和舍入模式
正确的使用:
// 第二个参数,设置保留位数 // 第三个参数,舍入模式 System.out.println(bd1.divide(bd2, 5, BigDecimal.ROUND_HALF_UP));
关于舍入模式,BigDecimal提供了8种方式
2)除数为0
当除数为0时,也会抛出异常:java.lang.ArithmeticException: / by zero
所以,需要注意除数为0的情况,需要对除数为0进行处理
BigDecimal bd1 = BigDecimal.valueOf(100); BigDecimal bd2 = BigDecimal.valueOf(0); System.out.println(bd1.divide(bd2, 8, BigDecimal.ROUND_HALF_UP));
3.比较大小
BigDecimal的比较要使用compareTo() 方法,该方法会返回 int,int 有三种值
返回值为: 0,两者相等
返回值为:-1,前者小于后者
返回值为: 1,前者大于后者
// 返回值 0,相等 System.out.println(BigDecimal.valueOf(100.01).compareTo(BigDecimal.valueOf(100.01))); // 返回值 -1 ,小于 System.out.println(BigDecimal.valueOf(100.001).compareTo(BigDecimal.valueOf(100.01))); // 返回值 1 ,大于 System.out.println(BigDecimal.valueOf(100.01).compareTo(BigDecimal.valueOf(100.001)));
效率对比
double与BigDecimal计算累加,效率如下:
十万 | 五十万 | 一百万 | 五百万 | 一千万 | 五千万 | |
double | 1ms | 2ms | 3ms | 14ms | 17ms | 84ms |
BigDecimal | 12ms | 32ms | 47ms | 236ms | 518ms | 2446ms |
表格中的数据,不准确,只能看个大概,随着数据量增长,double比BigDecimal的效率要快几十倍
优缺点比较与使用场景
最后来总结一下,各个优缺点与使用场景
优点 | 缺点 | 使用场景 | |
double | 计算效率高,存在栈中 有包装类Double自动拆箱,装箱 | 精度丢失 | 对数据的精度敏感程度不高 可以容忍精度损失的情况 |
BigDecimal | 精度准确 能够设置保留位数,舍入方式 | BigDecimal是对象类型,在堆中 没有自动拆封箱机制,操作起来较麻烦 | 需要精准计算 |