深入理解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 static void main(String[] args){
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]);
}
}
通過數組可以方便的實現值類型的數據源的交換,不過還有一種方法是將所有變量封裝到一個類里面去,通過引用類型來實現。