1. 程式人生 > >java中傳值和傳址及其引伸深度克隆的思考

java中傳值和傳址及其引伸深度克隆的思考

       大家都知道java中沒有指標。難道java真的沒有指標嗎?控制代碼是什麼?變數地址在哪裡?沒有地址的話簡直不可想象!

      java中記憶體的分配方式有兩種,一種是在堆中分配,一種是在堆疊中分配,所有new出來的物件都是在堆中分配的,函式中引數的傳遞是在棧中分配的。通常情況下堆的記憶體可以很大,比如32位作業系統中的虛擬記憶體都可以被堆所使用(當記憶體緊張的時候甚至硬碟都可以是堆的儲存空間),而堆疊的記憶體分配是有限的。這和c++中記憶體分配差不多(c++中還要有另一種方式用於全域性變數或者區域性靜態變數的記憶體分配,這裡就不說了)。java中有幾種基本型別如int,float,double,char,byte等,他們不是物件,除此之外一切都是物件,所有的物件都是在堆上分配的。java中物件陣列是什麼,和c++類似,是控制代碼陣列或者叫指標陣列,裡面儲存的是每個元素的地址。和c++中不同,java沒有操作符過載和拷貝建構函式(如果不瞭解這些也沒有關係),因此當建立物件或者對已經建立的物件賦值時(注意是物件,不是基本型別):Object a=new Object 和Object a=b(b是Object的子型別或者同類型)時,進行的是物件地址的傳遞並複製。這就是所說的控制代碼的傳遞和賦值。控制代碼裡儲存的就是物件的地址,控制代碼就是指標,只不過是你無法得到的地址,java就是通過這一點巧妙的將指標隱藏起來。當物件作為引數傳遞到方法中時,傳遞的就是物件的地址,而行參中儲存的是實參地址的副本(這就是最關鍵的地方,也是值傳遞,值傳遞就是將實參的值的副本作為行參)如:


public class Example{
  int i=0;
}
public class A{
  public int i=0;
  public Example add0(Example e)
  {
     e.i++; 
     return e; 
   }
   
   public void add1(Example e)
   {
      e.i++;
   }

   public void modify0(Example e)
   { 
      Example b=e;//將e行參物件的地址賦給控制代碼b
      b.i++;//也同時修改了e.i和實參的值
   }
   
   public void modify1(Example e)
   {
     e=new Example();
     e.i++;
   }
   public static void main(String[] args)
  {
     Example ex=new Example();
     A a=new A();
     a=a.add0(ex);//等價於a.add0(ex),無需返回值,因為通過傳遞的物件地址(控制代碼),直接修改了ex中i的值
     a.add1(ex);//add0,add1都在其中的方法體中直接修改了ex.i的值,因此add0的返回值有點多餘
     a.modify0(ex);//對ex所產生的影響同add1
     a.modify1(ex);//對ex沒有產生任何影響(而且這就是等價於什麼也沒有做).

        這可能會讓一部分人搞不清了。為什麼呢?因為是物件地址的副本"值傳遞",在modify1中e=new Example();實際上e僅僅是儲存ex物件地址的副本的一個控制代碼,當對e賦值時僅僅是對堆疊中e的賦值(對ex指標副本的變數e賦值),而並沒有改變ex的控制代碼的指向,當方法呼叫完畢堆疊彈出,e就將要被垃圾回收,沒有任何用處。當然你可以將它作為返回值,這就是另外一回事了。
   }
 }

        這裡比較繞,如果你能明白這個原理,那麼你就可以寫出合理並且高效的程式,並且可以避免一些潛在的邏輯錯誤,如:物件在方法中被改動了,可能你還不知道!記住c++在這一點上和java有很大的不同,c++預設的是值傳遞,行參會按照位複製實參(如果用指標或者引用就和java很類似了),在方法中作為引數傳遞物件,java更象是c++中傳遞引用,當然還是有區別的,那就是c++中物件的引用不可再賦值為另一個物件,也就是說modify1中的再賦值對引用是不可以的。如果你對c++不瞭解,那麼就當我什麼也沒有說,和c++的比較只是為了幫助更好的理解(針對熟悉c++而不熟悉java的人)。我本人也對c++瞭解甚少,平時主要工作側重於java。因此如果哪位高人發現以上解釋有什麼錯誤請不吝賜教。

    引申到克隆技術java中的所有物件都是Object類的子類,Object類定義了protected clone()方法,它的作用和c++中按位複製是一樣的,因此同樣會帶來如果物件中包含另一個物件(注意是物件不是基本資料型別,基本資料型別直接就會被複制)的指標(java中的控制代碼),clone並沒有將被包含的物件clone,而是複製了被包含物件的控制代碼或者說指標。因此並不能認為複製出來的物件就可以隨心所欲的修改,因為它和被clone的物件都包含同一個物件,因此可能會引起潛在的衝突問題。至於深度clone的方法很簡單,就是在子類中覆蓋父類Object類中clone方法,保證每一個被包含的物件都被按照位被clone。如果包含的資料全部是基本型別資料,那麼就什麼也不用做了。深度clone還有另一種方法就是利用Serializable,但是物件中被transient關鍵字修飾的變數是不會被序列化的. 因為clone用到的地方並不多,就不多說了。但是當你遇到的時候,一定要小心。