原型模式

一个价值 50 万的复制粘贴 bug

2022年,我们团队接到了一个线上告警:订单系统的优惠券数据全部错乱。几百个用户收到了错误的优惠券金额。

排查了 4 个小时,发现根因是一位同学在复制优惠券模板时,直接用了这个操作:

// 他写的代码
List<Coupon> coupons = new ArrayList<>();
Coupon template = couponService.getTemplate();

for (int i = 0; i < 100; i++) {
    coupons.add(template);  // 直接添加同一个对象引用!
}

结果是 100 个"优惠券"其实都是同一个对象,修改任何一个,所有优惠券都会变。

这就是原型模式要解决的核心问题:如何正确地复制一个对象,而不是复制引用。


一、核心思想

原型模式:用原型实例指定创建对象的种类,并且通过复制原型来创建新的对象。

直接引用:    A --引用--> [对象]
原型复制:    A --复制--> [新对象](内容相同,但独立存在)

二、Cloneable 方案🔴

2.1 基础用法

Java 提供了一个 Cloneable 标记接口来实现原型模式:

public class Coupon implements Cloneable {
    private String code;
    private int amount;
    private Date expireTime;

    @Override
    protected Coupon clone() {
        try {
            return (Coupon) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new AssertionError(e);
        }
    }
}
Coupon template = new Coupon();
template.setCode("DISCOUNT50");
template.setAmount(50);

// 复制模板创建新优惠券
Coupon copy = template.clone();
copy.setCode("DISCOUNT50_COPY"); // 修改不影响原对象

2.2 浅拷贝的问题

Object.clone() 是浅拷贝——只复制对象的基本类型字段和引用字段的地址,不复制引用指向的对象。

Coupon template = new Coupon();
template.setCode("DISCOUNT50");
template.setAmount(50);
template.setExpireTime(new Date()); // 引用类型字段

Coupon copy = template.clone();
copy.setCode("DISCOUNT50_V2");
copy.setAmount(100);

// 基本类型:copy 的修改不影响 template ✅
System.out.println(template.getAmount()); // 50
System.out.println(copy.getAmount());     // 100

// 引用类型:指向同一个对象!❌
copy.getExpireTime().setTime(0); // 修改了 shared 的 Date 对象!
System.out.println(template.getExpireTime().getTime()); // 0(也被改了!)
浅拷贝内存模型:
template --> [Coupon] --> code="DISCOUNT50", amount=50, expireTime=-->[Date]

              copy ------> expireTime(和 template 指向同一个 Date)

【架构权衡】 浅拷贝适用于:对象结构简单、没有嵌套引用、或者嵌套对象不需要独立复制的场景。深拷贝适用于:对象结构复杂、嵌套对象需要独立修改、或者出于线程安全的考虑需要完全隔离的场景。


三、深拷贝方案🟡

3.1 方案一:递归手动拷贝

public class Coupon implements Cloneable {
    private String code;
    private int amount;
    private Date expireTime;

    @Override
    public Coupon clone() {
        Coupon copy = new Coupon();
        copy.code = this.code;           // String 不可变,直接赋值即可
        copy.amount = this.amount;       // 基本类型,直接复制
        copy.expireTime = new Date(this.expireTime.getTime()); // 复制新对象
        return copy;
    }
}

3.2 方案二:序列化拷贝

import java.io.*;

public class DeepCloneUtil {

    public static <T extends Serializable> T deepClone(T obj) {
        try {
            // 写入字节流
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(obj);
            oos.close();

            // 读出字节流
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            @SuppressWarnings("unchecked")
            T clone = (T) ois.readObject();
            ois.close();

            return clone;
        } catch (Exception e) {
            throw new RuntimeException("Deep clone failed", e);
        }
    }
}
Coupon template = new Coupon();
template.setExpireTime(new Date());

// 一行代码完成深拷贝
Coupon copy = DeepCloneUtil.deepClone(template);
⚠️

序列化拷贝有两个严重问题:

  1. 性能开销大:需要序列化所有字段,复杂对象可能很慢
  2. 需要实现 Serializable:每个嵌套对象都要实现
  3. transient 字段会被跳过:需要深度拷贝的字段不能加 transient

对于性能敏感的场景(如高频交易、游戏服务器),序列化拷贝是不可接受的。

3.3 方案三:JSON 序列化拷贝

import com.fasterxml.jackson.databind.ObjectMapper;

public class JsonCloneUtil {
    private static final ObjectMapper mapper = new ObjectMapper();

    public static <T> T deepClone(T obj, Class<T> clazz) {
        try {
            String json = mapper.writeValueAsString(obj);
            return mapper.readValue(json, clazz);
        } catch (Exception e) {
            throw new RuntimeException("JSON clone failed", e);
        }
    }
}

JSON 拷贝的好处:

  • 不需要类实现 Serializable
  • 对嵌套对象的拷贝是全自动的
  • 可以控制忽略哪些字段(Jackson 注解)

坏处:

  • 性能最慢
  • 精度问题(浮点数、日期格式)
  • 不能拷贝非 JSON 兼容的对象

3.4 深拷贝方案对比

维度手动递归序列化JSON
性能最快最慢
代码量多(每个类都要写)少(通用工具类)
依赖SerializableJackson/Gson
精度保留✅ 完美⚠️ 日期精度问题⚠️ 日期精度问题
循环引用✅ 支持⚠️ 需处理⚠️ 需处理
适用场景嵌套结构固定嵌套结构复杂临时方案、快速原型

