深拷贝和浅拷贝
yuuki Lv5

大多数对象的设计目标是提供由一个整体块制成的相似性,尽管大多数对象并非如此。由于对象由几个不同的部分组成,因此复制变得非常重要。有几种策略可以解决这个问题。

考虑一个对象 A,它包含字段 xi(更具体地说,考虑 A 是否是一个字符串,而 x 是它的字符数组)。制作 A 的副本有不同的策略,称为浅拷贝深拷贝。许多语言允许通过一种或两种策略进行通用复制,定义一个复制操作或单独的浅复制深复制操作。请注意,更浅的方法是使用对现有对象 A 的引用,在这种情况下没有新对象,只有新引用。

浅拷贝

复制对象的一种方法是浅拷贝。在这种情况下,将创建一个新对象 B ,并将 A 的字段值复制到 B。这也称为*逐字段复制(field-by-field)*, 字段对字段副本(field-for-field copy),或字段复制(field copy)。如果字段值是对对象的引用(例如,内存地址),则它复制该引用,因此引用与 A 相同的对象,如果字段值是原始类型,则复制原始类型的值。在没有原始类型的语言中(一切都是对象),副本 B 的所有字段都是对与原始 A 的字段相同的对象的引用。因此,引用的对象是共享的,因此如果修改了这些对象之一(来自 A或 B),更改在另一个中可见。浅拷贝很简单而且通常很便宜,因为它们通常可以通过简单地精确复制位来实现。

深拷贝

另一种方法是深拷贝,这意味着字段被取消引用:不是对正在复制的对象的引用,而是为任何被引用的对象创建新的副本对象,并将对这些对象的引用放在 B 中。结果与浅拷贝给出的结果不同因为副本 B 引用的对象与 A 引用的对象不同,并且是独立的。由于需要创建额外的对象,深拷贝更昂贵,并且由于引用可能形成复杂的图,因此可能更加复杂。

深拷贝是复制过程递归发生的过程。这意味着首先构造一个新的集合对象,然后递归地用在原始集合中找到的子对象的副本填充它。在深拷贝的情况下,对象的副本被复制到其他对象中。这意味着对对象副本所做的任何更改都不会反映在原始对象中。在 python 中,这是使用“deep copy()”函数实现的。

在 Java 中

Java 中的对象总是通过引用间接访问。对象永远不会隐式创建,而是始终由引用变量传递或分配。(Java 中的方法总是按值传递,但是,传递的是引用变量的值。)Java 虚拟机管理垃圾收集,以便在对象不再可访问后对其进行清理。在 Java 中没有自动复制任何给定对象的方法。

复制通常由类的clone() 方法执行。该方法通常依次调用其父类的 clone() 方法来获取副本,然后执行任何自定义复制过程。最终到达Object(最上面的类)的 clone() 方法,该方法创建一个与对象相同的类的新实例,并将所有字段复制到新实例(“浅拷贝”)。如果使用此方法,则该类必须实现Cloneable标记接口,否则将抛出CloneNotSupportedException。在从父类获得副本后,类自己的 clone() 方法可以提供自定义克隆功能,如深度复制(即复制对象引用的某些结构)或为新实例提供新的唯一 ID。

clone() 的返回类型是Object,但是由于 Java 对协变返回类型的支持,克隆方法的实现者可以编写被克隆对象的类型。使用 clone() 的一个优点是,由于它是一个可覆盖的方法,我们可以在任何对象上调用 clone(),它将使用其类的 clone() 方法,而调用代码不需要知道该类是什么(复制构造函数需要)。

一个缺点是通常无法访问抽象类型上的 clone() 方法。Java 中的大多数接口和抽象类都没有指定公共 clone() 方法。因此,通常使用 clone() 方法的唯一方法是对象的类是已知的,这与使用可能的最通用类型的抽象原则背道而驰。例如,如果在 Java 中有一个 List 引用,则不能对该引用调用 clone(),因为 List 没有指定公共 clone() 方法。ArrayList 和 LinkedList 等 List 的实现通常都有 clone() 方法,但是携带对象的类类型不方便且抽象不好。

在 Java 中复制对象的另一种方法是通过接口将它们序列化。Serializable这通常用于持久性和有线协议的目的,但它确实创建了对象的副本,并且与克隆不同,一个优雅地处理对象循环图的深层副本很容易获得,而程序员只需最少的努力。

这两种方法都有一个明显的问题:构造函数不能用于通过克隆或序列化复制的对象。这可能会导致数据初始化不正确的错误,阻止使用final成员字段,并使维护变得困难。一些实用程序试图通过对深度复制对象使用反射来克服这些问题,例如深度克隆库。[

 评论