面试官问"== 和 equals 到底有什么不同",真正想听的不是一句"== 比地址、equals 比内容",而是一整套完整的心智模型:基本类型和引用类型在 == 下的行为为什么完全两样、equals 的默认实现是什么、为什么重写 equals 必须重写 hashCode。这套东西平时没用过、没踩过坑,现场是编不出来的。
先把结论放在前面:== 是运算符,比较的是"值是否相等"——对基本类型比的是数值,对引用类型比的是两个引用是否指向同一个对象(地址);equals 是 Object 的方法,默认实现就是 ==,只有被子类重写后才去比较"逻辑内容"。这题要答好,得先把"比较"拆成两层:比的是数值还是比的是身份,再逐层定位。
机制拆解
这篇的主角是== 与 equals 的区别:它是什么、底层怎么运转、真实项目里长什么样,层层往下讲。真正拉开差距的,不是记得住定义,而是能说清那些反直觉的现场表现。
这些坑的正确绕法
最常见的坑是用 == 比较包装类或字符串,测试环境通过、生产环境偶发失败。根因是 Integer 的缓存区间(-128~127)和字符串常量池复用:小数值或字面量恰好命中同一对象,== 为真;一旦数值超出缓存区间,或字符串走了 new 的路径,就变成两个独立的堆对象,== 立刻为假。这种 bug 在单测里复现不出来,因为测试数据往往集中在小整数或字面量上。
其次是只重写 equals 没重写 hashCode。放进 HashSet/HashMap 后,两个内容相等的对象被分到不同桶,"去重失效、查找不到"却毫无报错。很多人以为 equals 对了就万事大吉,忘了哈希集合是先按 hashCode 找桶、再按 equals 比内容的,桶都不在同一处,equals 根本不会被调用。
还有一个更隐蔽的坑:对枚举用 equals 而不是 ==。枚举在 JVM 里是单例,== 比较既安全(遇 null 返回 false,不会抛空指针)又直白;用 equals 虽然也能工作,却平白多了一层方法调用,且语义上弱了——既然枚举全局唯一,比身份才是最自然的选择。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
// 1) 基本类型:== 比值
int x = 5, y = 5;
System.out.println(x == y); // true:比的是值,与内存无关
// 2) 字符串:字面量进常量池,new 强制堆新对象
String a = "hi";
String b = new String("hi");
System.out.println(a == b); // false:地址不同
System.out.println(a.equals(b)); // true:字符序列相同
// 3) Integer 自动装箱缓存:-128~127 复用,超出则新对象
Integer i1 = 100, i2 = 100;
System.out.println(i1 == i2); // true:命中缓存,同一对象
Integer i3 = 128, i4 = 128;
System.out.println(i3 == i4); // false:超出缓存,两个对象
// 4) 自定义类型:必须同时重写 equals 与 hashCode,否则进 HashMap 查不到
class User {
final int id;
User(int id) {
this.id = id; }
@Override public boolean equals(Object o) {
return o instanceof User u && u.id == id;
}
@Override public int hashCode() {
return Integer.hashCode(id); }
}
这段代码值得盯三处:第一处,基本类型 == 比的是值,跟对象地址完全无关;第二处,字符串字面量进常量池共享、new 强制新建,所以 a == b 为假而 a.equals(b) 为真;第三处,Integer 缓存区间的存在,让 i1 == i2 为真、i3 == i4 为假,这种"只差一个数值结果就反转"的现象,正是 == 在引用类型上最坑的地方。面试讲到这一层,基本就稳了。
这题在面试里怎么问、怎么答
"请简单介绍一下 == 与 equals 的区别,它在 Android 开发中起什么作用?"按"是什么、干什么用、项目里怎么用"递进讲,别超三分钟。对基本类型,== 比值;对引用类型,== 比地址(是否同一对象),equals 默认也比特地址,重写后才比逻辑内容。项目里凡是涉及缓存命中、对象去重、集合查找,都绕不开这两者分工,讲清这一点就够了。如果只能留一条团队约定,建议是"凡是值对象比较一律用 equals,且 equals 与 hashCode 配对重写"。
"== 与 equals 的底层原理是什么?能不能详细说一下?"原理题三步:动机、机制、开销与边界主动交代;机制别空讲,配核心片段最稳。要解释 == 的种种反直觉表现,必须回到内存布局:局部变量存在栈帧的局部变量表,对象实体在堆,引用类型变量里装的是堆地址;== 比较引用类型时比的就是这个地址。而 equals 是方法调用,默认实现 return (this == obj),所以不重写时它和 == 等价。String 的常量池、Integer 的缓存都是在"减少重复对象"这件事上做文章,理解了这个动机,所有反直觉表现都顺了。
"在使用 == 与 equals 时遇到过什么问题?"按"现象 → 定位 → 修复 → 验证"讲一个真实案例。比如在循环或数值计算里对包装类做 ==,自动装箱频繁发生,== 比较的是缓存对象而非数值,偶发返回 false 导致分支走错。定位靠把包装类型改成基本类型或用 equals 比对;修复后单测覆盖边界值(127/128、-128)验证。重点放在定位手段与修复后的量化效果,比空谈"要注意"有说服力得多。
"== 与 equals 和相关的替代方案相比,有什么优劣?"广度和选型一起考,别只答一个点。对比抓四个维度:性能(== 是运算符、零方法调用,equals 有虚调用开销)、易用性(equals 需正确重写)、生态(Objects.equals 提供空安全比较,首选)、成本(重写 equals 要同步 hashCode)。没有万能答案:能说清什么场景用什么,才叫真懂。最容易被忽略的一条铁律:覆写 equals 必须同时覆写 hashCode。选型时要把"只重写 equals 没重写 hashCode,导致 HashMap 去重失效"这类代价摆到台面上,再决定是否引入。
再补一个工程上高频的用法:Objects.equals(a, b) 是空安全的比较首选,它内部先判两个引用是否都为 null(都为 null 视为相等),再调用 a.equals(b),因此再也不用自己写 a == null ? b == null : a.equals(b) 这种样板。另一个容易混淆的点是 IdentityHashMap——它用 == 而不是 equals 来比较 key,适用于"对象身份即语义"的场景(比如给每个对象挂元数据、避免 equals 冲突)。理解 IdentityHashMap 能帮你解释"为什么有时需要绕过 equals",也反过来加深了对 == 与 equals 分工的理解。在集合查找、缓存键这类场景,提前想清楚"比的是地址还是内容",能省掉一大类隐性 bug。
在 Android 具体场景里,Parcelable 对象的相等比较尤其容易出错:两个从 Intent 里取出来的 Parcelable,即便字段完全相同,往往也是不同对象,== 一律为 false,必须依赖 equals 而非地址。又如两个 Rect 实例,用 == 比较比不出"区域是否重合",得用 equals 或 contains/intersect 这类语义方法。把"比地址还是比内容"这个判断固化成习惯,能在 UI 状态比对、列表 diff、缓存命中这些高频地方少写一堆隐性 bug。
给正在准备面试的你
\== 与 equals 的区别读完别停,真实编码加复盘各来一次,知识才留得住。第一,所有值对象比较一律用 equals,并用 Objects.equals(a, b) 获得空安全;第二,重写 equals 必重写 hashCode,且用相同字段集合;第三,包装类/字符串比较绝不用 ==,宁可多写一次 equals;第四,枚举比较直接用 ==,既快又语义正确。
再补工程案例与踩坑——应用落点是动手演示 new String 与字面量在 == 与 equals 下的四种组合,并逐一解释为什么。理解这四种组合,等于把"地址比较"和"内容比较"的边界彻底划清。
复习时别孤立刷题:ArrayList 源码与扩容机制——1.5 倍增长的细节相邻考点常被一起问,边界提前划清楚。
收个尾,两句最要紧的:基本类型 == 比值,引用类型 == 比地址;equals 不重写等于 ==,重写则比逻辑内容,且必须与 hashCode 配对。
下篇进入异常体系 Throwable:Checked 与 Unchecked 的边界,算是今天内容的下半场。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:Object-通用方法:equals、hashCode-与-clone-契约
下一篇预告:异常体系-Throwable:Checked-与-Unchecked-的边界
有任何问题欢迎在评论区留言交流。