当前位置:天才代写 > tutorial > JAVA 教程 > 浅析Java的“克隆”要领

浅析Java的“克隆”要领

2017-11-11 08:00 星期六 所属: JAVA 教程 浏览:278

副标题#e#

Java语言的一个利益就是打消了指针的观念,但也导致了很多措施员在编程中经常忽略了工具与引用的区别,本文会试图澄清这一观念。而且由于Java不能通过简朴的赋值来办理工具复制的问题,在开拓进程中,也经常要要应用clone()要领来复制工具。本文会让你相识什么是影子clone与深度clone,认识它们的区别、利益及缺点。

看到这个标题,是不是有点狐疑:Java语言明晰说明打消了指针,因为指针往往是在带来利便的同时也是导致代码不安详的来源,同时也会使措施的变得很是巨大难以领略,滥用指针写成的代码不亚于利用早已污名昭著的"GOTO"语句。Java放弃指针的观念绝对是极其明智的。但这只是在Java语言中没有明晰的指针界说,实质上每一个new语句返回的都是一个指针的引用,只不外在大多时候Java中不消体贴如何操纵这个"指针",更不消象在操纵C++的指针那样提心吊胆。独一要多多体贴的是在给函数通报工具的时候。如下例程:

package reference;
class Obj{
 String str = "init value";
 public String toString(){
  return str;
 }
}
public class ObjRef{
 Obj aObj = new Obj();
 int aInt = 11;
 public void changeObj(Obj inObj){
  inObj.str = "changed value";
 }
 public void changePri(int inInt){
  inInt = 22;
 }
 public static void main(String[] args)
 {
  ObjRef oRef = new ObjRef();
  System.out.println("Before call changeObj() method: " + oRef.aObj);
  oRef.changeObj(oRef.aObj);
  System.out.println("After call changeObj() method: " + oRef.aObj);
  System.out.println("==================Print Primtive=================");
  System.out.println("Before call changePri() method: " + oRef.aInt);
  oRef.changePri(oRef.aInt);
  System.out.println("After call changePri() method: " + oRef.aInt);
 }
}
/* RUN RESULT
Before call changeObj() method: init value
After call changeObj() method: changed value
==================Print Primtive=================
Before call changePri() method: 11
After call changePri() method: 11
*
*/

这段代码的主要部门挪用了两个很临近的要领,changeObj()和changePri()。独一差异的是它们一个把工具作为输入参数,另一个把Java中的根基范例int作为输入参数。而且在这两个函数体内部都对输入的参数举办了窜改。看似一样的要领,措施输出的功效却不太一样。changeObj()要领真正的把输入的参数改变了,而changePri()要领对输入的参数没有任何的改变。

从这个例子知道Java对工具和根基的数据范例的处理惩罚是纷歧样的。和C语言一样,当把Java的根基数据范例(如int,char,double等)作为进口参数传给函数体的时候,传入的参数在函数体内部酿成结局部变量,这个局部变量是输入参数的一个拷贝,所有的函数体内部的操纵都是针对这个拷贝的操纵,函数执行竣事后,这个局部变量也就完成了它的使命,它影响不到作为输入参数的变量。这种方法的参数通报被称为"值通报"。而在Java顶用工具的作为进口参数的通报则缺省为"引用通报",也就是说仅仅通报了工具的一个"引用",这个"引用"的观念同C语言中的指针引用是一样的。当函数体内部对输入变量改变时,实质上就是在对这个工具的直接操纵。

除了在函数传值的时候是"引用通报",在任何用"="向工具变量赋值的时候都是"引用通报"。如:

package reference;
class PassObj
{
 String str = "init value";
}
public class ObjPassvalue
{
 public static void main(String[] args)
 {
  PassObj objA = new PassObj();
  PassObj objB = objA;
  objA.str = "changed in objA";
  System.out.println("Print objB.str value: " + objB.str);
 }
}
/* RUN RESULT
Print objB.str value: changed in objA
*/

第一句是在内存中生成一个新的PassObj工具,然后把这个PassObj的引用赋给变量objA,第二句是把PassObj工具的引用又赋给了变量objB。此时objA和objB是两个完全一致的变量,今后任何对objA的改变都等同于对objB的改变。

纵然大白了Java语言中的"指针"观念也许还会不经意间犯下面的错误。


#p#副标题#e#

Hashtable真的能存储工具吗?

看一看下面的很简朴的代码,先是声明白一个Hashtable和StringBuffer工具,然后分四次把StriingBuffer工具放入到Hashtable表中,在每次放入之前都对这个StringBuffer工具append()了一些新的字符串:

#p#分页标题#e#

package reference;
import java.util.*;
public class HashtableAdd{
 public static void main(String[] args){
  Hashtable ht = new Hashtable();
  StringBuffer sb = new StringBuffer();
  sb.append("abc,");
  ht.put("1",sb);
  sb.append("def,");
  ht.put("2",sb);
  sb.append("mno,");
  ht.put("3",sb);
  sb.append("xyz.");
  ht.put("4",sb);
  int numObj=0;
  Enumeration it = ht.elements();
  while(it.hasMoreElements()){
   System.out.print("get StringBufffer "+(++numObj)+" from Hashtable: ");
   System.out.println(it.nextElement());
  }
 }
}

假如你认为输出的功效是:

get StringBufffer 1 from Hashtable: abc,
get StringBufffer 2 from Hashtable: abc,def,
get StringBufffer 3 from Hashtable: abc,def,mno,
get StringBufffer 4 from Hashtable: abc,def,mno,xyz.

那么你就要回过甚再仔细看一看上一个问题了,把工具时作为进口参数传给函数,实质上是通报了工具的引用,向Hashtable通报StringBuffer工具也是只通报了这个StringBuffer工具的引用!每一次向Hashtable表中put一次StringBuffer,并没有生成新的StringBuffer工具,只是在Hashtable表中又放入了一个指向同一StringBuffer工具的引用罢了。

对Hashtable表存储的任何一个StringBuffer工具(更确切的说应该是工具的引用)的窜改,实际上都是对同一个"StringBuffer"的窜改。所以Hashtable并不能真正存储能工具,而只能存储工具的引用。也应该知道这条原则对与Hashtable相似的Vector, List, Map, Set等都是一样的。

上面的例程的实际输出的功效是:

/* RUN RESULT
get StringBufffer 1 from Hashtable: abc,def,mno,xyz.
get StringBufffer 2 from Hashtable: abc,def,mno,xyz.
get StringBufffer 3 from Hashtable: abc,def,mno,xyz.
get StringBufffer 4 from Hashtable: abc,def,mno,xyz.
*/

类,工具与引用

Java最根基的观念就是类,类包罗函数和变量。假如想要应用类,就要把类生成工具,这个进程被称作"类的实例化"。有几种要领把类实例化成工具,最常用的就是用"new"操纵符。类实例化成工具后,就意味着要在内存中占据一块空间存放实例。想要对这块空间操纵就要应用到工具的引用。引用在Java语言中的浮现就是变量,而变量的范例就是这个引用的工具。固然在语法上可以在生成一个工具后直接挪用该工具的函数或变量,如:

new String("Hello NDP")).substring(0,3)  //RETURN RESULT: Hel

但由于没有相应的引用,对这个工具的利用也只能范围这条语句中了。

发生:引用老是在把工具作参数"通报"的进程中自动产生,不需要工钱的发生,也不能工钱的节制引用的发生。这个通报包罗把工具作为函数的进口参数的环境,也包罗用"="举办工具赋值的时候。

范畴:只有局部的引用,没有局部的工具。引用在Java语言的浮现就是变量,而变量在Java语言中是有范畴的,可以是局部的,也可以是全局的。

保留期:措施只能节制引用的保留周期。工具的保留期是由Java节制。用"new Object()"语句生成一个新的工具,是在计较机的内存中声明一块区域存储工具,只有Java的垃圾收集器才气抉择在适当的时候接纳工具占用的内存。

没有步伐阻止对引用的窜改。

#p#副标题#e#

什么是"clone"?

在实际编程进程中,我们经常要碰着这种环境:有一个工具A,在某一时刻A中已经包括了一些有效值,此时大概会需要一个和A完全沟通新工具B,而且从此对B任何窜改都不会影响到A中的值,也就是说,A与B是两个独立的工具,但B的初始值是由A工具确定的。在Java语言中,用简朴的赋值语句是不能满意这种需求的。要满意这种需求固然有许多途径,但实现clone()要领是个中最简朴,也是最高效的手段。

Java的所有类都默认担任java.lang.Object类,在java.lang.Object类中有一个要领clone()。JDK API的说明文档表明这个要领将返回Object工具的一个拷贝。要说明的有两点:一是拷贝工具返回的是一个新工具,而不是一个引用。二是拷贝工具与用new操纵符返回的新工具的区别就是这个拷贝已经包括了一些本来工具的信息,而不是工具的初始信息。

奈何应用clone()要领?

一个很典范的挪用clone()代码如下:

#p#分页标题#e#

class CloneClass implements Cloneable{
 public int aInt;
 public Object clone(){
  CloneClass o = null;
  try{
   o = (CloneClass)super.clone();
  }catch(CloneNotSupportedException e){
   e.printStackTrace();
  }
  return o;
 }

有三个值得留意的处所,一是但愿能实现clone成果的CloneClass类实现了Cloneable接口,这个接口属于java.lang包,java.lang包已经被缺省的导入类中,所以不需要写成java.lang.Cloneable。另一个值得请留意的是重载了clone()要领。最后在clone()要领中挪用了super.clone(),这也意味着无论clone类的担任布局是什么样的,super.clone()直接或间接挪用了java.lang.Object类的clone()要领。下面再具体的表明一下这几点。

应该说第三点是最重要的,仔细调查一下Object类的clone()一个native要领,native要领的效率一般来说都是远高于java中的非native要领。这也表明白为什么要用Object中clone()要领而不是先new一个类,然后把原始工具中的信息赋到新工具中,固然这也实现了clone成果。对付第二点,也要调查Object类中的clone()照旧一个protected属性的要领。这也意味着假如要应用clone()要领,必需担任Object类,在Java中所有的类是缺省担任Object类的,也就不消体贴这点了。然后重载clone()要领。尚有一点要思量的是为了让其它类能挪用这个clone类的clone()要领,重载之后要把clone()要领的属性配置为public。

那么clone类为什么还要实现Cloneable接口呢?稍微留意一下,Cloneable接口是不包括任何要领的!其实这个接口仅仅是一个符号,并且这个符号也仅仅是针对Object类中clone()要领的,假如clone类没有实现Cloneable接口,并挪用了Object的clone()要领(也就是挪用了super.Clone()要领),那么Object的clone()要领就会抛出CloneNotSupportedException异常。

以上是clone的最根基的步调,想要完成一个乐成的clone,还要相识什么是"影子clone"和"深度clone"。

#p#副标题#e#

什么是影子clone?

下面的例子包括三个类UnCloneA,CloneB,CloneMain。CloneB类包括了一个UnCloneA的实例和一个int范例变量,而且重载clone()要领。CloneMain类初始化UnCloneA类的一个实例b1,然后挪用clone()要领生成了一个b1的拷贝b2。最后考查一下b1和b2的输出:

package clone;
class UnCloneA {
 private int i;
 public UnCloneA(int ii) { i = ii; }
 public void doublevalue() { i *= 2; }
 public String toString() {
  return Integer.toString(i);
 }
}
class CloneB implements Cloneable{
 public int aInt;
 public UnCloneA unCA = new UnCloneA(111);
 public Object clone(){
  CloneB o = null;
  try{
   o = (CloneB)super.clone();
  }catch(CloneNotSupportedException e){
   e.printStackTrace();
  }
  return o;
 }
}
public class CloneMain {
 public static void main(String[] a){
  CloneB b1 = new CloneB();
  b1.aInt = 11;
  System.out.println("before clone,b1.aInt = "+ b1.aInt);
  System.out.println("before clone,b1.unCA = "+ b1.unCA);
  CloneB b2 = (CloneB)b1.clone();
  b2.aInt = 22;
  b2.unCA.doublevalue();
  System.out.println("=================================");
  System.out.println("after clone,b1.aInt = "+ b1.aInt);
  System.out.println("after clone,b1.unCA = "+ b1.unCA);
  System.out.println("=================================");
  System.out.println("after clone,b2.aInt = "+ b2.aInt);
  System.out.println("after clone,b2.unCA = "+ b2.unCA);
 }
}
/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 222
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/

输出的功效说明int范例的变量aInt和UnCloneA的实例工具unCA的clone功效纷歧致,int范例是真正的被clone了,因为改变了b2中的aInt变量,对b1的aInt没有发生影响,也就是说,b2.aInt与b1.aInt已经占据了差异的内存空间,b2.aInt是b1.aInt的一个真正拷贝。相反,对b2.unCA的改变同时改变了b1.unCA,很明明,b2.unCA和b1.unCA是仅仅指向同一个工具的差异引用!从中可以看出,挪用Object类中clone()要领发生的结果是:先在内存中开发一块和原始工具一样的空间,然后原样拷贝原始工具中的内容。对根基数据范例,这样的操纵是没有问题的,但对非根基范例变量,我们知道它们生存的仅仅是工具的引用,这也导致clone后的非根基范例变量和原始工具中相应的变量指向的是同一个工具。

#p#分页标题#e#

大多时候,这种clone的功效往往不是我们所但愿的功效,这种clone也被称为"影子clone"。要想让b2.unCA指向与b2.unCA差异的工具,并且b2.unCA中还要包括b1.unCA中的信息作为初始信息,就要实现深度clone。

#p#副标题#e#

怎么举办深度clone?

把上面的例子改成深度clone很简朴,需要两个改变:一是让UnCloneA类也实现和CloneB类一样的clone成果(实现Cloneable接口,重载clone()要领)。二是在CloneB的clone()要领中插手一句o.unCA = (UnCloneA)unCA.clone();

措施如下:

package clone.ext;
class UnCloneA implements Cloneable{
 private int i;
 public UnCloneA(int ii) { i = ii; }
 public void doublevalue() { i *= 2; }
 public String toString() {
 return Integer.toString(i);
}
public Object clone(){
 UnCloneA o = null;
 try{
  o = (UnCloneA)super.clone();
 }catch(CloneNotSupportedException e){
  e.printStackTrace();
 }
 return o;
}
}
class CloneB implements Cloneable{
 public int aInt;
 public UnCloneA unCA = new UnCloneA(111);
 public Object clone(){
  CloneB o = null;
  try{
   o = (CloneB)super.clone();
  }catch(CloneNotSupportedException e){
   e.printStackTrace();
  }
  o.unCA = (UnCloneA)unCA.clone();
  return o;
 }
}
public class CloneMain {
 public static void main(String[] a){
  CloneB b1 = new CloneB();
  b1.aInt = 11;
  System.out.println("before clone,b1.aInt = "+ b1.aInt);
  System.out.println("before clone,b1.unCA = "+ b1.unCA);
  CloneB b2 = (CloneB)b1.clone();
  b2.aInt = 22;
  b2.unCA.doublevalue();
  System.out.println("=================================");
  System.out.println("after clone,b1.aInt = "+ b1.aInt);
  System.out.println("after clone,b1.unCA = "+ b1.unCA);
  System.out.println("=================================");
  System.out.println("after clone,b2.aInt = "+ b2.aInt);
  System.out.println("after clone,b2.unCA = "+ b2.unCA);
 }
}
/** RUN RESULT:
before clone,b1.aInt = 11
before clone,b1.unCA = 111
=================================
after clone,b1.aInt = 11
after clone,b1.unCA = 111
=================================
after clone,b2.aInt = 22
after clone,b2.unCA = 222
*/

可以看出,此刻b2.unCA的改变对b1.unCA没有发生影响。此时b1.unCA与b2.unCA指向了两个差异的UnCloneA实例,并且在CloneB b2 = (CloneB)b1.clone();挪用的那一刻b1和b2拥有沟通的值,在这里,b1.i = b2.i = 11。

要知道不是所有的类都能实现深度clone的。譬喻,假如把上面的CloneB类中的UnCloneA范例变量改成StringBuffer范例,看一下JDK API中关于StringBuffer的说明,StringBuffer没有重载clone()要领,更为严重的是StringBuffer照旧一个final类,这也是说我们也不能用担任的步伐间接实现StringBuffer的clone。假如一个类中包括有StringBuffer范例工具或和StringBuffer相似类的工具,我们有两种选择:要么只能实现影子clone,要么就在类的clone()要领中加一句(假设是SringBuffer工具,并且变量名仍是unCA): o.unCA = new StringBuffer(unCA.toString()); //本来的是:o.unCA = (UnCloneA)unCA.clone();

还要知道的是除了根基数据范例能自动实现深度clone以外,String工具是一个破例,它clone后的表示好象也实现了深度clone,固然这只是一个假象,但却大大利便了我们的编程。

#p#副标题#e#

Clone中String和StringBuffer的区别

应该说明的是,这里不是着重说明String和StringBuffer的区别,但从这个例子里也能看出String类的一些与众差异的处所。

下面的例子中包罗两个类,CloneC类包括一个String范例变量和一个StringBuffer范例变量,而且实现了clone()要领。在StrClone类中声明白CloneC范例变量c1,然后挪用c1的clone()要领生成c1的拷贝c2,在对c2中的String和StringBuffer范例变量用相应的要领窜改之后打印功效:

package clone;
class CloneC implements Cloneable{
 public String str;
 public StringBuffer strBuff;
 public Object clone(){
  CloneC o = null;
  try{
   o = (CloneC)super.clone();
  }catch(CloneNotSupportedException e){
   e.printStackTrace();
  }
  return o;
 }
}
public class StrClone {
 public static void main(String[] a){
  CloneC c1 = new CloneC();
  c1.str = new String("initializeStr");
  c1.strBuff = new StringBuffer("initializeStrBuff");
  System.out.println("before clone,c1.str = "+ c1.str);
  System.out.println("before clone,c1.strBuff = "+ c1.strBuff);
  CloneC c2 = (CloneC)c1.clone();
  c2.str = c2.str.substring(0,5);
  c2.strBuff = c2.strBuff.append(" change strBuff clone");
  System.out.println("=================================");
  System.out.println("after clone,c1.str = "+ c1.str);
  System.out.println("after clone,c1.strBuff = "+ c1.strBuff);
  System.out.println("=================================");
  System.out.println("after clone,c2.str = "+ c2.str);
  System.out.println("after clone,c2.strBuff = "+ c2.strBuff);
 }
}
/* RUN RESULT
before clone,c1.str = initializeStr
before clone,c1.strBuff = initializeStrBuff
=================================
after clone,c1.str = initializeStr
after clone,c1.strBuff = initializeStrBuff change strBuff clone
=================================
after clone,c2.str = initi
after clone,c2.strBuff = initializeStrBuff change strBuff clone
*
*/

#p#分页标题#e#

打印的功效可以看出,String范例的变量好象已经实现了深度clone,因为对c2.str的窜改并没有影响到c1.str!莫非Java把Sring类当作了根基数据范例?其实否则,这里有一个小小的花招,奥秘就在于c2.str = c2.str.substring(0,5)这一语句!实质上,在clone的时候c1.str与c2.str仍然是引用,并且都指向了同一个String工具。但在执行c2.str = c2.str.substring(0,5)的时候,它浸染相当于生成了一个新的String范例,然后又赋回给c2.str。这是因为String被Sun公司的工程师写成了一个不行变动的类(immutable class),在所有String类中的函数都不能变动自身的值。下面给出很简朴的一个例子:

package clone;
public class StrTest
{
 public static void main(String[] args)
 {
  String str1 = "This is a test for immutable";
  String str2 = str1.substring(0,8);
  System.out.println("print str1 : " + str1);
  System.out.println("print str2 : " + str2);
 }
}
/* RUN RESULT print str1 : This is a test for immutable print str2 : This is */

例子中,固然str1挪用了substring()要领,但str1的值并没有改变。雷同的,String类中的其它要领也是如此。虽然假如我们把最上面的例子中的这两条语句

c2.str = c2.str.substring(0,5);
c2.strBuff = c2.strBuff.append(" change strBuff clone");

改成下面这样:

c2.str.substring(0,5);
c2.strBuff.append(" change strBuff clone");

去掉了从头赋值的进程,c2.str也就不能有变革了,我们的花招也就露馅了。但在编程进程中只挪用

c2.str.substring(0,5);

语句是没有任何意义的。

应该知道的是在Java中所有的根基数据范例都有一个相对应的类,象Integer类对应int范例,Double类对应double范例等等,这些类也与String类沟通,都是不行以改变的类。也就是说,这些的类中的所有要领都是不能改变其自身的值的。这也让我们在编clone类的时候有了一个更多的选择。同时我们也可以把本身的类编成不行变动的类。

 

    关键字:

天才代写-代写联系方式