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

《计算机“十万个为什么”》之 [特殊字符] 深浅拷贝 引用拷贝:内存世界的复制魔法 ✨

《计算机“十万个为什么”》之 🧩 深浅拷贝 & 引用拷贝:内存世界的复制魔法 ✨

深拷贝与浅拷贝是内存世界中的两种复制方式,它们各有其独特的特征和适用场景。在 Java 中,深拷贝和浅拷贝的实现方式各有不同,需要根据具体情况进行选择。

本文将介绍深拷贝与浅拷贝的核心特征、Java 中的实现方式、适用场景以及注意事项。

作者:无限大

推荐阅读时间:15min

引言:为什么复制也会出问题? 🤔

在编程世界里,我们经常需要复制数据。但你是否遇到过这样的情况:明明修改了新变量,旧变量却跟着一起变了?或者反过来,以为修改了新变量,结果旧变量纹丝不动?这背后其实隐藏着内存复制的奥秘——深浅拷贝与引用拷贝。

今天,就让我们一起揭开这个内存世界的复制魔法吧!🔮


一、变量与内存:数据存储的基本原理 🧠

在了解拷贝之前,我们首先要明白变量在内存中是如何存储的。

1.1 基本数据类型 vs 引用数据类型

计算机中的数据类型可以分为两大类,这一分类在 Java 语言中尤为重要:

类型特点传递方式Java 常见例子存储方式
基本数据类型不可再分的原子值值传递int, double, boolean, char, byte, short, long, float直接存储在栈内存中
引用数据类型由多个值构成的对象引用传递String, Object, Array, List, Map, 自定义类(Java 当中的包装类都是引用数据类型)栈内存存储引用地址,堆内存存储实际数据

📌 Java 特殊说明
String 虽然是引用数据类型,但在 Java 中具有值类型的特性,因为它是不可变的(immutable)。当你修改 String 对象时,实际上会创建一个新的 String 对象;若要对 String 对象进行频繁修改,建议使用 StringBuilder(非线程安全)或 StringBuffer(线程安全)类以避免不必要的内存开销和性能损耗

1.2 内存分配:栈内存 vs 堆内存

在 Java 中,内存主要分为栈内存和堆内存:

  • 栈内存:用于存储方法调用时的局部变量、方法参数、返回值等。它的内存分配和释放速度很快,因为栈内存的管理是自动的(遵循"先进后出"原则)。
  • 堆内存:用于存储对象实例、数组等动态分配的内存。堆内存的管理相对复杂,因为需要手动分配和释放内存,且垃圾回收机制会自动回收不再使用的对象。

📝 Java 内存模型要点

  • 基本数据类型直接存储在栈内存中,操作简单快速
  • 引用数据类型在栈内存中存储引用地址,堆内存中存储实际数据
  • 引用数据类型的赋值操作是引用拷贝,而非值拷贝
  • 引用数据类型的比较操作是比较引用地址,而非内容
  • 引用数据类型的修改会影响所有指向该对象的引用变量

二、引用拷贝:看似复制,实则共享 🔗

引用拷贝是最简单也最容易让人迷惑的拷贝方式。

2.1 什么是引用拷贝?

引用拷贝只是复制了对象的引用地址,而不是实际数据。这意味着新变量和原变量指向堆内存中的同一个对象。

2.2 代码示例:Java 中的引用拷贝

在 Java 中,对象赋值操作默认就是引用拷贝,这是初学者最容易混淆的概念之一:

// 创建一个自定义类
class Person {String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}
}/*** 引用拷贝示例类* 演示Java中对象赋值默认是引用拷贝,而非值拷贝*/
public class ReferenceCopyExample {public static void main(String[] args) {// 创建原始对象实例Person original = new Person("Tom", 20);// 引用拷贝 - 仅复制对象引用,不复制实际对象// 此时original和copy指向堆内存中的同一个Person对象Person copy = original;// 修改拷贝对象的属性值// 由于是引用拷贝,original和copy指向同一对象,所以两者都会受到影响copy.age = 21;// 验证原对象也被修改System.out.println(original.age); // 输出: 21 - 原对象年龄也被修改System.out.println(copy.age);     // 输出: 21 - 拷贝对象年龄// 使用==运算符比较对象引用是否相同// 结果为true,证明两个引用指向同一个对象System.out.println(original == copy); // 输出: true(比较引用地址)}
}

📝 Java 引用拷贝要点

