臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 核心区别

一句话记忆

== 看是不是同一个对象,equals() 看两个对象是否符合类定义的“相等”规则。

比较方式基本类型引用类型
==比较数值比较是不是同一个对象
equals()不能直接使用根据类中定义的规则比较

二、⚙️ 简单示例

2.1 基本类型

int a = 10;
int b = 10;
 
System.out.println(a == b); // true,数值相同

2.2 对象类型

String a = new String("Java");
String b = new String("Java");
 
System.out.println(a == b);      // false,不是同一个对象
System.out.println(a.equals(b)); // true,字符串内容相同

可以把它理解为:

  • == 问:这是同一个人吗?
  • equals() 问:这两个人的信息是否符合“相同”的标准?

三、🧭 容易出错的地方

3.1 equals() 不一定比较内容

equals() 的比较规则由类决定。

如果自定义类没有重写 equals(),它会继承 Object.equals(),默认效果和 == 一样,仍然比较是不是同一个对象。

所以更准确的说法是:

equals() 通常用来比较内容,但最终要看这个类是否重写了它。

3.2 hashCode() 是什么

hashCode() 是 Object 类定义的方法,每个 Java 对象都可以调用它。它会返回一个 int 整数,这个整数叫作哈希码。

可以先把哈希码理解成:

白话理解

hashCode() 是对象的“快速分组编号”。HashMap 和 HashSet 先根据这个编号缩小查找范围,再用 equals() 确认是不是真正相等的对象。

hashCode() 有两个容易混淆的点:

  • 它不是必须唯一的对象 ID;不同对象可以有相同的哈希码,这叫作哈希冲突。
  • 不要把它简单理解成对象的内存地址;Java 官方契约只规定它的行为,没有规定必须返回内存地址。

3.2.1 哈希冲突是什么

**哈希冲突(Hash Collision)**是指:两个不同的对象,通过 hashCode() 计算后得到了相同的哈希码。

例如,Java 中的字符串 "Aa" 和 "BB" 内容不同,但哈希码相同:

String a = "Aa";
String b = "BB";
 
System.out.println(a.equals(b));                  // false,内容不同
System.out.println(a.hashCode());                 // 2112
System.out.println(b.hashCode());                 // 2112
System.out.println(a.hashCode() == b.hashCode()); // true

这就是:

"Aa" ──hashCode()──→ 2112
"BB" ──hashCode()──→ 2112

为什么哈希码不能保证唯一?因为 hashCode() 只能返回有限范围的 int 整数,而 Java 可以创建的对象数量远远更多。对象足够多时,不同对象就可能拿到同一个哈希码。

HashMap 遇到哈希冲突时,不会立即认为两个 Key 相等,而是继续使用 equals() 确认:

System.out.println(a.equals(b)); // false

因为 equals() 返回 false,两个 Key 会被作为两条不同的数据保存:

Map<String, Integer> map = new HashMap<>();
map.put("Aa", 100);
map.put("BB", 200);
 
System.out.println(map.get("Aa")); // 100
System.out.println(map.get("BB")); // 200

哈希冲突不等于数据覆盖

hashCode() 相同只表示“分到了同一组”。HashMap 还会使用 equals() 进一步确认。只有 equals() 也返回 true 时,才会被当成同一个 Key。

3.3 HashMap 为什么同时需要 hashCode() 和 equals()

假设需要从 HashMap 中查找一个 Key,可以把查找过程简化成两步:

第一步:调用 key.hashCode(),计算应该去哪个桶里找
                        ↓
第二步:在该桶内使用 equals(),确认具体是哪个 Key

两个方法的分工是:

方法作用类比
hashCode()快速确定查找范围先确定快递在哪一个货架
equals()在该范围内判断对象是否真正相等再根据收件人信息确认具体包裹

只使用 equals() 逐个比较所有 Key 也能查找,但数据很多时会很慢。hashCode() 负责快速缩小范围,equals() 负责处理哈希冲突并最终确认。可结合 ../基础/1.HashMap 理解完整过程。

3.4 为什么重写 equals() 也要重写 hashCode()

Java 官方规定了一条重要契约:

必须遵守的契约

如果 a.equals(b) 返回 true,那么 a.hashCode() 和 b.hashCode() 必须返回相同的整数。

假设我们认为学号相同的学生就是同一个学生:

Student a = new Student("1001", "小明");
Student b = new Student("1001", "小明");

