Java拷贝精讲:彻底分清浅拷贝与深拷贝

在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()(反序列化)方法,能快速实现深拷贝------核心原理是"将原对象序列化到字节流,再从字节流反序列化为新对象",反序列化会创建全新的对象,自然实现深拷贝。

使用步骤
  1. 导入依赖(Maven):
XML 复制代码
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
  1. 让实体类(包括引用类型)实现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省略
}
  1. 调用工具类方法实现深拷贝:
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字符串转为新对象",完全独立于原对象。

使用步骤
  1. 导入依赖(Maven):
XML 复制代码
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.10.1</version>
</dependency>
  1. 无需实现任何接口,直接调用方法:
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已集成)。

使用步骤
  1. 导入依赖(Maven,Spring项目可省略):
XML 复制代码
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.15.2</version>
</dependency>
  1. 调用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项目,企业级开发

四、全文核心总结(面试高频考点)

  1. 浅拷贝拷贝地址,深拷贝拷贝数据;浅拷贝共享底层资源,深拷贝完全独立,这是两者的核心区别;

  2. Java中直接赋值是浅拷贝,new关键字+构造函数、clone()、序列化等方式可实现深拷贝;

  3. 重写clone()方法易踩"伪深拷贝"坑------引用类型需递归clone,否则仅拷贝引用地址;

  4. 实际开发中,优先选择Gson/Jackson序列化(Spring项目选Jackson)或Apache Commons Lang,兼顾简洁性和实用性;

  5. 无需数据隔离时,浅拷贝更高效;需要数据隔离(如多线程、数据修改)时,必须使用深拷贝,避免出现数据安全问题。

最后提醒:拷贝操作的核心是"数据隔离",选择哪种方式,关键看"对象复杂度、性能要求、项目依赖"------理解了浅拷贝和深拷贝的本质,无论遇到哪种场景,都能快速选型、避免踩坑。

相关推荐
七夜zippoe1 小时前
微服务架构下Spring Session与Redis分布式会话实战全解析
java·redis·maven·spring session·分布式会话
一晌小贪欢1 小时前
PyQt5 实战:批量图片添加水印工具(带右侧实时预览)(附代码及下载链接)
开发语言·qt·添加水印·图片添加水印·蹄片水印
超绝振刀怪2 小时前
【C++ vector】
开发语言·c++
少云清2 小时前
【UI自动化测试】3_PO模式 _封装思想
python·ui·po模式
机器视觉的发动机2 小时前
图像处理-机器视觉算法中的数学基础
开发语言·人工智能·算法·决策树·机器学习·视觉检测·机器视觉
guohahaya2 小时前
attention-2026
开发语言·c#
vx_Biye_Design3 小时前
【关注可免费领取源码】云计算及其应用网络教学系统--毕设附源码35183
java·spring·spring cloud·servlet·eclipse·云计算·课程设计
lntu_ling5 小时前
Python-基于Haversine公式计算两点距离
开发语言·python·gis算法
ShineWinsu10 小时前
对于C++:继承的解析—上
开发语言·数据结构·c++·算法·面试·笔试·继承