性能问答>Java中的浮点数到底应该怎么比较,才算是最优解?你真的知其然,知其所以然了吗?>
1回复
2年前

Java中的浮点数到底应该怎么比较,才算是最优解?你真的知其然,知其所以然了吗?



《阿里巴巴Java开发手册(泰山版).pdf》里面说:“浮点数之间的等值判断,基本数据类型不能用==来比较,包装数据类型不能用equals来判断。说明:浮点数采用“尾数+阶码”的编码方式,类似于科学计数法的“有效数字+指数”的表示方式。二进制无法精确表示大部分的十进制小数,具体原理参考《码出高效》。”

反例: 
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
if (a == b) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上a==b的结果为false
}
Float x = Float.valueOf(a);
Float y = Float.valueOf(b);
if (x.equals(y)) {
// 预期进入此代码快,执行其它业务逻辑
// 但事实上equals的结果为false
}
正例: 
(1) 指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。 float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
float diff = 1e-6f;
if (Math.abs(a - b) < diff) {
System.out.println("true");
} 
(2) 使用BigDecimal来定义值,再进行浮点数的运算操作。
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)) {
System.out.println("true");
}

我的疑问如下:
1、《阿里巴巴Java开发手册(泰山版).pdf》这个手册里面说的情况或者场景是浮点数基本类型(float,double)在参与运算之后的结果不能用==来比较,而是应该指定一个误差范围。这个我能理解,因为我知道二进制无法精确表示大部分的十进制小数,就比如float af = 1.0f - 0.9f;这个计算,我预期结果是0.1,但是实际结果是0.100000024。所以你不能直接拿af跟0.1用==比较,结果肯定是false。但是,如果俩个浮点数基本类型(float,double)是常量,不参与运算应该没关系吧,假如我把常量传到方法参数里面去比较应该没有关系吧?我认为常量浮点数基本类型(float,double)是可以直接用==比较的。
2, 为啥不能用equals啊?下面是Float类里面的equals源码

public boolean equals(Object obj) {
   return (obj instanceof Float)
               && (floatToIntBits(((Float)obj).value) == floatToIntBits(value));
}

可以看到Float类里面的equals源码最终用的也是==来比较的,只不过是先把Float的value小数值转换成二进制位(bit)再去比较的。写到这里我好像都明白了,不管用==去比较还是用equals去比较,==和equals比较的都是精确的值,注意虽然比较的是精确的值,但是却是不符合我们预期的值。比如上面阿里巴巴手册上面举的俩个例子,这个俩个例子我们想要的是预期结果,但是==和equals比较的都是精确的值,所以==和equals返回的结果都不是我们想要的。再看一遍这个例子

float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
if (a == b) {
// 我们预期进入此代码快,执行其它业务逻辑
// 但事实上a==b的结果为false,为什么是false呢?因为==和equals比较的都是精确的值,a的值是实际上是0.100000024,b的值实际上是0.099999964,所以a == b肯定是false
}

写到这里,我又有一个疑问,既然==和equals比较的都是精确的值,为啥Float类里面的equals源码不直接用==去比较啊?为啥多此一举把Float的value小数值转换成二进制位(bit)再去比较呢?这个似乎只能去问写这个源码的人是怎么想的了。
3、包装数据类型不能用equals来判断,那可以用compareTo方法判断俩个浮点数是否相等吗?我发现Float.compareTo方法里面用的是Float.floatToIntBits(f2)这方法,这个方法靠谱吗?
微信截图_20210421202414.png
Float.floatToIntBits(f1)
微信截图_20210421202428.png
Float.floatToIntBits方法源码,如下截图
微信截图_20210421202443.png
floatToIntBits方法里面调用的是floatToRawIntBits方法,floatToRawIntBits方法是native方法
微信截图_20210421202456.png
写到这里我又自己明白了,如果你仔细观察,你会发现Float类里面的equals方法跟Float.compareTo方法用的是同一个套路,他们都是把Float的value小数值转换成二进制位(bit)再去比较的,Float类里面的equals方法跟Float.compareTo方法背后都是调用的floatToIntBits方法。所以说Float类里面的equals方法跟Float.compareTo方法比较的都是精确的值,不是你预期的值,哈哈。
Float.compareTo方法的关键代码,如下截图:
微信截图_20210421203100.png
Float.equals方法的关键代码,如下截图:
微信截图_20210421203259.png
4、阿里巴巴也没说应该用什么比较,难道必须用BigDecimal吗?
我自己回答我自己,阿里巴巴当然说了应该用什么比较。答案就是,如果你想让参与运算后的浮点数基本类型的比较结果符合你自己的预期,注意是符合你的预期,而不是精确比较俩个浮点数基本类型的值。你应该指定一个误差范围,两个浮点数的差值在此范围之内,则认为是相等的。或者直接使用BigDecimal来进行精确运算,使用精确运算,那运算结果肯定就符合你的预期了。除此之外别无他法。
但是,BigDecimal使用也是有技巧的哦。注意人家阿里巴巴推荐的使用方式是这样的

