double、Double与BigDecimal

璩珂
2023-12-01

在电商与金融场景中,对数据的敏感程度较高,一旦出现了精度的问题,涉及到钱,精度问题造成了误差,会造成一定的经济损失,这是大家都不希望看到的。

所以今天,来学习一下,如何正确的使用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计算累加,效率如下:

十万五十万一百万五百万 一千万五千万
double1ms2ms3ms14ms17ms84ms
BigDecimal12ms32ms47ms236ms518ms2446ms

表格中的数据,不准确,只能看个大概,随着数据量增长,double比BigDecimal的效率要快几十倍

优缺点比较与使用场景

最后来总结一下,各个优缺点与使用场景

优点缺点使用场景
double计算效率高,存在栈中
有包装类Double自动拆箱,装箱
精度丢失对数据的精度敏感程度不高
可以容忍精度损失的情况
BigDecimal精度准确
能够设置保留位数,舍入方式
BigDecimal是对象类型,在堆中
没有自动拆封箱机制,操作起来较麻烦
需要精准计算
 类似资料: