深拷贝、浅拷贝、克隆
Posted
技术标签:
【中文标题】深拷贝、浅拷贝、克隆【英文标题】:Deep copy, shallow copy, clone 【发布时间】:2011-09-05 04:04:49 【问题描述】:我需要澄清一下Java中深拷贝、浅拷贝和克隆之间的区别
【问题讨论】:
听起来既像一个家庭作业问题,又像一个在线上有数千篇文章的东西。喜欢this。 你能再具体一点吗?是否有一组特定的方法或库可以用于解决特定问题? 【参考方案1】:不幸的是,“浅拷贝”、“深拷贝”和“克隆”都是定义不明确的术语。
在Java上下文中,我们首先需要区分“复制值”和“复制对象”。
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);
简而言之,对类型为引用类型的变量的引用赋值是“复制值”,其中该值是对象引用。要复制一个对象,需要使用new
,无论是显式的还是幕后的。
现在针对对象的“浅”与“深”复制。浅拷贝一般是指只拷贝一个对象的一层,而深拷贝一般是指拷贝多于一层。问题在于确定我们所说的水平是什么意思。考虑一下:
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 上下文中所说的浅拷贝......)
还有两个问题/不确定的领域:
有多深?是不是停在两个层面?三级?是指连接对象的整个图吗?
封装的数据类型呢?例如一个字符串? String 实际上不仅仅是一个对象。事实上,它是一个带有一些标量字段的“对象”,以及对字符数组的引用。但是,API 完全隐藏了字符数组。那么,当我们谈论复制字符串时,将其称为“浅”副本还是“深”副本有意义吗?还是我们应该称之为副本?
最后,克隆。克隆是一种存在于所有类(和数组)上的方法,通常被认为会生成目标对象的副本。然而:
此方法的规范故意没有说明这是浅拷贝还是深拷贝(假设这是有意义的区别)。
事实上,规范甚至没有明确说明克隆会产生一个新对象。
the javadoc 是这样说的:
“创建并返回此对象的副本。“副本”的确切含义可能取决于对象的类。一般意图是,对于任何对象 x,表达式
x.clone() != x
将是为真,并且表达式x.clone().getClass() == x.getClass()
为真,但这些不是绝对要求。虽然通常情况下x.clone().equals(x)
为真,但这不是绝对要求。"
注意,这是说在一个极端,克隆可能是目标对象,而在另一个极端,克隆可能不等于原始对象。这假设甚至支持克隆。
简而言之,克隆对于每个 Java 类都有不同的含义。
有些人争论(就像@supercat 在 cmets 中所做的那样)Java 的 clone()
方法被破坏了。但我认为正确的结论是克隆的概念在OO的上下文中被打破了。 AFAIK,不可能开发出一个统一的克隆模型,该模型在所有对象类型中都是一致且可用的。
【讨论】:
x.clone().equals(x)
应该被认为是真实的想法对我来说似乎很奇怪。 equals
我能想到的唯一含义对于所有对象类型都是一致的,那就是等效,并且不应该将可变类型的实例视为与任何其他实例等效。如果一个对象是不可变的,就没有理由克隆它,如果一个对象是可变的,它不应该等同于它的克隆。
@supercat - 这是合乎逻辑的。但尽管如此,一些人对这一事实感到惊讶。并注意 javadoc 中的引用!!
恕我直言,Java 和 .net 中的一些早期设计决策和建议应该被视为“错误”。处理封装对象标识的字段和封装可变对象状态的字段可以简化运行时,但缺乏任何人类可解析的区分不同类型字段的约定会导致相当混乱的思维。对象应该只公开一种类型的克隆方法,该方法必须克隆封装可变状态的嵌套项,必须不克隆封装身份的嵌套项,并且...
...可能克隆也可能不克隆那些既不封装也可能不克隆(通常,克隆此类项目效率低,但并非不正确)。 List<T>
的状态是其中包含的项目引用序列;因此,应该期望类类型T
的List<T>
的正确克隆引用与原始列表相同的项目。有一个EncapsulatedItemList<T>
可能会有所帮助,它将被定义为封装其中项目的可变状态,并且其Clone
方法将使用在构造时提供的Cloner<T>
来复制其中的项目。
关于equals
,我认为Java 最大的失败在于许多集合要求类型覆盖它以表示等价以外的含义;在.net 中,Dictionary<TKey,TValue>
的构造函数可以接受IEqualityComparer<TKey>
,它可以使用比对象等价更宽松的东西,即使TKey
对Equals(Object)
的覆盖没有;我认为Java中不存在这样的功能。另一方面,.net 提出了==
和.Equals
通常应该工作相同的概念,尽管==
不能定义适当的等价关系。【参考方案2】:
术语“克隆”是模棱两可的(尽管 Java 类库包含一个Cloneable 接口)并且可以指代深拷贝或浅拷贝。深/浅复制与 Java 无关,而是与复制对象相关的一般概念,指的是如何复制对象的成员。
举个例子,假设你有一个 person 类:
class Person
String name;
List<String> emailAddresses
你如何克隆这个类的对象?如果您正在执行浅拷贝,您可能会复制名称并将对 emailAddresses
的引用放入新对象中。但是,如果您修改了 emailAddresses
列表的内容,您将同时修改两个副本中的列表(因为这就是对象引用的工作方式)。
深拷贝意味着您递归地复制每个成员,因此您需要为新的Person
创建一个新的List
,然后将内容从旧对象复制到新对象。
虽然上面的例子是微不足道的,但深拷贝和浅拷贝之间的差异是显着的,并且对任何应用程序都有重大影响,特别是如果您试图提前设计一个通用的克隆方法,而不知道以后有人会如何使用它.有时您需要深层或浅层语义,或者需要一些混合,其中您需要深度复制某些成员而不是其他成员。
【讨论】:
+1 得到很好的答案,但是这个“对 emailAddresses 的引用”真的怎么样?因为我觉得 emailAddresses 本身就是参考。 在说“引用 emailAddresses”时就像“引用引用”,这没什么。我明白你想说什么,但这可能会让少数人感到困惑。我们的答案也不应该让少数人感到困扰:)【参考方案3】: 深拷贝:克隆此对象以及对其拥有的所有其他对象的每个引用 浅拷贝:克隆此对象并保留其引用 Object clone() 抛出 CloneNotSupportedException:未指定这应该返回深拷贝还是浅拷贝,但至少:o.clone() != o【讨论】:
其实o.clone() == o
可以是真的;看我的回答。【参考方案4】:
术语“浅拷贝”和“深拷贝”有点模糊;我建议使用术语“按成员克隆”和我所说的“语义克隆”。对象的“成员克隆”是一个新对象,与原始对象具有相同的运行时类型,对于每个字段,系统有效地执行“newObject.field = oldObject.field”。基 Object.Clone() 执行成员克隆;按成员克隆通常是克隆对象的正确起点,但在大多数情况下,在按成员克隆之后需要进行一些“修复工作”。在许多情况下,尝试使用通过成员克隆生成的对象而不首先执行必要的修复会导致坏事发生,包括被克隆的对象以及可能的其他对象的损坏。有些人使用术语“浅层克隆”来指代成员克隆,但这并不是该术语的唯一用途。
“语义克隆”是一个对象,它包含与原始数据相同的数据,从类型的角度来看。为了检查,考虑一个 BigList,它包含一个 Array> 和一个计数。这种对象的语义级克隆将执行成员克隆,然后将 Array> 替换为新数组,创建新的嵌套数组,并将所有 T 从原始数组复制到新数组。 它不会尝试对 T 本身进行任何形式的深度克隆。具有讽刺意味的是,有人将克隆称为“浅克隆”,而另一些人则称其为“深度克隆”。不完全有用的术语。
虽然在某些情况下真正的深度克隆(递归复制所有可变类型)很有用,但它只能由其组成部分是为这种架构设计的类型执行。在许多情况下,真正的深度克隆是过度的,并且它可能会干扰实际需要一个对象的情况,该对象的可见内容与另一个对象引用相同的对象(即语义级别的副本)。在对象的可见内容是从其他对象递归派生的情况下,语义级克隆将意味着递归深度克隆,但在可见内容只是某种通用类型的情况下,代码不应盲目地深度克隆所有内容看起来它可能可以进行深度克隆。
【讨论】:
以上是关于深拷贝、浅拷贝、克隆的主要内容,如果未能解决你的问题,请参考以下文章