從一道面試題探究 Integer 的實現

記得有次面試,面試官問我: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 有他本身的獨特之處。

第二個知識點: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 實現緩存細節

  • 使用反射修改私有屬性的值

  • 拆箱和裝箱

有沒有不總結不知道,一總結嚇一跳的感受,這麼一道看似簡單的題,居然考察到了這麼多東西

面試官:好了,技術問題咱們今天就先面到這裏,接下來可否說一說你有什麼長處?

我:我是一個思想積極樂觀向上的人。

面試官:可否舉個例子。

我:何時開始上班?

歡迎關注公衆號:

相關文章
相關標籤/搜索