12. 原型模式:克隆对象、浅拷贝与深拷贝、实战:对象池优化

原型模式,说白了就是“复制粘贴”。

你想想看,有时候创建一个对象特别费劲——要查数据库、要算一堆参数、要组装复杂的内部结构。这时候如果有个现成的对象,直接克隆一份,改改关键字段,是不是省事多了?

我在项目中遇到过好几次这种场景。最典型的就是报表引擎,每次生成报表都要加载模板、初始化几十个配置项。后来我改用原型模式,把模板对象缓存起来,需要时直接clone,性能提升了将近10倍。

12.1 为什么需要原型模式

先看一个痛点。假设你有个订单对象:

public class Order {
    private String orderId;
    private List<OrderItem> items;
    private Address shippingAddress;
    private PaymentInfo paymentInfo;
    // 还有20个字段...
    
    public Order(String orderId) {
        // 这里要查数据库、调外部服务、做各种校验
        // 构造一个Order可能要花200ms
    }
}

每次new一个Order都要花200ms,如果同时要创建100个类似的订单,光构造时间就要20秒。用户早就摔键盘了。

原型模式就是来解决这个问题的。它通过克隆已有的对象来创建新对象,而不是通过new。Java里最直接的体现就是Cloneable接口和clone()方法。

核心思想:用已有的对象作为“原型”,通过拷贝来创建新对象。避免重复的初始化开销。

12.2 浅拷贝 vs 深拷贝

这里有个坑,我当年踩过。Java的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 Employee implements Cloneable {
    private String name;
    private Address address;  // 引用类型
    
    @Override
    protected Object clone() throws CloneNotSupportedException {
        Employee cloned = (Employee) super.clone();  // 浅拷贝
        // 注意:这里cloned.address和原对象的address是同一个对象!
        return cloned;
    }
}

为什么会这样?因为super.clone()只复制了基本类型和引用本身,不会递归复制引用指向的对象。

我曾经在项目中用浅拷贝克隆了一个用户对象,然后改了克隆对象的地址,结果原对象的地址也跟着变了。排查了半天才发现是浅拷贝的问题。嗯,从那以后我对深拷贝就特别敏感。

12.2.1 实现深拷贝的几种方式

方式 优点 缺点 适用场景
重写clone()手动深拷贝 性能好,可控性强 代码量大,容易遗漏 对象结构固定,字段少
序列化/反序列化 自动处理所有层级 性能差,需要实现Serializable 对象结构复杂,对性能不敏感
JSON序列化 跨语言,调试方便 性能中等,依赖第三方库 需要和前端交互的场景
BeanUtils.copyProperties 使用简单 只复制一层,本质是浅拷贝 DTO转换等简单场景

我个人习惯用序列化的方式来实现深拷贝。虽然性能差一点,但不容易出错。给你看个例子:

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);
            
            // 从字节流读取
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (T) ois.readObject();
        } catch (Exception e) {
            throw new RuntimeException("深拷贝失败", e);
        }
    }
}

小技巧:如果对象里有些字段不需要拷贝(比如缓存、连接池等),可以用transient关键字标记。序列化时会自动跳过这些字段。

12.3 实战:对象池优化

原型模式最经典的应用场景就是对象池。我做过一个数据库连接池的优化,当时用原型模式把连接创建时间从50ms降到了1ms以内。

对象池的核心逻辑是这样的:

  1. 预先创建一批原型对象
  2. 需要对象时,从池里克隆一个
  3. 用完归还,或者直接丢弃
  4. 池里对象不够时,自动扩容

来看一个简单的对象池实现:

public class ObjectPool<T extends Cloneable> {
    private final List<T> pool = new ArrayList<>();
    private final T prototype;
    private final int maxSize;
    
    public ObjectPool(T prototype, int initialSize, int maxSize) {
        this.prototype = prototype;
        this.maxSize = maxSize;
        // 预创建对象
        for (int i = 0; i < initialSize; i++) {
            pool.add(createClone());
        }
    }
    
    @SuppressWarnings("unchecked")
    private T createClone() {
        try {
            // 关键:通过克隆创建新对象
            Method cloneMethod = prototype.getClass().getMethod("clone");
            return (T) cloneMethod.invoke(prototype);
        } catch (Exception e) {
            throw new RuntimeException("克隆失败", e);
        }
    }
    
    public synchronized T borrow() {
        if (pool.isEmpty()) {
            if (pool.size() < maxSize) {
                return createClone();  // 动态扩容
            }
            throw new RuntimeException("池已满,无法创建新对象");
        }
        return pool.remove(pool.size() - 1);
    }
    
    public synchronized void return_(T obj) {
        if (pool.size() < maxSize) {
            pool.add(obj);  // 归还对象
        }
        // 如果池已满,直接丢弃(GC会处理)
    }
}

注意:对象池不是万能的。如果对象创建开销很小,或者对象状态变化频繁,用对象池反而会增加复杂度。我建议只在以下场景使用:

  • 对象创建成本高(数据库连接、线程、大对象)
  • 对象创建频率高
  • 对象可复用,状态可重置

12.4 原型模式的知识体系

下面这张图总结了原型模式的核心逻辑和实战要点:

原型模式知识体系 核心概念 Cloneable + clone() 浅拷贝 复制引用,不复制内容 深拷贝 递归复制所有层级 实战:对象池 预创建 + 克隆 + 归还 深拷贝实现方式对比 手动重写clone() 性能好,易遗漏 序列化/反序列化 自动处理,性能差 JSON序列化 跨语言,依赖库 BeanUtils.copy 简单,只复制一层

12.5 避坑指南

最后分享几个我踩过的坑:

  • 忘记实现Cloneable:调用clone()会抛CloneNotSupportedException。这是Java的“故意设计”,提醒你显式声明支持克隆。
  • 循环引用导致死循环:深拷贝时如果对象A引用B,B又引用A,序列化方式会抛StackOverflowError。我建议在业务层做引用检查。
  • 单例对象被克隆:如果原型对象里引用了单例,克隆后所有副本共享同一个单例实例。这通常不是你想要的效果,记得用transient排除。
  • 对象池里的对象状态污染:归还对象前一定要重置状态。我曾经漏了重置一个计数器字段,结果下一个使用者拿到的是脏数据。

我的建议:如果项目里用到了Spring,可以考虑用BeanUtils.copyProperties做浅拷贝,用SerializationUtils.clone(Apache Commons)做深拷贝。别自己造轮子,除非你有特殊需求。

原型模式说白了就是“复制粘贴”的工程化实现。它不复杂,但用好了能省很多事。下次遇到创建对象特别慢的场景,不妨想想:能不能克隆一个?

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