(本文非引戰或diss,只是說出自己的理解,歡迎擺正心態觀看或探討)
引子
之所以寫這篇文章是因為前些天寫了一篇《Java中真的只有值傳遞么?》探討了網上關於Java只有值傳遞的說法,當時寫這篇文章的緣由是因為之前看的文章講解的Java只有值傳遞,講的不是讓我很明白,沒有拿出比較專業的解釋或定義,沒有說服我。而我在《Java中真的只有值傳遞么?》這篇文章中又做了一些解讀,發現自己也是沒有抓住重點,這才有了今天這篇文章,對之前的這篇文章做一個補充。
從那篇文章后,我了解到Java的參數傳遞其實牽涉到了Java語言的設計中的參數傳遞方式,可能在語言設計之時就考慮了這個問題,所以在工作之余自己簡單的研究了一下,最終也能根據自己的理解解釋一下關於Java是值傳遞還是引用傳遞的說法。
Java 是引用傳遞還是值傳遞現在有以下這些說法:
1、值傳遞和引用傳遞,區分的條件是傳遞的內容,如果是個值,就是值傳遞。如果是個引用,就是引用傳遞。
2、傳遞的參數如果是普通類型,那就是值傳遞,如果是對象,那就是引用傳遞。
3、Java中只有值傳遞。
關於這個問題應該是分情況討論的,存在即合理,或許在不同的認識下有不同的說法,也不能簡單的就說是值傳遞還是引用傳遞。
對或錯都是相對的。
回顧
在談這個問題之前我們先了解下值傳遞和引用傳遞的概念及現象。可以簡單的通過幾個例子來講解的,大概是這樣的。
值傳遞
例子1:
public static void main(String[] args){
TestJavaParamPass() tjpp = new TestJavaParamPass();
int num = 10;
tjpp.change(num);
System.out.println("num in main():"+i);
}
public void change(int param){
param = 20;
System.out.println("param in change():"+param);
}
控制台輸出:
param in change():20
num in main():10
mian()方法中的int變量num傳遞給change()方法,change()方法接收到后將值改變為20。通過看控制台輸出,main()方法中的num變量的值沒有改變。
結論:實參沒有被形參影響,基本類型是值傳遞。
引用傳遞
例子2:
public static void main(String[] args){
TestJavaParamPass() tjpp = new TestJavaParamPass();
User user = new User();
user.setName("Jerry");
tjpp.change(user);
System.out.println("user in mian():"+user);
}
public void change(User param){
param.setName("Tom");
System.out.println("param in change():"+param);
}
控制台輸出:
param in change():User(name=Tom}
user in mian():User(name=Tom}
main()方法中的user變量傳遞給change()方法,change()方法改變了其name屬性值。通過看控制台輸出,main()方法中的user變量的name屬性值發生改變。
結論:形參變了實參也變了,引用類型是引用傳遞。
特殊的值傳遞
例子3:
public static void main(String[] args){
TestJavaParamPass() tjpp = new TestJavaParamPass();
String name = "Jerry";
tjpp.change(name);
System.out.println("name in main():"+i);
}
public void change(String param){
param = "Tom";
System.out.println("param in change():"+param);
}
控制台輸出:
param in change():Tom
name in mian():Jerry
String也是引用類型的數據類型,為什么值沒改變?
因為在change()方法里param = "Tom";
相當於param = new String("Tom");
就相當於param被重新賦值指向了另外一個對象。所以,其實String類型傳的是引用,只不過被重新賦值指向了別的對象了,沒有修改原對象。即,String本質上還是引用傳遞,表像上是值傳遞。
結論:基本類型是值傳遞,引用類型是引用傳遞,String是特殊的值傳遞。
看到這樣的結論,沒有去深究過,可能大部分程序員的認知都是這樣的。
根據上面的例子我們先初步給值傳遞和引用傳遞下個定義,以及解釋為什么大多數程序員都將String理解為是特殊的值傳遞。
概念提取
與其叫概念提取還不如叫結論總結呢。
-
值傳遞:基本類型的變量在被傳遞給方法時,傳遞的是該變量的值(即復制自己的值傳遞給方法)。
-
引用傳遞:引用類型的變量在被傳遞給方法時, 傳遞的是該變量的引用(即自己所指向的內存地址)。
-
為什么說String是特殊的值傳遞:是因為String和基本類型從表象來說表現出來的結果是一樣,大概是為了便於記憶這個結果才這樣說的吧。但是要知道String也是傳遞的引用,只不過它的引用被重新賦值,指向了別的對象了,所以不會影響原值。所以String不能簡單的說是值傳遞。
而僅僅根據上面的實驗就給值傳遞,引用傳遞下這樣的結論是不是太草率了?
解析
對於文章開始時提到的那些說法,前兩種可以這樣解釋:
大概是因為int沒有因為change方法而改變原值,所以就說它傳過去的是自身的值,因而叫值傳遞;User對象經過change方法后,對象的數據變了,就認為是因為實參和形參指向的是同一片內存空間,內存空間的數據變了就都變了,傳過去的是引用所以就說對象是引用傳遞。這樣說的側重點是傳遞的東西。
所以,如果從傳遞的東西的角度來看這兩種說法也是沒問題的呀。
至於Java只有值傳遞
的說法,我查閱了一些資料結合網上的文章了解到了求值策略這個名詞,這大概牽涉到了語言本身的設計。所以就從這些名詞來探究Java的方法調用時參數傳遞的奧秘。
我們先來看看這些編程語言里關於參數傳遞函數調用有關的術語。
(以下術語來自Wiki )
求值策略(Evaluation strategy)
在計算機科學中,求值策略(英語:Evaluation strategy)是確定編程語言中表達式的求值的一組(通常確定性的)規則。 重點典型的位於函數或算子上——求值策略定義何時和以何種次序求值給函數的實際參數,什么時候把它們代換入函數,和代換以何種形式發生。
求值策略:是一組求值規則,用來定義如何為函數的實際參數求值。它是用來規定程序語言在方法、函數或過程調用時的傳參策略,是在程序語言設計時就應該考慮的問題。而下面的這幾個調用方式都屬於求值策略。
傳值調用(Call by value)
“傳值調用”求值是最常見的求值策略,C和Scheme這樣差異巨大的語言都在使用。在傳值調用中實際參數被求值,其值被綁定到函數中對應的變量上(通常是把值復制到新內存區域)。如果函數或過程能把值賦給它的形式參數,則被賦值的只是局部拷貝——就是說,在函數返回后調用者作用域里的曾傳給函數的任何東西都不會變。
傳值調用不是一個單一的求值策略,而是指一類函數的實參在被傳給函數之前就被求值的求值策略。 盡管很多使用傳值調用的編程語言(如Common Lisp、Eiffel、Java)從左至右的求值函數的實際參數,某些語言(比如OCaml)從右至左的求值函數和它們的實際參數,而另一些語言(比如Scheme和C)未指定這種次序(盡管它們保證順序一致性)。
傳值調用:在傳值調用中,實際參數被求值后傳遞給被調函數。也就是說傳值調用是實參在被傳給函數之前就被求值的一種求值策略。
在Java中的體現
那什么叫實參在被傳給函數之前就被求值呢?求的是誰的值呢?這個值又是什么呢?是怎么求得呢?
帶着這些疑問,我們來看下面的例子。
如下,在調用change()方法時實參為i
,當程序執行到change(i)這一行時,i
是實參,這時i
就要被求值了,會求出i
的值即4傳給change()方法;change()的形參a
拿到的是實參i
的值,是一個拷貝副本。
偽代碼:
void change(int a){//拿到求得的實參的值
a = a/2;
}
int i = 4;
change(i);
System.out.print(i);
因為是值的副本,所以在函數內對形參操作不會影響實參,所以輸出是4。
這里我們舉的例子是基本類型int類型的。那對於引用類型呢?
同樣需要對實參求值,這時得到的值是實參的地址值,形參拿到的是實參的地址值,這個地址值指向的是u1等號后面使用new關鍵字開辟出來的那片內存空間,所以此時u2也指向這片內存空間,所以打印出來u2將會和u1輸出同樣的內容。
偽代碼:
void change(User u2){//拿到求得的實參的地址值
System.out.print(u2);
u2 = getNewUser();
u2.setName("$%#@*")
System.out.print(u2);
}
public static void main(){
User u1 = new User();
u1.setName("1234");
change(u1);
System.out.print(u1);
}
然后,我們模仿上面的change(int a)的方法里,對形參接收到的值進行改變。注意,是形參的值,對change(User u2)來說,形參u2接收到的值是地址值,我們咋改變它呢?我們可以讓u2指向另一個內存空間,即通過getNewUser()方法獲取一個新的User對象,用這種方式給u2一個新的地址值,這不就改變了嗎。
此時我們看輸出,發現經過change()方法實參u1打印信息沒變,為什么?因為u1的地址值沒變,且u2是獲得新地址后(指向另一片內存),在新的這片內存里操作的,故而不會影響到之前的那片內存空間的數據。
這樣基本類型和引用類型的實驗方法是一樣的,看到的效果也是一樣的,即實參沒有隨形參的改變而改變。
總結
最后得出的結論:從語言設計的角度
,Java的方法調用時參數的求值策略是傳值調用(Call by value)的。
如果我們想表達引用類型傳遞的是引用,僅僅是想說傳的是引用不是別的東西的話,我們可以說的明確點:引用類型傳的是引用,和程序語言中的求值策略不沾邊 。那你說的引用傳遞就和求值策略中的傳引用調用沒關系,只是想表達傳的是引用的話也沒人會說你錯。由此來看文章開頭提到的前2種說法是不是也有解釋的余地?
存在即合理,不同的說法有不同的前提條件不同的解釋方式。如果是從程序語言設計的求值策略角度來問Java是哪種求值策略的話,那可以肯定的說是傳值調用(Call by value)。
(以下術語摘抄自Wiki。能力有限,對這樣些專業名詞還無法完美解讀,僅供參考)
附錄
傳引用調用和傳共享對象調用都是求值策略的一種。
傳引用調用(Call by reference)
在“傳引用調用”求值中,傳遞給函數的是它的實際參數的隱式引用而不是實參的拷貝。通常函數能夠修改這些參數(比如賦值),而且改變對於調用者是可見的。因此傳引用調用提供了一種調用者和函數交換數據的方法。傳引用調用的語言中追蹤函數調用的副作用比較難,易產生不易察覺的bug。
很多語言支持某種形式的傳引用調用,但是很少有語言默認使用它。FORTRAN II 是一種早期的傳引用調用語言。一些語言如C++、PHP、Visual Basic .NET、C#和REALbasic默認使用傳值調用,但是提供一種傳引用的特別語法。
在那些使用傳值調用又不支持傳引用調用的語言里,可以用引用(引用其他對象的對象),比如指針(表示其他對象的內存地址的對象)來模擬。C和ML就用了這種方法。這不是一種不同的求值策略(語言本身還是傳值調用)。它有時被叫做“傳地址調用”(call by address)。這可能讓人不易理解。在C之類不安全的語言里會引發解引用空指針之類的錯誤。但ML的引用是類型安全和內存安全的。
類似的效果可由傳共享對象調用(傳遞一個可變對象)實現。比如Python、Ruby。
例:C用指針模擬的傳引用調用
void modify(int p, int* q, int* r) {
p = 27; // passed by value: only the local parameter is modified
*q = 27; // passed by value or reference, check call site to determine which
*r = 27; // passed by value or reference, check call site to determine which
}
int main() {
int a = 1;
int b = 1;
int x = 1;
int* c = &x;
modify(a, &b, c); // a是傳值調用, b通過創建指針實現引用傳遞,c是按值傳遞的指針
//b and x are changed
return 0;
}
傳共享對象調用(Call by sharing)
此方式由Barbara Liskov命名[1],並被Python、Java(對象類型)、JavaScript、Scheme、OCaml等語言使用。
與傳引用調用不同,對於調用者而言在被調用函數里修改參數是沒有影響的。如果要達成傳引用調用的效果就需要傳一個共享對象,一旦被調用者修改了對象,調用者就可以看到變化(因為對象是共享的,沒有拷貝)。比如這段Python代碼:
def f(l):
l.append(1)
l = [2]
m = []
f(m)
print(m)會輸出[1]而不是[2]。因為列表是可變的,append方法改變了m。而賦值局部變量l的行為對外面作用域沒有影響(在這類語言中賦值是給變量綁定一個新對象,而不是改變對象)。
使用C/C++語言的程序員可能因不能用指針等使函數返回多個值而感到不便,但是像Python這樣的語言提供了替代方案:函數能方便的返回多個值,比C++11的std::tie更加簡單。