你真的瞭解JAVA的形參和實參嗎?
前幾天在頭條上看到一道經典面試題,引發了一些思考。也是寫這篇文章的導火索。
背景
請看題:
看到這個題後 瞬間覺得有坑。也覺得為什麼要書寫一個 swap
方法呢?如下實現不是更簡單:
輸出:
完美實現交換。但是請注意,這是一道面試題,要的就是考驗一些知識點。所以還是老老實實的實現 swap
方法吧。 有的同學可能會想, Integer
是一個包裝型別,是對Int的裝箱和拆箱操作。其實也是一個物件。既然是物件,直接更改物件的引用不就行了?
思路沒問題,我們首先看看實現:
輸出:
不出意外,沒有成功
這是什麼原因呢? 技術老手一看就知道問題出在形參和實參混淆了
JAVA的形參和實參的區別:
形參顧名思義:就是形式引數,用於定義方法的時候使用的引數,是用來接收呼叫者傳遞的引數的。 形參只有在方法被呼叫的時候,虛擬機器才會分配記憶體單元,在方法呼叫結束之後便會釋放所分配的記憶體單元。 因此,形參只在方法內部有效,所以針對引用物件的改動也無法影響到方法外。
實參顧名思義:就是實際引數,用於呼叫時傳遞給方法的引數。實參在傳遞給別的方法之前是要被預先賦值的。 在本例中 swap 方法 的numa, numb 就是形參,傳遞給 swap 方法的 a,b 就是實參
注意:
在 值傳遞
呼叫過程中,只能把實參傳遞給形參,而不能把形參的值反向作用到實參上。在函式呼叫過程中,形參的值發生改變,而實參的值不會發生改變。
而在 引用傳遞
呼叫的機制中,實際上是將實參引用的地址傳遞給了形參,所以任何發生在形參上的改變也會發生在實參變數上。
那麼問題來了,什麼是 值傳遞
和 引用傳遞
值傳遞和引用傳遞
在談 值傳遞
和 引用傳遞
之前先了解下 Java的資料型別有哪些
JAVA的資料型別
基本型別
和
物件型別
。相應的,變數也有兩種型別:
基本型別
和
引用型別
基本型別
的變數儲存
原始值
,即它代表的值就是數值本身,
原始值
一般對應在記憶體上的
棧區
而 引用型別
的變數儲存 引用值
, 引用值
指向記憶體空間的地址。代表了某個物件的引用,而不是物件本身。物件本身存放在這個引用值所表示的地址的位置。 被引用的物件
對應記憶體上的 堆記憶體區
。
基本型別包括: byte
, short
, int
, long
, char
, float
, double
, boolean
這八大基本資料型別 引用型別包括: 類型別
, 介面型別
和 陣列
變數的基本型別和引用型別的區別
基本資料型別在宣告時系統就給它分配空間
好了,Java的資料型別說完了,繼續我們的 值傳遞
和 引用傳遞
的話題。 先背住一個概念: 基本型別
的變數是 值傳遞
; 引用型別
的變數 結合前面說的 形參
和 實參
。
值傳遞
方法呼叫時,實際引數把它的值傳遞給對應的形式引數,函式接收的是原始值的一個copy, 此時記憶體中存在兩個相等的基本型別,即實際引數和形式引數,後面方法中的操作都是對形參這個值的修改,不影響實際引數的值
引用傳遞
也稱為 地址傳遞
, 址傳遞
。方法呼叫時,實際引數的引用(地址,而不是引數的值)被傳遞給方法中相對應的形式引數,函式接收的是原始值的記憶體地址 在方法執行中,形參和實參內容相同,指向同一塊記憶體地址,方法執行中對引用的操作將會影響到實際物件 通過例子來說話:
輸出:
看見 值傳遞
a的值並沒有改變,而 引用傳遞
的 persion.age已經改變了 有人說
為什麼 輸出的 person.age 還是20呢?
我想說 瞭解一下什麼是 引用型別
吧? 方法內把 形參
的地址引用換成了另一個物件,並沒有改變這個物件,並不能影響 外邊 實參
還引用原來的物件,因為 形參只在方法內有效哦。
有人或許還有疑問,按照文章開頭的例子, Integer
也是 引用型別
該當如何呢? 其實 類似的 String
, Integer
, Float
, Double
, Short
, Byte
, Long
, Character
等等基本包裝型別類。因為他們本身沒有提供方法去改變內部的值,例如 Integer
內部有一個 value
來記錄 int
基本型別的值,但是沒有提供修改它的方法,而且 也是 final
型別的,無法通過 常規手段
更改。
所以雖然他們是 引用型別
的,但是我們可以認為它是 值傳遞
,這個也只是 認為
,事實上還是 引用傳遞
, 址傳遞
。
好了,基礎知識補充完畢,然我們回到面試題吧
迴歸正題
通過補習基礎知識,我們很明顯知道 上面這個方法實現替換 是不可行的。因為 Interger
雖然是 引用型別
但是上述操作只是改變了 形參
的引用,而沒有改變 實參
對應的 物件
。
那麼思路來了,我們 通過特殊手段
改變 Integer
內部的 value
屬性
輸出結果: java a=1,b=2a=2,b=2
又來疑問了?為何 a
的值改變成功,而 b
的改變失敗呢?
見程式碼註釋 所以其實 field.set(numb,tmp);
是更改成功的,只是 tmp 經過前一行程式碼的執行,已經變成了 2。 那麼如何破呢? 我們有了一個思路,既然是 tmp
的引用的物件值變數,那麼我讓 tmp
不引用 numa
了
這種情況下 對 numa
這個物件的修改就不會導致 tmp
的值變化了,看一下執行結果
這是為啥?有沒有 快瘋
啦? 難道我們的思路錯了? 先彆著急,我們看看這個例子: 僅僅是將前面的例子 a
的值改為 129, b
的值改為130
執行結果:
有沒有 懷疑人生
?我們的思路沒有問題啊?為什麼 換個數值就行了呢? 我們稍微修改一下程式
執行結果:
哎?為啥 1 和 2 也可以了?
我們這時肯定猜想和 Integer
的裝箱 拆箱有關
裝箱,拆箱 概念
Integer的裝箱操作
為什麼 Integera=1
和 Integera=newInteger(1)
效果不一樣 那就瞅瞅原始碼吧?
通過註釋知道,java推薦 Integer.valueOf
方式初始化一個 Interger
因為有 快取了 -128-127
的數字 我們直接定義 Integera=1
具有這個功能,所以 Jvm 底層實現 是通過 Integer.valueOf
這個方法 再看 field.set(numb,tmp);
我們打斷點,發現通過反射設定 value
時 竟然走了 Integer.valueOf
方法 下面是 我們呼叫 swap
前後的 IntegerCache.cache
值得變化
反射修改前:


