1. 程式人生 > >I belive I can fly in the sky!**I am waiting for the future!

I belive I can fly in the sky!**I am waiting for the future!


1所謂傳值和傳引用

傳值和傳引用的問題一直是Java裡爭論的話題。與C++不同的,Java裡面沒有指標的概念,Java的設計者巧妙的對指標的操作進行了管理。事實上,在懂C++的Java程式設計師眼中,Java到處都是精美絕倫的指標。
下面舉個簡單的例子,說明什麼是傳值,什麼是傳引用。
//例1
void method1(){
int x=0;
this.change(x);
System.out.println(x);
}

void int change(int i){
 i=1;
}

很顯然的,在mothod1中執行了change(x)後,x的值並不會因為change方法中將輸入引數賦值為1而變成1,也就是說在執行change(x)後,x的值z依然是0。這是因為x傳遞給change(int i)的是值。這就是最簡單的傳值。
同樣的,進行一點簡單的變化。
//例2
void method1(){
StringBuffer x=new StringBuffer("Hello");
this.change(x);
System.out.println(x);
}

void int change(StringBuffer i){
 i.append(" world!");
}
看起來沒什麼變化,但是這次mothed1中執行了change (x)後,x的值不再是"Hello"了,而是變成了"Hello world!"。這是因為x傳遞給change(i)的是x的引用。這是最經典的傳引用。
似乎有些奇怪了,兩段程式沒有特別的不同,可是為什麼一個傳的是值而另一個傳的是引用呢?......

2非要搞清楚傳值還是傳引用的問題嗎?


搞清楚這自然是有必要的,不然我也不需要寫這麼多了,不過的確沒有到"非要"的地步。
首先,如果我們不太關心什麼是傳值什麼是傳引用,我們一樣能寫出漂亮的程式碼,但是這些程式碼在執行過程中可能會存在著極大的隱患。
全域性變數是讓大家深惡痛絕(又難以割捨)的東西,原因就是使用全域性變數要特別注意資料的保護。如果在多執行緒的程式裡使用全域性變數簡直就等於跟自己過不去。不瞭解傳值和傳引用的問題,跟使用全域性變數不考慮資料保護的罪過是不相上下下的,甚至有時候比它還要糟。你會莫名其妙,為什麼我的返回引數沒有起作用,為什麼我傳進去的引數變成了這樣......?
一個例子:
//例3
void mothed1(){
 int x=0;
 int y=1;
 switchValue(x,y);
 System.out.println("x="+x);
 System.out.println("y="+y);
}
void switchValue(int a,int b){

 int c=a;
 a=b;
 b=c;
}
上面是一個交換a,b值的函式,看起來似乎蠻正確的,但是這個函式永遠也不會完成你想要的工作。
還有一個例子:
//例4
StringBuffer a=new StringBuffer("I am a ");
StringBuffer b=a;
a.append("after append");
a=b;
System.out.println("a="+a);
在程式設計過程中,經常會遇到這種情況,一個變數的值要被臨時改變一下,等用完之後再恢復到開始的值。就好像上面的例子,a為了保持它的值,使用b=a做賦值,之後a被改變,再之後a把暫存在b裡面的值取回來。這是我們一廂情願的想法,而事實上,這段程式碼執行後,你會發現a的值已經改變了。
以上是兩個最簡單的例子,真正的程式開發過程中,比這要複雜的情況每天都會遇到。

3型別和類


Java 提出的思想,在Java裡面任何東西都是類。但是Java裡面同時還有簡單資料型別:int,byte,char,boolean,與這些資料型別相對應的類是Integer,Byte,Character,Boolean,這樣做依然不會破壞Java關於任何東西都是類的提法。

這裡提到資料型別和類似乎和我們要說的傳值和傳引用的問題無關,但這是我們分辨傳值和傳引用的基礎。

4試圖分辨傳值還是傳引用

為什麼是"試圖分辨"呢?很簡單,傳值和傳引用的問題無處不在,但是似乎還沒有人能正統的給出標準,怎樣的就是值拷貝呼叫,怎樣的就是引用呼叫。面對這個問題,我們更多的應該是來自平時積累對Java的理解。
回過頭來,我們分析一下上面的幾個例子:
先看例1,即使你不明白為什麼,但是你應該知道這樣做肯定不會改變x的值。為了方便說明,我們給例子都加上行號。

//例1

1 void method1(){
2  int x=0;
3  this.change(x);
4 }
5
6 void int change(int i){
7 i=7;
8}
讓我們從記憶體的儲存方式看一下x和I之間到底是什麼關係。
在執行到第2行的時候,變數x指向一個存放著int 0的記憶體地址。

變數x---->[存放值0]

執行第3行呼叫change(x)方法的時候,記憶體中是這樣的情形:x把自己值在記憶體中複製一份,然後變數i指向這個被複製出來的0。

變數x---->[存放值0]
              ↓進行了一次值複製
變數i---->[存放值0]

這時候再執行到第7行的時候,變數i的被賦值為7,而這一步的操作已經跟x沒有任何關係了。

變數x---->[存放值0]
             
變數i---->[存放值7]

說到這裡應該已經理解為什麼change(x)不能改變x的值了吧?因為這個例子是傳值的。
那麼,試著分析一下為什麼例三中的switchValue()方法不能完成變數值交換的工作?
再看例2。

//例2
1void method1(){
2 StringBuffer x=new StringBuffer("Hello");
3 this.change(x);
4}
5
6void int change(StringBuffer i){
7 i.append(" world!");
8}
例2似乎和例1從程式碼上看不出什麼差別,但是執行結果卻是change(x)能改變x的值。依然才從記憶體的儲存角度來看看例2的蹊蹺在哪裡。
在執行到第2行時候,同例1一樣,x指向一個存放"Hello"的記憶體空間。

變數x---->[存放值"Hello"]

接下來執行第三行change(x),注意,這裡就與例1有了本質的不同:呼叫change(x)時,變數i也指向了x指向的記憶體空間,而不是指向x的一個拷貝。(x COPY了一份引用給 i)

變數x /
       -->[存放值"Hello"]
變數i/

於是,第7行對i呼叫append方法,改變i指向的記憶體空間的值,x的值也就隨之改變了。

變數x /
       -->[追加為"Hello World!"]
變數i /

為什麼x值能改變呢?因為這個例子是傳引用的。

這幾個例子是明白了,可是很多人會開始有另一個疑問了:這樣看來,到底什麼時候是傳的值什麼時候是傳得引用呢?於是,我們前面講到的型別和類在這裡就派上了用場:對於引數傳遞,如果是簡單資料型別,那麼它傳遞的是值拷貝,對於類的例項它傳遞的是類的引用。需要注意的是,這條規則只適用於引數傳遞。為什麼這麼說呢?我們看看這樣一個例子:

//例5
String str="abcdefghijk";
str.replaceAll("b","B");
這兩句執行後,str的內容依然是"abcdefghijk",但是我們明明是對str操作的,為什麼是這樣的呢?因為str的值究竟會不會被改變完全取決於replaceAll這個方法是怎麼實現的。但是用