浅拷贝(Shallow Copy):
①对于数据类型是【基本数据类型】的成员变量,浅拷贝会直接进行值传递,也就是将该属性值复制一份给新的对象。因为是两份不同的数据,所以对其中一个对象的该成员变量值进行修改,不会影响另一个对象拷贝得到的数据。
②对于数据类型是【引用数据类型】的成员变量,比如说成员变量是某个【数组】、某个【类的对象】等,那么浅拷贝会进行引用传递,也就是只是将该成员变量的引用值(内存地址)复制一份给新的对象。因为
【实际上两个对象的成员变量都指向同一个实例】。在这种情况下,在一个对象中修改该成员变量会影响到另一个对象的该成员变量值。
Object类中有一个方法为protected Object clone() throws CloneNotSupportedException,这个方法就是进行的浅拷贝。
有了这个浅拷贝模板,我们可以通过【调用clone()方法来实现对象的浅拷贝】。但是需要注意:
1、Object类虽然有这个方法,但是【这个方法是受保护的(被protected修饰),所以我们无法直接使用】。
2、使用clone方法的类【必须实现Cloneable接口】,否则会抛出异常CloneNotSupportedException。对于这两点,我们的解决方法是,在要使用clone方法的类中重写clone()方法,通过super.clone()调用Object类中的原clone方法。
必须【显式的指定clone方法】并且你的类【必须实现Cloneable接口】。
Cloneable是“标记”接口的一个范例,接口自身不指定任何东西,但是,Object.clone检查类是否实现了它,如果没有就抛出一个CloneNotSupportedException异常。
深拷贝:设想一下,一个类有一个对象,其成员变量中又有一个对象,该对象指向另一个对象,另一个对象又指向另一个对象,直到一个确定的实例。这就形成了对象图。那么,对于深拷贝来说,不仅要复制对象的所有基本数据类型的成员变量值,还要为所有引用数据类型的成员变量申请存储空间,并复制每个引用数据类型成员变量所引用的对象,直到该对象可达的所有对象。也就是说,对象进行深拷贝要对整个对象图进行拷贝!
简单地说,深拷贝对引用数据类型的成员变量的对象图中所有的对象都开辟了内存空间;而浅拷贝只是传递地址指向,新的对象并没有对引用数据类型创建内存空间。
一个对象包含一个HashMap引用,当对象被拷贝时,HashMap引用也被拷贝了,这意味着拷贝生成的那个对象包含那个HashMap对象的原始引用。因此当原始对象中的HashMap的内容发生变化,拷贝生成的对象中的那个HashMap的内容也同时更新。
Object.clone完成的是对象的“浅”拷贝,即简单的成员到成员的拷贝。它不做“深度”拷贝,即成员或者数组指向的对象的递归拷贝。
===========================================================
假设在你的应用中使用一些对象,你如何拷贝你的对象呢?最明显的方法是讲一个对象简单的赋值给另一个,就像这样:
obj2 = obj1;
但是这个方法实际上没有拷贝对象而仅仅是拷贝了一个对象引用,换换言之,在你执行这个操作后仍然只有一个对象,但是多出了一个对该对象的引用。
如果这个看似明显的方法不能正常工作,那么如何实际的拷贝一个对象呢?为什么不试试Object.clone呢?这个方法对Object的所有子类都是可用的。例如:
class A { private int x; public A(int i) { x = i; } } public class CloneDemo1 { public static void main(String args[]) throws CloneNotSupportedException { A obj1 = new A(37); A obj2 = (A)obj1.clone(); } }
这个代码引发一个编译错误,因为Object.clone是一个protected方法。那么再试一次,换一种方法:
class A { private int x; public A(int i) { x = i; } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e.toString()); } } } public class CloneDemo2 { public static void main(String args[]) throws CloneNotSupportedException { A obj1 = new A(37); A obj2 = (A)obj1.clone(); } }
在这个方法中,呢定义自己的clone方法,它扩展Object.clone方法,CloneDemo2可以编译,但是当你运行它时会抛出一个CloneNotSupportedException异常。
这里仍然缺少一些东西,你必须让那些包含clone方法的类实现Cloneable接口,就像这样:
class A implements Cloneable { private int x; public A(int i) { x = i; } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e.toString()); } } public int getx() { return x; } } public class CloneDemo3 { public static void main(String args[]) throws CloneNotSupportedException { A obj1 = new A(37); A obj2 = (A)obj1.clone(); System.out.println(obj2.getx()); } }
成功了!CloneDemo3可以编译并产生期望的结果:
37
你已经了解了必须显式的指定clone方法并且你的类必须实现Cloneable接口。Cloneable是“标记”接口的一个范例,接口自身不指定任何东西,但是,Object.clone检查类是否实现了它,如果没有就抛出一个CloneNotSupportedException异常。
Object.clone方法做简单的拷贝操作,将一个对象的所有成员变量拷贝到一个新的对象。在CloneDemo3中,A.clone调用Object.clone,然后Object.clone创建一个新的A对象并将已经存在的那个对象的成员变量的内容拷贝到那个新对象。
CloneDemo3中还有大量其他值得考虑的东西。其中之一就是你可以防止你写的类的用户拷贝那个类对象。为了做到这个,你可以不实现Cloneable接口,因此拷贝操作总会抛出异常。
另外一点就是你可以支持无条件的和有条件的拷贝。CloneDemo3是无条件的支持拷贝,clone方法不会传播CloneNotSupportedException异常。
一个更通用的方法是有条件的支持类拷贝。在这种情况下,对象自身可以被拷贝,但是对象的子类可能不能拷贝。对于有条件拷贝,clone方法必须申明它能够传播CloneNotSupportedException异常。有条件拷贝的一个范例是一个集合类的对象的元素只有在那些元素是可以拷贝的时候才能进行拷贝。
有条件拷贝的另外的一种方式是实现一个合适的clone方法但是不实现Cloneable接口。在这种情况下,如果愿意,子类可以支持拷贝操作。
拷贝操作可能是很棘手的,因为Object.clone做简单的对象成员拷贝,有时候这不是你期望的,例如:
import java.util.*; class A implements Cloneable { public HashMap map; public A() { map = new HashMap(); map.put("key1", "value1"); map.put("key2", "value2"); } public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e.toString()); } } } public class CloneDemo4 { public static void main(String args[]) { A obj1 = new A(); A obj2 = (A)obj1.clone(); obj1.map.remove("key1"); System.out.println(obj2.map.get("key1")); } }
你可能希望CloneDemo4显示如下的结果:
value1
但是实际上它显示:
null
发生了什么事?在CloneDemo4中,一个对象包含一个HashMap引用,当对象被拷贝时,HashMap引用也被拷贝了,这意味着拷贝生成的那个对象包含那个HashMap对象的原始引用。因此当原始对象中的HashMap的内容发生变化,拷贝生成的对象中的那个HashMap的内容也同时更新。
要修正这个问题,你可以让clone方法更完善:
import java.util.*; class A implements Cloneable { public HashMap map; public A() { map = new HashMap(); map.put("key1", "value1"); map.put("key2", "value2"); } public Object clone() { try { A aobj = (A)super.clone(); aobj.map = (HashMap)map.clone(); return aobj; } catch (CloneNotSupportedException e) { throw new InternalError(e.toString()); } } } public class CloneDemo5 { public static void main(String args[]) { A obj1 = new A(); A obj2 = (A)obj1.clone(); obj1.map.remove("key1"); System.out.println(obj2.map.get("key1")); } }
Clone5Demo显示如下的期望的结果:
value1
Clone5Demo调用super.clone创建一个A对象并拷贝map成员,然后调用HashMap.clone完成HashMap类型的拷贝。这个操作包含创建一个新的hash表并且从老的那个里面拷贝成员到那个新的hash表。
如果两个对象共享一个引用,就像CloneDemo4中的情况一样,那么通常你会遇到问题,除非那个引用是只读的,要避开这个问题,你需要实现clone方法处理这个问题。这种情况的另一种说法是
Object.clone完成的是对象的“浅”拷贝,即简单的成员到成员的拷贝。它不做“深度”拷贝,即成员或者数组指向的对象的递归拷贝。
不使用"new CloneDemo5"创建一个对象,那么调用super.clone就是极度重要的。你应该在类层次的每一级上调用super.clone。这是因为每一级都可能有它自己的共享对象问题。如果你使用"new"而不是super.clone,那么你的代码对于那些从你的类继承的子类是不正确的,那些代码调用你的clone方法但是收到一个不正确的返回类型。
关于拷贝,另一个需要知道的事情是可以拷贝任何数组,只需简单的调用clone方法:
public class CloneDemo6 { public static void main(String args[]) { int vec1[] = new int[]{1, 2, 3}; int vec2[] = (int[])vec1.clone(); System.out.println(vec2[0] + " " + vec2[1] + " " + vec2[2]); } }
Final:
关于拷贝的最后一个重要的事情是:它是创建和初始化一个新对象的方式,但是它不同于调用一个构造方法。这个区别的一个例子是它事关空的final成员,也就是那些声明为"final"但是没有初始化的成员,它们只能在构造方法中被赋值。下面是一个空的final成员的用法:
public class CloneDemo7 { private int a; private int b; private final long c; public CloneDemo7(int a, int b) { this.a = a; this.b = b; this.c = System.currentTimeMillis(); } public static void main(String args[]) { CloneDemo7 obj = new CloneDemo7(37, 47); } }
在CloneDemo7的构造方法中,一个final成员"c"从系统时钟中获得一个时戳。如果你拷贝这样的类型的值你想得到什么?Object.clone拷贝所有的成员变量,但是你想那个时戳成员被设置为当前系统时钟的值。然而,如果一个成员是final类型的,你只能在构造方法中设置那个成员,不能在clone方法中。下面是这个问题的例子:
public class CloneDemo8 { private int a; private int b; private final long c; public CloneDemo8(int a, int b) { this.a = a; this.b = b; this.c = System.currentTimeMillis(); } public CloneDemo8(CloneDemo8 obj) { this.a = obj.a; this.b = obj.b; this.c = System.currentTimeMillis(); } public Object clone() throws CloneNotSupportedException { //this.c = System.currentTimeMillis(); return super.clone(); } public static void main(String args[]) { CloneDemo8 obj = new CloneDemo8(37, 47); CloneDemo8 obj2 = new CloneDemo8(obj); } }
如果你想取消final成员的赋值语句那一行的注释程序就不能编译。对于这样的问题,我们不使用clone方法,范例程序使用拷贝构造方法。拷贝构造方法的参数是和它自身类型相同并实现合适的拷贝逻辑。(译者注:在实现拷贝构造方法时需要注意共享对象问题,由于范例中的其他两个成员都是原始类型所以没有问题,但是如果你自己的类的成员的类型是对象类型就不能使用直接赋值也要使用拷贝进行或者是其他合适的拷贝构造方法,但是如果你需要使用的类型没有拷贝方法或者合适的拷贝构造方法,那么你就不能写你自己的合适的拷贝构造方法或者拷贝方法,所辛的是java的核心类基本上不存在这个问题,但是你如果使用其他的人的类就不好说了,因此如果你写自己的类并想让很多人用,那么你一定要实现合适的拷贝方法)
也许你认为你可以不使用空final成员而是在声明那些final成员的时候马上使用系统时间来初始化解决这样的问题,就像下面这样:
class A implements Cloneable { final long x = System.currentTimeMillis(); public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new InternalError(e.toString()); } } } public class CloneDemo9 { public static void main(String args[]) { A obj1 = new A(); // sleep 100 ms before doing clone, // to ensure unique timestamp try { Thread.sleep(100); } catch (InterruptedException e) { System.err.println(e); } A obj2 = (A)obj1.clone(); System.out.println(obj1.x + " " + obj2.x); } }
这样同样不能工作,当你运行这个程序,你可以看到obj1.x和obj2.x有相同的值。这指出当一个对象是拷贝生成的时候通常的对象初始化没有进行并且你不能在clone方法中设置final成员的值。因此如果简单的拷贝操作不能正确的初始化一个成员,你就不应该将它声明为final的。或者你需要使用拷贝构造方法作为拷贝的替代方法。(译者注:如果你将成员声明为private并且不提供修改它的值方法,那么效果和将它声明为final是相同的)
一些问题:
1、为什么以下的代码结果是"aaaa"
String[][] ss=new String[][]{{"342","fs"},{"32","ger"}}; String[][] s; s = (String[][])ss.clone(); ss[1][1]="aaaa"; System.out.println(s[1][1]);
如果两个对象共享一个引用,就像CloneDemo4中的情况一样,那么通常你会遇到问题,除非那个引用是只读的,要避开这个问题,你需要实现clone方法处理这个问题。这种情况的另一种说法是
Object.clone完成的是对象的“浅”拷贝,即简单的成员到成员的拷贝。它不做“深度”拷贝,即成员或者数组指向的对象的递归拷贝。
2、
private static void test1() { String[] s = {"a", "a"}; String[] ss = (String[]) s.clone(); pl("s: " + s); pl("ss: " + ss); ss[0] = "b"; pl("ss[0]=" + ss[0]); pl("s[0]=" + s[0]); }
我们在这里clone了一个数组对象,数组的元素是String,因此这里String是这个s的值。
当我们clone s时,Java生成了两个同样的String。并实现了我们想要作的。
输出为:
s: [Ljava.lang.String;@7172ea
ss: [Ljava.lang.String;@2f6684
ss[0]=b
s[0]=a
3、
private static void test2() { String[][] s = { {"a", "a"}, {"a", "a"} }; String[][] ss = (String[][]) s.clone(); pl("s="+s); pl("ss="+ss); pl("s[0]="+s[0]); pl("ss[0]="+ss[0]); pl("s[1]="+s[1]); pl("ss[1]="+ss[1]); ss[0][0]="b"; pl("s[0][0]="+s[0][0]); pl("ss[0][0]="+ss[0][0]); }
这里我们想要clone一个二维数组,并且[i]希望[/i]同样clone数组的每个值。
然而结果却事与愿违;
test2-----------
s=[[Ljava.lang.String;@738798
ss=[[Ljava.lang.String;@4b222f
s[0]=[Ljava.lang.String;@3169f8
ss[0]=[Ljava.lang.String;@3169f8
s[1]=[Ljava.lang.String;@2457b6
ss[1]=[Ljava.lang.String;@2457b6
s[0][0]=b
ss[0][0]=b
然而事实上结果却很好的符合的clone的原则。Java很好的clone了我们的二维数组s。并生成了它的一个副本ss。
而且ss中的每个值(一维数组)与原来的值完全相同。(我们可以从输出中清楚地看到)。
因为对于一个数组对象而言,数组的元素就是这个对象的值。因此二维数组的元素(值)就是一维数组。
而对于数组对象,Java把它作为类似指针处理。
4、
private static void test3(){ String[][] s = { {"a", "a"}, {"a", "a"} }; String[][]ss={ (String[]) s[0].clone(), (String[]) s[1].clone() }; pl("s="+s); pl("ss="+ss); pl("s[0]="+s[0]); pl("ss[0]="+ss[0]); pl("s[1]="+s[1]); pl("ss[1]="+ss[1]); ss[0][0]="b"; pl("s[0][0]="+s[0][0]); pl("ss[0][0]="+ss[0][0]); }
而当我们换一种clone方式(深度clone),我们可以看到结果发生了变化
test3-----------
s=[[Ljava.lang.String;@7a78d3
ss=[[Ljava.lang.String;@129206
s[0]=[Ljava.lang.String;@30f13d
ss[0]=[Ljava.lang.String;@2e000d
s[1]=[Ljava.lang.String;@55af5
ss[1]=[Ljava.lang.String;@169e11
s[0][0]=a
ss[0][0]=b
对于普通的clone而言(浅度clone),虚拟机使用本地方法,用类似copy内存的方式复制一个对象。因此会产生似乎是令人迷惑的结果。
因此通常clone数组或是复杂对象时,需要使用深度clone,一一clone每一个对象的元素。
一个疑惑:
为什么数组直接用.clone()方法。而自定义的A类却用 super.clone()方法。我觉得
用super.clone()方法,不符合面向对象思想。我想克隆的是A类的副本,而super.clone()方法克隆的是父类的对象,然后再用A obj2 = (A)obj1.clone()强制转换为A类。自定义的A类默认不就是继承Object类吗?它本身不就可以直接使用clone()方法吗,为什么还用super.clone()方法。
于是我将super.clone()换成this.clone(),编译通过。但运行抛出at A.clone(CloneDemo3.java:12)类外,您能不能给我一个合理的解释。
class A implements Cloneable { private int x; public A(int i) { x = i; } public Object clone() { try { return this.clone(); //我将super换成this //return super.clone(); } catch (Exception e) { throw new InternalError(e.toString()); } } public int getx() { return x; } } public class CloneDemo3 { public static void main(String args[])throws Exception { A obj1 = new A(37); A obj2 = (A)obj1.clone(); System.out.println(obj2.getx()); } }
?
相关推荐
原型模式中的拷贝分为“浅拷贝”和“深拷贝”: 浅拷贝:对值类型的成员变量进行值的复制,对引用类型的成员变量只复制引用,不复制引用的对象。 深拷贝:对值类型的成员变量进行值的复制,对引用类型的成员变量...
java设计模式【之】原型模式、深拷贝与浅拷贝【源码】【场景:克隆羊】 * 原型模式(Prototype) * 实现方式: * 需要被克隆的 class类, 重写Object中的clone()方法,并实现Cloneable接口(否则报错 ...
在Java中,如果一个类实现了`Cloneable`接口,那么就可以调用其`clone()`方法进行深拷贝或浅拷贝。对于深拷贝,需要在每个实例变量上都实现`clone()`方法,以确保所有嵌套的对象都被正确复制。而对于浅拷贝,只需...
在Java等支持克隆操作的编程语言中,实现起来相对简单,但需要注意深拷贝和浅拷贝的区别,以确保正确复制复杂的数据结构。在实际项目中,根据具体需求选择合适的设计模式,可以提升软件的可维护性和可扩展性。
1. **浅拷贝**与**深拷贝**:浅拷贝只复制对象本身,而不复制它引用的对象;深拷贝不仅复制对象本身,还递归地复制它引用的对象。 2. **JSON.parse() 和 JSON.stringify()**:在JavaScript中,这两个方法可以实现...
- 在Python等语言中,可以直接通过内置的复制机制(如`copy`模块)进行浅拷贝和深拷贝。 ### 使用场景 - 当创建新对象的过程非常复杂,或者初始化对象需要大量资源时,可以使用原型模式简化这一过程。 - 当系统...
C#中,我们可以使用`ICloneable`接口或`MemberwiseClone()`方法来实现对象的浅拷贝或深拷贝。 1. **ICloneable接口**: 这是C#提供的一种通用的克隆机制。一个类只需实现这个接口并定义`Clone()`方法,就可以实现...
在Java、C#等面向对象编程语言中,原型模式可以通过实现克隆接口或使用深拷贝和浅拷贝来实现。 ### 原理和组件 1. **原型(Prototype)**:这是模式的核心,它定义了接口用于复制自己。原型对象通常是具体的类,实现...
**原型模式(Prototype Pattern)**是一种基于克隆的创建型设计模式,它的主要目的是为了提高创建新对象的效率,特别是当创建新对象的过程复杂或者资源消耗较大时。在原型模式中,一个已经创建的对象(称为原型)被...
这个方法会返回一个与当前对象相同的新对象,通常通过深拷贝或浅拷贝来实现。深拷贝会创建一个全新的对象,包括所有嵌套的对象;而浅拷贝则只会复制对象本身,不复制嵌套的对象引用。 在C++中,有以下几种方式来...
这种模式主要涉及到`拷贝构造函数`和`深拷贝`、`浅拷贝`的概念。 在C++中,拷贝构造函数是类的一个特殊构造函数,它接受一个同类型的对象作为参数,用于创建一个新的对象作为已有对象的副本。拷贝构造函数的调用...
在C#编程中,面向对象...但要注意,正确处理深拷贝和浅拷贝是实现原型模式的关键,以免出现意外的共享状态。在实际项目中,根据需求选择适合的克隆方式,灵活运用原型模式,可以极大地提高代码的可维护性和可扩展性。
- **深拷贝与浅拷贝**:不正确地处理深拷贝和浅拷贝可能导致意料之外的结果,尤其是在含有指针成员的情况下。 在Demo20_Prototype中,我们可以看到具体的原型模式实现示例,它可能包含了不同类型的原型对象以及如何...
深拷贝和浅拷贝是原型模式中的两个重要概念: - 浅拷贝:只复制对象本身,而不复制它引用的对象。如果对象包含其他对象的引用,那么浅拷贝只会创建这些引用的副本,而不是引用的对象。 - 深拷贝:不仅复制对象本身...
2. **深拷贝与浅拷贝**: 在原型模式中,我们通常关心的是对象的拷贝方式。浅拷贝只复制对象的引用,而深拷贝会复制对象以及其引用的所有子对象。在Java中,`clone()`方法默认执行的是浅拷贝,但可以通过实现自定义的...
3. **深拷贝与浅拷贝**:原型模式中的拷贝分为两种类型——深拷贝和浅拷贝。深拷贝会创建原始对象的所有属性的新副本,包括引用类型。而浅拷贝只复制对象本身,不复制引用的对象。 ### 实现原型模式的步骤 1. 定义...
原型模式可以分为浅拷贝和深拷贝两种类型: 1. 浅拷贝:只复制对象本身的属性,不复制其引用的对象。也就是说,如果原型对象有对其他对象的引用,这些引用在克隆后新对象和原型对象共用,修改其中一个对象会影响到...
在.NET中可以通过`Object`类的`MemberwiseClone()`方法来实现浅拷贝,或者通过序列化的方式来实现深拷贝。 2. **稳定接口**:要求“易变类”具备稳定的接口,以便在克隆过程中不会引入额外的复杂性。 #### 应用实例...
总结起来,原型模式是一种有效的方式,特别是在C++中,通过拷贝构造函数和深拷贝来快速创建相似对象,而不需要每次都重新初始化所有属性。这种模式在需要大量创建相似对象的场景下非常有用,例如在游戏开发、模拟...
下面我们将深入探讨原型模式、浅复制和深复制的概念及其在C++中的应用。 ### 原型模式 原型模式的基本思想是通过对象的克隆(clone)方法创建新对象。在C++中,我们可以自定义`clone()`方法来返回对象的一个副本。...