本章是:为什么重写 equals() 方法时一定要重写 hashCode() 方法深入解析。

# equals 和 hashCode 是什么

Java 开发中每个对象都有一个默认的 equals() 方法,它比较的是对象的引用是否相等 (即:比较两个对象是否是同一个实例)。但是在实际开发中,我们通常需要比较对象的内容是否相等,而不仅仅是比较它们的引用。这就是为什么我们要重写 equals() 方法的原因。

hashCode(哈希码) 是另一个与对象相关的重要概念,哈希码是一个整数值,它是根据对象的内容计算得出的,在 Java 中哈希码主要用于散列数据结构,如:哈希表就是一种常见的数据结构。它可以快速查找存储在其中的对象,哈希码可以帮助我们确定对象在哈希表中的存储位置,从而实现高效的查找操作。

# 为什么要重写 equals 方法

在默认情况下 Java 中的 equals() 方法比较的是对象的引用,如果我们不重写 equals() 方法,那么对于两个不同的对象即使它们的内容相同,但是当调用 equals() 方法时也会返回 false , 因为他们的引用不同,如下示例:

Student.java
package top.rem.rain.override.equlals;
/**
 * @Author: LightRain
 * @Description: 学生对象
 * @DateTime: 2023-12-15 14:55
 * @Version:1.0
 **/
public class Student {
    private String name;
    private String gender;
    private int age;
    public Student(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
}
OverrideEqualsHashCodeApplicationTests.java
package top.rem.rain.override;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.rem.rain.override.equlals.Student;
@SpringBootTest
class OverrideEqualsHashCodeApplicationTests {
    @Test
    void equalsDemo1() {
        Student student1 = new Student("冈崎", "男", 17);
        Student student2 = new Student("冈崎", "男", 17);
        System.out.println("student1.equals(student2) = " + student1.equals(student2));
        /*
            执行结果:student1.equals (student2) = false
         */
    }
}

在上面这个示例中尽管 student1student2 的内容是相同的,但它们是不同的实例对象,因此 equals() 方法返回 false ,这肯定不是我们想要的结果,为了解决这个问题,我们就需要重写 equals() 方法,以便比较对象的内容而不是引用地址。通常我们会在自定义类中重写 equals() 方法以实现我们自己的相等性逻辑,比较对象的属性是否相等。

下面是重写后的 Student 类对象, IDEA 的重写快捷键是 Alt+Ins

Student.java
package top.rem.rain.override.equlals;
import java.util.Objects;
/**
 * @Author: LightRain
 * @Description: 学生对象
 * @DateTime: 2023-12-15 14:55
 * @Version:1.0
 **/
public class Student {
    private String name;
    private String gender;
    private int age;
    public Student(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return age == student.age && Objects.equals(name, student.name) && Objects.equals(gender, student.gender);
    }
    @Override
    public int hashCode() {
        return Objects.hash(name, gender, age);
    }
}
package top.rem.rain.override;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import top.rem.rain.override.equlals.Student;
@SpringBootTest
class OverrideEqualsHashCodeApplicationTests {
    @Test
    void equalsDemo1() {
        Student student1 = new Student("冈崎", "男", 17);
        Student student2 = new Student("冈崎", "男", 17);
        System.out.println("student1.equals(student2) = " + student1.equals(student2));
        /*
            执行结果:student1.equals (student2) = true
         */
    }
}

再次执行后结果就是 true 了,这样才是我们想要的内容比较结果,接下来深入分析一下 equals() 实现原理。

# 1️⃣ equals 内部原理

我们都知道 equals() 方法是超类 Object 类中的一个基本方法,用于检测一个对象是否与另一个对象相等。而在 Object 类中这个方法实际上是判断两个对象是否具有相同的引用,如果有它们就一定会相等,源码如下:

Object.java
package java.lang;
import jdk.internal.vm.annotation.IntrinsicCandidate;
public class Object {
    public boolean equals(Object obj) {
        return (this == obj);
    }
}

我们知道所有的对象都拥有标识 (内存地址) 和状态 (数据),同时 == 运算符比较的是两个对象的内存地址,所以说 Objectequals() 方法是比较两个对象的内存地址是否相等,即: object1.equals(object2)true 则表示 equals1equals2 实际上是引用的同一个对象。

# 2️⃣ equals 与 == 的区别

一般都会回答: qeuals 比较的是对象内容, == 比较的是对象地址。但从前面我们可以知道 equals() 方法在 Object 中的实现也是间接使用了 == 运算符进行比较的,从严格意义上来说,前面的回答并不完全正确,先来看下段示例代码:

People.java
package top.rem.rain.override.equlals;
/**
 * @Author: LightRain
 * @Description: 示例类
 * @DateTime: 2023-12-15 15:33
 * @Version:1.0
 **/
public class People {
    private String name;
    private String gender;
    private int age;
    public People(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
    public static void main(String[] args) {
        People people1 = new People("古河渚","女",18);
        People people2 = new People("古河渚","女",18);
        System.out.println(people1.equals(people2));
        System.out.println(people1 == people2);
        /*
          执行结果:
            false
            false
         */
    }
}

对于 == 运算符比较两个 People 对象返回了 false , 这点我们很容易就能明白,毕竟它们比较的是内存地址,而 people1people2 是两个不同的实例对象,所以 people1people2 的内存地址也不一样,现在的问题是我们希望判断实例中的内容是否相等,但 equals() 方法结果却返回了 false ,当然对于 equals() 方法返回了 false 我们也知道是怎么回事,因为 equals() 方法来自 Object 类,而我们并没有重写 equals() 方法,调用的必然是 Object 类中原始的 equals() 方法,根据前面分析我们也知道该原始 equals() 方法内部实现使用的是 == 运算符,因此我们必须在 People 类中重写 equals() 方法来实现比较内容,而不是比较内存地址,重写后的示例如下:

People.java
package top.rem.rain.override.equlals;
import java.util.Objects;
/**
 * @Author: LightRain
 * @Description: 示例类
 * @DateTime: 2023-12-15 15:33
 * @Version:1.0
 **/
public class People {
    private String name;
    private String gender;
    private int age;
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        People people = (People) o;
        return age == people.age && Objects.equals(name, people.name) && Objects.equals(gender, people.gender);
    }
    public People(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
    public static void main(String[] args) {
        People people1 = new People("古河渚","女",18);
        People people2 = new People("古河渚","女",18);
        System.out.println(people1.equals(people2));
        System.out.println(people1 == people2);
        /*
          执行结果:
            true
            false
         */
    }
}

总结:默认情况下从 Object 类中继承而来的 equals() 方法与 == 运算符是等价的,比较的是对象的内存地址,但我们可以重写 equals() 方法,使其按照我们的需求方式进行比较,而不再是比较内存地址。

# 3️⃣ equals 的重写规则

