首页 > 其他分享 >一个 hashCode() 函数引发的​「​惨案」

一个 hashCode() 函数引发的​「​惨案」

时间:2023-10-12 12:01:57浏览次数:30  
标签:函数 int 0.0 Float hashCode 惨案 points println


1、起因

让我关注到这一点的起因是一道题:牛客网上的max-points-on-a-line(答题:https://www.nowcoder.com/practice/bfc691e0100441cdb8ec153f32540be2)

题目是这么描述的:

Given n points on a 2D plane, find the maximum number of points that lie on the same straight line.

大意就是给我一些点的X,Y坐标,找到过这些点最多的直线,输出这条线上的点数量。

于是我就敲出了以下的代码:

public class Solution {
    public int maxPoints(Point[] points) {
        if (points.length <= 2) {
            return points.length;
        }

        int max = 2;
        for (int i = 0; i < points.length - 1; i++) {
            Map<Float, Integer> map = new HashMap<>(16);
            // 记录垂直点数; 当前和Points[i]在一条线上的最大点数; 和Points[i]垂直的点数
            int ver = 0, cur, dup = 0;
            for (int j = i + 1; j < points.length; j++) {
                if (points[j].x == points[i].x) {
                    if (points[j].y != points[i].y) {
                        ver++;
                    } else {
                        dup++;
                    }
                } else {
                    float d = (float)((points[j].y - points[i].y) / (double) (points[j].x - points[i].x));
                    map.put(d, map.get(d) == null ? 1 : map.get(d) + 1);
                }
            }

            cur = ver;
            for (int v : map.values()) {
                cur = Math.max(v, cur);
            }

            max = Math.max(max, cur + dup + 1);
        }
        return max;
    }
}

这段代码在天真的我看来是没啥问题的,可就是没办法过,经过长久的排查错误,我写了以下代码加在上面的代码里运行,另外,关注公号“终码一生”,回复关键词“资料”,获取视频教程和最新的面试资料!

public static void main(String[] args) {
    int[][] vals = {{2,3},{3,3},{-5,3}};
    Point[] points = new Point[3];

    for (int i=0; i<vals.length; i++){
        points[i] = new Point(vals[i][0], vals[i][1]);
    }

    Solution solution = new Solution();

    System.out.println(solution.maxPoints(points));
}

它输出的,竟然是2

也就是说,它认为(3-3) / (3-2) 和 (3-3) / (-5-2) 不同? 什么鬼…

经过debug,发现上述结果分别是0.0和-0.0

0.0 难道不等于 -0.0 ?

这时我心里已经一阵卧槽了,不过我还是写了验证代码:

System.out.println(0.0 == -0.0);

结果是True,没问题啊,我凌乱了……

一定是java底层代码错了! 我没错……

又是一阵debug,我找到了这条语句:

map.put(d, map.get(d) == null ? 1 : map.get(d) + 1);

我觉得map.get()很有问题, 它的源代码是这样的:

public V get(Object key) {
    Node<K,V> e;
    return (e = getNode(hash(key), key)) == null ? null : e.value;
}

唔,先获得hash()是吧,那我找到了它的hash函数:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

再来,这里是要比较h 和key的hashCode是吧,那我们去看hashCode()函数

public native int hashCode();

这是一个本地方法,看不到源码了,那我就使用它看看吧,测试一下不就好了吗,我写了以下的测试代码:

public static void main(String[] args) {
    System.out.println(0.0 == -0.0);
    System.out.println(new Float(0.0).hashCode() ==
        new Float(-0.0).hashCode());
}

结果竟然是True和False !!!

这个源头终于找到了, 0.0 和 -0.0 的hashCode值是不同的 !

经过一番修改,我通过了这道题(其实精度也会有问题,应该使用BigDecimal的,不过牛客网要求没那么高。后来我想了想只有把直线方程写成Ax+By+C=0的形式才能完全避免精度问题),另外,关注公号“终码一生”,回复关键词“资料”,获取视频教程和最新的面试资料!

接下来,探讨下实数的hashCode()函数是个啥情况!

2、实数的hashCode()

  • 在程序执行期间,只要equals方法的比较操作用到的信息没有被修改,那么对这同一个对象调用多次,hashCode方法必须始终如一地返回同一个整数。
  • 如果两个对象根据equals方法比较是相等的,那么调用两个对象的hashCode方法必须返回相同的整数结果。
  • 如果两个对象根据equals方法比较是不等的,则hashCode方法不一定得返回不同的整数。

——《effective java》

那么我们来看看,0.0和-0.0调用equals方法是否相等:

System.out.println(new Float(0.0).equals(0.0f));
System.out.println(new Float(0.0).equals((float) -0.0));

输出是True 和 False

好吧,二者调用equals() 方法不相等,看来是满足了书里说的逻辑的。

那我们看看Float底层equals函数咋写的:

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

哦,原来是把Float转换成Bits的时候发生了点奇妙的事,于是我找到了一切的源头:

/**
 * Returns a representation of the specified floating-point value
 * according to the IEEE 754 floating-point "single format" bit
 * layout.
 *
 * <p>Bit 31 (the bit that is selected by the mask
 * {@code 0x80000000}) represents the sign of the floating-point
 * number.
 * Bits 30-23 (the bits that are selected by the mask
 * {@code 0x7f800000}) represent the exponent.
 * Bits 22-0 (the bits that are selected by the mask
 * {@code 0x007fffff}) represent the significand (sometimes called
 * the mantissa) of the floating-point number.
 *
 * <p>If the argument is positive infinity, the result is
 * {@code 0x7f800000}.
 *
 * <p>If the argument is negative infinity, the result is
 * {@code 0xff800000}.
 *
 * <p>If the argument is NaN, the result is {@code 0x7fc00000}.
 *
 * <p>In all cases, the result is an integer that, when given to the
 * {@link #intBitsToFloat(int)} method, will produce a floating-point
 * value the same as the argument to {@code floatToIntBits}
 * (except all NaN values are collapsed to a single
 * "canonical" NaN value).
 *
 * @param   value a floating-point number.
 * @return the bits that represent the floating-point number.
 */
public static int floatToIntBits(float value) {
    int result = floatToRawIntBits(value);
    // Check for NaN based on values of bit fields, maximum
    // exponent and nonzero significand.
    if (((result & FloatConsts.EXP_BIT_MASK) ==
          FloatConsts.EXP_BIT_MASK) &&
         (result & FloatConsts.SIGNIF_BIT_MASK) != 0)
        result = 0x7fc00000;
    return result;
}

这文档挺长的,也查了其它资料,看了半天终于搞懂了

就是说Java浮点数的语义一般遵循IEEE 754二进制浮点算术标准。IEEE 754标准提供了浮点无穷,负无穷,负零和NaN(非数字)的定义。在使用Java过程中,一些特殊的浮点数通常会让大家很迷惑

里面提到,当浮点运算产生一个非常接近0的负浮点数时,会产生“-0.0”,而这个浮点数不能正常表示

我们可以输出一波0.0和-0.0的数据:

System.out.println(Float.floatToIntBits((float) 0.0));
System.out.println(Float.floatToIntBits((float) -0.0));
System.out.println(Float.floatToRawIntBits(0.0f));
System.out.println(Float.floatToRawIntBits((float)-0.0));

结果:

0
-2147483648
0
-2147483648

就是说,存储-0.0, 竟然用的是0x80000000

这也是我们熟悉的Integer.MIN_VALUE

PS:防止找不到本篇文章,可以收藏点赞,方便翻阅查找哦。

标签:函数,int,0.0,Float,hashCode,惨案,points,println
From: https://blog.51cto.com/zhongmayisheng/7825592

相关文章

  • 创建一个带有重试机制的请求函数,用于避免请求受限或失败时重新尝试请求。
    /***创建一个带有重试机制的请求函数,用于避免请求受限或失败时重新尝试请求。*@param{function}func-要执行的请求函数。*@param{number}maxCount-最大重试次数,默认为10。*@param{number}time-重试间隔时间(毫秒),默认为1500毫秒。*@returns{object}......
  • vue2常见选项和生命周期钩子函数
    Vue2提供了一些其他选项和钩子函数,以支持更高级的组件功能和配置,这些包括:data:data选项用于定义组件的响应式数据。这些数据将被Vue追踪,以便在数据发生变化时更新视图。data(){return{message:'Hello,Vue!'}}methods:methods选项用于定义组件的方法,这些方法......
  • LabWindows/CVI Scan( )函数
    背景介绍Scan()可以将字符串按照用户formatString格式说明分解成多个组件。最多可以分解29个组件。Scan()很强大且复杂,使用起来容易出错,但它却被频繁使用。Scan()函数函数头文件:#include<formatio.h>函数原型:intScan(void*Source,charFormat_String[],...);将字符......
  • 只有三行代码的神奇云函数的功能之三:100%成功获取unionid [纯转]
    微信小程序这是一个神奇的网站,哦不,神奇的云函数,它只有三行代码:(真的只有三行哦)云函数:loginindex.js:constcloud=require('wx-server-sdk')cloud.init()exports.main=async(event)=>{return{...event,...cloud.getWXContext()}} 神奇功能之三:100%成功获取unio......
  • python 基础笔记-函数
    函数是组织好的,可重复使用的,用来实现单一,或相关联功能的代码段·。   好处为: 一可以把程序中相对独立的功能模块抽取出来,减少重读代码的编写; 二是将来可以以重复的使用这些功能模块https://www.clw9335.com/zx/index-htm-page-5.html  定义一个函数 你可以定义一......
  • C语言函数和指针的关系之二(未完)
    指针作为函数的返回值一个函数可以返回整型数据、字符数据、浮点型的数据,也可以返回一个指针.例30:char*fun(){charstr[100]="helloworld";returnstr;}intmain(){char*p;p=fun();printf("%s\n",p);//}//总结:返回地址的时候,地址指向的内存的内容不能释放如果返回......
  • cpp中函数参数的默认值
    title:aliases:tags:-cpp/函数category:-方法stars:url:creation-time:2023-10-0919:24modification-time:2023-10-1014:20:19[[Cpp]]函数的默认值写法:voidDemo(intx,inty=1;intz=2);由于cpp中函数可能存在声明和定义,如果同时在声明和定......
  • 莫比乌斯函数及反演学习笔记
    前置知识\(1.\)艾佛森括号:\([P]=\begin{cases}1&\mathtt{(if\P\is\true)}\\0&\mathtt{(otherwise)}\end{cases}\)\(2.\)\(a\midb\)表示\(a\)是\(b\)的因子\(3.\)整除分块:\(\displaystyle\sum_{i=1}^n\lfloor\dfrac{N}{i}\rfloor\......
  • sql server导入表的一些函数使用
    truncatetableJC_BMDA;insertintoJC_BMDA(bh,mc,qdmc,pym,ty)selectright('0'+rtrim(convert(varchar(5),id)),2)bh,bz,'jnx',upper(dbo.fGG_GetPY(bz)),0from(selectROW_NUMBER()over(orderbyxh)asid,*from(selectconvert(int,......
  • C++ - move()函数
    C++11标准中借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数。注意,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化......