当前位置: 首页 > ops >正文

浅拷贝,深拷贝

下面我们来详细讲解一下 Java 中的深拷贝和浅拷贝。在 Java 中,这两个概念主要出现在对象复制的过程中,理解它们对于编写健壮的代码至关重要。


一、概念回顾

在 Java 中,一切皆对象。变量分为两种:

  1. 基本数据类型intdoublecharboolean 等。它们存储的是“值”本身。
  2. 引用数据类型String, 数组,以及所有自定义的类(如 PersonArrayList 等)。它们存储的不是对象本身,而是指向堆内存中对象的“引用”(可以理解为内存地址)。

浅拷贝 和 深拷贝 的核心区别就在于,当复制一个包含引用类型成员的对象时,如何处理这些引用。


二、浅拷贝

1. 定义

浅拷贝会创建一个新对象,然后将原对象的字段值逐个复制到新对象中。

  • 如果字段是基本数据类型,复制的是它的值。修改副本不会影响原对象。
  • 如果字段是引用数据类型,复制的是它的引用(内存地址)。这意味着,原对象和副本对象将共享同一个引用指向的对象

2. 效果

对副本对象中引用类型成员的修改,会直接反映到原对象上,因为它们操作的是同一个内存中的对象。

3. Java 中的实现方式

a. Object.clone() 方法

Java 中所有类的父类 Object 提供了一个 protected native Object clone() 方法。要使用它,需要满足两个条件:

  1. 实现 Cloneable 接口:这是一个标记接口,没有任何方法,只是告诉 JVM 这个对象是可被克隆的。如果不实现,调用 clone() 会抛出 CloneNotSupportedException
  2. 重写 clone() 方法:将 Object 类中 protected 的 clone() 方法重写为 public,以便外部类可以调用。
b. 示例代码

让我们定义一个 Person 类,它包含一个基本类型 age 和一个引用类型 Address

// Address.java - 一个引用类型
class Address {String city;public Address(String city) {this.city = city;}@Overridepublic String toString() {return "Address{city='" + city + "'}";}
}// Person.java - 包含基本类型和引用类型
class Person implements Cloneable {private int age;private Address address; // 引用类型public Person(int age, Address address) {this.age = age;this.address = address;}// Getter 和 Setterpublic int getAge() {return age;}public void setAge(int age) {this.age = age;}public Address getAddress() {return address;}public void setAddress(Address address) {this.address = address;}@Overridepublic String toString() {return "Person{age=" + age + ", address=" + address + "}";}// 重写 clone() 方法实现浅拷贝@Overridepublic Person clone() {try {// 直接调用 Object 的 clone() 方法return (Person) super.clone();} catch (CloneNotSupportedException e) {// 因为实现了 Cloneable 接口,所以这个异常理论上不会发生throw new AssertionError();}}
}
c. 测试浅拷贝
public class ShallowCopyDemo {public static void main(String[] args) {// 1. 创建原始对象Address address = new Address("北京");Person person1 = new Person(30, address);// 2. 执行浅拷贝Person person2 = person1.clone();System.out.println("--- 修改前 ---");System.out.println("Person1: " + person1);System.out.println("Person2: " + person2);System.out.println("Person1.address == Person2.address ? " + (person1.getAddress() == person2.getAddress()));System.out.println("\n--- 修改副本 person2 的基本类型 age ---");person2.setAge(31);System.out.println("Person1: " + person1); // Person1 的 age 未改变System.out.println("Person2: " + person2);System.out.println("\n--- 修改副本 person2 的引用类型 address ---");// 注意:我们是通过 person2 拿到它内部的 address 对象,然后修改这个对象的 city 属性person2.getAddress().city = "上海";System.out.println("Person1: " + person1); // Person1 的 address.city 也被改变了!System.out.println("Person2: " + person2);}
}
d. 输出结果与分析
--- 修改前 ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=30, address=Address{city='北京'}}
Person1.address == Person2.address ? true--- 修改副本 person2 的基本类型 age ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=31, address=Address{city='北京'}}--- 修改副本 person2 的引用类型 address ---
Person1: Person{age=30, address=Address{city='上海'}}
Person2: Person{age=31, address=Address{city='上海'}}