a 和 b 是两个不同的对象,但按照业务规则,a.equals(b) 应该为 true。

如果只重写 equals(),没有重写 hashCode(),它们可能得到不同的哈希码:

a ──hashCode──→ 1 号查找范围
b ──hashCode──→ 8 号查找范围

HashMap 查找 b 时可能会去 8 号范围,而之前使用 a 存入的数据在 1 号范围。它们根本没有机会进一步调用 equals() 比较,因此可能出现:

  • HashMap.get(b) 找不到使用 a 存入的数据;
  • HashSet 同时保存两个业务上相等的对象。

所以,只要修改了“什么叫对象相等”的规则,就必须同时修改“相等对象应该分到哪一类”的规则。

3.4.1 面试回答

标准回答:

Java 规定:如果两个对象通过 equals() 判断相等,那么它们的 hashCode() 必须相同。

因为 HashMap 和 HashSet 会先根据 hashCode() 确定查找范围,只有进入同一范围后,才会通过 equals() 判断对象是否真正相等。

如果只重写 equals(),两个逻辑上相等的对象可能拥有不同的哈希码,被分到不同位置,导致 HashMap 查找失败,或者 HashSet 保存重复元素。所以修改对象的相等规则时,也要使用相同的业务字段重写 hashCode()。

精简背诵版:

equals() 决定两个对象是否相等,hashCode() 决定对象在哈希容器中的查找位置。相等对象必须拥有相同的哈希码,否则 HashMap 和 HashSet 可能无法正确识别它们。

如果面试官追问“Java 是否强制”:

编译器不会强制同时重写,但只重写 equals() 会破坏 Object 规定的契约。当对象作为 HashMap 的 Key 或放入 HashSet 时,就可能出现问题。对象仅作为 HashMap 的 Value 时不一定立即出错,但工程上仍应遵守契约,同时重写两个方法。

最后补充一句

equals() 相等,hashCode() 必须相同;hashCode() 相同,equals() 不一定相等,因为可能发生哈希冲突。

3.5 正确的重写方式

equals() 和 hashCode() 应该使用同一组业务字段。下面的例子使用 studentNo 判断学生是否相等:

import java.util.Objects;
 
public class Student {
    private final String studentNo;
    private final String name;
 
    public Student(String studentNo, String name) {
        this.studentNo = studentNo;
        this.name = name;
    }
 
    @Override
    public boolean equals(Object obj) {
        // 两个引用指向同一个对象时,一定相等。
        if (this == obj) {
            return true;
        }
 
        // null 或运行时类型不同时,不相等。
        if (obj == null || getClass() != obj.getClass()) {
            return false;
        }
 
        Student other = (Student) obj;
 
        // 业务规则:学号相同,就认为是同一个学生。
        return Objects.equals(studentNo, other.studentNo);
    }
 
    @Override
    public int hashCode() {
        // 使用与 equals 相同的 studentNo 计算哈希码。
        return Objects.hash(studentNo);
    }
}

3.5.1 调用 a.equals(b) 时,栈和堆里有什么

先使用两个学号内容相同、姓名不同的对象:

Student a = new Student(new String("1001"), "小明");
Student b = new Student(new String("1001"), "小红");
 
boolean result = a.equals(b);

示例说明

这里故意使用两次 new String("1001"),用来证明两个学号字符串即使不是同一个对象,只要内容相同,Objects.equals() 仍会返回 true。实际代码中不需要主动 new String()。

创建完成后,可以把它们的引用关系简化为:

当前线程的栈                                      堆
 
┌─ main 方法的栈帧 ─────────────────┐         ┌─ Student 对象 A ────────────┐
│ a:保存对象 A 的引用 ────────────────┼──────→ │ studentNo ──→ String 对象 1:"1001" │
│ b:保存对象 B 的引用 ────────────────┼──┐    │ name      ──→ String 对象:"小明"   │
│ result:等待 equals 的返回值         │  │    └─────────────────────────────┘
└──────────────────────────────────────┘  │
                                            │    ┌─ Student 对象 B ────────────┐
                                            └───→ │ studentNo ──→ String 对象 2:"1001" │
                                                   │ name      ──→ String 对象:"小红"   │
                                                   └─────────────────────────────┘

这幅图中最需要注意的是:

  • 栈帧中的 a 和 b 保存的是引用;
  • 两个 Student 对象位于堆中;
  • studentNo 和 name 是 Student 对象的实例字段,它们保存的也是对应 String 对象的引用。

