5. 原型模式(Prototype):浅克隆与深克隆、Cloneable接口、序列化实现深克隆,以及原型管理器与性能优化

原型模式,说白了就是「复制粘贴」的设计模式。你想想看,有时候创建一个对象特别费劲——要查数据库、要算一堆参数、要组装复杂结构。这时候如果有个现成的对象,直接复制一份改改,是不是省事多了?

我个人习惯把原型模式理解为「对象的复印机」。Java里提供了Cloneable接口,C++里也有拷贝构造函数。但这里有个坑——浅克隆和深克隆的区别,很多人栽在这上面。

5.1 浅克隆 vs 深克隆:一个血泪教训

先说说浅克隆。它只复制基本类型字段,引用类型字段只复制引用地址。什么意思呢?就是两个对象共享同一个子对象。

我曾经在一个订单系统里用过浅克隆。当时有个订单模板对象,里面包含一个商品列表。我克隆了一份用来生成新订单,结果修改新订单的商品时,模板的商品也跟着变了。嗯,那天的线上事故,至今记忆犹新。

核心区别:
  • 浅克隆:复制对象本身,内部引用对象共享
  • 深克隆:复制对象及其所有引用对象,完全独立

5.2 Java中的Cloneable接口

Java里实现原型模式,需要做两件事:实现Cloneable接口,重写clone()方法。注意,Cloneable是个标记接口,不实现它的话调用clone()会抛异常。

// 浅克隆示例
public class Order implements Cloneable {
    private String orderId;
    private List<String> items;  // 引用类型
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();  // 默认浅克隆
    }
}

// 使用
Order template = new Order("T001", Arrays.asList("商品A", "商品B"));
Order cloned = (Order) template.clone();
// 注意:cloned.items 和 template.items 是同一个对象!

为什么会这样?因为Object的clone()方法默认做的是逐字段复制。对于基本类型没问题,但对于引用类型,它只复制了引用地址。

避坑指南:我曾经在团队里看到有人用浅克隆复制了包含Date字段的对象。修改克隆对象的日期后,原对象日期也跟着变了。排查了半天才发现是浅克隆的问题。所以,只要对象里有可变引用类型,就要考虑深克隆。

5.3 深克隆的两种实现方式

深克隆有两种主流做法:手动深克隆和序列化深克隆。我一般根据场景选择。

方式一:手动深克隆

@Override
protected Object clone() throws CloneNotSupportedException {
    Order cloned = (Order) super.clone();
    // 手动复制引用对象
    cloned.items = new ArrayList<>(this.items);
    return cloned;
}

这种方式性能好,但需要你清楚每个引用字段的结构。如果对象嵌套很深,写起来很痛苦。

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

我个人比较喜欢这种方式,尤其是对象结构复杂的时候。原理很简单:把对象写到流里,再读回来。这样所有引用对象都会被完整复制。

// 需要实现Serializable接口
public class Order implements Serializable {
    private String orderId;
    private List<String> items;
    
    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);
        }
    }
}
我的经验:序列化深克隆虽然方便,但性能开销大。如果对象很复杂且需要频繁克隆,建议用手动方式。我一般在配置类对象或模板对象上使用序列化深克隆,因为这些对象创建频率低,但结构复杂。

5.4 原型管理器与性能优化

原型管理器,说白了就是个对象缓存池。把常用的原型对象注册进去,需要的时候直接克隆,不用每次都从头创建。

public class PrototypeManager {
    private static Map<String, Prototype> prototypes = new ConcurrentHashMap<>();
    
    public static void register(String key, Prototype prototype) {
        prototypes.put(key, prototype);
    }
    
    public static Prototype getClone(String key) {
        Prototype prototype = prototypes.get(key);
        return prototype != null ? prototype.clone() : null;
    }
}

// 使用
PrototypeManager.register("orderTemplate", new Order("模板", ...));
Order order = (Order) PrototypeManager.getClone("orderTemplate");

性能优化方面,有几点值得注意:

  • 缓存原型对象:避免重复创建开销大的对象
  • 懒加载:原型对象可以按需创建并注册
  • 对象池:对于频繁克隆的场景,可以复用克隆后的对象
  • 避免循环引用:序列化深克隆时,循环引用会导致栈溢出
性能对比:
方式 性能 适用场景
浅克隆 极高 对象只有基本类型字段
手动深克隆 对象结构简单且明确
序列化深克隆 对象结构复杂,克隆频率低
原型模式核心知识体系 原型模式 克隆方式 实现方式 应用场景 浅克隆 深克隆 Cloneable接口 序列化 原型管理器 性能优化 核心思想:通过复制已有对象来创建新对象,避免重复初始化开销 适用场景:对象创建成本高、对象结构相似、需要避免构造器复杂逻辑 注意事项:深克隆性能开销、循环引用问题、序列化版本号管理

最后说个实际案例。我之前做一个报表系统,每个报表模板包含几十个配置项、数据源、样式定义。如果每次都从数据库加载再组装,响应时间要2秒多。后来用原型模式,把常用模板注册到管理器里,克隆一个只要几毫秒。嗯,性能提升就是这么明显。

我的建议:原型模式不是银弹。如果对象创建本身就很轻量,直接用new就好。只有当你发现「创建对象比复制对象贵得多」时,才值得引入原型模式。另外,记得处理好深克隆,否则bug会藏得很深。
公众号:蓝海资料掘金营,微信deep3321