为什么Float比较不要用==
Float类型比较不要用==
引出问题
在一个风和日丽的大雨天,一个辛勤劳作的码农正在刨着前辈留下的一座大山,debug了一个下午决定记录下这个不起眼小bug。
大致代码是这样的 :
//一大堆东西
//一大堆东西
//一大堆东西
if (point.getX()==linePoint.getX() && point.getY()==linePoint.getY()) {
//一大堆东西
//一大堆东西
}
//一大堆东西
//一大堆东西
反正事就是这么个事,并且IDEA也提示了要用.equals()
,不要用==
。
但是比较还要靠这个bug摸一下午🐟,于是有了这篇文章。。。
Float比较不能用==
下面有两个Float类型的变量a
、b
,并且值都为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
。
开个小差:
其实即使我们不运行程序,IDEA也会自动提醒我们注意!
当我们把鼠标放到黄色块(我的是波浪线是因为我的主题的缘故)时,IDE就会提示我们
Replace '==' with 'equals()'
,鼠标单击即可自动转换。上方翻译:使用’==‘比较数字对象,而不是’equals()’
原因
在Java中,Float类型是一个32位的浮点数,由符号位、指数位和尾数位组成。由于浮点数的存储和运算方式与整数不同,因此在使用==
时可能会存在以下爱几种问题:
精度问题
在计算机系统理论中,浮点数采用 IEEE 754 标准表示,编码方式是符号+阶码+尾数
比如 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方法源码如下👇:
==翻译:==
将此对象与指定的对象进行比较。当且仅当参数不为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
方法:
==翻译:==
根据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。
结果很显然,传入了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位整数所以才会是这么长的一串整数。
即使看不懂也无所谓,只需要知道这个方法返回的是一个长长的整数,继续往下
在获取到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_MASK
和FloatConsts.SIGNIF_BIT_MASK
是Java中定义的常量,分别表示浮点数的指数部分和尾数部分的掩码(mask)。
综上,笔者认为equals其实在三个方面确保数据比较时能够准确:
- 判断比较的对象类型是否属于Float(obj instanceof Float)
- 确保不是NaN
- 在精度方面,则是比较两个32位整数的值(floatToIntBits(((Float)obj).value) == floatToIntBits(value))