浮点数值计算精度丢失问题剖析及解决方法

1、原因分析

首先我们来看个反直觉的浮点数值计算

System.out.println(0.3*3);

在这里插入图片描述

有的同学可能要问为啥不是0.9?

首先要知道为什么会产生这个问题?我们知道计算机的底层世界都是由0和1组成的,而浮点数值就是采用二进制系统表示,常见两种基本的浮点类型: float 和 double。

其中单精度float为32位浮点数,1位符号,8位指数和23位尾数(小数部分)。

在这里插入图片描述

双精度double则为64位浮点数,1位符号,11位指数和52位尾数(小数部分)。

接下来我们先看下十进制小数转二进制的例子,例如将 0.3 转为二进制

0.3*2=0.6 //取整数0
0.6*2=1.2 //取整数1
0.2*2=0.4 //取整数0
0.4*2=0.8 //取整数0
0.8*2=1.6 //取整数1
0.6*2=1.2 //取整数1
0.2*2=0.4 //取整数0
0.4*2=0.8 //取整数0
0.8*2=1.6 //取整数1
......
二进制表示为:010011001......

可以看到计算开始循环,永远无法消除小数部分,根据精度不同会截取对应有效数字,所以小数的二进制有时候是不能精确的,就和我们十进制里不能准确表示1/3=0.33333333…是一个道理。

这种情况在计算时会造成了精度丢失,也就是舍入误差,对于金额计算会产生严重的后果。


2、解决方法

2.1、Java中使用 BigDecimal 类

我们先看下Java里面的BigDecimal类,构造方法如下

在这里插入图片描述

在这里插入图片描述

可以看到 BigDecimal 有好几个构造方法,BigDecimal(int)、BigDecimal(double)、BigDecimal(String)等,但是这里要保证精度不丢失,构造参数不要用double类型,因为double类型传入的时候本身就是不完全精确的。如下:

BigDecimal bd1=new BigDecimal("0.3");
BigDecimal bd2=new BigDecimal("3");
BigDecimal bd3=new BigDecimal(3);
System.out.println(bd1.multiply(bd2));
System.out.println(bd1.multiply(bd3));

在这里插入图片描述


扩展:在金融领域,也可以使用一些第三方库,例如

<dependency>
    <groupId>org.javamoney</groupId>
    <artifactId>moneta</artifactId>
    <version>1.1</version>
</dependency>

里面的Money类对金额做了显性的抽象,增加了金额的单位,避免了直接使用 BigDecimal 踩一些坑。


2.2、JavaScript 中解决计算精度丢失的问题

解决方法:decimal.js

decimal.js为 JavaScript 提供十进制类型的任意精度数值,是使用的二进制来计算的,所以能解决js的精度问题。

官网:http://mikemcl.github.io/decimal.js
GitHub地址:https://github.com/MikeMcl/decimal.js

用法如下

<!DOCTYPE html>
<html>
	<head>
		<meta charset="utf-8">
		<title></title>
	</head>
	<body>
		<script src='js/decimal.js'></script>
		<script type="text/javascript">
			var a=0.3*3;
			console.log(a);
			var b=0.1+0.2;
			console.log(b);
			
			//使用decimal
			let a1 = new Decimal(0.3).mul(new Decimal(3));
			console.log(a1.toNumber());
			let b2 = new Decimal(0.1).add(new Decimal(0.2));
			console.log(b2.toNumber());
		</script>
	</body>
</html>

结果如下:

在这里插入图片描述

// 加法
let c = new Decimal(a).add(new Decimal(b));
// 减法
let d = new Decimal(a).sub(new Decimal(b));
// 乘法
let e = new Decimal(a).mul(new Decimal(b));
// 除法
let f = new Decimal(a).div(new Decimal(b));

3、使用建议

针对浮点数值存储和计算,大佬超给大家简单罗列了以下几点看法,欢迎补充。

  • 禁止通过判断两个浮点数是否相等来控制某些业务流程:在比较浮点数时,由于存在误差,往往会出现意料之外的结果。

  • 整型存储其最小单位的值:在要求绝对精确表示的业务场景下,比如金融行业的货币表示,展示时可以转换成该货币的常用单位,比如人民币使用分存储,美元使用美分存储。

  • 数组保存小数部分的数据:在要求精确表示小数点位的业务场景下,比如圆周率要求存储小数点后 1000 位数字,使用单精度和双精度浮点数类型保存是难以做到的。

  • 数据库中保存小数时,推荐使用 decimal 类型



更多技术干货,请持续关注程序员大佬超。
原创不易,转载请注明出处。

猜你喜欢

转载自blog.csdn.net/xch_yang/article/details/129087073