一文彻底搞懂Java中的值传递和引用传递!
關(guān)于Java中方法間的參數(shù)傳遞到底是怎樣的、為什么很多人說Java只有值傳遞等問題,一直困惑著很多人,甚至我在面試的時候問過很多有豐富經(jīng)驗的開發(fā)者,他們也很難解釋的很清楚。
我很久也寫過一篇文章,我當時認為我把這件事說清楚了,但是,最近在整理這部分知識點的時候,我發(fā)現(xiàn)我當時理解的還不夠透徹,于是我想著通過Google看看其他人怎么理解的,但是遺憾的是沒有找到很好的資料可以說的很清楚。
于是,我決定嘗試著把這個話題總結(jié)一下,重新理解一下這個問題。
辟謠時間
關(guān)于這個問題,在StackOverflow上也引發(fā)過廣泛的討論,看來很多程序員對于這個問題的理解都不盡相同,甚至很多人理解的是錯誤的。還有的人可能知道Java中的參數(shù)傳遞是值傳遞,但是說不出來為什么。
在開始深入講解之前,有必要糾正一下大家以前的那些錯誤看法了。如果你有以下想法,那么你有必要好好閱讀本文。
錯誤理解一:值傳遞和引用傳遞,區(qū)分的條件是傳遞的內(nèi)容,如果是個值,就是值傳遞。如果是個引用,就是引用傳遞。
錯誤理解二:Java是引用傳遞。
錯誤理解三:傳遞的參數(shù)如果是普通類型,那就是值傳遞,如果是對象,那就是引用傳遞。
實參與形參
我們都知道,在Java中定義方法的時候是可以定義參數(shù)的。比如Java中的main方法,public static void main(String[] args),這里面的args就是參數(shù)。參數(shù)在程序語言中分為形式參數(shù)和實際參數(shù)。
形式參數(shù):是在定義函數(shù)名和函數(shù)體的時候使用的參數(shù),目的是用來接收調(diào)用該函數(shù)時傳入的參數(shù)。
實際參數(shù):在調(diào)用有參函數(shù)時,主調(diào)函數(shù)和被調(diào)函數(shù)之間有數(shù)據(jù)傳遞關(guān)系。在主調(diào)函數(shù)中調(diào)用一個函數(shù)時,函數(shù)名后面括號中的參數(shù)稱為“實際參數(shù)”。
簡單舉個例子:
public?static?void?main(String[]?args)?{ParamTest?pt?=?new?ParamTest();pt.sout("Hollis");//實際參數(shù)為?Hollis } public?void?sout(String?name)?{?//形式參數(shù)為?nameSystem.out.println(name); }實際參數(shù)是調(diào)用有參方法的時候真正傳遞的內(nèi)容,而形式參數(shù)是用于接收實參內(nèi)容的參數(shù)。
求值策略
我們說當進行方法調(diào)用的時候,需要把實際參數(shù)傳遞給形式參數(shù),那么傳遞的過程中到底傳遞的是什么東西呢?
這其實是程序設(shè)計中求值策略(Evaluation strategies)的概念。
在計算機科學中,求值策略是確定編程語言中表達式的求值的一組(通常確定性的)規(guī)則。求值策略定義何時和以何種順序求值給函數(shù)的實際參數(shù)、什么時候把它們代換入函數(shù)、和代換以何種形式發(fā)生。
求值策略分為兩大基本類,基于如何處理給函數(shù)的實際參數(shù),分位嚴格的和非嚴格的。
?嚴格求值
在“嚴格求值”中,函數(shù)調(diào)用過程中,給函數(shù)的實際參數(shù)總是在應用這個函數(shù)之前求值。多數(shù)現(xiàn)存編程語言對函數(shù)都使用嚴格求值。所以,我們本文只關(guān)注嚴格求值。
在嚴格求值中有幾個關(guān)鍵的求值策略是我們比較關(guān)心的,那就是傳值調(diào)用(Call by value)、傳引用調(diào)用(Call by reference)以及傳共享對象調(diào)用(Call by sharing)。
傳值調(diào)用(值傳遞)
?在傳值調(diào)用中,實際參數(shù)先被求值,然后其值通過復制,被傳遞給被調(diào)函數(shù)的形式參數(shù)。因為形式參數(shù)拿到的只是一個"局部拷貝",所以如果在被調(diào)函數(shù)中改變了形式參數(shù)的值,并不會改變實際參數(shù)的值。
傳引用調(diào)用(應用傳遞)
在傳引用調(diào)用中,傳遞給函數(shù)的是它的實際參數(shù)的隱式引用而不是實參的拷貝。因為傳遞的是引用,所以,如果在被調(diào)函數(shù)中改變了形式參數(shù)的值,改變對于調(diào)用者來說是可見的。
傳共享對象調(diào)用(共享對象傳遞)
傳共享對象調(diào)用中,先獲取到實際參數(shù)的地址,然后將其復制,并把該地址的拷貝傳遞給被調(diào)函數(shù)的形式參數(shù)。因為參數(shù)的地址都指向同一個對象,所以我們稱也之為"傳共享對象",所以,如果在被調(diào)函數(shù)中改變了形式參數(shù)的值,調(diào)用者是可以看到這種變化的。
不知道大家有沒有發(fā)現(xiàn),其實傳共享對象調(diào)用和傳值調(diào)用的過程幾乎是一樣的,都是進行"求值"、"拷貝"、"傳遞"。你品,你細品。
但是,傳共享對象調(diào)用和內(nèi)傳引用調(diào)用的結(jié)果又是一樣的,都是在被調(diào)函數(shù)中如果改變參數(shù)的內(nèi)容,那么這種改變也會對調(diào)用者有影響。你再品,你再細品。
那么,共享對象傳遞和值傳遞以及引用傳遞之間到底有很么關(guān)系呢?
對于這個問題,我們應該關(guān)注過程,而不是結(jié)果,因為傳共享對象調(diào)用的過程和傳值調(diào)用的過程是一樣的,而且都有一步關(guān)鍵的操作,那就是"復制",所以,通常我們認為傳共享對象調(diào)用是傳值調(diào)用的特例
我們先把傳共享對象調(diào)用放在一邊,我們再來回顧下傳值調(diào)用和傳引用調(diào)用的主要區(qū)別:
傳值調(diào)用是指在調(diào)用函數(shù)時將實際參數(shù)`復制`一份傳遞到函數(shù)中,傳引用調(diào)用是指在調(diào)用函數(shù)時將實際參數(shù)的引用`直接`傳遞到函數(shù)中。
所以,兩者的最主要區(qū)別就是是直接傳遞的,還是傳遞的是一個副本。
這里我們來舉一個形象的例子。再來深入理解一下傳值調(diào)用和傳引用調(diào)用:
你有一把鑰匙,當你的朋友想要去你家的時候,如果你直接把你的鑰匙給他了,這就是引用傳遞。
這種情況下,如果他對這把鑰匙做了什么事情,比如他在鑰匙上刻下了自己名字,那么這把鑰匙還給你的時候,你自己的鑰匙上也會多出他刻的名字。
你有一把鑰匙,當你的朋友想要去你家的時候,你復刻了一把新鑰匙給他,自己的還在自己手里,這就是值傳遞。
這種情況下,他對這把鑰匙做什么都不會影響你手里的這把鑰匙。
Java的求值策略
前面我們介紹過了傳值調(diào)用、傳引用調(diào)用以及傳值調(diào)用的特例傳共享對象調(diào)用,那么,Java中是采用的哪種求值策略呢?
很多人說Java中的基本數(shù)據(jù)類型是值傳遞的,這個基本沒有什么可以討論的,普遍都是這樣認為的。
但是,有很多人卻誤認為Java中的對象傳遞是引用傳遞。之所以會有這個誤區(qū),主要是因為Java中的變量和對象之間是有引用關(guān)系的。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);}輸出結(jié)果:
? ?
print?in?pass?,?user?is?User{name='hollischuang',?gender='Male'} print?in?main?,?user?is?User{name='hollischuang',?gender='Male'}可以看到,對象類型在被傳遞到pass方法后,在方法內(nèi)改變了其內(nèi)容,最終調(diào)用方main方法中的對象也變了。
所以,很多人說,這和引用傳遞的現(xiàn)象是一樣的,就是在方法內(nèi)改變參數(shù)的值,會影響到調(diào)用方。
但是,其實這是走進了一個誤區(qū)。
Java中的對象傳遞
很多人通過代碼示例的現(xiàn)象說明Java對象是引用傳遞,那么我們就從現(xiàn)象入手,先來反駁下這個觀點。
我們前面說過,無論是值傳遞,還是引用傳遞,只不過是求值策略的一種,那求值策略還有很多,比如前面提到的共享對象傳遞的現(xiàn)象和引用傳遞也是一樣的。那憑什么就說Java中的參數(shù)傳遞就一定是引用傳遞而不是共享對象傳遞呢?
那么,Java中的對象傳遞,到底是哪種形式呢?其實,還真的就是共享對象傳遞。
其實在 《The Java? Tutorials》中,是有關(guān)于這部分內(nèi)容的說明的。首先是關(guān)于基本類型描述如下:
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.
即,原始參數(shù)通過值傳遞給方法。這意味著對參數(shù)值的任何更改都只存在于方法的范圍內(nèi)。當方法返回時,參數(shù)將消失,對它們的任何更改都將丟失。
關(guān)于對象傳遞的描述如下:
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.
也就是說,引用數(shù)據(jù)類型參數(shù)(如對象)也按值傳遞給方法。這意味著,當方法返回時,傳入的引用仍然引用與以前相同的對象。但是,如果對象字段具有適當?shù)脑L問級別,則可以在方法中更改這些字段的值。
這一點官方文檔已經(jīng)很明確的指出了,Java就是值傳遞,只不過是把對象的引用當做值傳遞給方法。你細品,這不就是共享對象傳遞么?
其實Java中使用的求值策略就是傳共享對象調(diào)用,也就是說,Java會將對象的地址的拷貝傳遞給被調(diào)函數(shù)的形式參數(shù)。只不過"傳共享對象調(diào)用"這個詞并不常用,所以Java社區(qū)的人通常說"Java是傳值調(diào)用",這么說也沒錯,因為傳共享對象調(diào)用其實是傳值調(diào)用的一個特例。
值傳遞和共享對象傳遞的現(xiàn)象沖突嗎?
看到這里很多人可能會有一個疑問,既然共享對象傳遞是值傳遞的一個特例,那么為什么他們的現(xiàn)象是完全不同的呢?
難道值傳遞過程中,如果在被調(diào)方法中改變了值,也有可能會對調(diào)用者有影響嗎?那到底什么時候會影響什么時候不會影響呢?
其實是不沖突的,之所以會有這種疑惑,是因為大家對于到底是什么是"改變值"有誤解。
我們先回到上面的例子中來,看一下調(diào)用過程中實際上發(fā)生了什么?
在參數(shù)傳遞的過程中,實際參數(shù)的地址0X1213456被拷貝給了形參。這個過程其實就是值傳遞,只不過傳遞的值得內(nèi)容是對象的應用。
那為什么我們改了user中的屬性的值,卻對原來的user產(chǎn)生了影響呢?
其實,這個過程就好像是:你復制了一把你家里的鑰匙給到你的朋友,他拿到鑰匙以后,并沒有在這把鑰匙上做任何改動,而是通過鑰匙打開了你家里的房門,進到屋里,把你家的電視給砸了。
這個過程,對你手里的鑰匙來說,是沒有影響的,但是你的鑰匙對應的房子里面的內(nèi)容卻是被人改動了。
也就是說,Java對象的傳遞,是通過復制的方式把引用關(guān)系傳遞了,如果我們沒有改引用關(guān)系,而是找到引用的地址,把里面的內(nèi)容改了,是會對調(diào)用方有影響的,因為大家指向的是同一個共享對象。
那么,如果我們改動一下pass方法的內(nèi)容:
public?void?pass(User?user)?{user?=?new?User();user.setName("hollischuang");System.out.println("print?in?pass?,?user?is?"?+?user); }上面的代碼中,我們在pass方法中,重新new了一個user對象,并改變了他的值,輸出結(jié)果如下:
print?in?pass?,?user?is?User{name='hollischuang',?gender='Male'} print?in?main?,?user?is?User{name='Hollis',?gender='Male'}再看一下整個過程中發(fā)生了什么:
這個過程,就好像你復制了一把鑰匙給到你的朋友,你的朋友拿到你給他的鑰匙之后,找個鎖匠把他修改了一下,他手里的那把鑰匙變成了開他家鎖的鑰匙。這時候,他打開自己家,就算是把房子點了,對你手里的鑰匙,和你家的房子來說都是沒有任何影響的。
所以,Java中的對象傳遞,如果是修改引用,是不會對原來的對象有任何影響的,但是如果直接修改共享對象的屬性的值,是會對原來的對象有影響的。
總結(jié)
我們知道,編程語言中需要進行方法間的參數(shù)傳遞,這個傳遞的策略叫做求值策略。
在程序設(shè)計中,求值策略有很多種,比較常見的就是值傳遞和引用傳遞。還有一種值傳遞的特例——共享對象傳遞。
值傳遞和引用傳遞最大的區(qū)別是傳遞的過程中有沒有復制出一個副本來,如果是傳遞副本,那就是值傳遞,否則就是引用傳遞。
在Java中,其實是通過值傳遞實現(xiàn)的參數(shù)傳遞,只不過對于Java對象的傳遞,傳遞的內(nèi)容是對象的引用。
我們可以總結(jié)說,Java中的求值策略是共享對象傳遞,這是完全正確的。
但是,為了讓大家都能理解你說的,我們說Java中只有值傳遞,只不過傳遞的內(nèi)容是對象的引用。這也是沒毛病的。
但是,絕對不能認為Java中有引用傳遞。
OK,以上就是本文的全部內(nèi)容,不知道本文是否幫助你解開了你心中一直以來的疑惑。歡迎留言說一下你的想法。
參考資料
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/
String性能提升10倍的幾個方法!(源碼+原理分析)
9個小技巧讓你的 if else看起來更優(yōu)雅
關(guān)注公眾號發(fā)送”進群“,老王拉你進讀者群。
總結(jié)
以上是生活随笔為你收集整理的一文彻底搞懂Java中的值传递和引用传递!的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 别在Java代码里乱打日志了,这才是正确
- 下一篇: 面试官 | Class.forName