浅析Java 中 hashCode() 函数和 equals() 函数的一些坑

**注:**本文翻译至https://dzone.com/articles/working-with-hashcode-and-equals-in-java
默认情况下,java.lang.Object 类提供了两个非常重要的函数:equals() 和 hashCode(),当你需要对两个对象进行比较时,就会用到这两个方法。尤其是在大型项目中,多个类之间的需要交互时,这两个方法显得更加重要。在这篇文章中,我们将探讨这两个函数之间的关系,他们默认的实现方式,以及什么情况下开发者必须重载这两个方法。

JDK 中的定义以及默认的实现方式

  • equals(Object obj):该方法由 java.lang.Object 提供,作用是判断当前对象是否与参数对象相等。在 JDK 中,默认是根据内存地址来判断两个对象是否相等——若内存地址一样,则相等,否则不相等。
  • hashCode():该方法由 java.lang.Object 提供,该方法返回对象内存地址的整数表示形式。默认情况下,该方法返回的是一个随机的整数,并且每一个对象的 hashCode() 返回值都是唯一的。对每一个对象来说,该方法的返回值并不会保持不变,相反,在多次调用这个函数时,每次调用的返回值都有可能不一样。

equals() 函数和 hashCode() 函数之间的约定

JDK 提供的默认的 equals() 函数和 hashCode() 函数的实现往往不能满足业务需求,尤其是在一方面个大型应用中,当某些情景发生时,我们就可以将两个对象视作相等。在某些场景下,为了让判等机制依赖于自定义的业务逻辑,而不是依赖于内存地址,开发者会重载 equals() 函数和 hashCode() 函数。

根据 Java 开发文档,为了使判等机制完全生效,开发者应该同时重载 equals() 函数和 hashCode() 函数,即仅仅实现 equals() 函数是不够的。

如果 equals(Object obj) 函数判断两个对象相等,那么这两个对象的 hashCode() 函数必须要有相同的返回值

In the following sections, we provide several examples that show the importance of overriding both methods and the drawbacks of overriding equals() without hashcode().

在下面的章节中,我会提供一些例子,来展示同时重载 equals() 函数和 hashCode() 函数的重要性,同时展示只重载 equals() 函数而不重载 hashCode() 函数的缺陷。

案例

我们定义一个 Student 类:

package com.programmer.gate.beans;

public class Student {

    private int id;
    private String name;
    
    public Student(int id, String name) {
        this.name = name;
        this.id = id;
    }
    
    public int getId() {
        return id;
    }
    
    public void setId(int id) {
        this.id = id;
    }
    
    public String getName() {
        return name;
    }
    
    public void setName(String name) {
        this.name = name;
    }
}

为了方便测试,我们定义一个主类 HashCodeEquals ,用来检查两个 Student 类的实例(这两个实例有相同的属性)是否如我们想象中的那样相等。

public class HashcodeEquals {

    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");
        
        System.out.println("alex1 hashcode = " + alex1.hashCode());
        System.out.println("alex2 hashcode = " + alex2.hashCode());
        System.out.println("Checking equality between alex1 and alex2 = " + alex1.equals(alex2));
    }
}

输出:

alex1 hashcode = 1852704110
alex2 hashcode = 2032578917
Checking equality between alex1 and alex2 = false

虽然我们定义了两个具有相同属性的 Student 类的对象,但是他们存储在不同的内存区域。因此如果使用默认的 equals() 函数判断两个对象是否相等,会返回 false。hashCode() 函数也是如此——为每个实例生成唯一的随机代码。

重载 equals() 函数

假设在实际的业务中,我们将两个具有相同 ID 的学生视作相等,因此我们需要按照如下方式重载 equals() 函数:

@Override
public boolean equals(Object obj) {
    if (obj == null) return false;
    if (!(obj instanceof Student))
        return false;
    if (obj == this)
        return true;
    return this.getId() == ((Student) obj).getId();
}

在上面的实现中,如果两个 Student 对象存储在同一块内存中或者他们具有相同的 ID,那我们就说这两个学生相等。现在如果我们再去运行 HashcodeEquals,就会得到下面的输出:

alex1 hashcode = 2032578917
alex2 hashcode = 1531485190
Checking equality between alex1 and alex2 = true

