Skip to content

原型模式 (Prototype) —— 影分身之术

前言

在深入探讨原型模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。

1. 原型模式是什么?

原型模式(Prototype Pattern)用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

通俗解释

  • 鸣人的影分身:鸣人不用每次都从娘胎里重新生出来(new),而是直接“嘭”的一声,复制(Clone)出一个一模一样的自己。
  • 打印复印:你写好了一份简历(原型),然后去复印店复印 100 份。你不需要把简历手抄 100 遍。

1.1 为什么我们需要它?

核心痛点对象创建成本过高。 有些对象的创建过程非常繁琐,比如:

  1. 数据查询慢:需要从数据库查 10 张表,耗时 2 秒。
  2. 计算复杂:需要进行复杂的数学运算或加密。
  3. 构造函数太重:构造函数里做了一大堆初始化工作。

如果我们需要 10 个这样的对象,直接 new 10 次可能要花 20 秒。但如果用原型模式,先花 2 秒创建一个,然后内存复制(Clone)9 次,可能总共只要 2.1 秒。

2. Java 实战演练:克隆一只羊

Java 提供了内置的克隆机制:Cloneable 接口和 Object.clone() 方法。

2.1 简单的浅拷贝 (Shallow Copy)

假设我们要克隆一只多利羊(Dolly)。

java
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 客户端调用

java
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 对象。
  • 深拷贝 (Deep Copy):不仅复制对象本身,还把对象内部引用的所有对象也复制一份。

浅拷贝翻车现场

java
// 修改克隆羊的生日
dollyClone.getBirthday().setTime(999999999999L);

// 完蛋,原版羊的生日也变了!
System.out.println("原版生日: " + dolly.getBirthday());

3.1 如何实现深拷贝?

方法一:手动套娃clone() 方法里手动把属性也克隆一遍。

java
@Override
public Object clone() {
    Sheep sheep = (Sheep) super.clone();
    // 核心:把属性也克隆一份
    sheep.birthday = (Date) this.birthday.clone();
    return sheep;
}

缺点:如果属性很多,或者属性里面还有属性(套娃),代码会写死人。

方法二:序列化 (推荐) 把对象变成二进制流,再读出来,生成的绝对是全新的对象。 可以使用 Java 原生序列化,或者 JSON 序列化(Gson, Jackson)。

JSON 序列化示例 (使用 Gson)

java
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 工具类。

java
Sheep sheep2 = new Sheep();
BeanUtils.copyProperties(sheep1, sheep2);

注意:BeanUtils 通常也是浅拷贝

4.2 拷贝构造函数 (Copy Constructor)

C++ 中常用的方式,Java 也可以用。

java
public Sheep(Sheep target) {
    this.name = target.name;
    this.birthday = new Date(target.birthday.getTime()); // 手动深拷贝
}

5. 源码中的原型模式

  1. java.lang.Object.clone(): Java 原生支持。
  2. java.util.ArrayList.clone(): 集合类通常都实现了 clone(),但注意是浅拷贝。
  3. Spring 的 scope="prototype": Spring Bean 默认是单例的。如果你把 scope 设为 prototype,每次 getBean 时 Spring 都会创建一个新的 Bean 实例。

6. 优缺点与适用场景总结

优点

  • 性能极佳:直接内存二进制流复制,比 new 对象(调用构造函数、类加载检查)快得多。
  • 简化创建:逃避了构造函数的复杂约束。

缺点

  • 深拷贝实现复杂:每一层对象都要实现 Cloneable,稍有不慎就会出现浅拷贝 Bug。
  • 侵入性强:必须修改已有的类,实现接口并重写方法。

适用场景

  1. 类初始化消耗资源较多。
  2. new 一个对象需要非常繁琐的过程(数据准备、访问权限)。
  3. 构造函数比较复杂。
  4. 在循环体中产生大量对象时。

7. 结语

原型模式是内存级别的拷贝。在性能敏感的场景下(如高频交易系统、游戏引擎),它是优化对象创建的神器。但在普通的业务系统中,由于深拷贝的复杂性,我们往往更倾向于直接 new 或者使用 BeanUtils.copyProperties()。记住一点:慎用浅拷贝,除非你的对象全是不可变属性 (Immutable)。

8. 实验实操

design-patterns-web 项目中,你可以体验原型模式的克隆能力。 列表中列出了初始的图形对象(圆形、正方形)。点击对应的“复制”图标,系统会基于现有对象(原型)直接克隆出一个新的图形,并显示在右侧画布上。你会发现新图形保留了原型的属性(如颜色、形状),但有了新的 ID 和位置。这展示了如何通过“复制”而非“新建”来快速生成对象。 截图建议:截取包含原始图形和多个克隆图形的画布界面。