深拷贝和浅拷贝的区别?
结论:浅拷贝(shallow copy)只复制对象本身和其中的基本类型字段,引用类型字段仍指向同一个对象;深拷贝(deep copy)还要递归复制引用指向的对象,得到的两份数据完全独立,改其中一个不影响另一个。判断标准很简单:拷贝之后修改副本里的引用字段,看原对象会不会跟着变。
1. 内存结构上的差别
原对象:u1 ──→ User{name="Tom", addr=0x11} ──→ Address{city="杭州"} ← 0x11
浅拷贝:u2 ──→ User{name="Tom", addr=0x11} ──→ 同一个 Address(共享)
深拷贝:u3 ──→ User{name="Tom", addr=0x22} ──→ Address{city="杭州"} ← 新对象
注:String、Integer 等不可变(immutable)对象即使被共享也安全
注意 String 这类字段:浅拷贝共享的是同一个 String 引用,但 String 不可变,任何"修改"都会产生新对象,所以共享不可变对象不构成浅拷贝风险。风险只出现在可变引用字段上:List、Map、数组、自定义 Bean。
2. 四种实现方式
| 方式 | 深浅 | 优点 | 缺点 |
|---|---|---|---|
Object.clone() 默认实现 | 浅 | 简单、性能好 | 必须实现 Cloneable,引用字段仍需手动处理 |
| 拷贝构造器 / 工厂方法 | 可控 | 意图清晰、可逐字段深拷 | 字段多时样板代码多 |
序列化往返(Serializable/JSON) | 深 | 递归自动完成 | 要求全链路可序列化,性能差,可能丢类型 |
| 不可变对象 + 共享 | 无需拷贝 | 零成本 | 需要设计成不可变 |
Object.clone() 的语义常被误解:它按字段做逐位复制(field-by-field),基本类型复制值、引用类型复制地址,所以默认是浅拷贝。数组的 clone() 也是浅拷贝(元素引用被复制)。
3. 代码:从浅到深
class Address implements Cloneable {
String city;
Address(String city) { this.city = city; }
@Override protected Address clone() {
try {
return (Address) super.clone(); // 字段只有 String,浅拷也安全
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
class User implements Cloneable {
String name;
Address addr;
User(String name, Address addr) {
this.name = name;
this.addr = addr;
}
/** 深拷贝:先 super.clone(),再手动递归引用字段 */
@Override public User clone() {
try {
User copy = (User) super.clone(); // 第 1 步:浅拷贝
copy.addr = addr.clone(); // 第 2 步:替换为独立副本
return copy;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
}
public class CopyDemo {
public static void main(String[] args) {
User u1 = new User("Tom", new Address("杭州"));
User u2 = u1.clone();
u2.addr.city = "上海";
System.out.println(u1.addr.city); // 杭州,深拷贝互不影响
}
}
如果省掉 copy.addr = addr.clone(),上面输出会变成"上海"——这就是浅拷贝的经典事故。
4. 序列化实现深拷贝
public static <T extends Serializable> T deepCopy(T obj) {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(obj);
try (ObjectInputStream ois =
new ObjectInputStream(new ByteArrayInputStream(bos.toByteArray()))) {
@SuppressWarnings("unchecked")
T copy = (T) ois.readObject();
return copy;
}
} catch (IOException | ClassNotFoundException e) {
throw new IllegalStateException("deep copy failed", e);
}
}
它省去了逐字段处理,代价是:对象图里所有类都要实现 Serializable、transient 字段不会被复制、性能比手写慢一个数量级,而且会绕过构造器。JSON 往返(Jackson objectMapper.readValue(writeValueAsBytes(obj), Type))本质相同,但更容易丢类型信息与日期格式。
5. 常见追问
| 追问 | 回答要点 |
|---|---|
BeanUtils.copyProperties 是深拷贝吗? | 不是,Spring 与 Apache 的实现都是浅拷贝;改副本的引用字段会影响原对象 |
List.clone() 是深拷贝吗? | 不是,只复制了元素引用;要深拷得逐个元素 clone 或序列化 |
| 深拷贝遇到循环引用怎么办? | 手写递归会栈溢出,需要 IdentityHashMap 记录已拷贝对象;序列化方式由 JVM 自己处理 |
| 不可变对象还需要深拷贝吗? | 不需要,直接共享;这也是为什么推荐把值对象设计成不可变 |
record 的拷贝构造器是深拷贝吗? | 不是,record 的拷贝构造器只是逐字段赋值,引用字段仍共享 |
| 什么时候用浅拷贝? | 引用字段不可变、或业务上明确允许共享时;浅拷贝性能最好 |
还有一个细节:Cloneable 是标记接口(marker interface),不实现它调用 clone() 会抛 CloneNotSupportedException;clone() 在 Object 中是 protected,子类需要提升可见性才能被外部调用。
小结
浅拷贝复制"壳",深拷贝复制"整棵树"。实现优先用拷贝构造器(清晰可控),需要递归复制时再考虑序列化;日常开发中更推荐用不可变对象从源头消灭拷贝需求。