对象创建。顾名思义,克隆就是复制。在Java语言中,clone方法是由对象调用的,所以会复制对象。所谓复制对象,首先需要分配一个与源对象大小相同的空间,并在这个空间中创建一个新的对象。那么在java语言中,创建对象的方式有多少种呢?使用new操作符创建一个对象使用clone方法复制一个对象那么这两种方法有什么相同点和不同点呢?new操作符的初衷是为了分配内存。当程序执行到new操作符时,首先要看new操作符后面的类型,因为只有知道类型才能知道要分配多少内存空间。分配内存后,调用构造函数填充对象的字段。这一步称为对象初始化。构造函数返回后,创建了一个对象,可以把它的引用(地址)释放到外面,在那里可以用这个引用来操作这个对象。clone第一步和new类似,都是分配内存。调用clone方法时,分配的内存与源对象(即调用clone方法的对象)相同,然后使用原对象中的相应字段。填写新对象的字段。填充完成后clone方法返回,创建一个新的一模一样的对象。这个新对象的引用也可以对外释放。复制对象或复制引用在Java中,类似下面的代码很常见:Personp=newPerson(23,"zhang");Personp1=p;System.out.println(p);System.out.println(p1);当人p1=p;执行后,是否创建了一个新的对象?先看打印结果:com.pansoft.zhangjg.testclone.Person@2f9ee1accom.pansoft.zhangjg.testclone.Person@2f9ee1ac可以看出打印出来的地址值是一样的。由于地址相同,因此它们必须是同一个对象。p和p1只是引用,它们都指向同一个对象Person(23,"zhang")。这种现象可以称为重复引用。(关于引用和对象的区别,可以参考我之前的文章WhyisStringinJavaimmutable?——String源码分析,其中一篇讲了引用和对象的区别)。上面的代码执行后,内存中的场景如下图所示:而下面的代码实际上是克隆了一个对象。Personp=newPerson(23,"zhang");Personp1=(Person)p.clone();System.out.println(p);System.out.println(p1);从打印结果可以看出,两个对象的地址是不同的,也就是说创建了一个新的对象,而不是将原来对象的地址赋值给一个新的引用变量:com.pansoft.zhangjg.testclone.Person@2f9ee1accom.pansoft.zhangjg.testclone.Person@67f1fba0上面的代码执行后,内存中的场景如下图所示:#p#深拷贝还是浅拷贝上面的示例代码中,有两个memberPerson中的变量,即name和age,name是String类型,age是int类型。代码非常简单,看起来像这样:){returnname;}@OverrideprotectedObjectclone()throwsCloneNotSupportedException{return(Person)super.clone();}}既然age是基本数据类型,那么它的复制就不用怀疑了,直接复制一个4字节的整数值即可。但是name是String类型的,它只是一个引用,指向一个真正的String对象,那么复制有两种方式:直接将源对象中name的引用值复制到新对象的name字段中,或者根据原Person对象中name指向的字符串对象新建一个相同的字符串对象,并将这个新字符串对象的引用赋值给新复制的Person对象的name字段。这两种拷贝方式分别称为浅拷贝和深拷贝。深拷贝和浅拷贝的原理如下图所示:下面代码用于验证。如果两个Person对象的名字的地址值相同,说明这两个对象的名字指向同一个String对象,也就是浅拷贝,如果两个对象的name不同,就代表指向不同的String对象,即复制Person对象时,同时复制name引用的String对象,即深拷贝。验证码如下:Personp=newPerson(23,"zhang");Personp1=(Person)p.clone();Stringresult=p.getName()==p1.getName()?"clone是浅拷贝":"克隆是深拷贝";System.out.println(结果);打印出来的结果是:clone是一个浅拷贝。因此,clone方法执行的是浅拷贝。编写程序时要注意这个细节。重写Object中的clone方法实现深拷贝。现在,为了在克隆对象时进行深拷贝,需要Clonable接口重写并实现clone方法。除了调用父类中的clone方法获取新对象外,类中的引用变量也被克隆。如果只是使用Object中默认的clone方法,也就是浅拷贝,再用下面的代码验证一下:{returnsuper.clone();}}staticclassHead/*implementsCloneable*/{publicFaceface;publicHead(){}publicHead(Faceface){this.face=face;}}publicstaticvoidmain(String[]args)throwsCloneNotSupportedException{Bodybody=newBody(newHead());Bodybody1=(Body)body.clone();System.out.println("body==body1:"+(body==body1));System.out.println("body.head==body1.head:"+(body.head==body1.head));}上面代码中主要有两个类,分别是Body和Face。在Body类中,组合了一个Face对象。克隆Body对象时,仅对它组合的Face对象进行浅拷贝。结论可以通过打印结果来验证:body==body1:falsebody.head==body1.head:true如果在克隆的时候想对Body对象进行深拷贝,那么在Body的clone方法中,源对象引用的Head对象也被克隆。staticclassBodyimplementsCloneable{publicHeadhead;publicBody(){}publicBody(Headhead){this.head=head;}??@OverrideprotectedObjectclone()throwsCloneNotSupportedException{BodynewBody=(Body)super.clone();newBody.head=(Head)head.clone();returnnewBody;}}staticclassHeadimplementsCloneable{publicFaceface;publicHead(){}publicHead(Faceface){this.face=face;}@OverrideprotectedObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}publicstaticvoidmain(String[]args)throwsCloneNotSupportedBody{=newBody(newHead());Bodybody1=(Body)body.clone();System.out.println("body==body1:"+(body==body1));System.out.println("body.head==body1.head:"+(body.head==body1.head));}打印结果为:body==body1:falsebody.head==body1.head:false可以看出body和body1的head引用指向了不同的Head对象,也就是说,在克隆Body对象的时候,它所引用的Head对象也被复制,进行深拷贝。#p#真的是深拷贝吗?从上一节的内容,我们可以得出以下结论:如果要对一个对象进行深拷贝,该对象必须实现Cloneable接口,实现clone方法,在clone方法内部,把该对象的其他引用对象必须也被克隆,这就要求被引用的对象也必须实现Cloneable接口,并实现clone方法。那么根据上面的结论,Body类结合了Head类,Head类结合了Face类。如果要深拷贝Body类,必须在Body类的clone方法中同时拷贝Head类,但是在拷贝Head类时,默认进行浅拷贝,也就是说,Face对象合并在Head不会被复制。验证代码如下:(这里只给出Face类的代码就可以了,但是为了读起来更一致,避免丢失上下文信息,还是给出了整个程序,同时也给出了整个程序非常短)staticclassBodyimplementsCloneable{publicHeadhead;publicBody(){}publicBody(Headhead){this.head=head;}??@OverrideprotectedObjectclone()throwsCloneNotSupportedException{BodynewBody=(Body)super.clone();newBody.head=(Head)head。clone();returnnewBody;}}staticclassHeadimplementsCloneable{publicFaceface;publicHead(){}publicHead(Faceface){this.face=face;}@OverrideprotectedObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}staticclassFace{}publicstaticvoidmain(String[]args)throwsCloneNotSupportedException{Bodybody=adnewBody((newFace()));Bodybody1=(Body)body.clone();System.out.println("body==body1:"+(body==body1));System.out.println("body.head==body1.head:"+(body.head==body1.head));System.out.println("body.head.face==body1.head.face:"+(body.head.face==body1.head.face));}打印结果为:body==body1:falsebody.head==body1.head:falsebody.head.face==body1.head.face:true内存结构图如下图所示:那么,对于Body对象来说,这算是深拷贝吗?其实应该算是深拷贝,因为Body对象中引用的其他对象(目前只有Head)被拷贝了,也就是说两个独立的Body对象中的head引用已经指向了两个独立的Head对象.但是,对于两个Head对象来说,它们指向的是同一个Face对象,也就是说这两个Body对象还是有一定联系的,并不是完全独立的。这应该说是一个不完整的深拷贝。如何进行彻底的深拷贝对于上面的例子,我们如何保证两个Body对象是完全独立的呢?复制Head对象时只复制Face对象即可。这就需要Face类也实现Cloneable接口,实现clone方法,在Head对象的clone方法中复制它所引用的Face对象。修改部分代码如下:super.clone();newHead.face=(Face)this.face.clone();returnnewHead;}??}staticclassFaceimplementsCloneable{@OverrideprotectedObjectclone()throwsCloneNotSupportedException{returnsuper.clone();}}再次运行上面的例子,结果如下:body==body1:falsebody.head==body1.head:falsebody.head.face==body1.head.face:false这意味着两个body是完全独立的,face对象是间接的引用已被复制,即对独立Face对象的引用。内存结构图如下:以此类推,如果Face对象还引用了其他对象,比如Mouth,不处理的话,Body对象还是会通过逐级引用引用同一个Mouth对象复制后。同样,如果想让Body在引用链中完全独立,只能显式地让Mouth对象被复制。至此,可以得出如下结论:在复制一个对象时,如果想使被复制对象和源对象完全独立,那么引用链上的每一层对象都必须显式复制。因此,创建一个彻底的深拷贝是非常麻烦的,尤其是当引用关系很复杂,或者在引用链的某一层引用了第三方对象,而这个对象没有实现clone方法时,那么其中所有后续引用的对象都是共享的。比如Head引用的Face类是第三方库中的类,没有实现Cloneable接口,那么Face之后的所有对象都会被复制前后的两个Body对象共同引用。假设Face对象与Mouth对象结合,Mouth对象与Tooth对象结合,内存结构如下:写在***clone中,在正常项目开发中可能用的不是很频繁,但是区分深拷贝和浅拷贝会让我们对java内存结构及其工作原理有更深入的了解。至于完整的深拷贝,几乎是不可能实现的,原因在上一节已经解释过了。深拷贝和彻底的深拷贝,在创建不可变对象时,可能会对程序产生潜移默化的影响,可能会决定我们创建的不可变对象是否真的不可变。克隆的一个重要应用也是用于创建不可变对象。关于不可变对象的创建,我会在后续的文章中进行讲解,敬请期待。
