Skip to content

原型模式是"用克隆代替 new"——复制一个已有对象,而不是从头创建。它在"创建对象成本高"或"对象结构复杂"时特别有用。这篇讲清原型怎么实现、以及 Java 里克隆的坑。

一、解决什么问题:创建成本高、结构复杂

new 一个对象通常很快,但有些场景不想从头创建——要么创建成本高(查数据库、初始化一堆字段),要么需要大量"相似但独立"的副本(合同模板克隆出几十份、每份单独修改)。原型模式就是:以已有对象为原型,克隆出新对象

原型模式就是:以已有对象为原型,克隆出新对象

二、核心:克隆方法

java
// 原型接口:声明克隆方法
interface Prototype {
    Prototype clone();
}

// 文档:实现 Cloneable,用 super.clone() 自动复制所有字段
class Document implements Prototype, Cloneable {
    private String title;
    private String content;

    Document(String title, String content) {
        this.title = title;
        this.content = content;
    }

    @Override
    public Document clone() {
        try {
            return (Document) super.clone();   // Object.clone():自动复制所有字段
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    void setTitle(String t) { this.title = t; }
}

// 使用:从"合同模板"克隆出多份,每份独立修改互不影响
Document template = new Document("合同", "甲方:...");   // 模板(创建一次)
Document c1 = template.clone();
c1.setTitle("合同-张三");     // 改自己的,不影响模板和其他副本
Document c2 = template.clone();
c2.setTitle("合同-李四");     // 模板还在,还能继续克隆

两个关键点

  • 为什么用 super.clone()Object.clone()自动复制所有字段,不用手动一个个赋值——手动写(c.field = this.field)字段一多就容易漏,漏一个字段就是 bug。
  • 克隆的意义:得到字段相同但独立的对象——改克隆品(setTitle)不影响模板和其他克隆品。这是"复制对象"和"克隆对象"的区别:复制是拿到一样的值,克隆是拿到独立的副本

三、Java 的浅拷贝 vs 深拷贝(坑)

Object.clone() 默认是浅拷贝——引用类型字段只复制"引用"(指针),不复制指向的对象本身:

java
class Order implements Cloneable {
    private String title;      // 引用类型(String 不可变,共享无影响)
    private List<Item> items;  // 引用类型(List 可变,共享会出事!)

    @Override
    public Order clone() {
        return (Order) super.clone();   // 浅拷贝:items 复制的是"引用",还是同一个 List
    }
}

浅拷贝 vs 深拷贝:浅拷贝 items 共享同一个 List(改一个两个都变),深拷贝 items 是独立副本

浅拷贝的效果:克隆出的 Order 和原 Order,items 指向同一个 List——改一个的 items,另一个也跟着变(共享数据出 bug)。

要深拷贝,得手动复制引用字段:

java
@Override
public Order clone() {
    Order o = (Order) super.clone();          // ① 先浅拷贝:此时 o.items 还指向 this.items(同一个 List)
    o.items = new ArrayList<>(this.items);    // ② 深拷贝关键:new 一份"内容相同但独立"的新 List,让 o.items 指向它
    return o;                                 // ③ 返回:现在 o.items 和 this.items 是两个不同的 List,互不影响
}

为什么是这两步super.clone() 复制了所有字段(引用字段暂时共享),然后 o.items = new ArrayList<>(this.items)o.items 换成独立的新 List——new ArrayList<>(...) 是"复制内容、创建新对象",o.items = ... 是"让克隆对象指向新对象"。

关键:克隆时按字段类型区分——

字段类型例子要不要深拷贝为什么
基本类型int、double、boolean不用存的是值,复制即独立
不可变引用String不用内容改不了,共享也没副作用
可变引用List、Map、自定义对象必须内容会被修改,共享则互相传染

为什么 String 共享没影响(关键理解):区分"改内容"和"换指向"——

  • o.items.add(x)改 List 的内容(List 对象本身变了)→ 共享的另一个也看到 x
  • o.title = "新标题"换 String 的指向(让 o.title 指向新的 String,原来的 String 没被动)→ 共享的另一个不受影响

String 不可变,对它只能"换指向"、不能"改内容",共享自然无害;List/Map 可变,能被"改内容",共享就互相传染,必须深拷贝。

四、实际应用

  • Spring 的 @Scope("prototype"):每次获取都返回一个新的 bean 实例(原型作用域)
  • 配置模板:加载一次配置模板,之后 clone 出多个副本
  • 游戏/编辑器:复制对象(怪物、文档快照)

小结

  • 原型 = 用克隆代替 new,适合"创建成本高"的对象
  • Java clone() 默认浅拷贝,引用字段要手动深拷贝
  • Spring @Scope("prototype") 就是原型思想的落地

相关:《单例模式》(单例是"只有一个",原型是"每次克隆新对象",正好相反)。