深入理解Java引用型別
在Java中型別可分為兩大類:值型別與引用型別。值型別就是基本資料型別(如int ,double 等),而引用型別,是指除了基本的變數型別之外的所有型別(如通過 class 定義的型別)。所有的型別在記憶體中都會分配一定的儲存空間(形參在使用的時候也會分配儲存空間,方法呼叫完成之後,這塊儲存空間自動消失), 基本的變數型別只有一塊儲存空間(分配在stack中), 而引用型別有兩塊儲存空間(一塊在stack中,一塊在heap中),在函式呼叫時Java是傳值還是傳引用,這個估計很多人至今都很糊塗,下面用圖形與程式碼來解釋:
在上圖中引用型別在傳參時不是在heap中再分配一塊記憶體來存變數c 所指向的A(),而是讓a 指向同一個A 的例項,這就與C++ 中的指標一樣,先宣告指標變數a,b,c,d 在傳參的時候讓a 指向c所指向的記憶體,讓 d 指向 b 所指向的記憶體。很明顯Java中的引用與C++中的指標在原理上是相類似的,但記住Java沒有指標,只有引用。下面再通過一些具體的程式碼來討論引用:
1. 簡單型別是按值傳遞的
Java 方法的引數是簡單型別的時候,是按值傳遞的 (pass by value)。這一點我們可以通過一個簡單的例子來說明:
package test;
public class Test {
//交換兩個變數的值
public static void Swap(int a,int b){
int c=a;
a=b;
b=c;
System.out.println("a: "+a);
System.out.println("b: "+b);
}
public
int c=10;
int d=20;
Swap(c,d);
System.out.println("After Swap:");
System.out.println("c: "+d);
System.out.println("d: "+c);
}
}
執行結果:
a: 20
b: 10
After Swap:
c: 20
d: 10
不難看出,雖然在 Swap (a,b) 方法中改變了傳進來的引數的值,但對這個引數源變數本身並沒有影響,即對 main(String[]) 方法裡的 a,b 變數沒有影響。那說明,引數型別是簡單型別的時候,是按值傳遞的。以引數形式傳遞簡單型別的變數時,實際上是將引數的值作了一個拷貝傳進方法函式的,那麼在方法函式裡再怎麼改變其值,其結果都是隻改變了拷貝的值,而不是源值。
2. 什麼是引用
Java 是傳值還是傳引用,問題主要出在物件的傳遞上,因為 Java 中簡單型別沒有引用。既然爭論中提到了引用這個東西,為了搞清楚這個問題,我們必須要知道引用是什麼。
簡單的說,引用其實就像是一個物件的名字或者別名 (alias),一個物件在記憶體中會請求一塊空間來儲存資料,根據物件的大小,它可能需要佔用的空間大小也不等。訪問物件的時候,我們不會直接是訪問物件在記憶體中的資料,而是通過引用去訪問。引用也是一種資料型別,我們可以把它想象為類似 C++ 語言中指標的東西,它指示了物件在記憶體中的地址——只不過我們不能夠觀察到這個地址究竟是什麼。
如果我們定義了不止一個引用指向同一個物件,那麼這些引用是不相同的,因為引用也是一種資料型別,需要一定的記憶體空間(stack,棧空間)來儲存。但是它們的值是相同的,都指示同一個物件在記憶體(heap,堆空間)的中位置。比如:
String a="This is a Text!";
String b=a;
通過上面的程式碼和圖形示例不難看出,a 和 b 是不同的兩個引用,我們使用了兩個定義語句來定義它們。但它們的值是一樣的,都指向同一個物件 "This is a Text!"。但要注意String 物件的值本身是不可更改的 (像 b = "World"; b = a; 這種情況不是改變了 "World" 這一物件的值,而是改變了它的引用 b 的值使之指向了另一個 String 物件 a)。
如圖,開始b 的值為綠線所指向的“Word Two”,然後 b=a; 使 b 指向了紅線所指向的”Word“.
這裡我描述了兩個要點:
(1) 引用是一種資料型別(儲存在stack中),儲存了物件在記憶體(heap,堆空間)中的地址,這種型別即不是我們平時所說的簡單資料型別也不是類例項(物件);
(2) 不同的引用可能指向同一個物件,換句話說,一個物件可以有多個引用,即該類型別的變數。
3. 物件是如何傳遞的呢
隨著學習的深入,你也許會對物件的傳遞方式產生疑問,即物件究竟是“按值傳遞”還是“按引用傳遞”?
(1)認為是“按值傳遞”的:
package test;
public class Test {
public static void Sample(int a){
a+=20;
System.out.println("a: "+a);
}
public static void main(String[] args){
int b=10;
Sample(b);
System.out.println("b: "+b);
}
}
執行結果:
a: 30
b: 10
在這段程式碼裡,修改變數 a 的值,不改變變數 b 的值,所以它是“值傳遞”。
(2)認為是“按引用傳遞”的:
package test;
public class Test {
public static void Sample(StringBuffer a){
a.append(" Changed ");
System.out.println("a: "+a);
}
public static void main(String[] args){
StringBuffer b=new StringBuffer("This is a test!");
Sample(b);
System.out.println("b: "+b);
}
}
執行結果:
a: This is a test! Changed
b: This is a test! Changed
在Sample(StringBuffer)這個函式中,修改了引用 a 的值,同時 b 的值也變化了,所以它是“按引用傳遞”的!
那麼物件(記住在Java中一切皆物件,無論是int a;還是String a;,這兩個變數a都是物件)在傳遞的時候究竟是按什麼方式傳遞的呢?其答案就只能是:即是按值傳遞也是按引用傳遞,但通常基本資料型別(如int,double等)我們認為其是“值傳遞”,而自定義資料型別(class)我們認為其是“引用傳遞”。
4. 正確看待傳值還是傳引用的問題
要正確的看待這個問題必須要搞清楚為什麼會有這樣一個問題。
實際上,問題來源於 C,而不是 Java。
C 語言中有一種資料型別叫做指標,於是將一個數據作為引數傳遞給某個函式的時候,就有兩種方式:傳值,或是傳指標。 在值傳遞時,修改函式中的變數值不會改變原有變數的值,但是通過指標卻會改變。
void Swap(int a,int b){ int c=a;a=b;b=c;}
void Swap(int *a,int *b){ int c=*a;*a=*b;*b=c; }
int c=10;
int d=20;
Swap(c,d); //不改變 c , d 的值
Swap(&c,&d); //改變 c , d 的值
許多的 C 程式設計師開始轉向學習 Java,他們發現,使用類似 SwapValue(T,T)(當T 為值型別時) 的方法仍然不能改變通過引數傳遞進來的簡單資料型別的值,但是如果T時一個引用型別時,則可能將其成員隨意更改。於是他們覺得這很像是 C 語言中傳值/傳指標的問題。但是 Java 中沒有指標,那麼這個問題就演變成了傳值/傳引用的問題。可惜將這個問題放在 Java 中進行討論並不恰當。
討論這樣一個問題的最終目的只是為了搞清楚何種情況才能在方法函式中方便的更改引數的值並使之長期有效。
5. 如何實現類似 swap 的方法
傳值還是傳引用的問題,到此已經算是解決了,但是我們仍然不能解決這樣一個問題:如果我有兩個 int型的變數 a 和 b,我想寫一個方法來交換它們的值,應該怎麼辦?有很多方法,這裡介紹一種簡單的方法:
package test;
public class Test {
public static void Swap(int[] a){
int c=a[0];
a[0]=a[1];
a[1]=c;
}
public static void main(String[] args){
int[] a=new int[2];
a[0]=10;
a[1]=20;
Swap(a);
System.out.println(a[0]);
System.out.println(a[1]);
}
}
通過陣列可以方便的實現值型別的資料來源的交換,不過還有一種方法是將所有變數封裝到一個類裡面去,通過引用型別來實現。