原型模式:clone 的陷阱、深拷贝的三种做法与选型指南
「复制一个对象」听起来比「新建一个对象」简单,但它是创建型模式里最容易被写错的一个。这篇文章讲清原型模式(Prototype)的意图、Java 里
Cloneable+Object.clone()的真实契约、浅拷贝为什么会污染原型、深拷贝的三种做法与代价,以及一个常常被忽略的结论:大多数时候你需要的不是克隆,而是一个拷贝构造器。
一、意图与适用场景
GoF 对原型模式的意图定义是:用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
注意这句话的重心不在"复制",而在**"用实例指定种类"——它提供了一个把"创建"这件事从类名解耦到实例**的机制。这一点在下面会反复出现。
什么时候"复制"比"新建"划算?
| 场景 | 为什么原型更合适 |
|---|---|
| 创建成本高 | 对象初始化要读数据库、解析文件、跑大量计算(如一份预生成的报表样式、一棵装配好的配置树)——复制一个成品比重新算一遍便宜得多 |
| 需要"模板 + 微调" | 有一份标准配置,每次只用改一两个字段;用原型复制再改,比写一堆带参构造器清晰 |
| 运行时才知道具体类型 | 框架里只拿到 Prototype 引用,不知道(也不该知道)具体是哪个子类——p.clone() 天然返回正确类型,不需要任何 if-else 或工厂子类 |
| 需要大量相似对象 | 每个对象要独立可变,但初始状态高度相似(游戏里的怪物、文档里的样式) |
反过来,如果对象不可变,那么"创建新对象"本身就可以退化成"共享同一个实例"——这时候任何拷贝都是浪费。这是现代 Java 里最常见的替代方案(见第九节)。
二、Java 的原型机制:Cloneable + Object.clone()
Java 不走"每个类自己实现复制逻辑"的路子,而是在 Object 上放了一个 native 方法:
public class Object {
protected native Object clone() throws CloneNotSupportedException;
}
围绕它有三条必须记住的规则:
clone()是protected的:外部调用不了,子类必须重写并把它提升为public(或提供别的公开入口)才有意义。Cloneable是个空的标记接口:它不声明任何方法,作用只是让Object.clone()放行;没实现它,super.clone()会抛CloneNotSupportedException。(这是 Java 早期设计的一个众所周知的失误,见《Effective Java》Item 13。)Object.clone()默认是浅拷贝:它做的是"逐字段位复制"(field-by-field, bitwise),引用字段复制的是引用本身。
clone() 的通用契约(来自 JDK 文档):
x.clone() != x——返回的是新对象,不是this;x.clone().getClass() == x.getClass()——类型不变;x.clone().equals(x)——通常成立,但不是绝对要求;- 由约定俗成,
x.clone()内部的实现应当先super.clone(),再按需修正字段。
还有一条不写在文档里、但必须知道的事实:clone() 不会调用任何构造器。它直接在堆上复制字节,因此构造器里的校验、注册、计数器、final 字段初始化——全部被绕过了。
三、结构原型与 UML
原型模式的结构比工厂模式简单得多——它只有一条核心关系:具体原型实现抽象原型的复制方法,客户端只面向抽象原型。