  • 注意:我们在重写 equals() 方法时,需要注意下面几项规则:
    • 自反性 :对于任何非 null 的引用值 x , x.equals(x) 应该返回 true
    • 对称性 :对于任何非 null 的引用值 xy , 当且仅当 y.equals(x) 返回 truex.equals(y) 才返回 true
    • 传递性 :对于任何非 null 的引用值 x y z , 如果 y.equals(x) 返回 truey.equals(z) 返回 true 那么 x.equals(z) 也应该返回 true
    • 一致性 :对于任何非 null 的引用值 xy , 假设对象上 equals 比较中的信息没有被修改,则多次调用 x.equals(y) 始终应该返回 true 或始终返回 false
    • 对于任何非空引用值 x , x.equals(null) 应该返回 false
  • 当然在通常情况下,如果只是进行同一个类两个对象的相等比较一般都可以满足以上五点要求,示例代码如下:
People.java
package top.rem.rain.override.equlals;
import java.util.Objects;
/**
 * @Author: LightRain
 * @Description: 示例类
 * @DateTime: 2023-12-15 15:33
 * @Version:1.0
 **/
public class People {
    private String name;
    private String gender;
    private int age;
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof People people) {
            return age == people.age && Objects.equals(name, people.name) && Objects.equals(gender, people.gender);
        }
        return false;
    }
    public People(String name, String gender, int age) {
        this.name = name;
        this.gender = gender;
        this.age = age;
    }
    public static void main(String[] args) {
        People people1 = new People("古河渚", "女", 18);
        People people2 = new People("古河渚", "女", 18);
        People people3 = new People("古河渚", "女", 18);
        System.out.println("自反性:" + people1.equals(people1));
        System.out.println("对称性:" + people1.equals(people2) + " " + people2.equals(people1));
        System.out.println("传递性:" + people1.equals(people2) + " " + people2.equals(people3) + " " + people1.equals(people3));
        System.out.print("一致性:");
        for (int i = 0; i < 50; i++) {
            if (people1.equals(people2) != people1.equals(people2)) {
                System.out.print("equals方法没有遵守一致性!");
                break;
            }
        }
        System.out.println("equals方法遵守一致性!");
        System.out.println("与null比较:" + people1.equals(null));
        /*
          执行结果:
            自反性:true
            对称性:true true
            传递性:true true true
            一致性:equals 方法遵守一致性!
            与 null 比较:false
         */
    }
}

以上结果我看可以看出 equals() 方法在同一个类的两个对象间比较还是很容易理解的。如果是子类与父类混合比较,那么情况就不太简单了,接着来看下面的示例:

package top.rem.rain.override.equlals;
import java.util.Objects;
/**
 * @Author: LightRain
 * @Description: 继承 People
 * @DateTime: 2023-12-15 16:34
 * @Version:1.0
 **/
public class FemalePeople extends People {
    /**
     * 职业
     */
    private String occupation;
    public FemalePeople(String name, String gender, int age, String occupation) {
        super(name, gender, age);
        this.occupation = occupation;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof FemalePeople fp) {
            return super.equals(fp) && occupation == fp.occupation;
        }
        return false;
    }
    public static void main(String[] args) {
        People people1 = new People("古河渚", "女", 18);
        FemalePeople femalePeople1 = new FemalePeople("古河渚", "女", 18, "学生");
        FemalePeople femalePeople2 = new FemalePeople("杏", "女", 16, "学生");
        System.out.println(people1.equals(femalePeople1));
        System.out.println(femalePeople1.equals(people1));
        System.out.println(people1.equals(femalePeople2));
        /*
          执行结果:
            true
            false
            false
         */
    }
}

当然这个结果也是我们意料之中的,因为 FemalePeople 类型是属于 People 类型,因此 people1.equals(femalePeople1) 肯定为 true , 对于 femalePeople1.equals(people1) 返回 false 是因为 People 类型不一定是 FemalePeople 类型 ( People 还可以有其它子类),但是如果有这样一个需求,只要 FemalePeoplePeople 的姓名、性别、年龄一样我们就认为他们两个是相同的,在这样一种情况下的需求: People(父类)FemalePeople(子类) 的混合比较就不符合 equals() 方法的对称特性了。显然一个返回 true , 一个返回 false , 根据 equals 的对称特性此时两次比较都应该返回 true 才对,如何修改才符合对称特性呢?造成不符合对称性的原因是因为 People 类型不一定是 FemalePeople 类型,在这种情况下我们不应该在 equals() 方法中直接返回 false ,而应该继续使用父类的 equals() 方法进行比较,修改后的代码如下:

FemalePeople.java
package top.rem.rain.override.equlals;
/**
 * @Author: LightRain
 * @Description: 继承 People
 * @DateTime: 2023-12-15 16:34
 * @Version:1.0
 **/
public class FemalePeople extends People {
    /**
     * 职业
     */
    private String occupation;
    public FemalePeople(String name, String gender, int age, String occupation) {
        super(name, gender, age);
        this.occupation = occupation;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof FemalePeople fp) {
            return super.equals(fp) && occupation == fp.occupation;
        }
        return super.equals(o);
    }
    public static void main(String[] args) {
        People people1 = new People("古河渚", "女", 18);
        FemalePeople femalePeople1 = new FemalePeople("古河渚", "女", 18, "学生");
        FemalePeople femalePeople2 = new FemalePeople("杏", "女", 16, "学生");
        FemalePeople femalePeople3 = new FemalePeople("杏", "女", 16, "老师");
        System.out.println(people1.equals(femalePeople1));
        System.out.println(femalePeople1.equals(people1));
        System.out.println(people1.equals(femalePeople2));
        System.out.println(femalePeople2.equals(femalePeople3));
        /*
          执行结果:
            true
            true
            false
            false
         */
    }
}
  • 虽然上面代码现在符合了对称性,但还是不符合传递性,按我们之前的需求应该是相等,而且也应该符合 equals 的传递性才对,事实上执行结果却不是这样,出现违背传递性根本原因是:
    • 父类与子类混合比较。
    • 子类中声明了新变量,并且在子类 equals() 方法使用了新增的成员变量作为判断对象是否相等的条件。
  • 只要满足上面两个条件 equals() 方法的传递性便失效了,下面是重新修改后的代码,这样就解决了传递性。
FemalePeople.java
package top.rem.rain.override.equlals;
/**
 * @Author: LightRain
 * @Description: 继承 People
 * @DateTime: 2023-12-15 16:34
 * @Version:1.0
 **/
public class FemalePeople extends People {
    /**
     * 职业
     */
    private String occupation;
    public FemalePeople(String name, String gender, int age, String occupation) {
        super(name, gender, age);
        this.occupation = occupation;
    }
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o instanceof FemalePeople fp) {
            if (super.equals(fp) && occupation == fp.occupation){
               return super.equals(fp) && occupation == fp.occupation;
            }else {
                return super.equals(o);
            }
        }
        return super.equals(o);
    }
    public static void main(String[] args) {
        People people1 = new People("古河渚", "女", 18);
        FemalePeople femalePeople1 = new FemalePeople("古河渚", "女", 18, "学生");
        FemalePeople femalePeople2 = new FemalePeople("杏", "女", 16, "学生");
        FemalePeople femalePeople3 = new FemalePeople("杏", "女", 16, "老师");
        FemalePeople femalePeople4 = new FemalePeople("椋", "女", 16, "老师");
        System.out.println(people1.equals(femalePeople1));
        System.out.println(femalePeople1.equals(people1));
        System.out.println(people1.equals(femalePeople2));
        System.out.println(femalePeople2.equals(femalePeople3));
        System.out.println(femalePeople4.equals(femalePeople2));
        /*
          执行结果:
            true
            true
            false
            true
            false
         */
    }
}

