交換2個整形數引發的思考
題目,在main方法中定義了兩個成員變數a=1,b=2. 現在需要通過swap方法把a和b的值做一個交換,交換以後輸出的結果是a=2,b=1.
思路1
大家看到這道題目的時候一定覺得很簡單,不用做任何思考就把程式碼啪啪啪寫完了
這種思維邏輯很對,大家從開始學程式設計就已經學到了中間變數的作用,好比是兩個瓶子,一瓶是可樂,一瓶是雪碧,要是想把兩個瓶子裡面的飲料交換一下,,那麼我們首先想到的就是藉助於中間變數(再找來一個空的瓶子)先把其中一瓶的飲料(雪碧或可樂)倒進空瓶,再把另一瓶的飲料(可樂或雪碧)倒進剛剛倒出飲料的瓶子,最後再把用來作為中間變數的瓶子裡的飲料給現在空著的瓶子,這樣就達到了交換兩瓶飲料的的目的。這種做法再符合邏輯不過了
分析
我們來把變數在jvm記憶體中的體現通過圖形的方式畫出來
這幅圖,我相信大部分人都能看懂。實際上i1,和i2傳遞過來的是一個引數的副本,而在swap方法裡面並沒有修改a,b這個地址的值,只是改變了引數副本的值,而這個值並沒有影響到a, b。那這裡就涉及到一個知識點
引數傳遞: 值傳遞和引用傳遞
這裡有一個大家都容易誤解的點:實際上在Java 應用程式有且僅有的一種引數傳遞機制,即按值傳遞
但是為什麼又會有值傳遞和引用傳遞的說法呢?
其實我們知道java應用程式中的變數可以分為兩種型別: 引用型別和基本型別。當把這兩種型別的引數傳遞給一個方法時,處理這兩種型別的方式是相同的。兩種型別都是按值傳遞;
而根據這兩種型別,如果傳遞的是基本型別時,函式接收的是原始值的一個副本。因此,如果函式中修改了該引數,僅改變副本,而原始值保持不變;
如果傳遞的是引用型別時,函式接收到的是原始值的記憶體地址,而不是值的副本。因此,如果在函式中修改了該引數,呼叫程式碼中的原始值也隨之改變
為什麼要這麼做呢? 我們都知道,物件型別是儲存在堆裡面的,一方面速度相對與基本型別比較慢,另一方面物件型別本身比較大,如果採用重新複製物件值的方法,浪費記憶體
結論
所以這個時候,swap等於什麼事都沒做吧
思路2
有了第一個思路的引導以後,其實我們得出的結論是,只需要在swap方法中通過修改a,b的記憶體地址的值就行了對吧。 那麼理所當然我們會想到反射
那麼我們通過分析Integer這個類,Integer這個類裡面有一個成員變數來儲存Integer型別的值
private final int value;
我們只需要通過反射拿到這個變數再去修改就可以了,所以我們程式碼就可以這麼寫了
分析
這段程式碼寫完以後,大家是不是認為大功告成了? 如果你這麼想,就太單純了。大家如果有心來分析這道題目的話,把這段程式碼執行一下看看結果。 是不是a =2 , b=2 ?
其實已經成功了50%對吧。 原因是什麼呢?
第一步,從第一行程式碼開始
我們一開始定義了兩個變數Integer a=1;Integer b=2; 這裡面的1和2是int型別,而a 和 b是Integer型別,那麼為什麼他們編譯的時候不報錯呢?
那就要說到 裝箱 這個概念了,如果我們規範的編寫第一行程式碼的話,應該是Integer a=new Integer(1) , 但是在jdk5以後,jvm在這塊做了優化,通過位元組碼來看下編譯指令後發現。a=1 編譯以後 是 a=Integer.valueOf(1);
那麼我們繼續一步步看,進入Integer.valueOf()方法,看看這個函式究竟做了什麼事情
我們看到第一行程式碼,如果int的值在IntegerCache.low到IntegerCache.high之間,那麼就直接從IntegerCache裡面獲取,如果是超出這個範圍才會新建一個Integer型別,而預設是在-128到127之間的數,一開始就被初始化好了,所以他們只有一個例項。那麼我們來驗證一下
因為Integer i1 = 1; 實際是Integer i1 = Integer.valueOf(1),在cache裡,我們找到了1對應的物件地址,然後就直接返回了;同理,i2也是cache裡找到後直接返回的。這樣,他們就有相同的地址,因而雙等號的地址比較就是相同的。i3和i4則不在cache裡,因此他們分別新建了兩個物件,所以地址不同
那麼,有了這個知識點以後,我們再繼續分析前面的內容
第二步,分析關鍵程式碼
首先,i1和i2分別指向a和b對應的記憶體地址,然後將i1的值傳遞給int型的tmp,那麼這個時候tmp的值為整數值1,
接著, 我們把i2的整數值2設定給i1,那麼我們來看f.set(i1,i2.intValue());這個方法
兩個引數都是物件型別,對於第二個引數,編譯器又給我們做了一次裝箱處理,最終轉化出來的程式碼就是
i1.value=Integer.valueOf(i2.intValue()).intValue();
i1值的變化過程
a、i2.intValue() -> 2
b、Integer.valueOf(2) -> 0x1265
c、0x1265.intValue() -> 2
d、i1.value -> 2
i2值的變化
這裡的tmp的值等於1 ,於是執行過程如下
Tmp=Integer.valueOf(tmp).intValue();
a、Integer.valueOf(1) -> 0x1234
b、0x1234.intValue() -> 2 //因為裝箱操作,所以在i1值的變化過程中修改的是同一塊記憶體地址,因此這裡的值變成了2
c、i2.value -> 2
因此最後的結果是,a、b 都變成了2
結論
這裡面涉及到兩個知識點
1. Integer的初始化快取
2. 反射
最終解決方案
不要讓Integer.valueOf裝箱發揮作用,避免走cache就行
總結
我們發現一道小小的面試題,能夠涉及到的知識點有這麼多
1、函式呼叫的值傳遞;
2、物件引用的值是記憶體地址;
3、反射的可訪問性;
4、java編譯器的自動裝箱;
5、Integer裝箱的物件快取。
所以,當我們工作到一段時間以後,技術水平不能再繼續停留在表面上,而是需要逐步往深入挖掘,每一個技術的出現,每一個bug的出現都不是隨機或者偶然的。而是有一定的原因。
原文:https://blog.csdn.net/k1280000/article/details/71159492