交換2個整形數引發的思考

FeelTouch發表於2018-11-13

題目,在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 

相關文章