四、原型管理器

当原型对象较多时,需要一个管理器来统一管理:

public class CouponPrototypeManager {
    private static final Map<String, Cloneable> prototypes = new HashMap<>();

    static {
        // 注册原型
        prototypes.put("DISCOUNT_50", createDiscountCoupon(50));
        prototypes.put("DISCOUNT_100", createDiscountCoupon(100));
        prototypes.put("CASH_200", createCashCoupon(200));
    }

    private static Coupon createDiscountCoupon(int amount) {
        Coupon c = new Coupon();
        c.setType("DISCOUNT");
        c.setAmount(amount);
        c.setExpireTime(new Date(System.currentTimeMillis() + 30L * 24 * 3600 * 1000));
        return c;
    }

    private static Coupon createCashCoupon(int amount) {
        Coupon c = new Coupon();
        c.setType("CASH");
        c.setAmount(amount);
        return c;
    }

    public static Cloneable getPrototype(String type) {
        Cloneable prototype = prototypes.get(type);
        if (prototype == null) {
            throw new IllegalArgumentException("Unknown prototype: " + type);
        }
        return prototype;
    }

    public static Cloneable create(String type) {
        return (Cloneable) ((Cloneable) prototypes.get(type)).clone();
    }
}

使用方式:

// 不需要知道具体类,只需要类型名
Coupon c1 = (Coupon) CouponPrototypeManager.create("DISCOUNT_50");
Coupon c2 = (Coupon) CouponPrototypeManager.create("DISCOUNT_50");

c1.setCode("CODE_001");
c2.setCode("CODE_002"); // 完全独立
System.out.println(c1.getCode()); // CODE_001
System.out.println(c2.getCode()); // CODE_002

五、生产应用场景

5.1 文档模板系统

// 文档模板
public class Document implements Cloneable {
    private String title;
    private List<Section> sections = new ArrayList<>();
    private Map<String, Object> metadata;

    @Override
    public Document clone() {
        Document copy = new Document();
        copy.title = this.title;
        copy.sections = new ArrayList<>();
        for (Section s : this.sections) {
            copy.sections.add(s.clone()); // 递归深拷贝
        }
        copy.metadata = new HashMap<>(this.metadata); // Map 也需要深拷贝
        return copy;
    }
}

// 使用
Document contractTemplate = loadTemplate("CONTRACT");
Document specificContract = contractTemplate.clone();
// 修改 specificContract 不会影响 contractTemplate

5.2 游戏中的怪物/角色复制

public class Monster implements Cloneable {
    private String name;
    private int hp;
    private int attack;
    private List<Skill> skills = new ArrayList<>();
    private MonsterStats baseStats;

    @Override
    public Monster clone() {
        Monster copy = new Monster();
        copy.name = this.name;
        copy.hp = this.hp;
        copy.attack = this.attack;
        copy.skills = new ArrayList<>();
        for (Skill s : this.skills) {
            copy.skills.add(s.clone());
        }
        copy.baseStats = this.baseStats.clone(); // 如果 Stats 也是 Cloneable
        return copy;
    }
}

// 刷怪时复制怪物模板
Monster spider = monsterRegistry.get("SPIDER");
for (int i = 0; i < 10; i++) {
    Monster spawned = spider.clone();
    spawned.setPosition(calculatePosition(i));
    gameWorld.spawn(spawned);
}

【面试官心理】 原型模式在面试中属于"看起来简单但细节很多"的类型。能写出浅拷贝/深拷贝区别的有 60%,能说出各种深拷贝方案优缺点的有 30%,能结合自己项目场景说清楚选型原因的只有 10%。原型模式和其他创建型模式的区别,也是高频追问。


六、与工厂模式的对比

维度工厂模式原型模式
创建方式通过 new 创建通过 clone 复制
性能每次 new 可能很重复制已有对象可能更快
复杂度需要工厂类需要 clone 方法
状态从零开始继承已有状态
适用场景对象简单对象复杂/创建成本高
💡

原型模式和工厂模式不是互斥的。可以把原型模式理解为"工厂模式的变体"——工厂模式通过类型创建对象,原型模式通过复制创建对象。在实际系统中,两者经常共存。


七、工程代价评估

维度Cloneable序列化JSON
开发成本
运行时开销最高
维护成本高(深拷贝需手动)
可读性
适用频率低频创建高频创建低频创建

八、面试总结

8.1 核心追问

  1. "浅拷贝和深拷贝的区别是什么?" —— 基本题,必须答对
  2. "Object.clone() 为什么是浅拷贝?" —— 要说出内存模型和引用复制的原理
  3. "如何实现深拷贝?" —— 至少说出两种方案及各自的优缺点
  4. "什么场景下会选择原型模式而不是工厂模式?" —— 区分度高的题

8.2 常见错误

// ❌ 错误:没有重写 clone 方法,直接返回 this
@Override
protected Object clone() {
    return this; // 这不是拷贝,是同一个对象!
}

// ✅ 正确:调用 super.clone()
@Override
protected Object clone() {
    try {
        return super.clone();
    } catch (CloneNotSupportedException e) {
        throw new AssertionError(e);
    }
}