2024-03-09
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接:https://www.skjava.com/mianshi/baodian/detail/4164234699

回答

这是因为 equals()hashCode() 之间存在两个契约关系:

  1. 如果两个对象相等(即 equals() 返回 true),那么它们的 hashCode() 必须返回相同的整数值。
  2. 如果两个对象的 hashCode() 返回相同的值,并不要求这两个对象一定相等(即 equals() 返回 true),但如果两个对象的 hashCode() 返回不相同值,那要求这两个对象不相等(equals() 返回 false)。

基于这种契约关系,如果我们通过重写 equals() 来改变对象的相等逻辑,但是却没有重写 hashCode(),那么这两个方法的契约关系就被破坏了。破坏了这种契约关系会导致我们无法正常使用基于散列的集合类(如 HashSet, HashMap, HashTable):

  • HashSet 中,我们知道它是不能有重复数据的。如果 equals() 被重写以改变相等性判断,而 hashCode() 没有相应地被重写,那么即使两个对象根据 equals() 方法是相等的,它们也可能同时存在于集合中,因为它们的哈希码不同,从而违反了集合不包含重复元素的性质。
  • HashMap 中,元素的存储位置(桶的位置)是根据其哈希码计算得出的。如果两个对象根据 equals() 方法是相等的,但它们的哈希码不同,那么它们会被错误地存储在不同的桶中。

详解

equals()hashCode() 是 Java 中顶级类 Object 的两个方法,我们先看 equals(),定义如下:

    public boolean equals(Object obj) {
        return (this == obj);
    }

该方法直接是对比两个对象的地址是否一样,这里我们关注注释:

红色标注地方的意思是:每当我们重写 equals() 时,就需要重写 hashCode(),这样才能不违背 hashCode() 的约定:相等的对象必须具有相等的哈希值

我们再看 hashCode()

hashCode() 是个 native 方法,它返回一个对象的哈希值,在 hashCode() 中有三条注释(红色不分),大致意思如下:

  1. 同一个对象多次调用 hashCode(),只要该对象的信息没有被修改,该方法都必须返回相同的整数值。
  2. 两个对象如果通过 equals() 判定是相等的,那么这两个对象的 hashCode() 返回的值必须相等。
  3. 两个对象如果通过 equals() 判定是不相等的,那么不要求 hashCode() 返回不相等的整数值,但是要求拥有两个不同的哈希值的对象必须是不同的对象。

根据这三条规则,我们可以得到下面这张图:

这里有两个必须成立的条件(虚线部分):

  1. 相同的对象必须有相同的哈希值。
  2. 不同的哈希值必须是由不同的对象导致。

所以,到这里我们就应该明白: equals()hashCode() 这两个方法根本就是配套使用的。对于任何一个 Java 对象,不论你是直接使用 Object 的 equals() 还是重写 equals()hashCode() 的本质都是为该 equals() 认定为相同的对象返回相同的哈希值。

下面我们以 HashSet 为例如果只重写 equals() 不重写 hashCode() 所带来的问题。

  • 定义一个 User 类,重写它的 equals() 如下:
@Data
@ToString
@AllArgsConstructor
public class User {
    private String name;

    private Integer age;

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }

        if (o == null || getClass() != o.getClass()) {
            return false;
        }

        User user = (User) o;
        return user.getName().equals(this.getName())
                && user.getAge().equals(this.getAge());
    }
}

  • 简单测试下:
public class EqualsTest {
    public static void main(String[] args) {
        User user1 = new User("张三",15);
        User user2 = new User("张三",15);

        System.out.println("user1 == user2:" + (user1 == user2));
        System.out.println("user1.equals(user2):" + (user1.equals(user2)));
        System.out.println("user1.hashCode():" + user1.hashCode());
        System.out.println("user2.hashCode():" + user2.hashCode());
    }
}

结果:

user1 == user2:false
user1.equals(user2):true
user1.hashCode():1325547227
user2.hashCode():980546781

这里 user1.equals(user2) 为 true,我们就可以确认两个对象是相等的,我们将其添加到 HashSet 里面去:

public class EqualsTest {
    public static void main(String[] args) {
        User user1 = new User("张三",15);
        User user2 = new User("张三",15);

        HashSet<User> hashSet = new HashSet<>();
        hashSet.add(user1);
        hashSet.add(user2);

        hashSet.forEach(System.out::println);
    }
}

结果:

两个相等的对象添加到 HashSet 中为什么还是两个?他们应该去重的。我们根据源码去看:

HashSet 的底层实现是 HashMap,所以我们直接看 HashMap 的 put()

    public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

hash(key) 是计算 key 的哈希值,我们这里的 key 是 User 对象:

    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

其实就是调用 key 的 hashCode()。如果我们不在 User 对象里面重写 hashCode(),这里就调用了 Object 的 hashCode() ,返回的哈希值是对象的引用值的哈希码。

pubVal() 方法我们看下面一个地方就行:

if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) 这段代码是证明两个对象是否相等,它要求两者哈希值相当且二者地址值相等或调用equals()认定相等。

在上面示例中,我们只能确认 user1 和 user2 调用 equals()认定相等,他们的哈希值是不等的,所以 HashSet 就认定他们两个是不同的对象,尽管我们从业务逻辑上来说他们是相同。

所以,这里我们也重写

@Data
@ToString
@AllArgsConstructor
public class User {
    //省略重复代码

    @Override
    public int hashCode() {
        return Objects.hash(this.name,this.age);
    }
}

我们再执行那个测试类:

阅读全文