原型模式中的浅拷贝和深拷贝有什么区别?
简化版
浅拷贝只复制对象本身,引用类型字段仍然指向同一个对象;深拷贝不仅复制对象本身,也复制内部可变引用对象。原型模式中如果对象包含可变字段,必须认真判断是否需要深拷贝,否则副本和原型会互相影响。
详细版
区别可以这样看:
| 类型 | 对象本身 | 引用字段指向 |
|---|---|---|
| 浅拷贝 | 创建新对象 | 仍共享原来的引用对象 |
| 深拷贝 | 创建新对象 | 引用字段也创建新对象 |
例子:
class User {
String name;
List<String> roles;
}
浅拷贝后:
user1 != user2
user1.roles == user2.roles
深拷贝后:
user1 != user2
user1.roles != user2.roles
如果 roles 会被修改,应该深拷贝;如果引用对象是不可变的,可以安全共享。
完整版教学
一、浅拷贝复制到哪一层
浅拷贝只复制第一层对象。
class Profile implements Cloneable {
String name;
List<String> tags;
protected Profile clone() throws CloneNotSupportedException {
return (Profile) super.clone();
}
}
super.clone() 会创建新的 Profile 对象,但 tags 仍然指向原来的列表。
结果是:
Profile p1 = new Profile();
Profile p2 = p1.clone();
p1 != p2; // true
p1.tags == p2.tags; // true
这就是浅拷贝。
二、浅拷贝的风险
如果副本修改列表:
p2.tags.add("new");
原型对象的 tags 也会变化,因为它们共享同一个列表。
这会导致非常隐蔽的 bug:看起来复制出了两个对象,实际内部状态仍然连在一起。
三、深拷贝复制可变引用
深拷贝要为可变引用重新创建对象:
class Profile {
String name;
List<String> tags;
Profile copy() {
Profile copy = new Profile();
copy.name = this.name;
copy.tags = new ArrayList<>(this.tags);
return copy;
}
}
此时:
p1 != p2
p1.tags != p2.tags
副本修改 tags 不会影响原型。
四、深拷贝不是所有字段都复制得越深越好
深拷贝会带来成本。
如果对象图很大,完全深拷贝可能很慢,也会占用更多内存。
而有些对象本来就可以共享:
String;Integer等包装类型;LocalDateTime等不可变值对象;- 只读配置对象;
- 枚举。
这些对象通常没必要重新复制。
五、判断深浅拷贝的标准
可以用一个问题判断:
副本修改这个字段时,是否允许影响原型或其他副本?
如果不允许,就要深拷贝或使用不可变对象。
如果允许共享,浅拷贝即可。
例如:
- 用户权限列表会被修改:深拷贝;
- 全局只读字典配置:可以共享;
- 图片二进制大对象只读:可以共享;
- 临时状态列表:通常深拷贝。
六、常见误区与追问
深浅拷贝应通过引用图而不是术语判断。源对象 A 引用列表 L,浅拷贝得到 B 后满足 A != B 但 A.list == B.list;深拷贝则通常要求 A.list != B.list。若 L 内含2个可变元素,还要继续判断元素引用是否独立,不能只检查集合外壳。
| 检查维度 | 判定依据 |
|---|---|
| 浅拷贝 | 顶层新对象,嵌套引用可能共享 |
| 深拷贝 | 约定范围内的可变子图独立 |
易错点:判断标准是“修改副本会不会影响原对象”,不是复制了几层。
- 误区:浅拷贝就是两个变量指向同一对象。 那只是引用赋值;浅拷贝至少会创建新的顶层对象。
- 追问:字符串字段需要深拷贝吗? Java String 不可变,安全共享即可。
- 误区:深拷贝要求所有引用地址都不同。 不可变对象和有意共享的全局值可以保持同一引用。
- 追问:数组 clone 属于哪种拷贝? 基本类型数组复制值;对象数组只复制元素引用,因此对元素而言是浅拷贝。
- 追问:如何写最小测试? 修改副本的嵌套可变字段,断言原对象对应字段未变化。
七、加强记忆
浅拷贝复制对象外壳,引用字段仍共享;深拷贝连内部可变对象也复制。原型模式最重要的判断是可变引用是否能共享,能共享就浅拷贝,不能共享就深拷贝或改成不可变对象。