終極解釋: java方法傳遞引數的方式
如果你還對此問題不清楚,或者似懂非懂有些疑惑,請看下文,看完此文,保證不用再看其他文章。
首先,我們來看下現有網上大多數文章對此問題是如何解釋的。
如果你已經搜尋過這個問題了,那麼你會很容易看到大批的答案都是“java引數的傳遞方式是值傳遞”,除此之外還會解釋一通什麼是引用傳遞。
那麼,請思考一個問題,什麼是值傳遞?什麼是引用傳遞?
請看下面一段C語言:
#include <stdio.h>
fun(int arg) {
arg = arg + 2;
}
funAnother(int & arg) {
*arg = *arg + 2;
}
main()
{
int a = 1;
printf(a);
fun(a);
printf(a);
funAnother(*a);
}
fun函式直接傳遞值,通常被人們稱之為值傳遞;funAnother中傳遞的是地址,通常被人們稱作地址傳遞,人們將此概念帶到面向物件的語言中就叫做引用傳遞。
為了完全弄明白此文要闡述的問題,請你務必在此處停下來想清楚什麼叫傳遞,程式語言中傳遞指的是什麼?也許你從開始學習第一門語言就學著寫函式或者方法並熟練的使用他們,你一開始沒有思考函式和方法的呼叫是什麼樣的一個過程,後來你更加習慣於寫函式和方法並且程式碼寫的相當漂亮,你已經忘記了此時可能需要問這個問題,但是請你現在好好思考一下它,或者你還真不明白。
如果你沒有思考上面的問題,或者說你沒有思考清楚就看到了這裡,說明兩點:
1、
你覺得這個問題不值得考慮,你已經對它很清楚的知道。
2、
你沒有耐心去思考這個問題,你急於想指導答案
3、
你覺得我在瞎掰,扯這麼多幹什麼╮(╯▽╰)╭
如果不是1。
是2,我要告訴你,這個問題設計編譯器的相關知識和組合語言相關知識,篇幅所限,我不會在這裡解釋,還請自行Google - -。
是3,我現在就可以告訴你答案:java中引數傳遞方式既不能叫值傳遞,也不能叫引用傳遞。值傳遞和引用傳遞用在描述C++還差不多,描述java不貼切。
下面我們通過幾個例子來解釋下我為什麼這麼說。
String
我們先看下String這個特殊類作為引數的傳遞(這裡用String 舉例):
private String testString(String test) {
System.out.println(test.hashCode());
test = new String("234");
return test;
}
String testStr = new String("123");
String testStr1 = "123";
String testStr2 = testStr;
String testStr3 = testStr1;
System.out.println(testStr.hashCode());
System.out.println(testStr1.hashCode());
System.out.println(testStr2.hashCode());
System.out.println(testStr3.hashCode());
System.out.println(testString(testStr).hashCode());
System.out.println(testStr.hashCode());
此處解釋下,hashCode()在api中的解釋大意是對該物件在記憶體地址中的直接或間接運算,地址相同該值一定相同,反之亦然。
在我電腦上列印結果是這樣的:
1:48690
2:48690
3:48690
4:48690
inner:48690
5:49683
6:48690
你會看編號為5的結果不一樣。請你思考片刻在繼續看。
對於String,jvm對其處理是這樣的:
new String(“123”) 和 “123”對於jvm來說是一樣處理的,首先看到這個之後,jvm會在記憶體中初始化一個常量,這塊記憶體中的值就是字串’1’, ‘2’, ‘3’,它們三個緊鄰,jvm通過其他方式確定它們三個一起構成了一個變數的值,型別為字串。你也許已經想到了,那麼初始化在記憶體的那塊呢,對了,這就是地址的概念,其實初始化到那塊是不確定的,並且初始化到記憶體的哪塊對我們來說都是不用關心的,我們只需要關心它的開始地址是哪裡就行了,這樣即使你需要關心這個字串所佔的記憶體,也可以計算出來它所跨區域的所有記憶體地址。另外,java中你其實並不需要知道確切的地址是多少,jvm有意對此進行了遮蔽。
前面說到jvm會初始化一個常量的“123”在記憶體中,對於String這種型別,對它的特殊處理就在於以後你宣告多少變數=”123”,它都不會在初始化一個“123”出來,jvm只會將第一次初始化的那個首地址賦值給你宣告的變數,這就是為什麼第1和2的列印結果相同的原因,至於第3為什麼和第2相同、第4和第1相同,很好理解,不再多說。
testString方法中有一個列印編號為inner,結果和外面的編號1、2列印相同,但是這裡有不同的含義:
首先在呼叫testString(testStr)的時候,jvm並不是直接把testStr的地址傳遞過來(此處需注意“testStr的值”和“testStr的地址”的區別,這兩者只是為了表達問題的一種敘述方式,testStr的值指的是它所指記憶體中的儲存的值;testStr的地址指的“本質上其實是testStr這個變數真是的值”這句話很容易誤導人請了解編譯原理相關知識,但是這句話與後文中的相關介紹很有關係如果可以還是請搞清楚,容易理解的說就是指變數所在的記憶體首地址。),而是將testStr的首地址賦值給了另一個變數test,所以此時test和testStr同時指向了同一塊記憶體地址但是兩者又是不同的。所以此處列印hashCode必然相同。
但是請注意接下來一句test = new String(“234”),然後return test,緊接著外面列印了它的hashCode,也就是第5編號的列印,你會發現此時的值不同了。這也很好理解,new之後jvm重新初始化了一個字串“234”,在記憶體的另一塊,那麼地址就不一樣了。
如果讀者將方法裡test = new String(“234”)改成 test = new String(“123”),試試結果又是什麼?
對於Integer、Double、Float、Boolean、Short等這些類物件作為引數傳遞跟String是一樣的,自己測試一下。
int
對於int、double、short、float、byte、long、char、boolean這類基本資料型別作為引數傳遞,其實只是傳遞值而已,對於它們而言,本身就不用關心地址的概念。
Object
對於物件,你可以理解它在記憶體中其實是多個像String物件一樣的巢狀。
public class Person {
protected String name;
public void setName(String name)
{
this.name=name;
}
public String getName()
{
return this.name;
}
}
類就像一個盒子,盒子裡還可以有盒子,這就是上面說的類的巢狀的含義。Person類中定義了String型別的name屬性,類裡面定義了類。
先不看Name屬性,我們先看下面的程式碼:
private Person test(Person person) {
System.out.println("inner:" + person.hashCode());
person = new Person();
return person;
}
Person person = new Person();
System.out.println("1:" + person.hashCode());
System.out.println("2:" + test(person).hashCode());
我這裡執行結果如下:
1:662441761
inner:662441761
2:1618212626
沒錯,你會發現它和前面講的String方式是一樣的。
再來驗證一下,我們通過改變Name的值來驗證是否如我們預期的和String一樣:
private Person test(Person person) {
System.out.println("inner:" + person.getName());
System.out.println("inner':" + person.getName().hashCode());
person.setName("Adam");
re
Person person = new Person();
person.setName("John");
System.out.println("1:" + person.getName());
System.out.println("1':" + person.getName().hashCode());
System.out.println("2:" + test(person).getName());
System.out.println("2':" + test(person).getName().hashCode());
System.out.println("3:" + person.getName());
System.out.println("3':" + person.getName().hashCode());
執行結果:
1:John
1':2314539
inner:John
inner':2314539
2:Adam
inner:Adam
inner':2035631
2':2035631
3:Adam
3':2035631
請思考一下結果為什麼是這樣?
沒錯,當Person物件person作為引數傳遞給test方法的時候,jvm將person的地址拷貝了一份副本傳了進去,所以前面沒有考慮Name的程式碼中inner編號的hashcode是一樣的,當在test內部呼叫setName的時候,改變的依然是原person物件所在記憶體中Name位置的值,所以裡面更改,就改變了外面的物件的屬性值。
如果你足夠仔細,估計你已經想到了一件事情,根據前面的測試,我們還不能夠說明一點“jvm將person的地址拷貝了一份副本傳了進去”,目前還不能確定是拷貝的副本還是它自己。
下面繼續驗證,我們將test稍微修改一下:
private Person test(Person person) {
System.out.println("inner:" + person.getName());
System.out.println("inner':" + person.getName().hashCode());
person = new Person();
person.setName("Adam");
return person;
}
再次執行這段程式碼:
Person person = new Person();
person.setName("John");
System.out.println("1:" + person.getName());
System.out.println("1':" + person.getName().hashCode());
System.out.println("2:" + test(person).getName());
System.out.println("2':" + test(person).getName().hashCode());
System.out.println("3:" + person.getName());
System.out.println("3':" + person.getName().hashCode());
結果如下:
1:John
1':2314539
inner:John
inner':2314539
2:Adam
inner:John
inner':2314539
2':2035631
3:John
3':2314539
請看,此時的編號3和編號1的結果是一樣的,這就證明了傳進來的是對外面person物件首地址的一個拷貝,而不是它自己。
在程式設計中使用最廣泛的java集合類、JsonObject、jsonArray等這些類作為引數傳遞使用,要深刻理解java方法傳遞引數方式,以防使用錯誤導致資料不一致。
再回過頭來請思考一下開頭說的,java方法引數傳遞方式是值傳遞還是引用傳遞?我覺得理解了本質就不用去回答到底是值傳遞還是引用傳遞的問題了,如果面試中被問到這樣的問題,請反問面試官“請解釋值傳遞和引用傳遞的概念,好讓我回答這個問題”。