编程进阶网 编程进阶网
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接

杨充

专注编程 · 终身学习者
首页
  • 计算机原理
  • 操作系统
  • 网络协议
  • 数据库原理
  • 面向对象
  • 设计原则
  • 设计模式
  • 系统架构
  • 性能优化
  • 编程原理
  • 方案设计
  • 稳定可靠
  • 工程运维
  • 基础认知
  • 线性结构
  • 树与哈希
  • 工业级实现
  • 算法思想
  • 实战与综合
  • 算法题考核
  • C语言入门
  • C综合案例
  • C专栏博客
  • C标准集库
  • C++入门教程
  • C++综合案例
  • C++专栏博客
  • C++开发技巧
  • Java入门教程
  • Java综合案例
  • Java专栏博客
  • Go入门教程
  • Go综合案例
  • Go专栏博客
  • Go开发技巧
  • JavaScript入门
  • JavaScript高级
  • Android库解读
  • Android专栏
  • Android智能硬件
  • iOS ObjC入门
  • iOS Swift入门
  • iOS入门精通
  • Web之Html手册
  • Web之TypeScript
  • Web之Vue高级进阶
  • Linux之QML入门
  • Linux之QT核心库
  • Linux实践开发
  • Python教程
  • Shell&Bash教程
  • 工具脚本
  • 自动化脚本
  • 质量保障
  • 产品思考
  • 软实力
  • 开发流程
  • Git应用
  • 技术模版
  • 技术规范
  • Markdown
  • Mermaid
  • 开源协议
  • JSON工具
  • 文本工具
  • 图片处理
  • 文档转化
  • 代码压缩
  • 关于我
  • 自我精进
  • 职场管理
  • 职场面试
  • 心情杂货
  • 友情链接
  • README
  • C语言入门精通

  • Cpp入门到精通

  • Java入门精通

    • README
    • 入门教程

    • 综合案例

    • 专栏博客

      • README
      • JVM内存模型与对象
      • 类加载与双亲委派
      • 垃圾回收与GC调优
      • 异常体系与JVM机制
      • 字节码指令集javap实战
      • JIT编译与去优化机制
      • JVM性能诊断工具链
      • OOM八大现场全景剖析
      • JVM参数调优全景图
      • GraalVM与AOT编译原理
      • HashMap底层哈希设计
      • String不可变与常量池
      • ArrayList与LinkedList源码
      • ConcurrentHashMap并发
      • TreeMap与红黑树原理
      • LinkedHashMap与LRU实现
      • Java数字类型原理
      • Object通用方法的契约
        • 1. 案例引入
          • 1.1 去重失败
          • 1.2 克隆共享
          • 1.3 我们要回答什么
        • 2. Object 全景
          • 2.1 十一个方法
          • 2.2 三大职责切分
          • 2.3 native 方法名单
        • 3. equals 契约
          • 3.1 五大契约条款
          • 3.2 默认实现是地址
          • 3.3 对称性陷阱
          • 3.4 instanceof 与 getClass
        • 4. hashCode 契约
          • 4.1 三大契约条款
          • 4.2 与 equals 一致性
          • 4.3 默认实现的真相
          • 4.4 hash 算法实战
        • 5. clone 浅深拷贝
          • 5.1 Cloneable 标记接口
          • 5.2 浅拷贝陷阱
          • 5.3 深拷贝四方案
          • 5.4 拷贝构造器替代
        • 6. toString 与 finalize
          • 6.1 toString 工程价值
          • 6.2 finalize 已被废弃
          • 6.3 Cleaner 替代方案
          • 6.4 try-with-resources 兜底
        • 7. 监视器三方法
          • 7.1 wait notify 协议
          • 7.2 必须持有锁
          • 7.3 虚假唤醒与循环
          • 7.4 notify 还是 notifyAll
        • 8. getClass 与反射入口
          • 8.1 final 不可重写
          • 8.2 与 instance.class 对比
          • 8.3 与 getDeclaringClass
        • 9. 现代替代方案
          • 9.1 Record 自动实现
          • 9.2 Lombok 字节码生成
          • 9.3 Lock Condition 替代
        • 10. 综合案例串讲
          • 10.1 双案例真相揭晓
          • 10.2 一个对象的一生
          • 10.3 设计哲学回扣
          • 10.4 Object 方法速查表
      • 泛型擦除与类型系统
      • 枚举原理与最佳实践
      • 注解原理与编译期处理
      • Lambda与引用底层原理
      • Stream原理与流水线设计
      • Optional设计原理
      • Record密封类与模式
      • 反射机制与动态代理
      • MethodHandle与VarHandle
      • 三大字节码框架对比
      • JavaAgent与Instrumentation机制
      • AOP三种实现路线对比
      • synchronized与锁升级
      • volatile与JMM内存模型
      • 线程池核心源码设计
      • Thread线程生命周期
      • AQS同步框架源码
      • 并发锁三剑客
      • CAS和Atomic深入分析
      • 五大同步器对比
      • CompletableFuture异步
      • IO模型演进BIO到AIO
      • ByteBuffer与堆外内存
      • 序列化原理与替代方案
      • 文件IO与NIO.2
      • 面向对象的真意
      • JDK设计模式上
      • JDK设计模式下
      • SPI与模块化设计
  • Go入门到精通

  • JavaScript入门

  • CodeX
  • Java入门精通
  • 专栏博客
杨充
2026-06-02
目录

Object通用方法的契约

# 18.Object通用方法的契约

# 目录介绍

  • 1. 案例引入
    • 1.1 去重失败
    • 1.2 克隆共享
    • 1.3 我们要回答什么
  • 2. Object 全景
    • 2.1 十一个方法
    • 2.2 三大职责切分
    • 2.3 native 方法名单
  • 3. equals 契约
    • 3.1 五大契约条款
    • 3.2 默认实现是地址
    • 3.3 对称性陷阱
    • 3.4 instanceof 与 getClass
  • 4. hashCode 契约
    • 4.1 三大契约条款
    • 4.2 与 equals 一致性
    • 4.3 默认实现的真相
    • 4.4 hash 算法实战
  • 5. clone 浅深拷贝
    • 5.1 Cloneable 标记接口
    • 5.2 浅拷贝陷阱
    • 5.3 深拷贝四方案
    • 5.4 拷贝构造器替代
  • 6. toString 与 finalize
    • 6.1 toString 工程价值
    • 6.2 finalize 已被废弃
    • 6.3 Cleaner 替代方案
    • 6.4 try-with-resources 兜底
  • 7. 监视器三方法
    • 7.1 wait notify 协议
    • 7.2 必须持有锁
    • 7.3 虚假唤醒与循环
    • 7.4 notify 还是 notifyAll
  • 8. getClass 与反射入口
    • 8.1 final 不可重写
    • 8.2 与 instance.class 对比
    • 8.3 与 getDeclaringClass
  • 9. 现代替代方案
    • 9.1 Record 自动实现
    • 9.2 Lombok 字节码生成
    • 9.3 Lock Condition 替代
  • 10. 综合案例串讲
    • 10.1 双案例真相揭晓
    • 10.2 一个对象的一生
    • 10.3 设计哲学回扣
    • 10.4 Object 方法速查表

