原型模式:浅克隆与深克隆、Cloneable接口、序列化实现深克隆
原型模式,说白了就是「复制粘贴」的设计模式。你手头有一个对象,想再搞一个一模一样的,但又不想走一遍构造函数的流程——这时候原型模式就派上用场了。
我记得刚入行那会儿,有个需求是要生成大量配置相似但又有细微差别的报表对象。一开始我傻乎乎地 new 了几百次,每次都要 set 一堆属性。后来同事看了一眼我的代码,说了句:「你试试 Cloneable?」——嗯,从此我就记住了原型模式。
什么是原型模式
原型模式的核心思想是:通过复制已有对象来创建新对象,而不是通过 new 关键字。
它属于创建型模式,适用于以下场景:
- 对象创建成本高(比如需要从数据库加载数据)
- 对象之间只有少量差异
- 构造函数复杂,或者构造函数有副作用
你想想看,如果一个对象的构造函数里调了远程接口、查了数据库、做了大量计算,每次 new 一次就全来一遍——这谁受得了?
Java 中的 Cloneable 接口
Java 原生提供了 Cloneable 接口和 clone() 方法。但这里有个坑:Cloneable 是个标记接口,里面啥方法都没有。真正干活的是 Object.clone()。
使用方式很简单:
public class Report implements Cloneable {
private String title;
private List<String> dataRows;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
调用的时候:
Report original = new Report();
Report copy = (Report) original.clone();
看起来挺方便对吧?但这里藏着一个大坑——浅克隆。
浅克隆 vs 深克隆
这两个概念,我当年面试的时候被问过不止一次。说白了:
- 浅克隆:复制基本类型,引用类型只复制地址。你改我改,大家改。
- 深克隆:引用类型也复制一份新的。你改你的,我改我的,互不干扰。
来看个例子就明白了:
public class Address {
String city;
String street;
public Address(String city, String street) {
this.city = city;
this.street = street;
}
}
public class Employee implements Cloneable {
String name;
int age;
Address address;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 浅克隆
}
}
这时候你克隆一个 Employee:
Employee e1 = new Employee("张三", 30, new Address("北京", "长安街"));
Employee e2 = (Employee) e1.clone();
e2.address.city = "上海"; // 改 e2 的地址
System.out.println(e1.address.city); // 输出:上海 —— 因为 e1 和 e2 共享同一个 Address 对象
为什么会这样?因为 super.clone() 只复制了 address 这个引用变量的值(也就是内存地址),并没有复制 Address 对象本身。
核心结论:浅克隆只复制一层,深克隆递归复制所有层级。
如何实现深克隆
实现深克隆,常见的有两种方式:
方式一:重写 clone() 方法,手动复制引用对象
@Override
protected Object clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
cloned.address = new Address(this.address.city, this.address.street);
return cloned;
}
这种方式的问题很明显:如果对象嵌套很深,你得一层层手动复制。而且如果 Address 里面还有引用类型,你还得继续往下挖。代码写起来又臭又长。
方式二:序列化实现深克隆(我推荐的方式)
我个人习惯用序列化来做深克隆。原理很简单:把对象写到流里,再读回来。中间经过了一次完整的序列化和反序列化,所有引用类型都会被重新创建。
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);
oos.flush();
// 从字节数组流读取
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
return (T) ois.readObject();
} catch (Exception e) {
throw new RuntimeException("深克隆失败", e);
}
}
}
使用前提:所有涉及的类都必须实现 Serializable 接口。
public class Address implements Serializable {
private static final long serialVersionUID = 1L;
String city;
String street;
}
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
String name;
int age;
Address address;
}
// 使用
Employee e1 = new Employee("张三", 30, new Address("北京", "长安街"));
Employee e2 = DeepCloneUtil.deepClone(e1);
e2.address.city = "上海";
System.out.println(e1.address.city); // 输出:北京 —— 互不影响
我曾经踩过的坑
说个真实经历。有一次我在做一个订单系统的缓存模块,订单对象里有个 List<OrderItem> 的字段。我用 super.clone() 克隆了一份订单,然后修改了克隆订单的某个商品数量——结果原订单的商品数量也跟着变了。
排查了半天才发现是浅克隆的问题。那会儿我还没用序列化方案,只能手动去重写 clone(),把 List 里的每个 OrderItem 都 new 一遍。代码改完,测试通过,但心里总觉得别扭。
后来我养成了一个习惯:只要涉及克隆,先问自己一句——这里需要深克隆吗?
原型模式的知识体系
下面这张图帮你理清原型模式的核心脉络:
什么时候用原型模式
我总结了几条经验:
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 对象只有基本类型字段 | 浅克隆(默认 clone()) | 简单高效,没有引用共享问题 |
| 对象有引用类型,但层级浅 | 手动重写 clone() | 性能好,代码可控 |
| 对象嵌套深,或经常变化 | 序列化深克隆 | 通用性强,改字段不用改克隆代码 |
| 需要克隆的对象来自第三方库 | 序列化深克隆 | 无法修改源码,序列化是唯一选择 |
DeepCloneUtil。这样团队里所有人都用同一套方案,避免每个人各自实现、各自踩坑。
原型模式看起来简单,但浅克隆和深克隆的区别如果不搞清楚,线上出 bug 的时候排查起来真的很痛苦。尤其是那些引用类型嵌套了好几层的对象——你改了一个字段,结果影响了一大片,那场面,想想都头疼。
嗯,记住一句话:用 clone() 之前,先想清楚你要的是浅还是深。