Clone使用方法詳解 java clone詳解 收藏 java clone詳解 收藏

Clone 使用方法詳解
1. java “指針” ... 1
2. 類,對象與引用 ... 4
3.java 中的 clone . 5
3.1. 什麼是 "clone" ? ... 5
3.2. 怎樣應用 clone() 方法? ... 5
3.3 什麼是影子 clone ? ... 6
3.4 怎麼進行深度 clone ? ... 7
1. 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;
}
}
有三個值得注意的地方,
l 一是希望能實作 clone 功能的 CloneClass 類實作了 Cloneable 接口,這個接口屬于 java.lang 包, java.lang 包已經被預設的導入類中,是以不需要寫成 java.lang.Cloneable 。
l 另一個值得請注意的是重載了 clone() 方法。最 後在 clone() 方法中調用了 super.clone() ,這也意味着無論 clone 類的繼承結構是什麼樣的, super.clone() 直接或間接調 用了 java.lang.Object 類的 clone() 方法。下面再詳細的解釋一下這幾點。
l 應該說第三點是最重要的,仔細觀察一下 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 類的時候有了一個更多的 選擇。同時我們也可以把自己的類編成不可更改的類。