java中對象作為參數傳遞給一個方法,到底是值傳遞,還是引用傳遞?
String和int參數傳遞是按值傳遞還是引用傳遞?
一道面試題目,String的傳遞:
public String change(String s){ s = "222"; return s; } public static void main(Stirng[] args){ String s = "111"; change(s); sout(s); }
我看到題目愣了一下,本來不假思考的結果是111,但仔細想,String是對象類型的,對象傳遞的是地址,那么地址傳遞到方法里面后,將指向修改成222,那么結果應該是222才對。實際恰恰相反。
Java 編程語言只有值傳遞參數。當一個對象實例作為一個參數被傳遞到方法中時,參數的值就是該對象的引用一個副本。指向同一個對象,對象的內容可以在被調用的方法中改變,但對象的引用(不是引用的副本)是永遠不會改變的。
java傳遞參數都是值,如果是對象的話,就是將引用的值復制一份給方法當參數。如果是根據引用把堆里的對象修改了,那么對象真被修改了,不過不是被創建賦值給的那個引用修改的,是方法里的一個復制的引用副本給修改的。換句話說,施瓦星格的媳被施瓦星格的克隆人親了下。
用實例去理解,其實這個理解也就是根據jdk的結果告訴我自己記住規則是這樣的,以后要記住。
public String change(String s, int i, StringBuffer sb, Person p){ s="123"; i=3; sb.append("woshi"); p.setAge(100); sb = new StringBuffer("sbsb"); p = new Person("bb",44); return s; } @Test public void testChange(){ StringBuffer sb = new StringBuffer("buff"); String s = "aaa"; int i = 1; Person p = new Person("aa",12); i=2; change(s,i,sb,p); // s="222"; System.out.println(s); System.out.println(i); System.out.println(sb.toString()); System.out.println(p); }
這里一共測試了String,int,一個對象StringBuffer,一個對象people。讓我們來仔細看看這些傳遞都發生了什么。我想有很大一部分人都猜不出打印結果。
aaa 2 buffwoshi Person{id=0, name='aa', age=100, Country=null, hashcode=638783031}
我們來一個個分析。
首先是String。
String s = "aaa";
這里,jvm創建一個變量引用s,在堆中創建一個對象aaa,將aaa放進常量池。s指向aaa。
然后就到了change方法里。這里這樣理解:將s引用的一個拷貝傳給方法change。這樣change有一個變量s,這個s也是指向aaa的。那么我們來通過debug來看后來發生了什么。
1.s指向aaa的時候:
2.s運行到change方法里的時候
然后看s再次賦值的時候:
然后我們運行結束change方法后到主方法里:
到這里s就結束了。那么如果我們按照傳遞的是s這個變量的引用,即String s="aaa"中這個s本身,那么,s這個本身是個變量,s指向aaa,在方法change里s又指向了123,回到主方法后s變量的指向被改變了?錯!顯然s仍舊是aaa,那么只能這樣理解:s傳遞到方法里的時候,復制了s指向的地址給change,change里的s是另一個s,s指向aaa(@718),然后在change中s又指向了123(@731),由於String是不可變類(final and Immutable),這里只是把副本s的指向修改成731,原地址718里的對象沒有發生改變因為String不可變。那么,回到主方法的時候,s變量本身沒有任何改變,s仍舊指向地址718,718的內容是aaa。所以最終打印aaa。
然后是StringBuffer
int是基本類型,所以int只是將值復制一份給別的方法用,這個大家都知道,就不去測試了。現在看StringBuffer發生的改變。
1.初始化:
2.到change方法中:
3.發生append
4.指向新對象
這里就要說一下了,副本指向了新對象。就好比,施瓦星格的克隆人找了另一個女的當老婆,而真正的施瓦星格老婆沒有變。
5.回到主方法:
到這里,StringBuffer就結束了。我們必須知道,雖然我們沒有去研究源碼是怎樣實現的,change方法得到是一個sb的副本,只不過這個副本指向708,在change里對708的對象追加,708的對象就真的改變了。然后又把sb副本指向新地址737。這只是把副本指向的地址修改了,如果你在這里打印sb.toString(),打印的就是737里的內容。當跳出change,回到主方法的時候,原sb仍舊還是指向708的,最終就是打印708的結果。和String不同的是,StringBuffer的結果發生了變量,因為StringBuffer是可變的,可以append。而String是不可變的,在change中s=123就是發生兩個行為,一個是查找常量池中是否有123,如果沒有就在堆中創建123,一個是將s指向123.也就是說這時候是創建了一個新的String對象,而不是把原來的String對象s內容修改。這樣,回到主方法的時候,s仍舊是aaa。
同理,看自己創建的對象people
1.初始化:
2.p傳遞到change里的時候
3.p副本設置age
4.p副本重新賦值
這里仍舊要說一下,p副本修改了自己指向,並不影響主方法里的p的指向。主方法里的p的指向沒有發生變化,依舊應該還是720.
5.回到主方法
總結:
通過上面對String,StringBuffer,People的研究,應該明白一個道理,重要的話說三遍,重要的規則我都演示了三遍。如果跟着步驟一步步走的,肯定牢記住了:
java所有的參數傳遞都是傳遞的副本,變量所代表的值的副本!java所有的參數傳遞都是傳遞的副本,變量所代表的值的副本!java所有的參數傳遞都是傳遞的副本,變量所代表的值的副本!
這里必須記住的就是副本概念。在方法里,運行的時候到這里的線程都會把傳過來的參數拷貝副本帶自己的工作區中,在工作區中對這個副本的值發生一些改變。最終改變的是副本,如果通過副本的指向修改了指向中的內容,那么那個指向的地址里的內容確實改變了。如果修改了副本的指向,即給副本重新賦值,那么關原來的變量何事?元變量仍舊指向最初的地址。
那么,String傳遞過去的是副本,修改了副本的指向,打印元string是不會改變的,因為副本沒有能力修改final的String類。