首页 > 编程语言 >Java中为什么hashcode与equals要一起重写?

Java中为什么hashcode与equals要一起重写?

时间:2024-01-24 17:11:24浏览次数:42  
标签:hash key int equals hashcode Student Java

  首先,hashcode与equals并不是一定要一起重写的。

先说结论:在用到哈希相关的集合时,作为key的类一定要重写hashcode与equals方法,因为这些集合在计算下标时,使用到了key的hashcode方法,并且在判断key是否已经存在时,使用到了equals方法。如果不重写会允许多个相同的key插入,因此需要同时重写。

  但是在其他场景中,equals方法的使用更加频繁,并不必须使用hashcode,允许只重写单个方法。但是还是强烈建议同时重写,官方在equals方法上也有相关的说明:It is generally necessary to override the hashCode method whenever this method is overridden, so as to maintain the general contract for the hashCode method, which states that equal objects must have equal hash codes.


  以下是一个简单的案例分析,分析为什么Hash集合需要同时重写hashcode 与 equals

  1.先说没有重写的情况:

  hashcode方法:对于两个对象a b,无论这两个对象的属性是否相同,两个对象通过hashcode方法得到的哈希值是不同的(排除哈希碰撞的情况)。

  equals方法:a.equals(b) 即为判断a b是否为同一个对象,底层就是return a==b。

   设置一个类Student



public void HashHash(){
Student a= new Student("小明", 123);
Student b= new Student("小明", 123);
System.out.println(a.hashCode());//输出1866161430
System.out.println(b.hashCode());//输出2024918163
}

static class Student{ String name; int id; Student(){}; Student(String name,int id){ this.name=name; this.id=id; } }

  计算得到a的hashcode为1866161430,b的为2024918163。可见即使属性一致,只要对象不同,hashcode就不同 

 

  2.为什么要一起重写?

  一起重写的主要的应用场景在哈希相关的集合上。

  以HashMap为例子,他的底层是通过一个Node数组加上链表与红黑树实现的。

 1 static class Node<K,V> implements Map.Entry<K,V> {
 2         final int hash;
 3         final K key;
 4         V value;
 5         Node<K,V> next;
 6 
 7         Node(int hash, K key, V value, Node<K,V> next) {
 8             this.hash = hash;
 9             this.key = key;
10             this.value = value;
11             this.next = next;
12         }
13 
14         public final K getKey()        { return key; }
15         public final V getValue()      { return value; }
16         public final String toString() { return key + "=" + value; }
17 
18         public final int hashCode() {
19             return Objects.hashCode(key) ^ Objects.hashCode(value);
20         }
21 
22         public final V setValue(V newValue) {
23             V oldValue = value;
24             value = newValue;
25             return oldValue;
26         }
27 
28         public final boolean equals(Object o) {
29             if (o == this)
30                 return true;
31 
32             return o instanceof Map.Entry<?, ?> e
33                     && Objects.equals(key, e.getKey())
34                     && Objects.equals(value, e.getValue());
35         }
36 }

  四个属性为:int hash即哈希值,K key传入的key值,V value传入的value值,Node<K,V> next指向下一个节点(链表or红黑树使用)

  当传入一个新的对象时,HashMap通过该对象的hash值计算该对象在数组中对应的位置。

  //计算key的哈希值,需要调用key自己的hashcode方法