# 1. 案例引入

# 1.1 去重失败

某电商平台的会员系统,运营反馈"VIP 黑名单"功能出现严重 BUG——同一个用户被重复封禁了 3 次,每次都生成新的封禁记录。技术团队定位到这段"看起来非常正常"的代码:

public class User {
    private Long id;
    private String name;
    private Integer level;
    
    // 重写了 equals
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        User user = (User) o;
        return Objects.equals(id, user.id);
    }
    
    // ★ 没有重写 hashCode
}

// 业务代码
Set<User> blacklist = new HashSet<>();
blacklist.add(new User(100L, "Alice", 3));
blacklist.add(new User(100L, "Alice", 3));   // ★ 期望去重
blacklist.add(new User(100L, "Alice", 3));

System.out.println(blacklist.size());        // 输出 3 !期望 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

三个 id 完全相同的 User,HashSet 居然没有去重——三个对象都被存了下来。运营据此发了三条封禁短信,用户疯狂投诉。

事故复盘时,资深架构师在白板上画了三个问号:

追问 ①:equals 已经重写了,为什么 HashSet 还是没去重?
追问 ②:hashCode 不重写会怎么样?JVM 默认实现是什么?
追问 ③:equals 和 hashCode 之间有什么"契约"?为什么必须一起重写?
1
2
3

# 1.2 克隆共享

同一周,订单系统又爆出第二个事故——批量创建订单时,所有订单的优惠券列表"诡异地"指向同一个对象:

public class Order implements Cloneable {
    private Long orderId;
    private List<Coupon> coupons = new ArrayList<>();
    
    @Override
    public Order clone() {
        try {
            return (Order) super.clone();   // ★ 默认浅拷贝
        } catch (CloneNotSupportedException e) {
            throw new AssertionError();
        }
    }
}

// 业务代码
Order template = new Order(0L);
template.getCoupons().add(new Coupon("10元券"));

Order order1 = template.clone();
Order order2 = template.clone();

order1.getCoupons().add(new Coupon("20元券"));   // ★ 只想给 order1 加
System.out.println(order2.getCoupons().size());  // 输出 2 !期望 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

order1 加的券,order2 也"莫名其妙"多了一张——三个 Order 共享同一个 ArrayList。

事故复盘加了四个追问:

追问 ④:super.clone() 到底拷贝了什么?为什么内部 List 共享?
追问 ⑤:Cloneable 是个空接口,凭什么能调用 clone?
追问 ⑥:现代 Java 还应该用 clone 吗?有没有更好的替代?
追问 ⑦:除了 equals/hashCode/clone,Object 还有哪些"魔法方法"埋着雷?
1
2
3
4

# 1.3 我们要回答什么

第 24 篇要把"Object 类的 11 个方法契约"讲透——从 hashCode/equals 一致性到 clone 浅深拷贝、从 finalize 废弃到 wait/notify 监视器机制。这是 Java 一切类的"基类契约"——违反了它,HashMap、HashSet、ConcurrentHashMap、ThreadLocal 全部会出诡异 BUG。

带着 7 个核心问题展开:

① equals 重写了为什么 HashSet 还不去重?             → 第3、4章
② hashCode 不重写时 JVM 默认实现是什么?             → 第4章
③ equals 与 hashCode 之间的契约是什么?              → 第4章
④ super.clone() 浅拷贝拷了什么、漏了什么?           → 第5章
⑤ Cloneable 空接口的玄机在哪里?                     → 第5章
⑥ clone 现代替代方案是什么?                         → 第5、9章
⑦ wait/notify 为什么必须在 synchronized 里调用?     → 第7章
1
2
3
4
5
6
7

本篇路线:

Object 全景 (第2章) ─── 11 个方法 / 3 大职责
    ↓
equals 契约 (第3章)        ←—— 自反/对称/传递/一致/null 五条款
hashCode 契约 (第4章)       ←—— 与 equals 一致性 + 默认实现真相
    ↓
clone 浅深拷贝 (第5章)      ←—— Cloneable 标记接口的玄机
toString 与 finalize (第6章) ←—— 工程价值 + finalize 为什么被废
监视器三方法 (第7章)         ←—— wait/notify/notifyAll 协议
getClass 反射入口 (第8章)    ←—— final 不可重写的设计
    ↓
现代替代方案 (第9章)         ←—— Record / Lombok / Lock Condition
    ↓
综合案例串讲 (第10章)
1
2
3
4
5
6
7
8
9
10
11
12
13

# 2. Object 全景

# 2.1 十一个方法

打开 java.lang.Object 源码(JDK 21 删了 finalize),完整方法清单:

public class Object {
    // 反射入口
    public final native Class<?> getClass();
    
    // 哈希与判等
    public native int hashCode();
    public boolean equals(Object obj);
    
    // 字符串化
    public String toString();
    
    // 克隆
    protected native Object clone() throws CloneNotSupportedException;
    
    // 监视器(线程协作)
    public final native void notify();
    public final native void notifyAll();
    public final void wait() throws InterruptedException;
    public final native void wait(long timeoutMillis) throws InterruptedException;
    public final void wait(long timeoutMillis, int nanos) throws InterruptedException;
    
    // 析构(已废弃,JDK 9 deprecated,JDK 21 移除)
    @Deprecated(since="9", forRemoval=true)
    protected void finalize() throws Throwable;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

统计:除去 wait 重载,本质上是 11 个独立方法——8 个 native、3 个 Java 实现。

# 2.2 三大职责切分

11 个方法按职责切分成 3 个簇:

                       Object
                          │
        ┌─────────────────┼──────────────────┐
        │                 │                  │
   身份与判等族         对象生命周期族       线程协作族
   (5 个方法)           (3 个方法)          (3 个方法)
        │                 │                  │
   ├─ hashCode         ├─ clone           ├─ wait (×3)
   ├─ equals           ├─ toString        ├─ notify
   ├─ getClass         ├─ finalize        ├─ notifyAll
1
2
3
4
5
6
7
8
9
10
簇 方法 核心契约
身份判等族 hashCode / equals / getClass 一致性契约(§3、§4)
生命周期族 clone / toString / finalize 浅深拷贝 + 废弃替代(§5、§6)
线程协作族 wait / notify / notifyAll 必须持锁 + 循环等待(§7)

疑惑:为什么这三簇要塞进 Object,而不是放到接口里?

论证:

