Deep copy, shallow copy, clone


74

I need clarification on the differences between deep copy, shallow copy, and clone in Java


4
听起来既是一项家庭作业问题,又有数千篇有关该问题的在线文章。像这样
trutheality

Can you be a little more specific? Is there a certain set of methods or libraries you have in mind to use for a certain problem?
Dan

Answers:


112

Unfortunately, "shallow copy", "deep copy" and "clone" are all rather ill-defined terms.


In the Java context, we first need to make a distinction between "copying a value" and "copying an object".

int a = 1;
int b = a;     // copying a value
int[] s = new int[]{42};
int[] t = s;   // copying a value (the object reference for the array above)

StringBuffer sb = new StringBuffer("Hi mom");
               // copying an object.
StringBuffer sb2 = new StringBuffer(sb);

In short, an assignment of a reference to a variable whose type is a reference type is "copying a value" where the value is the object reference. To copy an object, something needs to use new, either explicitly or under the hood.


现在用于对象的“浅”复制与“深”复制。浅复制通常意味着仅复制对象的一个​​级别,而深复制通常意味着复制多个级别的对象。问题在于确定水平是什么意思。考虑一下:

public class Example {
    public int foo;
    public int[] bar;
    public Example() { };
    public Example(int foo, int[] bar) { this.foo = foo; this.bar = bar; };
}

Example eg1 = new Example(1, new int[]{1, 2});
Example eg2 = ... 

通常的解释是,“浅”副本eg1将是新的Example对象,其foo等于1,并且其bar字段引用与原始对象相同的数组。例如

Example eg2 = new Example(eg1.foo, eg1.bar);

的“深层”副本的常规解释eg1是一个新Example对象,该对象foo等于1,并且其bar字段引用原始数组的副本;例如

Example eg2 = new Example(eg1.foo, Arrays.copy(eg1.bar));

(来自C / C ++背景的人们可能会说,引用分配会产生浅表副本。但是,这通常不是Java上下文中的浅表副本的意思……)

存在另外两个问题/不确定性领域:

  • 有多深?它会停在两个级别吗?三个级别?这是否意味着连接对象的整个图形?

  • 封装的数据类型呢?例如一个字符串?字符串实际上不仅仅是一个对象。实际上,它是带有一些标量字段的“对象”,并且是对字符数组的引用。但是,API完全隐藏了字符数组。因此,当我们谈论复制字符串时,将其称为“浅”副本还是“深”副本有意义吗?还是我们应该称它为副本?


最后,克隆。克隆是一种存在于所有类(和数组)上的方法,通常认为该方法可生成目标对象的副本。然而:

  • 此方法的规范故意没有说明这是浅拷贝还是深拷贝(假设这是有意义的区别)。

  • 实际上,规范甚至没有明确声明克隆会产生一个新对象。

这是javadoc所说的:

“创建并返回此对象的副本。“ copy”的确切含义可能取决于对象的类。通常的意图是,对于任何对象x,表达式x.clone() != x将为true,而表达式x.clone().getClass() == x.getClass()将为true ,但这不是绝对要求。虽然通常情况x.clone().equals(x)是正确的,但这不是绝对要求。”

请注意,这是说克隆可能是目标对象,而另一极端是克隆可能与原始对象相等。并假设甚至支持克隆。

简而言之,对于每个Java类,克隆都可能意味着不同的东西。


有人认为(如@supercat在注释中所做的那样)Javaclone()方法已损坏。但是我认为正确的结论是,克隆概念在面向对象环境中被打破了。在AFAIK中,不可能开发出一个统一的克隆模型,该模型在所有对象类型之间都是一致且可用的。


2
对我来说,x.clone().equals(x)应该被认为是真实的想法似乎很奇怪。equals我可以想到的唯一含义是,所有对象类型的一致性将是等效的,并且任何可变类型的实例都不应被视为等同于任何其他实例。如果对象是不可变的,则没有理由对其进行克隆;如果对象是可变的,则不应等同于其克隆。
supercat 2012年

1
@supercat-这是合乎逻辑的。但是,尽管如此,一些人对此感到惊讶。并注意javadoc中的报价!!
斯蒂芬·C

恕我直言,Java和.net中的一些早期早期设计决策和建议应被视为“错误”。处理封装对象标识的字段和封装可变对象状态的字段可以完全相同,从而简化了运行时间,但是缺少区分这些类型的字段的任何人为可解析的约定都会导致思想混乱。对象仅应公开一种克隆方法,该方法必须克隆封装了可变状态的嵌套项目,不得克隆封装身份的嵌套项目,并且...
supercat 2012年

