Java 23 种设计模式:从踩坑到精通 | 原型模式 —— 克隆对象,深拷贝与浅拷贝的坑你踩过吗?

Java 23 种设计模式:从踩坑到精通 | 原型模式 ------ 克隆对象,深拷贝与浅拷贝的坑你踩过吗?

摘要 :当对象的创建成本高昂(如需要复杂的数据库查询或大量计算),或者需要保留对象某一时刻的快照时,直接 new 可能既低效又繁琐。原型模式通过"克隆"已有对象来创建新实例,将复制逻辑封装在原型类中。本文从复制粘贴的需求出发,深入剖析 Java 中的 Cloneable 接口、浅拷贝与深拷贝的本质区别,以及序列化、Spring Bean 作用域中克隆的应用,帮你彻底掌握拷贝的"深浅"之道。
📖 《Java 23 种设计模式:从踩坑到精通》

开篇:系列介绍与目录 | 上一篇:建造者模式 | 当前:原型模式 | 下一篇:适配器模式

🔗 返回系列总目录


1. 从一个"复制"需求说起

假设你正在开发一个报告系统,需要生成大量格式相同、但具体数据略有差异的报表对象。每个报表对象创建时都需要读取模板、查询数据库、计算样式等,成本很高。如果每次都从头 new,性能会极差。

我们自然会想:"能不能直接复制一个已有对象,再修改差异部分?" 这种需求在图形编辑器(复制图形)、游戏开发(复制敌人)中也非常常见。然而,直接用 = 赋值只是复制了引用,修改副本会污染原对象;如果简单粗暴地 new 一个再手动赋值,又可能遗漏某些深层字段。

原型模式(Prototype Pattern)正是为了解决这类问题:通过拷贝原型实例来创建新对象,而无需重新执行高昂的初始化过程。

1.1 你的场景该不该用原型?

判断标准 是 → 用原型 否 → 用 new / 工厂
对象创建成本极高(如 IO、网络、复杂计算)
需要保留对象某一时刻的快照(撤销/重做)
对象间差异很小,仅少量属性需要修改
对象构造简单,且无重复创建需求

2. 模式定义与 UML 结构

原型模式 用原型实例指定创建对象的种类,并通过拷贝这些原型创建新的对象。它属于 创建型设计模式

标准原型模式的 UML 类图:

两个核心角色

  • 抽象原型(Prototype) :声明克隆自身的接口,通常是 Cloneable 或自定义接口;
  • 具体原型(ConcretePrototype):实现克隆方法,返回自身的副本。

💡 核心理解 :原型模式让对象自己负责"如何复制自己",外界只需调用 clone(),无需了解内部结构。


3. Java 中的克隆基础:浅拷贝 vs 深拷贝

Java 提供了 Object.clone() 方法和 Cloneable 接口来实现原型模式,但默认的 clone()浅拷贝

  • 浅拷贝 :复制对象时,只复制基本类型的值和引用类型的引用地址。原对象和副本共享同一个引用对象,修改副本的引用字段会影响原对象。
  • 深拷贝:不仅复制对象本身,还递归复制所有引用的对象,生成完全独立的副本。

3.1 浅拷贝示例(踩坑现场)

java 复制代码
public class Report implements Cloneable {
    private String title;
    private List<String> data;  // 引用类型
    public Report(String title, List<String> data) {
        this.title = title;
        this.data = data;
    }
    @Override
    public Report clone() {
        try {
            return (Report) super.clone();  // 默认浅拷贝
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
    // getters/setters...
}

测试

java 复制代码
List<String> data = new ArrayList<>();
data.add("原始数据");
Report original = new Report("日报", data);
Report copy = original.clone();

copy.getData().add("新增数据");
System.out.println(original.getData()); // [原始数据, 新增数据]  → 原对象被污染!

⚠️ 踩坑了 :浅拷贝导致原对象和副本共用一个 data 列表,修改副本会影响原对象。

3.2 深拷贝解决方案

方案一:手动递归复制

java 复制代码
@Override
public Report clone() {
    try {
        Report cloned = (Report) super.clone();
        cloned.data = new ArrayList<>(this.data);  // 复制集合
        return cloned;
    } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e);
    }
}

