JAVA中的指標,引用及物件的clone

yangxi_001發表於2013-12-05

看到這個標題,是不是有點困惑: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("==================PrintPrimtive=================");

       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: changedvalue

==================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語言中的"指標"概念也許還會不經意間犯下面的錯誤。

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)+" fromHashtable: ");

           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("HelloNDP")).substring(0,3)  //RETURN RESULT: Hel

 

但由於沒有相應的引用,對這個物件的使用也只能侷限這條語句中了。

產生:引用總是在把物件作引數"傳遞"的過程中自動發生,不需要人為的產生,也不能人為的控制引用的產生。這個傳遞包括把物件作為函式的入口引數的情況,也包括用"="進行物件賦值的時候。

範圍:只有區域性的引用,沒有區域性的物件。引用在Java語言的體現就是變數,而變數在Java語言中是有範圍的,可以是區域性的,也可以是全域性的。

生存期:程式只能控制引用的生存週期。物件的生存期是由Java控制。用"newObject()"語句生成一個新的物件,是在計算機的記憶體中宣告一塊區域儲存物件,只有Java的垃圾收集器才能決定在適當的時候回收物件佔用的記憶體。

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

回頁首

什麼是"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()程式碼如下:

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"。

回頁首

什麼是影子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後的非基本型別變數和原始物件中相應的變數指向的是同一個物件。

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

回頁首

怎麼進行深度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 = newStringBuffer(unCA.toString()); //原來的是:o.unCA = (UnCloneA)unCA.clone();

還要知道的是除了基本資料型別能自動實現深度clone以外,String物件是一個例外,它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 = newStringBuffer("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 = initializeStrBuffchange strBuff clone

=================================

after clone,c2.str = initi

after clone,c2.strBuff = initializeStrBuffchange strBuff clone

*

*/

 

列印的結果可以看出,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 forimmutable"; String str2 = str1.substring(0,8);System.out.println("print str1 : " + str1);System.out.println("print str2 : " + str2); } } /* RUN RESULT printstr1 : 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 strBuffclone");

 

去掉了重新賦值的過程,c2.str也就不能有變化了,我們的把戲也就露餡了。但在程式設計過程中只呼叫

c2.str.substring(0,5);

 

語句是沒有任何意義的。

應該知道的是在Java中所有的基本資料型別都有一個相對應的類,象Integer類對應int型別,Double類對應double型別等等,這些類也與String類相同,都是不可以改變的類。也就是說,這些的類中的所有方法都是不能改變其自身的值的。這也讓我們在編clone類的時候有了一個更多的選擇。同時我們也可以把自己的類編成不可更改的類。

 


相關文章