同事被深拷贝坑了3小时,我教他原型模式的正确打开方式

修改克隆对象,原对象居然也跟着变了?这个Bug可能正在你的代码里潜伏。

一、一个让人崩溃的Bug

上周同事小王哭丧着脸找我:"我明明只改了新对象的属性,为什么原对象也跟着变了?"

来看这段代码:

java 复制代码
Student student = new Student("张三", "zhangsan@qq.com", "北京");
student.setTeacher(new Teacher("李老师"));
​
// 克隆一个新对象
Student cloneStudent = student.clone();
​
// 只想修改克隆对象的班主任
cloneStudent.getTeacher().setName("王老师");
​
// 结果???
System.out.println(student.getTeacher().getName());  // 输出:王老师 ❌

小王崩溃了:"我明明改的是cloneStudent,为什么student的班主任也变成了王老师?"

这就是典型的浅拷贝陷阱。今天我们来彻底搞懂Java中的原型模式。


二、什么是原型模式?

一句话定义:原型模式是一种创建型设计模式,它允许你通过复制现有对象来创建新对象,而无需通过构造函数。

2.1 为什么需要原型模式?

想象一下这个场景:

scss 复制代码
// 不用原型模式,你需要这样创建对象
Student student = new Student();
student.setName(userInfo.getName());
student.setEmail(userInfo.getEmail());
student.setAddress(userInfo.getAddress());
student.setTeacher(findTeacherById(teacherId));
// ... 还有20个字段要复制

痛苦点

  • 字段太多,复制起来又臭又长
  • 有些字段是通过复杂查询获取的,复制一遍就是重复计算
  • 构造函数参数爆炸

原型模式的优雅解决方案

ini 复制代码
Student newStudent = oldStudent.clone();  // 一行搞定

2.2 原型模式的应用场景

场景 说明
对象创建成本高 需要查询数据库或复杂计算才能初始化
对象状态需要快照 保存对象某个时刻的状态,用于撤销操作
避免子类爆炸 用克隆代替复杂的继承体系
保护原始对象 对外提供对象副本,防止内部状态被篡改

三、浅拷贝 vs 深拷贝:本质区别

这是理解原型模式最关键的概念。

3.1 内存模型图解

bash 复制代码
浅拷贝(Shallow Copy)
━━━━━━━━━━━━━━━━━━━━━━
栈内存              堆内存
┌─────────┐        ┌──────────────┐
│ student │───────→│ Student对象  │
│  引用    │        │ name: "张三"  │
└─────────┘        │ email: "..." │
                   │ teacher ─────────┐
┌─────────┐        └──────────────┘  │
│ clone   │───────→│ Student对象  │  │
│  引用    │        │ name: "张三"  │  │
└─────────┘        │ email: "..." │  │
                   │ teacher ─────────┘
                   └──────────────┘
                          ↓
                   ┌──────────────┐
                   │ Teacher对象  │
                   │ name: "李老师"│
                   └──────────────┘
                   【两个Student共享同一个Teacher】
​
深拷贝(Deep Copy)
━━━━━━━━━━━━━━━━━━━━━━
栈内存              堆内存
┌─────────┐        ┌──────────────┐
│ student │───────→│ Student对象  │
│  引用    │        │ name: "张三"  │
└─────────┘        │ email: "..." │
                   │ teacher ────→┌──────────────┐
                   └──────────────┤│ Teacher对象  │
                                  ││ name: "李老师"│
┌─────────┐        ┌──────────────┐│ └──────────────┘
│ clone   │───────→│ Student对象  ││
│  引用    │        │ name: "张三"  ││
└─────────┘        │ email: "..." ││
                   │ teacher ────→┌┘─────────────┐
                   └──────────────┤ Teacher对象  │
                                  │ name: "李老师"│【全新的对象】
                                  └──────────────┘
                   【每个Student都有自己的Teacher】

3.2 核心区别

特性 浅拷贝 深拷贝
基本类型 复制值 复制值
引用类型 复制引用(共享对象) 复制对象本身(独立对象)
实现难度 简单(一行代码) 复杂(需要递归克隆)
内存占用
修改影响 修改引用类型会影响原对象 完全独立,互不影响

