java中的深復制和淺復制


Java 語言的一個優點就是取消了指針的概念,但也導致了許多程序員在編程中常常忽略了對象與引用的區別,本文會試圖澄清這一概念。並且由於Java不能通過簡單 的賦值來解決對象復制的問題,在開發過程中,也常常要要應用clone()方法來復制對象。本文會讓你了解什么是影子clone與深度clone,認識它 們的區別、優點及缺點。 

      看到這個標題,是不是有點困惑:Java語 言明確說明取消了指針,因為指針往往是在帶來方便的同時也是導致代碼不安全的根源,同時也會使程序的變得非常復雜難以理解,濫用指針寫成的代碼不亞於使用 早已臭名昭著的"GOTO"語句。Java放棄指針的概念絕對是極其明智的。但這只是在Java語言中沒有明確的指針定義,實質上每一個new語句返回的 都是一個指針的引用,只不過在大多時候Java中不用關心如何操作這個"指針",更不用象在操作C++的指針那樣膽戰心驚。唯一要多多關心的是在給函數傳 遞對象的時候。

 

[java] view plain copy
  1. package com.zoer.src;  
  2.   
  3. public class ObjRef {  
  4.     Obj aObj = new Obj();  
  5.     int aInt = 11;  
  6.   
  7.     public void changeObj(Obj inObj) {  
  8.         inObj.str = "changed value";  
  9.     }  
  10.   
  11.     public void changePri(int inInt) {  
  12.         inInt = 22;  
  13.     }  
  14.   
  15.     public static void main(String[] args) {  
  16.         ObjRef oRef = new ObjRef();  
  17.   
  18.         System.out.println("Before call changeObj() method: " + oRef.aObj);  
  19.         oRef.changeObj(oRef.aObj);  
  20.         System.out.println("After call changeObj() method: " + oRef.aObj);  
  21.   
  22.         System.out.println("==================Print Primtive=================");  
  23.         System.out.println("Before call changePri() method: " + oRef.aInt);  
  24.         oRef.changePri(oRef.aInt);  
  25.         System.out.println("After call changePri() method: " + oRef.aInt);  
  26.   
  27.     }  
  28. }  
[java] view plain copy
  1. package com.zoer.src;  
  2.   
  3. public class Obj {  
  4.   
  5.     String str = "init value";  
  6.   
  7.     public String toString() {  
  8.         return str;  
  9.     }  
  10. }  

      這段代碼的主要部分調用了兩個很相近的方法,changeObj()和changePri()。唯一不同的是它們一個把對象作為輸入參數,另一個把 Java中的基本類型int作為輸入參數。並且在這兩個函數體內部都對輸入的參數進行了改動。看似一樣的方法,程序輸出的結果卻不太一樣。 changeObj()方法真正的把輸入的參數改變了,而changePri()方法對輸入的參數沒有任何的改變。 

      從這個例子知道Java對對象和基本的數據類型的處理是不一樣的。和C語言一樣,當把Java的基本數據類型(如int,char,double等)作為入口參數傳給函數體的時候,傳入的參數在函數體內部變成了局部變量,這個局部變量是輸入參數的一個拷貝,所有的函數體內部的操作都是針對這個拷貝的操作,函數執行結束后,這個局部變量也就完成了它的使命,它影響不到作為輸入參數的變量。這種方式的參數傳遞被稱為"值傳遞"。而在Java中用對象作為入口參數的傳遞則缺省為"引用傳遞"也就是說僅僅傳遞了對象的一個"引用"這個"引用"的概念同C語言中的指針引用是一樣的。當函數體內部對輸入變量改變時,實質上就是在對這個對象的直接操作。 

      除了在函數傳值的時候是"引用傳遞",在任何用"="向對象變量賦值的時候都是"引用傳遞"。就是類似於給變量再起一個別名。兩個名字都指向內存中的同一個對象。
      在實際編程過程中,我們常常要遇到這種情況:有一個對象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()代碼如下: 

 

[java] view plain copy
  1. public class CloneClass implements Cloneable {  
  2.     public int aInt;  
  3.   
  4.     public Object clone() {  
  5.         CloneClass o = null;  
  6.         try {  
  7.             o = (CloneClass) super.clone();  
  8.         } catch (CloneNotSupportedException e) {  
  9.             e.printStackTrace();  
  10.         }  
  11.         return o;  
  12.     }  
  13. }  

      有三個值得注意的地方,一是希望能實現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? 

[java] view plain copy
  1. package com.zoer.src;  
  2.   
  3. class UnCloneA {  
  4.     private int i;  
  5.   
  6.     public UnCloneA(int ii) {  
  7.         i = ii;  
  8.     }  
  9.   
  10.     public void doublevalue() {  
  11.         i *= 2;  
  12.     }  
  13.   
  14.     public String toString() {  
  15.         return Integer.toString(i);  
  16.     }  
  17. }  
  18.   
  19. class CloneB implements Cloneable {  
  20.     public int aInt;  
  21.     public UnCloneA unCA = new UnCloneA(111);  
  22.   
  23.     public Object clone() {  
  24.         CloneB o = null;  
  25.         try {  
  26.             o = (CloneB) super.clone();  
  27.         } catch (CloneNotSupportedException e) {  
  28.             e.printStackTrace();  
  29.         }  
  30.         return o;  
  31.     }  
  32. }  
  33.   
  34. public class ObjRef {  
  35.     public static void main(String[] a) {  
  36.         CloneB b1 = new CloneB();  
  37.         b1.aInt = 11;  
  38.         System.out.println("before clone,b1.aInt = " + b1.aInt);  
  39.         System.out.println("before clone,b1.unCA = " + b1.unCA);  
  40.   
  41.         CloneB b2 = (CloneB) b1.clone();  
  42.         b2.aInt = 22;  
  43.         b2.unCA.doublevalue();  
  44.         System.out.println("=================================");  
  45.         System.out.println("after clone,b1.aInt = " + b1.aInt);  
  46.         System.out.println("after clone,b1.unCA = " + b1.unCA);  
  47.         System.out.println("=================================");  
  48.         System.out.println("after clone,b2.aInt = " + b2.aInt);  
  49.         System.out.println("after clone,b2.unCA = " + b2.unCA);  
  50.     }  
  51. }  

       輸出結果:

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

 

[java] view plain copy
  1. package com.zoer.src;  
  2.   
  3. class UnCloneA implements Cloneable {  
  4.     private int i;  
  5.   
  6.     public UnCloneA(int ii) {  
  7.         i = ii;  
  8.     }  
  9.   
  10.     public void doublevalue() {  
  11.         i *= 2;  
  12.     }  
  13.   
  14.     public String toString() {  
  15.         return Integer.toString(i);  
  16.     }  
  17.   
  18.     public Object clone() {  
  19.         UnCloneA o = null;  
  20.         try {  
  21.             o = (UnCloneA) super.clone();  
  22.         } catch (CloneNotSupportedException e) {  
  23.             e.printStackTrace();  
  24.         }  
  25.         return o;  
  26.     }  
  27. }  
  28.   
  29. class CloneB implements Cloneable {  
  30.     public int aInt;  
  31.     public UnCloneA unCA = new UnCloneA(111);  
  32.   
  33.     public Object clone() {  
  34.         CloneB o = null;  
  35.         try {  
  36.             o = (CloneB) super.clone();  
  37.         } catch (CloneNotSupportedException e) {  
  38.             e.printStackTrace();  
  39.         }  
  40.         o.unCA = (UnCloneA) unCA.clone();  
  41.         return o;  
  42.     }  
  43. }  
  44.   
  45. public class CloneMain {  
  46.     public static void main(String[] a) {  
  47.         CloneB b1 = new CloneB();  
  48.         b1.aInt = 11;  
  49.         System.out.println("before clone,b1.aInt = " + b1.aInt);  
  50.         System.out.println("before clone,b1.unCA = " + b1.unCA);  
  51.   
  52.         CloneB b2 = (CloneB) b1.clone();  
  53.         b2.aInt = 22;  
  54.         b2.unCA.doublevalue();  
  55.         System.out.println("=================================");  
  56.         System.out.println("after clone,b1.aInt = " + b1.aInt);  
  57.         System.out.println("after clone,b1.unCA = " + b1.unCA);  
  58.         System.out.println("=================================");  
  59.         System.out.println("after clone,b2.aInt = " + b2.aInt);  
  60.         System.out.println("after clone,b2.unCA = " + b2.unCA);  
  61.     }  
  62. }  

輸出結果:

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,雖然這只是一個假象,但卻大大方便了我們的編程。 
       Clone中String和StringBuffer的區別 
       應該說明的是,這里不是着重說明String和StringBuffer的區別,但從這個例子里也能看出String類的一些與眾不同的地方。 
       下面的例子中包括兩個類,CloneC類包含一個String類型變量和一個StringBuffer類型變量,並且實現了clone()方法。在 StrClone類中聲明了CloneC類型變量c1,然后調用c1的clone()方法生成c1的拷貝c2,在對c2中的String和 StringBuffer類型變量用相應的方法改動之后打印結果: 

 

[java] view plain copy
  1. package com.zoer.src;  
  2.   
  3. class CloneC implements Cloneable {  
  4.     public String str;  
  5.     public StringBuffer strBuff;  
  6.   
  7.     public Object clone() {  
  8.         CloneC o = null;  
  9.         try {  
  10.             o = (CloneC) super.clone();  
  11.         } catch (CloneNotSupportedException e) {  
  12.             e.printStackTrace();  
  13.         }  
  14.         return o;  
  15.     }  
  16.   
  17. }  
  18.   
  19. public class StrClone {  
  20.     public static void main(String[] a) {  
  21.         CloneC c1 = new CloneC();  
  22.         c1.str = new String("initializeStr");  
  23.         c1.strBuff = new StringBuffer("initializeStrBuff");  
  24.         System.out.println("before clone,c1.str = " + c1.str);  
  25.         System.out.println("before clone,c1.strBuff = " + c1.strBuff);  
  26.   
  27.         CloneC c2 = (CloneC) c1.clone();  
  28.         c2.str = c2.str.substring(0, 5);  
  29.         c2.strBuff = c2.strBuff.append(" change strBuff clone");  
  30.         System.out.println("=================================");  
  31.         System.out.println("after clone,c1.str = " + c1.str);  
  32.         System.out.println("after clone,c1.strBuff = " + c1.strBuff);  
  33.         System.out.println("=================================");  
  34.         System.out.println("after clone,c2.str = " + c2.str);  
  35.         System.out.println("after clone,c2.strBuff = " + c2.strBuff);  
  36.     }  
  37. }  

執行結果:

 

[java] view plain copy
  1. <span style="font-family:'Microsoft YaHei';"><span style="font-size:16px;">before clone,c1.str = initializeStr  
  2. before clone,c1.strBuff = initializeStrBuff  
  3. =================================  
  4. after clone,c1.str = initializeStr  
  5. after clone,c1.strBuff = initializeStrBuff change strBuff clone  
  6. =================================  
  7. after clone,c2.str = initi  
  8. after clone,c2.strBuff = initializeStrBuff change strBuff clone  
  9. </span></span>  

        打印的結果可以看出,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類中的函數都不能更改自身的值。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM