原型模式:浅克隆与深克隆、Cloneable接口、序列化实现深克隆

原型模式,说白了就是「复制粘贴」的设计模式。你手头有一个对象,想再搞一个一模一样的,但又不想走一遍构造函数的流程——这时候原型模式就派上用场了。

我记得刚入行那会儿,有个需求是要生成大量配置相似但又有细微差别的报表对象。一开始我傻乎乎地 new 了几百次,每次都要 set 一堆属性。后来同事看了一眼我的代码,说了句:「你试试 Cloneable?」——嗯,从此我就记住了原型模式。

什么是原型模式

原型模式的核心思想是:通过复制已有对象来创建新对象,而不是通过 new 关键字

它属于创建型模式,适用于以下场景:

  • 对象创建成本高(比如需要从数据库加载数据)
  • 对象之间只有少量差异
  • 构造函数复杂,或者构造函数有副作用

你想想看,如果一个对象的构造函数里调了远程接口、查了数据库、做了大量计算,每次 new 一次就全来一遍——这谁受得了?

Java 中的 Cloneable 接口

Java 原生提供了 Cloneable 接口和 clone() 方法。但这里有个坑:Cloneable 是个标记接口,里面啥方法都没有。真正干活的是 Object.clone()

使用方式很简单:

public class Report implements Cloneable {
    private String title;
    private List<String> dataRows;

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

调用的时候:

Report original = new Report();
Report copy = (Report) original.clone();

看起来挺方便对吧?但这里藏着一个大坑——浅克隆

注意:如果不重写 clone() 方法,直接调用 super.clone() 得到的是浅克隆。引用类型的成员变量不会被真正复制,而是共享同一个引用。

浅克隆 vs 深克隆

这两个概念,我当年面试的时候被问过不止一次。说白了:

  • 浅克隆:复制基本类型,引用类型只复制地址。你改我改,大家改。
  • 深克隆:引用类型也复制一份新的。你改你的,我改我的,互不干扰。

来看个例子就明白了:

public class Address {
    String city;
    String street;

    public Address(String city, String street) {
        this.city = city;
        this.street = street;
    }
}

public class Employee implements Cloneable {
    String name;
    int age;
    Address address;

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

这时候你克隆一个 Employee:

Employee e1 = new Employee("张三", 30, new Address("北京", "长安街"));
Employee e2 = (Employee) e1.clone();

e2.address.city = "上海"; // 改 e2 的地址
System.out.println(e1.address.city); // 输出:上海 —— 因为 e1 和 e2 共享同一个 Address 对象

为什么会这样?因为 super.clone() 只复制了 address 这个引用变量的值(也就是内存地址),并没有复制 Address 对象本身。

核心结论:浅克隆只复制一层,深克隆递归复制所有层级。

如何实现深克隆

实现深克隆,常见的有两种方式:

方式一:重写 clone() 方法,手动复制引用对象

@Override
protected Object clone() throws CloneNotSupportedException {
    Employee cloned = (Employee) super.clone();
    cloned.address = new Address(this.address.city, this.address.street);
    return cloned;
}

这种方式的问题很明显:如果对象嵌套很深,你得一层层手动复制。而且如果 Address 里面还有引用类型,你还得继续往下挖。代码写起来又臭又长。

方式二:序列化实现深克隆(我推荐的方式)

我个人习惯用序列化来做深克隆。原理很简单:把对象写到流里,再读回来。中间经过了一次完整的序列化和反序列化,所有引用类型都会被重新创建。

public class DeepCloneUtil {

    @SuppressWarnings("unchecked")
    public static <T extends Serializable> T deepClone(T obj) {
        try {
            // 写入字节数组流
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            oos.flush();

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

使用前提:所有涉及的类都必须实现 Serializable 接口。

public class Address implements Serializable {
    private static final long serialVersionUID = 1L;
    String city;
    String street;
}

public class Employee implements Serializable {
    private static final long serialVersionUID = 1L;
    String name;
    int age;
    Address address;
}

// 使用
Employee e1 = new Employee("张三", 30, new Address("北京", "长安街"));
Employee e2 = DeepCloneUtil.deepClone(e1);

e2.address.city = "上海";
System.out.println(e1.address.city); // 输出:北京 —— 互不影响
小技巧:序列化深克隆虽然方便,但性能比手动复制要差一些。如果对象层级不深,或者克隆频率不高,用序列化完全没问题。如果每秒要克隆几万次,那还是老老实实手动重写 clone() 吧。

我曾经踩过的坑

说个真实经历。有一次我在做一个订单系统的缓存模块,订单对象里有个 List<OrderItem> 的字段。我用 super.clone() 克隆了一份订单,然后修改了克隆订单的某个商品数量——结果原订单的商品数量也跟着变了。

排查了半天才发现是浅克隆的问题。那会儿我还没用序列化方案,只能手动去重写 clone(),把 List 里的每个 OrderItem 都 new 一遍。代码改完,测试通过,但心里总觉得别扭。

后来我养成了一个习惯:只要涉及克隆,先问自己一句——这里需要深克隆吗?

原型模式的知识体系

下面这张图帮你理清原型模式的核心脉络:

原型模式 核心机制 Cloneable 标记接口 Object.clone() 本地方法 克隆类型 浅克隆:引用共享 深克隆:完全独立 实现方式 手动重写 clone() 序列化深克隆 ⚠️ 注意:序列化需实现 Serializable 性能敏感场景建议手动重写 clone()

什么时候用原型模式

我总结了几条经验:

场景 推荐做法 原因
对象只有基本类型字段 浅克隆(默认 clone()) 简单高效,没有引用共享问题
对象有引用类型,但层级浅 手动重写 clone() 性能好,代码可控
对象嵌套深,或经常变化 序列化深克隆 通用性强,改字段不用改克隆代码
需要克隆的对象来自第三方库 序列化深克隆 无法修改源码,序列化是唯一选择
我的建议:如果项目里克隆的场景比较多,可以封装一个通用的深克隆工具类,就像上面那个 DeepCloneUtil。这样团队里所有人都用同一套方案,避免每个人各自实现、各自踩坑。

原型模式看起来简单,但浅克隆和深克隆的区别如果不搞清楚,线上出 bug 的时候排查起来真的很痛苦。尤其是那些引用类型嵌套了好几层的对象——你改了一个字段,结果影响了一大片,那场面,想想都头疼。

嗯,记住一句话:用 clone() 之前,先想清楚你要的是浅还是深。


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