美文网首页
关于重写equals方法时必须重写hashcode方法的一系列问

关于重写equals方法时必须重写hashcode方法的一系列问

作者: 攻城虱小马褂 | 来源:发表于2021-09-27 10:20 被阅读0次

    Why

    日常开发中经常会遇到重写equals和hashocode的场景,以前对这些概念很模糊,只知其然,不知其所以然,现在对这些知识理论进行总结、归纳,加强理解。

    What

    Object是所有对象的基类,它包含两个基本的方法: equals和hashcode
    (1)equals

    public boolean equals(Object obj) {
            return (this == obj);
    }
    

    由图可知:equals比较的是两个对象的引用(内存空间地址)
    (2)hashcode

    public native int hashCode();
    

    hashcode是Java native方法,是JVM虚拟机为这个Object对象分配的一个int类型的数值(能够在OpenJDK或者其他开源JRE找到相应的C/C++源码)

    hashcode在jre中应该遵循的约定(PS:Object类的hashcode和equals):

    (1)一致性。程序的一次执行中(同一进程),同一个对象的hashcode数值必须相同(不同进程生成的hashcode应该不一样)。

    (2)如果两个对象通过equals比较为同一个对象,那么这两个对象的hashcode也应该相同

    (3)如果两个对象通过equals的比较不相同,这两个对象的hashcode不一定不相同(有可能会一样)

    疑问:hashcode的值到底是什么,

    Object类的hashcode要看具体的实现方式(Object的hashcode默认将对象的内存地址转成一个整数返回,由此可以判断两个对象是否相同,hashcode也是唯一的),查看openJDK的源码

    static inline intptr_t get_next_hash(Thread * Self, oop obj) {
      intptr_t value = 0 ;
      if (hashCode == 0) {
         // This form uses an unguarded global Park-Miller RNG,
         // so it's possible for two threads to race and generate the same RNG.
         // On MP system we'll have lots of RW access to a global, so the
         // mechanism induces lots of coherency traffic.
         value = os::random() ; // 随机数
      } else
      if (hashCode == 1) {
         // This variation has the property of being stable (idempotent)
         // between STW operations.  This can be useful in some of the 1-0
         // synchronization schemes.
         // 地址基础上hack
         intptr_t addrBits = intptr_t(obj) >> 3 ; 
         value = addrBits ^ (addrBits >> 5) ^ GVars.stwRandom ;
      } else
      if (hashCode == 2) {
         value = 1 ;            // for sensitivity testing, 实际不会使用
      } else
      if (hashCode == 3) {
         value = ++GVars.hcSequence ;
      } else
      if (hashCode == 4) {
         value = intptr_t(obj) ; // 直接用地址
      } else {
         // Marsaglia's xor-shift scheme with thread-specific state
         // This is probably the best overall implementation -- we'll
         // likely make this the default in future releases.
         unsigned t = Self->_hashStateX ;
         t ^= (t << 11) ;
         Self->_hashStateX = Self->_hashStateY ;
         Self->_hashStateY = Self->_hashStateZ ;
         Self->_hashStateZ = Self->_hashStateW ;
         unsigned v = Self->_hashStateW ;
         v = (v ^ (v >> 19)) ^ (t ^ (t >> 8)) ;
         Self->_hashStateW = v ;
         value = v ;
      }
    

    获取Hashcode值的方法有6种,包括随机法,地址基础上进行处理,默认值1,直接地址。顺序值,Marsaglia方法。

    有些jvm在实现的时候是采用的对象存储地址,但大多数时候并不是这样的,只能说跟存储地址有一定的关联。所以hashCode返回的并不一定是对象的(虚拟)内存地址,具体取决于运行时库和JVM的具体实现。

    问题1:问什么重写equals方法的时候必须重写hashcode方法

    假如在覆盖了equals方法的类中,没有覆盖hashcode,会导致该类无法结合基于散列集合(HashMap,HashSet,HashTable)一起正常运作。

    example:

    两个完全不同的实例,但是有可能存在逻辑上的相等,所以我们可以重写equals方法来来实现逻辑的比较,如

    public void test2(){
            class Student{
                private String name;
                private int age;
     
                public Student(String name, int age) {
                    this.name = name;
                    this.age = age;
                }
     
                @Override
                public boolean equals(Object obj) {
                    if(obj == this){
                        return true;
                    }
                    if(! (obj instanceof Student)){
                        return false;
                    }
                    Student student = (Student)obj;
                    return student.age == age&&student.name == name;
                }
            }
     
            Student student1 = new Student("xiaoming",11);
     
            Student student2 = new Student("xiaoming",11);
     
            System.out.println(student1.equals(student2));
     
            System.out.println("student1 hash code:"+student1.hashCode());
     
            System.out.println("student2 hash code:"+student2.hashCode());
     
            Map<Student,Integer> studentIntegerMap = new HashMap<Student,Integer>();
            
            studentIntegerMap.put(student1,11);
            
            Integer value = studentIntegerMap.get(new Student("xiaoming",11));
     
            System.out.println("student1 value:"+value);
        }
    
    true
    student1 hash code:2009832657
    student2 hash code:158460163
    student1 value:null
    

    由上面你的代码和运行结果可以看出(hashcode没有重写的情况下):

    (1)equals实现了逻辑上的比较,两个具有不同属性的实例具有逻辑意义上的相等。

    (2)equals相等的两个实例,hashcode不一样,这违反了(两个相等的对象,hashcode也必须相等的原则)

    (3)当通过get(new Student("xiaoming",11))方法去取student1的值的时候,取得值为空。(因为没有重写hashcode,导致new出来的不同对象具有不同hash值,put的时候根据hash值put到一个散列桶,get的时候根据new出来对象的hash去对应的散列桶,所以没有取到值)

    解决方案:所以考虑给类提供一个自定义的hashcode()方法,让相等的对象具有相等的hashcode,一种比较极端的做法,就是用hashcode返回一个常量

    public int hashcode(){
       return 42;
    }
    

    后果:每个对象都具有相等的hash值,操作hashMap的时候,每个对象都被映射到同一个散列桶,使散列表退化成了链表,使本该线性时间运行的程序变成了平方级时间。(所以好的散列函数就是为不相等对象产生不相等的散列码,能够把集合中不相等的实例均匀的分布在所有可能的散列值中,为不相等的对象生成不同的hash值可以提高hash表的性能)

    总结

    问题1:hashcode与equals的区别和联系
    联系:
    (1)hashcode和equals都是基类Object的基本方法。
    (2)若重写了equals方法,则有必要重写hashcode方法。
    (3)重写了equals方法,而不重写hashcode会对程序造成隐患(要想保证在散列集合中的元素的唯一性,必须同时覆盖hashcode和equals)。
    (4)两个对象equals为true,则两个对象的hashcode返回相同的hash值
    (5)两个对象equals返回false,则两个对象的hashcode不一定返回不一样的hash值
    (6)同一个对象在执行期间已经存储在集合中,则不能修改影响hashcode的香瓜内心戏,否则会内存泄露。
    区别:
    (1)当不在HashSet、Hashtable、HashMap中使用该类时,该类的equals与hashcode没有任何关系。equals主要用于比较对象的内容是否相等(覆盖后),hashcode则根本没用上。
    (2)当在HashSet、Hashtable、HashMap中使用该类时,hashcode和equals是有关系的,hashcode和equals需要同时重写才能保证元素的唯一性。hashcode是为了提高散列结构存储中查找的效率,在线性表中没有作用。

    问题2:hash在集合中的作用,好处是什么?
    能够提高查找效率。场景:当想查找一个集合是否包含或某个对象的时候,通常就是让集合的每个元素都与查找的对象进行比较,缺点就是当集合存在很多元素的时候,比如存在一万个元素,则要逐一进行比较,查找的效率非常低。所以有人就提出hash算法来提高集合查找元素的效率,将集合分成若干个存储区域(哈希桶),每个对象都可以计算出一个哈希码,,然后根据哈希码分到到不同的存储空间。当我们要查找对象的时候,不需要遍历整个集合了,只需要计算查找对象的key的hashcode,然后找到对应的存储空间,在对应的存储空间查找就可以了,如果找不到对应的存储空间,就直接返回了,省去了equals比较的过程,提高了查找效率。

    问题3:为什么重写equals,就必须重写hashcode,什么情况下可以不重写hashcode
    (1)当所在类不使用HashSet、Hashtable、HashMap等散列集合进行存储的时候,可以不使用hashcode。
    (2)当在HashSet、Hashtable、HashMap中使用该类时,hashcode和equals是有关系的,hashcode和equals需要同时重写才能保证元素的唯一性。hashcode是为了提高散列结构存储中查找的效率,在线性表中没有作用。

    问题4:若hashcode方法永远返回1或者常量 会出现什么后果
    每个对象都具有相等的hash值,操作hashMap的时候,每个对象都被映射到同一个散列桶,使散列表退化成了链表,使本该线性时间运行的程序变成了平方级时间。(所以好的散列函数就是为不相等对象产生不相等的散列码,能够把集合中不相等的实例均匀的分布在所有可能的散列值中,为不相等的对象生成不同的hash值可以提高hash表的性能)

    问题5:hashset里面的hashcode存在哪,如果重写equals,不重写hashcode会怎样。
    hashMap

    问题6:Object的hashcode是唯一的?
    Object的hashcode默认将对象的内存地址转成一个整数返回,由此可以判断两个对象是否相同,hashcode也是唯一的。

    问题7:Object的hashcode的值是唯一的,不会产生hash冲突,为什么还要重写
    我们想要实现的hashcode(具备相同属性的对象应该保持hash一致性,不同属性的对象尽可能保持不同的hash值)

    然而object的hashcode是唯一的,所以没法实现相同属性对象hash一致的目的,所以我们要进行重写。

    问题8:修改hashcode值相关信息,造成内存泄露的原因

    Collection set = new HashSet();
    Point p1 = new Point(1,2);
     
    set.add(p1);
     
    p1.setX(10);
    p1.setY(10);
     
    set.remove(p1)
    

    假设p1的hashCode为1,当存储时被分配到hashcode为1的哈希桶中,这时候修改p1的属性值,当调用remove的时候,会首先计算修改后的hashcode,根据修改后的hashcode去查找对应的哈希桶,假如修改后对象的hashcode为10,这时候找不到对应的元素,不会进行删除操作,导致对象长时间不能被释放,造成内存溢出。

    相关文章

      网友评论

          本文标题:关于重写equals方法时必须重写hashcode方法的一系列问

          本文链接:https://www.haomeiwen.com/subject/zmdhnltx.html