三个角色:
- Prototype(抽象原型):声明复制自身的接口。Java 里可以是一个接口,也可以直接用
Cloneable+clone()。 - ConcretePrototype(具体原型):实现复制,返回自己的副本;往往还要负责把可变字段深拷贝到副本里。
- Client(客户):持有抽象原型引用,调用复制方法拿新对象——它不知道也不需要知道具体类型。
与「简单工厂」的关键差别:简单工厂把"用哪个类"写在一个方法里,工厂方法把它交给子类,原型把它交给一个实例。三者都在解决"创建谁"的问题,原型走的是最短的一条路:不用类名,直接复制。
四、浅拷贝的坑:副本改了,原件也变了
先看一个"看起来没问题"的实现:
import java.util.ArrayList;
import java.util.List;
/** 浅拷贝:Object.clone() 的默认行为 */
public class ShallowCopyDemo {
static class Order implements Cloneable {
String id;
List<String> items = new ArrayList<>();
Order(String id, String... items) {
this.id = id;
for (String s : items) this.items.add(s);
}
@Override
public Order clone() {
try {
return (Order) super.clone(); // 逐字段位复制
} catch (CloneNotSupportedException e) {
throw new AssertionError(e); // 实现了 Cloneable,走不到这里
}
}
}
public static void main(String[] args) {
Order origin = new Order("A-1", "键盘", "鼠标");
Order copy = origin.clone();
System.out.println(origin != copy); // true:确实是两个对象
System.out.println(origin.getClass() == copy.getClass()); // true:类型也一致
System.out.println(origin.items == copy.items); // true:⚠️ 同一个 List 实例
copy.items.add("显示器");
System.out.println(origin.items); // [键盘, 鼠标, 显示器] ← 原件被污染
}
}
问题出在 origin.items == copy.items 为 true:String id 是引用类型,但 String 不可变,所以共享它无害;List<String> items 是可变引用字段,共享它意味着两个对象共用一份数据——修改副本等于修改原型。
原型的三种"可变字段"是重灾区:集合(List/Map/Set)、数组、以及对象自带的 Date/StringBuilder 这类可变类型。

