首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >== 和 equals() 的区别?重写 equals() 为什么必须重写 hashCode()?

== 和 equals() 的区别?重写 equals() 为什么必须重写 hashCode()?

原创
作者头像
用户11956557
发布2026-01-16 15:50:37
发布2026-01-16 15:50:37
1780
举报
文章被收录于专栏:java知识java知识

在 Java 开发中,比较对象或值是否相等是高频操作,而 ==equals() 是两种核心比较方式。很多开发者初期会混淆二者的用法,尤其在自定义对象中重写 equals() 后,常常忽略 hashCode() 的重写,最终引发潜在 Bug。本文将从“二者核心区别”“equals() 底层原理”“重写配对的原因”三个维度,帮你彻底理清逻辑,规避开发陷阱。

一、==:值比较与引用比较的双重逻辑

== 是 Java 中的运算符,其比较逻辑根据操作数的类型不同而有所差异,核心分为“基本数据类型”和“引用数据类型”两类场景:

1. 基本数据类型:直接比较值

对于 byte、short、int、long、float、double、char、boolean 这 8 种基本数据类型,== 直接比较变量存储的“具体数值”,与内存地址无关。因为基本类型的值存储在栈中(局部变量)或堆中(成员变量),不存在“引用”概念,比较逻辑直观。

代码语言:java
复制
public class CompareDemo {
    public static void main(String[] args) {
        int a = 100;
        int b = 100;
        byte c = 100;
        // 基本类型比较值,100 == 100,结果为 true
        System.out.println(a == b); // true
        // int 和 byte 自动类型转换后值相等,结果为 true
        System.out.println(a == c); // true
    }
}

2. 引用数据类型:比较对象内存地址

对于类、接口、数组等引用类型,变量存储的是“对象在堆内存中的地址引用”,而非对象本身。此时 == 比较的是两个变量是否指向同一个对象(即地址是否相同),而非对象的内容是否相等。

代码语言:java
复制
public class CompareDemo {
    public static void main(String[] args) {
        String s1 = new String("Java");
        String s2 = new String("Java");
        String s3 = s1;
        // s1 和 s2 是两个不同对象,地址不同,结果为 false
        System.out.println(s1 == s2); // false
        // s3 指向 s1 对应的对象,地址相同,结果为 true
        System.out.println(s1 == s3); // true
    }
}

这里需要注意 String 常量池的特殊情况:若通过String s4 = "Java" 方式创建对象,会优先从常量池获取实例,此时 s4 == s1 仍为 false(s1 是堆中新建对象),但 s4 == "Java" 为 true(复用常量池实例),本质还是地址比较。

二、equals():从“地址比较”到“内容比较”的演变

equals()java.lang.Object 类中的方法,所有 Java 类都默认继承该方法,其核心逻辑与 == 绑定,但可通过重写实现自定义比较规则。

1. 未重写时:等价于 ==

Object 类中 equals() 的源码如下,可见其本质就是通过== 比较对象地址,即判断两个变量是否指向同一个对象。

代码语言:java
复制
public boolean equals(Object obj) {
    return (this == obj);
}

这意味着,若自定义类未重写 equals(),调用该方法时,效果与 == 完全一致,比较的是对象地址。

2. 重写后:自定义内容比较

实际开发中,我们往往需要判断“两个对象的内容是否相等”(而非是否是同一个对象),此时就需要重写 equals() 方法。例如 String、Integer 等 JDK 内置类,都已重写 equals(),实现了基于内容的比较。

以 String 类为例,其重写的 equals() 会逐字符比较字符串内容,只有所有字符都相同,才返回 true:

