Java中为什么重写equals()就一定要重写hashCode()?
因为HashSet、HashMap以及HashTable中在比较key值时我们认为只要两个key的equals()相等就算是同一key。但实际比较时还要满足两者hashCode()相等才认为是同一key,本来Object类中equals()相等,hashCode()必然相等,但仅仅重写equals()打破了这种逻辑,这样一来就会出现问题(后面有例子)。
一.为什么Objec中两个对象equals()判定相等则hashCode()值必然相等?
Object中的equals()方法源码如下:
public boolean equals(Object obj) {
return (this == obj);
}
这里equals()比较的方法就是"==",比较的是两个对象的
内存地址
Object中的hashCode()是个本地方法,并没有给出实现:
public native int hashCode();
有些朋友误以为默认情况下,hashCode返回的就是对象的存储地址,事实上这种看法是不全面的,确实有些JVM在实现时是直接返回对象的存储地址,但是大多时候并不是这样,只能说可能存储地址有一定关联。下面是HotSpot JVM中生成hash散列值的实现:
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.
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 ;
}
value &= markOopDesc::hash_mask;
if (value == 0) value = 0xBAD ;
assert (value != markOopDesc::no_hash, "invariant") ;
TEVENT (hashCode: GENERATE) ;
return value;
}
该实现位于hotspot/src/share/vm/runtime/synchronizer.cpp文件下。
但不论怎么说它也是根据对象的内存地址根据某种方法计算出来的一个值:
y:hashCode()值;x:内存地址。假设y=kx^n+b。x相等y必然相等;y相等x未必相等。
也即:只要两个对象内存地址一样,计算出来的hashCode()必然相等,所以Object中若equals()判断相等,hashCode()值必然相等,但hashCode()值相等,equals()未必相等。
二.仅重写equals()不重写hashCode()存在什么问题?
举例如下:
package com.cxh.test1;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
class People{
private String name;
private int age;
public People(String name,int age) {
this.name = name;
this.age = age;
}
public void setAge(int age){
this.age = age;
}
@Override
public boolean equals(Object obj) {
if(this == obj){
return true;
}
if(obj == null){
return false;
}
if(this.getClass() != obj.getClass()){
return false;
}
People p = (People)obj;
return this.name.equals(p.name)&&this.age == p.age;
}
}
public class Main {
public static void main(String[] args) {
People p1 = new People("Jack", 12);
System.out.println(p1.hashCode());
HashMap hashMap = new HashMap();
hashMap.put(p1, 1);
System.out.println(hashMap.get(new People("Jack", 12)));
}
}
在这里我只重写了equals方法,也就说如果两个People对象,如果它的姓名和年龄相等,则认为是同一个人。
这段代码本来的意愿是想这段代码输出结果为“1”,但是事实上它输出的是“null”。为什么呢?我们先看看hashMap的get()方法:
final Entry getEntry(Object key) {
if (size == 0) {
return null;
}
int hash = (key == null) ? 0 : hash(key);
for (Entry e = table[indexFor(hash, table.length)];e != null;e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
return e;
}
return null;
}
两者的hash值和equals()判断都相等时,才认为是同一key值。虽然它们的姓名和年龄相等,这里并没有认为以下两个是同一key
People p1 = new People("Jack", 12);
和
new People("Jack", 12)
原因在于
两者是不同的对象,存储的地址不同,所以计算出来的hash值不同,故虽然按重写的equals()相等,但map判断它们不是同一key值,所以返回null。
在map的应用中我们判定key同否同一key往往是根据我们重写的equals()方法来判定的。但map判定时还加入hash值的比较,我们始终要保持equals()相等,hashCode()值必然相等这一逻辑不变。那为什么map在比较key时非要加入hash值的比较这么“累赘”的东西呢?
三.为什么map比较key时非要加入hash值比较这样“累赘”而且“麻烦”的东西呢?
考虑一种情况,当向集合中插入对象时,如何判别在集合中是否已经存在该对象了?(注意:集合中不允许重复的元素存在)
也许大多数人都会想到调用equals方法来逐个进行比较,这个方法确实可行。但是如果集合中已经存在一万条数据或者更多的数据,如果采用equals方法去逐一比较,效率必然是一个问题。此时hashCode方法的作用就体现出来了,当集合要添加新的对象时,先调用这个对象的hashCode方法,得到对应的hashcode值,实际上在HashMap的具体实现中会用一个table保存已经存进去的对象的hashcode值,如果table中没有该hashcode值,它就可以直接存进去,不用再进行任何比较了;如果存在该hashcode值, 就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地址,所以这里存在一个冲突解决的问题,这样一来实际调用equals方法的次数就大大降低了,说通俗一点:Java中的hashCode方法就是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。下面这段代码是java.util.HashMap的中put方法的具体实现:
public V put(K key, V value) {
if (key == null)
return putForNullKey(value);
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length);
for (Entry e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(hash, key, value, i);
return null;
}
put方法是用来向HashMap中添加新的元素,从put方法的具体实现可知,会先调用hashCode方法得到该元素的hashCode值,然后查看table中是否存在该hashCode值,如果存在则调用equals方法重新确定是否存在该元素,如果存在,则更新value值,否则将新的元素添加到HashMap中。从这里可以看出,hashCode方法的存在是为了减少equals方法的调用次数,从而提高程序效率。
四、String类中是如何怎样维持equals()相等,hashCode()必然相等这一逻辑的?
String类重写Object类的equals():
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String) anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
String类重写Object类的hashCode():
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
从源码可以看出,重写后的equals()比较的不再是的地址,而是内容。重写后的hashCode()计算逻辑为:
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
equals()相等,hashCode()必然相等。
参考地址:http://www.cnblogs.com/dolphin0520/p/3681042.html