1. 程式人生 > >Java方法引數的傳遞機制,值傳遞?引用傳遞?

Java方法引數的傳遞機制,值傳遞?引用傳遞?

在呼叫某個方法時你是不是經常有如下2個疑惑,
1、明明傳進去的引數在方法裡做了值的改變,但方法呼叫完後傳進去的引數在後面的程式碼中值卻沒有發生變化,疑惑中。(期望變化卻沒有發生變化)
2、一個物件在作為引數被方法呼叫之後,突然發現在後面的程式碼中值發生了變化,疑惑中。(不期望變化卻發生了變化)

為了解決這些疑惑,請執行下面的程式碼,注意println程式碼部分,看看結果是不是你期望的。

例1:
public class ParamTransfer {  
    public void chgValue(int param){  
        param = param + 1;  
        System.out.println(param);  
    }  
      
      
    public static void main(String[] args) {  
        int value = 1;  
        ParamTransfer pt = new ParamTransfer();  
        pt.chgValue(value);  
        System.out.println(value);  
    }  
  
}  
例2:
public class ParamTransfer{
public void chgValue(StringBuffer param) {
        param = param.append("World!");
        System.out.println(param);
}


public static void main(String[] args) {
ParamTransfer pt = new ParamTransfer();
StringBuffer value = new StringBuffer("Hello ");
pt.chgValue(value);
        System.out.println(value);
}
}
例3:
public class ParamTransfer{
public void chgValue(StringBuffer param) {
        StringBuffer sb = new StringBuffer("Hi ");
        param = sb;
        param.append("World!");
        System.out.println(param);
}


public static void main(String[] args) {
ParamTransfer pt = new ParamTransfer();
StringBuffer value = new StringBuffer("Hello ");
pt.chgValue(value);
        System.out.println(value);
}
}

如果這三個例子的結果完全在你意料之中,那麼恭喜你可以不用再看我後面的廢話了。如果不是請繼續。。。
第一個和第二個例子大家也許覺得並不意外,第三個例子可能會有某些人迷惑了。。。為什麼呢?
回頭看看這篇文章的題目,值傳遞?引用傳遞?其實,我這麼起名字只是為了吸引你的眼球,在我看來,根本不存在什麼值還是引用的傳遞,而是型別傳遞,即基本資料型別(不能new只能直接賦常量值或同類型變數賦值)和物件型別(也就是可以new的類)的傳遞。
下面,我們不妨把上面的程式碼改造一下,把呼叫方法的步驟直接變成main中的程式碼,還原始碼的真面目,發現沒有,其實型別傳遞就是變數賦值!
public class ParamTransfer {
	public static void main(String[] args) {
		int value = 1;
		int param = value;
		param = param + 1;
		System.out.println(param);
		System.out.println(value);
	}
}


public class ParamTransfer {
	public static void main(String[] args) {
		StringBuffer value = new StringBuffer("Hello ");
		StringBuffer param = value;
		param = param.append("World!");
		System.out.println(param);
		System.out.println(value);
	}
}


public class ParamTransfer {
	public static void main(String[] args) {
		StringBuffer value = new StringBuffer("Hello ");
		StringBuffer param = value;
		StringBuffer sb = new StringBuffer("Hi ");
		param = sb;
		param = param.append("World!");
		System.out.println(param);
		System.out.println(value);
	}
}


對於基本資料型別來說,因為不存在new的問題,值的改變就是記憶體地址指向的改變,值相同,記憶體地址就相同,值不同,記憶體地址就不同。這就是為什麼基本資料型別只要值相同(不管是怎麼賦的值)那麼==判斷就為真的原因(反之即為假)
而對於物件型別來說(String類為特例,後有詳述),值的改變有兩種,第一是記憶體中的值發生了改變,例2就是這種。第二是指向了新的記憶體地址,例3就是這種。
但不論是那種值的改變,物件變數的賦值不會改變原有變數即主值變數的引用地址!這也是為什麼例1和例3傳遞變數的值不隨引數變數變化的原因。而例2為何變化,很好理解,因為傳遞變數和引數變數引用的是同一地址,而值的改變是上面說的第一種。


