java-object类详解
Java Object 类全解析:源码+原理+面试考点+实战避坑
java.lang.Object 是 Java 中所有类的根父类,是整个 Java 语言体系的顶层基类。
- 所有类(包括自定义类、集合、包装类、数组)都会隐式继承
Object,编译器会自动为类补上extends Object,无需手动编写。 - 共包含 11 个方法(含重载、本地方法、废弃方法),分为:通用实例方法、反射相关、哈希与相等判断、克隆、线程通信、GC 回调、本地方法注册。
- 本文结合 JDK8 源码、底层原理、使用规范、高频面试题、实战坑点逐一拆解,并在原版基础上补充了现代 Java 开发中的替代方案与 Lombok 实战技巧。
一、Object 类总览
1. 完整方法清单(JDK8)
public class Object {
// 1. 静态私有本地方法:注册本地方法映射
private static native void registerNatives();
static {
registerNatives();
}
// 2. 获取运行时 Class 对象(反射)
public final native Class<?> getClass();
// 3. 计算对象哈希码
public native int hashCode();
// 4. 判断对象逻辑相等
public boolean equals(Object obj);
// 5. 对象克隆
protected native Object clone() throws CloneNotSupportedException;
// 6. 对象字符串描述
public String toString();
// 7. 线程等待(3个重载)
public final void wait() throws InterruptedException;
public final native void wait(long timeout) throws InterruptedException;
public final void wait(long timeout, int nanos) throws InterruptedException;
// 8. 唤醒单个等待线程
public final native void notify();
// 9. 唤醒所有等待线程
public final native void notifyAll();
// 10. GC 回收前回调(已废弃)
protected void finalize() throws Throwable;
}
2. 方法分类与面试频率
| 分类 | 方法 | 核心用途 | 面试频率 |
|---|---|---|---|
| 基础通用 | toString() / equals() / hashCode() |
对象描述、相等判断、哈希计算 | 必考 |
| 反射相关 | getClass() |
获取运行时类型 | 高频 |
| 对象克隆 | clone() |
复制对象 | 高频 |
| 线程通信 | wait() / notify() / notifyAll() |
多线程等待、唤醒 | 必考(并发核心) |
| GC 回调 | finalize() |
垃圾回收前置方法 | 了解(已废弃) |
| 底层支撑 | registerNatives() |
本地方法注册 | 了解原理 |
二、逐个方法深度解析(按面试优先级排序)
1. equals(Object obj) 相等判断方法
源码与设计思想
public boolean equals(Object obj) {
return (this == obj);
}
- 非
final方法,允许子类重写。 - 原生实现直接使用
==,仅比较两个对象的内存地址。 ==是语法层面的地址比较,而equals()设计初衷是实现业务逻辑上的内容相等判断。
等价性五大原则(重写必须遵守)
- 自反性:
x.equals(x) == true - 对称性:若
x.equals(y) == true,则y.equals(x) == true - 传递性:
x=y、y=z推导x=z - 一致性:对象内容不变时,多次调用结果一致
- 非空性:
x.equals(null) == false
标准重写步骤(手写模板)
public class User {
private Long id;
private String username;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null) return false;
// 推荐 getClass(),避免 instanceof 破坏对称性
if (getClass() != o.getClass()) return false;
User user = (User) o;
return Objects.equals(id, user.id)
&& Objects.equals(username, user.username);
}
}
【补充】现代开发中的 Lombok 替代方案
在实际项目中,手写 equals 过于繁琐,通常使用 Lombok 的 @EqualsAndHashCode 注解。
@Data
@EqualsAndHashCode(callSuper = false) // 若存在继承关系,需特别注意 callSuper 参数
public class User {
private Long id;
private String username;
}
【避坑指南】:当类存在继承关系时,如果子类使用了
@EqualsAndHashCode但没有设置callSuper = true,Lombok 生成的equals方法将不会比较父类的字段,导致逻辑错误。
【补充】经典追问:为什么 String 适合作为 HashMap 的 Key?
因为 String 类重写了 equals() 和 hashCode(),并且 String 是不可变类(Immutable)。不可变性保证了对象创建后哈希码不会改变,从而确保在 HashMap 中能够被正确检索。
2. hashCode() 哈希码方法
核心契约(重中之重)
该契约是 equals() 与 hashCode() 联动的规则,集合类(HashMap/HashSet)完全依赖此规则:
- 同一对象,内容未修改时,多次调用
hashCode()必须返回相同整数; - 若
a.equals(b) == true,则a.hashCode() == b.hashCode()必须成立; - 若
a.hashCode() == b.hashCode(),a.equals(b)可真可假(哈希碰撞)。
为什么重写 equals 必须重写 hashCode?
以 HashSet / HashMap 为例:
- 集合先调用
hashCode()定位数组下标; - 下标冲突时,再调用
equals()做内容判重。
如果只重写equals()不重写hashCode(),两个内容相同的对象会生成不同的哈希码,被分配到不同的桶中,导致去重失效、数据错乱。
【补充】HashMap 中的哈希扰动
HashMap 并不会直接使用对象的 hashCode(),而是会进行扰动计算(高16位与低16位异或),以减少哈希碰撞:
// HashMap 源码中的 hash 方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
【提示】:这也是为什么我们在重写
hashCode()时,尽量让生成的哈希值分布更均匀的原因,良好的哈希算法能显著降低 HashMap 的链表/红黑树转换概率。
3. toString() 字符串描述方法
原生源码与触发时机
public String toString() {
return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
自动触发时机:
System.out.println(对象)直接打印;- 字符串拼接:
"用户:" + user; - 日志框架(如 Logback/SLF4J)打印对象占位符时。
【补充】循环引用导致的 StackOverflowError
在使用 Lombok 的 @ToString 或手动重写 toString() 时,如果两个对象互相引用(如 Parent 包含 Child,Child 包含 Parent),在打印时会导致无限递归,最终抛出 StackOverflowError。
解决方案:使用 @ToString.Exclude 排除其中一个方向的引用,或使用 @ToString(callSuper = true) 谨慎处理。
4. getClass() 获取运行时类型
核心特性
public final native Class<?> getClass();
final修饰:禁止重写;native本地方法。- 返回值:
Class类实例,代表当前对象运行时真实类型,是 Java 反射的入口。
两种获取 Class 对象的方式对比
对象.getClass():运行时获取,跟随对象真实类型,支持多态;类名.class:编译期获取,静态绑定,不会触发多态。
【补充】getClass() 与类加载机制
getClass() 返回的 Class 对象是由类加载器(ClassLoader) 加载的。在判断两个类是否相等时,不仅要看类的全限定名是否相同,还要看加载它们的类加载器是否相同。这也是双亲委派模型和自定义类加载器(如 Tomcat、OSGi)中的核心考点。
5. clone() 对象克隆方法
使用前置条件
被克隆的类必须实现 Cloneable 接口。Cloneable 是一个标记接口(无任何方法),仅作为“允许克隆”的标识。不实现该接口调用 clone() 会抛出 CloneNotSupportedException。
浅拷贝 vs 深拷贝
- 浅拷贝:基本类型复制值,引用类型复制地址(新旧对象共享引用)。
- 深拷贝:完全复制整个对象,引用类型也会新建独立对象,彻底隔离。
【补充】现代开发中的深拷贝最佳实践
原生的 clone() 方法设计存在缺陷(违背面向对象原则、需要处理受检异常、浅拷贝易出错)。现代 Java 开发中,强烈推荐使用 JSON 序列化/反序列化来实现深拷贝。
// 使用 Jackson 实现深拷贝(推荐)
public static <T> T deepCopy(T obj, Class<T> clazz) {
try {
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(obj);
return objectMapper.readValue(json, clazz);
} catch (JsonProcessingException e) {
throw new RuntimeException("深拷贝失败", e);
}
}
// 使用 Hutool 工具类(更简洁)
// User copyUser = ObjectUtil.cloneByStream(user);
【避坑指南】:
clone()方法不会调用构造方法,它是直接在堆内存中分配空间并复制字节。如果对象的初始化逻辑写在构造方法中,克隆出来的对象可能会处于不一致的状态。
6. 线程通信:wait() / notify() / notifyAll()
统一前置规则
- 均被
final修饰,禁止重写; - 必须在
synchronized同步代码块中调用,否则抛出IllegalMonitorStateException; - 底层依赖 Java 对象监视器锁(Monitor)。
虚假唤醒与标准写法
JVM 存在虚假唤醒(Spurious Wakeup),线程可能在没有被 notify 的情况下意外唤醒。
规范写法:wait() 必须包裹在 while 循环中,而非 if:
synchronized (lock) {
while (条件不满足) {
lock.wait();
}
// 执行业务逻辑
}
【补充】现代并发编程的替代方案
在实际高并发项目中,极少直接使用 wait/notify,而是使用 JUC 包下的替代方案:
- ReentrantLock + Condition:
支持多个等待队列,支持公平锁,支持中断响应和超时机制,比synchronized + wait/notify更灵活。 - LockSupport:
JUC 底层最核心的线程阻塞/唤醒工具。LockSupport.park()和LockSupport.unpark()不需要在同步块中调用,且unpark可以先于park调用(自带许可机制),彻底解决了notify必须先于wait的死锁隐患。
经典解答:为什么 wait/notify 放在 Object 里?
Java 的锁是绑定在对象上的(每个对象一把 Monitor 锁),而非线程。等待、唤醒操作本质是对对象锁的操作。一个锁可能被多个线程竞争,方法定义在 Object 才能实现“基于锁的线程通信”。
7. finalize() GC 回调方法(已废弃)
废弃原因(致命缺陷)
- 执行时机不确定:由 GC 调度,开发者无法控制;
- 对象复活风险:在
finalize()中重新给对象赋值引用,对象会“死而复生”,导致内存泄漏; - 性能极差:额外队列、线程开销,严重阻塞 GC 流程;
- 执行不保证:程序退出时,GC 可能直接终止,
finalize()根本不执行。
【补充】现代 Java 的资源释放替代方案
- JDK 7+:
try-with-resources语法 +AutoCloseable接口(绝对主流)。 - JDK 9+:
java.lang.ref.Cleaner类。它比finalize更安全、性能更好,支持在对象被回收时执行清理操作,且不会导致对象复活。 - 底层方案:
PhantomReference(虚引用)配合ReferenceQueue,NIO 中的DirectByteBuffer就是利用虚引用来回收堆外内存的。
8. registerNatives() 本地方法注册
private static native void registerNatives();
static {
registerNatives();
}
- 作用:将 Java 层的 native 方法和底层 C/C++ 函数做函数指针映射、注册绑定,让 JVM 找到对应的本地实现。
- 说明:日常开发无需关注,属于 JVM 底层支撑机制。
三、Object 类高频面试题汇总(背诵版)
Q1:equals() 和 == 的区别?重写 equals 为什么一定要重写 hashCode?
答:
==对于基本类型比较值,对于引用类型比较内存地址。equals()默认等同于==,但通常被重写为比较对象内容。重写equals必须重写hashCode是为了满足哈希契约,保证对象在 HashMap/HashSet 等哈希集合中能够正确判重和检索,否则会导致逻辑相同的对象被当作不同对象处理。
Q2:什么是浅拷贝、深拷贝?clone() 的使用条件?
答:浅拷贝只复制引用地址,深拷贝会创建全新的引用对象。使用
clone()必须实现Cloneable标记接口,否则抛出异常。现代开发推荐使用 JSON 序列化实现深拷贝以规避原生clone的缺陷。
Q3:wait()、notify() 为什么定义在 Object 中而不是 Thread 中?
答:因为 Java 的内置锁(Monitor)是绑定在对象上的,每个对象都有一把锁。线程通信本质上是线程对对象锁的等待和释放,与具体的线程实例无关,因此定义在 Object 中。
Q4:wait() 和 sleep() 有什么区别?
答:
- 所属类:
wait属于 Object,sleep属于 Thread。- 锁行为:
wait会释放对象锁,sleep不会释放任何锁。- 调用位置:
wait必须在 synchronized 块中,sleep可在任意位置。- 唤醒方式:
wait靠 notify/超时唤醒,sleep靠时间到期自动恢复。
Q5:getClass() 和 instanceof 的区别?
答:
getClass()严格匹配运行时类型,父子类互不相等;instanceof判断对象是否属于该类或其子类,支持继承关系判定。在重写equals时,推荐使用getClass()以保证对称性。
Q6:finalize() 方法为什么被废弃?有什么替代方案?
答:因为执行时机不可控、存在对象复活风险、严重影响 GC 性能。替代方案包括
try-with-resources、手动close()、JDK9 引入的Cleaner以及虚引用PhantomReference。
四、总结与实战建议
- 必掌握核心组合:
equals()+hashCode()是 Java 对象相等判断的基石,所有实体类(尤其是作为 Map Key 或 Set 元素的类)必须成对重写。 - 调试必备:
toString()建议所有实体类重写(或使用 Lombok@Data),极大提升日志排查效率。 - 并发核心:
wait/notify是内置锁实现线程通信的基础,必须掌握锁规则和while防虚假唤醒写法;实战中优先使用ReentrantLock + Condition或LockSupport。 - 摒弃过时API:坚决不要在生产环境使用
finalize(),不要使用原生的clone()进行复杂对象的深拷贝。 - 拥抱工具库:合理利用 Lombok 注解(
@Data,@EqualsAndHashCode,@ToString)减少样板代码,但务必理解其底层生成的源码逻辑,避免踩坑。
更多推荐

所有评论(0)