深拷贝,浅拷贝,克隆

75

我需要澄清Java中深度复制、浅复制和克隆之间的区别。


4
听起来既像是一道作业题,又像是有成千上万篇关于它的文章在线上。比如这篇文章 - trutheality
你能再具体一些吗?你有特定的方法或库来解决某个问题吗? - Dan
4个回答

112

不幸的是,“浅拷贝”、“深拷贝”和“克隆”这些术语都没有明确定义。


在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上下文中,这不是我们通常所说的浅拷贝的意思...

还存在两个问题/不确定性:

  • 深度有多深?停在两层吗?三级?它是否指整个连接对象图形?

  • 关于封装数据类型;例如字符串?一个字符串实际上不只是一个对象。事实上,它是一个具有一些标量字段和对字符数组的引用的“对象”。但是,字符数组完全被API隐藏。因此,当我们谈论复制字符串时,将其称为“浅”复制或“深”复制是否有意义?还是应该只称其为副本?


最后是克隆。克隆是存在于所有类(和数组)上的方法,通常被认为会产生目标对象的副本。但是:

  • 该方法的规范故意没有说明这是否是浅复制或深复制(假设这是有意义的区别)。

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

以下是javadoc的说明:

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

请注意,这意味着在一个极端情况下,克隆可能是目标对象,而在另一个极端情况下,克隆可能与原始对象不相等。这还假设支持克隆。

简而言之,在OO上下文中,一些人认为Java clone()方法是有缺陷的,但我认为正确的结论是,在所有对象类型中开发一致且可用的克隆统一模型是不可能的。


2
在我的看法中,认为 x.clone().equals(x) 应该是正确的似乎有些奇怪。我所能想到的对于所有对象类型一致的 equals 意义应该是等同性,而不是任何一种可变类型的实例都被认为是等同的。如果一个对象是不可变的,那么没有克隆它的理由;如果一个对象是可变的,那么它不应该和它的克隆体等同。 - supercat
1
@supercat - 这是合乎逻辑的。但是,仍然有一些人会对这个事实感到惊讶。请注意Javadoc中的引用!! - Stephen C
在我看来,Java和.NET中一些早期的设计决策和建议应该被视为“错误”。处理封装对象标识和封装可变对象状态的相同字段简化了运行时,但缺乏任何人类可解析的约定来区分不同类型的字段会导致相当混乱的思考。对象只应公开一种类型的克隆方法,该方法必须克隆封装可变状态的嵌套项,不得克隆封装标识的嵌套项,以及... - supercat
请澄清一下,您的意思是“复制一个值”=浅复制,但“复制一个对象”=深复制吗?我理解您区分了复制值和复制对象,并且还区分了浅复制和深复制,但我不清楚这两个不同的区分如何联系在一起。 - randomUser47534
@randomUser47534 - 不,我的意思不是那样。我想表达的是,“浅拷贝”、“深拷贝”、“克隆”具有模糊的语义,试图在Java上下文中明确定义它们的含义是没有充分基础的。 - Stephen C
显示剩余4条评论

22

“克隆”一词含义不明确(尽管Java类库包括Cloneable接口),可以指深拷贝或浅拷贝。深/浅拷贝并非专门针对Java,而是涉及到制作对象副本的一般概念,并且指的是如何复制对象的成员。

例如,假设您有一个人类:

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

如果您正在执行浅拷贝,您可能会复制名称并在新对象中引用emailAddresses。但是,如果您修改了emailAddresses列表的内容,则会同时修改两个副本中的列表(因为这就是对象引用的工作方式)。
深层复制意味着您递归地复制每个成员,因此您需要为新的Person创建一个新的List,然后将旧对象的内容复制到新对象中。
尽管上面的示例很简单,但深度和浅度拷贝之间的差异非常重要,并且对任何应用程序都有重大影响,特别是如果您尝试提前设计通用克隆方法而不知道以后有人会如何使用它。有时您需要深度或浅度语义,或者一些混合方法,其中您深度复制某些成员但不复制其他成员。

1
+1 对于好的回答,但是关于“对emailAddresses的引用”,真的吗?因为我感觉emailAddresses本身就是一个引用。 - JAVA
就像说“引用电子邮件地址”的“引用”,它什么也不是。我明白你试图表达什么,但这可能会让一些人感到困惑。我们的答案也不应该给少数人带来麻烦 :) - JAVA

18
  • 深拷贝:克隆此对象及其所有引用的其他对象
  • 浅拷贝:克隆此对象并保留其引用
  • Object clone() throws CloneNotSupportedException: 没有指定此方法应返回深拷贝还是浅拷贝,但至少:o.clone() != o

4
实际上,o.clone() == o 可能是真的;请参见我的回答。 - Stephen C

2
术语“浅拷贝”和“深拷贝”有点模糊,我建议使用术语“成员逐一克隆”和我称之为“语义克隆”的术语。一个对象的“成员逐一克隆”是一个新对象,与原始对象具有相同的运行时类型,对于每个字段,系统实际上执行“newObject.field = oldObject.field”。基本Object.Clone()执行成员逐一克隆;成员逐一克隆通常是克隆对象的正确起点,但在大多数情况下,需要进行一些“修复工作”才能完成成员逐一克隆。在许多情况下,尝试使用通过成员逐一克隆产生的对象而不首先执行必要的修复工作将导致糟糕的事情发生,包括克隆的对象以及可能的其他对象的损坏。有些人用术语“浅克隆”来指代成员逐一克隆,但这并不是该术语的唯一用法。
“语义克隆”是一个包含与原始对象相同数据的对象,从类型的角度来看。例如,考虑一个包含Array>和count的BigList。这样一个对象的语义级别克隆将执行成员逐一克隆,然后用一个新数组替换Array>,创建新的嵌套数组,并将所有T从原始数组复制到新数组中。 “它不会尝试任何深度克隆T本身”。具有讽刺意味的是,有些人称克隆为“浅克隆”,而另一些人则称其为“深度克隆”。这并不是很有用的术语。
虽然有些情况下真正的深度克隆(递归地复制所有可变类型)是有用的,但它应该只由其构成部分设计用于此类架构的类型执行。在许多情况下,真正的深度克隆是过度的,它可能会干扰需要一个可见内容引用与另一个对象相同对象的情况(即语义级别的副本)。在对象的可见内容是递归地派生自其他对象的情况下,语义级别克隆将暗示递归深度克隆,但在可见内容仅是某些通用类型的情况下,代码不应盲目地深度克隆所有看起来可能是可深度克隆的东西。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接