原型模式是"用克隆代替 new"——复制一个已有对象,而不是从头创建。它在"创建对象成本高"或"对象结构复杂"时特别有用。这篇讲清原型怎么实现、以及 Java 里克隆的坑。
一、解决什么问题:创建成本高、结构复杂
new 一个对象通常很快,但有些场景不想从头创建——要么创建成本高(查数据库、初始化一堆字段),要么需要大量"相似但独立"的副本(合同模板克隆出几十份、每份单独修改)。原型模式就是:以已有对象为原型,克隆出新对象。
原型模式就是:以已有对象为原型,克隆出新对象。
二、核心:克隆方法
java
// 原型接口:声明克隆方法
interface Prototype {
Prototype clone();
}
// 文档:实现 Cloneable,用 super.clone() 自动复制所有字段
class Document implements Prototype, Cloneable {
private String title;
private String content;
Document(String title, String content) {
this.title = title;
this.content = content;
}
@Override
public Document clone() {
try {
return (Document) super.clone(); // Object.clone():自动复制所有字段
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
void setTitle(String t) { this.title = t; }
}
// 使用:从"合同模板"克隆出多份,每份独立修改互不影响
Document template = new Document("合同", "甲方:..."); // 模板(创建一次)
Document c1 = template.clone();
c1.setTitle("合同-张三"); // 改自己的,不影响模板和其他副本
Document c2 = template.clone();
c2.setTitle("合同-李四"); // 模板还在,还能继续克隆两个关键点:
- 为什么用
super.clone():Object.clone()会自动复制所有字段,不用手动一个个赋值——手动写(c.field = this.field)字段一多就容易漏,漏一个字段就是 bug。 - 克隆的意义:得到字段相同但独立的对象——改克隆品(
setTitle)不影响模板和其他克隆品。这是"复制对象"和"克隆对象"的区别:复制是拿到一样的值,克隆是拿到独立的副本。
三、Java 的浅拷贝 vs 深拷贝(坑)
Object.clone() 默认是浅拷贝——引用类型字段只复制"引用"(指针),不复制指向的对象本身:
java
class Order implements Cloneable {
private String title; // 引用类型(String 不可变,共享无影响)
private List<Item> items; // 引用类型(List 可变,共享会出事!)
@Override
public Order clone() {
return (Order) super.clone(); // 浅拷贝:items 复制的是"引用",还是同一个 List
}
}浅拷贝的效果:克隆出的 Order 和原 Order,items 指向同一个 List——改一个的 items,另一个也跟着变(共享数据出 bug)。
要深拷贝,得手动复制引用字段:
java
@Override
public Order clone() {
Order o = (Order) super.clone(); // ① 先浅拷贝:此时 o.items 还指向 this.items(同一个 List)
o.items = new ArrayList<>(this.items); // ② 深拷贝关键:new 一份"内容相同但独立"的新 List,让 o.items 指向它
return o; // ③ 返回:现在 o.items 和 this.items 是两个不同的 List,互不影响
}为什么是这两步:super.clone() 复制了所有字段(引用字段暂时共享),然后 o.items = new ArrayList<>(this.items) 把 o.items 换成独立的新 List——new ArrayList<>(...) 是"复制内容、创建新对象",o.items = ... 是"让克隆对象指向新对象"。
关键:克隆时按字段类型区分——
| 字段类型 | 例子 | 要不要深拷贝 | 为什么 |
|---|---|---|---|
| 基本类型 | int、double、boolean | 不用 | 存的是值,复制即独立 |
| 不可变引用 | String | 不用 | 内容改不了,共享也没副作用 |
| 可变引用 | List、Map、自定义对象 | 必须 | 内容会被修改,共享则互相传染 |
为什么 String 共享没影响(关键理解):区分"改内容"和"换指向"——
o.items.add(x):改 List 的内容(List 对象本身变了)→ 共享的另一个也看到 xo.title = "新标题":换 String 的指向(让 o.title 指向新的 String,原来的 String 没被动)→ 共享的另一个不受影响
String 不可变,对它只能"换指向"、不能"改内容",共享自然无害;List/Map 可变,能被"改内容",共享就互相传染,必须深拷贝。
四、实际应用
- Spring 的
@Scope("prototype"):每次获取都返回一个新的 bean 实例(原型作用域) - 配置模板:加载一次配置模板,之后 clone 出多个副本
- 游戏/编辑器:复制对象(怪物、文档快照)
小结
- 原型 = 用克隆代替 new,适合"创建成本高"的对象
- Java
clone()默认浅拷贝,引用字段要手动深拷贝 - Spring
@Scope("prototype")就是原型思想的落地
相关:《单例模式》(单例是"只有一个",原型是"每次克隆新对象",正好相反)。
