記得有次面試,面試官問我:
如何寫一個方法交換兩個 Integer 型別的值?
當時心裡一驚,這是把我當小白了呀!交換兩個數的值還不容易麼,最簡單的直接搞一箇中間變數,然後就可以交換了... ...
面試官隨即拿出一張雪白雪白的 A4 紙
工具用多了,有沒有體驗過白紙寫程式碼?來吧,開始你的表演,小夥子。
此時稍微有點心虛,但還是要裝腔作勢,把自己想象成大佬才行。
有的人可能會問,你不是說很簡單麼,還心虛個啥?寫過程式碼的都知道,工具寫程式碼是有自動補全提示的,這白板寫程式碼純粹就是考察你對程式碼的熟練度,其實相當考驗程式碼功底。
於是乎,提起筆,奮筆疾書,唰唰唰不到兩分鐘,我就寫完了。
程式碼如下:
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;}複製程式碼
當我胸有成竹的把紙遞過去的時候,我彷彿看見面試官嘴角哪不經意間的微笑。
這一笑不要緊,要緊的是一個大男人對著我笑幹嘛?
難道我的程式碼感動到他了?
明人不說暗話,這明顯不可能。
難道是他要對我... ...
想到此處,我不禁趕緊回憶了下來時的路,怎麼樣可以快速衝出去... ...
喂,醒醒,想啥呢
面試官瞄了一眼程式碼之後開始發問呢。
你確定你這段程式碼真的可以交換兩個 Integer 的值嗎?(竟然在 Integer 上加了重音)
我的天吶,難道有問題,多年面試經驗告訴我,面試重音提問要不就是在故意混淆,要不就是在善意提醒你,看能不能挖掘出點其他技術深度出來。
所以根據面試官的意思肯定是使用這段程式碼不能交換呢,哪麼不能交換的原因在哪裡?
首先,想了下,要交換兩個變數的值,利用中間變數這個思路是不會錯的。既然思路沒錯,哪就要往具體實現上想,問題出在哪裡。
第一個知識點:值傳遞和引用傳遞
我們都知道,Java 中有兩種引數傳遞
值傳遞
方法呼叫時,實際引數把它的值傳遞給對應的形式引數,方法執行中形式引數值的改變不影響實際引數的值。
引用傳遞
也稱為傳地址。方法呼叫時,實際引數的引用(地址,而不是引數的值)被傳遞給方法中相對應的形式引數,在方法執行中,對形式引數的操作實際上就是對實際引數的操作,方法執行中形式引數值的改變將會影響實際引數的值。
簡單總結一下就是:
也就是說 物件型別(地址空間)的變數存在於堆中,其引用存在於棧中。
至於為什麼這麼設計:主要還是為了考慮訪問效率和提升程式碼效能上考慮的。
難道問題出在這個地方?
可是 Integer 不就是 引用型別?
為什麼不能改變呢?
難道 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 實現快取細節
使用反射修改私有屬性的值
拆箱和裝箱
有沒有不總結不知道,一總結嚇一跳的感覺,這麼一道看似簡單的題,竟然考察到了這麼多東西
面試官:好了,技術問題我們今天就先面到這裡,接下來能否說一說你有什麼長處?
我:我是一個思想積極樂觀向上的人。
面試官:能否舉個例子。
我:什麼時候開始上班?
歡迎關注公眾號: