为什么Float比较不要用==

Float类型比较不要用==

引出问题

在一个风和日丽的大雨天,一个辛勤劳作的码农正在刨着前辈留下的一座大山,debug了一个下午决定记录下这个不起眼小bug。

大致代码是这样的 :

//一大堆东西
//一大堆东西
//一大堆东西
if (point.getX()==linePoint.getX() && point.getY()==linePoint.getY()) {
    //一大堆东西
	//一大堆东西
}
//一大堆东西
//一大堆东西

反正事就是这么个事,并且IDEA也提示了要用.equals(),不要用==

但是比较还要靠这个bug摸一下午🐟,于是有了这篇文章。。。

Float比较不能用==

下面有两个Float类型的变量ab,并且值都为1.0f,不看答案的前提下大家不妨先来猜猜结果是多少?

Float a = 1.0f;
Float b = 1.0f;
System.out.println(a == b);
System.out.println(a.equals(b));

揭晓答案:

当我们使用==来对两个Float类型进行比较是打印的结果是false;而使用.equals()比较时打印的结果才是我们期望的true

image-20230627202651872.png

开个小差:

其实即使我们不运行程序,IDEA也会自动提醒我们注意!

image-20230627203010983.png

当我们把鼠标放到黄色块(我的是波浪线是因为我的主题的缘故)时,IDE就会提示我们Replace '==' with 'equals()',鼠标单击即可自动转换。

上方翻译:使用’==‘比较数字对象,而不是’equals()’

原因

在Java中,Float类型是一个32位的浮点数,由符号位、指数位和尾数位组成。由于浮点数的存储和运算方式与整数不同,因此在使用==时可能会存在以下爱几种问题:

精度问题

在计算机系统理论中,浮点数采用 IEEE 754 标准表示,编码方式是符号+阶码+尾数

image-20230627202030107.png

比如 float 类型占用 32 位,单精度浮点表示法:

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

从这里可以看出,指数位决定了大小范围,小数位决定了计算精度。当十进制数值转换为二进制科学表达式后,得到的尾数位数是有可能很长甚至是无限长。所以当使用浮点格式来存储数字的时候,实际存储的尾数是被截取或执行舍入后的近似值。这就解释了浮点数计算不准确的问题,因为近似值和原值是有差异的,导致使用==比较时返回false。

NaN问题

NaN(Not a Number)是一个特殊的浮点数值,表示一个不确定或非法的计算结果。当两个浮点数中至少有一个为NaN时,使用==比较时返回false,即使它们都是NaN。

例如,下面的代码段输出的结果为false:

Float a = Float.NaN;
Float b = Float.NaN;
System.out.println(a == b);

无穷大问题

浮点数可以表示正无穷大、负无穷大和无穷小。当两个浮点数中至少有一个为无穷大时,使用==比较时返回false,即使它们都是无穷大。

例如,下面的代码段输出的结果为false:

Float a = Float.POSITIVE_INFINITY;
Float b = Float.POSITIVE_INFINITY;
System.out.println(a == b); // false

其中,Float.POSITIVE_INFINITY是一个常量,表示正无穷大,即一个大于任何有限浮点数的特殊值。

源码:

/**
 * A constant holding the positive infinity of type
 * {@code float}. It is equal to the value returned by
 * {@code Float.intBitsToFloat(0x7f800000)}.
 */
public static final float POSITIVE_INFINITY = 1.0f / 0.0f;

为什么可以用equals

Float类是Java中的一个包装类,用于封装基本数据类型float的值。由于Float类继承自Object类,因此它继承了Object类中的equals方法。在Float类中,equals方法被重写,用于比较两个Float对象的值是否相等。

至于为什么equals方法可以判断Float是否相等,我们就得到源码中找一下答案了。

Float类equals方法源码如下👇:

image-20230627205659362.png

==翻译:==

将此对象与指定的对象进行比较。当且仅当参数不为null并且是一个Float对象时,结果为true,该对象表示的浮点值与此对象表示的浮动值相同。为此,两个浮点值被认为是相同的,当且仅当方法floatToIntBits(float)在应用于每个浮点值时返回相同的int值。

注意,在大多数情况下,对于Float类的两个实例f1和f2,f1.equals(f2)的值为true当且仅当 f1.foatValue()==f2.foatvalue() 也具有true值。

但是,有两个例外:

  • 如果f1和f2都表示Float.NaN,那么equals方法返回true,即使Float.NaN==Float.Nn的值为false。

  • 如果f1表示+0.0f,而f2表示-0.0f,或者反之亦然,则相等测试的值为假,即使0.0f==-0.0f的值为真。

此定义允许哈希表正常运行

从源码可见,方法传入了一个Object obj,首先(obj instanceof Float)判断了传入参数的类型是否是Float类型,如果不是则直接返回false。之后调用floatToIntBits方法判断两个对象的value值是否相等。

