天天看點

Clone使用方法詳解

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); 

這段代碼的主要部分調用了兩個很相近的方法,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); 

第一句是在記憶體中生成一個新的PassObj對象,然後把這個PassObj的引用賦給變量objA,第二句是把PassObj對象的引用又賦給了變量objB。此時objA和objB是兩個完全一緻的變量,以後任何對objA的改變都等同于對objB的改變。 

即使明白了Java語言中的"指針"概念也許還會不經意間犯下面的錯誤。 

Hashtable真的能存儲對象嗎? 

看一看下面的很簡單的代碼,先是聲明了一個Hashtable和StringBuffer對象,然後分四次把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等都是一樣的。 

上面的例程的實際輸出的結果是: 

2.類,對象與引用

Java最基本的概念就是類,類包括函數和變量。如果想要應用類,就要把類生成對象,這個過程被稱作"類的執行個體化"。有幾種方法把類執行個體化成對象,最常用 的就是用"new"操作符。類執行個體化成對象後,就意味着要在記憶體中占據一塊空間存放執行個體。想要對這塊空間操作就要應用到對象的引用。引用在Java語言中 的展現就是變量,而變量的類型就是這個引用的對象。雖然在文法上可以在生成一個對象後直接調用該對象的函數或變量,如: 

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

但由于沒有相應的引用,對這個對象的使用也隻能局限這條語句中了。 

産生:引用總是在把對象作參數"傳遞"的過程中自動發生,不需要人為的産生,也不能人為的控制引用的産生。這個傳遞包括把對象作為函數的入口參數的情況,也包括用"="進行對象指派的時候。 

範圍:隻有局部的引用,沒有局部的對象。引用在Java語言的展現就是變量,而變量在Java語言中是有範圍的,可以是局部的,也可以是全局的。 

生存期:程式隻能控制引用的生存周期。對象的生存期是由Java控制。用"new Object()"語句生成一個新的對象,是在計算機的記憶體中聲明一塊區域存儲對象,隻有Java的垃圾收集器才能決定在适當的時候回收對象占用的記憶體。 

沒有辦法阻止對引用的改動。

3.java中的clone

3.1.什麼是"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操作符傳回的新對象的差別就是這個拷貝已經包含了一些原來對象的資訊,而不是對象的初始資訊。

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方法。這也解釋了為什麼要用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"。

3.3什麼是影子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); 

輸出的結果說明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後的非基本類型變量和原始對 象中相應的變量指向的是同一個對象。 

大多時候,這種clone的結果往往不是我們所希望的結果,這種clone也被稱為"影子clone"。要想讓b2.unCA指向與b2.unCA不同的對象,而且b2.unCA中還要包含b1.unCA中的資訊作為初始資訊,就要實作深度clone。

3.4怎麼進行深度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); 

可以看出,現在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對象,Integer,Double等是一個例外,它clone後的表現好象也實作了深度clone,雖然這隻是一個假象,但卻大大友善了我們的程式設計。 

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); 

列印的結果可以看出,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); } }  

例子中,雖然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類的時候有了一個更多的 選擇。同時我們也可以把自己的類編成不可更改的類。