关于栈和堆的图

这是用于理解引用关系的简化 JVM 模型。JIT 编译器在实际运行时可能做逃逸分析等优化,但不改变这段 Java 代码的语义。

3.5.2 进入 equals() 方法后的栈帧

执行:

a.equals(b);

可以把它理解为:

使用对象 a 调用 equals,所以 this 指向对象 A
把 b 作为参数传入,所以 obj 指向对象 B

JVM 会为这次 equals() 调用创建新的方法栈帧。执行到 Student other = (Student) obj; 后,引用关系可以简化为:

当前线程的栈                                      堆
 
┌─ equals 方法的栈帧 ────────────────┐
│ this  ──────────────────────────────────┼──────→ Student 对象 A
│ obj    ──┐                                  │
│ other  ──┼──────────────────────────────────┼──────→ Student 对象 B
│          └─ obj 和 other 指向同一对象       │
└───────────────────────────────────────┘
 
┌─ main 方法的栈帧 ──────────────────┐
│ a      ──────────────────────────────────┼──────→ Student 对象 A
│ b      ──────────────────────────────────┼──────→ Student 对象 B
│ result ── 等待 equals 返回                    │
└───────────────────────────────────────┘

Student other = (Student) obj; 没有创建新对象。它只是把 obj 中的引用复制给局部变量 other,所以 obj、other 和 b 都指向堆中的对象 B。

3.5.3 equals() 为什么最后返回 true

现在按顺序执行 equals() 中的每一段代码。

第一步:比较是否为同一个对象

if (this == obj) {
    return true;
}

this 指向对象 A,obj 指向对象 B,所以:

this == obj → false

这里只是说它们不是同一个 new 出来的对象,不代表业务上一定不相等,所以继续往下判断。

第二步:排除 null 和类型不同的情况

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

obj 指向对象 B,不是 null;A 和 B 的运行时类型都是 Student,所以这个条件也是 false,继续执行。

第三步:转换类型,复制引用

Student other = (Student) obj;

此时 other 和 obj 都指向对象 B。

第四步:比较两个对象的学号

return Objects.equals(studentNo, other.studentNo);

这里没写 this.studentNo,但在实例方法中,studentNo 就是 this.studentNo。因此实际比较的是:

Objects.equals(this.studentNo, other.studentNo);

对应到堆中的对象:

this.studentNo  ──→ String 对象 1:"1001"
other.studentNo ──→ String 对象 2:"1001"

两个 String 对象不是同一对象,但 String.equals() 会比较字符串内容。它们的内容都是 "1001",所以:

Objects.equals(this.studentNo, other.studentNo)
→ "1001".equals("1001")
→ true

最后,equals() 把 true 返回给 main 方法,存入 result:

result = true

姓名 name 一个是“小明”,一个是“小红”,但这段 equals() 没有比较 name,因此姓名不会影响本次结果。这就是“由类自己定义什么叫相等”。

对于这个类:

Student a = new Student("1001", "小明");
Student b = new Student("1001", "小明");
 
System.out.println(a.equals(b));                 // true
System.out.println(a.hashCode() == b.hashCode()); // true

关系要单向记忆

  • equals() 相等 → hashCode() 必须相同。
  • hashCode() 相同 → equals() 不一定相等,因为可能发生哈希冲突。

3.6 注意空指针

下面的代码会抛出 NullPointerException:

String name = null;
name.equals("Java");

可以使用 Objects.equals():

import java.util.Objects;
 
String name = null;
System.out.println(Objects.equals(name, "Java")); // false

四、🚀 面试回答

对基本类型,== 比较数值;对引用类型,== 比较两个引用是否指向同一个对象。equals() 是对象的方法,比较规则可以被子类重写。没有重写时,默认效果和引用类型的 == 相同;例如 String 重写了 equals(),所以比较的是字符串内容。重写 equals() 时通常也要重写 hashCode()。

五、📌 快速回顾

  • 基本类型的 ==:比较值。
  • 引用类型的 ==:比较是不是同一个对象。
  • equals():比较规则由类决定。
  • 自定义类没有重写 equals():默认仍然比较对象身份。
  • 重写 equals():通常也要重写 hashCode()。
  • 对象可能为 null:可以使用 Objects.equals()。

六、🔗 官方资料