Java 是傳值還是傳引用 (轉)

amyz發表於2007-10-17
Java 是傳值還是傳引用 (轉)[@more@]

1. 簡單型別是按值傳遞的

   方法的引數是簡單型別的時候,是按值傳遞的 (pass by value)。這一點我們可以透過一個簡單的例子來說明:

/* 例 1 */ /** * @(#) Test.java * @author fancy */ 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 來舉一個例子:

/* 例 2 */ /** * @(#) Test.java * @author fancy */ 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. 1. 引用是一種資料型別,儲存了物件在記憶體中的地址,這種型別即不是我們平時所說的簡單資料型別也不是類例項(物件);
  2. 2. 不同的引用可能指向同一個物件,換句話說,一個物件可以有多個引用,即該類型別的變數。

3. 物件是如何傳遞的呢

  關於物件的傳遞,有兩種說法,即“它是按值傳遞的”和“它是按引用傳遞的”。這兩種說法各有各的道理,但是它們都沒有從本質上去分析,即致於產生了爭論。

  既然現在我們已經知道了引用是什麼東西,那麼現在不妨來分析一下物件作是引數是如何傳遞的。還是先以一個為例:

/* 例 3 */ /** * @(#) Test.java * @author fancy */ 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 是一個引用,這一點是勿庸置疑的。前面提到,引用是一種資料型別,而且不是物件,所以它不可能按引用傳遞,所以它是按值傳遞的,它麼它的值究竟是什麼呢?是物件的地址。

  由此可見,物件作為引數的時候是按值傳遞的,對嗎?錯!為什麼錯,讓我們看另一個例子:

/* 例 4 */ /** * @(#) Test.java * @author fancy */ 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 方法的引數是按什麼傳遞的問題,其答案就只能是:即是按值傳遞也是按引用傳遞,只是參照物不同,結果也就不同。

4. 正確看待傳值還是傳引用的問題

  要正確的看待這個問題必須要搞清楚為什麼會有這樣一個問題。

  實際上,問題來源於 C,而不是 Java。

  C 語言中有一種資料型別叫做指標,於是將一個資料作為引數傳遞給某個函式的時候,就有兩種方式:傳值,或是傳指標,它們的區別,可以用一個簡單的例子說明:

/* 例 5 */ /** * @(#) test.c * @author fancy */ void SValue(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 = %dn", a, b); SwapValue(a, b); printf("2 : a = %d, b = %dn", a, b); SwapPointer(&a, &b); printf("3 : a = %d, b = %dn", 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。對於第一種情況,其改變不會影響到方法該方法以外的資料,或者直接說源資料。而第二種方法,則相反,會影響到源資料——因為引用指示的物件沒有變,對其成員資料進行改變則實質上是改變的該物件。

5. 如何實現類似 swap 的方法

  傳值還是傳引用的問題,到此已經算是解決了,但是我們仍然不能解決這樣一個問題:如果我有兩個 int 型的變數 a 和 b,我想寫一個方法來它們的值,應該怎麼辦?

  結論很讓人失望——沒有辦法!因此,我們只能具體情況具體討論,以經常使用交換方法的排序為例:

/** 例 6 */ /** * @(#) Test.java * @author fancy */ public class Test { public static void swap(int[] data, int a, int b) { int t = data[a]; data[a] = data[b]; data[b] = t; } public static void main(String[] args) { int[] data = new int[10]; for (int i = 0; i < 10; i++) { data[i] = (int) (Math.ran() * 100); System.out.print(" " + data[i]); } System.out.println(); for (int i = 0; i < 9; i++) { for (int j = i; j < 10; j++) { if (data[i] > data[j]) { swap(data, i, j); } } } for (int i = 0; i < 10; i++) { System.out.print(" " + data[i]); } System.out.println(); } }

  執行結果(情況之一):

78 69 94 38 95 31 50 97 84 1
1 31 38 50 69 78 84 94 95 97

  swap(int[] data, int a, int b) 方法在內部實際上是改變了 data 所指示的物件的成員資料,即上述討論的第二種改變引數值的方法。希望大家能夠舉一反三,使用類似的方法來解決相關問題。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752019/viewspace-977215/,如需轉載,請註明出處,否則將追究法律責任。

相關文章