Java 中的拷贝分为两种,浅拷贝和深拷贝,关于为什么要有两种拷贝方式而不是一种,就要涉及到 Java 的两种类型数据了。Java 的深浅拷贝都是针对于引用类型而言的,基本类型是没有深浅拷贝之分的,类似于 C++ 语言,浅拷贝中的引用有点像是 C++ 语言中指针。
浅拷贝只会拷贝引用类型的引用,而并不会拷贝整个引用对象的全部内容。也就是说,拷贝过程中,只有引用对象在栈上的引用被复制了一份,其指向的内容并没有被复制,因此当新浅拷贝的对象内容被修改时,原来对象的内容也会被同时修改。
Java 中的浅拷贝默认通过 Object 类的 clone 方法实现:
- import java.util.ArrayList;
-
- class ShallowCopyExample implements Cloneable { // 自定义类必须实现 Cloneable 接口
- public int num = 0; // 基本类型
- public String string = "shallow copy"; // 引用类型
- public ArrayList
arrayList = new ArrayList<>(); // 引用类型 -
- @Override
- protected Object clone() throws CloneNotSupportedException {
- return super.clone(); // 直接用父类的,不做任何修改,为浅拷贝
- }
- }
-
- public class Test {
- public static void main(String[] args) throws CloneNotSupportedException {
- ShallowCopyExample shallowCopyExample = new ShallowCopyExample();
- ShallowCopyExample copiedObject = (ShallowCopyExample) shallowCopyExample.clone(); // 浅拷贝
- copiedObject.num = 1;
- copiedObject.string = "copied"; // String 不可修改,指向一个新的 String 对象
- copiedObject.arrayList.add(1); // 修改 arrayList 本身
- System.out.println(shallowCopyExample.num); // Output: 0(未修改)
- System.out.println(shallowCopyExample.string); // Output: shollow copy(未修改)
- System.out.println(shallowCopyExample.arrayList); // Output: [1](已修改)
- }
- }
从上面的例子可以看出,浅拷贝会将对象的引用类型给同步修改,因为改的是引用对象的内容,并未改变引用本身。但上面的 String 也是引用对象,为何没有被更改呢?实际上面 copiedObject 的 String 类型字段是直接修改其引用,而非引用的内容,因为 String 类型不可更改,看似是引用对象的内容,实则为引用本身。
总结一下:浅拷贝会将对象的全部内容都拷贝过来,但是对于其引用对象的内容,只会拷贝它们的引用,而不会拷贝它们的内容。
这就相当于 C 语言的指针,复制了指针,但指针指向的那片内存空间并没有复制。
以字段的拷贝为例,给个理解图就是:
Java 的浅拷贝实际上和 Python 的拷贝非常相似,Python 的内置类 list 便是如此,浅拷贝相当于一个新的引用,或者说别名。
浅拷贝是 Java 本身就有的,但有些时候我们不想要浅拷贝的这种效果,我们想要彻底完全的拷贝,也就是在拷贝引用对象的引用的同时,将其内容也给拷贝一份,以此来完全除去拷贝对象对原来对象的影响。我们可以用上面类似浅拷贝的理解图,以字段的拷贝为例来展现深拷贝:
要达到这种效果,就必须实现深拷贝,也就是彻底的拷贝,但是很遗憾,Java 并没有像 Object 对象的 clone 方法那么方便的工具来实现深拷贝,想要实现深拷贝,可以采用的方法有:
简单来说:深拷贝就是完全复制,无论是值类型还是引用类型。
因为深拷贝必须使每一层的引用对象都被完全拷贝,因此拷贝过程中涉及到的每个引用类型都必须重写它们的 clone 方法(并非简单地继承父类 clone 方法)来实现深拷贝。下面是一个重写的案例:
- import java.util.ArrayList;
-
- class ShallowCopyExample implements Cloneable {
- public int num = 0; // 基本类型
- public String string = "shallow copy"; // 引用类型
- public ArrayList
arrayList = new ArrayList<>(); // 引用类型 -
- @Override
- protected Object clone() throws CloneNotSupportedException {
- ShallowCopyExample shallowCopyExample = (ShallowCopyExample) super.clone();
- shallowCopyExample.arrayList = new ArrayList<>(arrayList); // 手动将 arrayList 复制一份
- return shallowCopyExample;
- }
- }
-
- public class Test {
- public static void main(String[] args) throws CloneNotSupportedException {
- ShallowCopyExample shallowCopyExample = new ShallowCopyExample();
- ShallowCopyExample copiedObject = (ShallowCopyExample) shallowCopyExample.clone();
- copiedObject.num = 1;
- copiedObject.string = "copied"; // String 不可修改,指向一个新的 String 对象
- copiedObject.arrayList.add(1); // 修改 arrayList 本身
- System.out.println(shallowCopyExample.num); // Output: 0(未修改)
- System.out.println(shallowCopyExample.string); // Output: shollow copy(未修改)
- System.out.println(shallowCopyExample.arrayList); // Output: [](未修改)
- }
- }
从上面的代码中我们可以看出,深拷贝的实现并不是很难,但是当一个类中包含非常多不同类型的引用类型,每一个都要重写那将会是一件非常麻烦且繁琐的事情,这个时候,我们就要采取第二种方式了,也就是序列化。
Java 序列化是一种将对象转换为字节流的过程,以便可以将对象保存到磁盘上,将其传输到网络上,或者将其存储在内存中,以后再进行反序列化,将字节流重新转换为对象。注意每个需要序列化的类都要实现 Serializable 接口。
下面是一个序列化与反序列化的案例:
- import java.io.*;
- import java.util.ArrayList;
-
- class ShallowCopyExample implements Serializable { // 序列化对象必须实现 Serializable 接口
- public int num = 0; // 基本类型
- public String string = "deep copy"; // 引用类型
- public ArrayList
arrayList = new ArrayList<>(); // 引用类型 -
- public ShallowCopyExample deepClone() {
- // 序列化
- try (
- FileOutputStream fileOutputStream = new FileOutputStream("object.ser"); // 对象文件为 object.ser
- ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream);
- ) {
- objectOutputStream.writeObject(this);
- } catch(IOException ioException) {
- ioException.printStackTrace();
- }
-
- // 反序列化
- try (
- FileInputStream fileInputStream = new FileInputStream("object.ser");
- ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream);
- ) {
- return (ShallowCopyExample) objectInputStream.readObject();
- } catch (IOException ioException) {
- ioException.printStackTrace();
- } catch (ClassNotFoundException e) {
- throw new RuntimeException(e);
- }
-
- // 失败返回 null
- return null;
- }
- }
-
- public class Test {
- public static void main(String[] args) throws CloneNotSupportedException {
- ShallowCopyExample shallowCopyExample = new ShallowCopyExample();
- ShallowCopyExample copiedObject = shallowCopyExample.deepClone(); // 改用自己定义的,用序列化实现的深拷贝方法
- copiedObject.num = 1;
- copiedObject.string = "copied"; // String 不可修改,指向一个新的 String 对象
- copiedObject.arrayList.add(1); // 修改 arrayList 本身
- System.out.println(shallowCopyExample.num); // Output: 0(未修改)
- System.out.println(shallowCopyExample.string); // Output: deep copy(未修改)
- System.out.println(shallowCopyExample.arrayList); // Output: [](未修改)
- }
- }
因为序列化产生的是两个完全独立的对象,所有无论嵌套多少个引用类型,序列化都是能实现深拷贝的。用下面一张图可以很清晰地了解这一过程:
序列化这一方法不仅仅在 Java 中有,在其他编程语言中同样拥有。实际上,序列化的主要作用并非深拷贝,而是用于将对象持久化,从而用于网络传输等。Python 中的内置模块 pickle 就是序列化相关的模块,然而 Python 中的深拷贝却无需使用 pickle 模块来实现,因为其内置模块 copy 中有一个 deepcopy 深拷贝函数,可以非常方便地得到大部分对象的深拷贝,希望有朝一日 Java 也能拥有如此方便的内置工具,而不再需要手动重写 clone 方法了。