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() 设计初衷是实现业务逻辑上的内容相等判断
等价性五大原则(重写必须遵守)
  1. 自反性x.equals(x) == true
  2. 对称性:若 x.equals(y) == true,则 y.equals(x) == true
  3. 传递性x=yy=z 推导 x=z
  4. 一致性:对象内容不变时,多次调用结果一致
  5. 非空性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)完全依赖此规则:

  1. 同一对象,内容未修改时,多次调用 hashCode() 必须返回相同整数;
  2. a.equals(b) == true,则 a.hashCode() == b.hashCode() 必须成立;
  3. a.hashCode() == b.hashCode()a.equals(b) 可真可假(哈希碰撞)。
为什么重写 equals 必须重写 hashCode?

HashSet / HashMap 为例:

  1. 集合先调用 hashCode() 定位数组下标;
  2. 下标冲突时,再调用 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());
}

自动触发时机

  1. System.out.println(对象) 直接打印;
  2. 字符串拼接:"用户:" + user
  3. 日志框架(如 Logback/SLF4J)打印对象占位符时。
【补充】循环引用导致的 StackOverflowError

在使用 Lombok 的 @ToString 或手动重写 toString() 时,如果两个对象互相引用(如 Parent 包含 ChildChild 包含 Parent),在打印时会导致无限递归,最终抛出 StackOverflowError
解决方案:使用 @ToString.Exclude 排除其中一个方向的引用,或使用 @ToString(callSuper = true) 谨慎处理。


4. getClass() 获取运行时类型

核心特性
public final native Class<?> getClass();
  • final 修饰:禁止重写;native 本地方法。
  • 返回值:Class 类实例,代表当前对象运行时真实类型,是 Java 反射的入口。
两种获取 Class 对象的方式对比
  1. 对象.getClass():运行时获取,跟随对象真实类型,支持多态;
  2. 类名.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()

统一前置规则
  1. 均被 final 修饰,禁止重写;
  2. 必须在 synchronized 同步代码块中调用,否则抛出 IllegalMonitorStateException
  3. 底层依赖 Java 对象监视器锁(Monitor)。
虚假唤醒与标准写法

JVM 存在虚假唤醒(Spurious Wakeup),线程可能在没有被 notify 的情况下意外唤醒。
规范写法wait() 必须包裹在 while 循环中,而非 if

synchronized (lock) {
    while (条件不满足) {
        lock.wait();
    }
    // 执行业务逻辑
}
【补充】现代并发编程的替代方案

在实际高并发项目中,极少直接使用 wait/notify,而是使用 JUC 包下的替代方案:

  1. ReentrantLock + Condition
    支持多个等待队列,支持公平锁,支持中断响应和超时机制,比 synchronized + wait/notify 更灵活。
  2. LockSupport
    JUC 底层最核心的线程阻塞/唤醒工具。LockSupport.park()LockSupport.unpark() 不需要在同步块中调用,且 unpark 可以先于 park 调用(自带许可机制),彻底解决了 notify 必须先于 wait 的死锁隐患。
经典解答:为什么 wait/notify 放在 Object 里?

Java 的锁是绑定在对象上的(每个对象一把 Monitor 锁),而非线程。等待、唤醒操作本质是对对象锁的操作。一个锁可能被多个线程竞争,方法定义在 Object 才能实现“基于锁的线程通信”。


7. finalize() GC 回调方法(已废弃)

废弃原因(致命缺陷)
  1. 执行时机不确定:由 GC 调度,开发者无法控制;
  2. 对象复活风险:在 finalize() 中重新给对象赋值引用,对象会“死而复生”,导致内存泄漏;
  3. 性能极差:额外队列、线程开销,严重阻塞 GC 流程;
  4. 执行不保证:程序退出时,GC 可能直接终止,finalize() 根本不执行。
【补充】现代 Java 的资源释放替代方案
  1. JDK 7+try-with-resources 语法 + AutoCloseable 接口(绝对主流)。
  2. JDK 9+java.lang.ref.Cleaner 类。它比 finalize 更安全、性能更好,支持在对象被回收时执行清理操作,且不会导致对象复活。
  3. 底层方案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() 有什么区别?

  1. 所属类:wait 属于 Object,sleep 属于 Thread。
  2. 锁行为:wait 会释放对象锁,sleep 不会释放任何锁。
  3. 调用位置:wait 必须在 synchronized 块中,sleep 可在任意位置。
  4. 唤醒方式:wait 靠 notify/超时唤醒,sleep 靠时间到期自动恢复。

Q5:getClass()instanceof 的区别?

getClass() 严格匹配运行时类型,父子类互不相等;instanceof 判断对象是否属于该类或其子类,支持继承关系判定。在重写 equals 时,推荐使用 getClass() 以保证对称性。

Q6:finalize() 方法为什么被废弃?有什么替代方案?

:因为执行时机不可控、存在对象复活风险、严重影响 GC 性能。替代方案包括 try-with-resources、手动 close()、JDK9 引入的 Cleaner 以及虚引用 PhantomReference


四、总结与实战建议

  1. 必掌握核心组合equals() + hashCode() 是 Java 对象相等判断的基石,所有实体类(尤其是作为 Map Key 或 Set 元素的类)必须成对重写
  2. 调试必备toString() 建议所有实体类重写(或使用 Lombok @Data),极大提升日志排查效率。
  3. 并发核心wait/notify 是内置锁实现线程通信的基础,必须掌握锁规则while防虚假唤醒写法;实战中优先使用 ReentrantLock + ConditionLockSupport
  4. 摒弃过时API:坚决不要在生产环境使用 finalize(),不要使用原生的 clone() 进行复杂对象的深拷贝。
  5. 拥抱工具库:合理利用 Lombok 注解(@Data, @EqualsAndHashCode, @ToString)减少样板代码,但务必理解其底层生成的源码逻辑,避免踩坑。

更多推荐