暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

关于hashCode和equals的几个问题讨论

Coding的哔哔叨叨 2020-12-10
668

为什么重写hashCode()equals()方法?

Coding的哔哔叨叨


先奉上结论,如果想看原理分析的,可以继续接着看下面分析过程。

结论

  • 重写equals()是为了实现自己的区分逻辑。

  • 重写hashCode()是为了提高hash tables的使用效率,以及和equals()保持一致。

🙋过程分析:

一段代码先奉上:

    class Student{
    private Integer age;
    private String name;
    public Student(Integer age,String name){
    this.age=age;
    this.name=name;
    }
    }
    public static void main(String[] args) {
    Student s1 = new Student(10, "Bob");
    Student s2 = new Student(10, "Bob");
    System.out.println(s1.equals(s2));
    System.out.println(s1.hashCode());
    System.out.println(s2.hashCode());
    }
    相信懂java的应该都知道上面的equals()比较结果是false。
    输出:
      false
      1537358694
      804581391

      下面开始我们的分析,为什么要重写hashCode()和equals()方法?

      要回答这个问题,我们需要清楚java原生的equals()方法是怎么进行比较的。

      在没有重写hashCode()和equals()方法的类中,对象进行equals比较,实际是调用了Object类中的equals()方法。

      Object类中的equals()方法进行对象比较实际是对象内存地址的比较,上面s1和s2对象明显是new了两个对象,所以内存地址一定是不同的,所以结果是false。

        //Object类中的equals()方法。
        public boolean equals(Object obj) {
        //比较的是两个对象的内存地址
        return (this == obj);
        }

        Object类中的hashCode()方法是一个本地方法,hashCode值是根据对象内存地址经哈希算法得来的。

          public native int hashCode();

          java中规定:

            • 两个对象的equals()相同,hashCode一定相同。
            • 两个对象的equals()不同,hashCode不一定不同。
            • hashCode相同,但equals不一定相同。
            • hashCode不同,equals一定不同。

          在实际的开发应用中,我们经常要把对象存到集合当中,如Map、Set,我们都知道Map中key是不能重复的,java中判断key是否重复使用的就是equals()方法和hashCode()方法。

            //摘抄至源码中
            if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))

            而我们在实际中,对于上面s1和s2更愿意理解为是同一个对象,也是说假设我们将上面两个对象放到HashSet中,更希望得到的体现是两个对象是一样的,在HashSet中只保留一个,所以我们需要重写equals()和hashCode()方法来重新定义对象是否相等的条件,保证集合中存储的对象是不同。

              /**
              * 重写hashCode和equal方法
              */
              class Student{
              private Integer age;
              private String name;
              public Student(Integer age,String name){
              this.age=age;
              this.name=name;
              }
              @Override
              public boolean equals(Object o) {
              if (this == o) return true;
              if (!(o instanceof Student)) return false;
              Student s = (Student) o;
              if (age != s.age) return false;
              return name.equals(s.name);
              }


              @Override
              public int hashCode(){
              int result = name.hashCode();
              result = 31 * result + age;
              return result;
              }
              }
              public static void main(String[] args) {
              Student s1 = new Student(10, "Bob");
              Student s2 = new Student(10, "Bob");
              System.out.println(s1.equals(s2));
              System.out.println(s1.hashCode());
              System.out.println(s2.hashCode());
              }
              // true
              // 2075925
              // 2075925


              hashCode()equals()方法重写为啥是成对的出现?

              Coding的哔哔叨叨


              对于这个问题,我们分开来讨论。
              • 只重写equals()方法。


              以上面Student类为例,假设我们只重写了equals()方法,用到hashCode()方法时,依旧调用的是Object类中的hashCode()方法,根据内存地址来给出hashcode值,也就是说equals方法判断对象相等,但hashCode值却不一样,这不符合java的规定:equals比较相等时,hashcode值也一定相等。
              • 只重写hashCode()方法。

              只重写hashCode方法,虽保证了hashCode的不一样,但是equals方法却有可能判断对象不相等,还是以上面s1和s2为例,hashCode相等,但equals调用依然是进行内存地址比较,判断结果为不相等,这时候若要放到Map中,会用链地址法进行存储,也不符合我们的期望。

              重写hashCode()为什么选用31作为乘数。

              Coding的哔哔叨叨


              结论

              • 更少的乘积结果冲突。

              • 31可以被jvm优化。

              分下如下:
              • 更少的乘积结果冲突。

              31是质子数中一个“不大不小”的存在,如果你使用的是一个如2的较小质数,那么得出的乘积会在一个很小的范围,很容易造成哈希值的冲突。而如果选择一个100以上的质数,得出的哈希值会超出int的最大范围,这两种都不合适。而如果对超过 50,000 个英文单词(由两个不同版本的 Unix 字典合并而成)进行 hash code 运算,并使用常数 31, 33, 37, 39 和 41 作为乘子,每个常数算出的哈希值冲突数都小于7个(国外大神做的测试),那么这几个数就被作为生成hashCode值的备选乘数了。

              • 31可以被jvm优化

              JVM里最有效的计算方式就是进行位运算了:

                • 左移 << : 左边的最高位丢弃,右边补全0(把 << 左边的数据*2的移动次幂)。
                • 右移 >> : 把>>左边的数据/2的移动次幂。
                • 无符号右移 >>> : 无论最高位是0还是1,左边补齐0。   
                • 所以:31 * num = (32-1) * num= 32*num-num=(2<<5)*num-num,JVM就可以高效的进行计算啦。


              不积跬步,无以至千里。

              文章有帮助的话,点个转发、在看呗

              谢谢支持哟 (*^__^*)

              END


              👇

              文章转载自Coding的哔哔叨叨,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

              评论