java基础(2) 实现hashCode方法的通用约定

hashCode的通用约定确保哈希表等数据结构能正确运行:首先,如果两个对象通过equals方法比较是相等的,它们的hashCode必须相同;其次,只要用于equals比较的对象信息未变,单次程序运行中同一对象的hashCode必须保持一致;最后,虽然不强制要求,但为了提高性能,不相等的对象应尽量产生不同的hashCode以减少哈希冲突。

Posted by Hilda on April 6, 2025

实现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;
    }
}