深拷贝,浅拷贝,克隆

我需要澄清 Java 中深拷贝、浅拷贝和克隆之间的区别

160746 次浏览
  • 深度拷贝: 克隆这个对象以及它所有其他对象的每个引用
  • 浅拷贝: 克隆此对象并保留其引用
  • 对象 clone ()抛出 CloneNotSupportedException: 它没有指定这应该返回一个深拷贝还是浅拷贝,但是至少: o.clone () != o

术语“克隆”是模棱两可的(尽管 Java 类库包含 可以克隆接口) ,并且可以引用深拷贝或浅拷贝。深/浅拷贝并没有明确地与 Java 绑定在一起,而是一个关于复制对象的一般概念,指的是如何复制对象的成员。

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

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

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

深度复制意味着递归地复制每个成员,因此需要为新的 Person创建一个新的 List,然后将内容从旧对象复制到新对象。

尽管上面的示例很简单,但是深拷贝和浅拷贝之间的区别是显著的,并且对任何应用程序都有重大影响,特别是如果您试图提前设计一个通用的克隆方法,而不知道以后有人会如何使用它。有时需要深层或浅层语义,或者需要深层复制某些成员而不复制其他成员。

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


在 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?String 实际上不仅仅是一个对象。实际上,它是一个带有一些标量字段的“对象”,以及对字符数组的引用。但是,字符数组完全被 API 隐藏。因此,当我们讨论复制 String 时,将其称为“浅”拷贝或“深”拷贝有意义吗?或者我们应该叫它复制品?


终于,克隆人。Clone 是存在于所有类(和数组)上的一种方法,通常被认为会产生目标对象的副本。然而:

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

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

Javadoc是这么说的:

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

注意,这意味着在一个极端,克隆 也许吧是目标对象,而在另一个极端,克隆 也许不会等于原始对象。这里假设甚至支持克隆。

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


有些人认为(就像@supercat 在评论中所做的那样) Javaclone()方法已经坏了。但是我认为正确的结论是 克隆的概念克隆的概念在面向对象的环境中是破碎的。AFAIK,它是不可能开发一个统一的模型,克隆是一致的和可用的所有对象类型。

术语“浅拷贝”和“深拷贝”有点模糊; 我建议使用术语“成员克隆”和我称之为“语义克隆”。对象的“成员化克隆”是一个新对象,与原始对象具有相同的运行时类型,对于每个字段,系统都有效地执行“ newObject.field = oldObject.field”。基本对象。Clone ()执行成员方式的克隆; 成员方式的克隆通常是克隆对象的正确 起点,但在大多数情况下,在成员方式的克隆之后需要一些“修复工作”。在许多情况下,试图使用通过成员方式克隆产生的对象而不首先执行必要的修复会导致坏事情发生,包括被克隆的对象的损坏以及可能的其他对象。有些人使用“浅层克隆”这个术语来指成员方式的克隆,但这并不是这个术语的唯一用法。

“语义克隆”是包含与原始 从类型的角度来看相同数据的对象。为了便于检查,考虑一个包含 Array > 和 count 的 BigList。这样一个对象的语义级克隆将执行成员方面的克隆,然后用一个新数组替换 Array > ,创建新的嵌套数组,并将所有 T 从原始数组复制到新数组。它不会尝试任何形式的深度克隆 T 细胞本身.具有讽刺意味的是,一些人称克隆为“浅克隆”,而另一些人称之为“深度克隆”。这可不是什么有用的术语。

虽然在某些情况下,真正的深度克隆(递归地复制所有可变类型)是有用的,但它应该只由那些其组成部分是为这种体系结构设计的类型来执行。在许多情况下,真正的深度克隆是过度的,而且它可能会干扰这样的情况: 实际上需要的是一个对象,其可见内容引用的是与另一个对象相同的对象(即语义级别的副本)。如果一个对象的可见内容是从其他对象递归派生出来的,语义级别的克隆就意味着一个递归的深度克隆,但是如果可见内容只是某种通用类型,代码不应该盲目地深度克隆所有看起来可能是深度克隆的东西。