# 为什么要重写 hashCode 方法

在重写了 equals() 方法后为什么还需要重写 hashCode() 方法呢?这是因为在使用散列数据结构时,如:哈希表,我们希望相等的对象具有相等的哈希码。在 Java 中哈希表使用哈希码来确定存储对象的位置。如果:两个相等的对象具有不同的哈希码,那么它们将被存储在哈希表的不同位置,导致无法正确查找对象。

Demo.java
package top.rem.rain.override.equlals;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
 * @Author: LightRain
 * @Description: 重写 hashCOde Vs 不重写 hashCode
 * @DateTime: 2023-12-15 19:22
 * @Version:1.0
 **/
public class Demo {
    /**
     * 只重写 equals 方法未重写 hashCode
     */
    private static class Student1 {
        private String name;
        private String gender;
        private int age;
        public Student1(String name, String gender, int age) {
            this.name = name;
            this.gender = gender;
            this.age = age;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student1 student1 = (Student1) o;
            return age == student1.age && Objects.equals(name, student1.name) && Objects.equals(gender, student1.gender);
        }
    }
    /**
     * 重写 equals 和 hashCode 方法
     */
    private static class Student2 {
        private String name;
        private String gender;
        private int age;
        public Student2(String name, String gender, int age) {
            this.name = name;
            this.gender = gender;
            this.age = age;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (o == null || getClass() != o.getClass()) return false;
            Student2 student2 = (Student2) o;
            return age == student2.age && Objects.equals(name, student2.name) && Objects.equals(gender, student2.gender);
        }
        @Override
        public int hashCode() {
            return Objects.hash(name, gender, age);
        }
    }
    public static void main(String[] args) {
        Student1 student1 = new Student1("冈崎", "男", 17);
        Student1 student2 = new Student1("冈崎", "男", 17);
        Set<Student1> set1 = new HashSet<>();
        set1.add(student1);
        System.out.println("只重写了equals方法:" + set1.contains(student2));
        System.out.println("--------------------------------------------------------");
        Student2 student3 = new Student2("古河渚", "女", 18);
        Student2 student4 = new Student2("古河渚", "女", 18);
        Set<Student2> set2 = new HashSet<>();
        set2.add(student3);
        System.out.println("重写了equals和hashCode方法:" + set2.contains(student4));
        /*
          执行方法:
            只重写了 equals 方法:false 
            --------------------------------------------------------
            重写了 equals 和 hashCode 方法:true
         */
    }
}

在上面代码示例中, Student1 类中未重写 hashCode() 方法,尽管 student1student2 内容相同,但由于它们具有不同的哈希码,在使用 set1.contains(student2) 时返回 false , 这是说明哈希表无法正确定位到 student2 。而 Student2 类中重写了 hashCode() 方法,在使用 set2.contains(student4) 时哈希表就可以正确定位到 student4 ,因为它们两个的哈希码是相同的,这就是为什么我们需要确保重写 equals() 方法时为什么也要重写 hashCode() 方法的原因,以便它们的哈希码是相同的,这样哈希表就能正确地存储和查找这些对象了。

# 1️⃣ hashCode 的重写规则

  • 注意:我们在重写 hashCode() 方法时,需要注意下面几项规则:
    • 如果:两个对象通过 equals() 相等,那么它们的哈希码必须相等。
    • hashCode() 方法的计算应该是高效的,避免复杂的计算。
    • hashCode() 方法的结果应该在对象的生命周期内保持不变,如果:一个对象的内容发生了变化,它的哈希码也一个保持不变。
    • 对于不相等的对象,哈希码尽量不要相等,以提高哈希表的性能。
  • 为了遵守这些规则,通常我们可以使用对象的属性来计算哈希码,比如使用属性的哈希码相加或异或来得到对象的哈希码。

总结:为了保证对象的相等性和哈希表的准确性,我们需要在重写 equals() 方法时一起重写 hashCode() 方法。这两个方法是密切相关的,它们一起确保对象在使用散列数据结构时能够正确工作。当你在面试中遇到这个问题时,不要忘记强调 equalshashCode 方法的一致性和性能,以及遵守重写 hashCode 方法的规则。这将帮助你深刻理解这个重要的概念,并在实际开发中正确地使用它们。