长点心吧!别再用"=="比较浮点数了

共 4173字,需浏览 9分钟

 ·

2021-03-20 12:40


超全面!Java核心知识总结(点击查看)

超全面!Java核心知识总结(点击查看)


大家好,我是胖虎。前几天群内有个小伙伴去面试,在做笔试题的时候在这里掉坑里了。

float a1 = 0.1f;
double a2 = 0.1;
System.out.println((a1 - a2) == 0.0);

float b1 = 0.125f;
double b2 = 0.125;
System.out.println((b1 - b2) == 0.0);

double c1 = 0.625;
double c2 = 0.5;
double c3 = 0.375;
System.out.println((c1 - c2) == (c2 - c3));

double d1 = 0.8;
double d2 = 0.6;
double d3 = 0.4;
System.out.println((d1 - d2) == (d2 - d3));

大家可以先考虑一下最终的输出结果是怎么样的?


正确的答案为

false
true
true
false


这是因为在处理浮点数的时候如果操作不当很容易造成精度丢失,我们可以模拟一个发红包的场景。

一个红包中有0.5元,4个人每人从里面拿走了0.1元。

不要问我为啥钱这么少,因为我...

用代码来输出还剩多少钱。。。

double d1 = .5;
for (int i = 0; i < 4; i++) {
 d1 -= .1;
}
// 输出余额
System.out.println(d1);

非常简单的一个减法运算,但是执行之后却发现,输出的结果并不是0.1,而是0.10000000000000003

这就是我们常见的精度丢失问题,并不是Java语言才有这种的问题,很多语言都会遇到这样的问题。这是因为他们都遵循了IEEE 754标准,即:IEEE二进制浮点数算术标准(IEEE 754)

编码方式是符号+阶码+尾数,如图:

img

比如 float 类型来具体,我们都知道它占用 32 位,单精度浮点表示法如下:

  • 符号位(sign)占用 1 位,用来表示正负数,0 表示正数,1 表示负数
  • 指数位(exponent)占用 8 位,用来表示指数,实际要加上偏移量
  • 小数位(fraction)占用 23 位,用来表示小数,不足位数补 0

由此可以看出,指数位决定了代销范围,小数位决定了它的精度。当在计算机中进行计算时,十进制转化为二进制表达式后,得到的尾数会很长很长。

而实际显示的尾数十倍截取或执行舍入后的近似值。这样就解释清楚了浮点数计算不准确的问题。

同理由于上述原因的存在,在对浮点数进行比较的时候非常不建议使用 ==,因为精度问题,往往会返回不符合与其的结果。当然也不能使用包装类型的equals

double d1 = .1 * 3;
double d2 = .3;
System.out.println(d1 == d2);

上面的例子中,按照正常的逻辑来说.1 * 3 结果是 0.3 。所以最终的结果应该是True

然而执行之后结果确实False,d1最终的值并不是 0.3 而是0.30000000000000004

问题的原因我们已经知道了,那遇到浮点数的时候该如何正确的进行比较呢?

常用的方法有两种

1、规定误差区间

虽然我们无法做到精确的比较,但是我们可以确定一个误差范围,只要小于这个误差范围,我们就可以它们是相等的。

final double threshold = .00001;
double d1 = .1 * 3;
double d2 = .3;
if (Math.abs(d1 - d2) < threshold) {
    System.out.println("d1 = d2");
else {
    System.out.println("d1 != d2");
}

通过设定一个阈值threshold,然后对两个变量求绝对值,如果差值小于这个阈值,则默认相等.

2、使用BigDecimal的compareTo()

BigDecimal是在java.math中的一个API,用来对超过16位有效位的数字进行精确运算的类。在涉及到交易等商业运算中建议使用BigDecimal.

当使用它进行运算时,传统的 +-*/ 是不能使用的,需要使用其对应的方法

BigDecimal b1 = new BigDecimal("0.1");
BigDecimal b2 = new BigDecimal("2");
b1.add(b2); // b1 + b2 
b1.subtract(b2);    // b1 - b2
b1.multiply(b2);    // b1 * b2
b1.divide(b2);  // b1 ÷ b2

另外,在使用DigDecimal创建对象的时候,需要注意的是

使用的参数不要使用构造参数为double的,要使用string的构造方法!!!

使用的参数不要使用构造参数为double的,要使用string的构造方法!!!

使用的参数不要使用构造参数为double的,要使用string的构造方法!!!

使用string的构造方法,是为了防止精度丢失。

在进行比较的时候,刻意使用Decimal的compareTo()方法或者 equals方法。

在使用a.compareTo(b)的时候,返回结果如下

  • a < b :返回-1
  • a = b :返回0
  • a > b :返回1

还是最开始的例子,改写成BigDeciaml的方式

import java.math.BigDecimal;

BigDecimal b1 = new BigDecimal("0.1");
b1 = b1.multiply(new BigDecimal("3"));

BigDecimal b2 = new BigDecimal("0.3");
System.out.println(b1.compareTo(b2));
System.out.println(b1.equals(b2));

执行之后,均返回True

还有一点值得注意的,BigDecimal.equals()进行比较的时候会考虑位数,比如

BigDecimal b1 = new BigDecimal("0.10");
BigDecimal b2 = new BigDecimal("0.1");

b1的值为0.10,按照正常逻辑应该与b2的0.1是相等的,但是运行发现

BigDecimal b1 = new BigDecimal("0.10");
BigDecimal b2 = new BigDecimal("0.1");
System.out.println(b1.compareTo(b2)); // 0
System.out.println(b1.equals(b2)); // false

因此,相比于equalscompareTo更严谨一点。


写在最后,相信很多同学在这个地方踩过坑,所以在日常写代码的时候如果需要对浮点数(double/float)进行判断时,千万不要使用== 进行判断,为了避免精度和位数问题,建议使用BigDecimal来代替。


如有文章对你有帮助,

在看”和转发是对我最大的支持!


推荐 GitHub 书籍仓库
https://github.com/ebooklist/awesome-ebooks-list

整理了大部分常用 技术书籍PDF,持续更新中... 你需要的技术书籍,这里可能都有...


点击文末“阅读原文”可直达

整理不易,麻烦各位小伙伴在GitHub中来个Star支持一下

浏览 27
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