但是,有一個特例,那就是String這個類物件,我們例2改造下。
public class ParamTransfer {
	public void chgValue(String param) {
		param = param.concat("World!");
		System.out.println(param);
	}


	public static void main(String[] args) {
		ParamTransfer pt = new ParamTransfer();
		String value = new String("Hello ");
		pt.chgValue(value);
		System.out.println(value);
	}
}


執行一下,是不是有了新的疑惑?明明是物件型別傳遞,又是第一種值改變,為何傳遞引數的值沒有跟著改變?這就是前面說的的String類的特例,這個牽扯到java編譯器對字串常量優化的問題,也就是經典的
String str1 = "abc";  和 String str2 = new String("abc");有何不同的問題。但這已不是本文討論的重點,有興趣的朋友可以參考如下博文:
http://blog.sina.com.cn/s/blog_798b04f90100ta67.html


最後,如果你對上面的論述還是不太理解,那就只好扒一扒java記憶體的概念了,由於這也不是本文的重點,我就轉載一段文字吧。
------------------------------以下內容為轉載,由於出處不詳,無法註明原創,請諒解-------------------------------
1.java程式執行時有6中地方儲存資料,分別是:暫存器、棧、堆、靜態儲存、常量儲存、非RAM(隨機儲存器),主要是堆與棧的儲存。 
2.堆與棧是java用來在RAM中儲存資料的地方,java自動管理堆和棧,程式設計師不能直接設定堆和棧。 
3.棧的優勢是:存取速度比堆要快,僅次於直接位於cpu中的暫存器;棧資料可以共享。   但缺點是:存在棧中資料大小與生命週期必須是確定的,缺乏靈活性。 
4.堆的優勢在於可以動態分配記憶體大小,生存期也不必事先告訴編譯器,java的垃圾收集器會自動收走這些不再使用的資料,缺點是由於要在執行時動態分配記憶體,存取速度較慢。 5.基本資料型別的儲存,java的基本資料型別共有8種:int,short,byte,long,float,double,boolean,(基本資料型別中並沒有String的基本型別)。這種型別如int=3的形式來定義,稱為自動變數。自動變數存在的是字面值,即不是類的例項,也不是類的引用。a 是一個指向int型別的引用,指向3這個字面值。這些字面值的資料由於大小可知,生存期可知(這些字面值固定定義在某個程式塊裡面,程式塊退了,欄位值就消失了),處於追求速度的原因就存在棧中。 
6.另外棧有一個很重要的特殊性,就是存在棧中的資料可以共享。如 需要定義int a = 3; int b =3;這兩個自動變數。編譯器先處理int  a=3;首先在棧中建立一個變數為a的引用,然後查詢棧有沒有字面值為3的引用,沒有找到,就開闢一個存放3這個字面值的地址,然後將a指向3的地址。接著處理int b=3;在建立完b這個引用變數後,由於在棧中已經有了3這個字面值,即將b直接指向3的地址。這樣,就出現了a和b同事指向3的情況。 定義完a與b後,在令a=4,那麼b不會等於4,而是等於3,。在編譯器內部,遇到時,它就會中新搜尋棧中是否有4這個字面值,如果沒有,重新開闢地址存放4的值。如果已經有就直接將a指向這個地址,因此a的值改變不會影響b的值。  
7.物件的記憶體模型.建立一個物件包括物件的宣告和例項化兩步:宣告物件的引用和物件的例項化。宣告一個物件引用時,將在棧記憶體為物件的引用變數分配空間;物件例項化是,在堆記憶體中為類成員變數分配記憶體,並將其初始化為各資料型別預設值,接著進行顯示初始化,最後呼叫構造方法為成員變數賦值,返回堆記憶體中物件的引用(相當於首地址)給應用變數,通過引用變數來引用堆記憶體中的物件。 
8.包裝類資料的儲存:基本資料型別的定義都是直接在棧中,如果是包裝型別來建立物件,就和普通物件一樣。 
9.string資料型別是一種特殊資料型別,既可以用基本資料型別格式來建立,也可以用普通基本型別來建立。