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

2021-09-27  本文已影响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,这时候找不到对应的元素,不会进行删除操作,导致对象长时间不能被释放,造成内存溢出。

上一篇下一篇

猜你喜欢

热点阅读