記得有次面試,面試官問我:java
如何寫一個方法交換兩個 Integer 類型的值?程序員
當時內心一驚,這是把我當小白了呀!交換兩個數的值還不容易麼,最簡單的直接搞一箇中間變量,而後就能夠交換了... ... 面試
面試官隨即拿出一張雪白雪白的 A4 紙緩存
工具用多了,有沒有體驗過白紙寫代碼?來吧,開始你的表演,小夥子。bash
此時稍微有點心虛,但仍是要裝腔做勢,把本身想象成大佬才行。ide
有的人可能會問,你不是說很簡單麼,還心虛個啥?寫過代碼的都知道,工具寫代碼是有自動補全提示的,這白板寫代碼純粹就是考察你對代碼的熟練度,其實至關考驗代碼功底。工具
因而乎,提起筆,奮筆疾書,唰唰唰不到兩分鐘,我就寫完了。性能
代碼以下:優化
public static void main(String[] args) { Integer a = 1, b = 2; System.out.println("交換前:a = " + a + ", b = " + b); swap(a,b); System.out.println("交換後:a = " + a + ", b = " + b);}public static void swap(Integer i, Integer j) { int temp = i; i = j; j = temp;}複製代碼
當我成竹在胸的把紙遞過去的時候,我彷彿看見面試官嘴角哪不經意間的微笑。ui
這一笑沒關係,要緊的是一個大男人對着我笑幹嗎?
難道個人代碼感動到他了?
明人不說暗話,這明顯不可能。
難道是他要對我... ...
想到此處,我不由趕忙回憶了下來時的路,怎麼樣能夠快速衝出去... ...
喂,醒醒,想啥呢
面試官瞄了一眼代碼以後開始發問呢。
你肯定你這段代碼真的能夠交換兩個 Integer 的值嗎?(居然在 Integer 上加了重音)
個人天吶,難道有問題,多年面試經驗告訴我,面試重音提問要不就是在故意混淆,要不就是在善意提醒你,看能不能挖掘出點其餘技術深度出來。
因此根據面試官的意思確定是使用這段代碼不能交換呢,哪麼不能交換的緣由在哪裏?
首先,想了下,要交換兩個變量的值,利用中間變量這個思路是不會錯的。既然思路沒錯,哪就要往具體實現上想,問題出在哪裏。
咱們都知道,Java 中有兩種參數傳遞
值傳遞
方法調用時,實際參數把它的值傳遞給對應的形式參數,方法執行中形式參數值的改變不影響實際參數的值。
引用傳遞
也稱爲傳地址。方法調用時,實際參數的引用(地址,而不是參數的值)被傳遞給方法中相對應的形式參數,在方法執行中,對形式參數的操做實際上就是對實際參數的操做,方法執行中形式參數值的改變將會影響實際參數的值。
簡單總結一下就是:
也就是說 對象類型(地址空間)的變量存在於堆中,其引用存在於棧中。
至於爲何這麼設計:主要仍是爲了考慮訪問效率和提高代碼性能上考慮的。
難道問題出在這個地方?
但是 Integer 不就是 引用類型?
爲何不能改變呢?
難道 Integer 的實現有什麼特殊之處?
你別說,還真是 Integer 有他本身的獨特之處。
/** * The value of the {@code Integer}. * * @serial */private final int value;複製代碼
這個屬性也是表示這個 Integer 實際的值,可是他是 private final 的,Integer 的 API 也沒有提供給外部任何能夠修改它的值接口,也就是說這個值改變不了。
簡單理解就是上面的 swap 方法其實真實交換的是 兩個形參 i 和 j 的值,而沒有去改變 a 和 b 的值
畫個圖簡單理解一下:
哪如何去改變這個 value 值呢 ?
趕忙給面試官陪着笑臉說剛纔激動了,代碼我能不能再改改?
面試官:能夠,代碼原本就是一個不斷優化的過程,你改吧!
而後又是一頓奮筆疾書,再次唰唰唰寫了以下代碼:
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { /*int temp = i; i = j; j = temp;*/ Field value = Integer.class.getDeclaredField("value"); int temp = i.intValue(); value.set(i,j.intValue()); value.set(j,temp);}複製代碼
此次長腦子呢,我又回過頭檢查了一遍代碼,沒辦法,很蛋疼,這要是有電腦先跑一遍再說。
白紙只能靠你本身腦子想,腦子編譯,腦子運行(固然運行很差可能就燒壞了)
果真,查出問題來了(還好夠機智)
前邊不是說了這個 value 是私有屬性麼,既然是 private 的 ,final 的,在 Java 中是不容許的,再訪問的時候會報
java.lang.IllegalAccessException
異常,
在反射的時候還須要加value.setAccessible(true)
,設置代碼執行時繞過對私有屬性的檢查,哪麼代碼就變成了以下:
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { /*int temp = i; i = j; j = temp;*/ Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = i.intValue(); value.set(i,j.intValue()); value.set(j,temp);}複製代碼
另外多提幾句:設置了 setAccessible(true)
就能訪問到私有屬性是由於他的源碼是這樣的
public void setAccessible(boolean flag) throws SecurityException { SecurityManager sm = System.getSecurityManager(); if (sm != null) sm.checkPermission(ACCESS_PERMISSION); setAccessible0(this, flag);}複製代碼
能夠看到,他調用了 setAccessible0()
這個方法,繼續看下這個:
private static void setAccessible0(AccessibleObject obj, boolean flag) throws SecurityException { if (obj instanceof Constructor && flag == true) { Constructor<?> c = (Constructor<?>)obj; if (c.getDeclaringClass() == Class.class) { throw new SecurityException("Cannot make a java.lang.Class" + " constructor accessible"); } } obj.override = flag; }複製代碼
這段代碼咱們須要關注有兩點:
參數是 boolean flag
而這個 flag 實際的值剛好是咱們設置進去的setAccessible()
中的參數
這個參數真正的做用是把一個 AccessibleObject
對象的 override
屬性進行了賦值
哪麼這個 override
屬性的做用又是什麼呢?
咱們一塊兒來看下value.set()
這個方法的源碼
@CallerSensitive public void set(Object obj, Object value) throws IllegalArgumentException, IllegalAccessException { if (!override) { if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) { Class<?> caller = Reflection.getCallerClass(); checkAccess(caller, clazz, obj, modifiers); } } getFieldAccessor(obj).set(obj, value); }複製代碼
看着這段代碼是不瞬間就明白了,原來這個 overried
屬性就比如一個開關,負責控制在 set
值得時候是否須要檢查訪問權限(不少時候,一直說要閱讀源碼閱讀源碼,由於源碼就比如火眼金睛,在源碼面前,不少妖魔鬼怪都是無所遁形的)
看着這段代碼樂開了花,內心想着這下應該總能交換了吧,我又很是自信的把代碼遞給了面試官
面試官:爲何要這麼改?爲何要使用反射?爲何要加這行 setAccessible(true) ?
哇 此時正和我意啊,寫了這半天,就等你問我這幾個點,因而我很利索的把上邊描述的給面試官講了一遍,他聽完以後繼續微微一笑,這個笑很迷,也很滲人。
難道這還不對?
他又開始發問:
面試官:這段代碼仍是會有問題,最終輸出結果會是 a = 2, b = 2。能夠提示你一下,你知道拆箱裝箱嗎?
呃,這還涉及到拆箱裝箱了... ...
咱們在上面的代碼中
Integer a = 1, b = 2;複製代碼
a 和 b 是 Integer 類型,可是 1 和 2 是 int 類型,爲何把 int 賦值給 Integer 不報錯?
由於 Java 中有自動裝箱(若是感興趣的話可使用 javap 命令去查看一下這行代碼執行的字節碼)
實際上 Integer a = 1 就至關於執行了 Integer a = Integer.valueOf(1);
複製代碼
哪麼,valueOf()
方法的實現又是什麼樣的呢?
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }複製代碼
這個方法的代碼說明,若是你的值是在某個範圍以內,會從 IntegerCache
這個緩存中獲取值,而不是去 new 一個新的 Integer
對象。繼續研究 IntegerCache
這個類
static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; }複製代碼
根據以上代碼能夠獲得:在範圍在 -128 - 127 之間的數字,會被直接初始化好以後直接加入獲得緩存中,以後處於這個範圍中的全部Integer 會直接從緩存獲取值,這樣提升了訪問效率
爲了驗證這一點,你能夠直接試一試,寫一段代碼:
Integer a = 100,b = 100;System.out.println(a == b);複製代碼
根據咱們在 Java 領域的理解,對於引用類型,使用 == 比較的是他們在內存中的地址,哪麼,對於Integer這個引用類型,直接使用 == 結果應該是false
。
但是,你若是實際調試試一試的話,會發現這是 true
, 是否是有點難以想象?
哪麼爲何是ture
,就回到了上邊說的緩存問題,由於 100 處於 -128-127 這份範圍
若是你定義的變量是
Integer a = 200,b = 200;System.out.println(a == b);複製代碼
這個結果輸出確定是 false
,由於根據前邊Integer。valueOf()
實現的源碼能夠獲得:超過-128-127的值須要從新 new Integer(i)
,但凡是 new
出來的,使用 ==
比確定是 false
繼續深究下去你會發現面試官說的 a = 2, b = 2 是對的,具體緣由是:
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = i.intValue(); // 此處 咱們使用 j.intValue 返回結果是個 int 類型數據 // 而 value.set()方法須要的是一個 Object 對象 此處就涉及到了裝箱 // 因此 i 值的實際變化過程爲:i = Integer.valueOf(j.intValue()).intValue() value.set(i,j.intValue()); // 同理 j 值得實際變化過程爲:j = Integer.valueOf(temp).intValue() // 由於 valueOf() 要從緩存獲取值 也就是此時須要根據 temp 的下標來獲取值 // 但是在上一步中 i 的值已經被自動裝箱以後變成了 2 // 因此此處會把 j 的值設置成 2 value.set(j,temp); }複製代碼
綜上:咱們就搞清楚了爲何面試官會說結果是 a = 2, b = 2 .
既然分析出了爲何會變成 a = 2 b = 2,哪就好辦呢。
發現問題,解決問題,永遠是程序員最優秀的品質,對了,還要臉皮厚(小聲嗶嗶)
我又厚着臉把代碼要過來了(面試官仍是一如既往的微笑,一如既往很迷的笑)
把 set 改成 setInt 避免裝箱操做
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = i.intValue(); value.setInt(i,j.intValue()); value.setInt(j,temp); }}複製代碼
把 temp 從新建立一個對象進行賦值,這樣就不會和 i 的值產生相互影響
public static void swap(Integer i, Integer j) throws NoSuchFieldException, IllegalAccessException { Field value = Integer.class.getDeclaredField("value"); value.setAccessible(true); int temp = new Integer(i.intValue()); value.setInt(i,j.intValue()); value.setInt(j,temp); }複製代碼
靠着臉厚,我第三次把代碼交給了面試官,沒辦法,厚度不是你所能想象的... ...
這一次,他終於再也不笑了,再也不很迷的笑了
看來這場面試要迎來終結了 ... ...
面試官:嗯,你總算答對了,如今來總結一下這道題涉及到的知識點(這是要考察表達能力啊)
值傳遞和引用傳遞
Integer 實現緩存細節
使用反射修改私有屬性的值
拆箱和裝箱
有沒有不總結不知道,一總結嚇一跳的感受,這麼一道看似簡單的題,居然考察到了這麼多東西
面試官:好了,技術問題咱們今天就先面到這裏,接下來可否說一說你有什麼長處?
我:我是一個思想積極樂觀向上的人。
面試官:可否舉個例子。
我:何時開始上班?
歡迎關注公衆號: