BigDecimal
类用于表示一个高精度的数字类型,它可以表达一个无限大小的数字(只要内存够),而且表达小数没有精度损失,在需要精确计算的场景很常用,尤其是表达“钱”的时候!这篇笔记我们介绍BigDecimal
的用法。
Java中的基本类型float
和double
在进行浮点计算时都会出现精度丢失的现象,如果用它处理金融计算、高精度科学计算等场景,稍不注意就会造成非常严重的问题,下面是一段例子代码。
double productA = 0.05;
double productB = 0.01;
double money = 0.06;
if (productA + productB <= money) {
System.out.println("可以购买");
} else {
System.out.println("钱不够");
}
假设现在有一个电商系统,用户有0.06
元钱,现在要购买一个0.05
元和一个0.01
元的商品,显然他有足够的钱可以下单。然而,上面代码的输出却是钱不够!这是因为在在计算机中,浮点数是使用二进制表示的,这和我们十进制不能表达很多无限小数的分数是一样的道理,某些值以浮点数存储,会被处理成一个近似值,也就是说会有微小的精度损失。但就是这种微小的精度损失,却可能给我们系统造成逻辑错误。
当然,不止Java的浮点数有这个问题,只要是运行在二进制计算机上的“浮点数”,都有精度损失的问题。
Python测试例子如下。
NodeJS测试例子如下。
Java中使用BigDecimal
对象来表达高精度数字。下面代码展示了Java中如何创建BigDecimal
对象,记住绝对不要用浮点数构造BigDecimal。一个浮点数变量或是浮点数字面量,它在内存中实际上已经丢失精度了,即使赋值给BigDecimal也是一个错误的值!正确的做法是使用字符串构造BigDecimal,下面是一些例子。
// BigDecimal典型错误用法1
BigDecimal a = new BigDecimal(0.1);
// BigDecimal典型错误用法2
double num = 0.1;
BigDecimal b = new BigDecimal(num);
// 正确用法
BigDecimal c = new BigDecimal("0.1");
System.out.println(a);
System.out.println(b);
System.out.println(c);
上面代码输出结果如下。
0.1000000000000000055511151231257827021181583404541015625
0.1000000000000000055511151231257827021181583404541015625
0.1
可见,前两个结果不是我们想要的。
不可变性:BigDecimal
是不可变对象,也就是说,一旦创建了一个BigDecimal
对象,该对象的值就不能改变了。如果对BigDecimal
对象进行加减乘除运算,实际上返回的是一个新的BigDecimal
对象,原始的BigDecimal
对象并没有被改变。
线程安全:BigDecimal
的不可变性也保证了它的线程安全性,多个线程可以共享同一个BigDecimal
对象,而不需要担心线程安全问题。
高精度但速度较慢:BigDecimal
可以处理非常高精度的运算,但它的运算速度比基本数据类型慢很多,BigDecimal
有着明确的适用场景,如果不需要高精度计算,应该尽量避免使用BigDecimal
。
下面例子代码演示和BigDecimal
的加减乘除运算。
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.3");
BigDecimal addResult = a.add(b);
BigDecimal subResult = a.subtract(b);
BigDecimal mulResult = a.multiply(b);
BigDecimal divResult = a.divide(b, 2, BigDecimal.ROUND_HALF_UP);
System.out.println(addResult);
System.out.println(subResult);
System.out.println(mulResult);
System.out.println(divResult);
加法、减法、乘法没什么可说的,我们主要看除法。除法可能产生一个无限小数(十进制无法精确表示的分数),因此我们一般使用除法时都指定保留位数和保留算法。上面代码中的a.divide(b, 2, BigDecimal.ROUND_HALF_UP)
,我们保留两位小数,使用最传统的“四舍五入”。
对于更复杂的运算,如乘方、三角函数等,JDK本身并没有提供BigDecimal
的实现,这通常就需要借助第三方库了,如Apache的CommonsMath库。
对于整数或浮点数字面量,比较它们直接使用比较运算符就行了,但BigDecimal
比较复杂一点,我们需要调用compareTo()
方法,下面是一个例子。
BigDecimal a = new BigDecimal("0.1");
BigDecimal b = new BigDecimal("0.3");
System.out.println(a.compareTo(b));
上面代码输出结果如下。
-1
对于a.compareTo(b)
,当a<b
时返回-1
,当a=b
时返回0
,当a>b
时返回1
。
注意:比较两个BigDecimal
的值相等时使用equals()
是错误的!当两个BigDecimal的scale
不同时(比如2.0
和2.00
),它不会认为这两个数是相等的。
BigDecimal转字符串有两个方法,一个是toString()
,一个是toPlainString()
,它们之间的区别在于toPlainString()
不会对数字进行科学计数法表示,而toString()
在某些情况下可能会。
BigDecimal i = new BigDecimal("0.00000000000000000000000000000000000123456789");
System.out.println(i.toString());
System.out.println(i.toPlainString());
上面代码输出结果如下。
1.23456789E-36
0.00000000000000000000000000000000000123456789
这里额外补充一下,Java中BigDecimal
类型对应的MySQL数据库类型是Decimal
,它能表述一个定点小数,当然,也有一些奇葩的设计用varchar
类型等。然而,假如数据库用的Decimal
类型,保留位数不是太多,而Java代码又误用了double
,数据入库的时候很可能看不出来什么问题,那这个精度丢失的Bug就更加隐匿了。