Java變數之間傳值- 值傳遞還是引用傳遞的討論
Java變數之間傳值時可分為值傳遞和引用傳遞, 那麼它們有何區別?
1. 簡單型別是按值傳遞的
Java 方法的引數是簡單型別的時候,是按值傳遞的(pass by value),需要注意的是,對於基本型別的包裝型別,因為JVM的自動拆箱操作,包裝型別變成基本型別後也會按基本型別來進行操作,那麼也是按值進行傳遞。這一點我們可以通過一個簡單的例子來說明:
Java程式碼- public class PrimitiveValuePass {
- public static void main(String[] args) {
-
boolean
- System.out.println( " ======= Test Pass1 ======= " );
- System.out.println( "Before pass : test = " + test);
- pass1(test);
- System.out.println( "After pass : test = " + test);
- System.out.println( " ======= Test Pass2 ======= " );
-
Boolean test2 = true
- System.out.println( "Before pass : test = " + test);
- pass2(test2);
- System.out.println( "After pass : test = " + test);
- }
- // Primitive value pass
- public static void pass1( boolean test) {
- test = !test;
-
System.out.println( "in pass : test = "
- }
- // 會自動拆箱為基本型別
- public static void pass2(Boolean test) {
- test = !test;
- System.out.println( "in pass : test = " + test);
- }
- }
輸出結果:
======= Test Pass1 =======
Before pass : test = true
in pass : test = false
After pass : test = true
======= Test Pass2 =======
Before pass : test = true
in pass : test = false
After pass : test = true
不難看出,雖然在pass1(boolean)方法中改變了傳進來的引數的值,但對這個引數源變數本身並沒有影響,即對main(String[])方法裡的test變數沒有影響。那說明,引數型別是簡單型別的時候,是按值傳遞的。以引數形式傳遞簡單型別的變數時,實際上是將引數的值作了一個拷貝傳進方法函式的,那麼在方法函式裡再怎麼改變其值,其結果都是隻改變了拷貝的值,而不是源值。
2. 什麼是引用
Java 是傳值還是傳引用,問題主要出在物件的傳遞上,因為Java 中簡單型別沒有引用。既然爭論中提到了引用這個東西,為了搞清楚這個問題,我們必須要知道引用是什麼?
簡單的說,引用其實就像是一個物件的名字或者別名(alias),一個物件在記憶體中會請求一塊空間來儲存資料,根據物件的大小,它可能需要佔用的空間大小也不等。訪問物件的時候,我們不會直接訪問物件在記憶體中的資料,而是通過引用去訪問。引用也是一種資料型別,我們可以把它想象為類似C語言中指標的東西,它指示了物件在記憶體中的地址——只不過我們不能夠觀察到這個地址究竟是什麼。
如果我們定義了不止一個引用指向同一個物件,那麼這些引用是不相同的,因為引用也是一種資料型別,需要一定的記憶體空間來儲存。但是它們的值是相同的,都指示同一個物件在記憶體的中位置。比如
Java程式碼- String a = "Hello" ;
- String b = a;
這裡,a 和b 是不同的兩個引用,我們使用了兩個定義語句來定義它們。但它們的值是一樣的,都指向同一個物件"Hello"。也許你還覺得不夠直觀,因為String 物件的值本身是不可更改的(像b = "World"; b = a; 這種情況不是改變了"World" 這一物件的值,而是改變了它的引用b 的值使之指向了另一個String 物件a)。那麼我們用StringBuffer 來舉一個例子:
Java程式碼- 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. 按引用傳遞是什麼?
指的是在方法呼叫時,傳遞的引數是按引用進行傳遞,按前面的表述,傳遞引用其實就是傳遞引用的地址,也就是變數所對應的記憶體空間的地址。
事例:
Java程式碼- public class ObjPass {
- /**
- * @param args
- */
- public static void main(String[] args) {
- ObjPass pass = new ObjPass();
- Person person = pass. new Person();
- person.age = 10 ;
- System.out.println( "Before pass : main方法中的age = " + person.age);
- pass.passTest(person);
- System.out.println( "After pass : main方法中的age = " + person.age);
- }
- public void passTest (Person person) {
- person.age = 20 ;
- System.out.println( "passTest中的age = " + person.age);
- }
- class Person {
- public int age = 0 ;
- }
- }
Before pass : main 方法中的age = 10
passTest中的age = 20
After pass : main 方法中的age = 20
從這個例子可以看出,物件型別確實是按引用進行傳遞的,而不是按值的拷貝進行傳遞的。
4、一定要注意String物件的傳遞
從3的例子的輸出結果可以看出,物件的傳遞是引用傳遞,當引用指向的值發生改變後,傳遞的那個引用所指向的值當然就發生了改變。但是賦值方式的String的傳遞相當於值傳遞,而不是引用傳遞,即
String str = "Hello world"的傳遞不是引用傳遞,對String有一定了解的童鞋相信都知道為啥,因為String類很特殊,對於:
Java程式碼- String str = "123" ;
- str = "hello world" ;
- public class StringValuePass {
- public static void passTest(String str) {
- System.out.println( " === before append: hash code: " + str.hashCode());
- str = " hello : " + str;
- System.out.println( " === After append: hash code: " + str.hashCode());
- System.out.println( "in pass : str = " + str);
- }
- public static void main(String[] args) {
- System.out.println( " ======= Test Pass3 ======= " );
- String str = "wangsheng" ;
- System.out.println( "Before pass : str = " + str + " hash code: " + str.hashCode());
- passTest(str);
- System.out.println( "After pass : str = " + str);
- }
- }
======= Test Pass3 =======
Before pass : str = wangsheng hash code: -1185442234
=== before append: hash code: -1185442234
=== After append: hash code: -414738574
in pass : str = hello : wangsheng
After pass : str = wangsheng
從輸出結果的hash code值以及str的值,結果一目瞭然。
5、 結論:
從上面執行的結果,我們不難得出下面的結論:
(1):“在Java裡面引數傳遞都是按值傳遞”這句話的意思是:按值傳遞是傳遞的值的拷貝,按引用傳遞其實傳遞的是引用的地址值,所以統稱按值傳遞。
(2):在Java裡面只有基本型別和按照下面這種定義方式的String是按值傳遞,其它的都是按引用傳遞。就是直接使用雙引號定義字串方式:String str = "Hello World";
所以你可以說是按值傳遞,如結論1, 也可以嚴格的說大多物件是按引用傳遞,如結論2。
6、番外篇:
就像光到底是波還是粒子的問題一樣,Java 方法的引數是按什麼傳遞的問題,其答案就只能是:即是按值傳遞也是按引用傳遞,只是看問題的角度不同,結果也就不同。
正確看待傳值還是傳引用的問題
要正確的看待這個問題必須要搞清楚為什麼會有這樣一個問題。
實際上,問題來源於C,而不是Java。
C 語言中有一種資料型別叫做指標,於是將一個數據作為引數傳遞給某個函式的時候,就有兩種方式:傳值,或是傳指標,它們的區別,可以用一個簡單的例子說明:
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 中,改變引數的值有兩種情況:
第一種,使用賦值號“=”直接進行賦值使其改變,如PrimitiveValuePass ;
第二種,對於某些物件的引用,通過一定途徑對其成員資料進行改變,如ObjPass。對於第一種情況,其改變不會影響到方法該方法以外的資料,或者直接說源資料。而第二種方法,則相反,會影響到源資料——因為引用指示的物件沒有變,對其成員資料進行改變則實質上是改變了該物件。