8.数据拷贝设计原理
# 35.数据拷贝设计原理
📍 本篇位置:第 4 卷 · 内存与资源 · 第 5 篇(卷收官) 🎯 核心矛盾:安全要独立数据 vs 性能要共享数据 —— 拷贝是"保险费",用钱换确定性 🧭 设计灵魂:拷贝三梯度——浅拷贝 / 深拷贝 / COW 写时复制;COW 是最优解:读零成本 + 写才付费 🌐 跨语言覆盖:C++(拷贝构造 + 移动语义) · Java(Cloneable + 序列化深拷贝) · Swift(结构体 COW 自动) · Go(默认值拷贝) · JavaScript(structuredClone / 扩展运算符) 🔗 延伸阅读:← 34.多种引用技术设计 · → 40.窗口核心设计思想 · → 03.值型变量和引用
flowchart LR
A[需要复制数据] --> B1[浅拷贝<br/>只复制引用]
A --> B2[深拷贝<br/>递归复制全部]
A --> B3[COW<br/>共享 + 写时才拷]
B1 --> C1[最快 + 最危险]
B2 --> C2[最安全 + 最贵]
B3 --> C3[最优解<br/>读共享 / 写独立]
style B3 fill:#d4edda
style C3 fill:#d4edda
2
3
4
5
6
7
8
9
# 目录介绍
- 00.订单详情被串改事故说起
- 01.对象拷贝有哪些
- 02.理解浅拷贝
- 03.理解深拷贝
- 04.序列化进行拷贝
- 05.延迟拷贝
- 06.如何选择拷贝方式
- 07.数组的拷贝
- 08.集合的拷贝
- 09.跨语言拷贝机制对比
- 10.拷贝设计总结
- 11.经典陷阱与生产级反模式
- 12.一句话总结:拷贝设计哲学
# 00.订单详情被串改事故说起
# 0.1 投诉:刚提交订单价格自变
某电商 App 2020 年上线了新的「订单详情页 → 修改地址」流程。产品要求:用户点击修改地址后,进入编辑页修改,保存前不应该影响原订单——如果用户最终取消,应该回到原始数据。
上线后第三天,客服突然涌入大量投诉:
"我刚提交的订单,原价 199 的商品,过了 2 小时变成 0 元了!" "我修改地址只是看了一眼省份,啥都没改,回去发现订单总价对不上了!" "更离谱的是有人说他订单收货人变成了别人的名字!"
工程师调出代码,坚信代码没问题:
// 进入"修改地址"页时,把订单数据传过去
public void onClickEditAddress(Order order) {
// 拷贝一份给编辑页,避免影响原始订单
Order editingOrder = order.clone();
startEditPage(editingOrder);
}
// Order 的 clone 方法
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone(); // ← 用了 Cloneable,应该没问题吧?
}
2
3
4
5
6
7
8
9
10
11
12
"我都加了 clone() 了,怎么还会互相影响?"——这是事故现场最高频的灵魂之问。
# 0.2 老板的灵魂三问
问题 1:你确定 super.clone() 把所有字段都"独立复制"了吗?
工程师:嗯……Object.clone() 不就是拷贝所有字段嘛。
老板:你的 Order 里有 List<Item>、有 Address 对象、有 User 对象,
这些"引用类型字段"被复制后,是引用的同一个对象,还是新对象?
工程师:……
2
3
4
问题 2:单元测试为什么测不出来?
工程师:单测都过了,clone 后两个对象的字段都"看起来一样"。
老板:你测的是"看起来一样",可"看起来一样" ≠ "完全独立"。
你的测试有没有改一下副本的内层对象,再去看原对象?
工程师:……没有。
2
3
4
问题 3:为什么这种 Bug 总是延迟暴露?
工程师:因为大部分用户改完就保存了,覆盖了原数据看不出来。
老板:所以这是个潜伏期 Bug——它在等"修改 + 取消"这个组合出现。
生产环境跑几天,自然就有几千个用户踩中这个组合。
2
3
# 0.3 用慢动作回放看真相
把工程师的代码逐字段慢放,事故就一目了然:
原始订单 order: clone 后 editingOrder:
┌─────────────────────────────┐ ┌─────────────────────────────┐
│ orderNo: "20200312001" │ │ orderNo: "20200312001" │ ← 字符串:独立 ✓
│ totalPrice: 199.0 │ │ totalPrice: 199.0 │ ← 基本类型:独立 ✓
│ items: ──────────────┐ │ │ items: ──────────────┐ │
│ address: ─────────┐ │ │ │ address: ─────────┐ │ │
│ user: ────────┐ │ │ │ │ user: ────────┐ │ │ │
└────────────────┼───┼──┼─────┘ └────────────────┼───┼──┼─────┘
│ │ │ │ │ │
▼ ▼ ▼ │ │ │
┌─────────────────┐ │ │ │
│ User对象 ◄─────┼─────────────────────┘ │ │
│ Address对象 ◄──┼─────────────────────────┘ │
│ List<Item> ◄───┼────────────────────────────┘
└─────────────────┘
☠ 同一份数据被两个 Order 共享!
☠ 编辑页改 address.city,原订单的 address.city 跟着变!
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这就是经典的"浅拷贝陷阱"——Object.clone() 默认行为只复制对象本身的"字段槽位",引用类型的字段槽里装的还是原对象的引用。两个 Order 看似独立,实则共享所有内层对象。
flowchart TB
Q[订单串改事故] --> R1[表层认知<br/>"我加了 clone()"]
Q --> R2[深层认知<br/>clone 默认是浅拷贝]
Q --> R3[本质认知<br/>"独立"分多个层级<br/>顶层独立 ≠ 全树独立]
R1 -.错觉.-> R2
R2 -.根因.-> R3
style Q fill:#f8d7da
style R3 fill:#fff3cd
2
3
4
5
6
7
8
# 0.4 这个事故揭示了什么
工程师对 clone() 的直觉建立在**"复制 = 独立"**的朴素心智模型上:
我以为:
clone 后两个对象完全独立,改 A 不影响 B
实际:
clone 是"位拷贝"——只复制字段槽位本身
字段槽里的引用还指向同一个内层对象
→ "对象壳子独立,对象身体共享"
2
3
4
5
6
7
这个错位,本质上是"数据独立性"在三个层级上的张力:
| 层级 | 含义 | 工程师以为 | 实际 |
|---|---|---|---|
| 引用层 | 两个变量指向同一对象 | 不会 | clone 后不会 |
| 顶层字段 | 同一对象的字段槽 | 会独立 | clone 后会独立 |
| 内层引用对象 | 字段槽指向的对象 | 会独立 | clone 后仍共享! |
整个数据拷贝设计的核心矛盾就藏在这里:
"独立"不是布尔值,是个梯度——你想独立到哪一层?
# 0.5 五个层层递进的追问
带着"订单串改事故",整篇文章其实就是在回答下面五个递进的问题:
| 追问 | 答案章节 |
|---|---|
| 为什么"复制"会有那么多种?我直觉里复制就是复制 | §01 / §1.3 |
| 浅拷贝默认共享内层,那它存在的意义是什么? | §02 |
| 深拷贝能解决一切,为什么不默认深拷贝? | §03 / §06 |
| "改了再复制"的 COW 真的更好吗?什么时候反而是坑? | §05 |
| 为什么 Java、C++、Swift、Rust 给出的答案完全不同? | §09 / §11 |
# 0.6 三层解药预演
后面会展开,这里先把三把"解药"清单列出来,让读者带着对照感往下读:
解药 1(默认浅): 接受"共享内层",但只读使用
→ DTO、值对象、不可变对象
→ 代价:必须保证下游绝不修改
解药 2(手动深): 显式递归复制每一层
→ 重写 clone()、构造函数、序列化
→ 代价:性能、维护、循环引用处理
解药 3(编译期保证):让类型系统强制独立性
→ Rust 的 Move 语义、Swift 的 Value Type
→ 代价:心智模型转变(从"对象图"到"所有权")
2
3
4
5
6
7
8
9
10
11
带着这次事故的"具体感",进入正题——你将看到,所有抽象的"浅/深/COW"原理,最终都能落到这次订单串改的根因图上。
# 01.对象拷贝有哪些
# 1.1 为何需要拷贝
- 在Java中,拷贝(Copy)操作是常见的,它涉及将一个对象的值复制到另一个对象中。拷贝操作在许多情况下是有用的:
- 防止数据修改:通过拷贝对象,可以创建一个新的对象,使其具有相同的值。如果对其中一个对象进行修改,不会影响到原始对象。这在需要保护数据完整性的情况下很有用,特别是当多个对象需要独立操作相同数据时。
- 传递不可变性:在Java中,字符串(String)和基本数据类型(如整数、浮点数等)是不可变的。当需要将这些不可变对象传递给其他方法或对象时,拷贝操作可以确保传递的是对象的副本,而不是引用。这样可以防止外部修改原始对象。
- 多线程安全:在多线程环境下,如果多个线程需要同时访问同一个对象,为了避免竞态条件和数据不一致的问题,可以使用拷贝操作创建每个线程的私有副本。这样每个线程都可以独立地操作自己的副本,而不会影响其他线程。
- 数据备份:有时候需要对数据进行备份,以便在需要时可以还原到之前的状态。通过拷贝操作,可以创建数据的副本,以备份或存档目的。
- Java中的拷贝操作是为了保护数据完整性、传递不可变性、实现多线程安全以及进行数据备份等目的。
- 通过拷贝操作,可以创建对象的副本,使其具有独立的状态,以满足不同的需求。
# 1.2 数据拷贝的场景
- 在多线程环境下,多个线程可能同时访问和修改共享的数据。
- 为了避免竞态条件和数据不一致的问题,可以使用数据拷贝创建每个线程的私有副本。这样每个线程都可以独立地操作自己的数据副本,而不会影响其他线程。
- 数据传递,当需要将数据传递给其他方法、对象或线程时会使用拷贝
- 通过数据拷贝可以确保传递的是数据的副本,而不是引用。这样可以防止外部修改原始数据,保持数据的不可变性和安全性。
- 数据备份和还原:有时候需要对数据进行备份,以便在需要时可以还原到之前的状态。
- 通过数据拷贝,可以创建数据的副本,以备份或存档目的。这对于数据的恢复、回滚或历史记录等操作非常有用。
- 数据缓存:在某些情况下,为了提高性能,可以使用数据拷贝将数据缓存到内存中
- 这样可以避免频繁地从磁盘或网络中读取数据,提高数据访问的速度。
# 1.3 拷贝类型有哪些
- 对象拷贝(Object Copy)就是将一个对象的属性拷贝到另一个有着相同类类型的对象中去。在程序中拷贝对象是很常见的,主要是为了在新的上下文环境中复用对象的部分或全部数据。
- Java中有三种类型的对象拷贝:浅拷贝(Shallow Copy)、深拷贝(Deep Copy)、延迟拷贝(Lazy Copy)。
# 探索过程:为什么"复制"恰好是三档梯度?
读到这里,工程师的第一反应是:"复制就是复制,怎么会有三种?"——这个追问其实是理解整章的关键。
追问 1:能不能只有"复制 / 不复制"两种?
最朴素的二元论是:"要么复制(独立),要么不复制(共享)"。但这套二元论在订单事故里立刻崩塌:
不复制:editingOrder = order
→ 编辑页改任何字段,原订单都跟着变(顶层都没独立)
完全复制:递归把 user / address / items 全 new 一份
→ 顶层独立,内层也独立
→ 但是!每次进编辑页都要 deep copy 上千个 Item,性能爆炸
2
3
4
5
6
朴素二元论的问题是:它不区分"独立到哪一层"。
追问 2:那"独立的层级"有几档?
把对象想象成一棵"引用树":
Order (根)
/ | \
User Address List<Item>
|
Item × N
2
3
4
5
复制时,沿着这棵树往下走,走多深就停,决定了独立的强度:
| 走的深度 | 名称 | 复制成本 | 独立强度 |
|---|---|---|---|
| 走 0 层(不复制) | 引用赋值 | O(1) | ❌ 完全共享 |
| 走 1 层(只根节点) | 浅拷贝 | O(1) | ⚠️ 顶层独立,内层共享 |
| 走到底(递归到叶) | 深拷贝 | O(n) 递归 | ✅ 完全独立 |
| 走 1 层 + 写时再走深 | COW (延迟拷贝) | 读 O(1) / 写时 O(n) | ⏳ 按需独立 |
这就是"恰好三档"的根因:浅与深是两个端点,COW 是工程上发现的"中间最优解"。
追问 3:为什么不是四档、五档?比如"走 2 层"?
理论上完全可以——叫"半深拷贝"。但工程实践中,"走 2 层"永远不是稳定边界:
你今天定义"走 2 层":复制 Order + 复制它的 Address,但不复制 Address.country
明天产品需求变化:country 也要改了
→ 这条边界要不停往下推,最终变成"全部递归"或"全部不递归"
2
3
所以工程上的稳定切分只有两端 + 一个延迟优化,三档结构是几十年实践收敛的结果,不是设计师拍脑袋。
追问 4:那订单事故应该选哪一档?
带回我们的事故:
| 选择 | 后果 |
|---|---|
| 引用赋值 | 编辑页直接改原订单 → 这就是 Bug 本身 |
| 浅拷贝(事故现场) | 顶层独立 ≠ 全独立,仍出 Bug |
| 深拷贝 | ✅ 解决,但每次进编辑页都要深复制完整订单树 |
| COW | ✅ 最优——进编辑页零成本,用户真正改某字段时才独立 |
事故的真正"标准解"是 COW——读零成本、写才付费。但 Java 没有内置 COW 支持,所以现实中事故组只能选"深拷贝"作为可行解。这正是为什么 Swift / Rust 在这件事上比 Java 优雅得多——它们把 COW 做进了语言原生(详见 §09 / §11)。
# 这一段的认知跃迁
| 表层认知 | 深层认知 |
|---|---|
| "复制就是复制" | 复制是个"递归到第几层"的工程选择 |
| "三种拷贝是 Java 的设计" | 三种拷贝是对象图复制问题的数学切分,所有语言都得面对 |
| "用 clone 就行" | clone 默认只走 1 层,深复制必须显式编码每一层 |
| "性能与正确性不可兼得" | COW 是"读路径正确性免费、写路径性能可控"的双赢 |
带着这个梯度模型,下面三章 §02/§03/§05 就是分别拆解这三档的内部机制和代价边界。
# 02.理解浅拷贝
# 2.1 什么是浅拷贝
- 浅拷贝(Shallow Copy):浅拷贝创建一个新对象,该对象与原始对象共享相同的引用类型属性。
- 换句话说,浅拷贝只复制对象的引用,而不复制引用指向的实际对象。
- 这意味着对于引用类型属性的修改会影响到原始对象和副本对象。在Java中,可以使用clone()方法来实现浅拷贝。
# 内存模型:浅拷贝到底"浅"在哪里
回到 §0 的订单事故,把浅拷贝的内存视图画清楚:
栈 堆
───── ─────────────────────
order ───────► ┌──────────────────┐
│ Order #1 │
│ totalPrice: 199 │ ← 基本类型槽:值就是值
│ orderNo: "..." │ ← String 引用 → 指向常量池
│ user: ──────────┐│
│ address: ───────┼┼───┐
│ items: ─────────┼┼───┼───┐
└──────────────────┘│ │ │
│ │ │
editingOrder ───────► ┌──────────────────┐│ │ │
│ Order #2 ││ │ │ ← 顶层是新对象 ✓
│ totalPrice: 199 ││ │ │
│ orderNo: "..." ││ │ │
│ user: ──────────┘│ │ │
│ address: ────────┼───┘ │ ← 引用槽指向同一对象 ✗
│ items: ──────────┼───────┘
└──────────────────┘
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"浅"的精确含义:
浅拷贝复制的是对象本身的内存槽位(一行字段表);引用类型字段的"槽位值"是一个地址,复制地址 = 复制指针 = 仍指向同一个内层对象。
# 反向追问:Object.clone 凭什么默认是浅?
读到这里工程师会愤怒:
"Java 既然提供了
clone(),为什么默认搞个浅拷贝坑人?直接深拷贝多好!"
这是个值得追问的设计选择。让我们从 JDK 设计师的视角推演:
理由 1:性能可预测
浅拷贝是 O(1) ——一次 memcpy 或者 JVM 内部的字段批量复制。深拷贝是 O(n) 且 n 不可预测:
如果 Object.clone() 默认深拷贝:
你 new 一个对象引用了 ConfigManager(全局单例)
→ 默认深拷贝会顺着引用走过去,把整个 ConfigManager 深复制
→ 包括它内部的 1 万个配置项、连接池、线程池
→ 一次"看起来无害的 clone()"可能复制几百 MB
2
3
4
5
更糟糕的是:
你的对象引用了一个 Cache,Cache 引用了一个 Database 连接池
→ 默认深拷贝试图复制连接池
→ 连接池对象不可序列化(它持有 Socket)
→ 直接抛异常
2
3
4
所以 JDK 设计师选择了"绝对最小公共子集":复制能复制的(字段槽位),不碰可能炸掉的(递归遍历引用图)。
理由 2:由调用方负责语义
只有调用方知道"你想要哪一档独立":
有时候你只想要顶层独立(典型场景:只读 DTO 传输)
有时候你想要内层独立(典型场景:可变状态共享)
有时候你想要 COW(典型场景:大对象的写时分裂)
2
3
设计师的选择是:默认给最便宜、最快、最确定的那个,剩下的让调用方自己写。这是 Unix 哲学"机制 vs 策略"在 API 设计上的回响。
理由 3:历史包袱
Object.clone() 1995 年随 JDK 1.0 发布,那时连泛型都没有,更别说"如何递归识别引用图"。设计师选择了"protected + 浅复制 + 抛 CloneNotSupportedException"这个最保守的组合。Joshua Bloch 在《Effective Java》里干脆建议"避免使用 Cloneable"——这是历史遗留 API 最坏的一面。
# 这一段的认知跃迁
| 表层认知 | 深层认知 |
|---|---|
| "浅拷贝是 Java 的偷懒" | 浅拷贝是"机制最小化"的设计哲学 |
| "默认就该深拷贝" | 默认深拷贝在引用图里会引发计算爆炸 |
| "clone 不好用" | clone 是 1995 年的产物,现代代码应避开它 |
§3 我们来看,如果你愿意付 O(n) 代价,怎么把"独立"做到底。
# 2.2 如何实现浅拷贝
下面来看一看实现浅拷贝的一个例子
public class Subject { private String name; public Subject(String s) { name = s; } public String getName() { return name; } public void setName(String s) { name = s; } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class Student implements Cloneable { // 对象引用 private Subject subj; private String name; public Student(String s, String sub) { name = s; subj = new Subject(sub); } public Subject getSubj() { return subj; } public String getName() { return name; } public void setName(String s) { name = s; } /** * 重写clone()方法 * @return */ public Object clone() { //浅拷贝 try { // 直接调用父类的clone()方法 return super.clone(); } catch (CloneNotSupportedException e) { return null; } } }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如下所示
private void test1(){ // 原始对象 Student stud = new Student("杨充", "潇湘剑雨"); System.out.println("原始对象: " + stud.getName() + " - " + stud.getSubj().getName()); // 拷贝对象 Student clonedStud = (Student) stud.clone(); System.out.println("拷贝对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName()); // 原始对象和拷贝对象是否一样: System.out.println("原始对象和拷贝对象是否一样: " + (stud == clonedStud)); // 原始对象和拷贝对象的name属性是否一样 System.out.println("原始对象和拷贝对象的name属性是否一样: " + (stud.getName() == clonedStud.getName())); // 原始对象和拷贝对象的subj属性是否一样 System.out.println("原始对象和拷贝对象的subj属性是否一样: " + (stud.getSubj() == clonedStud.getSubj())); stud.setName("小杨逗比"); stud.getSubj().setName("潇湘剑雨大侠"); System.out.println("更新后的原始对象: " + stud.getName() + " - " + stud.getSubj().getName()); System.out.println("更新原始对象后的克隆对象: " + clonedStud.getName() + " - " + clonedStud.getSubj().getName()); }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21输出结果如下:
2019-03-23 13:50:57.518 24704-24704/com.ycbjie.other I/System.out: 原始对象: 杨充 - 潇湘剑雨 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 拷贝对象: 杨充 - 潇湘剑雨 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象是否一样: false 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象的name属性是否一样: true 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 原始对象和拷贝对象的subj属性是否一样: true 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新后的原始对象: 小杨逗比 - 潇湘剑雨大侠 2019-03-23 13:50:57.519 24704-24704/com.ycbjie.other I/System.out: 更新原始对象后的克隆对象: 杨充 - 潇湘剑雨大侠1
2
3
4
5
6
7可以得出的结论
- 在这个例子中,让要拷贝的类Student实现了Clonable接口并重写Object类的clone()方法,然后在方法内部调用super.clone()方法。从输出结果中我们可以看到,对原始对象stud的"name"属性所做的改变并没有影响到拷贝对象clonedStud,但是对引用对象subj的"name"属性所做的改变影响到了拷贝对象clonedStud。
# 03.理解深拷贝
# 3.1 什么是深拷贝
- 深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
深拷贝示意图: SourceObject CopiedObject ┌──────────────┐ ┌──────────────┐ │ field1: 10 │ │ field2: 10 │ (值拷贝) │ refObj1 ─────┼──→ [数据A] │ refObj2 ─────┼──→ [数据A'] (独立副本) └──────────────┘ └──────────────┘ 修改refObj1不影响refObj2,因为它们指向不同的内存1
2
3
4
5
6
7- 在上图中,SourceObject有一个int类型的属性 "field1"和一个引用类型属性"refObj1"(引用ContainedObject类型的对象)。当对SourceObject做深拷贝时,创建了CopiedObject,它有一个包含"field1"拷贝值的属性"field2"以及包含"refObj1"拷贝值的引用类型属性"refObj2" 。因此对SourceObject中的"refObj"所做的任何改变都不会影响到CopiedObject
# 深拷贝的真实困难:不是"递归一下"那么简单
教科书会告诉你"深拷贝就是递归复制每一层引用"。但是工程师真正动手写一个通用 deep copy 函数时,会发现至少有四道天堑——这也是为什么 JDK 不愿意提供默认深拷贝。
困难 1:循环引用导致的栈溢出
回到订单事故,假设业务逻辑里有:
Order ─┐
│
User ──┘ ← User.lastOrder 又指回 Order
2
3
朴素的深拷贝代码:
Order deepCopy(Order o) {
Order n = new Order();
n.user = deepCopy(o.user); // ← 递归
return n;
}
User deepCopy(User u) {
User n = new User();
n.lastOrder = deepCopy(u.lastOrder); // ← 又递归回去
return n;
}
2
3
4
5
6
7
8
9
10
→ 死循环 → StackOverflowError。
工程级解法必须维护一个 "已复制对象映射表":
Object deepCopy(Object src, IdentityHashMap<Object, Object> visited) {
if (visited.containsKey(src)) return visited.get(src); // 已经复制过,直接返回
Object n = newInstance(src);
visited.put(src, n); // ← 关键:先登记再递归
copyFields(src, n, visited);
return n;
}
2
3
4
5
6
7
这就是为什么 Python 的 copy.deepcopy() 第二个参数叫 memo —— 它就是这张映射表。
困难 2:不可复制的对象
对象图里经常混着"复制就破坏语义"的东西:
Order
└─ ConnectionPool(每个池有 Socket 句柄)
└─ 你深复制它 → 复制出来的 Socket 是无效引用 → 一用就崩
Order
└─ Logger(共享单例)
└─ 你深复制它 → 出来两个 Logger 实例,破坏单例语义
Order
└─ Lock(互斥锁)
└─ 你深复制它 → 复制出来的锁状态?锁住了还是没锁?
2
3
4
5
6
7
8
9
通用 deepcopy 没有"正确答案"——只有业务方知道哪些应该复制、哪些应该共享、哪些应该重新创建。这就是为什么 Java 没法提供"通用深拷贝",C++ 必须由程序员自己写拷贝构造函数。
困难 3:性能不是 O(n),而是 O(节点数 × 对象大小)
假设订单里有 1000 个 Item,每个 Item 平均 200 字节:
浅拷贝 Order: 40 字节 / 几纳秒
深拷贝 Order: 40 + 1000 × 200 = 200KB / 几十微秒
深拷贝 100 个订单:20MB / 几毫秒 ← 在主线程跑就是卡顿
深拷贝整个订单列表(10万条):20GB / 直接 OOM
2
3
4
深拷贝看似 O(n),实际放在生产环境就是性能黑洞。这也是为什么经验丰富的架构师宁可用"不可变对象 + 浅复制"模式,也不愿意写 deep copy。
困难 4:类的演化让 deep copy 失效
写一个深拷贝时你考虑了 Order 的当前 5 个字段:
Order deepCopy(Order o) {
Order n = new Order();
n.orderNo = o.orderNo;
n.totalPrice = o.totalPrice;
n.user = deepCopy(o.user);
n.address = deepCopy(o.address);
n.items = deepCopyList(o.items);
return n;
}
2
3
4
5
6
7
8
9
半年后另一个工程师给 Order 加了第 6 个字段 coupon,但忘记更新 deep copy ——
新代码上线后,editingOrder 和 order 共享 coupon
→ 编辑页改优惠券 → 原订单优惠券也变了
→ 一个新版本的事故诞生
2
3
这就是 deep copy 的"类演化脆弱性"。所以业界有了两个对策:
对策 A:用反射/序列化做泛型深拷贝(不依赖手写)
→ 慢 5-100 倍,但抗演化
对策 B:从根本上不让对象可变(不可变对象不需要 deep copy)
→ Scala / Kotlin / Rust 的主流方案
2
3
4
5
# 这一段的认知跃迁
| 表层认知 | 深层认知 |
|---|---|
| "深拷贝就是递归" | 深拷贝必须解决循环引用、不可复制对象、类演化三道难关 |
| "深拷贝 = 慢一点的浅拷贝" | 深拷贝是 O(对象图规模),可能从纳秒爆炸到秒级 |
| "深拷贝越深越好" | "通用深拷贝" 在工程上是个伪问题,必须按业务定义边界 |
| "我用 deep copy 就安全" | deep copy 是"贴膏药",从根本上你应该问"为什么要可变" |
# 3.2 实现深拷贝案例
- 下面是实现深拷贝的一个例子。只是在浅拷贝的例子上做了一点小改动,Subject 和CopyTest 类都没有变化。
public class Student implements Cloneable { // 对象引用 private Subject subj; private String name; public Student(String s, String sub) { name = s; subj = new Subject(sub); } public Subject getSubj() { return subj; } public String getName() { return name; } public void setName(String s) { name = s; } /** * 重写clone()方法 * * @return */ public Object clone() { // 深拷贝,创建拷贝类的一个新对象,这样就和原始对象相互独立 Student s = new Student(name, subj.getName()); return s; } }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 - 输出结果如下:
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象: 杨充 - 潇湘剑雨 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 拷贝对象: 杨充 - 潇湘剑雨 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象是否一样: false 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象的name属性是否一样: true 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始对象和拷贝对象的subj属性是否一样: false 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新后的原始对象: 小杨逗比 - 潇湘剑雨大侠 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 更新原始对象后的克隆对象: 杨充 - 潇湘剑雨1
2
3
4
5
6
7 - 得出的结论
- 很容易发现clone()方法中的一点变化。因为它是深拷贝,所以你需要创建拷贝类的一个对象。因为在Student类中有对象引用,所以需要在Student类中实现Cloneable接口并且重写clone方法。
# 04.序列化进行拷贝
# 4.1 序列化属于深拷贝
- 可能你会问,序列化是属于那种类型拷贝?答案是:通过序列化来实现深拷贝。可以思考一下,为何序列化对象要用深拷贝而不是用浅拷贝呢?
# 4.2 注意要点
- 可以序列化是干什么的?它将整个对象图写入到一个持久化存储文件中并且当需要的时候把它读取回来, 这意味着当你需要把它读取回来时你需要整个对象图的一个拷贝。这就是当你深拷贝一个对象时真正需要的东西。请注意,当你通过序列化进行深拷贝时,必须确保对象图中所有类都是可序列化的。
# 4.3 序列化案例
看一下下面案例,很简单,只需要实现Serializable这个接口。Android中还可以实现Parcelable接口。
public class ColoredCircle implements Serializable { private int x; private int y; public ColoredCircle(int x, int y) { this.x = x; this.y = y; } public int getX() { return x; } public void setX(int x) { this.x = x; } public int getY() { return y; } public void setY(int y) { this.y = y; } @Override public String toString() { return "x=" + x + ", y=" + y; } }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
31private void test3() { ObjectOutputStream oos = null; ObjectInputStream ois = null; try { // 创建原始的可序列化对象 DouBi c1 = new DouBi(100, 100); System.out.println("原始的对象 = " + c1); DouBi c2 = null; // 通过序列化实现深拷贝 ByteArrayOutputStream bos = new ByteArrayOutputStream(); oos = new ObjectOutputStream(bos); // 序列化以及传递这个对象 oos.writeObject(c1); oos.flush(); ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray()); ois = new ObjectInputStream(bin); // 返回新的对象 c2 = (DouBi) ois.readObject(); // 校验内容是否相同 System.out.println("复制后的对象 = " + c2); // 改变原始对象的内容 c1.setX(200); c1.setY(200); // 查看每一个现在的内容 System.out.println("查看原始的对象 = " + c1); System.out.println("查看复制的对象 = " + c2); } catch (IOException e) { System.out.println("Exception in main = " + e); } catch (ClassNotFoundException e) { e.printStackTrace(); } finally { if (oos != null) { try { oos.close(); } catch (IOException e) { e.printStackTrace(); } } if (ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } } }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输出结果如下:
2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 原始的对象 = x=100, y=100 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 复制后的对象 = x=100, y=100 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 查看原始的对象 = x=200, y=200 2019-03-23 13:53:48.096 25123-25123/com.ycbjie.other I/System.out: 查看复制的对象 = x=100, y=1001
2
3
4注意:需要做以下几件事儿:
- 确保对象图中的所有类都是可序列化的
- 创建输入输出流
- 使用这个输入输出流来创建对象输入和对象输出流
- 将你想要拷贝的对象传递给对象输出流
- 从对象输入流中读取新的对象并且转换回你所发送的对象的类
得出的结论
- 在这个例子中,创建了一个DouBi对象c1然后将它序列化 (将它写到ByteArrayOutputStream中). 然后我反序列化这个序列化后的对象并将它保存到c2中。随后我修改了原始对象c1。然后结果如你所见,c1不同于c2,对c1所做的任何修改都不会影响c2。
- 注意,序列化这种方式有其自身的限制和问题:因为无法序列化transient变量, 使用这种方法将无法拷贝transient变量。再就是性能问题。创建一个socket, 序列化一个对象, 通过socket传输它, 然后反序列化它,这个过程与调用已有对象的方法相比是很慢的。所以在性能上会有天壤之别。如果性能对你的代码来说是至关重要的,建议不要使用这种方式。它比通过实现Clonable接口这种方式来进行深拷贝几乎多花100倍的时间。
# 05.延迟拷贝
# 5.1 什么是延迟拷贝
延迟拷贝(Copy-on-Write,CoW)是浅拷贝和深拷贝的结合体,也称为写时复制。
核心思想:拷贝时只复制引用(浅拷贝),当任一副本需要修改时,才真正执行深拷贝。这是一种经典的延迟求值(Lazy Evaluation) 策略。
# 探索过程:COW 是怎么从浅与深的张力中"涌现"出来的
回到 §0 的订单事故,我们已经看到工程师的两难:
浅拷贝:进编辑页快(O(1)),但内层共享 → 出 Bug
深拷贝:内层独立 → 修复 Bug,但每次进页都 O(n) 全树复制
2
聪明的工程师会问:用户大部分时候只是"看一眼"或"改一两个字段",为什么要为整棵树付费?
把"用户在编辑页的真实行为"拆开看:
情况 A(占比 70%):用户进了编辑页,看一眼就退出
→ 全树深拷贝白做了
情况 B(占比 25%):用户改了 1-2 个字段
→ 只有那 1-2 个内层对象需要独立
情况 C(占比 5%): 用户改了大量字段
→ 这种情况下深拷贝才是真的"值"
2
3
4
5
6
如果有一种机制能做到:
进编辑页时: O(1) 浅拷贝(情况 A 全免单)
改字段时: O(对应字段) 复制(情况 B 只付小钱)
全改时: O(n) 退化为深拷贝(情况 C 不亏)
2
3
——这就是 COW 的设计思路。它不是一种新机制,而是"浅 + 深"的惰性求值:默认浅,写时再深。
这一招的精妙之处:
工程上从来没有"通用最优解"。"读多写少"是真实世界 80% 业务的特征——文件系统、数据库 MVCC、Git 提交历史、UI 视图树、配置中心,全部如此。COW 正好踩中了这个分布。
# 5.2 延迟拷贝原理
写时复制的内部实现通常包含一个引用计数器:
初始状态:
对象A ──引用──→ [数据块, refCount=1]
拷贝后:
对象A ──引用──→ [数据块, refCount=2] ←──引用── 对象B
修改对象A时:
对象A ──引用──→ [数据块副本, refCount=1] (新分配)
对象B ──引用──→ [原数据块, refCount=1]
2
3
4
5
6
7
8
9
# 硬件级 COW:Linux fork() 是怎么做到 O(1) 的
软件层的 COW 用引用计数。但硬件层有更优雅的实现——Linux fork() 的 COW,和 CPU 的 MMU 直接配合:
父进程内存布局:
逻辑页 0 ─→ 物理页 0x1000 [可读可写]
逻辑页 1 ─→ 物理页 0x2000 [可读可写]
逻辑页 2 ─→ 物理页 0x3000 [可读可写]
fork() 后(瞬间完成,几微秒):
父进程页表:所有页标记为 [只读],物理页不变
子进程页表:复制父进程页表,所有页标记为 [只读]
↓
父子两个进程共享同一份物理内存(只读)
2
3
4
5
6
7
8
9
10
当子进程尝试写入逻辑页 1:
① CPU 检测到对只读页的写 → 触发 Page Fault
② OS 内核处理:
─ 分配一个新物理页 0x4000
─ 把原物理页 0x2000 的内容 memcpy 到新页 0x4000
─ 更新子进程页表:逻辑页 1 ─→ 物理页 0x4000 [可读可写]
③ 重试写指令
2
3
4
5
6
7
sequenceDiagram
participant P as 父进程
participant K as 内核+MMU
participant C as 子进程
P->>K: fork()
K->>K: 复制页表(不复制物理页)
K->>K: 所有页标记只读
K-->>P: 返回(O(1))
K-->>C: 返回(O(1))
C->>K: 写入页 1
K->>K: Page Fault
K->>K: 分配新页 + memcpy
K->>K: 子进程页表指向新页
K-->>C: 写入成功
2
3
4
5
6
7
8
9
10
11
12
13
14
为什么这个方案这么牛:
- fork 复杂度从 O(进程内存) 降到 O(页表大小)——一个 1GB 的进程,fork 可能只需复制 4KB 页表
- OS 不需要知道哪些页"会被改"——MMU 硬件帮你检测
- 粒度精确到 4KB——只有真正写的页会被复制
- 零软件层引用计数——MMU 的"只读位"本身就是计数器
所有现代 OS 都用这套:Linux、macOS、FreeBSD、甚至 Windows 的 CreateProcess。这是 COW 思想的硬件级最优实现。
# 软件层 COW:引用计数的代价
但软件层的 COW 没有 MMU,只能靠引用计数。这带来三个隐藏成本:
成本 1:每次读都要原子操作
// C++11 之前的 std::string COW 实现的核心
class CowString {
struct Buffer {
std::atomic<int> refCount; // ← 必须原子,多线程安全
char data[];
};
Buffer* buf;
};
2
3
4
5
6
7
8
单线程:refCount++ ≈ 1 个时钟周期
多线程:原子 ++ ≈ 30-50 个时钟周期(lock 前缀)
2
对短字符串(< 50 字节)的拷贝:
非 COW 的 SSO(小字符串优化):直接 memcpy,几个时钟周期
COW 实现:原子计数 + 间接寻址 + 可能的写时分裂
→ 反而更慢!
2
3
这就是 C++11 标准强制 std::string 不能用 COW 的根本原因——多核时代,原子操作的代价比直接拷贝还高。
成本 2:写时分裂触发"突然慢"
正常使用:read 都很快(O(1))
某次写入:突然要 O(n) 复制全部数据 → 那一次操作"突然慢"
2
这种"99% 快 + 1% 突然慢"的特性,对实时系统、游戏渲染、高频交易是致命的——它们需要可预测的延迟而不是平均最快。
成本 3:循环引用 / 内存泄漏
引用计数有个经典缺陷——循环引用永远不会归零:
A 引用 B(B.refCount = 1)
B 引用 A(A.refCount = 1)
外部都不再引用 A 和 B
→ 但 A.refCount 和 B.refCount 都 ≥ 1,永远不释放
→ 内存泄漏
2
3
4
5
Swift / Objective-C 必须靠 weak 关键字手动打破循环;Rust 的 Rc<T> 文档明确警告这个问题。引用计数不是免费的。
经典应用场景对比表:
| 场景 | 语言/系统 | COW 实现层级 | 引用计数代价 | 结论 |
|---|---|---|---|---|
fork() 系统调用 | Linux内核 | 硬件 MMU | 0(页表标志位) | ✅ 最优 |
Swift Array / String | Swift | 运行时 + ARC | 中等(原子计数) | ✅ 单线程优秀 |
std::string(旧版) | C++ (GCC < 5) | 库实现 + 原子 | 高(多核更慢) | ❌ C++11 已废弃 |
String.substring | Java (< 7u6) | 共享 char[] | 0(无计数) | ⚠️ 7u6 后改为独立拷贝 |
Rc<T> / Arc<T> | Rust | 库 trait | 中等 | ⚠️ 不能解决循环 |
| 数据库 MVCC | PostgreSQL/MySQL | 页级版本链 | 取决于实现 | ✅ 读不阻塞写 |
| Git 提交对象 | Git | 内容寻址 hash | 0(不可变) | ✅ 永远不冲突 |
# 这一段的认知跃迁
| 表层认知 | 深层认知 |
|---|---|
| "COW 总是最优" | COW 是"读多写少"分布下的最优;写多场景反而更慢 |
| "COW 是软件优化" | 最高效的 COW 是 MMU 硬件级,软件 COW 都是它的近似 |
| "COW = 引用计数" | 引用计数是 COW 的一种实现,不可变 + 内容寻址(如 Git) 是另一种 |
| "C++ 不用 COW 是技术倒退" | 是技术进步——多核时代发现"原子计数比 memcpy 还贵" |
带回订单事故:
真正的"标准解"是把 Order 设计为不可变,编辑页生成一个新 Order(用 Builder 累积变化),保存时整体替换。这就是函数式编程在前端 / 后端业务建模上越来越流行的根本原因——它从根本上消除了"什么时候该复制"这个问题。
# 06.如何选择拷贝方式
选择拷贝方式需要综合考虑以下因素:
| 考量维度 | 浅拷贝 | 深拷贝 | 延迟拷贝(CoW) |
|---|---|---|---|
| 性能 | 最快 | 最慢 | 读多写少时最优 |
| 内存 | 共享引用,省内存 | 完全独立,耗内存 | 动态按需分配 |
| 安全性 | 修改会互相影响 | 完全隔离 | 写入时才隔离 |
| 适用场景 | 只读数据、基本类型 | 需要独立修改的场景 | 大对象的读多写少场景 |
决策规则:
- 对象只包含基本类型 → 浅拷贝即可(值语义,天然深拷贝)
- 对象有引用属性但不会被修改 → 浅拷贝
- 对象有引用属性且需要独立修改 → 深拷贝
- 大对象,大部分时间只读,偶尔修改 → 延迟拷贝
# 07.数组的拷贝
- 数组除了默认实现了clone()方法之外,还提供了Arrays.copyOf方法用于拷贝,这两者都是浅拷贝。
# 7.1 基本数据类型数组
- 如下所示
public void test4() { int[] lNumbers1 = new int[5]; int[] rNumbers1 = Arrays.copyOf(lNumbers1, lNumbers1.length); rNumbers1[0] = 1; boolean first = lNumbers1[0] == rNumbers1[0]; Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers1[0] + ",rNumbers2[0]=" + rNumbers1[0]+"---"+first); int[] lNumbers3 = new int[5]; int[] rNumbers3 = lNumbers3.clone(); rNumbers3[0] = 1; boolean second = lNumbers3[0] == rNumbers3[0]; Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"---"+second); }1
2
3
4
5
6
7
8
9
10
11
12
13 - 打印结果如下所示
2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小杨逗比: lNumbers2[0]=0,rNumbers2[0]=1---false 2019-03-25 14:28:09.907 30316-30316/org.yczbj.ycrefreshview D/小杨逗比: lNumbers3[0]=0,rNumbers3[0]=1---false1
2
# 7.2 引用数据类型数组
- 如下所示
public static void test5() { People[] lNumbers1 = new People[5]; lNumbers1[0] = new People(); People[] rNumbers1 = lNumbers1; boolean first = lNumbers1[0].equals(rNumbers1[0]); Log.d("小杨逗比", "lNumbers1[0]=" + lNumbers1[0] + ",rNumbers1[0]=" + rNumbers1[0]+"--"+first); People[] lNumbers2 = new People[5]; lNumbers2[0] = new People(); People[] rNumbers2 = Arrays.copyOf(lNumbers2, lNumbers2.length); boolean second = lNumbers2[0].equals(rNumbers2[0]); Log.d("小杨逗比", "lNumbers2[0]=" + lNumbers2[0] + ",rNumbers2[0]=" + rNumbers2[0]+"--"+second); People[] lNumbers3 = new People[5]; lNumbers3[0] = new People(); People[] rNumbers3 = lNumbers3.clone(); boolean third = lNumbers3[0].equals(rNumbers3[0]); Log.d("小杨逗比", "lNumbers3[0]=" + lNumbers3[0] + ",rNumbers3[0]=" + rNumbers3[0]+"--"+third); } public static class People implements Cloneable { int age; Holder holder; @Override protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public static class Holder { int holderValue; } }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 - 打印日志如下
2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18,rNumbers1[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18--true 2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671,rNumbers2[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671--true 2019-03-25 14:53:17.054 31093-31093/org.yczbj.ycrefreshview D/小杨逗比: lNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56,rNumbers3[0]=org.yczbj.ycrefreshview.MainActivity$People@91e9c56--true1
2
3
# 08.集合的拷贝
- 集合的拷贝也是我们平时经常会遇到的,一般情况下,我们都是用浅拷贝来实现,即通过构造函数或者clone方法。
# 8.1 集合浅拷贝
- 构造函数和 clone() 默认都是浅拷贝
public static void test6() { ArrayList<People> lPeoples = new ArrayList<>(); People people1 = new People(); lPeoples.add(people1); Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0)); ArrayList<People> rPeoples = (ArrayList<People>) lPeoples.clone(); Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0)); boolean b = lPeoples.get(0).equals(rPeoples.get(0)); Log.d("小杨逗比", "比较两个对象" + b); } public static class People implements Cloneable { int age; Holder holder; @Override protected Object clone() { try { People people = (People) super.clone(); people.holder = (People.Holder) this.holder.clone(); return people; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public static class Holder implements Cloneable { int holderValue; @Override protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } }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 - 打印日志
2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18 2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18 2019-03-25 14:56:56.931 31454-31454/org.yczbj.ycrefreshview D/小杨逗比: 比较两个对象true1
2
3
# 8.2 集合深拷贝
- 在某些特殊情况下,如果需要实现集合的深拷贝,那就要创建一个新的集合,然后通过深拷贝原先集合中的每个元素,将这些元素加入到新的集合当中。
public static void test7() { ArrayList<People> lPeoples = new ArrayList<>(); People people1 = new People(); people1.holder = new People.Holder(); lPeoples.add(people1); Log.d("小杨逗比", "lPeoples[0]=" + lPeoples.get(0)); ArrayList<People> rPeoples = new ArrayList<>(); for (People people : lPeoples) { rPeoples.add((People) people.clone()); } Log.d("小杨逗比", "rPeoples[0]=" + rPeoples.get(0)); boolean b = lPeoples.get(0).equals(rPeoples.get(0)); Log.d("小杨逗比", "比较两个对象" + b); } public static class People implements Cloneable { int age; Holder holder; @Override protected Object clone() { try { People people = (People) super.clone(); people.holder = (People.Holder) this.holder.clone(); return people; } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } public static class Holder implements Cloneable { int holderValue; @Override protected Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { e.printStackTrace(); } return null; } } }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 - 打印日志
2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: lPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@46a2c18 2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: rPeoples[0]=org.yczbj.ycrefreshview.MainActivity$People@d344671 2019-03-25 15:00:54.610 31670-31670/org.yczbj.ycrefreshview D/小杨逗比: 比较两个对象false1
2
3
# 09.跨语言拷贝机制对比
# 9.1 各语言拷贝机制总览
| 语言 | 浅拷贝 | 深拷贝 | 特有机制 |
|---|---|---|---|
| Java | clone() / 赋值 | 序列化 / 手动递归 | Cloneable接口 |
| JavaScript | Object.assign / 展开运算符 | JSON.parse(JSON.stringify()) / structuredClone | 结构化克隆 |
| Python | copy.copy() | copy.deepcopy() | 自动处理循环引用 |
| C++ | 拷贝构造函数 | 手动实现 / 拷贝构造深度复制 | 移动语义(std::move) |
| Swift | 值类型自动复制 | 手动实现 | COW(写时复制) |
| Rust | Clone trait | Clone trait (深拷贝) | 所有权转移 |
# 9.2 JavaScript的拷贝
JavaScript中对象拷贝是最常见的操作之一:
// 浅拷贝方案
const shallow1 = Object.assign({}, original);
const shallow2 = { ...original };
const shallow3 = Array.from(originalArray);
// 深拷贝方案
// 方案1: JSON(不支持函数、Date、RegExp、循环引用)
const deep1 = JSON.parse(JSON.stringify(original));
// 方案2: structuredClone(推荐,现代浏览器支持)
const deep2 = structuredClone(original);
// 方案3: 递归实现
function deepClone(obj, map = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (map.has(obj)) return map.get(obj); // 处理循环引用
const clone = Array.isArray(obj) ? [] : {};
map.set(obj, clone);
for (const key of Object.keys(obj)) {
clone[key] = deepClone(obj[key], map);
}
return clone;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 9.3 C++的拷贝与移动
C++11引入了移动语义,彻底改变了对象拷贝的方式:
class Buffer {
char* data;
size_t size;
public:
// 拷贝构造(深拷贝)
Buffer(const Buffer& other) : size(other.size) {
data = new char[size];
memcpy(data, other.data, size);
}
// 移动构造(零拷贝,转移所有权)
Buffer(Buffer&& other) noexcept : data(other.data), size(other.size) {
other.data = nullptr;
other.size = 0;
}
};
// 使用
Buffer a(1024);
Buffer b = a; // 调用拷贝构造,深拷贝
Buffer c = std::move(a); // 调用移动构造,零拷贝(a变为空)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
移动语义的核心思想:当源对象即将销毁时,直接"偷走"其资源,而不是复制。
# 9.4 写时复制(COW)机制
写时复制是一种延迟拷贝的优化技术,核心思想:
- 复制时只复制引用(共享底层数据)
- 修改时才真正复制一份独立的数据
- 多个副本读取同一份数据时零开销
应用场景:
- Swift的Array、String、Dictionary都使用COW
- Linux的fork()系统调用使用COW复制进程内存
- 早期C++ std::string实现也使用COW(C++11后废弃)
// Swift中COW的表现
var a = [1, 2, 3]
var b = a // 此时a和b共享同一块内存
b.append(4) // 此时才真正复制,a和b各有独立的内存
2
3
4
# 10.拷贝设计总结
# 10.1 拷贝选择决策树
需要独立的对象副本?
├── 否 → 直接赋值引用
└── 是 → 对象包含引用类型字段?
├── 否 → 浅拷贝足够
└── 是 → 需要修改内层对象?
├── 否 → 浅拷贝足够
└── 是 → 需要深拷贝
├── 对象可序列化? → 序列化拷贝
└── 手动实现深拷贝
2
3
4
5
6
7
8
9
# 10.2 性能考量
| 方式 | 性能 | 适用场景 |
|---|---|---|
| 引用赋值 | O(1) | 不需要独立副本 |
| 浅拷贝 | O(1) | 只需要顶层独立 |
| 深拷贝(手动) | O(n) | 需要完全独立 |
| 序列化拷贝 | O(n),常数大 | 对象图复杂,手动实现困难 |
| 写时复制 | 读O(1),写O(n) | 读多写少场景 |
# 11.经典陷阱与生产级反模式
# 11.1 陷阱一:Cloneable是糟糕契约
回到 §0 订单事故的根因——为什么 super.clone() 是浅拷贝,但工程师以为它是深拷贝?
根因不在工程师,而在 Cloneable 接口本身的设计缺陷:
// Cloneable 接口本体
public interface Cloneable {
// ↑ 空的!没有任何方法签名
}
// Object.clone() 的方法签名
protected native Object clone() throws CloneNotSupportedException;
2
3
4
5
6
7
观察这段 API 的反常之处:
| 问题 | 观察 |
|---|---|
| Cloneable 接口里有什么? | 什么都没有——它只是一个标记接口 |
| 真正的 clone 方法在哪? | 在 Object 类里,且是 protected |
| 如何调用? | 子类必须重写为 public 才能从外部调用 |
| 不实现 Cloneable 直接调 super.clone() 会怎样? | 抛 CloneNotSupportedException |
| 是浅拷贝还是深拷贝? | 默认浅,必须手动递归才能深 |
Joshua Bloch 在《Effective Java》第 13 条直接命名为 "明智地覆盖 clone 方法",并强烈推荐:
"...几乎所有的大牛工程师都会认为:最好的做法是不要使用 Cloneable。"
替代方案:
// 方案 1:拷贝构造函数(推荐)
public Order(Order src) {
this.orderNo = src.orderNo;
this.user = new User(src.user); // 显式深拷贝
this.address = new Address(src.address);
this.items = src.items.stream()
.map(Item::new)
.collect(Collectors.toList());
}
// 方案 2:静态工厂
public static Order copyOf(Order src) { ... }
2
3
4
5
6
7
8
9
10
11
12
# 11.2 陷阱二:序列化拷贝静默丢失
事故现场:用 ObjectOutputStream 做"通用深拷贝"工具,跑了一年没问题。某天 PM 加了个字段:
public class Order implements Serializable {
private String orderNo;
private double totalPrice;
// ... 老字段
private transient OrderMetrics metrics; // ← 新加的,标了 transient
}
2
3
4
5
6
序列化深拷贝完,新订单的 metrics 永远是 null——因为 transient 字段会被序列化跳过。
这种 Bug 极其隐蔽:
- 单元测试:测 orderNo、totalPrice 都对,pass
- 生产环境:metrics 用于风控,null 直接绕过风控
- 直到某天有人对着审计日志才发现
防御方法:
// 1. 给所有需要拷贝的类加上序列化测试
@Test
void testAllFieldsCopied() {
Order original = createTestOrder();
Order copy = deepCopy(original);
// 用反射对比所有字段(包括 transient)
for (Field f : Order.class.getDeclaredFields()) {
f.setAccessible(true);
assertEquals(f.get(original), f.get(copy),
"Field " + f.getName() + " not copied!");
}
}
// 2. 使用 readResolve / writeReplace 显式控制
private Object readResolve() {
if (metrics == null) {
metrics = OrderMetrics.fromOrder(this); // 重建 transient
}
return this;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 11.3 陷阱三:循环引用栈溢出
copy.deepcopy() 在 Python 里有原生支持,但很多语言的"手写 deepcopy"踩了这个雷:
// 现实代码
function badDeepCopy(obj) {
if (obj === null || typeof obj !== 'object') return obj;
const result = {};
for (let k of Object.keys(obj)) {
result[k] = badDeepCopy(obj[k]); // ← 循环引用直接爆栈
}
return result;
}
// 测试代码
const a = {};
a.self = a; // 循环引用
badDeepCopy(a); // RangeError: Maximum call stack size exceeded
2
3
4
5
6
7
8
9
10
11
12
13
14
正确做法:维护已访问对象表(也叫 memo):
function deepCopy(obj, memo = new WeakMap()) {
if (obj === null || typeof obj !== 'object') return obj;
if (memo.has(obj)) return memo.get(obj); // ← 关键:检测循环
const result = Array.isArray(obj) ? [] : {};
memo.set(obj, result); // ← 关键:先登记再递归
for (let k of Object.keys(obj)) {
result[k] = deepCopy(obj[k], memo);
}
return result;
}
2
3
4
5
6
7
8
9
10
JavaScript 的 structuredClone()(2022+)原生处理循环引用——这是浏览器层面给的礼物,比手写 deepCopy 安全得多。
# 11.4 陷阱四:浅拷贝+不可变集合假独立
List<Item> items = order.getItems();
List<Item> snapshot = new ArrayList<>(items); // ← "我做了浅拷贝快照"
// 1 秒后...
items.get(0).setPrice(0); // 改原集合的元素
snapshot.get(0).getPrice(); // → 0 WTF?
2
3
4
5
6
7
陷阱根因:new ArrayList<>(items) 只复制了 List 容器,元素本身仍是同一个对象引用。
items 容器 ──→ [A 的引用, B 的引用, C 的引用]
│ │ │
snapshot 容器 ──→ [A 的引用, B 的引用, C 的引用]
▼ ▼ ▼
Item A Item B Item C
↑ 仍然是同一个对象!
2
3
4
5
6
正确做法 (3 选 1):
// 方案 A:深拷贝列表元素
List<Item> snapshot = items.stream()
.map(Item::new)
.collect(Collectors.toList());
// 方案 B:让 Item 不可变(@Value / record)
record Item(String name, double price) {}
// 此时浅拷贝就够,因为元素本身不可变
// 方案 C:用 Java 16+ 的 List.copyOf(深复制 + 不可变)
List<Item> snapshot = List.copyOf(items); // 但 Item 内部仍可变!
2
3
4
5
6
7
8
9
10
11
# 11.5 陷阱五:多线程下clone非原子
public class Counter {
private int count;
private List<Long> history;
public Counter clone() {
return super.clone(); // 浅拷贝
}
}
// 线程 A 在 clone
// 线程 B 同时在 history.add(...)
// → clone 出来的 history 可能处于"半改"状态 → ConcurrentModificationException
2
3
4
5
6
7
8
9
10
11
根因:super.clone() 是 native 实现,对内存做 memcpy,但它不持有任何锁。多线程下需要:
public Counter clone() {
synchronized (this) { // ← 关键:先锁定源对象
Counter c = super.clone();
c.history = new ArrayList<>(history); // 元素也要复制
return c;
}
}
2
3
4
5
6
7
或者使用 CopyOnWriteArrayList / 不可变集合从源头解决。
# 12.一句话总结:拷贝设计哲学
# 12.1 拷贝的三层认知阶梯
| 阶段 | 思维方式 | 典型语言 / 模式 |
|---|---|---|
| 初级 | "需要复制就调 clone" | Java Cloneable |
| 中级 | "拷贝有梯度——根据修改边界选层级" | C++ 拷贝构造 / 序列化拷贝 |
| 高级 | "从根本上不让数据可变" | Rust / Swift / 函数式编程 |
# 12.2 决策清单(贴在工位旁边)
问 1:你真的需要"独立副本"吗?
├─ 不需要 → 引用赋值(最便宜)
└─ 需要 → 进入问 2
问 2:副本会被修改吗?
├─ 不会 → 浅拷贝 + 文档约定不可变
├─ 只改顶层字段 → 浅拷贝
└─ 会改内层对象 → 进入问 3
问 3:修改频繁吗?
├─ 极少 → COW(如果语言支持)
└─ 频繁 → 深拷贝 / 改用不可变对象
问 4:对象图有环吗?
├─ 有 → 必须维护 memo 表,或用 structuredClone
└─ 没有 → 直接递归
问 5:为什么这个对象需要可变?
├─ 业务真的要 → 老老实实做深拷贝
└─ 只是"以前这么写" → 重构为不可变对象
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 12.3 设计哲学一句话
"拷贝问题"的最优解,往往是"消灭拷贝"——不可变数据让"复制"和"共享"不再有区别。
Java 用
clone()修补,C++ 用拷贝构造,Swift/Rust 用值语义 + 编译期保证,函数式语言用持久化数据结构。这一路演进的方向,就是把"什么时候复制"这个问题从程序员脑子里赶出去。
回到 §0 的订单事故:真正的"零事故"修复不是把 clone 写得更细,而是让 Order 变成不可变对象——编辑页只能产出新 Order,永远不能改老 Order。Bug 在源头被消灭,而不是在症状上修补。
# 12.4 与本卷其它章节的呼应
05.序列化数据的思想 ─→ 序列化 = 跨进程深拷贝
33.内存回收机制设计 ─→ COW 减少 GC 压力
34.多种引用技术设计 ─→ 强/弱/软引用决定"复制后能否回收"
36.手写LRU缓存原理 ─→ 缓存的"快照拷贝"必须考虑写时复制
50.JVM虚拟机内存设计 ─→ Eden 区分配 + Survivor 复制就是大型 COW
2
3
4
5
# 12.5 延伸阅读
- 《Effective Java》第 13 条:明智地覆盖 clone 方法
- 《Functional Programming in Scala》第 3 章:纯函数式数据结构
- Rust 官方文档:
ClonevsCopy的差别 - 论文:Copy-on-Write Optimization for Versioned Data(USENIX 2010)
- Linux Kernel:
mm/memory.c的do_wp_page函数(COW 内核实现)