Java 拷贝

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<Integer> 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 方法以实现深拷贝;
  • 采用序列化与反序列化的方式来达到类似深拷贝的效果;

简单来说:深拷贝就是完全复制,无论是值类型还是引用类型。

2.1 重写 clone 方法

因为深拷贝必须使每一层的引用对象都被完全拷贝,因此拷贝过程中涉及到的每个引用类型都必须重写它们的 clone 方法(并非简单地继承父类 clone 方法)来实现深拷贝。下面是一个重写的案例:

import java.util.ArrayList;

class ShallowCopyExample implements Cloneable {
    public int num = 0; // 基本类型
    public String string = "shallow copy"; // 引用类型
    public ArrayList<Integer> 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: [](未修改)
    }
}

从上面的代码中我们可以看出,深拷贝的实现并不是很难,但是当一个类中包含非常多不同类型的引用类型,每一个都要重写那将会是一件非常麻烦且繁琐的事情,这个时候,我们就要采取第二种方式了,也就是序列化。

2.2 序列化与反序列化

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<Integer> 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 方法了。 

猜你喜欢

转载自blog.csdn.net/weixin_62651706/article/details/133808076