修改克隆对象,原对象居然也跟着变了?这个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 实战建议
- 简单对象:直接用构造函数或Builder模式
- 同类型拷贝:MapStruct是最佳选择
- 需要快照:序列化方式最安全
- 性能敏感:手写递归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("✅ 深拷贝验证通过!");
}
}