五、深拷贝的三种做法
做法 1:重写 clone(),手动深拷贝可变字段
最直接的原型模式写法——先 super.clone() 拿到浅拷贝,再逐个"补深":
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
public class ManualDeepCloneDemo {
static class Order implements Cloneable, Serializable {
private static final long serialVersionUID = 1L;
String id;
List<String> items;
int[] quantity;
Order(String id, List<String> items, int[] quantity) {
this.id = id;
this.items = items;
this.quantity = quantity;
}
@Override
public Order clone() {
try {
Order copy = (Order) super.clone(); // 1) 先浅拷贝
copy.items = new ArrayList<>(this.items); // 2) 集合:新建一份
copy.quantity = this.quantity.clone(); // 3) 数组:数组的 clone 是浅拷贝但够用
return copy; // 4) 若有嵌套对象,要递归拷贝
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
public static void main(String[] args) {
Order origin = new Order("A-1", new ArrayList<>(List.of("键盘")), new int[]{1});
Order copy = origin.clone();
copy.items.add("显示器");
copy.quantity[0] = 99;
System.out.println(origin.items); // [键盘] ← 不再被污染
System.out.println(origin.quantity[0]); // 1
}
}
要点:每一层可变引用都必须手动复制。如果对象图有三层嵌套,就得写三层——这是这种写法最大的维护负担。
做法 2:序列化中转(简单,但慢)
思路是把对象"写出去再读回来",内存里自然生成一份全新对象图,天然深拷贝,不用关心有几层嵌套:
import java.io.*;
public final class DeepCopy {
private DeepCopy() {}
@SuppressWarnings("unchecked")
public static <T extends Serializable> T of(T obj) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
try (ObjectInputStream ois =
new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
return (T) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new IllegalStateException("深拷贝失败", e);
}
}
}
代价与限制:
- 必须全链路可序列化:
transient字段会丢、没有serialVersionUID的类易受版本变化影响、不可序列化的字段(如Thread、Stream、某些框架对象)直接失败; - 性能最差:一次深拷贝至少要过一遍编解码,通常比手写慢一个数量级(也正因如此,它适合"偶尔用一次"的场景,而不是热路径);
- 好处是真的省心:递归、循环引用都由序列化机制处理,你不会漏掉任何一层。
工程里常见的变体是用 JSON 中转(Jackson / Gson
readValue(writeValueAsString(o)))——同样是"编码再解码",但依赖更轻、可跨语言;缺点是精度与类型信息可能丢失(如泛型、LocalDate、多态)。
做法 3:拷贝构造器 / 拷贝工厂(推荐)
《Effective Java》Item 13 的建议就是:别实现 Cloneable,改用拷贝构造器或拷贝工厂。
import java.util.ArrayList;
import java.util.List;
public class CopyConstructorDemo {
static final class Report {
private final String title;
private final List<String> rows; // final 字段也能正常拷贝
Report(String title, List<String> rows) {
this.title = title;
this.rows = new ArrayList<>(rows); // 构造时做防御性拷贝
}
/** 拷贝构造器:语义显式,不需要 Cloneable,不抛受检异常 */
Report(Report other) {
this(other.title, other.rows);
}
/** 拷贝工厂:静态方法版本,可以配合泛型做类型转换 */
static Report copyOf(Report other) {
return new Report(other);
}
Report withExtraRow(String row) { // 不可变风格的"改一点":返回新对象
List<String> next = new ArrayList<>(rows);
next.add(row);
return new Report(title, next);
}
List<String> rows() {
return new ArrayList<>(rows); // 出口也拷贝,防止外部改内部状态
}
}
public static void main(String[] args) {
Report origin = new Report("月度报表", new ArrayList<>(List.of("华东")));
Report copy = Report.copyOf(origin).withExtraRow("华南");
System.out.println(origin.rows()); // [华东] ← 原件不受影响
System.out.println(copy.rows()); // [华东, 华南]
}
}
它比 clone() 好在四个地方:
- 不依赖
Cloneable/clone()这套脆弱机制,不用处理CloneNotSupportedException; final字段可以正常参与(clone()里没法给final字段重新赋值,想做深拷贝只能把final去掉);- 可以校验——先过一遍构造器里的不变量检查再返回,
clone()是绕过构造器的; - 对子类友好——
clone()在非final类上很难同时满足"每个子类都正确复制"。
配合"改一点就返回新对象"的 withXxx() 方法,这套写法就是今天 Java 里事实上的主流。
六、clone() 的七个坑(写之前先知道)
| 坑 | 说明 |
|---|---|
| 浅拷贝陷阱 | 默认只复制引用;集合/数组/可变对象必须手动深拷贝,且每一层都不能漏 |
| 绕过构造器 | clone() 不调用构造器:校验、注册、计数器、日志统统跳过,被克隆出的对象可能不满足类的不变量 |
final 字段冲突 | 想深拷贝的可变字段不能是 final;用拷贝构造器则不受此限 |
| 受检异常 | CloneNotSupportedException 必须处理,可它实际上只表示"你没实现 Cloneable",属于可编程错误 |
Cloneable 语义混乱 | 空标记接口 + protected 的 Object.clone():既不是接口契约,也不是继承契约 |
非 final 类很难写对 | 子类若不重新实现,clone() 返回的是父类视角的副本,容易出微妙 bug |
数组的 clone() 有"特殊性" | 数组的 clone() 是 public 的、协变的、也是浅拷贝;元素是可变对象时同样要逐元素复制 |
顺带一个常被忽略的点:HashMap、ArrayList 这些集合的"拷贝构造器"new ArrayList<>(list) 也是浅拷贝——元素本身不会复制。要深拷贝集合,得对每个元素再复制一遍。
七、原型管理器:用"样本"取代工厂
原型模式最实用的扩展是原型注册表(Prototype Registry / Prototype Manager):把一批预配置好的原型存进 Map,客户端按名字取副本。
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PrototypeRegistryDemo {
interface Shape extends Cloneable {
Shape copy(); // 现代写法:自定义 copy(),避免暴露 Cloneable/clone()
void draw();
void scale(int factor);
}
static class Circle implements Shape {
private int radius;
private String color;
Circle(int radius, String color) { this.radius = radius; this.color = color; }
@Override public Shape copy() { return new Circle(radius, color); }
@Override public void scale(int factor) { radius *= factor; }
@Override public void draw() { System.out.println("圆形 r=" + radius + " color=" + color); }
}
static class Square implements Shape {
private int side;
private String color;
Square(int side, String color) { this.side = side; this.color = color; }
@Override public Shape copy() { return new Square(side, color); }
@Override public void scale(int factor) { side *= factor; }
@Override public void draw() { System.out.println("方形 a=" + side + " color=" + color); }
}
/** 原型管理器:保存"样本",create() 永远返回副本 */
static class ShapeRegistry {
private final Map<String, Shape> prototypes = new HashMap<>();
void register(String key, Shape prototype) {
prototypes.put(key, prototype);
}
Shape create(String key) {
Shape prototype = prototypes.get(key);
if (prototype == null) {
throw new IllegalArgumentException("未注册的原型: " + key);
}
return prototype.copy(); // 关键:交出去的是副本,不是样本本身
}
}
public static void main(String[] args) {
ShapeRegistry registry = new ShapeRegistry();
registry.register("small-circle", new Circle(10, "red"));
registry.register("small-square", new Square(5, "blue"));
Shape a = registry.create("small-circle");
a.scale(3); // 只影响副本
a.draw(); // 圆形 r=30 color=red
registry.create("small-circle").draw(); // 圆形 r=10 color=red ← 样本没被改动
}
}

它和工厂模式里那个"注册表式工厂"的区别值得玩味(参看工厂模式那篇第三节):
- 注册表式工厂存的是"怎么造"——
Supplier<Payment>或构造器引用,每次调用现场 new 一个; - 原型注册表存的是"造好的样本"——一个现成的实例,每次调用复制一份。
前者适合创建逻辑可以做参数化,后者适合对象状态本身就是配置的场景(样式、关卡模板、装配好的规则集)。如果原型注册表里的 copy() 写成 prototype.getClass().newInstance(),它就退化成了反射式工厂——差别的本质就是"复制实例"还是"重新构造"。
八、复制过程的时序

九、选型:什么时候用哪一种

| 做法 | 适用 | 代价 |
|---|---|---|
clone() + 手动深拷贝 | 必须实现 Cloneable(第三方框架要求)、对象层次浅 | 易漏层、final 字段受限、绕过构造器 |
| 拷贝构造器 / 拷贝工厂 | 默认选择:语义显式、可校验、支持 final | 每个类要写一遍(可用 Lombok @Builder(toBuilder = true) 之类的工具减少样板) |
| 序列化 / JSON 中转 | 偶发使用、对象图深且复杂、不想逐层维护 | 慢一到两个数量级;要求可序列化;transient 字段丢失 |
| 原型注册表 | 需要"按名字取样本"、模板 + 微调 | 样本必须是线程安全的只读模板,否则会被并发改坏 |
十、什么时候不要用原型
- 对象不可变:直接共享引用,拷贝纯属浪费(
String、Integer、record、枚举都是这一类)。 - 创建逻辑简单:
new Report(title, rows)一行就够,绕到clone()只会更难读。 - 需要"分步装配":用建造者模式——原型擅长"照抄一个成品",不擅长"逐步构造一个复杂对象"。
- 需要统一的创建策略:用工厂模式。原型与工厂不冲突:工厂内部完全可以拿原型来生产(注册表式工厂 + 原型注册表是同一思路的两种形态)。
- 拷贝后还要大量改字段:如果每次复制后要改八九个字段,说明这个"模板"并不合适,考虑建造者或不可变对象的
withXxx()链。
十一、参考资料
- GoF《设计模式:可复用面向对象软件的基础》——Prototype 章(意图、结构、参与者、协作)
- Joshua Bloch《Effective Java》Item 13:Override
clonejudiciously(为什么推荐拷贝构造器/拷贝工厂) - JDK API 文档:
java.lang.Object#clone()、java.lang.Cloneable(契约与"标记接口"说明) - 《Effective Java》Item 50:必要时做防御性拷贝(集合字段进出都要拷贝的原因)