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

程式猿雜貨鋪發表於2019-02-27

記得有次面試,面試官問我:

如何寫一個方法交換兩個 Integer 型別的值?

當時心裡一驚,這是把我當小白了呀!交換兩個數的值還不容易麼,最簡單的直接搞一箇中間變數,然後就可以交換了... ... 從一道面試題探究 Integer 的實現

面試官隨即拿出一張雪白雪白的 A4 紙

工具用多了,有沒有體驗過白紙寫程式碼?來吧,開始你的表演,小夥子。

此時稍微有點心虛,但還是要裝腔作勢,把自己想象成大佬才行。

有的人可能會問,你不是說很簡單麼,還心虛個啥?寫過程式碼的都知道,工具寫程式碼是有自動補全提示的,這白板寫程式碼純粹就是考察你對程式碼的熟練度,其實相當考驗程式碼功底。

於是乎,提起筆,奮筆疾書,唰唰唰不到兩分鐘,我就寫完了。

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

程式碼如下:

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 的值嗎?(竟然在 Integer 上加了重音)

我的天吶,難道有問題,多年面試經驗告訴我,面試重音提問要不就是在故意混淆,要不就是在善意提醒你,看能不能挖掘出點其他技術深度出來。

所以根據面試官的意思肯定是使用這段程式碼不能交換呢,哪麼不能交換的原因在哪裡? 從一道面試題探究 Integer 的實現

首先,想了下,要交換兩個變數的值,利用中間變數這個思路是不會錯的。既然思路沒錯,哪就要往具體實現上想,問題出在哪裡。

第一個知識點:值傳遞和引用傳遞

我們都知道,Java 中有兩種引數傳遞

  • 值傳遞

    方法呼叫時,實際引數把它的值傳遞給對應的形式引數,方法執行中形式引數值的改變不影響實際引數的值。

  • 引用傳遞

    也稱為傳地址。方法呼叫時,實際引數的引用(地址,而不是引數的值)被傳遞給方法中相對應的形式引數,在方法執行中,對形式引數的操作實際上就是對實際引數的操作,方法執行中形式引數值的改變將會影響實際引數的值。

簡單總結一下就是:

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

也就是說 物件型別(地址空間)的變數存在於堆中,其引用存在於棧中。

至於為什麼這麼設計:主要還是為了考慮訪問效率和提升程式碼效能上考慮的。

難道問題出在這個地方?

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

可是 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 的值

畫個圖簡單理解一下: 從一道面試題探究 Integer 的實現

哪如何去改變這個 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);}複製程式碼

這次長腦子呢,我又回過頭檢查了一遍程式碼,沒辦法,很蛋疼,這要是有電腦先跑一遍再說。

白紙只能靠你自己腦子想,腦子編譯,腦子執行(當然執行不好可能就燒壞了)

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

果然,查出問題來了(還好夠機智)

前邊不是說了這個 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值得時候是否需要檢查訪問許可權(很多時候,一直說要閱讀原始碼閱讀原始碼,因為原始碼就好比火眼金睛,在原始碼面前,很多妖魔鬼怪都是無所遁形的)

看著這段程式碼樂開了花,心裡想著這下應該總能交換了吧,我又非常自信的把程式碼遞給了面試官

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

面試官:為什麼要這麼改?為什麼要使用反射?為什麼要加這行 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

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

繼續深究下去你會發現面試官說的 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 .

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

既然分析出了為什麼會變成 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 的實現

這一次,他終於不再笑了,不再很迷的笑了

看來這場面試要迎來終結了 ... ...

面試官:嗯,你總算答對了,現在來總結一下這道題涉及到的知識點(這是要考察表達能力啊)

總結:
  • 值傳遞和引用傳遞

  • Integer 實現快取細節

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

  • 拆箱和裝箱

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

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

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

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

面試官:能否舉個例子。

我:什麼時候開始上班?

歡迎關注公眾號:

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


相關文章