做者:Intopass
連接:https://www.zhihu.com/question/31203609/answer/50992895
來源:知乎
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
java
首先,不要糾結於 Pass By Value 和 Pass By Reference 的字面上的意義,不然很容易陷入所謂的「一切傳引用其實本質上是傳值」這種並不能解決問題無心義論戰中。
更況且,要想知道Java究竟是傳值仍是傳引用,起碼你要先知道傳值和傳引用的準確含義吧?但是若是你已經知道了這兩個名字的準確含義,那麼你本身就能判斷Java究竟是傳值仍是傳引用。
這就好像用大學的名詞來解釋高中的題目,對於初學者根本沒有任何意義。windows
int num = 10;
String str = "hello";
如圖所示,num是基本類型,值就直接保存在變量中。而str是引用類型,變量中保存的只是實際對象的地址。通常稱這種變量爲"引用",引用指向實際對象,實際對象中保存着內容。數組
num = 20; str = "java";
對於基本類型 num ,賦值運算符會直接改變變量的值,原來的值被覆蓋掉。
對於引用類型 str,賦值運算符會改變引用中所保存的地址,原來的地址被覆蓋掉。可是原來的對象不會被改變(重要)。
如上圖所示,"hello" 字符串對象沒有被改變。(沒有被任何引用所指向的對象是垃圾,會被垃圾回收器回收)app
第一個例子:基本類型
void foo(int value) { value = 100; } foo(num); // num 沒有被改變
第二個例子:沒有提供改變自身方法的引用類型
void foo(String text) { text = "windows"; } foo(str); // str 也沒有被改變
第三個例子:提供了改變自身方法的引用類型
StringBuilder sb = new StringBuilder("iphone"); void foo(StringBuilder builder) { builder.append("4"); } foo(sb); // sb 被改變了,變成了"iphone4"。
第四個例子:提供了改變自身方法的引用類型,可是不使用,而是使用賦值運算符。
StringBuilder sb = new StringBuilder("iphone"); void foo(StringBuilder builder) { builder = new StringBuilder("ipad"); } foo(sb); // sb 沒有被改變,仍是 "iphone"。
重點理解爲何,第三個例子和第四個例子結果不一樣?iphone
下面是第三個例子的圖解:jvm
<img data-rawheight="398" src="https://pic2.zhimg.com/50/d8b82e07ea21375ca6b300f9162aa95f_hd.jpg" data-size="normal" data-rawwidth="772" class="origin_image zh-lightbox-thumb" width="772" data-original="https://pic2.zhimg.com/d8b82e07ea21375ca6b300f9162aa95f_r.jpg">builder.append("4")以後ui
<img data-rawheight="424" src="https://pic1.zhimg.com/50/ff2ede9c6c55568d42425561f25a0fd7_hd.jpg" data-size="normal" data-rawwidth="696" class="origin_image zh-lightbox-thumb" width="696" data-original="https://pic1.zhimg.com/ff2ede9c6c55568d42425561f25a0fd7_r.jpg">下面是第四個例子的圖解:spa
<img data-rawheight="398" src="https://pic2.zhimg.com/50/d8b82e07ea21375ca6b300f9162aa95f_hd.jpg" data-size="normal" data-rawwidth="772" class="origin_image zh-lightbox-thumb" width="772" data-original="https://pic2.zhimg.com/d8b82e07ea21375ca6b300f9162aa95f_r.jpg">
builder = new StringBuilder("ipad"); 以後線程
<img data-rawheight="438" src="https://pic1.zhimg.com/50/46fa5f10cc135a3ca087dae35a5211bd_hd.jpg" data-size="normal" data-rawwidth="710" class="origin_image zh-lightbox-thumb" width="710" data-original="https://pic1.zhimg.com/46fa5f10cc135a3ca087dae35a5211bd_r.jpg">2018年1月31日添加部份內容:設計
這個答案點讚的很多,雖然當時回答時並無講的特別詳細,今天就稍微多講一些各類類型數據在內存中的存儲方式。
從局部變量/方法參數開始講起:
局部變量和方法參數在jvm中的儲存方法是相同的,都是在棧上開闢空間來儲存的,隨着進入方法開闢,退出方法回收。以32位JVM爲例,boolean/byte/short/char/int/float以及引用都是分配4字節空間,long/double分配8字節空間。對於每一個方法來講,最多佔用多少空間是必定的,這在編譯時就能夠計算好。
咱們都知道JVM內存模型中有,stack和heap的存在,可是更準確的說,是每一個線程都分配一個獨享的stack,全部線程共享一個heap。對於每一個方法的局部變量來講,是絕對沒法被其餘方法,甚至其餘線程的同一方法所訪問到的,更遑論修改。
當咱們在方法中聲明一個 int i = 0,或者 Object obj = null 時,僅僅涉及stack,不影響到heap,當咱們 new Object() 時,會在heap中開闢一段內存並初始化Object對象。當咱們將這個對象賦予obj變量時,僅僅是stack中表明obj的那4個字節變動爲這個對象的地址。
數組類型引用和對象:
當咱們聲明一個數組時,如int[] arr = new int[10],由於數組也是對象,arr其實是引用,stack上僅僅佔用4字節空間,new int[10]會在heap中開闢一個數組對象,而後arr指向它。
當咱們聲明一個二維數組時,如 int[][] arr2 = new int[2][4],arr2一樣僅在stack中佔用4個字節,會在內存中開闢一個長度爲2的,類型爲int[]的數組,而後arr2指向這個數組。這個數組內部有兩個引用(大小爲4字節),分別指向兩個長度爲4的類型爲int的數組。
<img data-rawheight="740" src="https://pic4.zhimg.com/50/v2-6590cb935ae8bf3b7241cb309fe041d7_hd.jpg" data-size="normal" data-rawwidth="1498" class="origin_image zh-lightbox-thumb" width="1498" data-original="https://pic4.zhimg.com/v2-6590cb935ae8bf3b7241cb309fe041d7_r.jpg">
因此當咱們傳遞一個數組引用給一個方法時,數組的元素是能夠被改變的,可是沒法讓數組引用指向新的數組。
你還能夠這樣聲明:int[][] arr3 = new int[3][],這時內存狀況以下圖
<img data-rawheight="656" src="https://pic1.zhimg.com/50/v2-fdc86227021d56a02b559d6485983c71_hd.jpg" data-size="normal" data-rawwidth="1408" class="origin_image zh-lightbox-thumb" width="1408" data-original="https://pic1.zhimg.com/v2-fdc86227021d56a02b559d6485983c71_r.jpg">
你還能夠這樣 arr3[0] = new int [5]; arr3[1] = arr2[0];
<img data-rawheight="1026" src="https://pic3.zhimg.com/50/v2-fdc5e737a95d625a47d66ab61e4a2f55_hd.jpg" data-size="normal" data-rawwidth="1758" class="origin_image zh-lightbox-thumb" width="1758" data-original="https://pic3.zhimg.com/v2-fdc5e737a95d625a47d66ab61e4a2f55_r.jpg">
關於String:
本來回答中關於String的圖解是簡化過的,實際上String對象內部僅須要維護三個變量,char[] chars, int startIndex, int length。而chars在某些狀況下是能夠共用的。可是由於String被設計成爲了避免可變類型,因此你思考時把String對象簡化考慮也是能夠的。
String str = new String("hello")
<img data-rawheight="628" src="https://pic1.zhimg.com/50/v2-a143d0a3594d06f54c6853c46c429e08_hd.jpg" data-size="normal" data-rawwidth="1394" class="origin_image zh-lightbox-thumb" width="1394" data-original="https://pic1.zhimg.com/v2-a143d0a3594d06f54c6853c46c429e08_r.jpg">
固然某些JVM實現會把"hello"字面量生成的String對象放到常量池中,而常量池中的對象能夠實際分配在heap中,有些實現也許會分配在方法區,固然這對咱們理解影響不大。