1.2 Java的深拷贝和浅拷贝

关于Java的深拷贝和浅拷贝,简单来说就是创建一个和已知对象一模一样的对象。可能日常编码过程中用得不多,但是这是一个面试中经常会被问到的问题,而且了解深拷贝和浅拷贝的原理,对于Java中的值传递或者引用传递将会有更深的理解。

1.2.1 创建对象的5种方式

1. 通过new关键字

这是最常用的一种方式,通过new关键字调用类的有参或无参构造方法来创建对象。比如Object obj = new Object()。

2. 通过Class类的newInstance()方法

这种默认是调用类的无参构造方法创建对象。比如Person p2 =(Person)Class. forName("com. ys. test. Person"). newInstance()。

3. 通过Constructor类的newInstance方法

这和第2种方法类似,都是通过反射来实现的。通过java. lang. relect. Constructor类的newInstance()方法指定某个构造器来创建对象。

实际上第2种方法利用Class的newInstance()方法创建对象,其内部调用还是Con-structor的newInstance()方法。

4. 利用Clone方法

Clone是Object类中的一个方法,clone克隆顾名思义就是创建一个一模一样的对象出来。通过对象A. clone()方法会创建一个内容和对象A一模一样的对象B。

5. 反序列化

序列化是把堆内存中的Java对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。而反序列化则是把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

1.2.2 Clone方法

本节介绍Java的深拷贝和浅拷贝,其实现方式正是通过调用Object类的clone()方法来完成。在Object. class类中其源码如下:

这是一个用native关键字修饰的方法,关于native关键字,不理解也没关系,只需要知道用native修饰的方法就是告诉操作系统去实现。具体过程不需要了解,只需要知道clone方法的作用就是复制对象并产生一个新的对象。那么这个新的对象和原对象是什么关系呢?

1.2.3 基本类型和引用类型

这里普及一个概念,在Java中基本类型和引用类型的区别。

在Java中数据类型可以分为两大类:基本类型和引用类型。

基本类型也称为值类型,分别是字符类型char,布尔类型boolean以及数值类型byte、short、int、long、float、double。

引用类型则包括类、接口、数组、枚举等。

Java将内存空间分为堆和栈。基本类型直接在栈中存储数值,而引用类型是将引用放在栈中,实际存储的值是放在堆中,通过栈中的引用指向堆中存放的数据。基本类型和引用类型在JVM存储结构如图1-4所示。

图中定义的a和b都是基本类型,其值是直接存放在栈中的;而c和d是String声明的,这是一个引用类型,引用地址是存放在栈中,然后指向堆的内存空间。

下面d = c;这条语句表示将c的引用赋值给d,那么c和d将指向同一块堆内存空间。

●图1-4 基本类型和引用类型在JVM中的存储结构

1.2.4 浅拷贝

浅拷贝代码如下所示。

这是一个进行赋值的原始类Person。下面产生一个Person对象,并调用其clone方法复制一个新的对象。

注意:

调用对象的clone方法,必须要让类实现Cloneable接口,并且重写clone方法。

测试用例代码如下所示。

打印结果如图1-5所示。

●图1-5 浅拷贝测试方法打印结果

首先看原始类Person实现Cloneable接口,并且重写clone方法,它有三个属性,一个引用类型String定义的pname、一个基本类型int定义的page,还有一个引用类型Address,这是一个自定义类,这个类也包含两个属性provices和city。

接着看测试内容,首先创建一个Person类的对象p1,其pname为zhangsan,page为21,地址类Address的两个属性为湖北省和武汉市。接着调用clone()方法复制另一个对象p2,并打印这两个对象的内容。

从第1行和第3行打印结果:

可以看出这是两个不同的对象。

从第5行和第6行打印的对象内容看,原对象p1和克隆出来的对象p2内容完全相同。

代码中只是更改了克隆对象p2的属性Address为湖北省荆州市(原对象p1是湖北省武汉市),但是从第7行和第8行打印结果来看,原对象p1和克隆对象p2的Address属性都被修改了。

也就是说,对象Person的属性Address经过clone之后,其实只是复制了其引用,它们指向的还是同一块堆内存空间,当修改其中一个对象的属性Address时,另一个也会跟着变化,如图1-6所示。

●图1-6 浅拷贝:引用类型只复制引用

浅拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,如果字段是值类型的,那么对该字段执行复制;如果该字段是引用类型的,则复制引用但不复制引用的对象。因此,原始对象及其副本引用同一个对象。

1.2.5 深拷贝

弄清楚了浅拷贝后,深拷贝就很容易理解了。

深拷贝:创建一个新对象,然后将当前对象的非静态字段复制到该新对象,无论该字段是值类型的还是引用类型,都复制独立的一份。当用户修改其中一个对象的任何内容时,都不会影响另一个对象的内容。如图1-7所示。

●图1-7 深拷贝:所有属性都复制独立一份

1.2.6 如何实现深拷贝

深拷贝就是要让原始对象和克隆之后的对象所具有的引用类型属性不是指向同一块堆内存,这里介绍两种实现思路。

1. 让每个引用类型属性内部都重写clone()方法

既然引用类型不能实现深拷贝,那么将每个引用类型都拆分为基本类型,分别进行浅拷贝。比如上面的例子,Person类有一个引用类型Address(其实String也是引用类型,但是String类型有点特殊,后面会详细讲解),在Address类内部也重写clone方法,代码如下。

Person. class的clone()方法代码如下所示。

测试还是和浅拷贝一样,发现更改了p2对象的Address属性,p1对象的Address属性并没有变化。打印结果如图1-8所示。

●图1-8 深拷贝测试打印结果

但是这种做法有个弊端,这里Person类只有一个Address引用类型,而Address类没有,所以这里只重写Address类的clone方法,但是如果Address类也存在一个引用类型,那么也要重写其clone方法,这样有多少个引用类型,就要重写多少次,如果存在很多引用类型,那么代码量显然会很大,所以这种方法不太合适。

2. 利用序列化

序列化是将对象写到流中便于传输,而反序列化则是把对象从流中读取出来。这里写到流中的对象则是原始对象的一个拷贝,因为原始对象还存在JVM中,所以可以利用对象的序列化产生克隆对象,然后通过反序列化获取这个对象。

注意每个需要序列化的类都要实现Serializable接口,如果有某个属性不需要序列化,可以将其声明为transient,即将其排除在克隆属性之外。代码如下所示。

因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。