因為一個小小的Integer問題導致阿里一面沒過,遺憾!

跟著Mic學架構發表於2021-10-30

面試題:new Integer(112)和Integer.valueOf(112)的區別

面試官考察點猜想

這道題,考察的是對Integer這個物件原理的理解,關於這道題的變體有很多,我們會一一進行分析。

理解這道題,對於實際開發過程中防止出現意想不到的Bug很有用,建議大家認真思考和解讀。

背景知識詳解

關於Integer的實現

Integer是int的一個封裝類,它的構造實現如下。

    /**
     * The value of the {@code Integer}.
     *
     * @serial
     */
    private final int value;

    /**
     * Constructs a newly allocated {@code Integer} object that
     * represents the specified {@code int} value.
     *
     * @param   value   the value to be represented by the
     *                  {@code Integer} object.
     */
    public Integer(int value) {
        this.value = value;
    }

Integer中定義了一個int型別的value屬性。由於該屬性是final型別,因此需要通過構造方法來賦值。這個邏輯非常簡單,沒有太多要關注得。

結論: 當通過new關鍵字構建一個Integer例項時,和所有普通物件的例項化相同,都是在堆記憶體地址中分配一塊空間。

Integer.valueOf

Integer.valueOf方法,是把一個字串轉換為Integer型別,該方法定義如下

public static Integer valueOf(String s) throws NumberFormatException {
  return Integer.valueOf(parseInt(s, 10));
}

這個方法呼叫另外一個過載方法,該方法定義如下。

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

從這段程式碼中發現,如果i的值是在IntegerCache.lowIntegerCache.high這個區間範圍,則通過下面這段程式碼返回Integer物件例項。

IntegerCache.cache[i+(-IntegerCache.low)];

否則,使用new Integer(i)建立一個新的例項物件。

IntegerCache是什麼?

從它的命名來看,不難猜出它應該和快取有關係,簡單猜測就是:如果i的值在某個區間範圍內,則直接從快取中獲取物件。

IntegerCache的程式碼定義如下。

private static class 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; 
    //high的值允許通過系統屬性來調整
    String integerCacheHighPropValue =
      sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
    //如果配置了high的屬性值,則取兩者中最大的一個值作為IntegerCache的最高區間值。
    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;
  }

  private IntegerCache() {}
}

上述程式碼的實現邏輯非常簡單:

  1. IntegerCache的取值區間為: IntegerCache.low=-128, IntegerCache.hign=127,其中hign是可以通過系統引數來調整。
  2. 建立一個Integer陣列,迴圈初始化這個區間中的每一個值。

Integer為什麼這麼設計? 但凡涉及到Cache的,一定和效能有關,在Integer這個物件中,常用的數值區間是在-128到127之間,所以為了避免對這個區間範圍內的資料頻繁建立和銷燬物件,所以構建了一個快取。意味著後續只要不是通過new關鍵字建立的Integer例項,在這個區間內的數值都會從IntegerCache中獲取。

image-20211029101125958

問題解答

面試題:new Integer(112)和Integer.valueOf(112)的區別

理解了上面的原理後,再來解答這個問題就很容易了。

  • new Integer,是建立一個Integer物件例項。

  • Integer.valueOf(112),Integer預設提供了Cache機制,在-128到127區間範圍內的資料,通過valueOf方法不需要建立新的物件例項,只需要從快取中獲取即可。

問題總結

Integer這個物件的變形面試題比較多,其中一個面試題比較典型。

有兩個Integer變數a,b,通過swap方法之後,交換a,b的值,請寫出swap的方法。

public class SwapExample {
 
    public static void main(String[] args){
        Integer a=1;
        Integer b=2;
        System.out.println("交換前:a="+a+",b="+b);
        swap(a,b);
        System.out.println("交換後:a="+a+",b="+b);
    }
 
    private static void swap(Integer a,Integer b){
       //doSomething
    }
}

基礎不是很好的同學,可能會很直接的按照”正確的邏輯“來編寫程式,可能的程式碼如下。

private static void swap(Integer a,Integer b){
  Integer temp=a;
  a=b;
  b=temp;
}

程式邏輯,理論上是沒問題,定義一個臨時變數儲存a的值,然後再對ab進行交換。而實際執行結果如下