继续往下floatToIntBits方法:

image-20230627210309639.png

==翻译:==

根据IEEE 754浮点“单一格式”位布局,返回指定浮点值的表示形式。

位31(掩码0x80000000选择的位)表示浮点数的符号。位30-23(由掩码0x7f800000选择的位)表示指数。

位22-0(掩码0x007fffff选择的位)表示浮点数的有效位(有时称为尾数)。

如果参数为正无穷大,则结果为0x7f800000。

如果参数为负无穷大,则结果为0xff800000。

如果参数为NaN,则结果为0x7fc00000。

在所有情况下,结果都是一个整数,当赋予intBitsToFloat(int)方法时,它将产生一个与floatToIntBits的参数相同的浮点值(除了所有NaN值都折叠为一个“规范”NaN值)

根据翻译可以看到根据IEEE 754浮点“单一格式”位布局,返回指定浮点值的表示形式。看起来这段描述很复杂,其实就是就是将一个 Float 类型的值转换为对应的 int 类型的二进制表示,而不进行舍入或者格式化。最初我以为代码只是单纯的将十进制数转为二进制,于是我传入一个Float a=1.0f,debug观察返回的result。

image-20230627210932362.png

结果很显然,传入了1.0f,但是返回的是result(slot_1): 1065353216。于是我陷入了沉思,决定看看这个floatToRawIntBits(value)到底是何方妖孽。

源码👇:

public static native int floatToRawIntBits(float value);

然鹅,这个方法被native关键字修饰,也就是说我们无法再Ctrl + 左键继续往下查看源码。

在我一下午的努力下,功夫不负有心人终于在GitHub中的openjdk中找到了这个方法的出处:jdk/src/java.base/share/native/libjava/Float.c at master · openjdk/jdk · GitHub

👇只截取了关键性代码,完整代码可以到👆的网址查看

/*
 * Find the bit pattern corresponding to a given float, NOT collapsing NaNs
 */
JNIEXPORT jint JNICALL
Java_java_lang_Float_floatToRawIntBits(JNIEnv *env, jclass unused, jfloat v)
{
    union {
        int i;
        float f;
    } u;
    u.f = (float)v;
    return (jint)u.i;
}

要了解这个方法的作用就得先搞懂这段代码是什么意思❗❗❗

一行一行往下看:


/*
 * Find the bit pattern corresponding to a given float, NOT collapsing NaNs
 */

这是一个注释,说明了这个函数的作用是找到给定float类型值的二进制位模式,不会将NaN值折叠成一个特定的值。


JNIEXPORT jint JNICALL
Java_java_lang_Float_floatToRawIntBits(JNIEnv *env, jclass unused, jfloat v)

这是一个JNI函数的声明,用于将Java中的float类型值转换为其对应的二进制位模式。其中,JNIEXPORT和JNICALL是JNI的关键字,用于告诉编译器这是一个JNI函数。JNIEnv是一个指向JNI环境的指针,jclass是Java类的指针,jfloat是Java中的float类型。


{
  union {
    int i;
    float f;
  } u;

这是一个联合体,用于将int类型和float类型共用同一块内存空间。联合体中定义了两个成员变量,一个是int类型的i,一个是float类型的f。


u.f = (float)v;

将传入的Java float类型值v赋值给联合体中的float类型成员变量f。


return (jint)u.i;

将联合体中的int类型成员变量i强制转换为jint类型,并作为函数返回值。由于联合体中的int类型成员变量和float类型成员变量共用同一块内存空间,因此返回的int类型值就是传入的float类型值的二进制位模式。


也就是说传入floatToRawIntBits()方法中返回的其实是C/C++有符号32位整数所以才会是这么长的一串整数。

即使看不懂也无所谓,只需要知道这个方法返回的是一个长长的整数,继续往下

image-20230627214307381.png

在获取到result之后做了这样一个判断

if ( ((result & FloatConsts.EXP_BIT_MASK) ==
      FloatConsts.EXP_BIT_MASK) &&
     (result & FloatConsts.SIGNIF_BIT_MASK) != 0)

这段代码是在检查result值的指数部分是否全部为1,且尾数部分不为0。如果满足条件,则表示这个浮点数为特殊值0x7fc00000,即NaN(非数字)。其中,FloatConsts.EXP_BIT_MASKFloatConsts.SIGNIF_BIT_MASK是Java中定义的常量,分别表示浮点数的指数部分和尾数部分的掩码(mask)。


综上,笔者认为equals其实在三个方面确保数据比较时能够准确:

  • 判断比较的对象类型是否属于Float(obj instanceof Float)
  • 确保不是NaN
  • 在精度方面,则是比较两个32位整数的值(floatToIntBits(((Float)obj).value) == floatToIntBits(value))
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值