深拷贝,浅拷贝,克隆
可能重复:
如何在Java中复制对象?
我需要澄清Java中深拷贝,浅拷贝和克隆之间的区别
不幸的是,“浅拷贝”,“深拷贝”和“克隆”都是相当不明确的术语。
在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);
简而言之,对types为引用types的variables的引用赋值是“复制值”,其中的值是对象引用。 要复制一个对象,需要明确地或者隐藏地使用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上下文中浅拷贝的意思。
还有两个问题/不确定性的地方存在:
-
有多深? 它停在两个层面吗? 三个层面? 这是否意味着连接对象的整个graphics?
-
什么封装的数据types; 例如一个string? 一个string实际上不只是一个对象。 实际上,它是一个带有一些标量字段的“对象”,是对一系列字符的引用。 但是,字符数组完全被API隐藏。 所以,当我们谈论复制一个string时,将它称为“浅”副本还是“深度”副本是否有意义? 还是应该把它叫做副本?
最后克隆。 克隆是存在于所有类(和数组)上的方法,通常被认为是产生目标对象的副本。 然而:
-
这个方法的规范故意不说这是一个浅的还是深的拷贝(假设这是一个有意义的区别)。
-
实际上,规范甚至没有具体说明克隆产生了一个新的对象。
这就是javadoc所说的:
“创build并返回这个对象的副本,”copy“的确切含义可能取决于对象的类别,一般意图是,对于任何对象x,expression式
x.clone() != x
将为true ,并且expression式x.clone().getClass() == x.getClass()
将是真的,但是这些并不是绝对的需求x.clone().equals(x)
是真的,这不是绝对的要求。“
请注意,这就是说在一个极端情况下克隆可能是目标对象,另一个极端是克隆可能不等于原始对象。 而且这个假设甚至支持克隆。
简而言之,克隆可能对每个Java类都有不同的含义。
有些人认为(就像@supercat在评论中所说)Java clone()
方法被破坏了。 但是我认为正确的结论是在面向对象的情况下克隆的概念被打破了。 AFAIK,不可能开发一个统一的克隆模型,在所有对象types中都是一致的和可用的。
术语“克隆”是不明确的(虽然Java类库包括一个Cloneable接口),并且可以引用深层复制或浅层复制。 深/浅拷贝并不是专门与Java绑定的,而是与拷贝对象相关的一般概念,并且指的是拷贝对象成员的方式。
举一个例子,假设你有一个人类:
class Person { String name; List<String> emailAddresses }
你如何克隆这个类的对象? 如果您正在执行浅拷贝,则可以复制名称并在新对象中添加对emailAddresses
的引用。 但是,如果您修改了emailAddresses
列表的内容,则会修改两个副本中的列表(因为这是对象引用的工作方式)。
深拷贝将意味着您recursion复制每个成员,因此您需要为新的Person
创build一个新的List
,然后将内容从旧的复制到新的对象。
虽然上面的例子是微不足道的,但是深度和浅度副本之间的差别是显着的,并且对任何应用程序都有重大影响,特别是如果您想提前devise一个通用的克隆方法,而不知道稍后有人可能会使用它。 有些时候你需要深层或浅层的语义,或者有些时候你需要复制一些成员而不是其他的。
- 深层复制:克隆这个对象和每个其他对象的引用
- 浅拷贝:克隆这个对象并保留它的引用
- Object clone()抛出CloneNotSupportedException:没有指定是否应该返回深度或浅度的副本,但至less:o.clone()!= o
浅拷贝只不过是对现有对象的另一个引用。 您对浅拷贝所做的任何更改都会被对该对象的其他引用看到(并感觉到)。 考虑下面的代码:
List<Integer> listA, listB; listA = new ArrayList<>(); listA.add(new Integer(1)); listB = listA; listB.add(new Integer(2));
声明: listB = listA使得listB成为listA的“浅拷贝”,并且这两个variables引用相同的列表。 声明之后: listB.add(new Integer(2)); , listB (更具体地说, listB引用的列表)包含两个元素(1和2)。 但是,由于listA也引用了相同的列表,所以它也包含相同的两个元素。
在Java中, 深拷贝没有精确的定义。 有一个接口允许开发人员定义什么克隆的手段,但这是由开发人员的兴致。 深拷贝和克隆 意味着不是对对象的引用,而是获得完全独立的不同对象。
考虑上面的代码示例。 使用深层复制(或克隆), listA和listB将是彼此不同的。 (也就是说,他们会参考不同的项目)。 因此,对于深层复制或克隆 , listB.add()不会影响列表listA 。 也就是说, listA仍然会有一个元素(1),而listB会有两个元素(1和2)。
唉,这可能也可能不是这样。 有一个接口( Cloneable )指定克隆对象创build的方式。 它的深度取决于定义和实施课程的团队构成了一个克隆。 决定在一个物体上做什么深度或浅度的复制并不是一个微不足道的决定。 有一些对象使得深层复制没有意义(比如对单例的引用),或者效率不高(比如一个常量的副本,比如String)。
看到这个相关的StackOverflow文章 ,或这一个 。
术语“浅拷贝”和“深拷贝”有点模糊; 我build议使用术语“成员副本”和我称之为“语义克隆”。 对象的“成员副本”是与原始对象相同的运行时types的新对象,对于每个字段,系统有效地执行“newObject.field = oldObject.field”。 基础Object.Clone()执行成员的克隆; 成员克隆通常是克隆对象的正确的起点 ,但在大多数情况下,在成员级克隆之后将需要一些“修复工作”。 在许多情况下,尝试使用通过成员方法克隆生成的对象,而没有首先执行必要的修复操作将导致不好的事情发生,包括被克隆的对象以及可能的其他对象的损坏。 有人使用“浅层克隆”这个术语来指成员克隆,但这不是该术语的唯一用法。
从types的angular度来看, “语义克隆”是包含与原始数据相同的数据的对象。 对于检查,请考虑一个BigList,它包含一个Array>和一个计数。 这样一个对象的语义级别的克隆将执行成员级克隆,然后用一个新数组replaceArray>,创build新的嵌套数组,并将所有T从原始数组复制到新数组。 它不会试图对T本身进行任何深层次的克隆 。 具有讽刺意味的是,有人提到克隆“浅层克隆”,而其他人称之为“深度克隆”。 不完全有用的术语。
虽然有些情况下,真正的深度克隆(recursion复制所有可变types)是有用的,但是它只能由其构成要素为这种体系结构devise的types来执行。 在很多情况下,真正的深度克隆是过度的,它可能会干扰所需要的事实上是一个对象,其可见内容是指与另一个对象相同的对象(即语义级拷贝)。 如果一个对象的可见内容是从其他对象中recursion地派生的,那么语义级别的克隆就意味着一个recursion的深层克隆,但是在可见内容只是某种generics的情况下,代码不应该盲目地深入克隆所有的东西看起来它可能是可以深入克隆的。