Java 中的深拷贝和浅拷贝你了解吗?
前言
Java 开发中,对象拷贝是常有的事,很多人可能搞不清到底是拷贝了引用还是拷贝了对象。本文将详细介绍相关知识,让你充分理解 Java 拷贝。
一、对象是如何存储的?
方法执行过程中,方法体中的数据类型主要分两种,它们的存储方式是不同的(如下图):
- 基本数据类型: 直接存储在栈帧的局部变量表中;
- 引用数据类型: 对象的引用存储在栈帧的局部变量表中,而对实例本身及其所有成员变量存放在堆内存中。
详情可见JVM基础
二、前置准备
创建两个实体类方便后续的代码示例
@Data @AllArgsConstructor public class Animal{ private int id; private String type; @Override public String toString () { return "Animal{" + "id=" + id + ", type='" + type + '\'' + '}'; } }
@Data @AllArgsConstructor public class Dog { private int age; private String name; private Animal animal; @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
三、直接赋值
直接赋值是我们最常用的方式,它只是拷贝了对象引用地址,并没有在内存中生成新的对象。
下面我们进行代码验证:
public class FuXing { public static void main (String[] args) { Animal animal = new Animal(1, "dog"); Dog dog = new Dog(18, "husky", animal); Dog dog2 = dog; System.out.println("两个对象是否相等:" + (dog2 == dog)); System.out.println("----------------------------"); dog.setAge(3); System.out.println("变化后两个对象是否相等:" + (dog2 == dog)); } }
两个对象是否相等:true ---------------------------- 变化后两个对象是否相等:true
通过运行结果可知,dog
类的age
已经发生变化,但重新打印两个类依然相等。所以它只是拷贝了对象引用地址,并没有在内存中生成新的对象。
直接赋值的 JVM 的内存结构大致如下:
四、浅拷贝
浅拷贝后会创建一个新的对象,且新对象的属性和原对象相同。但是,拷贝时针对原对象的属性的数据类型的不同,有两种不同的情况:
- 属性的数据类型基本类型,拷贝的就是基本类型的值;
- 属性的数据类型引用类型,拷贝的就是对象的引用地址,意思就是拷贝对象与原对象引用同一个对象。
要实现对象浅拷贝还是比较简单的,只需要被拷贝的类实现Cloneable
接口,重写clone
方法即可。下面我们对Dog
进行改动:
@Data @AllArgsConstructor public class Dog implements Cloneable{ private int age; private String name; private Animal animal; @Override public Dog clone () throws CloneNotSupportedException { return (Dog) super.clone(); } @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
接下来我们运行下面的代码,看一下运行结果:
public class FuXing { public static void main (String[] args) throws Exception { Animal animal = new Animal(1, "dog"); Dog dog = new Dog(18, "husky", animal); // 克隆对象 Dog cloneDog = dog.clone(); System.out.println("dog:" + dog); System.out.println("cloneDog:" + cloneDog); System.out.println("两个对象是否相等:" + (cloneDog == dog)); System.out.println("两个name是否相等:" + (cloneDog.getName() == dog.getName())); System.out.println("两个animal是否相等:" + (cloneDog.getAnimal() == dog.getAnimal())); System.out.println("----------------------------------------"); // 更改原对象的属性值 dog.setAge(3); dog.setName("corgi"); dog.getAnimal().setId(2); System.out.println("dog:" + dog); System.out.println("cloneDog:" + cloneDog); System.out.println("两个对象是否相等:" + (cloneDog == dog)); System.out.println("两个name是否相等:" + (cloneDog.getName() == dog.getName())); System.out.println("两个animal是否相等:" + (cloneDog.getAnimal() == dog.getAnimal())); }
dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 两个对象是否相等:false 两个name是否相等:true 两个animal是否相等:true ---------------------------------------- dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=2, type='dog'}} 两个对象是否相等:false 两个name是否相等:false 两个animal是否相等:true
我们分析下运行结果,重点看一下 “两个name是否相等”,改动后变成 false
.
这是因为String
、Integer
等包装类都是不可变的对象,当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值,然后将原来的引用指向新的地址。
这里dog
对象的name
属性已经指向一个新的对象,而cloneDog
的name
属性仍然指向原来的对象,所以就不同了。
然后我们看下两个对象的animal
属性,原对象属性值变动后,拷贝对象也跟着变动,这就是因为拷贝对象与原对象引用同一个对象。
浅拷贝的 JVM 的内存结构大致如下:
五、深拷贝
与浅拷贝不同之处,深拷贝在对引用数据类型进行拷贝的时候,创建了一个新的对象,并且拷贝其成员变量。也就是说,深拷贝出来的对象,与原对象没有任何关联,是一个新的对象。
实现深拷贝有两种方式
1. 让每个引用类型属性都重写clone()方法
注意: 这里如果引用类型的属性或者层数太多了,代码量会变很大,所以一般不建议使用
@Data @AllArgsConstructor public class Animal implements Cloneable{ private int id; private String type; @Override protected Animal clone () throws CloneNotSupportedException { return (Animal) super.clone(); } @Override public String toString () { return "Animal{" + "id=" + id + ", type='" + type + '\'' + '}'; } }
@Data @AllArgsConstructor public class Dog implements Cloneable{ private int age; private String name; private Animal animal; @Override public Dog clone () throws CloneNotSupportedException { Dog clone = (Dog) super.clone(); clone.animal = animal.clone(); return clone; } @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
我们再次运行浅拷贝部分的main
方法,结果如下。
dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 两个对象是否相等:false 两个name是否相等:true 两个animal是否相等:false # 变为false ---------------------------------------- dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 两个对象是否相等:false 两个name是否相等:false 两个animal是否相等:false # 变为false
2.序列化
序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。我们可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。
@Data @AllArgsConstructor public class Animal implements Serializable { private int id; private String type; @Override public String toString () { return "Animal{" + "id=" + id + ", type='" + type + '\'' + '}'; } }
@Data @AllArgsConstructor public class Dog implements Serializable { private int age; private String name; private Animal animal; @SneakyThrows @Override public Dog clone () { // 序列化 ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(bos); oos.writeObject(this); //反序列化 ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bis); return (Dog) ois.readObject(); } @Override public String toString () { return "Dog{" + "age=" + age + ", name='" + name + '\'' + ", animal=" + animal + '}'; } }
我们再次运行浅拷贝部分的main
方法,结果如下。
dog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 两个对象是否相等:false 两个name是否相等:false # 变为false 两个animal是否相等:false # 变为false ---------------------------------------- dog:Dog{age=3, name='corgi', animal=Animal{id=2, type='dog'}} cloneDog:Dog{age=18, name='husky', animal=Animal{id=1, type='dog'}} 两个对象是否相等:false 两个name是否相等:false 两个animal是否相等:false # 变为false
深拷贝的 JVM 的内存结构大致如下:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南