clone()方法的坑:浅拷贝在嵌套对象上踩的雷
clone()方法的坑:浅拷贝在嵌套对象上踩的雷
适读人群:Java初中级开发者、有过对象拷贝困惑的后端工程师 | 阅读时长:约13分钟 | 文章类型:原理剖析+实战方案
开篇故事
有一次做促销活动,需要根据基础模板创建多个活动配置。新人小陈写了这样的代码:
PromotionConfig template = new PromotionConfig();
template.setDiscount(0.9);
template.setProductList(new ArrayList<>(Arrays.asList("P001", "P002")));
// 基于模板创建不同渠道的活动配置
PromotionConfig channelA = (PromotionConfig) template.clone();
channelA.getProductList().add("P003"); // channelA专属商品
PromotionConfig channelB = (PromotionConfig) template.clone();
channelB.getProductList().add("P004"); // channelB专属商品
// 检查模板
System.out.println("模板商品: " + template.getProductList());他预期模板里只有P001和P002。但实际输出:
模板商品: [P001, P002, P003, P004]模板的商品列表也被修改了。他以为clone()创建了一个完全独立的副本,但实际上clone()做的是浅拷贝,嵌套的List对象仍然是共享的。
一、浅拷贝和深拷贝的区别
浅拷贝(Shallow Copy)
创建一个新对象,并把原对象的字段值复制到新对象:
- 基本类型字段:直接复制值,互相独立
- 引用类型字段:复制引用(地址),两个对象的这个字段指向同一个对象
所以浅拷贝后,两个对象的引用字段指向同一个堆上的对象——修改其中一个,另一个也会变。
深拷贝(Deep Copy)
创建一个新对象,同时递归地复制所有引用类型字段:
- 基本类型字段:复制值
- 引用类型字段:创建新的对象并复制内容,两个对象完全独立
深拷贝后,修改一个对象的任何字段,不会影响另一个。
二、核心原理深挖
Object.clone()的实现
Object.clone()是一个native方法,默认行为是:
- 分配一块和原对象相同大小的内存
- 把原对象的所有字节按位复制过去
- 返回新对象的引用
这是浅拷贝。基本类型字段直接复制值是对的(独立的),但引用类型字段复制的是引用地址,两个对象共享同一个引用指向的对象。
使用clone()的前提条件
要让一个类支持clone(),需要:
- 实现
Cloneable接口(标记接口,没有方法) - 重写
clone()方法(并改为public,因为Object.clone()是protected)
如果只调用了Object.clone()但没有实现Cloneable,会抛:
java.lang.CloneNotSupportedException: com.example.SomeClass
at java.base/java.lang.Object.clone(Native Method)
at com.example.SomeClass.clone(SomeClass.java:15)浅拷贝的内存模型
三、完整代码实现
代码一:浅拷贝和深拷贝的对比
package com.laozhang.trap.clone;
import java.util.ArrayList;
import java.util.List;
/**
* 浅拷贝vs深拷贝完整对比
*/
public class ShallowVsDeepCopy {
// ===== 浅拷贝版本 =====
static class ShallowConfig implements Cloneable {
double discount;
List<String> productList; // 引用类型,浅拷贝时共享
ShallowConfig(double discount, List<String> products) {
this.discount = discount;
this.productList = products;
}
@Override
public ShallowConfig clone() {
try {
return (ShallowConfig) super.clone(); // 只做浅拷贝
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return "Config{discount=" + discount + ", products=" + productList + "}";
}
}
// ===== 深拷贝版本 =====
static class DeepConfig implements Cloneable {
double discount;
List<String> productList;
DeepConfig(double discount, List<String> products) {
this.discount = discount;
this.productList = new ArrayList<>(products); // 构造时就复制
}
@Override
public DeepConfig clone() {
try {
DeepConfig copy = (DeepConfig) super.clone(); // 先浅拷贝
// 手动深拷贝引用类型字段
copy.productList = new ArrayList<>(this.productList); // 创建新List
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
@Override
public String toString() {
return "Config{discount=" + discount + ", products=" + productList + "}";
}
}
public static void main(String[] args) {
System.out.println("=== 浅拷贝陷阱 ===");
ShallowConfig original = new ShallowConfig(0.9,
new ArrayList<>(List.of("P001", "P002")));
ShallowConfig clone1 = original.clone();
clone1.productList.add("P003"); // 修改clone的list
System.out.println("原对象: " + original); // P001, P002, P003 ← 被改了!
System.out.println("克隆: " + clone1);
// 修改基本类型字段,互相独立
clone1.discount = 0.8;
System.out.println("改discount后原对象: " + original.discount); // 0.9,没变
System.out.println("\n=== 深拷贝正确行为 ===");
DeepConfig original2 = new DeepConfig(0.9,
new ArrayList<>(List.of("P001", "P002")));
DeepConfig clone2 = original2.clone();
clone2.productList.add("P003"); // 修改clone的list
System.out.println("原对象: " + original2); // P001, P002 ← 没变!
System.out.println("克隆: " + clone2); // P001, P002, P003
}
}代码二:多层嵌套的深拷贝与替代方案
package com.laozhang.trap.clone;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
/**
* 深拷贝的多种实现方式
* 1. 逐层手动clone()
* 2. 序列化/反序列化
* 3. 构造函数复制(推荐)
*/
public class DeepCopyStrategies {
static class Address implements Cloneable, Serializable {
String city;
String street;
Address(String city, String street) {
this.city = city;
this.street = street;
}
// 方式1:实现clone做深拷贝
@Override
public Address clone() {
try {
return (Address) super.clone(); // Address只有String字段,String不可变,浅拷贝即可
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// 方式3:复制构造函数
Address(Address other) {
this.city = other.city;
this.street = other.street;
}
@Override
public String toString() {
return city + " " + street;
}
}
static class Person implements Cloneable, Serializable {
String name;
Address address; // 嵌套对象
List<String> hobbies; // 集合字段
Person(String name, Address address, List<String> hobbies) {
this.name = name;
this.address = address;
this.hobbies = hobbies;
}
// ===== 方式1:手动clone,逐层深拷贝 =====
@Override
public Person clone() {
try {
Person copy = (Person) super.clone();
// 手动深拷贝每个引用字段
copy.address = this.address.clone(); // address也实现了clone
copy.hobbies = new ArrayList<>(this.hobbies); // 创建新ArrayList
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// ===== 方式2:序列化深拷贝(简单但有性能开销)=====
@SuppressWarnings("unchecked")
public Person deepCopyViaSerialization() {
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos)) {
oos.writeObject(this);
oos.flush();
try (ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis)) {
return (Person) ois.readObject();
}
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException("序列化深拷贝失败", e);
}
}
// ===== 方式3:复制构造函数(最推荐,清晰明确)=====
Person(Person other) {
this.name = other.name; // String不可变,直接复制引用即可
this.address = new Address(other.address); // 用复制构造函数
this.hobbies = new ArrayList<>(other.hobbies); // 浅复制List(String不可变)
}
@Override
public String toString() {
return "Person{name='" + name + "', address=" + address + ", hobbies=" + hobbies + "}";
}
}
public static void main(String[] args) throws Exception {
List<String> hobbies = new ArrayList<>(List.of("读书", "旅行"));
Person original = new Person("张三",
new Address("北京", "朝阳区"),
hobbies);
System.out.println("=== 方式1:手动clone深拷贝 ===");
Person clone1 = original.clone();
clone1.address.city = "上海"; // 修改克隆的address
clone1.hobbies.add("音乐"); // 修改克隆的hobbies
System.out.println("原对象: " + original); // 北京,读书旅行(未受影响)
System.out.println("克隆1: " + clone1);
System.out.println("\n=== 方式2:序列化深拷贝 ===");
Person clone2 = original.deepCopyViaSerialization();
clone2.address.street = "浦东新区";
System.out.println("原对象: " + original); // 朝阳区(未受影响)
System.out.println("克隆2: " + clone2);
System.out.println("\n=== 方式3:复制构造函数(推荐)===");
Person clone3 = new Person(original);
clone3.name = "李四";
clone3.address.city = "广州";
System.out.println("原对象: " + original); // 张三,北京(未受影响)
System.out.println("克隆3: " + clone3);
}
}四、踩坑实录
坑1:clone()浅拷贝嵌套List,修改影响原对象
这就是开篇的案例,详见上面代码的演示。
解法要点:
@Override
public MyClass clone() {
try {
MyClass copy = (MyClass) super.clone();
copy.myList = new ArrayList<>(this.myList); // 创建新List
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}坑2:忘了实现Cloneable,调用clone()抛CloneNotSupportedException
报错现象:
java.lang.CloneNotSupportedException: com.example.domain.Config
at java.base/java.lang.Object.clone(Native Method)
at com.example.domain.Config.clone(Config.java:25)根本原因:
类没有实现Cloneable接口(标记接口)。Object.clone()会检查这个标记,没有就抛异常。
具体解法:
class Config implements Cloneable { // 实现Cloneable
@Override
public Config clone() throws CloneNotSupportedException {
return (Config) super.clone();
}
}坑3:多层嵌套,只克隆了第一层,漏了内层对象
报错现象:
修改了克隆对象的深层嵌套字段,原对象也跟着变了。
触发代码:
// 三层结构:Order → OrderItem → Product
@Override
public Order clone() {
try {
Order copy = (Order) super.clone();
// 只拷贝了items列表(创建了新List),但List里的OrderItem还是共享的
copy.items = new ArrayList<>(this.items); // 浅拷贝List元素!
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}
// 这样写,修改clone.items.get(0).getProduct()仍然影响原对象具体解法:
需要递归深拷贝每一层:
@Override
public Order clone() {
try {
Order copy = (Order) super.clone();
// 深拷贝List:同时克隆每个元素
copy.items = this.items.stream()
.map(item -> item.clone()) // 每个OrderItem也需要clone
.collect(Collectors.toList());
return copy;
} catch (CloneNotSupportedException e) {
throw new RuntimeException(e);
}
}或者用复制构造函数,更清晰:
Order(Order other) {
this.id = other.id;
this.items = other.items.stream()
.map(item -> new OrderItem(item)) // 每个元素用复制构造函数
.collect(Collectors.toList());
}五、总结与延伸
关于clone(),我的实际建议是:在大多数业务场景中,不要用clone()接口,用复制构造函数代替。
理由:
- clone()是浅拷贝,容易踩嵌套对象的坑
- Cloneable接口标记了但没方法,设计奇怪
- clone()返回Object,需要强转
- 复制构造函数更显式、更可控,代码更清晰
// clone()
Config copy = (Config) original.clone();
// 复制构造函数(更推荐)
Config copy = new Config(original);三种深拷贝方案的适用场景:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 手动clone() | 性能好 | 每层都要写,容易遗漏 | 性能敏感,层次浅 |
| 序列化 | 简单,递归自动处理 | 性能差,需要Serializable | 快速原型,深层嵌套 |
| 复制构造函数 | 清晰,可控 | 需要手动编写 | 推荐,日常业务 |
还有一个方案没提到:JSON序列化/反序列化(Jackson的objectMapper.readValue(objectMapper.writeValueAsString(obj), SomeClass.class))。优点是简单,缺点是性能更差,而且会丢失无法JSON序列化的字段。只在没有选择的情况下用。
