Java问题解读系列之IO相关---Java深拷贝和浅拷贝

时间:2023-03-08 17:10:50

前几天和棒棒童鞋讨论Java(TA学的是C++)的时候,他提到一个浅拷贝和深拷贝的问题,当时的我一脸懵圈,感觉自己学Java居然不知道这个知识点,于是今天研究了一番Java中的浅拷贝和深拷贝,下面来做一下总结:

一、定义

调研过程中发现普遍的解释如下:

Java问题解读系列之IO相关---Java深拷贝和浅拷贝

我在用代码实战之后总结出的定义是:

浅拷贝,就是创建了一个新对象,这个新对象以及新对象中的基本类型都被分配了新的存储空间,从此与原对象毫无瓜葛;但是原对象中的引用类型没有被分配新的空间,新对象和原对象都指向同一片内存,任何一方的改变都会影响到对方。

深拷贝,也是创建了一个新的对象,不过这个新对象以及新对象中的所有属性都被分配了新的空间,从此彻底与原对象毫无关联,任何一方的改变都不会影响到对方。

用一个很生活化的例子来解释,就好比张三本来是和李四是男女朋友关系,但是后来张三和李四分手了,找了一个新的对象王五,这个王五就相当于是李四的拷贝

浅拷贝就是李四和王五都有自己的姓名、年龄、职业,且互不影响,但是张三和李四还有一个共同的qq号,且他们分手之后李四把这个qq号告诉了王五,并且这时李四和王五都能使用该号,这时李四和王五任何一方在该号中的操作对对方都是可见的,而且他们看到的所有变化都是一样的,比如修改qq昵称,签名等;

深拷贝就是张三和李四分手后,他也给了王五一个qq号,不过这个qq号是一个新申请的,是他和王五专用的,这时李四和王五的姓名、年龄、职业以及他们和张三的共用qq号都是不同的,任何一方的改变都不会影响到对方。

这里的张三,就是用来起到对李四和王五开辟空间的作用。

二、理解

下面我用代码一步一步来解释浅拷贝和深拷贝的实现:

1、先来看看普通的赋值操作

第一步:创建一个普通的Student类

/**
* 创建一个学生对象
*
* @createtime 2017年3月21日 上午10:15:20
* @description
*/
public class Student {
String name; int age; String gender; public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} public String getGender() {
return gender;
} public void setGender(String gender) {
this.gender = gender;
}
}

第二步:创建两个对象,将第一个赋值给第二个,然后比较他们是否相等

/**
* 普通的赋值
* @createtime 2017年3月21日 下午2:15:49
* @description
*/
public class GeneralCopy {
public static void main(String[] args) {
/**
* 普通对象的赋值
* 结论:
* 1、将对象a赋值给对象b时,对象a和对象b都指向同一片内存地址
* 2、任何一个对象的改变都会引起另一个对象的改变
* 3、普通的对象赋值就是浅拷贝
*/
System.out.println("---------普通的对象赋值---------");
Student a = new Student("zhangsan", 20, "man");
Student b = a;
b.name = "lisi";
System.out.println(a==b);//true
System.out.println(a.name);//lisi
a.name = "zhangsan1";
System.out.println(a==b);//true
System.out.println(b.name);//zhangsan1
}
}

从代码可以看出,当把a赋值给b时,a和b是相等的,即它们指向同一片内存空间,所以a的改变会引起b的改变;b的改变也会引起a的改变,由此看来普通的赋值应该是个浅拷贝

2、使用克隆的方式,用一个对象sca克隆另一个对象scb

第一步:先创建一个实现Cloneable接口的类,并且该类重写了clone方法

/**
* @createtime 2017年3月21日 下午2:25:17
* @description 实现了cloneable接口
*/
public class StudentClone implements Cloneable{
public String name;
public int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public StudentClone(String name, int age) {
this.name = name;
this.age = age;
} /**
* 重写clone方法
*/
public Object clone(){
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
} }
}

第二步:使用clone方法用sca克隆一个scb的实例,并比较他们是否相同:

/**
* 普通的赋值
* @createtime 2017年3月21日 下午2:15:49
* @description
*/
public class GeneralCopy {
public static void main(String[] args) { /**
* 对象的拷贝
* 结论:
* 1、拷贝对象时新建了一个实例
* 2、任何一个对象的改变都不会引起另一个对象的改变
* 3、但这不能说明克隆就是深拷贝
* 4、无引用变量时克隆为深拷贝
*/
System.out.println("---------无引用变量的克隆---------");
StudentClone sca = new StudentClone("zhangsan",20);
StudentClone scb = (StudentClone) sca.clone();
scb.name = "lisi";
System.out.println(sca==scb);//false
System.out.println(sca.name);//zhangsan
sca.name = "zhangsan1";
System.out.println(sca==scb);//false
System.out.println(scb.name);//lisi
}
}

以上代码做了四件事:
(1)创建一个StudentClone对象的实例sca

(2)用sca克隆一个新的实例scb

(3)先改变克隆实例scb的name属性值,比较这两个实例是否相等以及sca的name属性值是否被改变

(4)再改变原实例sca的name属性值,比较这两个实例是否相等以及scb的name属性值是否被改变

根据测试结果可以看出:

克隆原对象时,创建了一个新的实例,病为之分配了新的空间,原实例和克隆实例指向的是不同的内存空间,所以他们不相等;

改变任何一个实例的基本类型属性值,都不会改变原实例或克隆实例的该属性;

由此可以得出如同代码注释中的结论:

即没有引用类型的变量时,克隆就是深拷贝

3、含有引用变量的拷贝,引用变量没实现Cloneable接口

第一步:创建一个普通的Student类,不实现Cloneable接口(就是上面的Student类)

/**
* 创建一个学生对象
*
* @createtime 2017年3月21日 上午10:15:20
* @description
*/
public class Student {
String name; int age; String gender; public Student(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
} public String getGender() {
return gender;
} public void setGender(String gender) {
this.gender = gender;
}
}

第二步:创建一个AddAttrStudent类,实现Cloneable接口,并且添加Student引用类型的变量

/**
* @createtime 2017年3月21日 下午2:41:26
* @description 含有一个未实现cloneable接口的引用变量
*/
public class AddAttrStudent implements Cloneable{
public String name; public Student student;//引用变量 public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Student getStudent() {
return student;
}
public void setStudent(Student student) {
this.student = student;
}
public AddAttrStudent(String name, Student student) {
this.name = name;
this.student = student;
}
/**
* 实现Cloneable接口,重写clone方法
*/
public Object clone(){
try {
return super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
}
}
}

第三步:创建一个AddAttrStudent实例,并克隆一个新实例,然后分情况比较

