百度面试有个经典环节:白板手写equals。
看起来简单,可十个人里有六个,写出来能编译,放进 HashMap 却怎么都get不出来。面试官扫一眼就知道了——你只重写了equals,忘了hashCode。
这道题考的不是 API,是你对"对象相等"这件事在 JVM 和散列表里到底怎么运作的理解。今天拆成五组,把约定、坑、底层一次讲透。
一、== 到底比的是什么
面试官:先说==和equals区别?
候选人(标准答法):==比地址,equals比值。
深度解析:这句话对,但得拆开。
- 基本类型(
int、long、boolean…):==比的是值本身。 - 引用类型:
==比的是栈里存的引用是否指向同一个堆对象(即内存地址相同),与内容无关。
equals是Object的方法,默认实现就是==:
public boolean equals(Object obj) { return (this == obj); }所以不重写的话,equals和==完全等价。String 之所以能比值,是因为它重写了equals:先比地址,再比长度,再逐字符比。
普通答法 vs 高分答法:
- 普通:== 比地址,equals 比值。
- 高分:我会强调"对基本类型 == 比值、对引用类型 == 比地址",并点出"Object.equals 默认就是 =="这个事实。顺带补:Kotlin 的
===才是 Java 的==(比地址),==在 Kotlin 里自动调用equals还可空安全——这层语言差异一提,考官知道你跨语言有体感。
二、equals 与 hashCode 的硬约定
本篇第一道核心问答,是整个散列体系的基石。
面试官:重写 equals 时,hashCode 也要重写,为什么?
深度解析:这是Object类的契约(Java 语言规范明写):
如果两个对象根据equals(Object)相等,那么调用这两个对象的hashCode()必须产生相同的整数结果。
注意反向不成立:hashCode 相同,equals 不一定相等(哈希碰撞)。
为什么有这条约定?因为hashCode的唯一使命是服务散列表(HashMap、HashSet、Hashtable)。约定保证了"相等的对象必须落在同一个桶里",否则基于 equals 的查找就崩了。
高分答法:我会把"契约"二字拎出来——这不是建议,是规范。违反它不会编译报错,但会在运行时埋雷,而且雷只在你把对象塞进 HashSet/HashMap 时才爆。这种"编译通过、运行抽风"的 bug 最难查。
三、只重写 equals 不重写 hashCode 会怎样
本篇最关键的深挖,也是白板题翻车点。
面试官:那我只重写 equals,不写 hashCode,放 HashMap 会出啥事?
深度解析:灾难性的"存得进、取不出"。看代码:
data class User(val id: Int, val name: String) { override fun equals(other: Any?): Boolean { // 只重写了 equals if (other !is User) return false return id == other.id } // 没重写 hashCode! } val map = HashMap<User, String>() val u1 = User(1, "张三") map[u1] = "vip" println(map[u1]) // 大概率 "vip"?还是 null?如果User没重写hashCode,它继承自Object的默认实现——基于对象内存地址的身份哈希。问题来了:
1.put(u1)时,用u1的地址哈希算桶,存进去。
2. 你哪怕 new 一个u2 = User(1,"张三"),u2.equals(u1)为 true(因为重写了 equals),但u2.hashCode()是 u2 的地址哈希,和 u1 不同。
3.map[u2]去另一个桶找,找不到 → 返回 null。
更隐蔽:就算用同一个u1去get,表面看没问题;但只要中间有任何环节用了"内容相等但引用不同"的对象做 key,就彻底失联。
普通答法 vs 高分答法:
- 普通:会出问题,取不出来。
- 高分:我会完整复述"put 用原 hashCode 入桶、get 用 key 的 hashCode 找桶,两个对象 equals 相等但 hashCode 不同就落不同桶"的链路,并点出默认
hashCode是身份哈希。这条讲清楚,面试官就知道你真 debug 过。
四、HashMap 为什么用 hashCode 定位桶
本篇第三道核心问答,横向拉到容器底层。
面试官:HashMap 为啥非得用 hashCode 定位,直接 equals 比不行吗?
深度解析:因为要 O(1)。
HashMap 底层是数组 + 链表/红黑树。存元素时:
1. 算key.hashCode();
2. 经过扰动函数(Java 8:(h = key.hashCode()) ^ (h >>> 16))打散高位;
3. 与数组长度取模定位桶:(n - 1) & hash。
这样无论表里有 1 个还是 10 万个元素,定位桶都是一次位运算,常数时间。如果不用哈希、纯靠equals遍历比,那就是 O(n),散列表失去意义。
// Java 8 HashMap.putVal 关键一步 if ((p = tab[i = (n - 1) & hash]) == null) // 直接位运算定位 tab[i] = newNode(hash, key, value, null);补充:hashCode 分布越均匀,碰撞越少,链表越短,查询越快。这就是为什么好的hashCode要让字段都参与运算(IDE 自动生成或Objects.hash(id, name)就是干这个)。
高分答法:我会提扰动函数的作用——只取 hashCode 低位会和数组长度耦合,高位信息浪费,扰动把高位搅进低位,减少碰撞。这展示你不止会用 Map,还看过putVal源码。
五、开放追问:重写的坑与最佳实践
面试官:那实际项目里重写,有啥坑?
深度解析:两个高频雷区。
雷区一:用可变字段做 equals/hashCode。
val s = mutableSetOf(User(1, "张三")) users.add(u) u.name = "李四" // 改了参与 hashCode 的字段 println(users.contains(u)) // false!元素"丢失",因为它在新的桶里HashSet 里元素的 hashCode 变了的瞬间,它就既不在旧桶也不在新桶的"逻辑位置",集合彻底找不到它,还造成内存泄漏。
雷区二:Kotlin 的data class自动生成。好消息是data class会自动基于主构造函数所有属性生成equals+hashCode,双双对齐,契约天然满足。但上面"可变字段"的坑对 data class 同样存在——所以参与相等的字段尽量用val。
最佳实践:
- 用 IDE 一键生成,或
Objects.hash(field1, field2); - 保证
equals和hashCode用同一组字段; - 参与相等的字段保持不可变(
val/final)。
高分答法:我会补一句——Android 里Bundle、Intent传对象、RecyclerView的DiffUtil.ItemCallback都依赖正确的 equals,写错一个数据就错乱,这比 HashMap 取不出更隐蔽。
收尾:几点能落地的提醒
equals 和 hashCode 这道题,百度考的是"你有没有被散列表坑过"。只重写一个、用可变字段、字段不一致——这三个错,新手几乎必踩其一。
面试 Tips(具体答法):
- 被问区别,先分基本类型/引用类型讲
==,再讲Object.equals默认就是==。 - 说约定,务必背出"相等对象必须同 hashCode,反之不成立"。
- 讲坑,完整复述"put 入桶、get 找桶,hashCode 不同就失联"的链路。
- 提 HashMap,带出扰动函数和
(n-1)&hash位运算,直接拉到源码层。
我的看法:这题答得好不好,能看出一个人写没写过生产级 Model 类。Android 里每个data class背后都是这套契约,搞懂它,序列化、去重、缓存全都不会再翻车。
评论区聊聊:你有没有因为 equals/hashCode 写错,导致过一个查不出来的诡异 bug?
---
点赞、在看、转发,三连是对我最大的支持!
「Android 大厂面经·从入门到精通」连载系列
上一篇:第003篇 快手·Android 开发面试——StringBuilder 和 StringBuffer 选错一个
下一篇预告:第005篇 京东·Android 面试——接口和抽象类的区别
评论区聊聊:你面试遇到过最难的 Android 问题是哪道?
关于本系列:「Android 大厂面经·从入门到精通」是360篇连载系列,覆盖美团、字节跳动、阿里巴巴、快手、百度、京东、华为、小米等30+家公司从 Java 基础到架构师终面的真实面试内容。本篇属于阶段1·入门篇——Java 语言基础。