四、实现方式一:Cloneable接口(浅拷贝)

Java内置的原型模式支持, easiest way。

4.1 基础实现

typescript 复制代码
public class Student implements Cloneable {
    private String name;
    private String email;
    private String address;
    private Teacher teacher;  // 引用类型
​
    // 构造函数、getter、setter省略...
​
    @Override
    protected Student clone() {
        try {
            return (Student) super.clone();  // 调用Object的clone方法
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

4.2 浅拷贝的问题演示

ini 复制代码
public static void main(String[] args) {
    Student student = new Student("张三", "zhangsan@qq.com", "北京");
    student.setTeacher(new Teacher("李老师"));
​
    System.out.println("原始学生:" + student);
    // 输出:Student{name='张三', email='zhangsan@qq.com', address='北京', teacher=Teacher{name='李老师'}}
​
    Student cloneStudent = student.clone();
    System.out.println("克隆学生:" + cloneStudent);
    // 输出:Student{name='张三', email='zhangsan@qq.com', address='北京', teacher=Teacher{name='李老师'}}
​
    // 关键测试:修改克隆对象的teacher
    cloneStudent.getTeacher().setName("王老师");
​
    System.out.println("修改后原始学生:" + student);
    // 输出:Student{name='张三', email='zhangsan@qq.com', address='北京', teacher=Teacher{name='王老师'}}
    // ⚠️ 原始对象的teacher也被改了!
​
    System.out.println("原始学生和克隆学生是同一个对象吗?" + (student == cloneStudent));
    // 输出:false(不是同一个对象)
}

问题根源super.clone()只做了浅拷贝,teacher引用被复制了,但没有创建新的Teacher对象。


五、实现方式二:深拷贝的两种方案

方案1:递归调用clone(推荐)

让每个引用类型也实现Cloneable,然后在clone()方法中递归克隆。

typescript 复制代码
public class Teacher implements Cloneable {
    private String name;
​
    @Override
    protected Teacher clone() {
        try {
            return (Teacher) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}
​
public class Student implements Cloneable {
    private String name;
    private String email;
    private String address;
    private Teacher teacher;
​
    @Override
    protected Student clone() {
        try {
            Student clone = (Student) super.clone();
            // 关键:对引用类型也进行克隆
            if (this.teacher != null) {
                clone.setTeacher(this.teacher.clone());
            }
            return clone;
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
            return null;
        }
    }
}

优点

  • 性能高(直接内存复制)
  • 代码可控

缺点

  • 每个引用类型都要实现Cloneable
  • 嵌套层级深时代码繁琐
  • 循环引用会死循环

方案2:序列化实现(通用)

利用Java序列化机制,将对象写入流再读出,天然就是深拷贝。

java 复制代码
public class Student implements Cloneable, Serializable {
    private static final long serialVersionUID = 1L;
​
    private String name;
    private String email;
    private String address;
    private Teacher teacher;
​
    // 深拷贝:序列化方式
    @Override
    protected Student clone() {
        try {
            // 写入字节流
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(baos);
            oos.writeObject(this);
​
            // 读出对象
            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bais);
            return (Student) ois.readObject();
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}
​
// Teacher也要实现Serializable
public class Teacher implements Serializable {
    private static final long serialVersionUID = 1L;
    private String name;
}

优点

  • 简单粗暴,一行代码搞定所有层级
  • 无需递归处理引用类型
  • 适合复杂对象图

缺点

  • 性能较差(IO操作)
  • 所有类必须实现Serializable
  • 无法克隆transient字段

六、现代替代方案:BeanUtils vs MapStruct

在实际项目中,我们可能不需要手写clone()方法。现代Java生态提供了更优雅的方案。

6.1 Apache BeanUtils

scss 复制代码
// 添加依赖
// implementation 'commons-beanutils:commons-beanutils:1.9.4'

Student newStudent = new Student();
BeanUtils.copyProperties(newStudent, oldStudent);  // 浅拷贝

// 注意:BeanUtils是浅拷贝!

评价

  • 使用反射,性能一般
  • 是浅拷贝,有同样的陷阱
  • 字段名和类型必须匹配

6.2 MapStruct(推荐)

编译期生成映射代码,性能接近手写。

scss 复制代码
// 添加依赖
// implementation 'org.mapstruct:mapstruct:1.5.5.Final'

@Mapper
public interface StudentMapper {
    StudentMapper INSTANCE = Mappers.getMapper(StudentMapper.class);

    // 默认浅拷贝
    Student copy(Student student);

    // 深拷贝:手动映射引用类型
    default Student deepCopy(Student student) {
        if (student == null) return null;

        Student copy = new Student();
        copy.setName(student.getName());
        copy.setEmail(student.getEmail());
        copy.setAddress(student.getAddress());

        // 手动克隆teacher
        if (student.getTeacher() != null) {
            Teacher teacherCopy = new Teacher();
            teacherCopy.setName(student.getTeacher().getName());
            copy.setTeacher(teacherCopy);
        }

        return copy;
    }
}

// 使用
Student copy = StudentMapper.INSTANCE.deepCopy(original);

评价

  • 编译期生成代码,零运行时开销
  • 灵活可控,可以自定义深拷贝逻辑
  • 适合DTO/Entity转换场景

七、总结与最佳实践

7.1 如何选择拷贝方式?

scss 复制代码
                    是否需要深拷贝?
                          │
            ┌─────────────┴─────────────┐
            ↓                           ↓
           否                          是
            │                           │
    ┌───────┴───────┐           ┌───────┴───────┐
    ↓               ↓           ↓               ↓
 直接赋值      BeanUtils     递归clone      序列化
(=操作符)    /MapStruct    (推荐简单场景)  (复杂对象图)

7.2 避坑指南

坑点 解决方案
浅拷贝陷阱 修改克隆对象前,确认是深拷贝还是浅拷贝
循环引用 序列化方式会栈溢出,使用递归clone并标记已克隆对象
final字段 clone()方法无法给final字段赋值,改用构造函数
线程安全 克隆的对象如果是共享的,注意并发修改问题

7.3 实战建议

  1. 简单对象:直接用构造函数或Builder模式
  2. 同类型拷贝:MapStruct是最佳选择
  3. 需要快照:序列化方式最安全
  4. 性能敏感:手写递归clone

八、快速测试代码

csharp 复制代码
public class PrototypeTest {

    public static void main(String[] args) {
        // 创建原对象
        Student original = new Student("张三", "zhangsan@qq.com", "北京");
        original.setTeacher(new Teacher("李老师"));

        // 深拷贝
        Student cloned = original.clone();

        // 验证深拷贝
        cloned.setName("李四");
        cloned.getTeacher().setName("王老师");

        System.out.println("原对象:" + original);
        System.out.println("克隆对象:" + cloned);

        // 断言验证
        assert !original.getName().equals(cloned.getName()) :
            "基本类型应该独立";
        assert !original.getTeacher().getName().equals(cloned.getTeacher().getName()) :
            "引用类型应该独立";

        System.out.println("✅ 深拷贝验证通过!");
    }
}
相关推荐
NE_STOP1 小时前
MyBatis-缓存与注解式开发
java
码路飞2 小时前
不装 OpenClaw,我用 30 行 Python 搞了个 QQ AI 机器人
java
Re_zero2 小时前
以为用了 try-with-resources 就稳了?这三个底层漏洞让TCP双向通讯直接卡死
java·后端
SimonKing2 小时前
Fiddler抓包完全指南:从安装配置到抓包,一文讲透
java·后端·程序员
磊磊落落4 小时前
如何将 Spring Statemachine 作为一个轻量级工作流引擎来使用?
java
兆子龙17 小时前
ahooks useRequest 深度解析:一个 Hook 搞定所有请求
java·javascript
兆子龙17 小时前
React Suspense 从入门到实战:让异步加载更优雅
java·javascript
刀法如飞19 小时前
AI时代,程序员都应该是算法思想工程师
人工智能·设计模式·程序员