原型模式:定义、结构、实现(浅克隆、深克隆)、优缺点、应用场景
原型模式,说白了就是「复制粘贴」的设计模式。你想想看,在写代码的时候,是不是经常遇到这种情况:创建一个对象特别费劲,要初始化一堆属性,还要调各种 setter 方法。这时候如果能直接拿一个现成的对象,克隆一份出来,是不是省事多了?
我记得我刚接触这个模式时,第一反应是:「这不就是 new 一个对象然后 copy 属性吗?」嗯,后来我才发现,事情没那么简单。特别是涉及到引用类型的时候,浅克隆和深克隆的区别,坑了不少人。
什么是原型模式
原型模式的定义很简单:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。
说白了,就是先有一个样板对象(原型),然后通过克隆的方式生成新对象。你不需要关心对象的创建细节,只需要告诉系统:「给我一个和这个一模一样的」。
我在项目中遇到过这样一个场景:系统需要生成大量配置相似的报表对象。每个报表对象有几十个字段,大部分字段的值都一样,只有少数几个不同。如果用 new 的方式,每次都要重复设置那几十个字段,代码又臭又长。用原型模式,克隆一份原型,只修改差异部分,代码瞬间清爽了。
原型模式的结构
原型模式的结构其实不复杂,就三个角色:
| 角色 | 说明 |
|---|---|
| Prototype(抽象原型) | 声明克隆方法的接口,通常是 Cloneable |
| ConcretePrototype(具体原型) | 实现克隆方法的具体类 |
| Client(客户端) | 通过原型对象创建新对象 |
我习惯把原型模式理解成「复印机」:原型就是原件,克隆方法就是复印按钮,客户端就是按按钮的人。
核心要点:原型模式的关键在于克隆方法的实现。Java 中默认的 clone() 是浅克隆,深克隆需要自己实现。
浅克隆 vs 深克隆
这里有个容易踩坑的地方。我刚开始用原型模式时,以为 clone() 出来的对象是完全独立的。结果有一次在项目中,修改了克隆对象的某个属性,原对象也跟着变了。排查了半天,才发现是浅克隆的问题。
浅克隆:只复制基本类型的值,引用类型只复制引用地址。也就是说,克隆对象和原对象共享同一个引用对象。
深克隆:不仅复制基本类型的值,引用类型也会递归复制。克隆对象和原对象完全独立,互不影响。
为什么会这样?因为 Java 的 Object.clone() 方法默认就是浅克隆。如果你需要深克隆,必须自己重写 clone() 方法。
浅克隆示例
public class Address implements Cloneable {
private String city;
private String street;
// getter/setter 省略
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅克隆
}
}
public class Person implements Cloneable {
private String name;
private int age;
private Address address; // 引用类型
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅克隆,address 还是同一个对象
}
}
你看,Person 的 clone() 方法只复制了 name 和 age,address 字段还是指向原来的对象。这时候如果修改克隆人的地址,原人的地址也会变。
深克隆示例
public class Person implements Cloneable {
private String name;
private int age;
private Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) address.clone(); // 手动深克隆
return cloned;
}
}
嗯,这里要注意:深克隆要求所有引用类型的类都实现了 Cloneable 接口,并且 clone() 方法要正确实现。如果引用链比较长,每一层都要处理。
我的建议:如果对象结构比较复杂,可以用序列化方式实现深克隆。把对象写到流里再读出来,天然就是深克隆。不过性能会差一些,适合对象不大、克隆频率不高的场景。
原型模式的知识体系
下面这张图是我梳理的原型模式核心逻辑,你可以对照着理解:
优缺点分析
任何模式都有两面性,原型模式也不例外。我根据自己的使用经验,总结了一下:
优点
- 性能好:克隆比 new 再初始化要快,特别是对象创建成本高的时候
- 简化创建过程:客户端不需要知道对象的创建细节,直接克隆就行
- 可以动态增加产品类:原型模式不依赖具体的类,只依赖原型接口
- 保存对象状态:可以克隆一个对象作为快照,方便回滚
缺点
- 深克隆实现复杂:如果对象嵌套层次深,每一层都要处理克隆
- 需要实现 Cloneable 接口:Java 中必须实现这个标记接口,否则会抛异常
- 循环引用问题:如果对象之间存在循环引用,深克隆会陷入死循环
我曾经踩过的坑:有一次在项目中用原型模式克隆一个配置对象,这个对象里有一个 List 字段。我以为 clone() 会复制整个 List,结果只是复制了 List 的引用。后来在克隆对象里添加了一个配置项,原对象的配置项也跟着变了。排查了半天才发现是浅克隆的问题。从那以后,我每次用原型模式都会仔细检查引用类型的处理。
应用场景
原型模式不是万能的,但有些场景用起来特别顺手:
- 对象创建成本高:比如需要从数据库加载数据、需要复杂计算的对象,克隆比重新创建快得多
- 对象之间差异很小:大部分属性相同,只有少数属性不同。克隆一份原型,只修改差异部分
- 需要保存对象状态:比如实现撤销功能,可以克隆对象作为历史快照
- 避免工厂类的层次结构:如果产品类太多,用工厂模式会导致工厂类爆炸,原型模式可以避免这个问题
- 动态加载的类:有些类是在运行时才加载的,无法用 new 创建,但可以通过克隆已有的实例来创建
我记得在一个报表系统中,需要生成几十种不同格式的报表。每种报表的创建逻辑都很复杂,但大部分配置是相同的。我用原型模式创建了一个基础报表原型,然后每种报表克隆一份,只修改差异部分。代码量减少了 60%,而且新增报表类型特别方便。
嗯,最后说一句:原型模式虽然简单,但浅克隆和深克隆的区别一定要搞清楚。我建议你在使用前,先画一下对象的内存结构图,看看哪些是基本类型,哪些是引用类型。这样能避免很多不必要的麻烦。