原型模式中深拷贝有哪些实现方式?
简化版
深拷贝常见实现方式有手写 copy()、拷贝构造器、递归复制子对象、序列化反序列化、JSON 转换、映射工具等。工程里最推荐手写复制规则,因为它最清楚、最可控;序列化方式通用但性能和类型安全较差。
详细版
常见深拷贝方式:
| 方式 | 优点 | 缺点 |
|---|---|---|
| 手写 copy | 可控、清晰 | 字段多时代码多 |
| 拷贝构造器 | 语义明确 | 类多时要逐个维护 |
| 递归复制 | 适合对象图 | 容易遗漏循环引用 |
| Java 序列化 | 通用 | 慢、要求可序列化 |
| JSON 转换 | 简单 | 类型信息、精度、性能有风险 |
| Bean 映射工具 | 省代码 | 深拷贝语义不一定可靠 |
深拷贝不是越自动越好。业务对象里哪些字段复制、哪些字段重建、哪些字段共享,往往需要人工判断。
完整版教学
一、手写 copy 方法
这是最推荐的方式。
class OrderTemplate {
private String type;
private List<OrderItem> items;
OrderTemplate copy() {
OrderTemplate copy = new OrderTemplate();
copy.type = this.type;
copy.items = this.items.stream()
.map(OrderItem::copy)
.toList();
return copy;
}
}
每个子对象也提供自己的 copy():
class OrderItem {
private String sku;
private int count;
OrderItem copy() {
OrderItem copy = new OrderItem();
copy.sku = this.sku;
copy.count = this.count;
return copy;
}
}
这种方式虽然代码多一点,但复制语义最准确。
二、拷贝构造器
拷贝构造器把复制逻辑放进构造方法:
class OrderTemplate {
OrderTemplate(OrderTemplate source) {
this.type = source.type;
this.items = source.items.stream()
.map(OrderItem::new)
.toList();
}
}
对应子对象:
class OrderItem {
OrderItem(OrderItem source) {
this.sku = source.sku;
this.count = source.count;
}
}
它适合希望用构造器表达对象创建规则的团队。
三、序列化反序列化
序列化方式的思路是:
对象 -> 字节流或 JSON -> 新对象
它可以自动复制整个对象图,但有明显问题:
- 性能通常较差;
- 所有对象要支持序列化;
- 某些字段不适合序列化;
- 类型信息可能丢失;
- 循环引用处理复杂;
- 业务字段重建不灵活。
因此它更适合工具类场景,不适合核心业务里不加判断地使用。
四、JSON 转换不一定是真正可靠的深拷贝
很多人会写:
User copy = JSON.parseObject(JSON.toJSONString(user), User.class);
它看起来方便,但要注意:
- 日期、泛型、多态类型可能处理不准确;
- transient 或忽略字段会丢失;
- 精度和格式可能变化;
- 性能不如手写;
- 复制规则被 JSON 框架隐式控制。
面试里可以说它能实现某些场景下的深拷贝,但不应作为默认推荐。
五、循环引用要特殊处理
如果对象图里有循环引用:
A -> B
B -> A
简单递归复制会无限递归。
这时要使用对象映射表:
旧对象 -> 新对象
复制前先查表,已经复制过就直接返回已有副本。
复杂对象图的深拷贝不只是复制字段,还涉及对象身份和引用关系维护。
六、常见误区与追问
深拷贝实现要先定义对象图终点。一个对象含2层嵌套集合并存在父子回指时,递归复制必须用 IdentityHashMap<原对象, 副本> 记录已访问节点,否则会无限递归;遇到同一源对象两次时应返回同一个副本。手写复制最可控,序列化方案则受类型支持、性能和多态信息约束。
| 检查维度 | 判定依据 |
|---|---|
| 手写 copy | 语义明确、性能可控,维护字段较多 |
| 序列化复制 | 代码少,但受协议和类型能力限制 |
记忆钩子:深拷贝不是“递归到底”,而是保持原对象图的拓扑关系且不共享可变节点。
- 误区:JSON 往返能复制任意对象。 循环引用、多态类型、精度和瞬态字段都可能丢失或改变。
- 追问:为什么要用对象身份而非 equals 做访问表? 不同节点可能业务相等却必须生成不同副本,身份映射才能保留拓扑。
- 误区:不可变对象也必须重新创建。 字符串和可靠的不可变值对象可以安全共享。
- 追问:深拷贝如何测试环和共享节点? 断言复制终止,并验证源图中同一节点的多处引用在副本中仍指向同一副本节点。
- 追问:反射通用复制器有什么风险? 它难以理解字段业务语义、构造约束以及不该复制的资源句柄。
七、加强记忆
深拷贝实现方式很多,但工程里最稳的是手写 copy() 或拷贝构造器,因为它能明确表达字段级复制规则。序列化和 JSON 方案省代码,但性能、类型安全和业务语义都要谨慎评估。