参考资料
- 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))))
...
复制代码
执行上述代码,因没有重写 hashCode
和 equals
方法,导致 m.containsKey(new Key(i))
判断总是 false,导致程序不断向 Map 中插入新的 key-value
,造成死循环,最终将导致内存溢出。