/**
* 普通的赋值
* @createtime 2017年3月21日 下午2:15:49
* @description
*/
public class GeneralCopy {
public static void main(String[] args) {
/**
* 含有引用变量的拷贝(引用变量没实现cloneable接口)
* 结论:
* 1、拷贝对象建立了一个实例
* 2、其中一个对象的普通变量改变时不会引起另一个对象普通变量的改变
* 3、其中一个对象的引用变量的改变引起另一个对象引用变量的改变
* 4、有引用变量且引用变量未实现cloeable接口时克隆为浅拷贝
*/
System.out.println("---------有引用变量的克隆---------");
Student stu = new Student("张三",20,"男");
AddAttrStudent aas1 = new AddAttrStudent("克隆1",stu);
AddAttrStudent aas2 = (AddAttrStudent) aas1.clone();
System.out.println(aas1==aas2);//false
System.out.println(aas1.name);//克隆1
System.out.println(aas2.name);//克隆1
System.out.println(aas1.student == aas2.student);//true
System.out.println(aas1.student.name);//张三
System.out.println(aas2.student.name);//张三
System.out.println("------改变原对象-------");
aas1.name ="克隆2";
aas1.student.name = "张三1";
System.out.println(aas1==aas2);//false
System.out.println(aas1.name);//克隆2
System.out.println(aas2.name);//克隆1
System.out.println(aas1.student == aas2.student);//true
System.out.println(aas1.student.name);//张三1
System.out.println(aas2.student.name);//张三1
System.out.println("------改变拷贝对象-------");
aas2.name ="克隆";
aas2.student.name = "李四";
System.out.println(aas1==aas2);//false
System.out.println(aas1.name);//克隆2
System.out.println(aas2.name);//克隆
System.out.println(aas1.student == aas2.student);//true
System.out.println(aas1.student.name);//李四
System.out.println(aas2.student.name);//李四 }
}

以上代码有三个比较过程

第一个:用原实例克隆一个新实例时,克隆实例除了内存地址外,其他的都跟原实例完全相同

第二个:改变原实例对象的name属性和其引用变量student的name属性,此时除了两个实例的内存地址和name属性值之外,其他方面两个实例保持一致,包括被改变的引用变量student,其内存地址和name属性值;

第三个:改变克隆实例对象的name属性和其引用变量student的name属性,此时除了两个实例的内存地址和name属性值之外,其他方面两个实例保持一致,包括被改变的引用变量student,其内存地址和name属性值;

由以上结论可以看出:

原实例和克隆实例,基本类型的name属性值和各自的内存空间地址保持各自的值不受对方影响;引用类型的属性和内存空间都会受对方的影响,并和对方保持一致,所以可以得出如下结论:

有引用变量且引用变量未实现Cloeable接口时克隆为浅拷贝

4、含有引用变量的拷贝,且引用变量也实现了cloneable接口

这个例子与上面的唯一区别就是引用变量也实现了Cloneable接口,并且重写了clone方法

第一步:创建一个StudentClone类,实现Cloneable接口,重写clone方法

/**
* @createtime 2017年3月21日 下午2:25:17
* @description 实现了cloneable接口
*/
public class StudentClone implements Cloneable{
public String name;
public int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public StudentClone(String name, int age) {
this.name = name;
this.age = age;
} /**
* 重写clone方法
*/
public Object clone(){
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
}

第二步:创建一个AddAttrStudentClone类,实现Cloneable接口,添加StudentClone类的引用变量studentClone,且调用studentClone的clone方法

/**
* @createtime 2017年3月21日 下午2:57:42
* @description 含有实现了cloneable接口的引用对象StudentClone
*/
public class AddAttrStudentClone implements Cloneable{
public String name;
public StudentClone studentClone; public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
} public StudentClone getStudentClone() {
return studentClone;
}
public void setStudentClone(StudentClone studentClone) {
this.studentClone = studentClone;
} public AddAttrStudentClone(String name, StudentClone studentClone) {
this.name = name;
this.studentClone = studentClone;
} /**
* 重写clone方法,同时调用引用对象的clone方法
*/
public Object clone(){
try {
AddAttrStudentClone aasc = (AddAttrStudentClone) super.clone();
aasc.studentClone = (StudentClone) studentClone.clone();
return aasc;
} catch (CloneNotSupportedException e) {
e.printStackTrace();
return null;
} }
}

第三步:创建实例,克隆实例,分情况比较

/**
* @createtime 2017年3月21日 下午2:15:49
* @description
*/
public class GeneralCopy {
public static void main(String[] args) {
/**
* 含有引用变量的拷贝(引用变量也实现了cloneable接口)
* 结论:
* 1、拷贝对象创建了一个新的实例
* 2、其中一个对象的任何变量改变时都不会引起另一个对象的变量改变
* 3、拷贝的对象和源对象之间没有任何关系,它们是两个完全独立的个体,即深拷贝
* 4、想要实现深拷贝,在重写clone时还有拷贝它里面的引用变量
* 缺点:这种实现深拷贝的方式要让所有的引用变量都实现cloneable接口,并且要递归地clone所有引用变量,比较复杂
* 解决方法:序列化--反序列化
*/
System.out.println("---------有实现cloneabe接口的引用变量的克隆---------");
StudentClone sc = new StudentClone("张三", 20);
AddAttrStudentClone aasc1 = new AddAttrStudentClone("克隆1", sc);
//克隆对象
AddAttrStudentClone aasc2 = (AddAttrStudentClone) aasc1.clone(); System.out.println(aasc1==aasc2);//false
System.out.println(aasc1.name);//克隆1
System.out.println(aasc2.name);//克隆1
System.out.println(aasc1.studentClone == aasc2.studentClone);//false
System.out.println(aasc1.studentClone.name);//张三
System.out.println(aasc2.studentClone.name);//张三
//改变克隆对象
aasc2.name = "克隆2";
aasc2.studentClone.name = "李四";
System.out.println(aasc1==aasc2);//false
System.out.println(aasc1.name);//克隆1
System.out.println(aasc2.name);//克隆2
System.out.println(aasc1.studentClone == aasc2.studentClone);//false
System.out.println(aasc1.studentClone.name);//张三
System.out.println(aasc2.studentClone.name);//李四 //改变原对象
aasc1.name ="克隆3";
aasc1.studentClone.name = "张三2";
System.out.println(aasc1==aasc2);//false
System.out.println(aasc1.name);//克隆3
System.out.println(aasc2.name);//克隆2
System.out.println(aasc1.studentClone == aasc2.studentClone);//false
System.out.println(aasc1.studentClone.name);//张三2
System.out.println(aasc2.studentClone.name);//李四 }
}

以上代码有三个比较过程

第一个:用原实例克隆一个新实例时,克隆实例除了内存地址外,其他的都跟原实例完全相同;

第二个:改变原实例对象的name属性和其引用变量StudentClone的name属性,此时两个实例的内存地址、基本类型变量、引用类型变量的内存空间地址,引用类型变量的name属性都保持各自的值,不随对方的改变而改变;

第三个:改变原实例对象的name属性和其引用变量StudentClone的name属性,此时两个实例的内存地址、基本类型变量、引用类型变量的内存空间地址,引用类型变量的name属性都保持各自的值,不随对方的改变而改变;

通过比较结果可以看出,无论原实例和克隆实例怎样改变,它们都各自保持自己的值,不受对方的影响,实现了深拷贝,所以得出如下结论:

要用克隆实现深拷贝,则所有类都要实现Cloneable接口,并重写clone方法,并且在包含引用类型变量的类中,要调用其重写的clone方法

根据上面的结论来看,要用克隆方法实现深拷贝很繁琐,当引用变量的包含很深时,就要让所有类型都实现Cloneable接口,且重写clone方法,这显然是一件很繁琐的工作,所以要采取更简单的方法来实现深拷贝,Java中解决深拷贝问题最好的方式就是采用序列化方式,这样各种类均不用实现Cloneable接口的,直接序列化反序列化就可以了,下面我们来实现这个方法.

5、用序列化实现深拷贝

第一步:创建StaffNoSerial类,实现Serializable接口

/**
* @createtime 2017年3月21日 下午3:35:26
* @description 被序列化的员工类
*/
public class StaffNoSerial implements Serializable{
public String name;
public int age; public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
} public StaffNoSerial(String name, int age) {
this.name = name;
this.age = age;
} }

第二步:创建一个部门类DepartmentSeria,实现Serializable接口,包含引用变量staffNoSerial

/**
* @createtime 2017年3月21日 下午3:34:24
* @description 被序列化的部门类
*/
public class DepartmentSeria implements Serializable{
public String name;
public StaffNoSerial staffNoSerial; public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public StaffNoSerial getStaffNoSerial() {
return staffNoSerial;
}
public void setStaffNoSerial(StaffNoSerial staffNoSerial) {
this.staffNoSerial = staffNoSerial;
}
public DepartmentSeria(String name, StaffNoSerial staffNoSerial) {
this.name = name;
this.staffNoSerial = staffNoSerial;
} }

第三步:创建一个公司类CompanySerial,实现Serializable接口,包含引用变量departmentNoSerial

/**
* @createtime 2017年3月21日 下午3:33:26
* @description 被序列化的公司类
*/
public class CompanySerial implements Serializable{
public String name;
public DepartmentSeria departmentNoSerial;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public DepartmentSeria getDepartmentNoSerial() {
return departmentNoSerial;
}
public void setDepartmentNoSerial(DepartmentSeria departmentNoSerial) {
this.departmentNoSerial = departmentNoSerial;
} public CompanySerial(String name, DepartmentSeria departmentNoSerial) {
this.name = name;
this.departmentNoSerial = departmentNoSerial;
} }

第四步:序列化实现深拷贝,并分情况比较

/**
* @createtime 2017年3月21日 下午3:38:33
* @description 序列化实现深度拷贝
*/
public class DeepCopySerial { public static void deepCopy() throws Exception{
StaffNoSerial staff = new StaffNoSerial("张三",20); DepartmentSeria department = new DepartmentSeria("研发类", staff); CompanySerial company = new CompanySerial("阿里巴巴", department); CompanySerial companyCopy = null;
/**
* 第一种:文件输入输出流方式
*/
FileOutputStream fos = new FileOutputStream(new File("F:/deep.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(company); FileInputStream fis = new FileInputStream(new File("F:/deep.txt"));
ObjectInputStream ois = new ObjectInputStream(fis);
companyCopy = (CompanySerial) ois.readObject(); /**
* 第二种:字节数组输入输出流方式
*/
// ByteArrayOutputStream baos = new ByteArrayOutputStream();
// ObjectOutputStream oos1 = new ObjectOutputStream(baos);
// oos1.writeObject(company);
//
// ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
// ObjectInputStream ois1 = new ObjectInputStream(bais);
// companyCopy = (CompanySerial) ois1.readObject(); //沒做修改的情況下
System.out.println(company==companyCopy);//false
System.out.println(company.name);//阿里巴巴
System.out.println(companyCopy.name);//阿里巴巴
System.out.println(company.departmentNoSerial == companyCopy.departmentNoSerial);//false
System.out.println(company.departmentNoSerial.name);//研发类
System.out.println(companyCopy.departmentNoSerial.name);//研发类
System.out.println(company.departmentNoSerial.staffNoSerial == companyCopy.departmentNoSerial.staffNoSerial);//false
System.out.println(company.departmentNoSerial.staffNoSerial.name);//张三
System.out.println(companyCopy.departmentNoSerial.staffNoSerial.name);//张三 System.out.println("\n修改原對象\n");
company.name = "拷贝原阿里巴巴";
company.departmentNoSerial.name = "拷贝原研发类";
company.departmentNoSerial.staffNoSerial.name = "拷贝原张三";
System.out.println(company==companyCopy);//false
System.out.println(company.name);//拷贝原阿里巴巴
System.out.println(companyCopy.name);//阿里巴巴
System.out.println(company.departmentNoSerial == companyCopy.departmentNoSerial);//false
System.out.println(company.departmentNoSerial.name);//拷贝原研发类
System.out.println(companyCopy.departmentNoSerial.name);//研发类
System.out.println(company.departmentNoSerial.staffNoSerial == companyCopy.departmentNoSerial.staffNoSerial);//false
System.out.println(company.departmentNoSerial.staffNoSerial.name);//拷贝原张三
System.out.println(companyCopy.departmentNoSerial.staffNoSerial.name);//张三 System.out.println("\n修改拷贝對象\n");
companyCopy.name = "腾讯";
companyCopy.departmentNoSerial.name = "财务类";
companyCopy.departmentNoSerial.staffNoSerial.name = "李四";
System.out.println(company==companyCopy);//false
System.out.println(company.name);//拷贝原阿里巴巴
System.out.println(companyCopy.name);//腾讯
System.out.println(company.departmentNoSerial == companyCopy.departmentNoSerial);//false
System.out.println(company.departmentNoSerial.name);//拷贝原研发类
System.out.println(companyCopy.departmentNoSerial.name);//财务类
System.out.println(company.departmentNoSerial.staffNoSerial == companyCopy.departmentNoSerial.staffNoSerial);//false
System.out.println(company.departmentNoSerial.staffNoSerial.name);//拷贝原张三
System.out.println(companyCopy.departmentNoSerial.staffNoSerial.name);//李四 }
}

第五步:运行例子,分析比较结果

/**
* @createtime 2017年3月21日 下午3:57:36
* @description 测试深拷贝
*/
public class DeepCopy {
public static void main(String[] args) {
try {
DeepCopySerial.deepCopy();
} catch (Exception e) {
e.printStackTrace();
}
}
}

通过比较可以看出,效果与4中的深拷贝是一样的,原实例和拷贝实例是两个完全不同的实例,它们有各自的变量,不管是基本类型还是引用类型,都不受对方的约束,想怎么变就怎么变。

第四步代码中有两种用流实现的方式,一种是文件流,另一种是字节数组流方式,两种都可以用来实现深拷贝。

以上就是本次我对Java中深拷贝和浅拷贝的全部理解以及实践,下面是我参考的一篇文章:

http://www.jb51.net/article/88916.htm

本集完   2017-3-21 21:46