關於Java中方法間的參數傳遞究竟是怎樣的、爲何不少人說Java只有值傳遞等問題,一直困惑着不少人,甚至我在面試的時候問過不少有豐富經驗的開發者,他們也很難解釋的很清楚。html
我好久也寫過一篇文章,我當時認爲我把這件事說清楚了,可是,最近在整理這部分知識點的時候,我發現我當時理解的還不夠透徹,因而我想着經過Google看看其餘人怎麼理解的,可是遺憾的是沒有找到很好的資料能夠說的很清楚。java
因而,我決定嘗試着把這個話題總結一下,從新理解一下這個問題。程序員
關於這個問題,在StackOverflow上也引起過普遍的討論,看來不少程序員對於這個問題的理解都不盡相同,甚至不少人理解的是錯誤的。還有的人可能知道Java中的參數傳遞是值傳遞,可是說不出來爲何。面試
在開始深刻講解以前,有必要糾正一下你們之前的那些錯誤見解了。若是你有如下想法,那麼你有必要好好閱讀本文。編程
錯誤理解一:值傳遞和引用傳遞,區分的條件是傳遞的內容,若是是個值,就是值傳遞。若是是個引用,就是引用傳遞。
bash錯誤理解二:Java是引用傳遞。
oracle
錯誤理解三:傳遞的參數若是是普通類型,那就是值傳遞,若是是對象,那就是引用傳遞。編程語言
咱們都知道,在Java中定義方法的時候是能夠定義參數的。好比Java中的main方法,public static void main(String[] args),這裏面的args就是參數。參數在程序語言中分爲形式參數和實際參數。ide
形式參數:是在定義函數名和函數體的時候使用的參數,目的是用來接收調用該函數時傳入的參數。函數
實際參數:在調用有參函數時,主調函數和被調函數之間有數據傳遞關係。在主調函數中調用一個函數時,函數名後面括號中的參數稱爲「實際參數」。
簡單舉個例子:
public static void main(String[] args) { ParamTest pt = new ParamTest(); pt.sout("Hollis");//實際參數爲 Hollis } public void sout(String name) { //形式參數爲 name System.out.println(name); }複製代碼
實際參數是調用有參方法的時候真正傳遞的內容,而形式參數是用於接收實參內容的參數。
咱們說當進行方法調用的時候,須要把實際參數傳遞給形式參數,那麼傳遞的過程當中到底傳遞的是什麼東西呢?
這實際上是程序設計中求值策略(Evaluation strategies)的概念。
在計算機科學中,求值策略是肯定編程語言中表達式的求值的一組(一般肯定性的)規則。求值策略定義什麼時候和以何種順序求值給函數的實際參數、何時把它們代換入函數、和代換以何種形式發生。
求值策略分爲兩大基本類,基於如何處理給函數的實際參數,分爲嚴格的和非嚴格的。
在「嚴格求值」中,函數調用過程當中,給函數的實際參數老是在應用這個函數以前求值。多數現存編程語言對函數都使用嚴格求值。因此,咱們本文只關注嚴格求值。
在嚴格求值中有幾個關鍵的求值策略是咱們比較關心的,那就是傳值調用(Call by value)、傳引用調用(Call by reference)以及傳共享對象調用(Call by sharing)。
傳值調用(值傳遞)
在傳值調用中,實際參數先被求值,而後其值經過複製,被傳遞給被調函數的形式參數。由於形式參數拿到的只是一個"局部拷貝",因此若是在被調函數中改變了形式參數的值,並不會改變實際參數的值。
傳引用調用(應用傳遞)
在傳引用調用中,傳遞給函數的是它的實際參數的隱式引用而不是實參的拷貝。由於傳遞的是引用,因此,若是在被調函數中改變了形式參數的值,改變對於調用者來講是可見的。
傳共享對象調用(共享對象傳遞)
傳共享對象調用中,先獲取到實際參數的地址,而後將其複製,並把該地址的拷貝傳遞給被調函數的形式參數。由於參數的地址都指向同一個對象,因此咱們稱也之爲"傳共享對象",因此,若是在被調函數中改變了形式參數的值,調用者是能夠看到這種變化的。
不知道你們有沒有發現,其實傳共享對象調用和傳值調用的過程幾乎是同樣的,都是進行"求值"、"拷貝"、"傳遞"。你品,你細品。
可是,傳共享對象調用和內傳引用調用的結果又是同樣的,都是在被調函數中若是改變參數的內容,那麼這種改變也會對調用者有影響。你再品,你再細品。
那麼,共享對象傳遞和值傳遞以及引用傳遞之間到底有什麼關係呢?
對於這個問題,咱們應該關注過程,而不是結果,由於傳共享對象調用的過程和傳值調用的過程是同樣的,並且都有一步關鍵的操做,那就是"複製",因此,一般咱們認爲傳共享對象調用是傳值調用的特例
咱們先把傳共享對象調用放在一邊,咱們再來回顧下傳值調用和傳引用調用的主要區別:
傳值調用是指在調用函數時將實際參數`複製`一份傳遞到函數中,傳引用調用是指在調用函數時將實際參數的引用`直接`傳遞到函數中。
因此,二者的最主要區別就是是直接傳遞的,仍是傳遞的是一個副本。
這裏咱們來舉一個形象的例子。再來深刻理解一下傳值調用和傳引用調用:
你有一把鑰匙,當你的朋友想要去你家的時候,若是你直接把你的鑰匙給他了,這就是引用傳遞。
這種狀況下,若是他對這把鑰匙作了什麼事情,好比他在鑰匙上刻下了本身名字,那麼這把鑰匙還給你的時候,你本身的鑰匙上也會多出他刻的名字。
你有一把鑰匙,當你的朋友想要去你家的時候,你復刻了一把新鑰匙給他,本身的還在本身手裏,這就是值傳遞。
這種狀況下,他對這把鑰匙作什麼都不會影響你手裏的這把鑰匙。
前面咱們介紹過了傳值調用、傳引用調用以及傳值調用的特例傳共享對象調用,那麼,Java中是採用的哪一種求值策略呢?
不少人說Java中的基本數據類型是值傳遞的,這個基本沒有什麼能夠討論的,廣泛都是這樣認爲的。
可是,有不少人卻誤認爲Java中的對象傳遞是引用傳遞。之因此會有這個誤區,主要是由於Java中的變量和對象之間是有引用關係的。Java語言中是經過對象的引用來操縱對象的。因此,不少人會認爲對象的傳遞是引用的傳遞。
並且不少人還能夠舉出如下的代碼示例:
public static void main(String[] args) { Test pt = new Test(); User hollis = new User(); hollis.setName("Hollis"); hollis.setGender("Male"); pt.pass(hollis); System.out.println("print in main , user is " + hollis); } public void pass(User user) { user.setName("hollischuang"); System.out.println("print in pass , user is " + user); }複製代碼
輸出結果:
print in pass , user is User{name='hollischuang', gender='Male'} print in main , user is User{name='hollischuang', gender='Male'}複製代碼
能夠看到,對象類型在被傳遞到pass方法後,在方法內改變了其內容,最終調用方main方法中的對象也變了。
因此,不少人說,這和引用傳遞的現象是同樣的,就是在方法內改變參數的值,會影響到調用方。
可是,其實這是走進了一個誤區。
不少人經過代碼示例的現象說明Java對象是引用傳遞,那麼咱們就從現象入手,先來反駁下這個觀點。
咱們前面說過,不管是值傳遞,仍是引用傳遞,只不過是求值策略的一種,那求值策略還有不少,好比前面提到的共享對象傳遞的現象和引用傳遞也是同樣的。那憑什麼就說Java中的參數傳遞就必定是引用傳遞而不是共享對象傳遞呢?
那麼,Java中的對象傳遞,究竟是哪一種形式呢?其實,還真的就是共享對象傳遞。
其實在 《The Java™ Tutorials》中,是有關於這部份內容的說明的。首先是關於基本類型描述以下:
Primitive arguments, such as an int or a double, are passed into methods by value. This means that any changes to the values of the parameters exist only within the scope of the method. When the method returns, the parameters are gone and any changes to them are lost.
即,原始參數經過值傳遞給方法。這意味着對參數值的任何更改都只存在於方法的範圍內。當方法返回時,參數將消失,對它們的任何更改都將丟失。
關於對象傳遞的描述以下:
Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object’s fields can be changed in the method, if they have the proper access level.
也就是說,引用數據類型參數(如對象)也按值傳遞給方法。這意味着,當方法返回時,傳入的引用仍然引用與之前相同的對象。可是,若是對象字段具備適當的訪問級別,則能夠在方法中更改這些字段的值。
這一點官方文檔已經很明確的指出了,Java就是值傳遞,只不過是把對象的引用當作值傳遞給方法。你細品,這不就是共享對象傳遞麼?
其實Java中使用的求值策略就是傳共享對象調用,也就是說,Java會將對象的地址的拷貝傳遞給被調函數的形式參數。只不過"傳共享對象調用"這個詞並不經常使用,因此Java社區的人一般說"Java是傳值調用",這麼說也沒錯,由於傳共享對象調用實際上是傳值調用的一個特例。
看到這裏不少人可能會有一個疑問,既然共享對象傳遞是值傳遞的一個特例,那麼爲何他們的現象是徹底不一樣的呢?
難道值傳遞過程當中,若是在被調方法中改變了值,也有可能會對調用者有影響嗎?那到底何時會影響何時不會影響呢?
實際上是不衝突的,之因此會有這種疑惑,是由於你們對於究竟是什麼是"改變值"有誤解。
咱們先回到上面的例子中來,看一下調用過程當中實際上發生了什麼?
在參數傳遞的過程當中,實際參數的地址0X1213456被拷貝給了形參。這個過程其實就是值傳遞,只不過傳遞的值得內容是對象的應用。
那爲何咱們改了user中的屬性的值,卻對原來的user產生了影響呢?
其實,這個過程就好像是:你複製了一把你家裏的鑰匙給到你的朋友,他拿到鑰匙之後,並無在這把鑰匙上作任何改動,而是經過鑰匙打開了你家裏的房門,進到屋裏,把你家的電視給砸了。
這個過程,對你手裏的鑰匙來講,是沒有影響的,可是你的鑰匙對應的房子裏面的內容倒是被人改動了。
也就是說,Java對象的傳遞,是經過複製的方式把引用關係傳遞了,若是咱們沒有改引用關係,而是找到引用的地址,把裏面的內容改了,是會對調用方有影響的,由於你們指向的是同一個共享對象。
那麼,若是咱們改動一下pass方法的內容:
public void pass(User user) { user = new User(); user.setName("hollischuang"); System.out.println("print in pass , user is " + user); }複製代碼
上面的代碼中,咱們在pass方法中,從新new了一個user對象,並改變了他的值,輸出結果以下:
print in pass , user is User{name='hollischuang', gender='Male'} print in main , user is User{name='Hollis', gender='Male'}複製代碼
再看一下整個過程當中發生了什麼:
這個過程,就好像你複製了一把鑰匙給到你的朋友,你的朋友拿到你給他的鑰匙以後,找個鎖匠把他修改了一下,他手裏的那把鑰匙變成了開他家鎖的鑰匙。這時候,他打開本身家,就算是把房子點了,對你手裏的鑰匙,和你家的房子來講都是沒有任何影響的。
因此,Java中的對象傳遞,若是是修改引用,是不會對原來的對象有任何影響的,可是若是直接修改共享對象的屬性的值,是會對原來的對象有影響的。
咱們知道,編程語言中須要進行方法間的參數傳遞,這個傳遞的策略叫作求值策略。
在程序設計中,求值策略有不少種,比較常見的就是值傳遞和引用傳遞。還有一種值傳遞的特例——共享對象傳遞。
值傳遞和引用傳遞最大的區別是傳遞的過程當中有沒有複製出一個副原本,若是是傳遞副本,那就是值傳遞,不然就是引用傳遞。
在Java中,實際上是經過值傳遞實現的參數傳遞,只不過對於Java對象的傳遞,傳遞的內容是對象的引用。
咱們能夠總結說,Java中的求值策略是共享對象傳遞,這是徹底正確的。
可是,爲了讓你們都能理解你說的,咱們說Java中只有值傳遞,只不過傳遞的內容是對象的引用。這也是沒毛病的。
可是,絕對不能認爲Java中有引用傳遞。
OK,以上就是本文的所有內容,不知道本文是否幫助你解開了你心中一直以來的疑惑。歡迎留言說一下你的想法。
參考資料:
https://docs.oracle.com/javase/tutorial/java/javaOO/arguments.html
https://en.wikipedia.org/wiki/Evaluation_strategy
https://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value
https://blog.penjee.com/passing-by-value-vs-by-reference-java-graphical/
轉載公衆號:Hollis,一個對Coding有着獨特追求的人,現任阿里巴巴技術專家,我的技術博主,技術文章全網閱讀量數千萬,《程序員的三門課》聯合做者。