原型模式:定义、结构、实现(浅克隆、深克隆)、优缺点、应用场景

原型模式,说白了就是「复制粘贴」的设计模式。你想想看,在写代码的时候,是不是经常遇到这种情况:创建一个对象特别费劲,要初始化一堆属性,还要调各种 setter 方法。这时候如果能直接拿一个现成的对象,克隆一份出来,是不是省事多了?

我记得我刚接触这个模式时,第一反应是:「这不就是 new 一个对象然后 copy 属性吗?」嗯,后来我才发现,事情没那么简单。特别是涉及到引用类型的时候,浅克隆和深克隆的区别,坑了不少人。

什么是原型模式

原型模式的定义很简单:用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象

说白了,就是先有一个样板对象(原型),然后通过克隆的方式生成新对象。你不需要关心对象的创建细节,只需要告诉系统:「给我一个和这个一模一样的」。

我在项目中遇到过这样一个场景:系统需要生成大量配置相似的报表对象。每个报表对象有几十个字段,大部分字段的值都一样,只有少数几个不同。如果用 new 的方式,每次都要重复设置那几十个字段,代码又臭又长。用原型模式,克隆一份原型,只修改差异部分,代码瞬间清爽了。

原型模式的结构

原型模式的结构其实不复杂,就三个角色:

角色 说明
Prototype(抽象原型) 声明克隆方法的接口,通常是 Cloneable
ConcretePrototype(具体原型) 实现克隆方法的具体类
Client(客户端) 通过原型对象创建新对象

我习惯把原型模式理解成「复印机」:原型就是原件,克隆方法就是复印按钮,客户端就是按按钮的人。

核心要点:原型模式的关键在于克隆方法的实现。Java 中默认的 clone() 是浅克隆,深克隆需要自己实现。

浅克隆 vs 深克隆

这里有个容易踩坑的地方。我刚开始用原型模式时,以为 clone() 出来的对象是完全独立的。结果有一次在项目中,修改了克隆对象的某个属性,原对象也跟着变了。排查了半天,才发现是浅克隆的问题。

浅克隆:只复制基本类型的值,引用类型只复制引用地址。也就是说,克隆对象和原对象共享同一个引用对象。

深克隆:不仅复制基本类型的值,引用类型也会递归复制。克隆对象和原对象完全独立,互不影响。

为什么会这样?因为 Java 的 Object.clone() 方法默认就是浅克隆。如果你需要深克隆,必须自己重写 clone() 方法。

浅克隆示例

public class Address implements Cloneable {
    private String city;
    private String street;
    
    // getter/setter 省略
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();  // 浅克隆
    }
}

public class Person implements Cloneable {
    private String name;
    private int age;
    private Address address;  // 引用类型
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();  // 浅克隆,address 还是同一个对象
    }
}

你看,Person 的 clone() 方法只复制了 name 和 age,address 字段还是指向原来的对象。这时候如果修改克隆人的地址,原人的地址也会变。

深克隆示例

public class Person implements Cloneable {
    private String name;
    private int age;
    private Address address;
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        cloned.address = (Address) address.clone();  // 手动深克隆
        return cloned;
    }
}

嗯,这里要注意:深克隆要求所有引用类型的类都实现了 Cloneable 接口,并且 clone() 方法要正确实现。如果引用链比较长,每一层都要处理。

我的建议:如果对象结构比较复杂,可以用序列化方式实现深克隆。把对象写到流里再读出来,天然就是深克隆。不过性能会差一些,适合对象不大、克隆频率不高的场景。

原型模式的知识体系

下面这张图是我梳理的原型模式核心逻辑,你可以对照着理解:

原型模式核心逻辑 原型对象 (样板实例) clone() 方法 (浅克隆/深克隆) 克隆对象 (独立副本) 浅克隆 • 基本类型:复制值 • 引用类型:复制引用地址 • 克隆对象与原对象共享引用对象 • 修改引用属性会相互影响 深克隆 • 基本类型:复制值 • 引用类型:递归复制 • 克隆对象与原对象完全独立 • 修改引用属性互不影响

优缺点分析

任何模式都有两面性,原型模式也不例外。我根据自己的使用经验,总结了一下:

优点

  • 性能好:克隆比 new 再初始化要快,特别是对象创建成本高的时候
  • 简化创建过程:客户端不需要知道对象的创建细节,直接克隆就行
  • 可以动态增加产品类:原型模式不依赖具体的类,只依赖原型接口
  • 保存对象状态:可以克隆一个对象作为快照,方便回滚

缺点

  • 深克隆实现复杂:如果对象嵌套层次深,每一层都要处理克隆
  • 需要实现 Cloneable 接口:Java 中必须实现这个标记接口,否则会抛异常
  • 循环引用问题:如果对象之间存在循环引用,深克隆会陷入死循环

我曾经踩过的坑:有一次在项目中用原型模式克隆一个配置对象,这个对象里有一个 List 字段。我以为 clone() 会复制整个 List,结果只是复制了 List 的引用。后来在克隆对象里添加了一个配置项,原对象的配置项也跟着变了。排查了半天才发现是浅克隆的问题。从那以后,我每次用原型模式都会仔细检查引用类型的处理。

应用场景

原型模式不是万能的,但有些场景用起来特别顺手:

  1. 对象创建成本高:比如需要从数据库加载数据、需要复杂计算的对象,克隆比重新创建快得多
  2. 对象之间差异很小:大部分属性相同,只有少数属性不同。克隆一份原型,只修改差异部分
  3. 需要保存对象状态:比如实现撤销功能,可以克隆对象作为历史快照
  4. 避免工厂类的层次结构:如果产品类太多,用工厂模式会导致工厂类爆炸,原型模式可以避免这个问题
  5. 动态加载的类:有些类是在运行时才加载的,无法用 new 创建,但可以通过克隆已有的实例来创建

我记得在一个报表系统中,需要生成几十种不同格式的报表。每种报表的创建逻辑都很复杂,但大部分配置是相同的。我用原型模式创建了一个基础报表原型,然后每种报表克隆一份,只修改差异部分。代码量减少了 60%,而且新增报表类型特别方便。

嗯,最后说一句:原型模式虽然简单,但浅克隆和深克隆的区别一定要搞清楚。我建议你在使用前,先画一下对象的内存结构图,看看哪些是基本类型,哪些是引用类型。这样能避免很多不必要的麻烦。

公众号:蓝海资料掘金营,微信deep3321