[Java学习日记11】聊聊深拷贝和浅拷贝

🚀 目录

  1. [底层基石:Java 内存模型与引用本质](#底层基石:Java 内存模型与引用本质)

    • 1.1 栈(Stack)与堆(Heap)的博弈

    • 1.2 值传递与引用传递的终极迷思

  2. 引用拷贝:多一把钥匙,开同一扇门

    • 2.1 内存指针的指向过程

    • 2.2 为什么这不是"拷贝"?

  3. [浅拷贝 (Shallow Copy):复制皮囊的艺术](#浅拷贝 (Shallow Copy):复制皮囊的艺术)

    • 3.1 Cloneable 接口:一个被吐槽的"标记"

    • 3.2 Object.clone() 的执行逻辑与保护模式

    • 3.3 致命缺陷:嵌套引用的"连坐反应"

  4. [深拷贝 (Deep Copy):灵魂的完美克隆](#深拷贝 (Deep Copy):灵魂的完美克隆)

    • 4.1 方案一:构造器重载(最原始但最稳)

    • 4.2 方案二:层层递归 clone(代码的地狱)

    • 4.3 方案三:Java 原生序列化(IO 流的魔法)

    • 4.4 方案四:JSON 序列化(现代开发的主流选择)

  5. [集合框架的"拷贝天坑":ArrayList 真的复制了吗?](#集合框架的“拷贝天坑”:ArrayList 真的复制了吗?)

    • 5.1 new ArrayList<>(list) 是深是浅?

    • 5.2 Arrays.copyOf() 的真实面目

  6. 性能与权衡:我们该如何选择?

    • 6.1 时间复杂度与空间消耗对比

    • 6.2 循环引用(Circular Reference)的灭顶之灾

  7. [面试通关:10 道深浅拷贝连环炮](#面试通关:10 道深浅拷贝连环炮)

  8. 结语:拒绝藕断丝连,做独立的开发者

1. 底层基石:Java 内存模型与引用本质

在讨论拷贝之前,如果不理解 Java 对象在内存里是怎么躺着的,那所有的代码都是空中楼阁。

1.1 栈(Stack)与堆(Heap)的博弈

Java 内存主要分为两块我们关心的区域:

  • 栈内存 (Stack) :存放基本类型变量(int, double 等)和对象的引用地址。它的特点是速度快,随着方法的结束自动释放。

  • 堆内存 (Heap) :存放真正的对象实例 (也就是 new 出来的东西)。

当我们写下 Student s = new Student() 时,栈里存的是一个 16 进制的地址(指针),而堆里存的是这个学生的具体姓名、年龄等属性。

1.2 值传递与引用传递的终极迷思

Java 只有值传递

  • 传递基本类型:传递的是具体数值的副本。

  • 传递对象:传递的是地址值的副本。 正是因为传递的是地址,才导致了我们"改了 A,B 也变了"的惨剧。

2. 引用拷贝:多一把钥匙,开同一扇门

这是新手最容易混淆的概念。

2.1 内存指针的指向过程

复制代码
Student s1 = new Student("林北", 20);
Student s2 = s1; // 这就是引用拷贝

在 JVM 层面,这行代码只是在栈里新开了一个位置 s2,然后把 s1 存的那个内存地址直接复制给了 s2

2.2 为什么这不是"拷贝"?

这就像是你把你的 B 站账号给了你室友。虽然你们有两个手机(变量),但登录的是同一个账号(对象)。你室友把关注列表清空了,你上去看也是空的。这种操作在内存中没有产生任何新对象,仅仅是增加了引用的计数。

3. 浅拷贝 (Shallow Copy):复制皮囊的艺术

3.1 Cloneable 接口:一个被吐槽的"标记"

在 Java 中,如果你想调用 clone() 方法,类必须实现 Cloneable 接口。 尴尬的是,这个接口里一个方法都没有 。它只是一个"标记接口",告诉 JVM:这个类允许被克隆。如果不实现它就调用 clone(),会直接甩你一个 CloneNotSupportedException

3.2 Object.clone() 的执行逻辑

Object.clone() 是一个 native 方法(本地方法,底层由 C++ 实现)。它的执行逻辑是:

  1. 在堆中开辟一块和原对象大小一模一样的空间。

  2. 逐个字段进行二进制流的复制。

3.3 致命缺陷:嵌套引用的"连坐反应"

如果字段是基本类型(int, float, boolean),直接复制值,没问题。 如果字段是引用类型(比如 Student 类里有一个 Address 对象),它复制的依然是地址

代码惨案现场:

class Address { String city; }ss; // 这是一个引用对象 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); // 默认是浅拷贝 } }

3. 深拷贝 (Deep Copy):真正的"克隆人"技术

3.1 什么是深拷贝?

深拷贝不仅复制了对象本身,还把对象内部引用的所有对象也都完整地复制了一份

生活化场景 : 你不仅配了一把钥匙,还直接在隔壁盖了一栋一模一样的房子,连装修和里面的家具都重新买了一套新的。 这时候,你在新房子里拆迁,旧房子纹丝不动。这才是真正的独立。

3.2 怎么实现深拷贝?

  1. 手动套娃 :在 clone() 方法里手动克隆内部的对象(非常麻烦,如果层级深,简直是噩梦)。

  2. 序列化(推荐):把对象写进流里,再读出来。这种方式会自动处理所有的嵌套引用。

    // 序列化实现深拷贝的核心思想
    public Object deepCopy(Object obj) {
    // 1. 将对象写入内存流
    // 2. 从内存流读回对象
    // 3. 返回新对象(此时是完全独立的副本)
    }

4. 核心对比:一张表分胜负

特征 引用拷贝 浅拷贝 (Shallow) 深拷贝 (Deep)
对象数量 1个 2个 2个
基本类型字段 共享 独立 独立
引用类型字段 共享 共享 独立
修改 B 对 A 有影响吗? 百分百影响 部分影响 (改内部引用时) 零影响
大学生比喻 同一套房的备用钥匙 复制了作业本但共用资料链接 完全独立的克隆人

5. 深度思考:为什么不默认全是深拷贝?

这是面试官喜欢追问的:"既然深拷贝这么好用,为什么 Java Object.clone() 不直接做成深拷贝?"

你可以这样回答

  1. 性能开销:深拷贝需要递归复制所有层级的对象,如果对象树非常庞大,会非常吃内存和 CPU。

  2. 循环引用问题:如果 A 引用了 B,B 又引用了 A,深拷贝就会陷入死循环。

  3. 灵活性:Java 把它交给程序员决定,这叫"按需分配"。

6. 实战技巧:如何优雅地深拷贝?

作为大学生,我们写项目时不需要手写复杂的克隆逻辑,可以用大厂造好的轮子:

  • Apache Commons LangSerializationUtils.clone(obj)

  • JSON 转换:先把对象转成 JSON 字符串,再转回对象(简单粗暴但有效)。

7. 结语

写完这篇日记,我终于把那个"改了新对象导致老对象也变了"的 Bug 修复了。我直接给那个复杂对象做了一次深拷贝,世界瞬间清静了。

作为大学生的我们:

  • 要分清 "皮囊" (外壳对象) 和 "灵魂" (内部引用)。

  • 别让对象之间 "藕断丝连",该切断的时候果断深拷贝。

如果你也曾被"分身术"对象搞得头大,点个赞支持一下!下期咱们聊聊如何创建对象

相关推荐
字节高级特工1 小时前
C++11(二) 革新:引用折叠与lambda表达式
java·开发语言·c++·算法
Mr.朱鹏1 小时前
基于 postgres_fdw 的跨库查询方案
java·数据库·spring boot·sql·spring·postgresql
xiaoshuaishuai81 小时前
C# AvaloniaUI‌的IValueConverter
开发语言·c#
敲个大西瓜1 小时前
Java并发实用干货
java
一只豌豆象1 小时前
第3讲:单端传输线的时域TDR仿真(基于实战的第一次仿真视角切换)
学习·信号完整性·cst·仿真实战
1368木林森1 小时前
【Spring源码17·完结篇】SpringBoot核心注解+高频坑点+失效场景万字全集!收官Spring全家桶源码系列
java·spring boot·后端
南山十一少1 小时前
基于 Quartz 组件在 Spring Boot 框架下的周期任务调度实验
java·spring boot·spring
txh05071 小时前
从零开始学习FOC
单片机·嵌入式硬件·学习
罗超驿1 小时前
14.LeetCode 438 题解:滑动窗口+哈希表找所有字母异位词
java·算法·leetcode