0%

使用BigDecimal计算防止精度丢失

使用BigDecimal计算防止精度丢失

背景

​ 最近工作的业务上需要使用到计算相关的函数,虽然有过一点基本的理论知识,知道计算相关必须使用BigDecimal处理进行处理,若使用floatdouble会丢失一定的精度,但是在使用过程中还是踩了一些坑,所以对BigDecimal原理进行更深入的了解。

原理

为什么floatdouble会存在精度丢失?

​ 主要是由于二进制浮点表示方式的限制,在计算机中存储的是二进制数,而十进制和二进制之间的转换可能并不完美,某些十进制小数无法完美的转换为二进制表示,比如0.1无法使用二进制精确表示,所以就有了IEEE 754规范,用一个无限小数近似表示,对于float单精度和double双精度,简单理解就是保留的位数长度不同。

为什么BigDecimal能防止精度丢失?

BigDecimal是专门设计来解决浮点数精度问题,它是一个类,注意floatdouble是数据类型,而BigDecimal是一个类。

1
2
3
4
5
6
public class BigDecimal extends Number implements Comparable<BigDecimal> {
// 省略部分字段
private final int scale;

private final transient long intCompact;
}

简单理解它使用两个数来表示一个浮点数,比如0.03scale表示“负的多少次幂”,intCompact表示移除小数点之后的值,所以这时intCompact=3, scale=2。也就是3*0.01=0.03来表示这个数。

调试:

但是我在调试的时候发现一个问题,可能会造成一些误解。

1
2
3
4
5
6
7
public static void main(String[] args) {
double a = 0.2;
float b = 0.1F;
System.out.println(a);
System.out.println(b);
System.out.println(a + b);
}

可以先自己尝试理解,如果这段代码直接运行会得到什么样的结果。

上面提到,浮点数是无法精确的表达0.10.2的,但是实际上前两句的输出是会精确的输出0.10.2,但是第三句的求和运算后输出的值是一个近视值0.30000000149011613,到这里可能很多人会想不明白,甚至如果你通过ideadebug模式,会发现,debuga=0.2b=0.1,到这里可能就想不明白到底是为啥。

其实是System.out.println()是被特殊处理的,如果采用System.out.printf("%.20f\n", b);的方式打印,就会打印出一个近似值0.10000000149011612000

至于ideadebug模式为什么也是显示a=0.2b=0.1,是因为ideafloatdouble通常是经过格式化处理的,目的是更易于人类阅读和理解(chatgpt解答)

注意点

1.使用浮点数创建BigDecimal对象

1
2
3
4
5
6
BigDecimal(double)
BigDecimal(long)

public static void main(String[] args) {
System.out.println(new BigDecimal(0.1));
}

这里输出的结果是0.1000000000000000055511151231257827021181583404541015625

这是因为double表示的小数是不精确的,只能表示对应的近似值,所以使用这个近似值来创建BigDecimal,最后得到的也只是一个近似值。所以使用浮点数来创建BigDecimal对象的方式在实际中是不可取的。

2.BigDecimal比较大小

由于BigDecimal是一个类,在BigDecimal.equals()方法中是比较scaleintCompact等值是否相等来判定是否相等。比如

0.2intCompact=2, scale=1

0.20intCompact=2, scale=2

那么这时equals()方法会判定二者不相等,但是在实际的业务场景中应该99.9%是想让这两个值相等。

所以BigDecimal是否相等的判断最好是通过compareTo()方法,若两个值相等则返回0。

3.创建BigDecimal

如果需要创建一个精确表示浮点数的BigDecimal对象:

1
2
BigDecimal v1 = new BigDecimal("0.1");
BigDecimal v2 = BigDecimal.valueOf(0.1);

第一种方式是使用字符串进行BigDecimal的初始化,先使用字符串来表是0.1,再创建BigDecimal对象。

第二种方式会调用到Dable.toString()方法,前面说到double可能无法表达精确的小数,那么这里为什么可以精确的创建出0.1BigDecimal对象?其实Double.toString(),这个toString()方法是经过特殊设计,能够返回一个最简短的、没有信息损失的、可恢复的字符串表示,这意味着你可以从该字符串表示重新构建一个与原始double值完全相等的double值。

-------------本文结束感谢您的阅读-------------