正如你看到的,根据我们自己的业务需求重载 equals() 函数会迫使 Java 在比较两个 Student 对象时考虑对象的 ID 属性。

ArrayList 中的 equals()

equals() 函数的一种非常广泛的使用方式是,定义一个 ArrayList 的对象,里面的元素是 Student 对象,然后在其中找到指定的 Student 对象。为了达到这个目的,我们将测试类的代码改成下面这个样子:

public class HashcodeEquals {

    public static void main(String[] args) {
        Student alex = new Student(1, "Alex");
        
        List < Student > studentsLst = new ArrayList < Student > ();
        studentsLst.add(alex);
        
        System.out.println("Arraylist size = " + studentsLst.size());
        System.out.println("Arraylist contains Alex = " + studentsLst.contains(new Student(1, "Alex")));
    }
}

运行上面的测试代码,我们得到了下面的输出:

Arraylist size = 1
Arraylist contains Alex = true

重载 hashCode() 函数

我们现在已经重载了 equals() 函数并且得到了我们想要的结果,即使两个对象的 hashcode 是不同的。那么,还有必要重载 hashCode() 函数吗?

HashSet 中的 equals()

Let’s consider a new test scenario. We want to store all the students in a HashSet, so we update HashcodeEquals as the following:
现在让我们思考一个新的测试场景。我们想在一个 HashSet 中存储所有的学生对象,所以我们更新了 HashcodeEquals 的代码如下:

public class HashcodeEquals {

    public static void main(String[] args) {
        Student alex1 = new Student(1, "Alex");
        Student alex2 = new Student(1, "Alex");
        
        HashSet < Student > students = new HashSet < Student > ();
        students.add(alex1);
        students.add(alex2);
        
        System.out.println("HashSet size = " + students.size());
        System.out.println("HashSet contains Alex = " + students.contains(new Student(1, "Alex")));
    }
}

运行上面的测试代码,我们得到了下面的输出:

HashSet size = 2
HashSet contains Alex = false

等等!我们不是已经重载了 equals 函数了吗?而且我们也已经验证过了,alex1 和 alex2 是相等的,而且我们都知道,HashSet 中存储的是互不相同的元素,那为什么 HashSet 会认为 alex1 和 alex2 是不同的呢?

HashSet 将它内部的元素存储在内存桶中,每一个桶都与一个 hashcode 相连。当我们调用 students.add(alex1)时,Java 将 alex1 存储到一个桶中,并且将这个桶与 alex1.hashCode() 的返回值联系起来。下次如果我们往 HashSet 中插入一个对象,并且这个对象的 hashcode和 alex1 的相同,那么这个对象就会把 alex1 替换掉。然而,由于 alex2 的 hashcode 与 alex1 的 hashcode 不同,所以 alex2 会被视作一个完全不同的对象,并被存储到另外一个桶里。

当 HashSet 在它内部查找 一个元素时,它会首先生成这个元素的 hashcode,然后查找和这个 hashcode 相对应的桶。

这就体现出重载 hashCode() 函数的重要性了,所以让我们把 Student 类的 hashCode() 函数重载一下,让它返回 Student 的 ID,这样两个具有相同 ID 的学生对象就会被存储到同一个桶中了。

@Override
public int hashCode() {
    return id;
}

运行测试代码,我们得到下面的输出:

HashSet size = 1
HashSet contains Alex = true

看到 hashCode() 函数的神奇之处了吧!现在这两个元素被视作相等了,并且存到了同一个内存桶中,所以任何时候你调用contains()函数并且传递一个具有相同 hashcode 的对象参数给它,HashSet 都能够找到这个元素。

同样,hashcode 也应用于 HashMap,HashTable,以及所有使用哈希机制存储元素的数据结构中。

总结

为了实现能够彻底工作的判等机制,每次重载 equals() 函数的时候都要重载 hashCode() 函数。只要遵循下面几条建议,你自定义的判等机制就永远不会出现 Bug:

  1. 如果两个对象相等,那么他们必须要有相同的 hashcode。
  2. 如果两个对象有相同的 hashcode,并不意味着他们相等。
  3. 只重载 equals() 函数会使你的业务在哈希的数据结构中失效,比如:HashSet、HashMap、HashTable 等等。
  4. 只重载 hashCode() 函数不会使 Java 在比较两个对象的时候忽略内存地址。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值