分析

  • person1.address == person2.address 返回 true,有力地证明了它们共享同一个 Address 对象。
  • 修改 person2 的基本类型 ageperson1 不受影响。
  • 修改 person2 的引用类型 address 的内部属性 cityperson1 的 address.city 也随之改变。这就是浅拷贝的“副作用”。

三、深拷贝

1. 定义

深拷贝不仅会创建一个新对象,还会递归地复制对象内部的所有引用类型成员,直到所有对象都是全新的副本。

  • 对于基本数据类型,行为和浅拷贝一样,复制值。
  • 对于引用数据类型,会创建一个全新的对象,并将新对象的引用赋值给副本。

2. 效果

原对象和副本对象之间没有任何共享。对副本的任何修改都不会影响到原对象,反之亦然。它们是完全独立的两个对象。

3. Java 中的实现方式

a. 重写 clone() 方法(手动实现)

这是最原始的方式,但也是最繁琐、最容易出错的方式。你需要确保所有包含引用类型的成员变量也都实现了 Cloneable 接口,并在其 clone() 方法中进行深拷贝。

修改 Address 和 Person 类:

// Address.java - 也需要实现 Cloneable
class Address implements Cloneable {String city;// ... 构造函数和 toString() 同上 ...@Overridepublic Address clone() {try {return (Address) super.clone();} catch (CloneNotSupportedException e) {throw new AssertionError();}}
}// Person.java - 修改 clone() 方法
class Person implements Cloneable {// ... 其他成员和方法同上 ...// 重写 clone() 方法实现深拷贝@Overridepublic Person clone() {try {Person person = (Person) super.clone();// 关键:对引用类型的成员,也调用其 clone() 方法,创建一个新对象person.address = this.address.clone(); // 深拷贝的核心return person;} catch (CloneNotSupportedException e) {throw new AssertionError();}}
}

测试:使用上面同样的测试代码,你会发现输出结果完全不同:

--- 修改前 ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=30, address=Address{city='北京'}}
Person1.address == Person2.address ? false  // <-- 关键变化!--- 修改副本 person2 的基本类型 age ---
Person1: Person{age=30, address=Address{city='北京'}}
Person2: Person{age=31, address=Address{city='北京'}}--- 修改副本 person2 的引用类型 address ---
Person1: Person{age=30, address=Address{city='北京'}} // <-- Person1 不再受影响
Person2: Person{age=31, address=Address{city='上海'}}

缺点:如果对象层级很深,或者包含集合、数组等复杂结构,你需要为每一层、每一个元素都实现 clone(),代码会变得非常冗长和难以维护。

b. 序列化方式(推荐)

这是一种更通用、更健壮的实现深拷贝的方式。其原理是:将对象序列化(写入)到一个字节流中,然后再从字节流中反序列化(读出)来,得到一个全新的对象。

要求:对象及其内部所有引用类型的成员都必须实现 Serializable 接口。

实现一个工具类:

