在Java开发中,对象拷贝是高频场景------无论是业务数据复用、集合操作,还是多线程环境下的资源隔离,都离不开拷贝操作。但很多开发者容易混淆浅拷贝 和深拷贝,导致出现"修改拷贝对象,原对象莫名变化"的bug,甚至引发线上故障。
今天这篇博客,将从"概念本质→可视化示意图→完整代码实例→误区拆解"四个层面,彻底讲透Java的浅拷贝与深拷贝,结合实战案例演示每种拷贝方式的用法、优缺点,以及最容易踩的坑(比如clone方法的"伪深拷贝"),新手能快速入门,老手能查漏补缺,干货无冗余。
先抛出核心结论(必记): - 浅拷贝:拷贝的是原对象的地址引用 ,原对象与拷贝对象共享底层数据,一方修改,另一方随之变化; - 深拷贝:拷贝的是原对象的所有数据(值拷贝),原对象与拷贝对象完全独立,一方修改,另一方不受任何影响。
一、浅拷贝:"共享底层,牵一发而动全身"
浅拷贝的核心逻辑的是"拷贝引用,不拷贝数据本身"。就像两个人共用一把钥匙,无论谁打开房门、修改房间里的东西,另一个人看到的都是修改后的结果------原对象和拷贝对象指向的是同一块内存地址,本质上还是同一个对象的"别名"。
浅拷贝示意图(通俗理解)
「原对象」→ 内存地址A(存储数据:username=张三,password=123456) 「拷贝对象」→ 直接指向内存地址A(未创建新内存,仅拷贝原对象的地址引用) 当拷贝对象修改username为"李四",本质是修改内存地址A中的数据,原对象读取的也是地址A的数据,因此会同步变化。
实战演示:浅拷贝的基本用法与现象
我们通过简单的Java代码,直观感受浅拷贝的特性------定义User类,通过"直接赋值"的方式实现浅拷贝(Java中直接赋值就是最基础的浅拷贝)。
步骤1:定义User实体类
java
/**
* 实体类:用于演示拷贝操作
* 包含用户名、密码两个基本类型属性
*/
public class User {
// 基本类型属性(默认值初始化)
private String username = "张三";
private String password = "123456";
// getter/setter方法(用于读取和修改属性值)
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
步骤2:编写测试类,实现浅拷贝并验证
java
/**
* 拷贝测试类
* 演示浅拷贝的特性:原对象与拷贝对象相互影响
*/
public class CopyTest {
public static void main(String[] args) {
// 1. 创建原对象user01
User user01 = new User();
// 2. 浅拷贝:直接赋值(拷贝地址引用)
User user02 = user01;
// 3. 修改拷贝对象user02的username
user02.setUsername("李四");
// 打印原对象和拷贝对象的username
System.out.println("修改user02的username后:");
System.out.println("user01的username:" + user01.getUsername());
System.out.println("user02的username:" + user02.getUsername());
System.out.println("-----------------------------------");
// 4. 修改原对象user01的password
user01.setPassword("654321");
// 打印原对象和拷贝对象的password
System.out.println("修改user01的password后:");
System.out.println("user01的password:" + user01.getPassword());
System.out.println("user02的password:" + user02.getPassword());
}
}
运行结果与分析
java
修改user02的username后:
user01的username:李四
user02的username:李四
-----------------------------------
修改user01的password后:
user01的password:654321
user02的password:654321
结果很明显: 1. 修改拷贝对象user02的username,原对象user01的username同步变成"李四"; 2. 修改原对象user01的password,拷贝对象user02的password同步变成"654321"。
核心原因:user01和user02指向同一块内存地址,两者是"绑定关系",无论修改哪一个,本质都是修改同一块内存中的数据,自然会相互影响。
注意:浅拷贝并非毫无用处,当我们仅需要"复用对象引用",不需要修改数据时,浅拷贝是高效的(无需创建新内存,开销极小);但如果需要"数据隔离",浅拷贝就会引发bug。
二、深拷贝:"完全独立,各自安好"
深拷贝的核心逻辑是"拷贝数据本身,不拷贝地址引用"。就像两个人各自拥有一把独立的钥匙,各自的房间完全独立,无论谁修改自己房间里的东西,都不会影响到另一方------原对象和拷贝对象指向的是不同的内存地址,是两个完全独立的对象,只是初始数据相同。
深拷贝示意图(通俗理解)
「原对象」→ 内存地址A(存储数据:username=张三,password=123456) 「拷贝对象」→ 新建内存地址B(复制地址A中的所有数据,初始值与原对象一致) 当拷贝对象修改username为"李四",仅修改内存地址B中的数据,原对象读取的还是地址A的数据,因此不受影响;反之亦然。
常见的5种深拷贝方式(实战必备)
Java中实现深拷贝的方式有多种,各自有优缺点,我们结合实例逐一讲解,重点关注"用法+适用场景",同时拆解最容易踩的坑。
① 构造函数方式(简单但不推荐)
核心思路:通过new关键字创建新对象,在构造函数中传入原对象的所有属性值,实现"值拷贝"。
java
// 改造User类,增加带参构造函数
public class User {
private String username = "张三";
private String password = "123456";
// 带参构造函数:传入原对象,拷贝所有属性值
public User(User user) {
this.username = user.username;
this.password = user.password;
}
// 无参构造函数(保留默认)
public User() {}
// getter/setter方法(省略,与前文一致)
}
测试代码:
java
public static void main(String[] args) {
// 原对象
User user01 = new User();
// 深拷贝:通过带参构造函数创建新对象
User user02 = new User(user01);
// 修改拷贝对象的属性
user02.setUsername("李四");
user02.setPassword("654321");
// 打印对比
System.out.println("user01的username:" + user01.getUsername()); // 张三
System.out.println("user02的username:" + user02.getUsername()); // 李四
System.out.println("user01的password:" + user01.getPassword()); // 123456
System.out.println("user02的password:" + user02.getPassword()); // 654321
}
优点:实现简单,无需依赖任何工具类,适合简单实体类(属性少); 缺点:当对象属性较多、层级较深(比如包含引用类型属性、嵌套对象)时,需要手动在构造函数中拷贝所有属性,代码冗余且易出错;同时创建对象过多时,会增加系统内存开销,因此不推荐用于复杂场景。
② 重写clone()方法(重点,但易踩坑)
Java中所有类都默认继承自Object类,Object类中有一个native方法clone(),该方法的默认实现是浅拷贝;我们可以通过"重写clone()方法+实现Cloneable接口",将其改造为深拷贝------但这里有一个极易踩的坑,下文会重点拆解。
步骤1:实现Cloneable接口,重写clone()方法
Cloneable接口是一个"标记接口"(空接口,没有任何抽象方法),其作用是"告诉JVM:此类允许进行拷贝操作";如果不实现该接口,调用clone()方法会抛出CloneNotSupportedException异常。
java
/**
* 重写clone()方法实现深拷贝(初步改造)
* 1. 实现Cloneable标记接口
* 2. 重写Object类的clone()方法,修改访问权限为public
* 3. 将返回值强转为当前类类型
*/
public class User implements Cloneable {
private String username = "张三";
private String password = "123456";
// 重写clone()方法
@Override
public User clone() throws CloneNotSupportedException {
// 调用父类的clone()方法,强转为User类型
return (User) super.clone();
}
// getter/setter方法(省略,与前文一致)
}
步骤2:测试clone()方法的拷贝效果
java
public static void main(String[] args) {
try {
// 原对象
User user01 = new User();
// 深拷贝:调用重写后的clone()方法
User user02 = user01.clone();
// 打印两个对象的内存地址(验证是否是不同对象)
System.out.println("user01的内存地址:" + user01);
System.out.println("user02的内存地址:" + user02);
// 修改拷贝对象的属性,验证是否影响原对象
user02.setUsername("李四");
System.out.println("-----------------------------------");
System.out.println("user01的username:" + user01.getUsername()); // 张三
System.out.println("user02的username:" + user02.getUsername()); // 李四
} catch (CloneNotSupportedException e) {
// 捕获未实现Cloneable接口的异常
e.printStackTrace();
}
}
运行结果与分析
java
user01的内存地址:kaobei.User@15db9742
user02的内存地址:kaobei.User@6d06d69c
-----------------------------------
user01的username:张三
user02的username:李四
结果符合预期: 1. 两个对象的内存地址不同(@后面的哈希值不同),说明clone()方法创建了新的对象,实现了"值拷贝"; 2. 修改拷贝对象的username,原对象的username未变化,说明两者完全独立,初步实现了深拷贝。
重点坑点:clone()方法的"伪深拷贝"(必看)
上面的案例中,User类的属性都是基本类型(String本质是常量,也视为基本类型处理) ,因此重写clone()方法能实现深拷贝;但如果User类中包含引用类型属性(比如自定义对象),单纯重写clone()方法,就会变成"伪深拷贝"------仅拷贝引用类型的地址,不拷贝引用对象本身。
实战演示:伪深拷贝的现象
步骤1:新增引用类型实体类Person
java
/**
* 引用类型实体类:用于演示clone()方法的坑
*/
public class Person {
private String name;
// getter/setter方法
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
步骤2:修改User类,增加Person类型的引用属性
java
public class User implements Cloneable {
private String username = "张三";
private String password = "123456";
// 新增引用类型属性:Person对象
private Person person = new Person();
// 新增方法:修改Person对象的name属性
public void changePersonName(String name) {
this.person.setName(name);
}
// 新增方法:获取Person对象的name属性
public String getPersonName() {
return this.person.getName();
}
// 重写的clone()方法(未修改)
@Override
public User clone() throws CloneNotSupportedException {
return (User) super.clone();
}
// getter/setter方法(省略)
}
步骤3:测试clone()方法对引用类型的拷贝效果
java
public static void main(String[] args) {
try {
User user01 = new User();
User user02 = user01.clone();
// 打印两个User对象的内存地址(确认是不同对象)
System.out.println("user01的内存地址:" + user01);
System.out.println("user02的内存地址:" + user02);
// 修改原对象user01中的Person对象的name
user01.changePersonName("李四");
// 打印两个User对象中Person的name
System.out.println("-----------------------------------");
System.out.println("user01的Person.name:" + user01.getPersonName());
System.out.println("user02的Person.name:" + user02.getPersonName());
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
}
运行结果与关键结论
java
user01的内存地址:kaobei.User@15db9742
user02的内存地址:kaobei.User@6d06d69c
-----------------------------------
user01的Person.name:李四
user02的Person.name:李四
惊人的现象:两个User对象是独立的(内存地址不同),但它们内部的Person对象却相互影响------修改user01的Person.name,user02的Person.name也同步变化!
核心结论(必记):重写clone()方法的默认实现,只能对当前类的"基本类型属性"进行深拷贝,无法对"引用类型属性"进行深拷贝。也就是说,clone()方法会拷贝引用类型的地址,让原对象和拷贝对象的引用属性指向同一块内存地址,本质上还是浅拷贝(伪深拷贝)。
解决方案:要实现真正的深拷贝,需要在clone()方法中,手动对引用类型属性也进行clone()拷贝(递归clone),下文会补充完整实现。
③ Apache Commons Lang序列化(推荐,简洁高效)
Apache Commons Lang是Apache提供的工具类库,其中的SerializationUtils类提供了serialize()(序列化)和deserialize()(反序列化)方法,能快速实现深拷贝------核心原理是"将原对象序列化到字节流,再从字节流反序列化为新对象",反序列化会创建全新的对象,自然实现深拷贝。
使用步骤
- 导入依赖(Maven):
XML
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
- 让实体类(包括引用类型)实现Serializable接口(序列化标记接口,与Cloneable类似,无抽象方法):
java
// User类实现Serializable
public class User implements Serializable {
private static final long serialVersionUID = 1L; // 序列化版本号,避免反序列化异常
private String username = "张三";
private String password = "123456";
private Person person = new Person(); // 引用类型,也需实现Serializable
// getter/setter、方法省略
}
// Person类实现Serializable
public class Person implements Serializable {
private static final long serialVersionUID = 1L;
private String name;
// getter/setter省略
}
- 调用工具类方法实现深拷贝:
java
import org.apache.commons.lang3.SerializationUtils;
public static void main(String[] args) {
User user01 = new User();
// 深拷贝:序列化+反序列化
User user02 = SerializationUtils.clone(user01);
// 修改原对象的引用属性
user01.changePersonName("李四");
// 验证:拷贝对象不受影响
System.out.println("user01的Person.name:" + user01.getPersonName()); // 李四
System.out.println("user02的Person.name:" + user02.getPersonName()); // null(初始值)
}
优点:无需手动重写clone()方法,无需递归处理引用类型,一行代码实现深拷贝,支持任意层级的嵌套对象,简洁高效; 缺点:需要导入第三方依赖(非JDK原生),且实体类及其所有引用类型都需实现Serializable接口,略繁琐。
④ Gson序列化(推荐,无依赖限制)
Gson是Google提供的JSON解析工具类,也能通过"对象→JSON字符串→新对象"的方式实现深拷贝------核心原理是"将原对象转为JSON字符串(脱离内存引用),再将JSON字符串转为新对象",完全独立于原对象。
使用步骤
- 导入依赖(Maven):
XML
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.10.1</version>
</dependency>
- 无需实现任何接口,直接调用方法:
java
import com.google.gson.Gson;
public static void main(String[] args) {
Gson gson = new Gson();
User user01 = new User();
// 深拷贝:对象→JSON→新对象
String json = gson.toJson(user01); // 原对象转为JSON字符串
User user02 = gson.fromJson(json, User.class); // JSON字符串转为新对象
// 修改原对象的属性(包括引用类型)
user01.setUsername("李四");
user01.changePersonName("王五");
// 验证:拷贝对象完全独立
System.out.println("user01的username:" + user01.getUsername()); // 李四
System.out.println("user02的username:" + user02.getUsername()); // 张三
System.out.println("user01的Person.name:" + user01.getPersonName()); // 王五
System.out.println("user02的Person.name:" + user02.getPersonName()); // null
}
优点:无需实现Serializable或Cloneable接口,代码简洁,支持复杂嵌套对象,且Gson是开发中常用的JSON工具,无需额外引入多余依赖; 缺点:序列化/反序列化过程会有一定的性能开销(比clone()方法慢),适合中小规模数据的拷贝场景。
⑤ Jackson序列化(推荐,Spring项目首选)
Jackson是Spring框架默认的JSON解析工具,用法与Gson类似,也是通过"对象→JSON→新对象"实现深拷贝,适合Spring项目(无需额外导入依赖,Spring已集成)。
使用步骤
- 导入依赖(Maven,Spring项目可省略):
XML
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
- 调用Jackson工具类实现深拷贝:
java
import com.fasterxml.jackson.databind.ObjectMapper;
public static void main(String[] args) throws Exception {
ObjectMapper objectMapper = new ObjectMapper();
User user01 = new User();
// 深拷贝:对象→JSON字节流→新对象
byte[] bytes = objectMapper.writeValueAsBytes(user01);
User user02 = objectMapper.readValue(bytes, User.class);
// 修改原对象,验证拷贝对象独立性
user01.setPassword("654321");
user01.changePersonName("赵六");
System.out.println("user01的password:" + user01.getPassword()); // 654321
System.out.println("user02的password:" + user02.getPassword()); // 123456
System.out.println("user01的Person.name:" + user01.getPersonName()); // 赵六
System.out.println("user02的Person.name:" + user02.getPersonName()); // null
}
优点:Spring项目原生支持,无需额外导入依赖,性能优于Gson,支持复杂对象和泛型,适合企业级开发; 缺点:需要处理IO异常(writeValueAsBytes和readValue方法会抛出异常),代码略繁琐。
三、深拷贝方式对比与选型建议(实战必备)
为了方便大家在实际开发中快速选型,我们整理了5种深拷贝方式的优缺点和适用场景,一目了然:
| 拷贝方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 构造函数 | 实现简单,无依赖 | 代码冗余,易出错,开销大 | 简单实体类(属性少),临时使用 |
| 重写clone()方法 | JDK原生,性能高 | 需实现Cloneable,引用类型需递归clone,易踩坑 | 性能要求高,对象层级简单 |
| Apache Commons Lang | 简洁高效,支持复杂对象 | 需导入第三方依赖,需实现Serializable | 普通Java项目,复杂对象拷贝 |
| Gson序列化 | 无接口要求,代码简洁,常用依赖 | 性能一般,需导入Gson依赖 | 已有Gson依赖,中小规模数据拷贝 |
| Jackson序列化 | Spring原生支持,性能优,支持复杂对象 | 需处理异常,代码略繁琐 | Spring Boot/Cloud项目,企业级开发 |
四、全文核心总结(面试高频考点)
-
浅拷贝拷贝地址,深拷贝拷贝数据;浅拷贝共享底层资源,深拷贝完全独立,这是两者的核心区别;
-
Java中直接赋值是浅拷贝,new关键字+构造函数、clone()、序列化等方式可实现深拷贝;
-
重写clone()方法易踩"伪深拷贝"坑------引用类型需递归clone,否则仅拷贝引用地址;
-
实际开发中,优先选择Gson/Jackson序列化(Spring项目选Jackson)或Apache Commons Lang,兼顾简洁性和实用性;
-
无需数据隔离时,浅拷贝更高效;需要数据隔离(如多线程、数据修改)时,必须使用深拷贝,避免出现数据安全问题。
最后提醒:拷贝操作的核心是"数据隔离",选择哪种方式,关键看"对象复杂度、性能要求、项目依赖"------理解了浅拷贝和深拷贝的本质,无论遇到哪种场景,都能快速选型、避免踩坑。