  • 身份判等族:每个对象都需要"身份"——HashMap、Set 等基础容器依赖它,不能让用户"忘了实现"
  • 线程协作族:Java 早期把锁与对象绑定(每个对象都有 monitor)——监视器协议必须在 Object 上
  • 生命周期族:clone/toString/finalize 是历史包袱——JDK 1.0 时代的设计,今天已被 Record / @Override toString / try-with-resources 取代

结论:Object 既是"基类契约",也是 Java 早期设计哲学的化石层——它的方法选择反映了 1995 年 Sun 工程师对"对象基础能力"的理解。今天看来三簇里只有"身份判等族"是真正必要的。

# 2.3 native 方法名单

11 个方法中有 8 个是 native(用 C/C++ 实现):

方法 native? 原因
getClass ✅ 必须读 JVM 内部 Klass 指针
hashCode ✅ 默认实现要读对象头或随机值
clone ✅ 要在 JVM 层面分配内存并按位拷贝
notify / notifyAll ✅ 操作 monitor 的 WaitSet
wait ✅ 操作 monitor 的 EntryList + WaitSet
equals ❌(Java 实现) return this == obj;——不需 native
toString ❌(Java 实现) 字符串拼接而已
finalize ❌(Java 实现) 空方法体,由 JVM 在 GC 时调度

结论:Java 实现的 3 个方法(equals、toString、finalize)是用户最可能重写的——这不是巧合,正是 Object 设计者埋的"扩展点"。native 方法基本不让你重写(getClass/wait/notify 还都是 final)。

# 3. equals 契约

# 3.1 五大契约条款

JDK 文档里写得清清楚楚的 5 条数学契约:

契约 公式 含义
自反性 Reflexive x.equals(x) == true 自己等于自己
对称性 Symmetric x.equals(y) ⇔ y.equals(x) 互换两边结果一致
传递性 Transitive x.equals(y) && y.equals(z) → x.equals(z) 等价关系可传递
一致性 Consistent 多次调用结果不变(除非对象状态改变) 幂等
null 处理 x.equals(null) == false 非空对象不等于 null

疑惑:这五条看起来是常识,为什么还要写进契约?

论证:HashMap、HashSet、ArrayList.contains、Collections.replaceAll 等数百个 JDK API 都假设 equals 满足这 5 条——违反任意一条都会导致集合异常。这是契约式编程(Design by Contract)的典型应用——API 文档不是建议,是法律。

# 3.2 默认实现是地址

Object 的默认 equals 实现:

public boolean equals(Object obj) {
    return (this == obj);
}
1
2
3

就这一行——比较的是对象引用(内存地址)。这意味着:

String a = new String("hello");
String b = new String("hello");
System.out.println(a == b);             // false(不同对象)
// 如果 String 没重写 equals,下面也是 false
System.out.println(a.equals(b));        // true(String 重写了)
1
2
3
4
5

结论:默认 equals 等同于 ==——任何业务对象(订单、用户、商品)都应该重写 equals,否则会出现"两个内容相同的对象不相等"的反直觉行为。

# 3.3 对称性陷阱

最经典的对称性破坏案例——父类与子类的 equals:

public class Point {
    private final int x, y;
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof Point)) return false;     // ★ 用 instanceof
        Point p = (Point) o;
        return p.x == x && p.y == y;
    }
}

public class ColorPoint extends Point {
    private final Color color;
    
    @Override
    public boolean equals(Object o) {
        if (!(o instanceof ColorPoint)) return false;   // 子类多比 color
        ColorPoint cp = (ColorPoint) o;
        return super.equals(o) && cp.color == color;
    }
}

// 验证
Point p = new Point(1, 2);
ColorPoint cp = new ColorPoint(1, 2, Color.RED);

System.out.println(p.equals(cp));    // ★ true(Point 角度看:x、y 相等)
System.out.println(cp.equals(p));    // ★ false(ColorPoint 角度看:p 不是 ColorPoint)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

对称性被破坏! p.equals(cp) 与 cp.equals(p) 结果不同。这会导致 HashSet 的诡异行为:

Set<Point> set = new HashSet<>();
set.add(p);
System.out.println(set.contains(cp));    // 时而 true 时而 false(取决于 hash 桶)
1
2
3

疑惑:怎么修?

论证——三种思路:

  1. 改对称:让父类用 getClass() 严格判等——但失去多态判等能力
  2. 组合替代继承:ColorPoint 不继承 Point,而是 class ColorPoint { Point point; Color color; }——彻底绕开
  3. 声明等价边界:在 ColorPoint.equals 里若 o 是纯 Point,则只比 x、y——但破坏传递性

Joshua Bloch 在《Effective Java》第 10 条结论:没有完美方案——优先组合而非继承。这正是 §9.1 Record 直接 final 的设计哲学——禁止继承,从根源消灭对称性陷阱。

# 3.4 instanceof 与 getClass

equals 实现中两种类型检查的取舍:

// 方案 A:instanceof(多态判等)
if (!(o instanceof Point)) return false;
// 优点:子类对象可以与父类对象判等
// 缺点:可能破坏对称性(§3.3)

// 方案 B:getClass(严格判等)
if (o == null || getClass() != o.getClass()) return false;
// 优点:永远不会跨类判等,对称性安全
// 缺点:HibernateProxy / CGLIB 代理对象会判不等
1
2
3
4
5
6
7
8
9

结论:ORM 框架(Hibernate、JPA)选 instanceof,因为代理对象的 getClass 是 User$$EnhancerByCGLIB,与原类不等——会导致"从数据库查回的 User 不等于自己"。一般业务对象选 getClass 更安全。

最佳实践——优先用 Record(§9.1),它直接生成正确的 equals,无需手写。

# 4. hashCode 契约

# 4.1 三大契约条款

hashCode 的 3 条契约:

契约 含义
一致性 同一对象多次调用返回相同值(除非 equals 涉及的字段变了)
equals 等则 hash 等 a.equals(b) → a.hashCode() == b.hashCode() ★ 最重要
不等可哈希相同 a.hashCode() == b.hashCode() ↛ a.equals(b)(哈希冲突允许)

第二条是 §1.1 案例的根因——只重写 equals 不重写 hashCode 时,两个 equals 相等的对象 hashCode 不同——HashSet 把它们扔到不同的桶里,永远不会进入 equals 比较。

# 4.2 与 equals 一致性

回到 §1.1 案例——为什么 HashSet 没去重?

