浅拷贝&深拷贝
在我们的日常的学习或者开发中基本上必须进行的一个操作就是对象拷贝,但是其中也存在着一些小细节,今天我们就来梳理一下。
1、值类型与引用类型
这两个概念的区分,对于理解深、浅拷贝的理解而言是十分重要的。
引用Java圣经的一句话:
《Java编程思想》:Java中一切都可以视为对象。
所以在学习Java的过程中,我们要习惯用引用去操作对象。
接着我们来说一下引用类型和值类型
-
引用类型:基本的数据类型(四类八种)
-
四类:整型、浮点型、字符型、布尔型
-
八种:整型4种——byte、short、int、long
浮点型2种——float、double
字符型——char
布尔型——boolean
-
-
值类型:数组、类Class、枚举Enum、基本数据类型的包装类Integer等
因此,操作引用类型时一般为引用传递,而操作基本类型时一般都为值传递。
为了便于下文的讲述和举例,我们在这里先定义两个类:Student(学生类)和Major(所学专业类),且二者为包含关系:
package edu.zhku.test04;
public class Major {
private String majorName; //专业名称
private long majorId; //专业代号
/*构造器、get方法、set方法、toString方法都省略*/
}
package edu.zhku.test04;
public class Student {
private String name; //姓名
private int age; //年龄
private Major major; //所学的专业
/*构造器、get方法、set方法省略*/
}
其类的结构图如 图1-1所示。
2、赋值、浅拷贝和深拷贝
2.1、赋值
赋值是日常编程过程最常见的操作,最简单的比如:
package edu.blog.test04;
public class CopyTestDemo01 {
public static void main(String[] args) {
Major major = new Major("IoT", 10086);
System.out.println(major);
Student student01 = new Student("zhangsan", 3, major);
System.out.println(student01);
Student student02 = student01;
System.out.println(student02);
}
}
结果:
edu.blog.test04.Major@1b6d3586
edu.blog.test04.Student@4554617c
edu.blog.test04.Student@4554617c
其实严格来说,这并不是算是对象拷贝,因为拷贝的仅仅是引用关系(即内存地址),并没有生成新的实例化对象。由上面例子的输出也可证明。
其内存模式如 图2-1所示。
2.2、浅拷贝
浅拷贝属于对象克隆方式的一种,重要的特性体现在“浅”这个字上。
例如,我们试图通过student01实例化对象,拷贝得到student02,如果是浅拷贝这种方式,大致的模型可以示意成如下图 2-2。
很明显,值类型的字段会赋值一份,而引用类型的字段拷贝仅仅是引用地址,而该引用地址指向的的实例对象空间其实是同一份(只有一份)。
我想通过图 2-2也已经表达得相当明显了。
2.3、浅拷贝代码的实现
如果我想通过student01拷贝得到student02,浅拷贝的典型方式就是:对象克隆,即让复制对象的类实现Cloneable接口,并重写clone()方法即可。
tips:关于对象克隆,之前有一篇文章相关的文章。
示例:
package edu.blog.test04;
public class Student implements Cloneable{
private String name; //姓名
private int age; //年龄
private Major major; //所学的专业
public Student() {
}
public Student(String name, int age, Major major) {
this.name = name;
this.age = age;
this.major = major;
}
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 Major getMajor() {
return major;
}
public void setMajor(Major major) {
this.major = major;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", major=" + major +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
package edu.blog.test04;
public class CopyTestDemo02 {
public static void main(String[] args) throws CloneNotSupportedException {
Major major = new Major("物联网工程", 1001);
Student student01 = new Student("zhangsan", 18, major);
//由 student01 拷贝得到 student02
Student student02 = (Student) student01.clone();
System.out.println(student01 == student02);
System.out.println("=========================");
System.out.println(student01);
System.out.println(student02);
System.out.println("=========================");
System.out.println();
System.out.println("浅拷贝操作");
//修改 student01 的值类型字段
student01.setAge(30);
//修改 student01 的引用类型字段
major.setMajorName("计算机科学技术");
major.setMajorId(8888);
System.out.println(student01 == student02);
System.out.println("=========================");
System.out.println(student01);
System.out.println(student02);
}
}
结果:
false
=========================
Student{name='zhangsan', age=18, major=Major{majorName='物联网工程', majorId=1001}}
Student{name='zhangsan', age=18, major=Major{majorName='物联网工程', majorId=1001}}
=========================
浅拷贝操作
false
=========================
Student{name='zhangsan', age=30, major=Major{majorName='计算机科学技术', majorId=8888}}
Student{name='zhangsan', age=18, major=Major{majorName='计算机科学技术', majorId=8888}}
从结果可以看出:
- student01==student02打印false,说明clone()方法确实克隆出一个新的对象(内存地址不同);
- 修改值类型字段并不影响克隆出来的新的对象,和预期相符合;
- 修改了student01内部的引用类型,克隆出来的student02也会相应改变。
2.4、深拷贝
深拷贝相比较于上述所展示的浅拷贝,除了值类型字段会赋值一份外,引用类型的字段所指向的对象实例空间,也会在堆内存中创建一个副本,如下图 2-3所示。
2.5、深拷贝代码的实现
2.5.1、深度遍历式拷贝
虽然clone()方法可以完成对象的拷贝工作,但是注意:clone()方法默认的是浅拷贝行为。若想实现深拷贝需要覆写clone()方法实现引用对象的深度遍历式拷贝。
就上面而言,想实现深拷贝,首先需要对更深一层次的引用类Major类做改造,让其也实现Cloneable接口并重写clone()方法:
package edu.blog.test04;
public class Major implements Cloneable{
private String majorName; //专业名称
private long majorId; //专业代号
public Major() {
}
public Major(String majorName, long majorId) {
this.majorName = majorName;
this.majorId = majorId;
}
public String getMajorName() {
return majorName;
}
public void setMajorName(String majorName) {
this.majorName = majorName;
}
public long getMajorId() {
return majorId;
}
public void setMajorId(long majorId) {
this.majorId = majorId;
}
@Override
public String toString() {
return "Major{" +
"majorName='" + majorName + '\'' +
", majorId=" + majorId +
'}';
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
其次我们还需要在顶层的调用类中重写clone()方法,来调用引用类型字段的clone()方法实现深度拷贝,对于此文章来说便是Student类:
package edu.blog.test04;
public class Student implements Cloneable {
private String name; //姓名
private int age; //年龄
private Major major; //所学的专业
public Student() {
}
public Student(String name, int age, Major major) {
this.name = name;
this.age = age;
this.major = major;
}
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 Major getMajor() {
return major;
}
public void setMajor(Major major) {
this.major = major;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", major=" + major +
'}';
}
@Override
public Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.major = (Major) major.clone(); // 这一步很重要!!!
return student;
}
}
测试代码:
package edu.blog.test04;
public class CopyTestDemo03 {
public static void main(String[] args) throws CloneNotSupportedException {
Major major = new Major("通信工程", 1002);
Student student01 = new Student("lisi", 22, major);
//由 student01 拷贝得到 student02
Student student02 = (Student) student01.clone();
System.out.println(student01 == student02);
System.out.println("=========================");
System.out.println(student01);
System.out.println(student02);
System.out.println("=========================");
System.out.println();
System.out.println("深拷贝操作");
//修改 student01 的值类型字段
student01.setAge(35);
//修改 student01 的引用类型字段
major.setMajorName("网络工程");
major.setMajorId(6666);
System.out.println(student01 == student02);
System.out.println("=========================");
System.out.println(student01);
System.out.println(student02);
}
}
结果:
false
=========================
Student{name='lisi', age=22, major=Major{majorName='通信工程', majorId=1002}}
Student{name='lisi', age=22, major=Major{majorName='通信工程', majorId=1002}}
=========================
深拷贝操作
false
=========================
Student{name='lisi', age=35, major=Major{majorName='网络工程', majorId=6666}}
Student{name='lisi', age=22, major=Major{majorName='通信工程', majorId=1002}}
很明显,这时候student01和student2两个对象就完全独立了,不受互相的干扰。
2.5.2、利用反序列化实现深拷贝
之前已经在文章《序列化&反序列化》中就已经详细梳理和总结了「序列化和反序列化」这个知识点了。
利用反序列化技术,我们也可以从一个实例对象深拷贝出另一个复制实例对象,而且利用反序列化可以十分有效地解决多层嵌套式的深拷贝问题。
所以我们这里改造一下Student类,让其clone()方法通过序列化和反序列化的方式来生成一个原对象的深拷贝副本:
package edu.blog.test05;
import java.io.*;
public class Student implements Serializable {
private String name; // 姓名
private int age; // 年龄
private Major major; // 所学专业
public Student() {
}
public Student(String name, int age, Major major) {
this.name = name;
this.age = age;
this.major = major;
}
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 Major getMajor() {
return major;
}
public void setMajor(Major major) {
this.major = major;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", major=" + major +
'}';
}
public Student clone() {
try {
// 将对象本身序列化到字节流
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
ObjectOutputStream objectOutputStream =
new ObjectOutputStream(byteArrayOutputStream);
objectOutputStream.writeObject(this);
// 再将字节流通过反序列化方式得到对象副本
ObjectInputStream objectInputStream =
new ObjectInputStream(new ByteArrayInputStream(byteArrayOutputStream.toByteArray()));
return (Student) objectInputStream.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
当然这种情况下要求被引用的子类(比如这里的Major类)也必须是可以序列化的,即实现了Serializable接口:
package edu.blog.test05;
import java.io.Serializable;
public class Major implements Serializable {
private String majorName; //专业名称
private long majorId; //专业代号
public Major() {
}
public Major(String majorName, long majorId) {
this.majorName = majorName;
this.majorId = majorId;
}
public String getMajorName() {
return majorName;
}
public void setMajorName(String majorName) {
this.majorName = majorName;
}
public long getMajorId() {
return majorId;
}
public void setMajorId(long majorId) {
this.majorId = majorId;
}
@Override
public String toString() {
return "Major{" +
"majorName='" + majorName + '\'' +
", majorId=" + majorId +
'}';
}
}
测试代码:
package edu.blog.test05;
public class CopyTestDemo04 {
public static void main(String[] args) {
Major major = new Major("信息管理与信息系统", 1003);
Student student01 = new Student("wangwu", 22, major);
//由 student01 拷贝得到 student02
Student student02 = (Student) student01.clone();
System.out.println(student01 == student02);
System.out.println("=========================");
System.out.println(student01);
System.out.println(student02);
System.out.println("=========================");
System.out.println();
System.out.println("深拷贝操作");
//修改 student01 的值类型字段
student01.setAge(35);
//修改 student01 的引用类型字段
major.setMajorName("电子信息工程");
major.setMajorId(2333);
System.out.println(student01 == student02);
System.out.println("=========================");
System.out.println(student01);
System.out.println(student02);
}
}
结果:
false
=========================
Student{name='wangwu', age=22, major=Major{majorName='信息管理与信息系统', majorId=1003}}
Student{name='wangwu', age=22, major=Major{majorName='信息管理与信息系统', majorId=1003}}
=========================
深拷贝操作
false
=========================
Student{name='wangwu', age=35, major=Major{majorName='电子信息工程', majorId=2333}}
Student{name='wangwu', age=22, major=Major{majorName='信息管理与信息系统', majorId=1003}}
很明显,这时候student01和student02两个对象也是完全独立的,不受互相的干扰,深拷贝完成。
注:此文章只是学习过程中记录的笔记,如有错误,敬请指正。