✅ 简单直接,但需要为每个引用字段手动编写复制代码。

方案二:序列化深拷贝(适用于对象层次深的场景)

java 复制代码
public Report 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 (Report) ois.readObject();
    } catch (IOException | ClassNotFoundException e) {
        throw new RuntimeException(e);
    }
}

✅ 通用性强,但要求 Report 及其所有引用类实现 Serializable 接口,且序列化开销较大。


4. 代码实现:图形编辑器中的原型(含原型管理器)

4.1 抽象原型

java 复制代码
public interface Shape extends Cloneable {
    Shape clone();
    void draw();
}

4.2 具体原型:矩形

java 复制代码
public class Rectangle implements Shape {
    private int width;
    private int height;
    private Color color;  // 假设 Color 是自定义的复杂对象,需实现 Cloneable

    public Rectangle(int width, int height, Color color) {
        this.width = width;
        this.height = height;
        this.color = color;
    }

    @Override
    public Rectangle clone() {
        try {
            Rectangle cloned = (Rectangle) super.clone();
            cloned.color = this.color.clone(); // 深拷贝 Color 对象
            return cloned;
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public void draw() {
        System.out.println("绘制矩形[宽:" + width + ",高:" + height + ",颜色:" + color + "]");
    }
}

4.3 原型管理器(缓存常用原型)

java 复制代码
public class ShapeCache {
    private static Map<String, Shape> cache = new HashMap<>();

    public static void loadCache() {
        Rectangle rect = new Rectangle(100, 50, new Color("红色"));
        cache.put("red_rect", rect);
    }

    public static Shape getShape(String id) {
        return cache.get(id).clone();  // 返回克隆副本,不污染原型
    }
}

4.4 客户端

java 复制代码
ShapeCache.loadCache();
Shape shape1 = ShapeCache.getShape("red_rect");
Shape shape2 = ShapeCache.getShape("red_rect");
shape1.draw();
shape2.draw();
// shape1 和 shape2 是独立对象,修改互不影响

✅ 通过原型管理器,客户端只需从缓存取原型并克隆,完全屏蔽了构造细节。


5. 原型模式 vs 工厂模式 vs 建造者模式

对比维度 原型模式 工厂模式 建造者模式
核心作用 通过拷贝已有实例创建新对象 封装对象创建的类型选择 分步骤构建复杂对象
创建方式 clone() 内存复制 new 出具体产品 一步步设置参数后 build()
适用场景 创建成本高、需要保留对象状态 产品种类多,需要统一管理创建逻辑 参数多且可选,构建过程复杂
性能考量 避免重新初始化,高效 普通 new,无特殊优势 与工厂类似

💡 原型模式的性能优势只有在构造过程非常昂贵时才凸显 ,简单对象直接 new 更直接。


6. 优缺点一览

优点 缺点
性能高:直接内存复制,避免耗时的初始化过程 深拷贝实现复杂:每个关联对象都要支持克隆,有循环引用时更麻烦
简化创建过程:客户端无需知道具体类名 必须实现 Cloneable 接口,有一定侵入性
动态增加产品:运行时通过注册新原型扩展系统 clone() 方法使用不当可能引入浅拷贝 Bug
可保存状态:创建对象快照,支持撤销操作

7. 框架与实践中的应用

7.1 JDK:Object.clone() 与 Cloneable

所有 Java 类都继承 clone(),但需实现 Cloneable 并重写为 public 才能正常使用。

7.2 Spring Bean 作用域:Prototype

当 Bean 作用域设为 prototype 时,每次 getBean() 都会创建一个新实例。若结合原型管理器,可将模板对象缓存,克隆出多个实例。

7.3 MyBatis 对象映射

MyBatis 查询后将数据库记录映射为 Java 对象,底层大量使用 Bean 拷贝(如 CGLIB 或反射),体现了原型思想。


8. 常见误区与面试高频题

❌ 误区1:clone() 默认就是深拷贝

Object.clone() 是浅拷贝,必须手动实现深拷贝。

❌ 误区2:原型模式一定比 new 快

只有在构造过程非常昂贵(如涉及 IO、网络、大量计算)时才有优势,简单对象反而因为克隆机制有额外开销。

💡 面试高频追问

  • 深拷贝和浅拷贝的区别? → 浅拷贝共享引用,深拷贝完全独立。
  • 如何实现深拷贝? → 手动递归复制、序列化/反序列化、或使用 Cloneable 层层克隆。
  • Spring 的 prototype 作用域和原型模式的关系? → 思想上类似,都是创建多实例;但 Spring 是通过 getBean() 动态创建,不一定是克隆。
  • 原型模式如何破坏单例? → 如果单例类实现 Cloneable 并重写 clone(),可能通过克隆生成第二个实例。

🎉 恭喜:如果你能立刻说出浅拷贝和深拷贝的区别,并知道如何通过序列化实现深拷贝,你已经超越了 80% 的初级 Java 开发者。面试中问到"如何安全地复制一个复杂对象",你的答案就是原型模式。


9. 六大设计原则在原型模式中的体现

设计原则 体现
单一职责(SRP) 原型类自己负责拷贝逻辑,职责内聚
开闭原则(OCP) 可以通过注册新原型扩展系统,无需修改客户端代码
里氏替换(LSP) 客户端依赖抽象原型,具体原型克隆后仍可替换
依赖倒置(DIP) 客户端依赖抽象 Shape,不关心具体类
接口隔离(ISP) 原型接口仅包含 clone(),精简
迪米特法则(LoD) 客户端只与原型管理器交互,不知内部结构

🧭 《Java 23 种设计模式:从踩坑到精通》快速导航

🔔 关注《Java 23 种设计模式:从踩坑到精通》,用 25 篇文章彻底吃透设计模式。

📦 福利预告 :全系列代码及 UML 源码将在完结时统一打包开放,点击「关注」「收藏」第一时间获取。

🚀 下一篇:适配器模式 ------ 让不兼容的接口也能一起工作!🚧 即将发布,敬请关注!
📌 除了设计模式,我也在深挖智能物流实战 (WMS、托盘调度、机器学习落地)。欢迎点击头像,看看专栏 《出版社物流WMS智能调度实战》。技术相通,思路可鉴。

复制代码
相关推荐
装不满的克莱因瓶1 小时前
基于 OpenResty 扩展开发实现动态服务注册与发现能力
java·开发语言·架构·openresty
程序员小羊!1 小时前
06Java 异常机制与常用类
java
caimouse1 小时前
Reactos 第 4 章 对象管理 — 4.3 句柄和句柄表(Handle & Handle Table)
c语言·windows·架构
weixin_523185322 小时前
Java基础知识总结(四):引用数据类型与参数传递机制
java·开发语言·python
故渊at2 小时前
第二板块:Android 四大组件标准化学理 | 第六篇:四大组件架构总论与 Manifest 规范
android·架构·zygote·manifest·四大组件
李燚2 小时前
erlang_migrate 架构拆解:behaviour 驱动的多数据库迁移引擎
数据库·postgresql·架构·erlang·migrate·behaviour·erlang_migrate
宸津-代码粉碎机2 小时前
Spring AI企业级实战|从RAG优化到Agent多工具调度
java·大数据·人工智能·后端·python·spring
噢,我明白了2 小时前
QueryWrapper的使用
java
Chase_______2 小时前
【Java基础 | 15】集合框架(中):Set、HashSet、TreeSet 与哈希表
java·windows·散列表