原型模式:克隆的艺术与陷阱

原型模式,说白了就是「复制粘贴」的设计模式版本。你想想看,在代码里创建一个对象,有时候成本很高——比如要查数据库、要发网络请求、要做一堆复杂的初始化。这时候如果能直接复制一个现成的对象,稍微改改就能用,多省事。

我个人习惯把原型模式理解为「对象的复印机」。你手里有一份原件,想再要一份差不多的,按一下复印键就行。但问题来了——复印出来的东西,和原件到底是不是完全独立的?这就引出了我们今天要聊的核心:浅克隆与深克隆。

浅克隆:只复制一层

Java 里实现原型模式,第一步是实现 Cloneable 接口。这个接口其实是个标记接口,里面啥方法都没有。真正干活的是 Object.clone() 方法。

public class Order implements Cloneable {
    private String orderId;
    private Address address;  // 引用类型

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

这段代码看起来简单,但坑就在 address 字段上。super.clone() 做的是浅克隆——它把基本类型和引用变量的值都复制了一份。但引用变量复制的是地址,不是对象本身。

我曾经踩过的坑:有一次做订单系统,用浅克隆复制了一个订单对象,然后改了新订单的地址。结果原订单的地址也跟着变了。排查了半天才发现,两个订单的 address 字段指向的是同一个内存地址。改一个,另一个也跟着遭殃。

为什么会这样?因为浅克隆只复制了引用,没复制引用指向的对象。说白了就是复印了一张地图,但地图上标注的「宝藏位置」还是同一个地方。

深克隆:彻底独立

深克隆要解决的就是这个问题。它要把对象内部所有引用类型的对象都复制一遍,直到所有层级都独立。

实现深克隆有两种主流方式:

方式一:重写 clone 方法,手动深克隆

@Override
protected Object clone() throws CloneNotSupportedException {
    Order cloned = (Order) super.clone();
    cloned.address = (Address) address.clone();  // 手动克隆引用对象
    return cloned;
}

这种方式要求所有引用类型都实现了 Cloneable 接口。如果对象嵌套很深,比如 Address 里还有 Province 对象,那每个层级都得手动克隆。代码写起来很累,也容易漏掉某个字段。

方式二:序列化实现深克隆

我个人更推荐这种方式。利用 Java 的序列化机制,把对象写到流里再读回来,天然就是深克隆。

public class Order implements Serializable {
    private String orderId;
    private Address address;

    public Order deepClone() {
        try {
            // 写入字节数组流
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);

            // 从字节数组流读取
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (Order) ois.readObject();
        } catch (Exception e) {
            throw new RuntimeException("深克隆失败", e);
        }
    }
}

提示:使用序列化方式,所有涉及的类都必须实现 Serializable 接口。如果某个字段不想被克隆,可以用 transient 关键字修饰。

序列化方式的优点是省心,不管对象嵌套多深,一次搞定。缺点是性能比手动克隆差一些,毕竟涉及 IO 操作。不过对于大多数业务场景,这点性能差异可以忽略不计。

原型模式在 Spring Bean 中的作用域

说到原型模式,就不得不提 Spring 框架。Spring 里的 Bean 作用域,其实就用了原型模式的思想。

作用域 说明 与原型模式的关系
singleton 单例,整个容器只有一个实例 不涉及原型模式
prototype 每次获取都创建新实例 本质就是原型模式的应用
request 每个 HTTP 请求一个实例 类似原型模式,但由容器管理生命周期
session 每个 HTTP 会话一个实例 同上

Spring 的 prototype 作用域,每次调用 getBean() 时,容器都会创建一个新的 Bean 实例。这个创建过程,其实就是原型模式的应用——容器内部维护了一个「原型 Bean」的定义,每次根据这个定义克隆出新对象。

我记得有一次做微服务项目,有个配置类需要每次调用都生成新实例,因为里面包含了请求上下文的信息。我一开始用了 singleton,结果发现所有请求共享同一个实例,数据全乱套了。改成 prototype 后,问题迎刃而解。

核心要点:

  • 浅克隆:复制引用,不复制对象。适合对象内部只有基本类型或不可变对象的情况。
  • 深克隆:复制整个对象树。适合对象内部有可变引用类型的情况。
  • 序列化深克隆:通用性强,但要求所有类实现 Serializable。
  • Spring prototype:本质是原型模式,每次获取都创建新实例。

嗯,这里要注意一点:原型模式不是万能的。如果对象的创建成本很低,直接用 new 反而更清晰。原型模式的价值在于「复制一个已经初始化好的对象」,省去重复的初始化过程。

你想想看,如果一个对象需要从数据库加载 10 个关联数据才能初始化完成,每次 new 都要查一遍数据库,那性能得多差?这时候用原型模式,克隆一个已经加载好的对象,改改关键字段就能用,效率提升不是一点半点。

原型模式知识体系 原型模式 核心概念 Cloneable 接口(标记接口) Object.clone() 方法 克隆类型 浅克隆:复制引用 深克隆:复制整个对象树 实现方式 手动重写 clone() 序列化深克隆 Spring Bean 作用域:prototype

最后说一句,原型模式在实际项目中用得不算特别频繁,但一旦用对地方,效果立竿见影。我建议你在设计阶段就考虑好:哪些对象可能需要被复制?复制的时候是浅克隆就够了,还是必须深克隆?提前想清楚,后面能省不少事。

避坑指南:

  • 不要忘记实现 Cloneable 接口,否则会抛 CloneNotSupportedException
  • 浅克隆后修改引用类型字段,会影响原对象
  • 序列化深克隆要小心 transient 字段会被忽略
  • Spring prototype 作用域下,容器不负责 Bean 的销毁
公众号:蓝海资料掘金营,微信deep3321