原型模式(Prototype):浅克隆与深克隆

原型模式,说白了就是「复制对象」。

我刚开始接触这个模式时,觉得它挺简单的——不就是拷贝一份数据嘛。但后来在项目中踩过几次坑,才发现「拷贝」这两个字背后,藏着不少门道。

什么时候需要用原型模式?

  • 创建对象成本很高(比如从数据库加载、网络请求)
  • 对象之间只有少量差异,不想从头 new 一个
  • 运行时需要动态生成对象,但类名不确定

Java 里提供了 Cloneable 接口,C++ 则靠拷贝构造函数。但两者都有个共同的问题:浅克隆 vs 深克隆。咱们一个一个聊。

浅克隆:复制了引用,没复制内容

先看 Java 的浅克隆。实现 Cloneable 接口,重写 clone() 方法,默认就是浅克隆。

public class Address implements Cloneable {
    private String city;
    // getter/setter 省略

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

public class Person implements Cloneable {
    private String name;
    private Address address;

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone(); // 浅克隆
    }
}

看起来没问题对吧?但实际跑起来你会发现:克隆出来的 Person 和原 Person 共享同一个 Address 对象。

关键点:浅克隆只复制了基本类型和引用地址。引用指向的对象,还是同一个。

我在项目中遇到过这样的场景:用户配置了一个订单模板,里面有收货地址。我浅克隆了一份让用户编辑,结果原模板的地址也被改了。嗯,那场面挺尴尬的。

深克隆:彻底复制,互不干扰

深克隆要怎么做?两种主流方式:

  1. 手动深克隆——在 clone() 方法里,把引用对象也 clone 一遍
  2. 序列化深克隆——把对象写到流里再读回来,天然就是深拷贝

先看手动方式:

@Override
protected Object clone() throws CloneNotSupportedException {
    Person cloned = (Person) super.clone();
    cloned.address = (Address) this.address.clone(); // 手动深克隆
    return cloned;
}

这种方式要求所有引用对象都实现 Cloneable。如果对象嵌套很深,写起来很痛苦。我一般只在层级简单时用。

序列化实现深克隆

我个人更推荐用序列化。Java 里把对象写到字节数组,再读回来,所有引用都会被重新创建。

public class DeepCloneUtil {
    @SuppressWarnings("unchecked")
    public static <T> T deepClone(T obj) {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);

            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (T) ois.readObject();
        } catch (Exception e) {
            throw new RuntimeException("深克隆失败", e);
        }
    }
}

注意:所有涉及的类必须实现 Serializable 接口。这是序列化的前提。

小技巧:如果某些字段不想被克隆(比如缓存、连接池),可以用 transient 关键字标记。序列化时会跳过它们。

C++ 拷贝构造:浅与深的抉择

C++ 没有 Cloneable 接口,但拷贝构造函数和赋值运算符是标配。默认的拷贝构造也是浅克隆。

class Address {
public:
    std::string city;
    // 默认拷贝构造就是浅克隆
};

class Person {
public:
    std::string name;
    Address* address; // 指针成员

    // 浅克隆拷贝构造
    Person(const Person& other)
        : name(other.name), address(other.address) {}

    // 深克隆拷贝构造
    Person(const Person& other)
        : name(other.name), address(new Address(*other.address)) {}
};

这里有个坑:如果类里有指针成员,浅克隆会导致两个对象指向同一块内存。析构时 double free,程序直接崩掉。

避坑指南:我曾经在 C++ 项目里用默认拷贝构造复制了一个带指针的类,结果程序跑着跑着就 segfault。查了两天才发现是浅克隆导致的重复释放。从那以后,我只要看到指针成员,第一反应就是「必须深克隆」。

C++ 里实现深克隆,通常用「拷贝并交换」惯用法(copy-and-swap),配合智能指针会更安全。

class Person {
    std::shared_ptr<Address> address;
public:
    Person(const Person& other)
        : name(other.name), address(std::make_shared<Address>(*other.address)) {}
};

shared_ptr 后,浅克隆其实也能接受——引用计数会帮你管理生命周期。但如果你需要独立修改地址内容,还是得深克隆。

原型模式的核心逻辑

我画了一张图,帮你理清原型模式的知识体系:

原型模式 Java 实现 Cloneable 接口 重写 clone() 方法 序列化深克隆 C++ 实现 拷贝构造函数 赋值运算符重载 智能指针 + 深拷贝 核心问题:浅克隆 vs 深克隆

什么时候选浅克隆,什么时候选深克隆?

场景 推荐方式 原因
对象只有基本类型 浅克隆 简单高效,没有共享风险
对象包含不可变引用(如 String) 浅克隆 不可变对象不怕共享
对象包含可变引用 深克隆 避免意外修改原对象
对象嵌套层级深 序列化深克隆 省去手动 clone 的繁琐
C++ 指针成员 深克隆或智能指针 防止 double free

我的习惯:除非我能 100% 确定引用对象不会被修改,否则一律用深克隆。多花几毫秒,省去几小时的调试时间,这笔账怎么算都划算。

原型模式看起来简单,但「浅 vs 深」这个选择题,几乎每个项目都会遇到。你想想看,如果团队里有人不小心用了浅克隆,改了一个对象的属性,结果影响了另一个对象——这种 bug 排查起来真的很头疼。

所以我的建议是:先想清楚你的对象结构,再决定克隆策略。如果拿不准,就用深克隆。安全第一。


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