使用BigDecimal计算防止精度丢失
背景
最近工作的业务上需要使用到计算相关的函数,虽然有过一点基本的理论知识,知道计算相关必须使用BigDecimal
处理进行处理,若使用float
、double
会丢失一定的精度,但是在使用过程中还是踩了一些坑,所以对BigDecimal
原理进行更深入的了解。
原理
为什么float
、double
会存在精度丢失?
主要是由于二进制浮点表示方式的限制,在计算机中存储的是二进制数,而十进制和二进制之间的转换可能并不完美,某些十进制小数无法完美的转换为二进制表示,比如0.1
无法使用二进制精确表示,所以就有了IEEE 754
规范,用一个无限小数近似表示,对于float
单精度和double
双精度,简单理解就是保留的位数长度不同。
为什么BigDecimal
能防止精度丢失?
BigDecimal
是专门设计来解决浮点数精度问题,它是一个类,注意float
和double
是数据类型,而BigDecimal
是一个类。
1 | public class BigDecimal extends Number implements Comparable<BigDecimal> { |
简单理解它使用两个数来表示一个浮点数,比如0.03
,scale
表示“负的多少次幂”,intCompact
表示移除小数点之后的值,所以这时intCompact=3, scale=2
。也就是3*0.01=0.03
来表示这个数。
调试:
但是我在调试的时候发现一个问题,可能会造成一些误解。
1 | public static void main(String[] args) { |
可以先自己尝试理解,如果这段代码直接运行会得到什么样的结果。
上面提到,浮点数是无法精确的表达0.1
、0.2
的,但是实际上前两句的输出是会精确的输出0.1
、0.2
,但是第三句的求和运算后输出的值是一个近视值0.30000000149011613
,到这里可能很多人会想不明白,甚至如果你通过idea
的debug
模式,会发现,debug
中a=0.2
、b=0.1
,到这里可能就想不明白到底是为啥。
其实是System.out.println()
是被特殊处理的,如果采用System.out.printf("%.20f\n", b);
的方式打印,就会打印出一个近似值0.10000000149011612000
。
至于idea
的debug
模式为什么也是显示a=0.2
、b=0.1
,是因为idea
对float
和double
通常是经过格式化处理的,目的是更易于人类阅读和理解(chatgpt
解答)
注意点
1.使用浮点数创建BigDecimal对象
1 | BigDecimal(double) |
这里输出的结果是0.1000000000000000055511151231257827021181583404541015625
。
这是因为double
表示的小数是不精确的,只能表示对应的近似值,所以使用这个近似值来创建BigDecimal
,最后得到的也只是一个近似值。所以使用浮点数来创建BigDecimal
对象的方式在实际中是不可取的。
2.BigDecimal比较大小
由于BigDecimal
是一个类,在BigDecimal.equals()
方法中是比较scale
、intCompact
等值是否相等来判定是否相等。比如
0.2
:intCompact=2, scale=1
0.20
:intCompact=2, scale=2
那么这时equals()
方法会判定二者不相等,但是在实际的业务场景中应该99.9%
是想让这两个值相等。
所以BigDecimal
是否相等的判断最好是通过compareTo()
方法,若两个值相等则返回0。
3.创建BigDecimal
如果需要创建一个精确表示浮点数的BigDecimal
对象:
1 | BigDecimal v1 = new BigDecimal("0.1"); |
第一种方式是使用字符串进行BigDecimal
的初始化,先使用字符串来表是0.1
,再创建BigDecimal
对象。
第二种方式会调用到Dable.toString()
方法,前面说到double
可能无法表达精确的小数,那么这里为什么可以精确的创建出0.1
的BigDecimal
对象?其实Double.toString()
,这个toString()
方法是经过特殊设计,能够返回一个最简短的、没有信息损失的、可恢复的字符串表示,这意味着你可以从该字符串表示重新构建一个与原始double
值完全相等的double
值。