33

一个工作三年的同事,居然还搞不清深拷贝、浅拷贝...

 3 years ago
source link: https://mp.weixin.qq.com/s/ypCIMGxyp7AX5cxG5UJ1Hg
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
7NNv6jA.png!mobile

对象拷贝在我们日常写代码的时候基本上是刚性需求,经常遇到,只不过很多人天天忙于写业务,忽视了一些细节问题和理解,有时候这方面一旦出了问题,就不太容易排查了。

所以本篇好好梳理一下。

注:本文已收录于Github开源项目: github.com/hansonwang99/JavaCollection ,里面有详细自学编程学习路线、面试题和面经、编程资料及系列技术文章等,资源持续更新中...

值类型 vs 引用类型

这两个概念的准确区分,对于深、浅拷贝问题的理解非常重要。

正如 Java 圣经《 Java 编程思想》第二章的标题所言,在 Java 中一切都可以视为对象!

所以来到 Java 的世界,我们要习惯用引用去操作对象。在 Java 中,像数组、类 Class 、枚举 EnumInteger 包装类等等,就是典型的引用类型,所以操作时一般来说采用的也是 引用传递 的方式;

但是 Java 的语言级基础数据类型,诸如 int 这些基本类型,操作时一般采取的则是 值传递 的方式,所以有时候也称它为值类型。

为了便于下文的讲述和举例,我们这里先定义两个类: StudentMajor ,分别表示「学生」以及「所学的专业」,二者是包含关系:

// 学生的所学专业
public class Major {
private String majorName; // 专业名称
private long majorId; // 专业代号

// ... 其他省略 ...
}
// 学生
public class Student {
private String name; // 姓名
private int age; // 年龄
private Major major; // 所学专业

// ... 其他省略 ...
}
bMRjuia.png!mobile

赋值 vs 浅拷贝 vs 深拷贝

对象赋值

赋值是日常编程过程中最常见的操作,最简单的比如:

Student codeSheep = new Student();
Student codePig = codeSheep;

严格来说,这种不能算是对象拷贝,因为拷贝的仅仅只是引用关系,并没有生成新的实际对象:

zQjqqif.png!mobile

浅拷贝

浅拷贝属于对象克隆方式的一种,重要的特性体现在这个 「浅」 字上。

比如我们试图通过 studen1 实例,拷贝得到 student2 ,如果是浅拷贝这种方式,大致模型可以示意成如下所示的样子:

qQ7J7v2.png!mobile

很明显, 值类型 的字段会复制一份,而 引用类型 的字段拷贝的仅仅是引用地址,而该引用地址指向的实际对象空间其实只有一份。

一图胜前言,我想上面这个图已经表现得很清楚了。

深拷贝

深拷贝相较于上面所示的浅拷贝,除了值类型字段会复制一份,引用类型字段所指向的对象,会在内存中也 创建一个副本 ,就像这个样子:

ZbMFJrQ.png!mobile

原理很清楚明了,下面来看看具体的代码实现吧。

浅拷贝代码实现

还以上文的例子来讲,我想通过 student1 拷贝得到 student2 ,浅拷贝的典型实现方式是:让被复制对象的类实现 Cloneable 接口,并重写 clone() 方法即可。

以上面的 Student 类拷贝为例:

public class Student implements Cloneable {

private String name; // 姓名
private int age; // 年龄
private Major major; // 所学专业

@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}

// ... 其他省略 ...

}

然后我们写个测试代码,一试便知:

public class Test {

public static void main(String[] args) throws CloneNotSupportedException {

Major m = new Major("计算机科学与技术",666666);
Student student1 = new Student( "CodeSheep", 18, m );

// 由 student1 拷贝得到 student2
Student student2 = (Student) student1.clone();

System.out.println( student1 == student2 );
System.out.println( student1 );
System.out.println( student2 );
System.out.println( "\n" );

// 修改student1的值类型字段
student1.setAge( 35 );

// 修改student1的引用类型字段
m.setMajorName( "电子信息工程" );
m.setMajorId( 888888 );

System.out.println( student1 );
System.out.println( student2 );

}
}

运行得到如下结果:

n6N7r2Y.png!mobile

从结果可以看出:

  • student1==student2 打印false,说明 clone() 方法的确克隆出了一个新对象;
  • 修改值类型字段并不影响克隆出来的新对象,符合预期;

  • 而修改了 student1 内部的引用对象,克隆对象 student2 也受到了波及,说明内部还是关联在一起的

深拷贝代码实现

深度遍历式拷贝

虽然 clone() 方法可以完成对象的拷贝工作,但是注意: clone() 方法默认是浅拷贝行为,就像上面的例子一样。若想实现深拷贝需覆写  clone() 方法实现引用对象的深度遍历式拷贝,进行地毯式搜索。

所以对于上面的例子,如果想实现深拷贝,首先需要对更深一层次的引用类 Major 做改造,让其也实现 Cloneable 接口并重写 clone() 方法:

public class Major implements Cloneable {

@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}

// ... 其他省略 ...
}

其次我们还需要在 顶层的 调用类中重写 clone 方法,来调用引用类型字段的 clone() 方法实现深度拷贝,对应到本文那就是 Student 类:

public class Student implements Cloneable {

@Override
public Object clone() throws CloneNotSupportedException {
Student student = (Student) super.clone();
student.major = (Major) major.clone(); // 重要!!!
return student;
}

// ... 其他省略 ...
}

这时候上面的测试用例不变,运行可得结果:

NJVrUvv.png!mobile

很明显,这时候 student1student2 两个对象就完全独立了,不受互相的干扰。

利用反序列化实现深拷贝

记得在前文 《序列化/反序列化,我忍你很久了》 中就已经详细梳理和总结了「序列化和反序列化」这个知识点了。

利用反序列化技术,我们也可以从一个对象深拷贝出另一个复制对象,而且这货在解决多层套娃式的深拷贝问题时效果出奇的好。

所以我们这里改造一下 Student 类,让其 clone() 方法通过序列化和反序列化的方式来生成一个原对象的深拷贝副本:

public class Student implements Serializable {

private String name; // 姓名
private int age; // 年龄
private 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 接口:

public class Major implements Serializable {

// ... 其他省略 ...

}

这时候测试用例完全不变,直接运行,也可以得到如下结果:

FVz2ymV.png!mobile

很明显,这时候 student1student2 两个对象也是完全独立的,不受互相的干扰,深拷贝完成。

后 记

好了,关于「深拷贝」和「浅拷贝」这个问题这次就聊到这里吧。本以为这篇会很快写完,结果又扯出了这么多东西,不过这样一梳理、一串联,感觉还是清晰了不少。

就这样吧,下篇见。

注:本文已收录于Github开源项目: github.com/hansonwang99/JavaCollection ,里面有详细自学编程学习路线、面试题和面经、编程资料及系列技术文章等,资源持续更新中...

每天进步一点点

慢一点才能更快

给个[ 在看 ],是对程序羊最大的支持


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK