Clone使用方法详解

java“指针” java

      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的基本数据类型(如intchardouble等)做为 入口参数传给函数体的时候,传入的参数在函数体内部变成了局部变量,这个局部变量是输入参数的一个拷贝,全部的函数体内部的操做都是针对这个拷贝的操做, 函数执行结束后,这个局部变量也就完成了它的使命,它影响不到做为输入参数的变量。这种方式的参数传递被称为"值传递"。而在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。此时objAobjB是两个彻底一致的变量,之后任何对objA的改变都等同于对objB的改变。 

即便明白了Java语言中的"指针"概念也许还会不经意间犯下面的错误。 

Hashtable真的能存储对象吗? 

看一看下面的很简单的代码,先是声明了一个HashtableStringBuffer对象,而后分四次把StriingBuffer对象放入到Hashtable表中,在每次放入以前都对这个StringBuffer对象append()了一些新的字符串: 

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. 
*/
程序员

2.类,对象与引用 编程


Java最基本的概念就是类,类包括函数和变量。若是想要应用类,就要把类生成对象,这个过程被称做"类的实例化"。有几种方法把类实例化成对象,最经常使用 的就是用"new"操做符。类实例化成对象后,就意味着要在内存中占据一块空间存放实例。想要对这块空间操做就要应用到对象的引用。引用在Java语言中 的体现就是变量,而变量的类型就是这个引用的对象。虽然在语法上能够在生成一个对象后直接调用该对象的函数或变量,如: 

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

但因为没有相应的引用,对这个对象的使用也只能局限这条语句中了。 

产生:引用老是在把对象做参数"传递"的过程当中自动发生,不须要人为的产生,也不能人为的控制引用的产生。这个传递包括把对象做为函数的入口参数的状况,也包括用""进行对象赋值的时候。 
范围:只有局部的引用,没有局部的对象。引用在Java语言的体现就是变量,而变量在Java语言中是有范围的,能够是局部的,也能够是全局的。 
生存期:程序只能控制引用的生存周期。对象的生存期是由Java控制。用"new Object()"语句生成一个新的对象,是在计算机的内存中声明一块区域存储对象,只有Java的垃圾收集器才能决定在适当的时候回收对象占用的内存。 
没有办法阻止对引用的改动。
安全

3.java中的clone app

3.1.什么是"clone" 函数

在实际编程过程当中,咱们经常要遇到这种状况:有一个对象A,在某一时刻A中已经包含了一些有效值,此时可能会须要一个和A彻底相同新对象B,而且此后对任何改动都不会影响到A中的值,也就是说,AB是两个独立的对象,但B的初始值是由A对象肯定的。在Java语言中,用简单的赋值语句是不能知足这种需 求的。要知足这种需求虽然有不少途径,但实现clone()方法是其中最简单,也是最高效的手段。 

Java的全部类都默认继承java.lang.Object类,在java.lang.Object类中有一个方法clone()JDK API的说明文档解释这个方法将返回Object对象的一个拷贝。要说明的有两点:一是拷贝对象返回的是一个新对象,而不是一个引用。二是拷贝对象与用 new操做符返回的新对象的区别就是这个拷贝已经包含了一些原来对象的信息,而不是对象的初始信息。
spa

3.2.怎样应用clone()方法? 指针

一个很典型的调用clone()代码以下: 

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方法。这也解释了为何要用Objectclone()方法而不是先new一个类,而后把原始对象中的信息赋到新对象中,虽然这也实现了 clone功能。对于第二点,也要观察Object类中的clone()仍是一个protected属性的方法。这也意味着若是要应用clone() 法,必须继承Object类,在Java中全部的类是缺省继承Object类的,也就不用关心这点了。而后重载clone()方法。还有一点要考虑的是为 了让其它类能调用这个clone类的clone()方法,重载以后要把clone()方法的属性设置为public 

那么clone类为何还要实现Cloneable接口呢?稍微注意一下,Cloneable接口是不包含任何方法的!其实这个接口仅仅是一个标志,并且 这个标志也仅仅是针对Object类中clone()方法的,若是clone类没有实现Cloneable接口,并调用了Objectclone()法(也就是调用了super.Clone()方法),那么Objectclone()方法就会抛出 CloneNotSupportedException异常。

以上是clone的最基本的步骤,想要完成一个成功的clone,还要了解什么是"影子clone""深度clone"

3.3什么是影子clone

下面的例子包含三个类UnCloneACloneBCloneMainCloneB类包含了一个UnCloneA的实例和一个int类型变量,而且 重载clone()方法。CloneMain类初始化UnCloneA类的一个实例b1,而后调用clone()方法生成了一个b1的拷贝b2。最后考察 一下b1b2的输出: 

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类型的变量aIntUnCloneA的实例对象unCAclone结果不一致,int类型是真正的被clone了,由于改变了 b2中的aInt变量,对b1aInt没有产生影响,也就是说,b2.aIntb1.aInt已经占据了不一样的内存空间,b2.aInt b1.aInt的一个真正拷贝。相反,对b2.unCA的改变同时改变了b1.unCA,很明显,b2.unCAb1.unCA是仅仅指向同一个对象的 不一样引用!从中能够看出,调用Object类中clone()方法产生的效果是:先在内存中开辟一块和原始对象同样的空间,而后原样拷贝原始对象中的内 容。对基本数据类型,这样的操做是没有问题的,但对非基本类型变量,咱们知道它们保存的仅仅是对象的引用,这也致使clone后的非基本类型变量和原始对 象中相应的变量指向的是同一个对象。 

大多时候,这种clone的结果每每不是咱们所但愿的结果,这种clone也被称为"影子clone"。要想让b2.unCA指向与b2.unCA不一样的对象,并且b2.unCA中还要包含b1.unCA中的信息做为初始信息,就要实现深度clone

 

3.4怎么进行深度clone

把上面的例子改为深度clone很简单,须要两个改变:一是让UnCloneA类也实现和CloneB类同样的clone功能(实现Cloneable 口,重载clone()方法)。二是在CloneBclone()方法中加入一句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.unCAb2.unCA指向了两个不一样的UnCloneA实例,并且在 CloneB b2 = (CloneB)b1.clone();调用的那一刻b1b2拥有相同的值,在这里,b1.i = b2.i = 11 

要知道不是全部的类都能实现深度clone的。例如,若是把上面的CloneB类中的UnCloneA类型变量改为StringBuffer类型,看一下 JDK API中关于StringBuffer的说明,StringBuffer没有重载clone()方法,更为严重的是StringBuffer仍是一个 final类,这也是说咱们也不能用继承的办法间接实现StringBufferclone。若是一个类中包含有StringBuffer类型对象或和 StringBuffer类似类的对象,咱们有两种选择:要么只能实现影子clone,要么就在类的clone()方法中加一句(假设是 SringBuffer对象,并且变量名还是unCA): o.unCA = new StringBuffer(unCA.toString()); //原来的是:o.unCA = (UnCloneA)unCA.clone(); 

还要知道的是除了基本数据类型能自动实现深度clone之外,String对象,IntegerDouble等是一个例外,它clone后的表现好象也实现了深度clone,虽然这只是一个假象,但却大大方便了咱们的编程。 

CloneStringStringBuffer的区别 

应该说明的是,这里不是着重说明StringStringBuffer的区别,但从这个例子里也能看出String类的一些不同凡响的地方。 

下面的例子中包括两个类,CloneC类包含一个String类型变量和一个StringBuffer类型变量,而且实现了clone()方法。在 StrClone类中声明了CloneC类型变量c1,而后调用c1clone()方法生成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 

*/ 

打印的结果能够看出,String类型的变量好象已经实现了深度clone,由于对c2.str的改动并无影响到c1.str!难道Java Sring类当作了基本数据类型?其实否则,这里有一个小小的把戏,秘密就在于c2.str = c2.str.substring(0,5)这一语句!实质上,在clone的时候c1.strc2.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类的时候有了一个更多的 选择。同时咱们也能够把本身的类编成不可更改的类。

相关文章
相关标签/搜索