原型模式:克隆的艺术与陷阱
原型模式,说白了就是「复制粘贴」的设计模式版本。你想想看,在代码里创建一个对象,有时候成本很高——比如要查数据库、要发网络请求、要做一堆复杂的初始化。这时候如果能直接复制一个现成的对象,稍微改改就能用,多省事。
我个人习惯把原型模式理解为「对象的复印机」。你手里有一份原件,想再要一份差不多的,按一下复印键就行。但问题来了——复印出来的东西,和原件到底是不是完全独立的?这就引出了我们今天要聊的核心:浅克隆与深克隆。
浅克隆:只复制一层
Java 里实现原型模式,第一步是实现 Cloneable 接口。这个接口其实是个标记接口,里面啥方法都没有。真正干活的是 Object.clone() 方法。
public class Order implements Cloneable {
private String orderId;
private Address address; // 引用类型
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
这段代码看起来简单,但坑就在 address 字段上。super.clone() 做的是浅克隆——它把基本类型和引用变量的值都复制了一份。但引用变量复制的是地址,不是对象本身。
我曾经踩过的坑:有一次做订单系统,用浅克隆复制了一个订单对象,然后改了新订单的地址。结果原订单的地址也跟着变了。排查了半天才发现,两个订单的 address 字段指向的是同一个内存地址。改一个,另一个也跟着遭殃。
为什么会这样?因为浅克隆只复制了引用,没复制引用指向的对象。说白了就是复印了一张地图,但地图上标注的「宝藏位置」还是同一个地方。
深克隆:彻底独立
深克隆要解决的就是这个问题。它要把对象内部所有引用类型的对象都复制一遍,直到所有层级都独立。
实现深克隆有两种主流方式:
方式一:重写 clone 方法,手动深克隆
@Override
protected Object clone() throws CloneNotSupportedException {
Order cloned = (Order) super.clone();
cloned.address = (Address) address.clone(); // 手动克隆引用对象
return cloned;
}
这种方式要求所有引用类型都实现了 Cloneable 接口。如果对象嵌套很深,比如 Address 里还有 Province 对象,那每个层级都得手动克隆。代码写起来很累,也容易漏掉某个字段。
方式二:序列化实现深克隆
我个人更推荐这种方式。利用 Java 的序列化机制,把对象写到流里再读回来,天然就是深克隆。
public class Order implements Serializable {
private String orderId;
private Address address;
public Order deepClone() {
try {
// 写入字节数组流
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(this);
// 从字节数组流读取
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (Order) ois.readObject();
} catch (Exception e) {
throw new RuntimeException("深克隆失败", e);
}
}
}
提示:使用序列化方式,所有涉及的类都必须实现 Serializable 接口。如果某个字段不想被克隆,可以用 transient 关键字修饰。
序列化方式的优点是省心,不管对象嵌套多深,一次搞定。缺点是性能比手动克隆差一些,毕竟涉及 IO 操作。不过对于大多数业务场景,这点性能差异可以忽略不计。
原型模式在 Spring Bean 中的作用域
说到原型模式,就不得不提 Spring 框架。Spring 里的 Bean 作用域,其实就用了原型模式的思想。
| 作用域 | 说明 | 与原型模式的关系 |
|---|---|---|
| singleton | 单例,整个容器只有一个实例 | 不涉及原型模式 |
| prototype | 每次获取都创建新实例 | 本质就是原型模式的应用 |
| request | 每个 HTTP 请求一个实例 | 类似原型模式,但由容器管理生命周期 |
| session | 每个 HTTP 会话一个实例 | 同上 |
Spring 的 prototype 作用域,每次调用 getBean() 时,容器都会创建一个新的 Bean 实例。这个创建过程,其实就是原型模式的应用——容器内部维护了一个「原型 Bean」的定义,每次根据这个定义克隆出新对象。
我记得有一次做微服务项目,有个配置类需要每次调用都生成新实例,因为里面包含了请求上下文的信息。我一开始用了 singleton,结果发现所有请求共享同一个实例,数据全乱套了。改成 prototype 后,问题迎刃而解。
核心要点:
- 浅克隆:复制引用,不复制对象。适合对象内部只有基本类型或不可变对象的情况。
- 深克隆:复制整个对象树。适合对象内部有可变引用类型的情况。
- 序列化深克隆:通用性强,但要求所有类实现 Serializable。
- Spring prototype:本质是原型模式,每次获取都创建新实例。
嗯,这里要注意一点:原型模式不是万能的。如果对象的创建成本很低,直接用 new 反而更清晰。原型模式的价值在于「复制一个已经初始化好的对象」,省去重复的初始化过程。
你想想看,如果一个对象需要从数据库加载 10 个关联数据才能初始化完成,每次 new 都要查一遍数据库,那性能得多差?这时候用原型模式,克隆一个已经加载好的对象,改改关键字段就能用,效率提升不是一点半点。
最后说一句,原型模式在实际项目中用得不算特别频繁,但一旦用对地方,效果立竿见影。我建议你在设计阶段就考虑好:哪些对象可能需要被复制?复制的时候是浅克隆就够了,还是必须深克隆?提前想清楚,后面能省不少事。
避坑指南:
- 不要忘记实现 Cloneable 接口,否则会抛 CloneNotSupportedException
- 浅克隆后修改引用类型字段,会影响原对象
- 序列化深克隆要小心 transient 字段会被忽略
- Spring prototype 作用域下,容器不负责 Bean 的销毁