  • 使用 =操作符为对象赋值时,永远是引用拷贝
  • ==运算符比较的是对象引用地址,而非内容
  • 基本数据类型使用 =时是值拷贝,而非引用拷贝

==equals的区别

在基本数据类型当中
== 运算符用于比较两个值是否相等,并且没有 equals方法。

在引用数据类型中,==equals 的区别尤为重要:
在 Java 中,==equals 是两个常用的比较操作符,但它们有着本质的区别:
==运算符比较的是对象的引用地址,而 equals方法比较的是对象的内容。

2.3 引用拷贝的内存变化

堆内存 Heap
栈内存 Stack
name: 'Tom' \n age: 21
地址 0x123
引用地址: 0x123
original
copy

三、浅拷贝:表面复制,深层共享 🚰

浅拷贝比引用拷贝进了一步,但仍有局限。

3.1 什么是浅拷贝?

浅拷贝会创建一个新对象,并复制原对象的所有基本数据类型属性。但对于引用类型属性,它只复制引用地址,而不是实际数据。

3.2 Java 中的浅拷贝实现方式

Java 没有内置的浅拷贝函数,需要通过以下方式实现:

方法实现方式适用场景
Cloneable 接口实现 Cloneable 并重写 clone()方法自定义对象浅拷贝
构造方法拷贝新建对象并逐个复制基本类型属性简单对象浅拷贝
Arrays.copyOf()Arrays.copyOf(original, length)数组浅拷贝
Collections.copy()Collections.copy(dest, src)集合浅拷贝

3.3 代码示例:Java 中的浅拷贝

// 地址类
class Address {String city;String country;public Address(String city, String country) {this.city = city;this.country = country;}
}// 人员类 - 实现Cloneable接口以支持浅拷贝
class Person implements Cloneable {String name; // 基本类型包装类(String是不可变引用类型)int age;     // 基本数据类型Address address; // 引用数据类型public Person(String name, int age, Address address) {this.name = name;this.age = age;this.address = address;}/*** 重写clone()方法实现浅拷贝* Object类的clone()方法默认实现浅拷贝* @return 拷贝的Person对象* @throws CloneNotSupportedException 如果未实现Cloneable接口会抛出此异常*/@Overridepublic Person clone() throws CloneNotSupportedException {return (Person) super.clone();}
}/*** 浅拷贝示例类* 演示浅拷贝对基本类型和引用类型的不同处理方式*/
public class ShallowCopyExample {public static void main(String[] args) throws CloneNotSupportedException {// 创建地址对象(引用类型)Address address = new Address("Beijing", "China");// 创建原始人员对象Person original = new Person("Tom", 20, address);// 使用clone()方法进行浅拷贝Person shallowCopy = original.clone();// 修改拷贝对象的基本类型属性(age是基本类型)shallowCopy.age = 21;// 修改拷贝对象的引用类型属性(address是引用类型)shallowCopy.address.city = "Shanghai";// 基本数据类型属性修改不影响原对象System.out.println(original.age); // 输出: 20 - 原对象年龄不变System.out.println(shallowCopy.age); // 输出: 21 - 拷贝对象年龄已修改// 引用数据类型属性修改影响原对象System.out.println(original.address.city); // 输出: Shanghai - 原对象地址被修改System.out.println(shallowCopy.address.city); // 输出: Shanghai - 拷贝对象地址// 验证是否为不同对象System.out.println(original == shallowCopy); // 输出: false - 两个是不同对象System.out.println(original.address == shallowCopy.address); // 输出: true - 引用类型属性指向同一对象System.out.println(original.name == shallowCopy.name); // 输出: true - String是不可变对象,共享引用// 验证对象内容System.out.println(original.name.equals(shallowCopy.name)); // 输出: trueSystem.out.println(original.address.city.equals(shallowCopy.address.city)); // 输出: true}
}

📘 浅拷贝关键点