代码语言:java
复制
// String 类重写的 equals() 核心逻辑(简化版)
public boolean equals(Object anObject) {
    if (this == anObject) {
        return true; // 地址相同直接返回 true,优化性能
    }
    if (anObject instanceof String) {
        String anotherString = (String) anObject;
        int n = value.length;
        if (n == anotherString.value.length) {
            char v1[] = value;
            char v2[] = anotherString.value;
            int i = 0;
            while (n-- != 0) { // 逐字符比较内容
                if (v1[i] != v2[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

自定义类重写 equals() 的示例(以 User 类为例,判断 id 和 name 都相同则视为相等):

代码语言:java
复制
public class User {
    private Integer id;
    private String name;

    // 构造方法、getter/setter 省略

    @Override
    public boolean equals(Object o) {
        if (this == o) return true; // 地址相同,直接返回 true
        if (o == null || getClass() != o.getClass()) return false; // 空判断、类型判断
        User user = (User) o;
        // 自定义比较规则:id 和 name 都相等则视为对象相等
        return Objects.equals(id, user.id) && Objects.equals(name, user.name);
    }
}

这里用到 Objects.equals() 而非直接 ==,是为了避免空指针异常(若 id 为 null,直接调用 id.equals() 会报错)。

三、核心问题:重写 equals() 为什么必须重写 hashCode()?

这是面试高频考点,核心原因是要遵守 equals()hashCode() 的“通用约定”——该约定是 Java 集合框架(如 HashMap、HashSet)正常工作的基础,若违反约定,会导致集合操作出现异常。

1. 先明确:hashCode() 的作用

hashCode() 也是 Object 类的方法,返回一个 int 类型的哈希值,其核心作用是“为集合提供快速定位依据”。例如 HashMap 中,会先通过对象的哈希值确定存储位置(桶位),再通过 equals() 精确比较,从而大幅提升查询效率。

2. 关键约定:equals() 相等 → hashCode() 必须相等

Java 官方对 equals()hashCode() 制定了以下核心约定(缺一不可):

  • 约定 1:若两个对象通过 equals() 比较相等,则它们的hashCode() 必须返回相同的值。
  • 约定 2:若两个对象的 hashCode() 返回相同的值,它们的 equals() 不一定相等(哈希冲突场景)。
  • 约定 3:若两个对象通过equals() 比较不相等,它们的 hashCode() 可以相同,也可以不同(但不同能减少哈希冲突,提升集合效率)。

3. 不重写 hashCode() 的致命后果

若只重写 equals() 而不重写 hashCode(),会违反“约定 1”,导致依赖哈希值的集合(HashMap、HashSet 等)出现逻辑错误。

举个反例(基于上文 User 类,仅重写 equals() 未重写 hashCode()):

代码语言:java
复制
public class HashCodeDemo {
    public static void main(String[] args) {
        User u1 = new User(1, "张三");
        User u2 = new User(1, "张三");

        // equals() 重写后,内容相等,返回 true
        System.out.println(u1.equals(u2)); // true

        // 未重写 hashCode(),默认返回对象地址对应的哈希值,u1 和 u2 地址不同,哈希值不同
        System.out.println(u1.hashCode()); // 123456(示例值)
        System.out.println(u2.hashCode()); // 654321(示例值)

        // 将两个对象放入 HashSet(HashSet 不允许重复元素,依赖 hashCode() 和 equals())
        Set&lt;User&gt; set = new HashSet<>();
        set.add(u1);
        set.add(u2);

        // 预期:set 中只有 1 个元素(u1 和 u2 相等)
        // 实际:set 中有 2 个元素(哈希值不同,被视为两个不同元素)
        System.out.println(set.size()); // 2
    }

出现该问题的原因的是:HashSet 添加元素时,先通过 hashCode() 计算存储位置,若该位置无元素,则直接存入;若有元素,再通过 equals() 比较是否重复。本例中 u1 和 u2 的哈希值不同,被分配到不同位置,即使 equals() 相等,也被视为两个不同元素,违背了 HashSet“去重”的核心逻辑。

4. 正确做法:重写 equals() 时同步重写 hashCode()

重写 hashCode() 时,需保证“equals() 相等的对象,哈希值一定相等”,通常会基于 equals() 中用到的字段(如 User 类的 id、name)计算哈希值,确保逻辑一致。

手动重写 hashCode() 需注意哈希值的均匀性(减少冲突),推荐使用 Objects.hash()方法(JDK 7+),它会自动处理字段空值,且能基于多个字段生成哈希值,简洁又安全。

完善 User 类,同步重写 equals() 和 hashCode():

代码语言:java
复制
import java.util.Objects;

public class User {
    private Integer id;
    private String name;

    // 构造方法、getter/setter 省略

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        // 基于 id 和 name 比较内容
        return Objects.equals(id, user.id) && Objects.equals(name, user.name);
    }

    @Override
    public int hashCode() {
        // 基于 id 和 name 生成哈希值,与 equals() 逻辑一致
        return Objects.hash(id, name);
    }
}

此时再执行上文 HashSet 示例,u1 和 u2 的哈希值相同,且 equals() 相等,HashSet 会判定为重复元素,最终 set 大小为 1,符合预期。

四、常见误区与使用建议

1. 误区 1:equals() 相等,hashCode() 却不同

这是最常见的错误,本质是违反约定,会导致集合操作异常,解决方式就是同步重写两个方法,基于相同字段计算逻辑。

2. 误区 2:hashCode() 相等,就认为 equals() 一定相等

哈希冲突是不可避免的(不同对象可能生成相同哈希值),因此 hashCode() 相等仅代表“可能相等”,最终需通过 equals() 确认,这也是 HashMap 等集合的核心工作逻辑。

3. 误区 3:重写 hashCode() 时使用随机值

若 hashCode() 返回随机值,会导致同一对象每次调用 hashCode() 结果不同,放入 HashMap 后无法被正常查询(定位不到原存储位置),需基于对象的稳定字段(如 id、name)生成哈希值。

4. 使用建议

  • 比较基本类型:用 ==,直接比较值,高效且无歧义。
  • 比较引用类型:优先用 equals(),若需判断“是否是同一个对象”,再用 ==
  • 自定义类重写:必须同时重写 equals()hashCode(),且基于相同字段逻辑实现,推荐用 Objects 工具类简化代码。
  • 集合场景:使用 HashMap、HashSet 等哈希集合时,务必保证对象的 equals()hashCode() 符合约定,否则会出现添加重复元素、查询不到元素等问题。

总结

  1. == 是运算符,基本类型比“值”,引用类型比“地址”;equals() 是方法,未重写时等价于 ==,重写后可实现“内容比较”。
  2. 重写 equals() 必须重写 hashCode(),核心是遵守 Java 官方约定,确保“equals() 相等的对象,hashCode() 必相等”,否则会导致哈希集合逻辑异常。
  3. 实际开发中,推荐用 Objects.equals()Objects.hash() 重写,兼顾空指针安全、逻辑一致性和哈希均匀性,规避常见陷阱。(注:文档部分内容可能由 AI 生成)

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、==:值比较与引用比较的双重逻辑
    • 1. 基本数据类型:直接比较值
    • 2. 引用数据类型:比较对象内存地址
  • 二、equals():从“地址比较”到“内容比较”的演变
    • 1. 未重写时:等价于 ==
    • 2. 重写后:自定义内容比较
  • 三、核心问题:重写 equals() 为什么必须重写 hashCode()?
    • 1. 先明确:hashCode() 的作用
    • 2. 关键约定:equals() 相等 → hashCode() 必须相等
    • 3. 不重写 hashCode() 的致命后果
    • 4. 正确做法:重写 equals() 时同步重写 hashCode()
  • 四、常见误区与使用建议
    • 1. 误区 1:equals() 相等,hashCode() 却不同
    • 2. 误区 2:hashCode() 相等,就认为 equals() 一定相等
    • 3. 误区 3:重写 hashCode() 时使用随机值
    • 4. 使用建议
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档