文章目录
- 前言
- 一、 前置知识:Java 的数据类型与对象存储
- 二、 浅拷贝(Shallow Copy):只拷贝 “表面”
- 1. 浅拷贝的定义
- 2. 浅拷贝的实现方式
- 3. 浅拷贝的核心特点
- 三、 深拷贝(Deep Copy):拷贝 “完全独立” 的对象
- 1. 深拷贝的定义
- 2. 深拷贝的两种实现方式
- 3. 深拷贝的核心特点
- 四、 浅拷贝 vs 深拷贝,核心区别对比
- 五、 开发中的避坑指南
- 六、 总结
前言
大家好,我是程序员梁白开,今天我们聊一聊Java 深拷贝与浅拷贝。
在 Java 开发中,对象复制是高频操作。你是否遇到过 “修改新对象,原对象也跟着变” 的诡异情况?是否在clone()方法上踩过无数坑?这些问题的根源,都在于没分清浅拷贝(Shallow Copy)和深拷贝(Deep Copy)的本质区别!
一、 前置知识:Java 的数据类型与对象存储
要搞懂深浅拷贝,必须先明白 Java 中数据的存储逻辑,这是一切的基础!
Java 中的数据类型分为两类:
- 基本数据类型:byte、short、int、long、float、double、char、boolean
- 存储位置:直接存储在栈内存中,变量保存的是值本身。
- 赋值逻辑:赋值时直接拷贝值,新旧变量互不影响。
- 引用数据类型:类、接口、数组
- 存储位置:对象本身存在堆内存,变量保存的是堆内存的地址引用。
- 赋值逻辑:赋值时拷贝的是地址引用,新旧变量指向同一个堆内存对象。
举个简单的赋值例子:
// 基本数据类型赋值inta=10;intb=a;b=20;System.out.println(a);// 输出10,a不受b影响// 引用数据类型赋值Useruser1=newUser("张三",20);Useruser2=user1;// 拷贝的是引用地址user2.setName("李四");System.out.println(user1.getName());// 输出李四,原对象被修改!二、 浅拷贝(Shallow Copy):只拷贝 “表面”
1. 浅拷贝的定义
浅拷贝是指:创建一个新对象,新对象的基本数据类型成员变量会拷贝原对象的值;但引用数据类型成员变量,拷贝的是原对象的地址引用。
简单说:浅拷贝只拷贝 “第一层”,引用类型成员变量还是共用同一个堆对象。
2. 浅拷贝的实现方式
Java 中实现浅拷贝有两种常见方式:
- 方式 1:手动 new 对象,逐个赋值
- 方式 2:实现Cloneable接口,重写clone()方法(默认浅拷贝)、
方式 1:手动浅拷贝
手动创建新对象,将原对象的成员变量逐个赋值,引用类型变量直接赋值地址。
// 用户类classUser{privateStringname;// 引用类型privateintage;// 基本类型// 构造器、getter、setter省略}// 手动浅拷贝publicclassShallowCopyDemo{publicstaticvoidmain(String[]args){Useruser1=newUser("张三",20);// 手动浅拷贝Useruser2=newUser();user2.setName(user1.getName());user2.setAge(user1.getAge());// 修改基本类型user2.setAge(25);System.out.println(user1.getAge());// 输出20,不受影响// 修改引用类型(String是不可变类,这里换个例子更直观)// 我们换一个自定义引用类型成员变量演示,比如给User加一个Address属性}}方式 2:实现Cloneable接口(推荐)
Java 中Object类提供了clone()方法,但它是protected修饰的,且默认是浅拷贝。要使用该方法,需要:
- 实现Cloneable标记接口(没有任何方法,仅作标记)
- 重写clone()方法,修改访问权限为public
我们给User类增加一个引用类型成员变量Address,更直观地看浅拷贝的问题:
// 地址类(引用类型)classAddress{privateStringcity;// 构造器、getter、setter省略publicAddress(Stringcity){this.city=city;}}// 用户类实现Cloneable接口classUserimplementsCloneable{privateStringname;// 引用类型privateintage;// 基本类型privateAddressaddr;// 自定义引用类型// 构造器、getter、setter省略publicUser(Stringname,intage,Addressaddr){this.name=name;this.age=age;this.addr=addr;}// 重写clone方法,默认浅拷贝@OverridepublicObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}// 测试浅拷贝publicclassShallowCopyTest{publicstaticvoidmain(String[]args)throwsCloneNotSupportedException{Addressaddr=newAddress("北京");Useruser1=newUser("张三",20,addr);// 浅拷贝Useruser2=(User)user1.clone();// 1. 修改基本类型成员变量user2.setAge(25);System.out.println(user1.getAge());// 输出20,不受影响// 2. 修改引用类型成员变量(核心坑点!)user2.getAddr().setCity("上海");System.out.println(user1.getAddr().getCity());// 输出上海!原对象被修改}}3. 浅拷贝的核心特点
- 优点:实现简单,拷贝效率高。
- 缺点:引用类型成员变量共享堆内存,修改新对象的引用成员,会影响原对象,存在数据安全风险。
三、 深拷贝(Deep Copy):拷贝 “完全独立” 的对象
1. 深拷贝的定义
深拷贝是指:创建一个新对象,不仅拷贝原对象的基本数据类型成员变量值,还会为引用数据类型成员变量重新创建一个独立的新对象。
简单说:深拷贝会拷贝 “所有层级”,新对象和原对象完全独立,修改任何一方都不会影响另一方。
2. 深拷贝的两种实现方式
深拷贝的实现比浅拷贝复杂,常见的有两种方式:
- 方式 1:嵌套重写clone()方法(适用于自定义类较少的场景)
- 方式 2:序列化与反序列化(通用推荐,适用于复杂对象)
方式 1:嵌套重写clone()方法
思路:不仅要让外层类实现Cloneable,其所有引用类型成员变量对应的类,也需要实现Cloneable并重写clone()方法,然后在外层类的clone()方法中手动拷贝引用成员。
// 地址类实现Cloneable接口classAddressimplementsCloneable{privateStringcity;publicAddress(Stringcity){this.city=city;}// getter、setter省略// 地址类重写clone方法@OverridepublicObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}// 用户类classUserimplementsCloneable{privateStringname;privateintage;privateAddressaddr;// 构造器、getter、setter省略// 重写clone方法,实现深拷贝@OverridepublicObjectclone()throwsCloneNotSupportedException{// 1. 先拷贝外层对象(浅拷贝)Useruser=(User)super.clone();// 2. 再手动拷贝引用类型成员变量(关键步骤!)user.addr=(Address)this.addr.clone();returnuser;}}// 测试深拷贝publicclassDeepCopyTest{publicstaticvoidmain(String[]args)throwsCloneNotSupportedException{Addressaddr=newAddress("北京");Useruser1=newUser("张三",20,addr);Useruser2=(User)user1.clone();// 修改新对象的引用成员变量user2.getAddr().setCity("上海");// 原对象不受影响!System.out.println(user1.getAddr().getCity());// 输出北京}}方式 2:序列化与反序列化(推荐)
思路:将对象序列化为字节流,再从字节流反序列化为新对象,这个过程会自动创建一个完全独立的新对象。
注意:所有涉及的类都需要实现Serializable标记接口,否则会抛出序列化异常。
importjava.io.*;// 地址类实现SerializableclassAddressimplementsSerializable{privatestaticfinallongserialVersionUID=1L;// 序列化版本号privateStringcity;// 构造器、getter、setter省略}// 用户类实现SerializableclassUserimplementsSerializable{privatestaticfinallongserialVersionUID=1L;privateStringname;privateintage;privateAddressaddr;// 构造器、getter、setter省略// 深拷贝工具方法publicUserdeepCopy()throwsIOException,ClassNotFoundException{// 1. 序列化:将对象写入字节流ByteArrayOutputStreambos=newByteArrayOutputStream();ObjectOutputStreamoos=newObjectOutputStream(bos);oos.writeObject(this);// 2. 反序列化:从字节流读取新对象ByteArrayInputStreambis=newByteArrayInputStream(bos.toByteArray());ObjectInputStreamois=newObjectInputStream(bis);return(User)ois.readObject();}}// 测试序列化深拷贝publicclassDeepCopyBySerialize{publicstaticvoidmain(String[]args)throwsException{Addressaddr=newAddress("北京");Useruser1=newUser("张三",20,addr);Useruser2=user1.deepCopy();user2.getAddr().setCity("上海");System.out.println(user1.getAddr().getCity());// 输出北京,完全独立}}3. 深拷贝的核心特点
- 优点:新对象与原对象完全隔离,数据安全,不存在相互影响的问题。
- 缺点:实现相对复杂,序列化方式会有一定的性能开销。
四、 浅拷贝 vs 深拷贝,核心区别对比
为了方便大家记忆,我们用一张表格总结两者的核心区别:
| 特性 | 浅拷贝 | 深拷贝 |
|---|---|---|
| 拷贝层级 | 仅拷贝第一层(基本类型值 + 引用地址) | 拷贝所有层级(基本类型值 + 引用对象新实例) |
| 引用成员变量 | 与原对象共享同一个引用对象 | 重新创建独立的引用对象 |
| 修改影响 | 修改新对象的引用成员,会影响原对象 | 修改新对象,完全不影响原对象 |
| 实现难度 | 简单,手动赋值或重写 clone () 即可 | 较复杂,嵌套 clone 或序列化 |
| 性能 | 效率高,开销小 | 效率较低,序列化有额外开销 |
| 适用场景 | 对象无引用类型成员,或无需独立引用成员 | 对象包含多层引用类型,需要完全独立的拷贝 |
五、 开发中的避坑指南
- 不要滥用clone()方法:默认是浅拷贝,一定要检查引用成员变量是否会导致数据安全问题。
- 序列化深拷贝注意事项:
- 所有相关类必须实现Serializable接口。
- 静态变量不会被序列化,因为静态变量属于类,不属于对象。
- transient修饰的变量不会被序列化,可用于排除不需要拷贝的敏感字段。
- 不可变类的特殊情况:比如String、Integer等不可变类,因为其值不可修改,即使是浅拷贝,也不会出现数据联动问题。
六、 总结
- 直接赋值≠拷贝:引用类型赋值只是传递地址,新旧变量指向同一对象。
- 浅拷贝:只拷贝第一层,引用成员共享,适合简单对象。
- 深拷贝:拷贝所有层级,对象完全独立,适合复杂嵌套对象。
选择哪种拷贝方式,核心看业务需求:如果只是临时使用且不需要修改引用成员,浅拷贝足够;如果需要长期存储或修改数据,一定要用深拷贝保证数据安全!