12. 原型模式:克隆对象、浅拷贝与深拷贝、实战:对象池优化
原型模式,说白了就是“复制粘贴”。
你想想看,有时候创建一个对象特别费劲——要查数据库、要算一堆参数、要组装复杂的内部结构。这时候如果有个现成的对象,直接克隆一份,改改关键字段,是不是省事多了?
我在项目中遇到过好几次这种场景。最典型的就是报表引擎,每次生成报表都要加载模板、初始化几十个配置项。后来我改用原型模式,把模板对象缓存起来,需要时直接clone,性能提升了将近10倍。
12.1 为什么需要原型模式
先看一个痛点。假设你有个订单对象:
public class Order {
private String orderId;
private List<OrderItem> items;
private Address shippingAddress;
private PaymentInfo paymentInfo;
// 还有20个字段...
public Order(String orderId) {
// 这里要查数据库、调外部服务、做各种校验
// 构造一个Order可能要花200ms
}
}
每次new一个Order都要花200ms,如果同时要创建100个类似的订单,光构造时间就要20秒。用户早就摔键盘了。
原型模式就是来解决这个问题的。它通过克隆已有的对象来创建新对象,而不是通过new。Java里最直接的体现就是Cloneable接口和clone()方法。
核心思想:用已有的对象作为“原型”,通过拷贝来创建新对象。避免重复的初始化开销。
12.2 浅拷贝 vs 深拷贝
这里有个坑,我当年踩过。Java的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 Employee implements Cloneable {
private String name;
private Address address; // 引用类型
@Override
protected Object clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone(); // 浅拷贝
// 注意:这里cloned.address和原对象的address是同一个对象!
return cloned;
}
}
为什么会这样?因为super.clone()只复制了基本类型和引用本身,不会递归复制引用指向的对象。
我曾经在项目中用浅拷贝克隆了一个用户对象,然后改了克隆对象的地址,结果原对象的地址也跟着变了。排查了半天才发现是浅拷贝的问题。嗯,从那以后我对深拷贝就特别敏感。
12.2.1 实现深拷贝的几种方式
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 重写clone()手动深拷贝 | 性能好,可控性强 | 代码量大,容易遗漏 | 对象结构固定,字段少 |
| 序列化/反序列化 | 自动处理所有层级 | 性能差,需要实现Serializable | 对象结构复杂,对性能不敏感 |
| JSON序列化 | 跨语言,调试方便 | 性能中等,依赖第三方库 | 需要和前端交互的场景 |
| BeanUtils.copyProperties | 使用简单 | 只复制一层,本质是浅拷贝 | DTO转换等简单场景 |
我个人习惯用序列化的方式来实现深拷贝。虽然性能差一点,但不容易出错。给你看个例子:
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);
}
}
}
小技巧:如果对象里有些字段不需要拷贝(比如缓存、连接池等),可以用transient关键字标记。序列化时会自动跳过这些字段。
12.3 实战:对象池优化
原型模式最经典的应用场景就是对象池。我做过一个数据库连接池的优化,当时用原型模式把连接创建时间从50ms降到了1ms以内。
对象池的核心逻辑是这样的:
- 预先创建一批原型对象
- 需要对象时,从池里克隆一个
- 用完归还,或者直接丢弃
- 池里对象不够时,自动扩容
来看一个简单的对象池实现:
public class ObjectPool<T extends Cloneable> {
private final List<T> pool = new ArrayList<>();
private final T prototype;
private final int maxSize;
public ObjectPool(T prototype, int initialSize, int maxSize) {
this.prototype = prototype;
this.maxSize = maxSize;
// 预创建对象
for (int i = 0; i < initialSize; i++) {
pool.add(createClone());
}
}
@SuppressWarnings("unchecked")
private T createClone() {
try {
// 关键:通过克隆创建新对象
Method cloneMethod = prototype.getClass().getMethod("clone");
return (T) cloneMethod.invoke(prototype);
} catch (Exception e) {
throw new RuntimeException("克隆失败", e);
}
}
public synchronized T borrow() {
if (pool.isEmpty()) {
if (pool.size() < maxSize) {
return createClone(); // 动态扩容
}
throw new RuntimeException("池已满,无法创建新对象");
}
return pool.remove(pool.size() - 1);
}
public synchronized void return_(T obj) {
if (pool.size() < maxSize) {
pool.add(obj); // 归还对象
}
// 如果池已满,直接丢弃(GC会处理)
}
}
注意:对象池不是万能的。如果对象创建开销很小,或者对象状态变化频繁,用对象池反而会增加复杂度。我建议只在以下场景使用:
- 对象创建成本高(数据库连接、线程、大对象)
- 对象创建频率高
- 对象可复用,状态可重置
12.4 原型模式的知识体系
下面这张图总结了原型模式的核心逻辑和实战要点:
12.5 避坑指南
最后分享几个我踩过的坑:
- 忘记实现Cloneable:调用clone()会抛
CloneNotSupportedException。这是Java的“故意设计”,提醒你显式声明支持克隆。 - 循环引用导致死循环:深拷贝时如果对象A引用B,B又引用A,序列化方式会抛
StackOverflowError。我建议在业务层做引用检查。 - 单例对象被克隆:如果原型对象里引用了单例,克隆后所有副本共享同一个单例实例。这通常不是你想要的效果,记得用
transient排除。 - 对象池里的对象状态污染:归还对象前一定要重置状态。我曾经漏了重置一个计数器字段,结果下一个使用者拿到的是脏数据。
我的建议:如果项目里用到了Spring,可以考虑用BeanUtils.copyProperties做浅拷贝,用SerializationUtils.clone(Apache Commons)做深拷贝。别自己造轮子,除非你有特殊需求。
原型模式说白了就是“复制粘贴”的工程化实现。它不复杂,但用好了能省很多事。下次遇到创建对象特别慢的场景,不妨想想:能不能克隆一个?