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 studentIntegerMap = new HashMap();
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,这时候找不到对应的元素,不会进行删除操作,导致对象长时间不能被释放,造成内存溢出。