交換前:a=1,b=2
交換後:a=1,b=2

Integer物件的重新賦值思考

Integer作為封裝物件型別,通過函式傳遞該引用以後,理論上來說,main方法中定義的ab,以及傳遞到swap方法中的a、和b,指向同一個記憶體地址,那麼按照上述程式碼的實現,理論上來說也是成立的。

image-20211029103929388

Java中有兩種引數傳遞型別。

  • 值傳遞,傳遞的是資料的副本,方法執行中形式引數值的改變不影響實際引數的值。
  • 引用傳遞,傳遞的是記憶體地址的引用,在方法執行中,由於引用物件的地址指向同一塊記憶體,所以對於物件資料的修改,會影響到引用了該地址的變數。

這麼設計的好處,是為了減少記憶體的佔用,提升訪問效率和效能。

那麼Integer作為封裝型別,為什麼傳遞的是副本,而不是引用呢?

我們來看一下Integer中value值得定義,可以發現該屬性是final修飾,意味著是不可更改。

    /**
     * The value of the {@code Integer}.
     *
     * @serial
     */
    private final int value;

結論:在Java中,只有一種引數傳遞方式,就是值傳遞。但是,當引數傳的是基本型別時,傳的是值的拷貝,對拷貝變數的修改不影響原變數;當傳的是引用型別時,傳的是引用地址的拷貝,但是拷貝的地址和真實地址指向的都是同一個真實資料,因此可以修改原變數中的值;當傳的是Integer型別時,雖然拷貝的也是引用地址,指向的是同一個資料,但是Integer的值不能被修改,因此無法修改原變數中的值。

因此,上述程式碼之所以沒有交換成功,是因為傳遞到swap方法中的ab,會建立一個變數副本,這個副本中的值雖然發生了交換,但不影響原始值。

image-20211029114043119

瞭解了這塊知識之後,我們的問題就變成了,如何對一個修飾了final關鍵字的屬性進行資料修改。那就是通過反射來實現,實現程式碼如下.

public class SwapExample  {

    public static void main(String[] args){
        Integer a=1;
        Integer b=2;
        System.out.println("交換前:a="+a+",b="+b);
        swap(a,b);
        System.out.println("交換後:a="+a+",b="+b);
    }

    private static void swap(Integer a,Integer b){
        try {
            Field field=Integer.class.getDeclaredField("value");
            Integer temp= a;
            field.setAccessible(true); //針對private修飾的變數,需要通過該方法設定。
            field.set(a,b);
            field.set(b,temp);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

那這段程式碼執行完是否能達到預期呢? 上述程式執行結果如下:

交換前:a=1,b=2
交換後:a=2,b=2

從結果來看,確實是發生了變化,但是變化並不完整,因為b=1這個預期值並沒有出現。為什麼呢?其實還是和今天分享得主題有關係,我們來逐步看一下。

  1. Integer temp=a這個地方,基於IntegerCache的原理,這裡並不會產生一個新的temp例項,意味著temp變數和a變數指向的記憶體地址是同一個。
  2. 當通過field.set方法,把a記憶體地址的值通過反射修改成b以後,那麼此時a的值應該是2。注意:由於記憶體地址的值變成了2,而temp這個變數又指向該記憶體地址,因此temp的值自然就變成了2.
  3. 接著使用filed.set(b,temp)修改b屬性的值,此時temp的值時2,所以得到的結果b也變成了2.
private static void swap(Integer a,Integer b){
  try {
    Field field=Integer.class.getDeclaredField("value");
    Integer temp= a;  
    field.setAccessible(true); //針對private修飾的變數,需要通過該方法設定。
    field.set(a,b);
    field.set(b,temp);
  } catch (NoSuchFieldException e) {
    e.printStackTrace();
  } catch (IllegalAccessException e) {
    e.printStackTrace();
  }
}

理解了原理後,我們只需要修改Integer temp=a這段程式碼,改成下面這種寫法。保證temp變數是一個獨立的例項。

Integer temp=new Integer(a);

修改以後執行結果如下

交換前:a=1,b=2
交換後:a=2,b=1

Mic說: 只有基本功足夠紮實,才能對任何問題的本質一眼看透,解決這些問題的時候也能得心應手。

關注[跟著Mic學架構]公眾號,獲取更多精品原創

相關文章