static final int hash(Object key) { int h; return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
  //得到hash后,该对象在Node数组中的下标为i = (n - 1) & hash 其中n是Node数组的长度,是2的整数次方,该计算下标实际上就是取模,取hash的后log2 n位

  由于HashMap<key , value>要求集合中的key具有唯一性,集合中不能存在两个key相同的对象。这里的“相同”指的是数值上的相同,并非一定要是同一个对象,只要属性相同就行。而如果key没有重写hashcode方法,会导致即使是两个属性完全相同的对象,计算得到的两个hash也不同。这就导致插入这两个对象。还以Student为例。

@Test
   public void HashHash(){
      Student a= new Student("小明", 123);
      Student b= new Student("小明", 123);
      HashMap<Student,String> map=new HashMap<>();
      map.put(a,"学生a");
      map.put(b,"学生b");
      System.out.println(map);
   }

  得到结果{com.nys.LeetcodeTest$Student@78b1cc93=学生b, com.nys.LeetcodeTest$Student@6f3b5d16=学生a}

  可以看到,没有重写hashcode方法时,即使a b两个对象属性相同,还是全部存入了hashmap中。这就违背了hashmap的原则。

  所以,如果使用哈希相关的集合,必须重写key的hashcode方法。

   那么我们就先把hashcode给重写了

@Test
   public void HashHash(){
      Student a= new Student("小明", 123);
      Student b= new Student("小明", 123);
      HashMap<Student,String> map=new HashMap<>();
      map.put(a,"学生a");
      map.put(b,"学生b");
      System.out.println(map);
   }

   static class Student{
      String name;
      int id;

      Student(){};
      Student(String name,int id){
         this.name=name;
         this.id=id;
      }

      @Override
      public int hashCode() {
         int result = 1;
         result = 31 * result + (name == null ? 0 : name.hashCode());
         result = 31 * result + (Integer.valueOf(id) == null ? 0 : Integer.valueOf(id).hashCode());
         return result;
      }
   }

  重写key的hashcode过后,输出,发现map里还是有两个元素!{com.nys.LeetcodeTest$Student@165f43d=学生a, com.nys.LeetcodeTest$Student@165f43d=学生b}

  这是为啥呢?不都重写了吗?怎么还这样?

  且慢。不要忘了,HashMap的底层不仅仅是一个Node数组,还有链表与红黑树,hashmap对于key是否相同的判断不仅仅使用到了hashcode,同时用到了equals!重写hashcode后,虽然解决了属性相同的对象哈希值不一致的问题,但是在表中,还是会发生碰撞。当map中已经有了a,在加入b时,就会使得a.next=b,形成一个链表。因此重写hashcode并不能完全满足要求。

  看看HashMap是如何添加新元素的吧

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; //Node数组,HashMap的底层存储
Node<K,V> p;
int n, i;
    //判断是否为空,为空则resize()扩容到16 if ((tab = table) == null || (n = tab.length) == 0) n = (tab = resize()).length;

    //计算得到key对应的下标i
    //若tab[i]为空则可以直接插入到这 if ((p = tab[i = (n - 1) & hash]) == null) tab[i] = newNode(hash, key, value, null); else {
      //tab[i]不为空 Node<K,V> e; K k;
       //判断key与Hashmap中的元素的key是否一致 if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) e = p;
       //判断是否为红黑树节点 else if (p instanceof TreeNode) e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
       //与tab[i]不一致,并且不是红黑树,因此只能是链表
       //遍历链表,寻找key相同的节点
else { for (int binCount = 0; ; ++binCount) {
            //没找到,插入到后面 if ((e = p.next) == null) { p.next = newNode(hash, key, value, null);
              //判断是否达到红黑树化的标准(链表长度达到8) if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st treeifyBin(tab, hash); break; }
            //找到了就是e if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) break; p = e; } }
      //e为空,则代表没有找到key值,可以直接插入
      //e非空,代表找到相应的节点。进入下面的if,直接修改value即可
if (e != null) { // existing mapping for key V oldValue = e.value; if (!onlyIfAbsent || oldValue == null) e.value = value; afterNodeAccess(e); return oldValue; } } ++modCount;
    //插入后判断是否需要扩容(size达到0.75*table.length) if (++size > threshold) resize(); afterNodeInsertion(evict); return null; }

  可以顺便复习一下hashmap底层,版本是jdk17。可以看到,判断的核心代码是

p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))

  对于key相同的判断,不仅仅需要判断hash的一致性(通过hashcode移位以及异或运算得到),key相同还需要满足这俩引用的是同一个对象或者这俩equals为true

  而由于equal没有重写,因此在刚刚的测试中,上面这一段判断等价于

p.hash == hash && ((k = p.key) == key || (key != null && key == k))

  也就是判断了两次是否为同一对象,由于a b本来就是不同的,因此都插入到map中了。

  通过模拟HashMap计算下标的方法,可以得到a和b的下标都是8(仅重写hashcode)

@Test
   public void HashMapIndex(){
      Student a= new Student("小明", 123);
      Student b= new Student("小明", 123);
      int initialCap=16;
      int hashA=hash(a);
      int hashB=hash(b);
      int indexA=(initialCap - 1) & hashA;
      int indexB=(initialCap - 1) & hashB;
      System.out.println("HashMap初始化的长度"+ initialCap);//HashMap初始化的长度16
      System.out.println("a的hashcode:"+a.hashCode()+"  a的hash:"+hashA+"  a的index:"+indexA);//a的hashcode:23458877  a的hash:23459160  a的index:8
      System.out.println("b的hashcode:"+b.hashCode()+"  b的hash:"+hashB+"  b的index:"+indexB);//b的hashcode:23458877  b的hash:23459160  b的index:8
   }

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

  当hashcode与equals都没重写

HashMap初始化的长度16
a的hashcode:2024918163  a的hash:2024911906  a的index:2
b的hashcode:107241811  b的hash:107243319  b的index:7

  现在我们补上hashcode与equals方法

  再次执行,得到结果如下

{com.nys.LeetcodeTest$Student@165f43d=学生b}

  终于

 

标签:hash,key,int,equals,hashcode,Student,Java
From: https://www.cnblogs.com/nys1999/p/17984833

相关文章

  • 基于javaweb的学生二手书籍交易平台的设计与实现
    1.立题意义随着互联网技术的快速发展,网络时代的到来,网络信息也将会改变当今社会。各行各业在日常企业经营管理等方面也在慢慢的向规范化和网络化趋势汇合[1]。校园二手书交易系统的信息化程度体现在将互联网与信息技术应用于经营与管理,以现代化工具代替传统手工作业[2]。无疑,使用......
  • 基于java的水果网上商城的开发与设计
    一、研究背景和意义(一)研究背景随着互联网技术的迅猛发展,现在互联网已从单纯的学术科研向综合性商业网络发展,人们对于互联网技术的要求已不单单是浏览一下网页、收发电子邮件等简单的网络应用。高速发展的互联网技术为在线购物的发展提供了强大的动力,同时,在线购物的环境已经逐步得......
  • java代码通过百度获取第一条搜索结果代码以及注意事项
    导入依赖:<dependency><groupId>io.github.bonigarcia</groupId><artifactId>webdrivermanager</artifactId><version>4.4.3</version></dependency><de......
  • [转]JavaScript:解决计算精度问题/mathjs/bignumber.js/big.js/decimal.js
    原文地址:JavaScript:解决计算精度问题/mathjs/bignumber.js/big.js/decimal.js_js计算精度问题-CSDN博客一、计算精度现象举例举例1、加法 举例2、减法 举例3、乘法 举例3、除法 二、JS为什么会有计算精度的问题JavaScript内部只有一种数字类型Number,也就是说,Java......
  • 【中介者模式(Mediator)】使用Java实现中介者模式
    引言中介者,何为中介者,顾名思义就是我们的在处理A和B之间的关系的时候,引入一个中间人,来处理这两者之间的关系,例如生活中我们需要去租房,买房,都会有中介,来处理房东和租客之间的协调关系,这个就是中介者,落实到具体的代码中呢,就像我们的Controller可能会依赖很多的Service层面......
  • 使用Java中的HttpClient进行网络请求
    使用Java中的HttpClient进行网络请求是一种常见的操作,它可以帮助我们轻松地发送HTTP请求并获取响应。以下是使用Java中的HttpClient进行网络请求的基本步骤:1. 添加依赖:首先,确保您的项目中包含了HttpClient的依赖。如果您使用的是Maven项目,请在pom.xml文件中添加以下依赖:2. xml复......
  • Java Chassis 3技术解密:易扩展的多种注册中心支持
    原文链接:https://bbs.huaweicloud.com/blogs/420748JavaChassis的早期版本依赖于ServiceCenter,提供了很多差异化的竞争力:接口级别转发。通过注册中心管理微服务的每个版本的元数据,特别是契约数据。结合契约数据,能够实现版本级别的路由能力。比如一个微服务存在v1和v2两......
  • Java中的死锁问题及其解决方案
    第1章:引言大家好,我是小黑。今天咱们来聊聊Java编程中一个让人头疼的问题——死锁。你可能听说过死锁,或者在编码时不小心遇到过。死锁就像是交通堵塞,在程序的世界里,它会让线程陷入无尽的等待,导致程序无法正常运行。在Java并发编程中,理解死锁并学会如何处理它是非常关键的。接下......
  • Java21 + SpringBoot3整合Redis,使用Lettuce连接池,推荐连接池参数配置,封装Redis操作
    目录前言相关技术简介Redis实现步骤引入maven依赖修改配置文件定义Redis配置类定义Redis服务类,封装Redis常用操作使用Redis服务类总结前言近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展......
  • Java和C++的区别:传闻这个问题能分辨你是不是科班出生?
    大家好,欢迎来到程序视点!我是小二哥。今天听到一个面试的小伙伴分享了他的面试经历,说面试官第一个问题是:Java语言和C++语言的区别有哪些?坊间流传,早些年间这个问题能区分一个Java程序员是不是科班出身!小伙伴怎么认为呢?缘由由于Java本来就是从C++衍生出来的,而且Java语言......