实现hashCode方法的通用约定
- 在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对这个同一对象调用多次,hashCode方法必须始终如一地返回同一个整数。在同一个应用程序的多次执行过程中,每次执行所返回的整数可以不一致。
- 如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。反之,如果两个对象hashCode方法返回整数结果一样,则不代表两个对象相等,因为equals方法可以被重载。
- 如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但,如果能让不同的对象产生不同的整数结果,则有可能提高散列表的性能。
如何理解上面的三条通用约定?
约定1:一致性
例如有Student类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Student {
Integer age;
String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) { // 先判断是不是自己,提高运行效率
return true;
}
if (obj instanceof Student) { // 再判断是不是Person类,提高代码的健壮性
Student s = (Student) obj;// 向下转型,父类无法调用子类的成员和方法
// 最后判断类的所有属性是否相等,其中String类型和Object类型可以用相应的equals()来判断
return (this.getName().equals(s.getName()) && this.getAge() == s.getAge());
}
return false;
}
@Override
public int hashCode() {
int result = 17;// 准备一个质数
result = result * 17 + age;
result = result * 17 + name.hashCode();
return result;
}
}
上面重写了hashCode和equals方法。
执行下面的测试:
1
2
3
4
5
Student s1 = new Student(17, "张三");
System.out.println(s1.hashCode());// 780091
System.out.println(s1.hashCode());// 780091
s1.setName("周粥");
System.out.println(s1.hashCode());// 706959
在 setName 前,hashCode 保持一致。
修改 name 后,hashCode 改变是允许的,因为 equals 依赖的字段变了。
总结:如果对象的某些字段(用于 equals 比较的字段)没有改变,那么每次调用 hashCode 必须返回相同的值。不同次运行程序时,同一个对象的 hashCode 值可以不同(但通常实现上保持一致,除非涉及随机化)。
为什么要这么规定呢?
哈希表(如 HashSet)依赖 hashCode 将对象映射到存储桶。如果同一个对象的 hashCode 在程序运行中变了,而它已经被放入某个桶,哈希表就找不到它,导致逻辑错误。
跨运行不要求一致性是为了给实现者灵活性(例如可以用随机种子生成哈希值以防止哈希攻击)。
约定二:相等对象的哈希值必须相等
约定2内容如下:
如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中任意一个对象的hashCode方法都必须产生同样的整数结果。反之,如果两个对象hashCode方法返回整数结果一样,则不代表两个对象相等,因为equals方法可以被重载。
如果 a.equals(b) 返回 true,则 a.hashCode() 必须等于 b.hashCode()。
这是哈希表正确性的核心保证。
为什么?
- 哈希表先用 hashCode 定位桶,再用 equals 检查桶内的对象。
- 如果两个相等的对象哈希值不同,它们会被放到不同桶,导致 contains 或 remove 找不到对象。
例如还是上面定义的Student类,现在执行如下测试:
1
2
3
4
5
6
7
8
Student s1 = new Student(17, "张三");
Student s2 = new Student(17, "张三");
HashSet<Student> set = new HashSet<>();
set.add(s1);
System.out.println(s1.equals(s2)); // true
System.out.println(s1.hashCode() == s2.hashCode()); // true
System.out.println(set.contains(s2)); // true
System.out.println(set.size()); // 1
因为 s1 和 s2 的 hashCode 相同且 equals 为 true,HashSet 认为它们是同一个对象。
但是如果注释掉Student类的hashCode方法:
1
2
3
4
5
6
7
8
9
10
Student s1 = new Student(17, "张三");
Student s2 = new Student(17, "张三");
HashSet<Student> set = new HashSet<>();
set.add(s1);
System.out.println(s1.equals(s2)); // true
System.out.println(s1.hashCode() == s2.hashCode()); // false
System.out.println(set.contains(s2)); // false
System.out.println(set.size()); // 1
set.add(s2);
System.out.println(set.size()); // 2
或者注释掉equals方法:
1
2
3
4
5
6
7
8
9
10
Student s1 = new Student(17, "张三");
Student s2 = new Student(17, "张三");
HashSet<Student> set = new HashSet<>();
set.add(s1);
System.out.println(s1.equals(s2)); // false
System.out.println(s1.hashCode() == s2.hashCode()); // true
System.out.println(set.contains(s2)); // false
System.out.println(set.size()); // 1
set.add(s2);
System.out.println(set.size()); // 2
这条约定中还有一句话:反之,如果两个对象hashCode方法返回整数结果一样,则不代表两个对象相等,因为equals方法可以被重载。
a.hashCode() == b.hashCode() 不要求 a.equals(b) 为 true。
哈希值相同只是表示它们可能在同一个桶中,最终由 equals 决定是否相等。
为什么?
- 哈希函数会产生冲突(不同的对象映射到同一值),这是不可避免的。
- equals 是最终的相等性判断,hashCode 只是初步定位。
例如:(但是一般不这么写hashCode,但是下面的写法也是合法的)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Example {
private int id;
public Example(int id) {
this.id = id;
}
@Override
public int hashCode() {
return 1; // 故意返回固定值
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Example)) return false;
Example e = (Example) obj;
return id == e.id;
}
}
Example e1 = new Example(1);
Example e2 = new Example(2);
System.out.println(e1.hashCode() == e2.hashCode()); // true
System.out.println(e1.equals(e2)); // false
约定三:不相等对象尽量产生不同哈希值
如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中任意一个对象的hashCode方法,则不一定要产生不同的整数结果。但,如果能让不同的对象产生不同的整数结果,则有可能提高散列表的性能。
如果 a.equals(b) 为 false,a.hashCode() 和 b.hashCode() 可以相同,也可以不同。
但为了提高哈希表效率,尽量让不相等的对象有不同的哈希值,减少冲突。
为什么?
- 哈希冲突会导致多个对象放入同一桶,查找时需要遍历链表或红黑树(logN的时间复杂度),降低性能。
- 好的 hashCode 实现应该尽量均匀分布,减少冲突。
- 如果 hashCode 总是返回固定值(如 1),性能会下降,因为所有对象都挤在同一个桶中。
总结:一般Student类的写法都是
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class Student {
Integer age;
String name;
public Student(Integer age, String name) {
this.age = age;
this.name = name;
}
public Integer getAge() {
return age;
}
public String getName() {
return name;
}
public void setAge(Integer age) {
this.age = age;
}
public void setName(String name) {
this.name = name;
}
@Override
public boolean equals(Object obj) {
if (this == obj) { // 先判断是不是自己,提高运行效率
return true;
}
if (obj instanceof Student) { // 再判断是不是Person类,提高代码的健壮性
Student s = (Student) obj;// 向下转型,父类无法调用子类的成员和方法
// 最后判断类的所有属性是否相等,其中String类型和Object类型可以用相应的equals()来判断
return (this.getName().equals(s.getName()) && this.getAge() == s.getAge());
}
return false;
}
@Override
public int hashCode() {
int result = 17;// 准备一个质数
result = result * 17 + age;
result = result * 17 + name.hashCode();
return result;
}
}