...可能会克隆也可能不会封装两者的克隆(通常,克隆此类项目效率不高,但并非不正确)。a的状态List<T>是其中包含的项目引用的序列;因此,应该期望正确List<T>的class-type克隆T引用与原始列表相同的项。有一个可能会有所帮助EncapsulatedItemList<T>,它将定义为将项目的可变状态封装在其中,并且其Clone方法将使用Cloner<T>提供的构造在其中复制项目。
2012年

关于equals,我认为Java的最大缺点是许多集合要求类型覆盖它,以表示除等效项以外的其他含义。在.net中,a的构造函数Dictionary<TKey,TValue>可以接受,IEqualityComparer<TKey>它可以使用比对象等价要宽松得多的东西,即使TKey的替代Equals(Object)没有这样做;我认为Java中不存在这样的功能。在另一方面,.NET放提出的概念,==并且.Equals通常应该工作一样,尽管事实上==不能定义一个适当的等价关系。
supercat 2012年

22

术语“克隆”是模棱两可的(尽管Java类库包括一个Cloneable接口),并且可以引用深层副本或浅层副本。深层/浅层副本并不专门与Java绑定,而是与制作对象副本有关的一般概念,它指的是如何也复制对象的成员。

举例来说,假设您有一个人物类:

class Person {
    String name;
    List<String> emailAddresses
}

您如何克隆此类的对象?如果要执行浅表复制,则可以复制名称并emailAddresses在新对象中添加对它的引用。但是,如果您修改了emailAddresses列表的内容,则将同时修改两个副本中的列表(因为这就是对象引用的工作方式)。

深层复制意味着您递归复制每个成员,因此您需要为新成员创建一个新成员ListPerson,然后再复制从旧的内容给新的对象。

尽管上面的示例很简单,但深层副本和浅层副本之间的差异非常明显,并且对任何应用程序都具有重大影响,尤其是如果您试图提前设计通用克隆方法而又不知道以后如何使用它时,尤其如此。有时,您需要深层或浅层语义,或者需要某种混合,在其中深层复制某些成员而不是其他成员。


1
+1是个不错的答案,但是这个“对emailAddresses的引用”真的呢?因为我觉得emailAddresses本身就是参考。
JAVA 2014年

就像在说“对emailAddresses的引用”中的“对引用的引用”一样。我理解您要说的话,但它会使少数人感到困惑。我们的回答也不应使少数人感到麻烦:)
JAVA

18
  • 深层复制:克隆该对象及其对每个其他对象的引用
  • 浅表复制:克隆此对象并保留其引用
  • 对象clone()抛出CloneNotSupportedException:未指定是否返回深拷贝或浅拷贝,但至少:o.clone()!= o

4
实际上o.clone() == o是真的;看我的答案。
斯蒂芬·C

2

术语“浅拷贝”和“深拷贝”有点含糊;我建议使用术语“成员克隆”和我所说的“语义克隆”。对象的“成员克隆”是新对象,其运行时类型与原始对象相同,对于每个字段,系统都会有效地执行“ newObject.field = oldObject.field”。基本的Object.Clone()执行成员克隆;按成员克隆通常是正确的起点克隆对象,但是在大多数情况下,逐成员克隆之后需要进行一些“修复工作”。在许多情况下,尝试使用通过成员克隆生成的对象而未先执行必要的修复程序将导致不良情况的发生,其中包括被克隆的对象以及其他对象的损坏。有些人使用“浅克隆”一词来指称成员克隆,但这不是该术语的唯一用法。

从类型的角度来看, “语义克隆”是包含与原始数据相同的数据的对象。为了进行检查,请考虑一个BigList,其中包含一个Array>和一个计数。此类对象的语义级别克隆将执行成员克隆,然后将Array>替换为新数组,创建新的嵌套数组,并将所有T都从原始数组复制到新数组。 它不会尝试对T本身进行任何深度克隆。具有讽刺意味的是,有些人将克隆称为“浅克隆”,而另一些人将其称为“深克隆”。并非完全有用的术语。

虽然在某些情况下,真正的深度克隆(递归复制所有可变类型)很有用,但只能由其成分针对此类架构设计的类型执行。在很多情况下,真正的深度克隆是多余的,并且可能会干扰这样的情况,即实际上需要一个对象,该对象的可见内容与另一个对象引用相同的对象(即语义级别的副本)。如果对象的可见内容是从其他对象递归派生的,则语义级别的克隆将意味着递归深度克隆,但如果可见内容只是某种通用类型,则代码不应盲目地深化所有内容看起来可能是可以深度克隆的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.