在反射修改前
通過反射修改後
再呼叫 field.set(numb,tmp)
tmp這時等於1 對應的 角標 129 ,但是這個值已經變成了2 所以出現了剛才 奇怪的結果
原來都是 快取的鍋
下面趁機再看個例子 加深理解
輸出結果:
java testA=testBtrue,testC=testDfalse
通過這小示例,在 -128 到 127的數字都走了快取,這樣 testA
和 testB
引用的是同一片記憶體區域的同一個物件。 而 testC
testD
數值大於127 所以 沒有走快取,相當於兩個 Integer
物件,在堆記憶體區域有兩個物件。 兩個物件自如不相等。
在前面的示例中 我們 通過
方式初始化 a
, b
我們的交換演算法沒有問題,也是這個原因。
那麼到目前為止我們的 swap
方法可以完善啦
只需將之前的 field.set(numb,tmp)
改為 field.set(numb,newInteger(tmp))
到此, 這個面試我們已經通過了,還有一個疑問我沒有解答。 為什麼 field.set(numb,tmp)
會執行 Integer.valueOf()
而 field.set(numb,newInteger(tmp))
不會執行。 這就是 Integer的裝箱
操作,當 給 Integer.value
賦值 int
時,JVM 檢測到 int不是Integer型別
,需要裝箱,才執行了 Integer.valueOf()
方法。而 field.set(numb,newInteger(tmp))
設定的 是Integer型別了,就不會再拆箱後再裝箱。
原文釋出時間為:2018-09-18
本文作者:王偉
本文來自雲棲社群合作伙伴“ ofollow,noindex">Java架構沉思錄 ”,瞭解相關資訊可以關注“ Java架構沉思錄 ”。