BigDecimal

BigDecimal类用于表示一个高精度的数字类型,它可以表达一个无限大小的数字(只要内存够),而且表达小数没有精度损失,在需要精确计算的场景很常用,尤其是表达“钱”的时候!这篇笔记我们介绍BigDecimal的用法。

什么是浮点数精度损失

Java中的基本类型floatdouble在进行浮点计算时都会出现精度丢失的现象,如果用它处理金融计算、高精度科学计算等场景,稍不注意就会造成非常严重的问题,下面是一段例子代码。

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测试例子如下。

BigDecimal的构造函数

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四则运算

下面例子代码演示和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比较

对于整数或浮点数字面量,比较它们直接使用比较运算符就行了,但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.02.00),它不会认为这两个数是相等的。

BigDecimal转字符串

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

关于BigDecimal的数据库类型

这里额外补充一下,Java中BigDecimal类型对应的MySQL数据库类型是Decimal,它能表述一个定点小数,当然,也有一些奇葩的设计用varchar类型等。然而,假如数据库用的Decimal类型,保留位数不是太多,而Java代码又误用了double,数据入库的时候很可能看不出来什么问题,那这个精度丢失的Bug就更加隐匿了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。
Copyright © 2017-2024 Gacfox All Rights Reserved.
Build with NextJS | Sitemap