参考资料
- ref 1-阿里《Java开发手册》,「集合处理」章节
- ref 2-《Effective Java》,第3章节,「第11条 覆盖equals时总要覆盖hashcode」
- ref 3-为什么重写equals必须重写hashCode | Segmentfault
前言
根据阿里《Java开发手册》,对 Java 对象的 hashCode 和 equals 方法,有如下强制约定。
[强制] 关于
hashCode和equals的处理,遵循如下规则1)只要覆写 equals,就必须覆写 hashCode。
2)因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法。
3)如果自定义对象作为 Map 的键,那么必须覆写 hashCode 和 equals。
说明:String 已经覆写 hashCode 和 equals 方法,所以我们可以愉快地使用 String 对象作为 key 来使用。
下面进行必要的补充分析。
equals保证可靠性,hashCode保证性能
equals保证可靠性,hashCode保证性能。
equals 和 hashCode 都可用来判断两个对象是否相等,但是二者有区别
equals可以保证比较对象是否是绝对相等,即「equals保证可靠性」hashCode用来在最快的时间内判断两个对象是否相等,可能有「误判」,即「hashCode保证性能」- 两个对象
equals为 true 时,要求hashCode也必须相等 - 两个对象
hashCode为 true 时,equals可以不等(如发生哈希碰撞时)
hashCode 的「误判」指的是
- 同一个对象的
hashCode一定相等。 - 不同对象的
hashCode也可能相等,这是因为hashCode是根据地址hash出来的一个int 32位的整型数字,相等是在所难免。
此处以向 HashMap 中插入数据(调用 put 方法,put 方法会调用内部的 putVal 方法)为例,对「equals 保证可靠性,hashCode 保证性能」这句话加以说明,putVal 方法中,判断两个 Key 是否相同的代码如下所示。
// putVal 方法
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
...
在判断两个 Key 是否相同时,
- 先比较
hash(通过hashCode的高 16 位和低 16 位进行异或运算得出)。这可以在最快的时间内判断两个对象是否相等,保证性能。 - 但是不同对象的
hashCode也可能相等。所以对满足p.hash == hash的条件,需要进一步判断。 - 继续,比较两个对象的地址是否相同,
==判断是否绝对相等,equals判断是否客观相等。
自定义对象作为Set元素时
class Dog {
String color;
public Dog(String s) {
color = s;
}
}
public class SetAndHashCode {
public static void main(String[] args) {
HashSet<Dog> dogSet = new HashSet<Dog>();
dogSet.add(new Dog("white"));
dogSet.add(new Dog("white"));
System.out.println("We have " + dogSet.size() + " white dogs!");
if (dogSet.contains(new Dog("white"))) {
System.out.println("We have a white dog!");
} else {
System.out.println("No white dog!");
}
}
}
运行程序,输出结果如下。
We have 2 white dogs!
No white dog!
根据阿里《Java开发手册》可知,「因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须覆写这两个方法」。将 Dog 代码修改为如下。
class Dog {
String color;
public Dog(String s) {
color = s;
}
//重写equals方法, 最佳实践就是如下这种判断顺序:
public boolean equals(Object obj) {
if (!(obj instanceof Dog))
return false;
if (obj == this)
return true;
return this.color == ((Dog) obj).color;
}
public int hashCode() {
return color.length();//简单原则
}
}
此时,再运行程序,输出结果如下。
We have 1 white dogs!
We have a white dog!
自定义对象作为Map的键和内存溢出
如下代码,自定义 KeylessEntry 对象,作为 Map 的键。
class KeylessEntry {
static class Key {
Integer id;
Key(Integer id) {
this.id = id;
}
@Override
public int hashCode() {
return id.hashCode();
}
}
public static void main(String[] args) {
Map m = new HashMap();
while (true){
for (int i = 0; i < 10000; i++){
if (!m.containsKey(new Key(i))){
m.put(new Key(i), "Number:" + i);
}
}
System.out.println("m.size()=" + m.size());
}
}
}
上述代码中,使用 containsKey(keyElement) 判断 Map 是否已经包含 keyElement 键值。containsKey 的关键代码如下所示,使用了 hashCode 和 equals 方法进行判断。
if (first.hash == hash &&
((k = first.key) == key || (key != null && key.equals(k))))
...
执行上述代码,因没有重写 equals 方法,导致 m.containsKey(new Key(i)) 判断总是 false,导致程序不断向 Map 中插入新的 key-value,造成死循环,最终将导致内存溢出。