一道面試題考驗了你對java的理解程度

簡介

最近有點忙,好久沒更新文章了,後面會慢慢恢復...回顧正題java

最近看到一篇文章,關於一道面試題,先看一下題目,以下:面試

public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.out.printf("a = %s, b = %s\n", a, b);
        swap(a, b);
        System.out.printf("a = %s, b = %s\n", a, b);
    }

public static void swap(Integer a, Integer b) {
    // TODO 實現
}複製代碼

有人可能在沒通過仔細考慮的狀況下,給出如下的答案緩存

// 特別提醒,這是錯誤的方式
// 特別提醒,這是錯誤的方式
// 特別提醒,這是錯誤的方式
public static void swap(Integer a, Integer b) {
    // TODO 實現
    Integer temp = a;
    a = b;
    b = temp;
}複製代碼

很遺憾,這是錯誤的。重要的事註釋三遍bash

那麼爲何錯誤,緣由是什麼?函數

想要搞清楚具體的緣由,在這裏你須要搞清楚如下幾個概念,若是這個概念搞清楚了,你也不會把上面的實現方法寫錯ui

  • 形參和實參
  • 參數值傳遞
  • 自動裝箱

因此,上面的問題先放一邊,先看一下這幾個概念this

形參和實參

什麼是形參?什麼是實參?概念上的東西,參考教科書或者google去吧,下面直接代碼說明更加明顯google

public void test() {
    int shi_can = 0;

    testA(shi_can);
}

public void testA(int xing_can) {

}複製代碼
注:爲了清楚的表達意思,我命名的時候並無按照java的駝峯規則命名,這裏只是爲了演示複製代碼

經過上面的代碼很清楚的表達形參和實參的概念,在調用testA時,傳遞的就是實參,而在testA方法簽名中的參數爲形參spa

從做用域上看,形參只會在方法內部生效,方法結束後,形參也會被釋放掉,因此形參是不會影響方法外的3d

值傳遞和引用傳遞

值傳遞:傳遞的是實際值,像基本數據類型
引用傳遞:將對象的引用做爲實參進行傳遞

java基本類型數據做爲參數是值傳遞,對象類型是引用傳遞

實參是能夠傳遞給形參的,可是形參卻不能影響實參,因此,當進行值傳遞的狀況下,改變的是形參的值,並無改變實參,因此不管是引用傳遞仍是值傳遞,只要更改的是形參自己,那麼都沒法影響到實參的。對於引用傳遞而言,不一樣的引用能夠指向相同的地址,經過形參的引用地址,找到了實際對象分配的空間,而後進行更改就會對實參指向的對象產生影響

額,上面表述,可能有點繞,看代碼

// 僅僅是一個java對象
public class IntType {

    private int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

// main方法
public class IntTypeSwap {
    public static void main(String[] args) {

        // CODE_1
        IntType type1 = new IntType();
        type1.setValue(1);

        IntType type2 = new IntType();
        type2.setValue(2);
      // CODE_1

        swap1(type1, type2);
        System.out.printf("type1.value = %s, type2.value = %s", type1.getValue(), type2.getValue());
        swap2(type1, type2);
        System.out.println();
        System.out.printf("type1.value = %s, type2.value = %s", type1.getValue(), type2.getValue());
    }

    public static void swap2(IntType type1, IntType type2) {
        int temp = type1.getValue();
        type1.setValue(type2.getValue());
        type2.setValue(temp);
    }

