Java中的深拷贝和浅拷贝
在之前做项目的时候,同组的人因为深拷贝和浅拷贝的问题产生过BUG,当时不是自己负责的部分,没对这个问题做过深入的了解,今天深入学习一下????
在介绍深拷贝和浅拷贝之前,先温习几个概念。
数据类型
Java中的数据类型有两种:
- 基本类型:粒度最小的数据类型。主要包括
- 4种整数类型:int、short、long、byte
- 2种浮点数类型:float、double
- 1种字符类型:char
- 1种布尔类型:boolean
- 引用类型:也叫做句柄,实际存放的应该是一个地址,地址指向实际内容。主要包括:
- 类
- 接口
- 数组
数据如何在内存中储存
JVM在程序运行的时候会将内存划分为四个区域:
- 堆
- 栈
- 静态方法区
- 常量区
针对第一节的数据类型,下面分为基本数据类型的局部变量、基本数据类型的成员变量、引用类型变量来讲解。
基本数据类型的局部变量
基本数据类型的局部变量的储存形式如下图所示:
方法中的基本类型的局部变量,都是储存虚拟机的堆内存中的,数据本身的值就是储存在栈内存中。每次给一个新变量赋值的时候,JVM首先会检验内存中有无该数据,如果有,就直接将这个变量指向该数据,没有的话就开辟一个新的内存空间存放数据,然后将变量指向该数据。
int age = 50;
int weight = 50;
int height = 100;
看上面的代码段,三个变量都是某个方法中的局部变量,当执行int age = 50的时候,首先会定义变量,存入变量表中,然后去栈中找是否存在值为50的内容,并将age变量指向50。当执行int weight = 50时,会在栈中找到50,直接将weight指向50。当方法执行完毕以后,方法局部变量会被JVM的垃圾回收机制回收。
基本数据类型的成员变量和引用类型变量
前面提到,除了基本数据类型,还有一种是引用数据类型,对于引用数据类型在内存中的存储形式如下图所示。
public class Student {
private String name;
private int age;
private int grade;
}
上面是一个Student类,如果我们不重写toString方法,直接输出一个实例化的对象的话,会得到这样的输出:
aaa@qq.com
,其实这就是Student类的地址。
所以,对于引用数据类型来说,引用数据类型只储存一个在堆内存中的地址,而成员变量和方法被储存在堆内存中。
深拷贝和浅拷贝
概念
前面说了这么多,现在步入正题,首先介绍深拷贝和浅拷贝的概念。
在Java中使用 “=” 做赋值操作时,对于基本数据类型,会直接将值直接赋值给变量,而对于引用数据类型来说,会将其引用赋值给另一个对象。
- 浅拷贝
浅拷贝会创建一个新的对象,这个对象拥有被拷贝对象的一个精确的副本。在该类的成员变量中,对于基本数据类型,拷贝的就是其值;对于引用数据类型,拷贝的就是其内存地址。如果其中一个对象改变了这个引用数据类型的变量的数据,相应地,另一个对象地该引用数据类型地数据也会随之改变。
如下图,Student类有两个成员变量,一个Schoole类成员变量,一个int类型的成员变量,Student类有两两个实例,student1是对student0的浅拷贝的对象。对于引用类型的School来说,两个对象都储存的是School对象的地址,所以说,如果有其中一个对象的School对象的内容发生了改变,另一个对象也会发生改变;对于基本类型的成员变量来说,两个对象都是直接储存的age的值,双方互不干涉。
- 深拷贝
深拷贝不仅会为拷贝新建对象,也会对该对象中任何的引用类型变量建立新的对象,这样,拷贝对象和原对象就是两个完全相同的对象,两者各自的成员变量也互不受影响了。
实现浅拷贝
先看一个例子(不是浅拷贝)。
public class Tester {
public static void main(String[] args) {
int age = 10;
int age1 = 20;
String name = "aaa";
String name1 = "bbb";
Student student = new Student();
student.setAge(age);
student.setName(name);
Student student1 = student;
student.setAge(age1);
student.setName(name1);
System.out.println(student);
System.out.println(student1);
}
}
----Output----
Student{age=20, name='bbb'}
Student{age=20, name='bbb'}
上面代码直接使用了 “=” 对对象进行拷贝,发现,无论是基本类型还是引用类型的数据,只要其中一个对象的值被改变,另一个对象的数据也随之改变。
其实Student student1 = student;
这一句代码,相当于只是将student的地址赋值给了student1,两个对象指向一个地址,所有数据都是一起共享的。
而实现浅拷贝之后,创建的新对象会单独开辟一个内存空间有自己的地址,这样对于基本类型的数据来说,每个对象都有自己的数据,而对于引用类型的数据来说,新对象是拷贝的引用类型的地址,所以两个对象是共享该成员变量的。
public class School implements Cloneable {
String name;
public School(String name) {
this.name = name;
}
...
@Override
public String toString() {
return "School{" +
"name='" + name + '\'' +
'}';
}
}
public class Student implements Cloneable{
private int age;
private School school;
public Student(int age, School school) {
this.age = age;
this.school = school;
}
...
@Override
public String toString() {
return "Student{" +
"age=" + age +
", school=" + school +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Tester {
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student(10, new School("QUST"));
Student student1 = (Student) student.clone();
student.setAge(20);
student.getSchool().setName("THU");
System.out.println(student);
System.out.println(student1);
}
}
----Output----
Student{age=20, school=School{name='THU'}}
Student{age=10, school=School{name='THU'}}
以上就是一个实现浅拷贝的例子,首先,需要拷贝的类要实现Cloneable接口(此接口是个标识性接口,接口内无任何方法),重写clone方法,在拷贝对象的时候,使用Student student = (Student) student.clone()
,这样就实现了浅拷贝。从测试类的输出可以看出来,修改原对象的基本数据类型的成员变量时,拷贝对象并没有收到影响,而修改原对象引用类型成员变量School的属性时,拷贝类型也受到了影响。
实现深拷贝
public class School implements Cloneable{
String name;
public School(String name) {
this.name = name;
}
@Override
public String toString() {
return "School{" +
"name='" + name + '\'' +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Student implements Cloneable{
private int age;
private School school;
public Student(int age, School school) {
this.age = age;
this.school = school;
}
@Override
public String toString() {
return "Student{" +
"age=" + age +
", school=" + school +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.school = (School) this.school.clone();
return student;
}
}
public class Tester {
public static void main(String[] args) throws CloneNotSupportedException {
Student student = new Student(10, new School("QUST"));
Student student1 = (Student) student.clone();
student.setAge(20);
student.getSchool().setName("THU");
System.out.println(student);
System.out.println(student1);
}
}
----Output----
Student{age=20, school=School{name='THU'}}
Student{age=10, school=School{name='QUST'}}
实现深拷贝的方法就是将类中的引用类型变量也进行拷贝,引用类型所在的类实现Cloneable接口,重写clone方法。这样就可以实现深拷贝了。
还有一种实现深拷贝的方式,就是通过序列化实现可以查看之前的关于序列化的文章。
总结
- 需要拷贝对象的时候,如果一个对象内部数据类型只有基本类型,那么就可以使用浅拷贝,如果一个对象内部还有引用数据类型,就需要使用深拷贝。
- 虽然深拷贝和浅拷贝可能用到的不会很多,但是对于理解JVM内部内存还是很有帮助的????
上一篇: PTA - 使用函数输出指定范围内的完数
下一篇: Java模拟QQ桌面截图功能实现方法