原型模式:复制对象,而不是重新造轮子
说实话,原型模式是我在实际项目中用得最频繁的创建型模式之一。为什么?因为很多时候,创建一个对象的成本实在太高了。高到让你心疼。
我记得有一次,我在做一个大型的图形编辑器。每个图形对象都需要加载大量的纹理、字体、样式配置。如果每次新建一个图形都从头加载一遍,用户早就把电脑砸了。这时候,原型模式就派上了用场——直接复制一个已有的对象,改改属性就完事。
什么是原型模式?
说白了,原型模式就是通过复制现有对象来创建新对象。你不需要知道对象内部是怎么构造的,只需要调用一个克隆方法。
它的核心思想是:用克隆代替构造。
你想想看,有些对象的构造过程有多复杂?
- 需要从数据库加载数据
- 需要执行复杂的计算
- 需要初始化大量的子对象
- 需要建立网络连接
如果每次都从头来一遍,性能上扛不住。原型模式就是解决这个问题的。
浅拷贝 vs 深拷贝
嗯,这里要注意。原型模式的关键在于拷贝的方式。我见过不少新手在这上面栽跟头。
先看一个简单的例子:
class Document {
String title;
List<String> tags;
Image image;
// 浅拷贝
public Document shallowClone() {
Document doc = new Document();
doc.title = this.title;
doc.tags = this.tags; // 同一个引用
doc.image = this.image; // 同一个引用
return doc;
}
// 深拷贝
public Document deepClone() {
Document doc = new Document();
doc.title = this.title;
doc.tags = new ArrayList<>(this.tags); // 新列表
doc.image = this.image.clone(); // 新对象
return doc;
}
}
看出区别了吗?
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 基本类型 | 复制值 | 复制值 |
| 引用类型 | 复制引用(指向同一对象) | 创建新对象 |
| 性能 | 快 | 慢(需要递归复制) |
| 独立性 | 共享内部对象 | 完全独立 |
原型模式的结构
我习惯把原型模式的结构画成一张图,这样理解起来更直观:
这个结构其实很简单:
- Prototype:声明克隆方法的接口
- ConcretePrototype:实现克隆方法,处理深拷贝或浅拷贝
- Client:通过调用 clone() 来创建新对象,而不是 new
什么时候用原型模式?
我个人总结了几个典型场景:
- 对象构造成本高:比如需要加载配置文件、建立数据库连接、初始化缓存
- 对象状态只有细微差别:比如文档模板,大部分内容相同,只有标题和日期不同
- 需要避免构造函数的约束:有些对象的构造函数参数太多,或者构造逻辑太复杂
- 运行时动态指定对象类型:你可以在运行时注册原型,然后动态克隆
实际项目中的例子:
我在做一个报表系统时,每个报表模板都包含:
- 复杂的图表配置(颜色、字体、布局)
- 数据源连接信息
- 权限控制列表
- 导出格式设置
如果每次新建报表都从头配置,用户会疯掉。所以我们用原型模式,用户选择一个模板,克隆一份,然后修改标题和数据范围就行了。效率提升了至少 10 倍。
Java 中的 Cloneable 接口
Java 自带了 Cloneable 接口和 clone() 方法。但我得说一句,这个设计其实有点坑。
public class User implements Cloneable {
private String name;
private List<String> roles;
@Override
public User clone() {
try {
// 默认是浅拷贝
return (User) super.clone();
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// 如果需要深拷贝,得自己实现
public User deepClone() {
User cloned = this.clone();
cloned.roles = new ArrayList<>(this.roles);
return cloned;
}
}
1. clone() 方法返回的是 Object,需要强转
2. 默认是浅拷贝,容易出 bug
3. 如果父类没有正确实现,会抛异常
我更喜欢自己定义一个 Prototype 接口,明确区分 shallowClone() 和 deepClone()。
深拷贝的几种实现方式
深拷贝说起来简单,做起来其实有不少门道。我整理了几种常见做法:
| 方式 | 实现 | 适用场景 |
|---|---|---|
| 手动复制 | 逐个字段 new 新对象 | 对象结构简单,字段少 |
| 序列化 | 对象 -> 字节流 -> 新对象 | 对象层级深,但所有类都需实现 Serializable |
| JSON 转换 | 对象 -> JSON -> 新对象 | 需要跨语言复制,或对象结构动态变化 |
| 克隆库 | 使用 Apache Commons Lang 的 SerializationUtils | 不想自己写序列化逻辑 |
// 序列化方式实现深拷贝
public class DeepCloneUtil {
@SuppressWarnings("unchecked")
public static <T extends Serializable> T deepClone(T obj) {
try {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(obj);
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
} catch (Exception e) {
throw new RuntimeException("深拷贝失败", e);
}
}
}
原型模式 vs 工厂模式
有同学会问:原型模式和工厂模式有什么区别?
我简单说一下:
- 工厂模式:每次都是 new 一个新对象,构造逻辑在工厂里
- 原型模式:复制一个已有对象,构造逻辑在 clone() 方法里
说白了,工厂模式是「从零开始造」,原型模式是「复制粘贴改一改」。如果你的对象构造很复杂,或者你需要保留对象的部分状态,原型模式更合适。
总结
原型模式不是什么高深的技术,但它解决了一个很实际的问题:如何高效地创建复杂对象。
记住几个要点:
- 浅拷贝快但不安全,深拷贝安全但慢
- 根据场景选择合适的深拷贝方式
- 不要盲目依赖语言自带的克隆机制
- 原型模式特别适合模板、配置、文档这类场景
我在项目中用原型模式最多的就是做「模板克隆」和「快照恢复」。这两个场景几乎每天都会遇到。你想想看,如果没有原型模式,每次都要重新加载配置、重新初始化对象,那得浪费多少时间?
好了,原型模式就聊到这里。记住一句话:能复制就别重新造。
公众号:蓝海资料掘金营,微信deep3321