1. 簡單類型是按值傳遞的
Java 方法的參數是簡單類型的時候,是按值傳遞的 (pass by value)。這一點我們可以通過一個簡單 的例子來說明:
public class Test {
public static void test(boolean test) {
test = ! test;
System.out.println("In test(boolean) : test = " + test);
}
public static void main(String[] args) {
boolean test = true;
System.out.println ("Before test(boolean) : test = " + test);
test(test);
System.out.println ("After test(boolean) : test = " + test);
}
}
運行結果:
Before test(boolean) : test = true
In test(boolean) : test = false
After test(boolean) : test = true
不難看出,雖然在 test(boolean) 方法中改變了傳進來的參數的值,但對這個參數源變量本身並沒有 影響,即對 main(String[]) 方法裡的 test 變量沒有影響。那說明,參數類型是簡單類型的時候,是按 值傳遞的。以參數形式傳遞簡單類型的變量時,實際上是將參數的值作了一個拷貝傳進方法函數的,那麼 在方法函數裡再怎麼改變其值,其結果都是只改變了拷貝的值,而不是源值。
2. 什麼是引用
Java 是傳值還是傳引用,問題主要出在對象的傳遞上,因為 Java 中簡單類型沒有引用。既然爭論中 提到了引用這個東西,為了搞清楚這個問題,我們必須要知道引用是什麼。
簡單的說,引用其實就像是一個對象的名字或者別名 (alias),一個對象在內存中會請求一塊空間來 保存數據,根據對象的大小,它可能需要占用的空間大小也不等。訪問對象的時候,我們不會直接是訪問 對象在內存中的數據,而是通過引用去訪問。引用也是一種數據類型,我們可以把它想象為類似 C 語言 中指針的東西,它指示了對象在內存中的地址——只不過我們不能夠觀察到這個地址究竟是什麼。
如果我們定義了不止一個引用指向同一個對象,那麼這些引用是不相同的,因為引用也是一種數據類 型,需要一定的內存空間來保存。但是它們的值是相同的,都指示同一個對象在內存的中位置。比如
String a = "Hello";
String b = a;
這裡,a 和 b 是不同的兩個引用,我們使用了兩個定義語句來定義它們。但它們的值是一樣的,都指 向同一個對象 "Hello"。也許你還覺得不夠直觀,因為 String 對象的值本身是不可更改的 (像 b = "World"; b = a; 這種情況不是改變了 "World" 這一對象的值,而是改變了它的引用 b 的值使之指向了 另一個 String 對象 a)。那麼我們用 StringBuffer 來舉一個例子:
public class Test {
public static void main(String[] args) {
StringBuffer a = new StringBuffer("Hello");
StringBuffer b = a;
b.append (", World");
System.out.println("a is " + a);
}
}
運行結果:
a is Hello, World
這個例子中 a 和 b 都是引用,當改變了 b 指示的對象的值的時候,從輸出結果來看,a 所指示的對 象的值也改變了。所以,a 和 b 都指向同一個對象即包含 "Hello" 的一個StringBuffer 對象。
這裡我描述了兩個要點:
1. 引用是一種數據類型,保存了對象在內存中的地址,這種類型即不是我們平時所說的簡單數據類型 也不是類實例(對象);
2. 不同的引用可能指向同一個對象,換句話說,一個對象可以有多個引用,即該類類型的變量。
3. 對象是如何傳遞的呢
關於對象的傳遞,有兩種說法,即“它是按值傳遞的”和“它是按引用傳遞的”。這兩種說法各有各 的道理,但是它們都沒有從本質上去分析,即致於產生了爭論。既然現在我們已經知道了引用是什麼東西 ,那麼現在不妨來分析一下對象作是參數是如何傳遞的。還是先以一個程序為例:
public class Test {
public static void test(StringBuffer str) {
str.append(", World!");
}
public static void main(String[] args) {
StringBuffer string = new StringBuffer("Hello");
test (string);
System.out.println(string);
}
}
運行結果:
Hello, World!
test(string) 調用了 test(StringBuffer) 方法,並將 string 作為參數傳遞了進去。這裡 string 是一個引用,這一點是勿庸置疑的。前面提到,引用是一種數據類型,而且不是對象,所以它不可能按引 用傳遞,所以它是按值傳遞的,它麼它的值究竟是什麼呢?是對象的地址。
由此可見,對象作為參數的時候是按值傳遞的,對嗎?錯!為什麼錯,讓我們看另一個例子:
public class Test {
public static void test(String str) {
str = "World";
}
public static void main(String[] args) {
String string = "Hello";
test(string);
System.out.println(string);
}
}
運行結果:
Hello
為什麼會這樣呢?因為參數 str 是一個引用,而且它與 string 是不同的引用,雖然它們都是同一個 對象的引用。str = "World" 則改變了 str 的值,使之指向了另一個對象,然而 str指向的對象改變了 ,但它並沒有對 "Hello" 造成任何影響,而且由於 string 和 str 是不同的引用,str 的改變也沒有對 string 造成任何影響,結果就如例中所示。
其結果是推翻了參數按值傳遞的說法。那麼,對象作為參數的時候是按引用傳遞的了?也錯!因為上 一個例子的確能夠說明它是按值傳遞的。
結果,就像光到底是波還是粒子的問題一樣,Java 方法的參數是按什麼傳遞的問題,其答案就只能是 :即是按值傳遞也是按引用傳遞,只是參照物不同,結果也就不同。
①單純考慮參數str存的也是一種數據類型,可以看成是值傳遞。
②考慮參數str它是對象string的一個引用,此時就可看做是引用傳遞。
4. 正確看待傳值還是傳引用的問題
要正確的看待這個問題必須要搞清楚為什麼會有這樣一個問題。
實際上,問題來源於 C,而不是 Java。
C 語言中有一種數據類型叫做指針,於是將一個數據作為參數傳遞給某個函數的時候,就有兩種方式 :傳值,或是傳指針,它們的區別,可以用一個簡單的例子說明: void SwapValue(int a, int b) {
int t = a;
a = b;
b = t;
}
void SwapPointer(int * a, int * b) {
int t = * a;
* a = * b;
* b = t;
}
void main() {
int a = 0, b = 1;
printf("1 : a = %d, b = %d\n", a, b);
SwapValue(a, b);
printf("2 : a = %d, b = %d\n", a, b);
SwapPointer(&a, &b);
printf("3 : a = %d, b = %d\n", a, b);
}
運行結果:
1 : a = 0, b = 1
2 : a = 0, b = 1
3 : a = 1, b = 0
大家可以明顯的看到,按指針傳遞參數可以方便的修改通過參數傳遞進來的值,而按值傳遞就不行。
當 Java 成長起來的時候,許多的 C 程序員開始轉向學習 Java,他們發現,使用類似SwapValue 的 方法仍然不能改變通過參數傳遞進來的簡單數據類型的值,但是如果是一個對象,則可能將其成員隨意更 改。於是他們覺得這很像是 C 語言中傳值/傳指針的問題。但是 Java 中沒有指針,那麼這個問題就演變 成了傳值/傳引用的問題。可惜將這個問題放在 Java 中進行討論並不恰當。
討論這樣一個問題的最終目的只是為了搞清楚何種情況才能在方法函數中方便的更改參數的值並使之 長期有效。
Java 中,改變參數的值有兩種情況,第一種,使用賦值號“=”直接進行賦值使其改變,如例 1 和例 4;第二種,對於某些對象的引用,通過一定途徑對其成員數據進行改變,如例 3。對於第一種情況,其 改變不會影響到方法該方法以外的數據,或者直接說源數據。而第二種方法,則相反,會影響到源數據— —因為引用指示的對象沒有變,對其成員數據進行改變則實質上是改變的該對象。