寫這個的原因主要是今天看到了知乎的一個問題江咳,發(fā)現(xiàn)自己有些地方有點懵逼,寫下來記錄一下哥放,知乎上排名第一的答案說的很清楚歼指,不過看了以后依舊有點迷迷糊糊爹土,所以自己寫了個幾行代碼測試。
首先上一個踩身,感覺比較對的結(jié)論:
Horstmann的《java核心技術(shù)》(中文第8版P115-P117)原文描述:
”java程序設(shè)計語言總是采用值調(diào)用胀茵。也就是說,方法得到的是所有參數(shù)值的一個拷貝挟阻,特別是琼娘,方法不能修改傳遞給它的任何參數(shù)變量的內(nèi)容「礁耄“
”有些程序員(甚至是本書的作者)脱拼,認(rèn)為java程序設(shè)計語言對對象采用的是引用調(diào)用,實際上這種理解是不對的坷备∠ㄅǎ”
然后補(bǔ)充幾句我的理解:
- 首先,Java在傳遞過程中省撑,傳遞的只有值赌蔑,但是表現(xiàn)出來的形式,卻既有值傳遞也有引用傳遞丁侄,因此惯雳,沒必要糾結(jié)于名字朝巫,能理解原理即可鸿摇。
- 在傳遞對象進(jìn)函數(shù)時,對象的所有數(shù)據(jù)會被拷貝到局部變量中劈猿,這也就導(dǎo)致了局部變量修改其成員變量值時會導(dǎo)致原始的變量的成員變量值產(chǎn)生響應(yīng)的改變拙吉,因為他們持有的成員變量的引用指向了同一個地址塊(內(nèi)存空間)。
- 而對于傳遞8種基本變量時揪荣,也只是拷貝了值筷黔,因此對基本變量其本身的修改,無法導(dǎo)致原始變量的的修改仗颈。
- 不過這里需要考慮特殊情況佛舱,就是String,其表現(xiàn)形式和8種基本變量一樣挨决,具體下文有分析请祖,而對于String為何要這么做,我也不清楚脖祈,不是很懂 jvm 和 Java 的設(shè)計肆捕。
一. 值類型和引用類型(此處先不考慮String)的傳遞:
public class Student {
int age;
String name;
}
public class TestReference {
public static void main(String[] args){
Student student = new Student();
student.age = 10;
System.out.println(student.age);//10
addAge(student);
System.out.println(student.age);//11
addAge(student.age);
System.out.println(student.age);//11
}
static public void addAge(Student paramStudent){
paramStudent.age = 11;
}
static public void addAge(int paramAge){
paramAge = 12;
}
}
對以上代碼進(jìn)行解釋
- 首先
addAge(student)
調(diào)用的是addAge(Student paramStudent)
,該部分其實很好理解盖高,首先慎陵,paramStudet
對象眼虱,拷貝了傳入的studet
對象所有的數(shù)據(jù),因此paramStudet
它所指向的地址席纽,其實和student
是一樣的捏悬,所以,當(dāng)paramStudent
改變它的age
值時胆筒,其觸發(fā)的操作和student
改變age
的值是一樣的 ,因為他們都指向了同一個地址塊邮破。 - 其次
addAge(student.age)
調(diào)用的是addAge(int paramAge)
,也很好理解仆救,paramAge
只是拷貝了studet.age
的值抒和,此處為10,然后改變了paramAge
的值彤蔽,但此時paramAge
與引用類型不同摧莽,它保存的只有一個值,所以其實這個parmaAge
作為一個局部變量顿痪,并不能對原本的student.age
產(chǎn)生任何影響
二. String的問題:
1. String問題來源
上面的例子其實很好搞清楚镊辕,但是我在碰到String的時候就有點懵逼了,如果調(diào)用以下方法,結(jié)果會如注釋顯示蚁袭。
public static void main(String[] args){
Student student = new Student();
student.age = 10;
student.name = "dove";
changeName(student);
System.out.println(student.name);//dove_2
changeName(student.name);
System.out.println(student.name);//dove_2
changeName2(student.name);
System.out.println(student.name);//dove_2
}
static void changeName(Student paramStudent){
paramStudent.name = "dove_2";
}
static void changeName(String paramName){
paramName = "dove_3";
}
static void changeName2(String paramName){
paramName += "233";
}
changeName2(String paramName)
此處講道理被調(diào)用后應(yīng)該是"dove_2233",因為String
是一個引用類型,也就是說此處的parmaName
應(yīng)該是指向和傳入的參數(shù)指向了相同的一個地址塊征懈,然后對指向的內(nèi)存進(jìn)行了修改,然而結(jié)果并不是,原因就在于String
是一個不可變的類型(為啥不可變呢,具體可以看String
類的實現(xiàn),它是一個final class
,并且其內(nèi)部正真保存著字符串的value[]
也是不可變的(final
),所以意味著修改Sting
是不可能的)揩悄。
2.腦洞猜想可能情況
所以猜測上述的changeName2
過程類似于
FuckString fuckString = new FuckString();//paramName
FuckString fuckString2 = new FuckString(fuckString);//構(gòu)造出的新的值
fuckString = fuckString2;//把paramName指向構(gòu)造出的新值
然后卖哎,這就有點想不通了,不可變的類型删性,String 的 +
是怎么弄的呢亏娜?打個斷點試試看留攒,強(qiáng)制進(jìn)入纺腊,發(fā)現(xiàn)跳轉(zhuǎn)到了StringBuilder的構(gòu)造方法里凄敢,這說明應(yīng)該是構(gòu)造了一個新的StringBuilder對象喇勋。
同時兵怯,底部的Debug里拋出了個錯誤堕绩,說是無法獲取StringBuilder.toString()
,也就進(jìn)一步證明此處有新的String
的產(chǎn)生擒悬。
到這里基本上就驗證了我的猜想窍帝,String
+
會產(chǎn)生一個新的String
對象榕茧,既然這樣垃沦,反編譯下,看下字節(jié)碼雪猪,估計基本就搞定這個懵逼的問題了栏尚。
3.字節(jié)碼驗證
于是就寫了以下的類,用來驗證:
public class Main {
public static void main(String[] args){
String s = "dove";
s += "233";
}
}
然后javac,然后javap -c译仗,看字節(jié)碼,如下圖抬虽。
嘗試著解釋下該部分代碼(不是很看的懂字節(jié)碼,所以有些解釋可能不是很規(guī)范纵菌,不過講道理大概意思不會差很遠(yuǎn))
-
String s = "dove";
部分字節(jié)碼及解釋
0: ldc #2 // String dove
2: astore_1
第0行阐污,將一個常量加載到操作數(shù)棧,也就是把“dove”這玩意,放進(jìn)了操作數(shù)棧(也不知道是什么東西咱圆,蛤蛤)
第2行笛辟,將一號數(shù)值(下劃線1代表一號,大概理解序苏,不是很準(zhǔn)確)從操作數(shù)棧存儲到局部變量表手幢,說白了就是把“dove”給存了起來?
-
s += "233";
部分字節(jié)碼及解釋
3: new #3 // class java/lang/StringBuilder
6: dup
7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
10: aload_1
11: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
14: ldc #6 // String 233
16: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
19: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
第3行忱详,這個很明顯围来,不google也知道,new StringBuilder()
匈睁,也就是搞了個StringBUilder
的實例监透。
第6行,Java虛擬機(jī)提供了一些用于直接操作操作數(shù)棧航唆,不是很懂胀蛮,貌似對整體理解影響不大,先過糯钙。
第7行粪狼,invokespecial 調(diào)用一些需要特殊處理的實例方法,包括實例初始化方法超营、私有方法和父類方法鸳玩,此處應(yīng)該是在初始化StringBuilder對象阅虫。
第10行演闭,將1號局部變量(下劃線1指代一號變量)加載到操作棧,這里應(yīng)該是指“dove”
第11行颓帝,調(diào)用對象的實例方法,此處就是調(diào)用StringBuilder.append
,也就是把“dove”加到了StringBuilder
中
第14行米碰,將一個常量加載到操作數(shù)棧,就是把“233”載入
第16行购城,調(diào)用對象的實例方法,此處就是調(diào)用StringBuilder.append
吕座,把“233”給加到“StringBuilder”中
第19行,調(diào)用對象的實例方法,此處就是調(diào)用StringBuilder.toString
瘪板,而該方法吴趴,會觸發(fā)new String()
的操作,因此侮攀,會返還一個新的String
對象
4.最終結(jié)論:
從腦洞斷點以及最后的字節(jié)碼分析可以看出锣枝,s +="233"
,會導(dǎo)致一個新的String對象生成厢拭,也就是說,調(diào)用changeName2(String paramName)
會使得paramName
指向一個新的String對象撇叁,這樣就意味著供鸠,對該數(shù)據(jù)的改變并不會影響本身student.name
的值,由此,String懵逼的問題也解決了陨闹。
以上楞捂,就是整個關(guān)于Java引用傳遞和值傳遞的理解,有說的不對的趋厉,望指正寨闹。