原型模式(Prototype):浅克隆与深克隆
原型模式,说白了就是「复制对象」。
我刚开始接触这个模式时,觉得它挺简单的——不就是拷贝一份数据嘛。但后来在项目中踩过几次坑,才发现「拷贝」这两个字背后,藏着不少门道。
什么时候需要用原型模式?
- 创建对象成本很高(比如从数据库加载、网络请求)
- 对象之间只有少量差异,不想从头 new 一个
- 运行时需要动态生成对象,但类名不确定
Java 里提供了 Cloneable 接口,C++ 则靠拷贝构造函数。但两者都有个共同的问题:浅克隆 vs 深克隆。咱们一个一个聊。
浅克隆:复制了引用,没复制内容
先看 Java 的浅克隆。实现 Cloneable 接口,重写 clone() 方法,默认就是浅克隆。
public class Address implements Cloneable {
private String city;
// getter/setter 省略
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Person implements Cloneable {
private String name;
private Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅克隆
}
}
看起来没问题对吧?但实际跑起来你会发现:克隆出来的 Person 和原 Person 共享同一个 Address 对象。
关键点:浅克隆只复制了基本类型和引用地址。引用指向的对象,还是同一个。
我在项目中遇到过这样的场景:用户配置了一个订单模板,里面有收货地址。我浅克隆了一份让用户编辑,结果原模板的地址也被改了。嗯,那场面挺尴尬的。
深克隆:彻底复制,互不干扰
深克隆要怎么做?两种主流方式:
- 手动深克隆——在 clone() 方法里,把引用对象也 clone 一遍
- 序列化深克隆——把对象写到流里再读回来,天然就是深拷贝
先看手动方式:
@Override
protected Object clone() throws CloneNotSupportedException {
Person cloned = (Person) super.clone();
cloned.address = (Address) this.address.clone(); // 手动深克隆
return cloned;
}
这种方式要求所有引用对象都实现 Cloneable。如果对象嵌套很深,写起来很痛苦。我一般只在层级简单时用。
序列化实现深克隆
我个人更推荐用序列化。Java 里把对象写到字节数组,再读回来,所有引用都会被重新创建。
public class DeepCloneUtil {
@SuppressWarnings("unchecked")
public static <T> 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);
}
}
}
注意:所有涉及的类必须实现 Serializable 接口。这是序列化的前提。
小技巧:如果某些字段不想被克隆(比如缓存、连接池),可以用 transient 关键字标记。序列化时会跳过它们。
C++ 拷贝构造:浅与深的抉择
C++ 没有 Cloneable 接口,但拷贝构造函数和赋值运算符是标配。默认的拷贝构造也是浅克隆。
class Address {
public:
std::string city;
// 默认拷贝构造就是浅克隆
};
class Person {
public:
std::string name;
Address* address; // 指针成员
// 浅克隆拷贝构造
Person(const Person& other)
: name(other.name), address(other.address) {}
// 深克隆拷贝构造
Person(const Person& other)
: name(other.name), address(new Address(*other.address)) {}
};
这里有个坑:如果类里有指针成员,浅克隆会导致两个对象指向同一块内存。析构时 double free,程序直接崩掉。
避坑指南:我曾经在 C++ 项目里用默认拷贝构造复制了一个带指针的类,结果程序跑着跑着就 segfault。查了两天才发现是浅克隆导致的重复释放。从那以后,我只要看到指针成员,第一反应就是「必须深克隆」。
C++ 里实现深克隆,通常用「拷贝并交换」惯用法(copy-and-swap),配合智能指针会更安全。
class Person {
std::shared_ptr<Address> address;
public:
Person(const Person& other)
: name(other.name), address(std::make_shared<Address>(*other.address)) {}
};
用 shared_ptr 后,浅克隆其实也能接受——引用计数会帮你管理生命周期。但如果你需要独立修改地址内容,还是得深克隆。
原型模式的核心逻辑
我画了一张图,帮你理清原型模式的知识体系:
什么时候选浅克隆,什么时候选深克隆?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 对象只有基本类型 | 浅克隆 | 简单高效,没有共享风险 |
| 对象包含不可变引用(如 String) | 浅克隆 | 不可变对象不怕共享 |
| 对象包含可变引用 | 深克隆 | 避免意外修改原对象 |
| 对象嵌套层级深 | 序列化深克隆 | 省去手动 clone 的繁琐 |
| C++ 指针成员 | 深克隆或智能指针 | 防止 double free |
我的习惯:除非我能 100% 确定引用对象不会被修改,否则一律用深克隆。多花几毫秒,省去几小时的调试时间,这笔账怎么算都划算。
原型模式看起来简单,但「浅 vs 深」这个选择题,几乎每个项目都会遇到。你想想看,如果团队里有人不小心用了浅克隆,改了一个对象的属性,结果影响了另一个对象——这种 bug 排查起来真的很头疼。
所以我的建议是:先想清楚你的对象结构,再决定克隆策略。如果拿不准,就用深克隆。安全第一。
公众号:蓝海资料掘金营,微信deep3321