《计算机“十万个为什么”》之 🧩 深浅拷贝 & 引用拷贝:内存世界的复制魔法 ✨
深拷贝与浅拷贝是内存世界中的两种复制方式,它们各有其独特的特征和适用场景。在 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 引用拷贝的内存变化
graph TD
subgraph 栈内存 Stack
A[original] --> B[引用地址: 0x123]
C[copy] --> B
end
subgraph 堆内存 Heap
D[地址 0x123] --> E{{name: 'Tom' \n age: 21}}
end
三、浅拷贝:表面复制,深层共享 🚰
浅拷贝比引用拷贝进了一步,但仍有局限。
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接口会抛出此异常
*/
@Override
public 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)); // 输出: true
System.out.println(original.address.city.equals(shallowCopy.address.city)); // 输出: true
}
}
📘 浅拷贝关键点:
- Java 的
Object.clone()方法默认实现的是浅拷贝- 要实现浅拷贝,类必须实现
Cloneable接口- 浅拷贝对于 String 类型表现特殊,因为 String 是不可变的,修改时会创建新对象而非修改原有对象
3.4 浅拷贝的内存变化
graph TD
subgraph "栈内存 Stack"
A[original] --> B["引用地址: 0x123"]
C[shallowCopy] --> D["引用地址: 0x456"]
end
subgraph "堆内存 Heap"
E["地址 0x123"] --> F{{"name: 'Tom' \n age: 20 \n address: 0x789"}}
G["地址 0x456"] --> H{{"name: 'Tom' \n age: 21 \n address: 0x789"}}
I["地址 0x789"] --> J{{"city: 'Shanghai' \n country: 'China'"}}
end
四、深拷贝:完全复制,彻底独立 🌊
深拷贝是最彻底的拷贝方式。
4.1 什么是深拷贝?
深拷贝会创建一个全新的对象,并递归复制原对象的所有属性,包括引用类型属性。这意味着新对象与原对象完全独立,互不影响。
深拷贝核心特征
graph LR
A[原对象] -- 拷贝 --> B[新对象]
B --> C[新基本数据副本]
B --> D[新建指针]
D --> E[新子对象]
E --> F[新建数组副本]
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对象,属性值与当前对象相同
*/
@Override
public 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对象,所有属性(包括引用类型)都是拷贝的
*/
@Override
public 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 深拷贝的内存变化
graph LR
%% ===== 栈内存 =====
subgraph stack[栈内存 Stack]
orig[original对象] -->|指针| addr_stack1(0x7ffe1100)
copy[deepCopy对象] -->|指针| addr_stack2(0x7ffe1200)
end
%% ===== 堆内存 =====
subgraph heap[堆内存 Heap]
%% 原始对象
addr_stack1 --> obj1[原始对象]
obj1["name: 'Tom'\nage: 20"] -->|address| heap_addr1(0x1003000)
obj1 -->|hobbies| heap_addr3(0x1005000)
%% 拷贝对象
addr_stack2 --> obj2[拷贝对象]
obj2["name: 'Tom'\nage: 21"] -->|address| heap_addr2(0x1004000)
obj2 -->|hobbies| heap_addr4(0x1006000)
%% 子对象
heap_addr1 --> addr_obj["地址对象\ncity: 'Beijing'"]
heap_addr2 --> copy_addr_obj["地址对象\ncity: 'Shanghai'"]
heap_addr3 --> array1[["爱好数组\n• reading\n• coding"]]
heap_addr4 --> array2[["爱好数组\n• reading\n• coding\n• gaming"]]
end
%% ===== 样式 =====
classDef stack fill:#E1F5FE,stroke:#0288D1;
classDef heap fill:#E8F5E9,stroke:#388E3C;
classDef obj fill:#FFECB3,stroke:#FFA000;
classDef array fill:#FCE4EC,stroke:#E91E63;
class stack stack
class heap heap
class obj1,obj2,addr_obj,copy_addr_obj obj
class array1,array2 array
五、三种拷贝方式的对比分析 🆚
| 拷贝方式 | 特点 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 引用拷贝 | 只复制引用地址,共用数据 | 效率最高,占用空间最少 | 相互影响,容易引发 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;
// 尝试序列化会抛出StackOverflowError
try {
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 深拷贝的节点对象
*/
@Override
public 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(以及其他面向对象语言)中非常基础但又极其重要的概念。理解它们的工作原理,能够帮助我们编写更健壮、更可预测的代码。
- 引用拷贝:快速但危险,像共享一个房间的钥匙
- 浅拷贝:表面独立但深层共享,像复制了房子但共享地下室
- 深拷贝:完全独立但代价高昂,像建造一座一模一样的新房子
在实际开发中,我们需要根据具体场景和性能需求,选择合适的拷贝方式,而不是一味追求最"安全"的深拷贝。
八、延伸阅读 📚
希望这篇文章能帮助你理解计算机世界中的"复制魔法"!如果有任何问题或想法,欢迎在评论区留言讨论哦~ 😊