  • Java 的 Object.clone()方法默认实现的是浅拷贝
  • 要实现浅拷贝,类必须实现 Cloneable接口
  • 浅拷贝对于 String 类型表现特殊,因为 String 是不可变的,修改时会创建新对象而非修改原有对象

3.4 浅拷贝的内存变化

堆内存 Heap
栈内存 Stack
name: 'Tom' \n age: 20 \n address: 0x789
地址 0x123
name: 'Tom' \n age: 21 \n address: 0x789
地址 0x456
city: 'Shanghai' \n country: 'China'
地址 0x789
引用地址: 0x123
original
引用地址: 0x456
shallowCopy

四、深拷贝:完全复制,彻底独立 🌊

深拷贝是最彻底的拷贝方式。

4.1 什么是深拷贝?

深拷贝会创建一个全新的对象,并递归复制原对象的所有属性,包括引用类型属性。这意味着新对象与原对象完全独立,互不影响。

深拷贝核心特征
拷贝
原对象
新对象
新基本数据副本
新建指针
新子对象
新建数组副本

4.2 Java 中的深拷贝实现方式

Java 实现深拷贝需要更多工作,常见方法如下:

方法实现方式优点缺点
递归深拷贝手动递归复制所有层级对象灵活可控,无第三方依赖实现复杂,需为每个类编写拷贝逻辑
序列化/反序列化对象转字节流再恢复为新对象实现简单,自动递归性能较差,要求所有对象可序列化
Apache Commons 工具类使用 SerializationUtils.clone()成熟稳定,处理边界情况增加第三方依赖

4.3 代码示例:Java 递归实现深拷贝

import java.util.ArrayList;
import java.util.List;// 地址类 - 实现深拷贝接口
class Address implements DeepCloneable {String city;String country;public Address(String city, String country) {this.city = city;this.country = country;}/*** 深拷贝方法,创建并返回一个新的Address对象* @return 新的Address对象,属性值与当前对象相同*/@Overridepublic Address deepClone() {return new Address(this.city, this.country);}
}/*** 深拷贝接口,定义深拷贝方法* 实现此接口的类需要提供深拷贝实现*/
interface DeepCloneable {/*** 创建并返回当前对象的深拷贝* @return 当前对象的深拷贝*/Object deepClone();
}// 人员类 - 实现深拷贝
class Person implements DeepCloneable {String name;int age;Address address;List<String> hobbies;public Person(String name, int age, Address address, List<String> hobbies) {this.name = name;this.age = age;this.address = address;this.hobbies = hobbies;}/*** 实现深拷贝方法,递归拷贝所有层级属性* @return 新的Person对象,所有属性(包括引用类型)都是拷贝的*/@Overridepublic Person deepClone() {// 拷贝基本类型属性(直接赋值)String newName = this.name; // String是不可变的,直接赋值即可int newAge = this.age;// 递归拷贝引用类型属性(调用Address的深拷贝方法)Address newAddress = this.address.deepClone();// 拷贝集合(创建新集合并添加元素)List<String> newHobbies = new ArrayList<>();for (String hobby : this.hobbies) {newHobbies.add(hobby); // String是不可变的,直接添加}// 返回新创建的Person对象return new Person(newName, newAge, newAddress, newHobbies);}
}/*** 深拷贝示例类* 演示递归实现深拷贝的完整流程和效果*/
public class DeepCopyExample {public static void main(String[] args) {// 创建原始对象Address address = new Address("Beijing", "China");List<String> hobbies = new ArrayList<>();hobbies.add("reading");hobbies.add("coding");Person original = new Person("Tom", 20, address, hobbies);// 进行深拷贝 - 创建完全独立的新对象Person deepCopy = original.deepClone();// 修改拷贝对象的属性deepCopy.age = 21;                  // 修改基本类型属性deepCopy.address.city = "Shanghai"; // 修改引用类型属性deepCopy.hobbies.add("gaming");     // 修改集合类型属性// 原对象不受影响(深拷贝特点)System.out.println(original.age); // 输出: 20 - 原对象年龄未变System.out.println(original.address.city); // 输出: Beijing - 原对象地址未变System.out.println(original.hobbies.size()); // 输出: 2 - 原对象集合大小未变// 拷贝对象已修改System.out.println(deepCopy.age); // 输出: 21 - 拷贝对象年龄已修改System.out.println(deepCopy.address.city); // 输出: Shanghai - 拷贝对象地址已修改System.out.println(deepCopy.hobbies.size()); // 输出: 3 - 拷贝对象集合已修改}
}

4.4 代码示例:使用序列化实现深拷贝

import java.io.*;
import java.util.ArrayList;
import java.util.List;// 所有需要序列化的类必须实现Serializable接口
class Address implements Serializable {String city;String country;public Address(String city, String country) {this.city = city;this.country = country;}
}class Person implements Serializable {String name;int age;Address address;List<String> hobbies;public Person(String name, int age, Address address, List<String> hobbies) {this.name = name;this.age = age;this.address = address;this.hobbies = hobbies;}/*** 使用序列化实现深拷贝* 通过将对象写入字节流再读取回来创建全新对象* @return 当前对象的深拷贝* @throws IOException 如果序列化过程出错* @throws ClassNotFoundException 如果反序列化时类找不到*/public Person deepClone() throws IOException, ClassNotFoundException {// 将对象写入字节流(序列化过程)ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(this);// 从字节流读取对象(反序列化过程,创建全新对象)ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());ObjectInputStream ois = new ObjectInputStream(bis);return (Person) ois.readObject();}
}/*** 序列化深拷贝示例类* 演示使用序列化/反序列化实现深拷贝的方法*/
public class SerializationDeepCopyExample {public static void main(String[] args) {try {// 创建原始对象Address address = new Address("Beijing", "China");List<String> hobbies = new ArrayList<>();hobbies.add("reading");hobbies.add("coding");Person original = new Person("Tom", 20, address, hobbies);// 使用序列化实现深拷贝Person deepCopy = original.deepClone();// 修改拷贝对象deepCopy.age = 21;                  // 修改基本类型属性deepCopy.address.city = "Shanghai"; // 修改引用类型属性deepCopy.hobbies.add("gaming");     // 修改集合类型属性// 验证原对象不受影响System.out.println(original.age); // 输出: 20 - 原对象年龄未变System.out.println(original.address.city); // 输出: Beijing - 原对象地址未变System.out.println(original.hobbies.size()); // 输出: 2 - 原对象集合大小未变// 验证拷贝对象已修改System.out.println(deepCopy.age); // 输出: 21 - 拷贝对象年龄已修改System.out.println(deepCopy.address.city); // 输出: Shanghai - 拷贝对象地址已修改System.out.println(deepCopy.hobbies.size()); // 输出: 3 - 拷贝对象集合已修改} catch (Exception e) {e.printStackTrace();}}
}

⚠️ 序列化拷贝注意事项

  • 所有参与序列化的类必须实现 Serializable 接口
  • 静态成员变量不会被序列化
  • 使用 transient 关键字标记的成员不会被序列化
  • 性能比手动递归拷贝差,但实现简单

4.5 深拷贝的内存变化

堆内存 Heap
栈内存 Stack
指针
指针
address
hobbies
address
hobbies
name: 'Tom'\nage: 20
0x1003000
0x1005000
name: 'Tom'\nage: 21
0x1004000
0x1006000
地址对象\ncity: 'Beijing'
地址对象\ncity: 'Shanghai'
爱好数组\n• reading\n• coding
爱好数组\n• reading\n• coding\n• gaming
0x7ffe1100
original对象
0x7ffe1200
deepCopy对象

五、三种拷贝方式的对比分析 🆚

拷贝方式特点优点缺点适用场景
引用拷贝只复制引用地址,共用数据效率最高,占用空间最少相互影响,容易引发 bug不需要独立对象,只想创建别名时
浅拷贝复制第一层属性,深层共用效率较高,实现简单深层引用类型仍共享对象结构简单,无深层引用类型时
深拷贝完全复制所有层级完全独立,互不影响效率较低,占用空间大需要完全独立的对象,有深层嵌套结构时

六、实际开发中的拷贝策略 💡

6.1 何时使用哪种拷贝方式?

  • 引用拷贝:函数参数传递、临时变量等不需要修改原对象的场景
  • 浅拷贝:简单数据结构、状态管理中的不可变状态更新
  • 深拷贝:复杂嵌套对象、需要完全隔离的数据

6.2 Java 拷贝中的常见问题与解决方案

问题 1:循环引用处理

Java 的序列化机制无法直接处理循环引用,会抛出 StackOverflowError:

/*** 节点类,用于演示序列化深拷贝中的循环引用问题* 实现Serializable接口,但不处理循环引用*/
class Node implements Serializable {// 节点数据String data;// 下一个节点引用Node next;// 构造方法,初始化节点数据public Node(String data) {this.data = data;}
}/*** 循环引用示例类* 演示序列化深拷贝无法处理循环引用的问题*/
public class CircularReferenceExample {public static void main(String[] args) {// 创建两个节点Node node1 = new Node("A");Node node2 = new Node("B");// 创建循环引用(node1指向node2,node2指向node1)node1.next = node2;node2.next = node1;// 尝试序列化会抛出StackOverflowErrortry {serializeAndDeserialize(node1);} catch (Exception e) {e.printStackTrace(); // 会抛出StackOverflowError,因为序列化无法处理循环引用}}
}

解决方案:手动控制拷贝过程,使用标识避免无限递归:

/*** 节点类,实现DeepCloneable接口处理循环引用* 使用标记变量解决深拷贝中的循环引用问题*/
class Node implements DeepCloneable {// 节点数据String data;// 下一个节点引用Node next;// 标记是否正在克隆,用于处理循环引用transient boolean isCloning = false;// 构造方法,初始化节点数据public Node(String data) {this.data = data;}/*** 深拷贝方法,处理循环引用* 使用isCloning标记避免无限递归* @return 深拷贝的节点对象*/@Overridepublic Node deepClone() {// 如果正在克隆过程中遇到自己,直接返回当前对象(处理循环引用)if (isCloning) {return this;}// 标记开始克隆isCloning = true;// 创建新节点Node clone = new Node(this.data);// 递归克隆下一个节点if (this.next != null) {clone.next = this.next.deepClone();}// 克隆完成,重置标记isCloning = false;return clone;}
}
问题 2:不可变对象的特殊处理

Java 中的 String、Integer 等不可变对象有特殊的拷贝行为:

/*** 不可变对象拷贝示例类* 演示不可变对象(如String)的特殊拷贝行为*/
public class ImmutableCopyExample {public static void main(String[] args) {// 创建原始字符串(不可变对象)String original = "Hello";// 引用拷贝 - 指向同一个String对象String copy = original;// String是不可变的,修改操作会创建新对象而非修改原有对象copy = copy + " World"; // 实际上创建了新的String对象// 验证原始对象未被修改System.out.println(original); // 输出: Hello - 原始对象保持不变System.out.println(copy);     // 输出: Hello World - 新创建的对象// 验证两个引用指向不同对象System.out.println(original == copy); // 输出: false}
}

📌 Java 拷贝最佳实践

  • 优先考虑不可变对象设计,减少拷贝需求
  • 对于简单对象,使用构造函数进行显式拷贝
  • 对于复杂对象,考虑使用 Builder 模式创建新对象
  • 深拷贝成本高,谨慎使用,优先考虑设计上避免深拷贝需求

七、总结与思考 🤔

深浅拷贝和引用拷贝是 JavaScript(以及其他面向对象语言)中非常基础但又极其重要的概念。理解它们的工作原理,能够帮助我们编写更健壮、更可预测的代码。

  • 引用拷贝:快速但危险,像共享一个房间的钥匙
  • 浅拷贝:表面独立但深层共享,像复制了房子但共享地下室
  • 深拷贝:完全独立但代价高昂,像建造一座一模一样的新房子

在实际开发中,我们需要根据具体场景和性能需求,选择合适的拷贝方式,而不是一味追求最"安全"的深拷贝。


八、延伸阅读 📚

  1. MDN Web Docs - 结构化克隆算法
  2. Lodash - cloneDeep

希望这篇文章能帮助你理解计算机世界中的"复制魔法"!如果有任何问题或想法,欢迎在评论区留言讨论哦~ 😊

http://www.dtcms.com/a/293742.html

相关文章:

  • 1.1 Deep learning?pytorch ?深度学习训练出来的模型通常有效但无法解释合理性? 如何 解释?
  • 英语词汇积累Day1-10(summary)
  • Django实战:Python代码规范指南
  • 【Java】Reflection反射(代理模式)
  • 算法竞赛备赛——【图论】最小生成树
  • 《元素周期表》超高清PDF
  • IDEA如何管理多个Java版本。
  • STM32 基础知识 定时器【概念】
  • 基于PyTorch的多视角二维流场切片三维流场预测模型
  • 【NLP舆情分析】基于python微博舆情分析可视化系统(flask+pandas+echarts) 视频教程 - 主页-微博点赞量Top6实现
  • 19.动态路由协议基础
  • 备受关注的“Facebook Email Scraper”如何操作?
  • 开源 Arkts 鸿蒙应用 开发(十)通讯--Http
  • 腾讯云推出CodeBuddy:革新AI全栈开发体验
  • 第六章 W55MH32 UDP Multicast示例
  • 神经架构搜索革命:从动态搜索到高性能LLM的蜕变之路
  • AI 搜索引擎:让信息“长脑子”而不是“堆数据”
  • 解决 i.MX6ULL 通过 ADB 连接时权限不足问题 not in the plugdev group
  • 网络调制技术对比表
  • Numpy 库 矩阵数学运算,点积,文件读取和保存等
  • 线段树学习笔记 - 练习题(1)
  • iOS 性能监控 苹果手机后台运行与能耗采样实战指南
  • 沉浸式文旅新玩法-基于4D GS技术的真人数字人赋能VR体验升级
  • 深度相机---像素转物理尺寸
  • 【基于OpenCV的图像处理】图像预处理之二值化处理以及图像的仿射变换
  • 基于Python flask的常用AI工具功能数据分析与可视化系统设计与实现,技术包括LSTM、SVM、朴素贝叶斯三种算法,echart可视化
  • linxu CentOS 配置nginx
  • 字节 AI 编辑器 Trae 2.0 SOLO 出道! 国际版不充分指南及与国内版的对比
  • 【web页面接入Apple/google/facebook三方登录】
  • 精准扫描,驱动未来:迁移科技3D视觉系统在工业自动化中的革命性应用