在 Android 面试里,Object 的通用方法经常被包装成一道开放题:"你在项目里是怎么重写 equals 和 hashCode 的?"这道题答不好,往往不是知识不够,而是从来没把做过的取舍按"场景、做法、代价"的结构梳理过。Object 是所有类的根,它约定的一组方法(equals、hashCode、toString、clone、getClass、以及 wait/notify/finalize)构成了 Java 对象行为的基础契约。能不能把这些方法讲清楚,面试官一眼就能判断出候选人是"用过"还是"懂原理"。
先把结论放在前面:equals 相等的两个对象必须拥有相同的 hashCode,这是放进基于哈希的集合后还能被正确查找的前提;clone 默认只做浅拷贝,凡是对象里持有可变引用,深拷贝都要自己负责;toString 则是最容易被忽视、却在排查问题时最救命的一个。这题要答好,关键是把概念落到具体的工程场景里,讲出取舍和代价,而不是背定义。
机制拆解
这篇聊 Object 通用方法之前,先想一个问题:面试官连问三层,你能接住吗?第一层问"有哪些方法",第二层问"equals 和 hashCode 的契约是什么",第三层问"如果违反了会怎样"。这篇就是为这个准备的,把三层一并铺开。
这些坑的正确绕法
最常见的坑是把可变对象放进 HashSet 之后,又修改了它参与 hashCode 计算的字段。集合在插入时按照当时的哈希值把元素挂到某个桶上,字段一变,hashCode 跟着变,但元素并没有被挪到新桶。后果是:你再也 remove 不掉它,contains 也查不到,内存里还实实在在留着一个"幽灵元素"。这类 bug 不会立刻报错,往往在代码跑了几天、集合膨胀之后才以"数据删不掉"的形式暴露,定位起来极其费劲。
其次是 clone 出的副本和原对象共享同一个可变元素的引用。比如一个 List 做了浅克隆,原对象和副本里的每个 byte[] 指向同一块内存,你在副本里改了其中一个数组,原对象也跟着变。这种"改副本污染原数据"的事故,在缓存快照、配置回滚这类场景里特别致命,因为当事人通常以为拿到的已经是一份独立拷贝。
还有一个更隐蔽的坑:equals 只重写了 equals 却忘了 hashCode。两个内容相同的对象因为 hashCode 不同被 HashMap 分到两个桶,于是"去重失效、查找不到"却全程没有任何异常。更麻烦的是,这种问题只在集合语义下才显现,单独跑 equals 测试是通过的,所以它的隐蔽性来自"单测覆盖不到真实用法"。
代码里见真章
看一段能直接跑的代码,把上面的机制落到具体写法上:
class Point {
int x, y;
Point(int x, int y) {
this.x = x; this.y = y; }
@Override
public boolean equals(Object o) {
if (this == o) return true; // 1) 同一引用
if (!(o instanceof Point p)) return false; // 2) 类型与空值
return x == p.x && y == p.y; // 3) 字段比较
}
@Override
public int hashCode() {
// 31 是个奇素数,减少哈希碰撞
return 31 * Integer.hashCode(x) + Integer.hashCode(y);
}
@Override
public Point clone() {
try {
Point p = (Point) super.clone(); // 浅拷贝:基本类型已独立
this.history = this.history.clone(); // 对可变字段补深拷贝
return p;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
int[] history; // 可变引用字段
}
这段代码值得盯三处:第一处,equals 的"自反、对称、传递"三条必须由代码保证,先判同一引用再判类型,能挡掉 null 和跨类型比较;第二处,hashCode 必须与 equals 保持同步,equals 相等的对象 hashCode 必须相等,否则哈希集合行为不可预期;第三处,clone 默认浅拷贝,对 int[] 这类可变字段要手动再 clone 一次,否则副本共享底层数组。面试讲到这一层,基本就稳了。
这题在面试里怎么问、怎么答
"请简单介绍一下 Object 通用方法,它在 Android 开发中起什么作用?"答法很简单:定位一句(Object 是所有类的根,约定了对象的基础行为)、场景一个(写个数据类要放进 HashMap 就必须重写 equals 和 hashCode)、细节一处(equals 相等则 hashCode 必相等),收住即可。clone 是浅拷贝,Cloneable 是个没有任何方法的标记接口,实现它只是给 super.clone() 一个"允许克隆"的信号,不实现会直接抛 CloneNotSupportedException。
"Object 通用方法的底层原理是什么?能不能详细说一下?"别用一句话打发:动机、机制、代价三层都给出才算合格。讲 equals 时抓住主线索——它定义了"逻辑相等"的语义,而 == 只比较引用地址;讲 hashCode 时说明它是哈希集合定位桶的依据,好的哈希函数应让不同对象尽量散布到不同桶;讲 clone 时说明它走的是字段逐位复制,引用类型只复制指针。聊到 Object 通用方法时,画图或写代码比形容词有说服力。
"在使用 Object 通用方法时遇到过什么问题?"这是行为题,考实战经验,拿真实案例说话。比如线上出现过"用可变对象当 HashMap 的 key,修改后查不到",复现手段是构造一个放入集合后改字段的对象,定位靠在 remove 前后打印 hashCode 发现不一致;修复是把 key 换成不可变对象或改用值语义的元组。讲经历用"场景、行动、结果"三段式,逻辑顺、有数字最加分。对这类问题先复现再修复:没有稳定复现路径的修复都是赌博,验证手段要提前设计。
"Object 通用方法和相关替代方案相比,有什么优劣?"对比要有维度:性能、接入成本、生态与团队熟悉度各占什么权重。比如不想手写 equals/hashCode,可以用 AutoValue、data class 或 Lombok 的 @EqualsAndHashCode 自动生成,但代价是引入了编译期依赖、且生成逻辑可能与团队约定不一致;手写的好处是可控、可读、改起来直观。结尾必须落到明确的选型建议:对外暴露的值对象手写并单测覆盖,内部临时结构可用工具生成。选型时要把"clone 出的副本共享可变引用"这类代价摆到台面上,再决定是否引入。
把视角再拉宽一点:除了 equals/hashCode/clone,Object 还有 getClass、wait/notify/finalize 这组方法,理解它们能让你在更底层的问题上有话可说。getClass 返回运行期真实类型,equals 里用 o instanceof Point 比用 o.getClass() == Point.class 更宽容(允许子类参与比较),但后者在要求"严格同类型"时更精确,两者取舍看你是否接受子类参与。wait/notify 是对象监视器的基础,配合 synchronized 实现线程间等待通知,不过现代代码更推荐用高层并发工具(Lock、Condition、阻塞队列)替代,可读性更好。finalize 由于执行时机不确定、可能拖慢 GC,早已被标记为废弃,需要清理资源请走 AutoCloseable 或 Cleaner,别再依赖它。clone 在现代 Java 里也常被建议用拷贝构造器或 Builder 替代:拷贝构造器类型安全、不会抛受检异常、且能清晰控制深拷贝的边界,比 clone 更易维护。
给正在准备面试的你
Object 通用方法的要点要用一次真实编码来验收,纸上懂不算懂。凡是会被放进 HashSet/HashMap 的类,equals 和 hashCode 必须成对重写,并且用相同的字段集合,改一个字段需要同步另一个。可变对象尽量别当 key 用,非要当就用不可变包装。clone 默认浅拷贝,任何持有可变引用的类,克隆时都要对可变字段补深拷贝,或者干脆换成拷贝构造器(更推荐,语义清晰且不会漏)。equals 里先判 this == o 再判类型,能同时挡掉空指针和跨类型比较。
面试中关于 Object 通用方法的问题,关键在于能够从原理、应用、踩坑三个层面给出有深度的回答。toString 也别偷懒用默认的类名@哈希,关键数据类覆写 toString,出问题时日志里一眼就能看清对象状态,省下的排查时间远超写那几行代码的成本。
复习时别孤立刷题:集合框架总览——Collection 与 Map 两大家族相邻的坑往往连着踩,提前绕开。
用两句话给这篇收个束:equals 契约要求自反、对称、传递、一致与非空,违反任一条都会让集合行为不可预期;equals 相等的对象 hashCode 必须相等,否则哈希集合会"找不到、删不掉"。
下篇进入 == 与 equals 的区别:从栈堆内存说起,正好接上今天的话题。
如果这篇文章对你有帮助,欢迎点赞、在看、转发三连。你的支持就是这个系列持续更新的动力。
「Android软件开发面试·从入门到精通」连载系列
上一篇:包装类与自动装箱:Integer-缓存池的坑
下一篇预告:==-与-equals-的区别:从栈堆内存说起
有任何问题欢迎在评论区留言交流。