原型模式 (Prototype) —— 影分身之术
前言
在深入探讨原型模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。
1. 原型模式是什么?
原型模式(Prototype Pattern)用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
通俗解释:
- 鸣人的影分身:鸣人不用每次都从娘胎里重新生出来(
new),而是直接“嘭”的一声,复制(Clone)出一个一模一样的自己。 - 打印复印:你写好了一份简历(原型),然后去复印店复印 100 份。你不需要把简历手抄 100 遍。
1.1 为什么我们需要它?
核心痛点:对象创建成本过高。 有些对象的创建过程非常繁琐,比如:
- 数据查询慢:需要从数据库查 10 张表,耗时 2 秒。
- 计算复杂:需要进行复杂的数学运算或加密。
- 构造函数太重:构造函数里做了一大堆初始化工作。
如果我们需要 10 个这样的对象,直接 new 10 次可能要花 20 秒。但如果用原型模式,先花 2 秒创建一个,然后内存复制(Clone)9 次,可能总共只要 2.1 秒。
2. Java 实战演练:克隆一只羊
Java 提供了内置的克隆机制:Cloneable 接口和 Object.clone() 方法。
2.1 简单的浅拷贝 (Shallow Copy)
假设我们要克隆一只多利羊(Dolly)。
import java.util.Date;
// 1. 实现 Cloneable 接口(这只是个标记接口,不实现会抛 CloneNotSupportedException)
public class Sheep implements Cloneable {
private String name;
private Date birthday;
public Sheep(String name, Date birthday) {
this.name = name;
this.birthday = birthday;
System.out.println("执行了构造函数,很耗时...");
}
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Date getBirthday() { return birthday; }
public void setBirthday(Date birthday) { this.birthday = birthday; }
// 2. 重写 clone 方法,提升访问权限为 public
@Override
public Object clone() {
Object clone = null;
try {
clone = super.clone(); // 调用 Object 的 native 方法,内存块复制
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return clone;
}
@Override
public String toString() {
return "Sheep{name='" + name + "', birthday=" + birthday + '}';
}
}2.2 客户端调用
public class Test {
public static void main(String[] args) {
Date date = new Date(123123123123L);
Sheep dolly = new Sheep("多利", date);
// 克隆一只
Sheep dollyClone = (Sheep) dolly.clone();
dollyClone.setName("多利2号");
System.out.println("原版: " + dolly);
System.out.println("克隆版: " + dollyClone);
// 注意:克隆对象不会执行构造函数!
}
}3. 核心坑点:深拷贝 vs 浅拷贝
这是原型模式最容易出 Bug 的地方。
- 浅拷贝 (Shallow Copy):
super.clone()默认是浅拷贝。它只复制基本数据类型(int, float)和引用地址。- 后果:如果你修改了克隆羊的生日(Date 对象),原版羊的生日也会变!因为它们指向堆内存中同一个
Date对象。
- 后果:如果你修改了克隆羊的生日(Date 对象),原版羊的生日也会变!因为它们指向堆内存中同一个
- 深拷贝 (Deep Copy):不仅复制对象本身,还把对象内部引用的所有对象也复制一份。
浅拷贝翻车现场:
// 修改克隆羊的生日
dollyClone.getBirthday().setTime(999999999999L);
// 完蛋,原版羊的生日也变了!
System.out.println("原版生日: " + dolly.getBirthday());3.1 如何实现深拷贝?
方法一:手动套娃 在 clone() 方法里手动把属性也克隆一遍。
@Override
public Object clone() {
Sheep sheep = (Sheep) super.clone();
// 核心:把属性也克隆一份
sheep.birthday = (Date) this.birthday.clone();
return sheep;
}缺点:如果属性很多,或者属性里面还有属性(套娃),代码会写死人。
方法二:序列化 (推荐) 把对象变成二进制流,再读出来,生成的绝对是全新的对象。 可以使用 Java 原生序列化,或者 JSON 序列化(Gson, Jackson)。
JSON 序列化示例 (使用 Gson):
import com.google.gson.Gson;
public Sheep deepClone() {
Gson gson = new Gson();
String json = gson.toJson(this);
return gson.fromJson(json, Sheep.class);
}这种方法代码最少,虽然性能比 Java 原生序列化略慢,但在大多数业务场景下完全够用。
4. 扩展:BeanUtils 和 拷贝构造函数
除了 clone(),我们还有其他方式实现原型模式。
4.1 BeanUtils
Spring 和 Apache Commons 都提供了 BeanUtils 工具类。
Sheep sheep2 = new Sheep();
BeanUtils.copyProperties(sheep1, sheep2);注意:BeanUtils 通常也是浅拷贝。
4.2 拷贝构造函数 (Copy Constructor)
C++ 中常用的方式,Java 也可以用。
public Sheep(Sheep target) {
this.name = target.name;
this.birthday = new Date(target.birthday.getTime()); // 手动深拷贝
}5. 源码中的原型模式
java.lang.Object.clone(): Java 原生支持。java.util.ArrayList.clone(): 集合类通常都实现了clone(),但注意是浅拷贝。- Spring 的
scope="prototype": Spring Bean 默认是单例的。如果你把 scope 设为prototype,每次getBean时 Spring 都会创建一个新的 Bean 实例。
6. 优缺点与适用场景总结
优点
- 性能极佳:直接内存二进制流复制,比
new对象(调用构造函数、类加载检查)快得多。 - 简化创建:逃避了构造函数的复杂约束。
缺点
- 深拷贝实现复杂:每一层对象都要实现
Cloneable,稍有不慎就会出现浅拷贝 Bug。 - 侵入性强:必须修改已有的类,实现接口并重写方法。
适用场景
- 类初始化消耗资源较多。
new一个对象需要非常繁琐的过程(数据准备、访问权限)。- 构造函数比较复杂。
- 在循环体中产生大量对象时。
7. 结语
原型模式是内存级别的拷贝。在性能敏感的场景下(如高频交易系统、游戏引擎),它是优化对象创建的神器。但在普通的业务系统中,由于深拷贝的复杂性,我们往往更倾向于直接 new 或者使用 BeanUtils.copyProperties()。记住一点:慎用浅拷贝,除非你的对象全是不可变属性 (Immutable)。
8. 实验实操
在 design-patterns-web 项目中,你可以体验原型模式的克隆能力。 列表中列出了初始的图形对象(圆形、正方形)。点击对应的“复制”图标,系统会基于现有对象(原型)直接克隆出一个新的图形,并显示在右侧画布上。你会发现新图形保留了原型的属性(如颜色、形状),但有了新的 ID 和位置。这展示了如何通过“复制”而非“新建”来快速生成对象。 截图建议:截取包含原始图形和多个克隆图形的画布界面。