    public static void swap1(IntType type1, IntType type2) {
        IntType type = type1;
        type1 = type2;
        type2 = type;
    }
}複製代碼

在main方法中,CODE_1中間的代碼爲聲明瞭兩個對象,分別設置value爲1和2,而swap1和swap2兩個方法的目的是爲了交互這兩個對象的value值

先思考一下,應該輸出的結果是什麼
...
...

type1.value = 1, type2.value = 2
type1.value = 2, type2.value = 1複製代碼

從輸出結果來看swap1並無達到目的,回頭看一下swap1

public static void swap1(IntType type1, IntType type2) {
        IntType type = type1;
        type1 = type2;
        type2 = type;
    }複製代碼

從值傳遞的角度來看,對象參數傳遞採用的是引用傳遞,那麼type1和type2傳遞過來的是指向對象的引用,在方法內部,直接操做形參,交換了形參的內容,這樣形參改變,都是並無對實參產生任何影響,也沒有改變對象實際的值,因此,結果是沒法交換

而對於swap2,對象引用做爲形參傳遞過來後,並無對形參作任何的改變,而是直接操做了形參所指向的對象實際地址,那這樣,不管是實參仍是其餘地方,只要是指向該對象的全部的引用地址對應的值都會改變

自動裝箱

看我上面的那個例子的swap1,是否是頓時以爲與上面的面試題的錯誤作法很是類似了,是的,錯誤的緣由是如出一轍的,就是稍微有一點區別,就是Integer不是new出來的,而是自動裝箱的一個對象,那麼什麼是自動裝箱呢?jdk到底作了什麼事?

若是你不想知道爲何,只想知道結果,那麼我就直說,自動裝箱就是jdk調用了Integer的valueOf(int)的方法,很簡單,看源碼

public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }複製代碼

上面那些若是不想深究能夠忽略,就看最後一句,是否是明白了什麼呢。沒錯,也是new出來一個對象,若是想知道上面的代碼作了什麼處理,能夠參考 Long==Long有趣的現象 這篇文章,裏面有介紹相似的

好了,有人可能會問,爲何會知道自動裝箱調用的是valueOf方法,這裏其餘人怎麼知道的我不清楚,我是經過查看反編譯的字節碼指令知道的

public static void main(String[] args) {
        Integer a = 1;
        Integer b = 2;
        System.out.printf("a = %s, b = %s\n", a, b);
        swap(a, b);
        System.out.printf("a = %s, b = %s\n", a, b);
    }

    public static void swap(Integer a, Integer b) {
        Integer temp = a;
        a = b;
        b = temp;
    }複製代碼

反編譯出來的結果爲

對比一下能夠很清楚的看到valueOf(int)方法被調用

迴歸

好,如今迴歸正題了,直接操做形參沒法改變實際值,而Integer又沒有提供set方法,那是否是無解了呢?我很好奇若是有人如下這樣寫,面試官會有什麼反應

public static void swap(Integer a, Integer b) {
        // TODO 實現
        // 無解,
    }複製代碼

既然出了確定是有解的,能夠實現,回頭看看,在上面swap2的那個例子中是經過set方法來改變值的,那麼Integer有沒有提供呢?答案沒有(我沒找到)

那就先看看源碼

private final int value;
...
public Integer(int value) {
        this.value = value;
    }複製代碼

這是Integer的構造函數,能夠看到Integer對象實際值是用value屬性來存儲的,可是這個value是被final修飾的,沒辦法繼續找,value沒有提供任何的set方法。既然在萬法皆不通的狀況下,那就只能動用反射來解決問題

public static void swap(Integer a, Integer b) {
        int temp = a.intValue();
        try {
            Field value = Integer.class.getDeclaredField("value");
            value.setAccessible(true);
            value.set(a, b);
            value.set(b, temp);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }複製代碼

如今感受很開心,終於找到解決方案,但是當你執行的時候,從輸出結果你會發現,jdk在跟我開玩笑嗎

a = 1, b = 2
a = 2, b = 2複製代碼

爲何會出現這種狀況,無奈,調試會發現是在value.set的時候將Integer的緩存值改變了,由於value.set(Object v1, Object v2)兩個參數都是對象類型,因此temp會進行自動裝箱操做,會調用valueOf方法,這樣會獲取到錯誤的緩存值,因此,爲了不這種狀況,就只能不須要調用緩存值,直接new Integer就能夠跳過緩存,因此代碼改爲以下便可

public static void swap(Integer a, Integer b) {
        int temp = a.intValue();
        try {
            Field value = Integer.class.getDeclaredField("value");
            value.setAccessible(true);
            value.set(a, b);
            value.set(b, new Integer(temp));

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }複製代碼

至此,這道題完美結束

相關文章
相關標籤/搜索