import java.io.*;public class DeepCopyUtil {@SuppressWarnings("unchecked")public static <T extends Serializable> T deepCopy(T object) {try {// 1. 将对象序列化到字节数组输出流ByteArrayOutputStream baos = new ByteArrayOutputStream();try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {oos.writeObject(object);}// 2. 从字节数组输入流中反序列化出对象try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(baos.toByteArray()))) {return (T) ois.readObject();}} catch (IOException | ClassNotFoundException e) {throw new RuntimeException("Failed to deep copy object", e);}}
}

修改 Address 和 Person 类:

// 只需要实现 Serializable 接口,不再需要 Cloneable
import java.io.Serializable;class Address implements Serializable { /* ... code ... */ }class Person implements Serializable { /* ... code ... */ }

测试序列化方式的深拷贝:

public class DeepCopySerializationDemo {public static void main(String[] args) {Address address = new Address("广州");Person person1 = new Person(25, address);// 使用工具类进行深拷贝Person person2 = DeepCopyUtil.deepCopy(person1);System.out.println("--- 修改前 ---");System.out.println("Person1: " + person1);System.out.println("Person2: " + person2);System.out.println("Person1.address == Person2.address ? " + (person1.getAddress() == person2.getAddress()));System.out.println("\n--- 修改副本 person2 ---");person2.setAge(26);person2.getAddress().city = "深圳";System.out.println("Person1: " + person1); // Person1 完全不受影响System.out.println("Person2: " + person2);}
}

优点

  • 代码简洁,一个工具类搞定所有。
  • 不需要关心对象内部的复杂结构,只要所有成员都 Serializable,就能正确复制。
  • 非常健壮,是业界推荐的方式。

缺点

  • 性能开销比 clone() 大,因为涉及 I/O 操作。
  • 所有相关的类都必须实现 Serializable 接口,有时可能无法做到(例如,第三方库的类)。

四、总结与对比

特性浅拷贝深拷贝
基本类型复制值,互不影响复制值,互不影响
引用类型复制引用,共享对象创建新对象,独立
实现方式Object.clone()1. 手动重写 clone()
2. 序列化/反序列化
速度快,只复制引用慢,需要创建新对象
内存占用较低,共享对象较高,创建新对象
对象独立性低,修改引用成员会影响原对象高,完全独立
实现复杂度简单较复杂(手动clone)或简单(序列化)

如何选择?

  • 默认情况下,如果你没有特殊需求,Java 的 clone() 提供的就是浅拷贝。 使用时必须清楚其副作用。
  • 当你的对象结构简单,且不包含引用类型,或者你明确希望共享内部引用时,可以使用浅拷贝。
  • 当你需要一个与原对象完全独立的副本,以避免任何意外的副作用时,必须使用深拷贝。 在 Java 中,强烈推荐使用序列化/反序列化的方式来实现深拷贝,因为它更通用、更安全、更易于维护。
http://www.xdnf.cn/news/18435.html

相关文章:

  • 【生成树+环】题解:P3907 环的异或_图论_环_异或_搜索_算法竞赛_C++
  • 【C++】多态(详解)
  • 单片机---------WIFI模块
  • 智能二维码QR\刷IC卡\人脸AI识别梯控系统功能设计需基于模块化架构,整合物联网、生物识别、权限控制等技术,以下是多奥分层次的系统设计框架
  • openEuler系统中home文件夹下huawei、HwHiAiUser、lost+found 文件夹的区别和作用
  • Linux:网络层IP协议
  • Spring Web MVC
  • 36v转5v峰值电流7A同步DC/DC降压芯片AH8655
  • C#开源库ACadSharp读取dwg图元的示例
  • Springboot项目的各层级详细总结
  • 【GaussDB】全密态等值查询功能测试及全密态技术介绍
  • Python socket远程部署工具服务
  • 论文阅读:Do As I Can, Not As I Say: Grounding Language in Robotic Affordances
  • 基于Django的学校实验室预约管理系统/基于python的实验室管理系统的设计与实现#python#django#FLASK
  • Spring Start Here 读书笔记:第9章 Using the Spring web scopes
  • Excel表格指定数据读取写入到另一个Excel表中(指定列指定行)
  • CXR-LT 2024:一场关于基于胸部X线的长尾、多标签和零样本疾病分类的MICCAI挑战赛|文献速递-深度学习人工智能医疗图像
  • 前端AI工具——TRAE
  • ExcelUtils实现 设置内容 插入行 复制行列格式
  • Blender模型动画导入到UE5
  • 【python】python进阶——推导式
  • 基于 SkyWalking + Elasticsearch + Grafana 的可落地调用链监控方案
  • 氙灯市场报告:亚太成增长主力,汽车、医疗、科研多领域需求驱动行业发展
  • 数据结构 -- 队列
  • Redis内存碎片深度解析:成因、检测与治理实战指南
  • Day16 二叉树part4
  • JDK21之虚拟线程的深入理解
  • Halcon那些事:什么是动态阈值,如何用dyn_threshold分割图片
  • 腾讯云COS SDK签名有效期设置为10分钟到期会自动刷新
  • Java后端学习路线