完整的 HashSet.add 流程:

hashSet.add(user2)
   ↓
1. 计算 user2.hashCode() → 假设 938472356(默认实现,基于地址)
2. 定位桶:938472356 % 16 = 4 → 桶 4
3. 桶 4 是空的,直接插入
   
hashSet.add(user1)
   ↓
1. 计算 user1.hashCode() → 假设 102837423(user1 与 user2 地址不同)
2. 定位桶:102837423 % 16 = 11 → 桶 11
3. ★ 桶 11 与桶 4 不同 → 根本不调用 equals → 直接插入
1
2
3
4
5
6
7
8
9
10
11

真相:equals 只在同一个桶内比较 ——hashCode 不一致的对象,永远不会进入 equals 流程。

修复方案:

public class User {
    private Long id;
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User)) return false;
        return Objects.equals(id, ((User) o).id);
    }
    
    @Override
    public int hashCode() {
        return Objects.hash(id);          // ★ 必须与 equals 用相同字段
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

结论:equals 与 hashCode 必须用相同字段计算——这是契约的核心要求。IDE(IDEA / Eclipse)的"Generate equals and hashCode"会强制你两个一起生成,正是为了保护这条契约。

# 4.3 默认实现的真相

Object.hashCode 是 native 方法——它的默认实现到底是什么?

HotSpot 6 种实现策略(由 -XX:hashCode=N 控制):

N 策略 说明
0 Park-Miller RNG 随机数
1 函数(对象地址)异或扰动 与地址相关
2 常量 1 测试用
3 全局自增计数器 单调递增
4 对象地址 直接用地址
5 ★ xorshift 伪随机(默认) 速度+分散性平衡

关键事实:默认 hashCode 与对象地址只有"弱相关"——它使用 xorshift 算法生成伪随机数,计算一次后存入对象头 Mark Word——后续调用直接读缓存,O(1)。

对象头中的 hashCode 位:

64 位 Mark Word (无锁状态):
┌────────────────────────────┬────────┬────────┐
│  identity_hashcode (31位) │age(4位)│0|01(锁状态)│
└────────────────────────────┴────────┴────────┘
            ↑
   ★ 默认 hashCode 缓存在这里
1
2
3
4
5
6

结论:默认 hashCode 只占 31 位(不是 32 位)——比 int 范围少一半。但它的"散列质量"和"稳定性"足以撑起 HashMap 的桶分布。

# 4.4 hash 算法实战

工业级 hashCode 实现的 三种范式:

范式 A:Objects.hash(最简洁)

@Override
public int hashCode() {
    return Objects.hash(id, name, level);   // 内部调用 Arrays.hashCode
}
1
2
3
4

优点:一行搞定,对齐 IDE 生成 缺点:每次会创建 Object[] 数组——热点路径上有性能代价

范式 B:经典 31 倍式

@Override
public int hashCode() {
    int result = id != null ? id.hashCode() : 0;
    result = 31 * result + (name != null ? name.hashCode() : 0);
    result = 31 * result + (level != null ? level.hashCode() : 0);
    return result;
}
1
2
3
4
5
6
7

疑惑:为什么是 31 不是 32 或 30?

论证:

  1. 31 是奇素数——避免位丢失(偶数会让低位永远为 0)
  2. 31 * x = (x << 5) - x——可以被 JIT 优化为左移+减法(比乘法快)
  3. 经验值——大量 String hash 实测显示分散性最佳

范式 C:JDK String 的 polynomial hash

public int hashCode() {
    int h = hash;                            // 缓存
    if (h == 0 && value.length > 0) {
        for (char c : value) h = 31 * h + c;
        hash = h;                            // ★ 写回缓存(不可变保护)
    }
    return h;
}
1
2
3
4
5
6
7
8

亮点:String 对象是不可变的——hashCode 计算一次后永久缓存(即使是 0 重新算一次也不影响正确性)。

结论:业务代码用 Objects.hash 即可(可读性优先),高性能场景手写 31 倍式 + 缓存。

# 5. clone 浅深拷贝

# 5.1 Cloneable 标记接口

回到 §1.2 案例的核心谜题——Cloneable 接口的玄机。

Cloneable 源码:

public interface Cloneable {
    // ★ 一个方法都没有!
}
1
2
3

疑惑:一个空接口,凭什么能让 clone 工作?

论证——这是 Java 早期"标记接口模式"(Marker Interface):

// Object.clone 内部逻辑(伪代码)
protected native Object clone() throws CloneNotSupportedException {
    if (!(this instanceof Cloneable)) {
        throw new CloneNotSupportedException();   // ★ JVM 在这里检查
    }
    // 1. 在堆上分配同样大小的内存
    // 2. 把当前对象的所有字段按位拷贝到新内存
    // 3. 返回新对象引用
    return new_object;
}
1
2
3
4
5
6
7
8
9
10

Cloneable 不是定义"如何 clone",而是给 JVM 一个许可证——"我授权你按位拷贝我"。这是 Java 1.0 设计师的妥协——他们想要一个 clone 机制,但又不想在 Object 上强制所有人实现——于是发明了"标记接口"模式。

Joshua Bloch 在《Effective Java》第 13 条直接抨击:

"The Cloneable interface was intended as a mixin interface for objects to advertise that they permit cloning. Unfortunately, it fails to serve this purpose."

结论:Cloneable 是 Java 设计史上最大的失败之一——它违反了"接口定义行为"的原则、强制 clone 协议绑定 protected 方法、必须捕获不可能发生的 CloneNotSupportedException——一切都很别扭。现代 Java 已不推荐使用,§5.4 给出替代方案。

# 5.2 浅拷贝陷阱

回到 §1.2——super.clone() 到底拷了什么?

JVM 层面的"按位拷贝":

clone 前:
template ────→ Order { orderId=0, coupons=─→ ArrayList[10元券] }

super.clone() 后:
template ────→ Order { orderId=0, coupons=─┐
order1   ────→ Order { orderId=0, coupons=─┼─→ 同一个 ArrayList[10元券]
order2   ────→ Order { orderId=0, coupons=─┘
                                    ★ 引用复制,对象共享
1
2
3
4
5
6
7
8

关键事实:super.clone() 只拷贝引用,不拷贝引用指向的对象——这就是"浅拷贝"。基本类型字段(orderId)按值拷贝,引用类型字段(coupons)只拷贝地址。

所以——order1.getCoupons() 和 order2.getCoupons() 拿到的是同一个 ArrayList——任何一方加元素,全员可见。

结论:clone 默认是浅拷贝——除非你手动深拷贝引用字段。这是 §1.2 事故的真相。

# 5.3 深拷贝四方案

修复 §1.2 的四种思路:

方案 A:手动深拷贝

@Override
public Order clone() {
    try {
        Order copy = (Order) super.clone();
        copy.coupons = new ArrayList<>(this.coupons);   // ★ 新建 List
        // 如果 Coupon 也包含引用字段,还需要递归
        return copy;
    } catch (CloneNotSupportedException e) {
        throw new AssertionError();
    }
}
1
2
3
4
5
6
7
8
9
10
11

优点:性能最优 缺点:易遗漏字段——加新字段忘了同步 clone 是经典 BUG

方案 B:序列化深拷贝

public static <T extends Serializable> T deepClone(T obj) {
    try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
         ObjectOutputStream oos = new ObjectOutputStream(baos)) {
        oos.writeObject(obj);
        try (ObjectInputStream ois = new ObjectInputStream(
                new ByteArrayInputStream(baos.toByteArray()))) {
            return (T) ois.readObject();
        }
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

优点:自动处理任意深度的引用 缺点:性能差(比手动慢 50~100 倍)+ 必须 Serializable

方案 C:JSON 序列化

public static <T> T deepClone(T obj, Class<T> clazz) {
    return JSON.parseObject(JSON.toJSONString(obj), clazz);
}
1
2
3

优点:跨类型(PO → DTO)也能用 缺点:丢失 transient、不支持循环引用、性能一般

方案 D:Apache SerializationUtils / Spring BeanUtils

Order copy = SerializationUtils.clone(template);    // 内部就是方案 B
1

性能对比(10⁶ 次克隆 Order 含 10 个 Coupon):

方案 耗时 备注
手动深拷贝 80 ms 最快
SerializationUtils(方案 B) 4500 ms 慢 56 倍
FastJSON(方案 C) 1200 ms 中等
BeanUtils.copyProperties(浅) 200 ms 注意是浅拷贝

结论:热点路径用方案 A,普通业务用方案 C,慎用方案 B——序列化深拷贝是性能杀手。

# 5.4 拷贝构造器替代

Joshua Bloch 推荐的"拷贝构造器" / "拷贝工厂"模式——彻底抛弃 Cloneable:

public class Order {
    private Long orderId;
    private List<Coupon> coupons;
    
    // 拷贝构造器
    public Order(Order other) {
        this.orderId = other.orderId;
        this.coupons = new ArrayList<>(other.coupons.size());
        for (Coupon c : other.coupons) {
            this.coupons.add(new Coupon(c));     // 递归拷贝
        }
    }
    
    // 或者拷贝工厂
    public static Order copyOf(Order other) {
        return new Order(other);
    }
}

// 用起来
Order copy = new Order(template);
Order copy2 = Order.copyOf(template);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

优势对比:

维度 clone() 拷贝构造器
接口契约 Cloneable 标记 普通构造器
异常处理 必须 try CloneNotSupportedException 无
类型安全 返回 Object,要强转 返回精确类型
继承友好 super.clone 链调用混乱 子类只需 super(other)
final 字段 clone 难以处理 构造器原生支持

结论:新代码永远用拷贝构造器——这是 Joshua Bloch 的明确建议,也是 Spring、Guava 等主流框架的统一做法。Cloneable 仅在维护遗留代码时才使用。

# 6. toString 与 finalize

# 6.1 toString 工程价值

Object 默认的 toString:

public String toString() {
    return getClass().getName() + "@" + Integer.toHexString(hashCode());
}
// 输出:com.example.User@1540e19d
1
2
3
4

毫无价值——这是默认实现的本意:提示你"必须重写"。

重写后的工程收益:

@Override
public String toString() {
    return "User{id=" + id + ", name='" + name + "', level=" + level + "}";
}

User u = new User(100L, "Alice", 3);
log.info("处理用户 {}", u);                  // SLF4J 自动调 toString
                                            // 输出:处理用户 User{id=100, name='Alice', level=3}
1
2
3
4
5
6
7
8

真实场景——三大不可替代价值:

  1. 日志可读性:log.info("user={}", user) 直接打印业务字段,排障不再靠堆栈
  2. 断言失败信息:assertEquals 失败时显示对象内容,不是地址
  3. 调试器查看:IDEA 在变量窗口默认调 toString——一眼看清对象状态

最佳实践:用 IDE 生成 toString,避开敏感字段:

@Override
public String toString() {
    return "User{id=" + id + ", name='" + name + "'}";   // ★ 不打 password / token
}
1
2
3
4

结论:toString 是工程师送给未来自己(半夜排障)的礼物——投入 10 秒重写,节省后续无数小时。

# 6.2 finalize 已被废弃

JDK 9 起 finalize() 被标记 @Deprecated(since="9", forRemoval=true),JDK 21 已正式移除。

疑惑:finalize 当年是用来干什么的?为什么被废?

论证——finalize 的设计意图:在对象被 GC 前提供"最后清理机会"——比如关闭 native 资源、释放文件句柄。

但它有 5 大致命缺陷:

缺陷 后果
执行时机不确定 可能 GC 永远不来——资源永久泄漏
复活对象 finalize 中重新引用 this,对象"诈尸"
性能极差 有 finalize 的对象进入 F-Queue,多走一次 GC,慢 50 倍
异常被吞 finalize 抛异常被 JVM 静默忽略
GC 线程优先级低 高负载下永远不执行

真实事故:某中间件用 finalize 关闭 DirectBuffer,JVM 重启时积压了 200 万个未执行的 finalize 任务——OOM 在所难免。

结论:finalize 是一个"看起来很美"的设计陷阱——它把"资源管理"包装成"GC 副作用",违反了 §10.3 的"机制策略分离"哲学——资源管理是显式的,GC 是隐式的,二者不应混合。

# 6.3 Cleaner 替代方案

JDK 9 引入的 java.lang.ref.Cleaner ——finalize 的现代替代:

public class FileResource implements AutoCloseable {
    private static final Cleaner CLEANER = Cleaner.create();
    
    private final FileChannel channel;
    private final Cleaner.Cleanable cleanable;
    
    public FileResource(Path path) throws IOException {
        this.channel = FileChannel.open(path);
        // ★ 注册清理任务,但不阻塞 GC
        this.cleanable = CLEANER.register(this, new CleanupAction(channel));
    }
    
    @Override
    public void close() {
        cleanable.clean();             // 主动调用清理
    }
    
    // ★ 静态内部类——避免持有外部 this 引用
    private static class CleanupAction implements Runnable {
        private final FileChannel channel;
        CleanupAction(FileChannel channel) { this.channel = channel; }
        
        @Override
        public void run() {
            try { channel.close(); }
            catch (IOException e) { /* log */ }
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

Cleaner 的优势:

  • 在专门的清理线程上运行——不阻塞 GC
  • 不复活对象——CleanupAction 是静态类,不持有 this
  • 可主动调用 clean() ——不依赖 GC 时机

结论:DirectByteBuffer、SocketChannel 等 JDK 内部资源都已迁移到 Cleaner——业务代码只在管理 native 资源时才需要它,普通 IO 用 try-with-resources 即可。

# 6.4 try-with-resources 兜底

99% 的资源管理只需要 try-with-resources:

// JDK 7+ 推荐写法
try (BufferedReader reader = Files.newBufferedReader(path)) {
    return reader.readLine();
}
// 自动调 reader.close()——比 finalize 更准时、更安全、更可控
1
2
3
4
5

结论:实现 AutoCloseable 接口 + try-with-resources 是资源管理的金标准——Cleaner 仅在用户可能"忘了 close"的兜底场景使用。

# 7. 监视器三方法

# 7.1 wait notify 协议

wait/notify/notifyAll 是 Java 最早的线程协作机制——基于"对象监视器"(Object Monitor):

每个对象都有一个 monitor(重量级锁):
┌─────────────────────────────────────┐
│              对象 X                  │
├─────────────────────────────────────┤
│  EntryList   ←  竞争锁的线程队列    │
│  Owner       ←  当前持有锁的线程     │
│  WaitSet     ←  调用 wait 的线程    │
│  Counter     ←  重入次数            │
└─────────────────────────────────────┘
1
2
3
4
5
6
7
8
9

协议流程图:

sequenceDiagram
    participant T1 as 线程1(消费者)
    participant Lock as 对象 X 的 monitor
    participant T2 as 线程2(生产者)
    
    T1->>Lock: synchronized(X) → 进入 Owner
    T1->>Lock: X.wait()
    Note over T1,Lock: 释放锁,进入 WaitSet
    
    T2->>Lock: synchronized(X) → 进入 Owner
    T2->>Lock: X.notify()
    Note over T2,Lock: T1 从 WaitSet 移到 EntryList
    T2->>Lock: synchronized 块结束 → 释放锁
    
    T1->>Lock: 从 EntryList 重新竞争锁
    T1->>Lock: 拿到锁 → 从 wait 处继续执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

核心三步:

  1. wait() — 释放锁 + 进入 WaitSet 等待
  2. notify() — 把 WaitSet 的一个线程移到 EntryList(不立即唤醒)
  3. 被唤醒的线程从 EntryList 重新竞争锁,拿到后从 wait 处继续

# 7.2 必须持有锁

经典踩坑——直接调 wait 抛异常:

Object lock = new Object();
lock.wait();    // ★ IllegalMonitorStateException !
1
2

疑惑:为什么必须在 synchronized 块里调?

论证——wait 的语义是"释放锁 + 等待"——必须先持有锁,才能"释放"。如果不要求 synchronized,那"释放锁"就成了无意义操作,整个协议崩溃。

正确写法:

synchronized (lock) {
    while (!conditionMet) {
        lock.wait();         // ★ 在锁内调,释放 lock 进入 WaitSet
    }
    // 处理业务
}
1
2
3
4
5
6

JVM 检查机制:wait 内部读对象头 Mark Word 中的"锁持有线程 ID"——不是当前线程则抛 IMSE。

结论:wait/notify/notifyAll 三个方法必须在 synchronized 块内调用——这是 JVM 强制的硬约束。

# 7.3 虚假唤醒与循环

虚假唤醒(Spurious Wakeup) ——Java 线程可能在没有 notify 的情况下"自己醒来":

// ❌ 错误:用 if 判断
synchronized (lock) {
    if (!conditionMet) {
        lock.wait();          // 醒来时条件可能仍不满足
    }
    // 处理业务(条件可能没满足!)
}

// ✅ 正确:用 while 循环
synchronized (lock) {
    while (!conditionMet) {
        lock.wait();          // 醒来后再次检查条件
    }
    // 处理业务(条件保证满足)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

疑惑:为什么会虚假唤醒?

论证——这是 POSIX 操作系统的特性:

  1. 信号中断、时钟中断可能导致 pthread_cond_wait 返回
  2. 即使没有 notify,系统调用也可能恢复——这是操作系统给的"性能优化"
  3. Java 沿袭了这个语义——保证跨平台一致

结论:wait 永远写在 while 循环里,不要用 if——这是并发编程的"经典咒语",违反它的代码迟早会出隐藏 BUG。

# 7.4 notify 还是 notifyAll

两种唤醒策略的取舍:

方法 行为 适用场景 风险
notify() 唤醒任意一个 WaitSet 线程 所有等待者都是"等价"的 错过唤醒——某些等待者饿死
notifyAll() 唤醒所有 WaitSet 线程 等待者有不同条件 性能差——惊群效应

Joshua Bloch 的建议:默认用 notifyAll,除非性能瓶颈才换 notify——错过唤醒的 BUG 比惊群难调试得多。

真实案例——生产者-消费者模型:

// 错误:用 notify
synchronized (queue) {
    while (queue.size() == CAPACITY) queue.wait();
    queue.add(item);
    queue.notify();         // ★ 可能唤醒一个生产者,而消费者饿死
}

// 正确:用 notifyAll
synchronized (queue) {
    while (queue.size() == CAPACITY) queue.wait();
    queue.add(item);
    queue.notifyAll();      // ★ 所有等待者都重新检查条件
}
1
2
3
4
5
6
7
8
9
10
11
12
13

结论:生产环境优先 notifyAll,性能压测后再换 notify——这是 §9.3 中 Lock + Condition 出现的根本原因——它能精确唤醒"对应条件"的线程,规避这个两难选择。

# 8. getClass 与反射入口

# 8.1 final 不可重写

Object.getClass 的签名:

public final native Class<?> getClass();
1

两个修饰符:

  • final — 子类不能重写
  • native — 由 JVM 实现

疑惑:为什么 final?

论证:getClass 返回的是对象的实际运行时类型——这是 JVM 的"绝对真理",由对象头中的 Klass 指针决定。如果允许重写,类型系统就崩了——你可以让 User 对象声称自己是 Admin,反射、序列化、安全检查全部失效。

结论:getClass 是 JVM 类型系统的根 ——不可重写是必然的安全设计。

# 8.2 与 instance.class 对比

获取 Class 对象的两种方式:

User user = new User();

Class<?> c1 = user.getClass();       // 运行时类型——动态
Class<?> c2 = User.class;            // 编译时类型——静态
1
2
3
4

关键差异:

User user = new Admin();             // Admin extends User
System.out.println(user.getClass()); // class Admin(运行时实际类型)
System.out.println(User.class);      // class User(声明类型)
1
2
3

结论:反射、序列化、equals 用 getClass();类型字面量、注解读取、泛型 token 用 .class。两者永远不会等价——记住这条铁律。

# 8.3 与 getDeclaringClass

进阶——三个 "Class" 的区分:

Method method = SomeClass.class.getMethod("doSomething");
Class<?> c1 = method.getDeclaringClass();   // 方法在哪个类声明
Class<?> c2 = method.getReturnType();        // 返回值的类型
Class<?> c3 = method.getClass();             // method 对象本身的类(Method.class)
1
2
3
4

结论:getDeclaringClass 是 Method/Field/Constructor 的契约——反射场景用它定位元素归属。普通对象用 getClass 即可。

# 9. 现代替代方案

# 9.1 Record 自动实现

JDK 14 预览、JDK 16 正式发布的 Record——自动生成 equals/hashCode/toString:

public record User(Long id, String name, Integer level) { }

// 等价于(编译器自动生成):
public final class User extends Record {
    private final Long id;
    private final String name;
    private final Integer level;
    
    public User(Long id, String name, Integer level) { ... }
    public Long id() { return id; }
    public String name() { return name; }
    public Integer level() { return level; }
    
    @Override
    public boolean equals(Object o) {
        // 使用 invokedynamic 自动比较所有字段
    }
    
    @Override
    public int hashCode() {
        // 使用 invokedynamic 自动哈希所有字段
    }
    
    @Override
    public String toString() {
        return "User[id=" + id + ", name=" + name + ", level=" + level + "]";
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

核心优势:

  • 三方法编译器自动生成——零样板代码
  • final 类——杜绝 §3.3 对称性陷阱(不能继承 = 不会出问题)
  • 不可变 — 字段全 final,天生线程安全
  • 自动实现 Comparable + Serializable 友好

回扣 §1.1 案例:

public record User(Long id) { }      // 一行搞定
Set<User> blacklist = new HashSet<>();
blacklist.add(new User(100L));
blacklist.add(new User(100L));
blacklist.add(new User(100L));
System.out.println(blacklist.size());     // ★ 1 !自动正确去重
1
2
3
4
5
6

结论:新代码遇到"不可变数据载体"场景一律用 Record——它是 Java 用 30 年走向"零样板"的最大里程碑。详细原理留到第 30 篇展开。

# 9.2 Lombok 字节码生成

Record 出现前的"业界事实标准":

@Data                       // 等价于 @Getter @Setter @ToString @EqualsAndHashCode
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Long id;
    private String name;
    private Integer level;
}
1
2
3
4
5
6
7
8

Lombok 在编译期通过注解处理器(APT)生成字节码——源码看起来 5 行,class 文件 200 行。

Lombok vs Record:

维度 Lombok Record
JDK 要求 任意版本 16+
可变性 默认可变(@Data 含 setter) 强制不可变
继承 可继承 final 不可继承
兼容性 第三方库,需 IDE 支持 JDK 原生

结论:遗留项目(JDK 8/11)用 Lombok,新项目(JDK 17+)用 Record——逐步淘汰 Lombok 是技术演进的必然。

# 9.3 Lock Condition 替代

JDK 5 引入的 java.util.concurrent.locks.Lock + Condition ——彻底取代 wait/notify:

private final Lock lock = new ReentrantLock();
private final Condition notFull = lock.newCondition();    // ★ 多个 Condition
private final Condition notEmpty = lock.newCondition();

public void put(E e) throws InterruptedException {
    lock.lock();
    try {
        while (count == capacity) {
            notFull.await();        // 等待"未满"条件
        }
        // 入队...
        notEmpty.signal();          // 通知"非空"条件——精确唤醒消费者
    } finally {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

优势对比:

维度 wait/notify Lock/Condition
条件数 一个对象只能有 1 组 WaitSet 任意多个 Condition
精确唤醒 notify 随机唤醒 signal 唤醒特定条件的线程
中断响应 wait 会响应中断 await 可选 lockInterruptibly
超时 wait(ms) 不区分超时与唤醒 awaitNanos 返回剩余时间
公平性 不公平 可选公平锁

结论:新代码永远用 Lock + Condition——除非维护遗留代码或学习线程协作底层。这是 §10.3 设计哲学的体现——精细化控制 > 粗放式控制。

# 10. 综合案例串讲

# 10.1 双案例真相揭晓

回到第 1 章的两起事故,逐条揭晓:

① equals 重写了为什么 HashSet 还不去重:HashSet 内部是 HashMap——add 流程先算 hashCode 定位桶,再用 equals 比对桶内元素。hashCode 不重写时,三个 User 对象有三个不同的默认 hashCode(基于地址),落在三个不同的桶——根本不会进入 equals 比较(§4.1、§4.2)。

② hashCode 不重写时 JVM 默认实现:HotSpot 默认策略 5——xorshift 伪随机,与对象地址只有"弱相关",计算后缓存在对象头 Mark Word 的 31 位 identity_hashcode 区域(§4.3)。

③ equals 与 hashCode 的契约:核心是第 2 条—— a.equals(b) → a.hashCode() == b.hashCode()。任何用 equals 判断的字段都必须参与 hashCode 计算。IDE 的"Generate equals and hashCode"会强制两个一起生成——正是为了保护这条契约(§4.2、§4.4)。

④ super.clone() 浅拷贝:JVM 在堆上分配同样大小的内存,按位拷贝所有字段——基本类型按值复制,引用字段只复制地址。所以 §1.2 中三个 Order 共享同一个 ArrayList,任何一方加元素全员可见(§5.2)。

⑤ Cloneable 空接口的玄机:它是"标记接口"——告诉 JVM"我授权按位拷贝"。Object.clone 内部检查 instanceof Cloneable,否则抛 CloneNotSupportedException。这是 Java 1.0 的设计妥协,Joshua Bloch 在《Effective Java》中明确批评为失败设计(§5.1)。

⑥ clone 现代替代:拷贝构造器 / 拷贝工厂——new Order(other) 或 Order.copyOf(other)。优势是返回精确类型、无异常处理、final 字段友好、子类继承友好(§5.4)。再进一步——用 Record 直接获得不可变副本语义(§9.1)。

⑦ wait 必须在 synchronized 里调用:wait 的语义是"释放锁 + 等待"——必须先持有锁才能"释放"。JVM 在 wait 内部读对象头 Mark Word 检查锁持有线程,不是当前线程则抛 IllegalMonitorStateException(§7.2)。wait 还必须写在 while 循环里防虚假唤醒(§7.3)——这是并发的经典咒语。

§1.1 修复方案:用 Record public record User(Long id) { } ——一行解决,自动生成正确的 equals/hashCode/toString,HashSet 完美去重。

§1.2 修复方案:抛弃 Cloneable,改用拷贝构造器 new Order(template) ——内部递归 new ArrayList<>(other.coupons) ——三个 Order 拥有各自的优惠券列表。

# 10.2 一个对象的一生

把 User user = new User(100L, "Alice", 3) 这个最普通的对象,串成一棵生命树,回扣本篇所有方法:

T 0      创建对象
         User user = new User(100L, "Alice", 3);
         [01篇] JVM 在堆上分配内存
         [04篇] 对象头初始化(Mark Word:未锁定 + age=0 + hashCode 暂未计算)
         
T+1ms    第一次调用 hashCode()
         user.hashCode()
         ★ JVM 用 xorshift 算法生成 31 位伪随机数 ──→ 写入 Mark Word
         后续调用直接读缓存,O(1)
         
T+2ms    放入 HashSet
         set.add(user);
         ★ HashMap 内部调 user.hashCode() 定位桶
         ★ 桶内调 user.equals(existing) 判等
         
T+3ms    日志打印
         log.info("user={}", user);
         ★ 自动调 user.toString() ──→ "User{id=100, name='Alice', level=3}"
         
T+4ms    线程协作
         synchronized (user) {
             while (!ready) user.wait();    ── 释放 user 的 monitor,进入 WaitSet
         }
         [09篇] 线程从 RUNNABLE 转 WAITING
         
T+5ms    被唤醒
         其他线程:synchronized (user) { user.notifyAll(); }
         ★ user 的 WaitSet 中所有线程进入 EntryList,重新竞争锁
         
T+10ms   反射读取类型
         Class<?> clazz = user.getClass();
         ★ 直接读 Mark Word 中的 Klass 指针(最快路径)
         
T+100ms  克隆(不推荐,但举例)
         User copy = user.clone();
         ★ Object.clone 检查 Cloneable → 按位拷贝 → copy 的 Mark Word 重置
         
T+1s     从根集合断开引用
         user = null;
         [03篇] 进入年轻代下次 GC 的回收候选
         
T+5s     被 GC 回收
         ★ 现代 JDK 不再调用 finalize(§6.2 已废弃)
         ★ 如果注册了 Cleaner,由 Cleaner 线程执行清理任务
         
跨篇引用全景:
         [01] 内存分配 + Mark Word 初始化
         [03] GC 回收
         [04] HashMap 桶定位用 hashCode
         [08] synchronized 锁升级用 Mark Word
         [09] wait/notify 操作 monitor
         [22] LinkedHashMap 也依赖 hashCode/equals
         [23] Integer/String 的 equals 实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

这条时间线串起本篇 100% 的方法 + 卷一二的 8 篇知识点——一个对象的全生命周期就是 Object 11 个方法的协奏曲。

# 10.3 设计哲学回扣

跳出技术细节,提炼三条贯穿 Object 设计的工程哲学:

  1. 契约即法律:equals 五条款、hashCode 三条款、wait 必须持锁——这些不是"建议",是"硬约束"。违反契约的代码不会立即崩溃,但会在某个深夜的 HashMap 查询中爆炸。这条原则贯穿所有标准库——Comparable 的 compareTo、Serializable 的 serialVersionUID、AutoCloseable 的 close 幂等性——API 文档是法律,不是建议。这与卷二第 19 篇 ArrayList 的 fail-fast、第 20 篇 ConcurrentHashMap 的 size() 取舍同源。

  2. 机制与策略分离:finalize 的失败正是因为它把"资源管理"(策略)塞进 GC(机制)——资源管理应该是显式的(close()),GC 应该是隐式的(不可控时机)。Cleaner 的成功在于把两者解耦——清理任务交给独立线程,不阻塞 GC。这条原则贯穿现代 JDK 设计——VirtualThread 把"调度策略"与"线程机制"分离、Loom 把"用户态调度"与"内核态执行"分离——好设计永远在分离,差设计永远在耦合。

  3. 替代不一定颠覆:Record 替代手写 equals/hashCode、Lock + Condition 替代 wait/notify、try-with-resources 替代 finalize——但老 API 都没被删除,因为生态需要兼容。这条哲学贯穿 Java 30 年——保留向后兼容的同时引入新范式——是它能成为最长寿语言的核心原因。Cloneable 留着不是因为它好,而是因为有人在用——工程不是科学,妥协才是常态。

# 10.4 Object 方法速查表

最后一张表——Object 11 方法的"何时用、何时避":

方法 何时重写 何时避开 现代替代
equals 业务对象(值对象) 实体对象(用 ID 判等) Record 自动生成
hashCode 重写 equals 必须重写 — Record / Lombok
toString 几乎所有业务对象 含敏感字段时小心 Record / Lombok
clone ❌ 永远不要 永远 拷贝构造器 / Record
finalize ❌ 已废弃 永远 Cleaner / try-with-resources
wait/notify ❌ 学习用 新代码避开 Lock + Condition
getClass ❌ final 不可重写 — —

Object 方法铁律 7 条:

1. 重写 equals 必须重写 hashCode,必须用相同字段
2. equals 优先用 Record,遗留代码用 Objects.equals 写,IDE 生成
3. 永远不要 implements Cloneable,永远用拷贝构造器
4. toString 是给未来自己的礼物,所有业务对象必须重写
5. finalize 已废弃,资源管理用 try-with-resources + Cleaner
6. wait 必须在 synchronized 里、必须 while 循环、优先 notifyAll
7. 新代码遇到"不可变值对象"一律用 Record
1
2
3
4
5
6
7

至此第 24 篇完成——我们用 1.7 万字把 Object 11 方法的契约、hashCode/equals 一致性、Cloneable 失败设计、finalize 废弃替代、监视器协议讲透。卷二容器与基础数据结构 8 篇全部收官 ✅。

下一篇我们顺着"Java 类型系统底层真相"这条线,进入卷三第 25 篇:枚举原理与最佳实践——把 enum 即 final class、values() 反射机制、单例枚举、EnumMap/EnumSet 位运算优化一次讲透,揭开"看似简单的 enum 关键字"背后的 Class<E extends Enum> 类型递归玄机。

上次更新: 2026/06/10, 11:13:41
Java数字类型原理
泛型擦除与类型系统

← Java数字类型原理 泛型擦除与类型系统→

最近更新
01
信号崩溃快速排查
06-15
02
CoreDump破案
06-15
03
perf火焰图实战
06-15
更多文章>
Theme by Vdoing | Copyright © 2019-2026 杨充 | MIT License | 桂ICP备2024034950号 | 桂公网安备45142202000030
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式