//注意BigDecimal类的构造方法,是通过字符串把浮点数传进去的哦
BigDecimal a = new BigDecimal("1.0");
BigDecimal b = new BigDecimal("0.9");
BigDecimal c = new BigDecimal("0.8");
BigDecimal x = a.subtract(b);
BigDecimal y = b.subtract(c);
if (x.equals(y)) {
System.out.println("true");
}

不是这样的哦,

//注意BigDecimal类的构造方法,禁止使用BigDecimal(double)这样的方式哦,
BigDecimal a = new BigDecimal(1.0);

至于为什么禁止使用BigDecimal(double)这样的方式,你可以试一下,下面这行代码

BigDecimal b2 = new BigDecimal(0.1);//严重不推荐使用这种方式
System.out.println("严重不推荐使用这种方式,因为double类型在存储类型时实际上存储的是一个近似值,而BigDecimal存储的是精确值:" + b2.toString());
输出结果是:0.1000000000000000055511151231257827021181583404541015625

看到这里你或许内心很着急内心在想:“什么他妈的,我要计算的是变量, new BigDecimal(“1.0”); 这个构造方法根本他妈的用不了,这他妈都是写死的数字,我要计算的是变量,你这怎么用?”。别着急,可以用下面这两种中办法解决你的疑虑:

BigDecimal b3 = new BigDecimal(0.1 + "");//第一种解决办法
System.out.println("解决办法就是先将double转换成String,再用BigDecimal就可以了" + b3.toString());
System.out.println("还有另外的解决方法就是直接使用BigDecimal的valueOf方法");
BigDecimal b4 = BigDecimal.valueOf(0.1);//第二种解决办法

可以看下BigDecimal.valueOf方法的源码,如下截图
微信截图_20210421205150.png

你们看这篇文章可能看不懂,不过看不懂没关系,因为这不属于你的经验,不属于你的思考,不属于你的亲身经历。我本来都没有打算写这篇文章,我只不过是在review我同事代码的时候,感觉他比较俩个Double类型的代码似乎有问题,就把阿里巴巴手册拿出来看了一下,看完我把我自己的疑问一个一个列出来了,本来打算在PerfMa社区的求助板块把我的疑问提出来呢,结果我在写问题的时候,我自己搞明白了。我本来打算把我的疑问写的尽可能清楚一点,最好加一点代码例子,让大神看到我的问题的时候能很清楚的明白我的疑问,尽快帮我解决疑问。但是我把问题写的越清楚,我自己反而越明白问题的本质了,到最后我自己解决我自己的疑问了。或许,这就是思考的过程吧。只看文章,不思考,看的再多都没用。尽信书,则不如无书。读书万卷不如行一里路。大家自己看着办吧。看别人的文章,一定要总结,一定要自己去敲代码做实验。鼓励大家把你看过的文章都总结一下,写成一篇文章发表出来,哪怕是把别人文章你觉得有用的地方复制过来,再加点你自己的理解也行,发表成文章对你也有帮助。

2890 阅读
请先登录,查看1条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步