1. 程式人生 > >自動裝箱和拆箱IntegerCache(二)

自動裝箱和拆箱IntegerCache(二)

文章來源:https://www.jb51.net/article/129640.htm
本文主要給大家介紹了關於Java中IntegerCache的相關內容
交換
首先來看一個示例。
請用Java完成swap函式,交換兩個整數型別的值。

  public static void test() throws Exception {
     Integer a = 1, b = 2;
     swap(a, b);
     System.out.println("a=" + a + ", b=" + b);
   }

    static void swap(Integer a, Integer b){
     // 需要實現的部分
}

第一次
  如果你不瞭解Java物件在記憶體中的分配方式,以及方法傳遞引數的形式,你有可能會寫出以下程式碼。

public static void swapOne(Integer a, Integer b) throws Exception {
     Integer aTempValue = a;
     a = b;
     b = aTempValue;
}

執行的結果顯示a和b兩個值並沒有交換
那麼讓我們來看一下上述程式執行時,Java物件在記憶體中的分配方式:image_1clm984ouhlmq70otjb1h1h799.png-160.8kB

物件地址分配

由此可以看到,在兩個方法的區域性變量表中分別持有的是對a、b兩個物件實際資料地址的引用。

上面實現的swap函式,僅僅交換了swap函式裡區域性變數a和區域性變數b的引用,並沒有交換JVM堆中的實際資料。
所以main函式中的a、b引用的資料沒有發生交換,所以main函式中區域性變數的a、b並不會發生變化。
那麼要交換main函式中的資料要如何操作呢?

第二次
根據上面的實踐,可以考慮交換a和b在JVM堆上的資料值?

簡單瞭解一下Integer這個物件,它裡面只有一個物件級int型別的value用以表示該物件的值。
所以我們使用反射來修改該值,程式碼如下:

public static void swapTwo(Integer a1, Integer b1) throws
Exception { Field valueField = Integer.class.getDeclaredField("value"); valueField.setAccessible(true); int tempAValue = valueField.getInt(a1); valueField.setInt(a1, b1.intValue()); valueField.setInt(b1, tempAValue); }

執行結果,符合預期。
驚喜
上面的程式執行成後,如果我在宣告一個Integer c = 1, d = 2;會有什麼結果

示例程式如下:

public static void swapTwo(Integer a1, Integer b1) throws Exception {
 Field valueField = Integer.class.getDeclaredField("value");
 valueField.setAccessible(true);
 int tempAValue = valueField.getInt(a1);
 valueField.setInt(a1, b1.intValue());
 valueField.setInt(b1, tempAValue);
}

public static void testThree() throws Exception {
 Integer a = 1, b = 2;
 swapTwo(a, b);
 System.out.println("a=" + a + "; b=" + b);
 Integer c = 1, d = 2;
 System.out.println("c=" + c + "; d=" + d);
}

輸出的結果如下:
a=2; b=1
c=2; d=1

驚喜不驚喜!意外不意外!刺激不刺激!

深入
究竟發生了什麼?讓我們來看一下反編譯後的程式碼:
作者使用IDE工具,直接反編譯了這個.class檔案

public static void testThree() throws Exception {
 Integer a = Integer.valueOf(1);
 Integer b = Integer.valueOf(2);
 swapTwo(a, b);
 System.out.println("a=" + a + "; b=" + b);
 Integer c = Integer.valueOf(1);
 Integer d = Integer.valueOf(2);
 System.out.println("c=" + c + "; d=" + d);
}

在Java對原始型別int自動裝箱到Integer型別的過程中使用了Integer.valueOf(int)這個方法了。

肯定是這個方法在內部封裝了一些操作,使得我們修改了Integer.value後,產生了全域性影響。
所有這涉及該部分的程式碼一次性粘完(PS:不拖拉的作者是個好碼農):

/**
     * Cache to support the object identity semantics of autoboxing for values between
     * -128 and 127 (inclusive) as required by JLS.
     *
     * The cache is initialized on first usage.  The size of the cache
     * may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
     * During VM initialization, java.lang.Integer.IntegerCache.high property
     * may be set and saved in the private system properties in the
     * sun.misc.VM class.
     */
 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;
            //查詢JVM,Integer初始化的快取設定
            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;
        }

        private IntegerCache() {}
    }

如上所示Integer內部有一個私有靜態類IntegerCache,該類靜態初始化了一個包含了Integer.IntegerCache.lowjava.lang.Integer.IntegerCache.high的Integer陣列。

其中java.lang.Integer.IntegerCache.high的取值範圍在[127~Integer.MAX_VALUE - (-low) -1]之間。
在該區間內所有的Integer.valueOf(int)函式返回的物件,是根據int值計算的偏移量,從陣列Integer.IntegerCache.cache中獲取,物件是同一個,不會新建物件。
所以當我們修改了Integer.valueOf(1)的value後,所有Integer.IntegerCache.cache[ 1 - IntegerCache.low ]的返回值都會變更。
我相信你們的智商應該理解了,如果不理解請在評論區call 10086。

好了,那麼不在[IntegerCache.low~IntegerCache.high)的部分呢?
很顯然,它們是幸運的,沒有被IntegerCache快取到,法外之民,每次它們的到來,都會new一邊,在JVM上分配一塊土(內)地(存)。
遐想
如果我把轉換的引數換成型別換成int呢?

public static void testOne() throws Exception {
 int a = 1, b = 2;
 swapOne(a, b);
 System.out.println("a=" + a + ", b=" + b);
}

static void swapOne(int a, int b){
 // 需要實現的部分
}

以作者目前的功力,無解。高手可以公眾號留言,萬分感謝!
至此swap部分已經講完了。
1 + 1
首先讓我們來看一下程式碼:

public static void testOne() {
 int one = 1;
 int two = one + one;
 System.out.printf("Two=%d", two);
}

請問輸出是什麼?

如果你肯定的說是2,那麼你上面是白學了,請直接撥打95169。
我可以肯定的告訴你,它可以是[Integer.MIN_VALUE~Integer.MAX_VALUE]區間的任意一個值。

驚喜不驚喜!意外不意外!刺激不刺激!
讓我們再擼(捋)一(一)串(遍)燒(代)烤(碼)。

作者使用IDE工具,直接反編譯了這個.class檔案

public static void testOne() {
 int one = 1;
 int two = one + one;
 System.out.printf("Two=%d", two);
}

這裡的變數two竟然沒有呼叫Integer.valueOf(int) ,跟想象的不太一樣,我懷疑這是IDE的鍋。
所以果斷檢視編譯後的位元組碼。以下為摘錄的部分位元組碼:

LDC "Two=%d"
ICONST_1
ANEWARRAY java/lang/Object
DUP
ICONST_0
ILOAD 2
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
AASTORE
INVOKEVIRTUAL java/io/PrintStream.printf (Ljava/lang/String;[Ljava/lang/Object;)Ljava/io/PrintStream;
POP

可以看出確實是IDE的鍋,這裡不僅呼叫了一次Integer.valueOf(int) ,而且還建立一個Object的陣列。
完整的Java程式碼應該是如下所示:


public static void testOne() {
 int one = 1;
 int two = one + one;
 Object[] params = { Integer.valueOf(two) };
 System.out.printf("Two=%d", params);
}

所以只要在方法呼叫前修改Integer.IntegerCache.cache[2+128]的值就可以了,所以在類的靜態初始化部分加些程式碼。

public class OnePlusOne {
 static {
 try {
 Class<?> cacheClazz = Class.forName("java.lang.Integer$IntegerCache");
 Field cacheField = cacheClazz.getDeclaredField("cache");
 cacheField.setAccessible(true);
 Integer[] cache = (Integer[]) cacheField.get(null);
 //這裡修改為 1 + 1 = 3
 cache[2 + 128] = new Integer(3);
 } catch (Exception e) {
 e.printStackTrace();
 }
 }

 public static void testOne() {
 int one = 1;
 int two = one + one;
 System.out.printf("Two=%d", two);
 }
}

two == 2 ?
在修改完Integer.IntegerCache.cache[2 + 128]的值後,變數two還等於2麼?


public static void testTwo() {
 int one = 1;
 int two = one + one;
 System.out.println(two == 2);
 System.out.println(Integer.valueOf(two) == 2);
}

上述程式碼輸出如下

true
false

因為two == 2不涉及到Integer裝箱的轉換,還是原始型別的比較,所以原始型別的2永遠等於2。
Integer.valueOf(two)==2的真實形式是Integer.valueOf(two).intValue == 2 ,即3==2,所以是false。

這裡可以看到如果拿一個值為null的Integer變數和一個int變數用雙等號比較,會丟擲NullPointException

這裡的方法如果換成System.out.println(“Two=” + two)的形式會有怎樣的輸出?你可以嘗試一下。

後記

java.lang.Integer.IntegerCache.high看了IntegerCache類獲取high的方法sun.misc.VM.getSavedProperty,可能大家會有以下疑問,我們不拖沓,採用一個問題一解答的方式。
1. 這個值如何如何傳遞到JVM中?
和系統屬性一樣在JVM啟動時,通過設定-Djava.lang.Integer.IntegerCache.high=xxx傳遞進來。
2. 這個方法和System.getProperty有什麼區別?

為了將JVM系統所需要的引數和使用者使用的引數區別開,
java.lang.System.initializeSystemClass在啟動時,會將啟動引數儲存在兩個地方:

2.1 sun.misc.VM.savedProps中儲存全部JVM接收的系統引數。

JVM會在啟動時,呼叫java.lang.System.initializeSystemClass方法,初始化該屬性。
同時也會呼叫sun.misc.VM.saveAndRemoveProperties方法,從java.lang.System.props中刪除以下屬性:

  • sun.nio.MaxDirectMemorySize

  • sun.nio.PageAlignDirectMemory

  • sun.lang.ClassLoader.allowArraySyntax

  • java.lang.Integer.IntegerCache.high

  • sun.zip.disableMemoryMapping

  • sun.java.launcher.diag

以上羅列的屬性都是JVM啟動需要設定的系統引數,所以為了安全考慮和隔離角度考慮,將其從使用者可訪問的System.props分開。
2.2 java.lang.System.props中儲存除了以下JVM啟動需要的引數外的其他引數。

  • sun.nio.MaxDirectMemorySize

  • sun.nio.PageAlignDirectMemory

  • sun.lang.ClassLoader.allowArraySyntax

  • java.lang.Integer.IntegerCache.high

  • sun.zip.disableMemoryMapping

  • sun.java.launcher.diag

PS:作者使用的JDK 1.8.0_91
Java 9的IntegerCache
幻想一下,如果以上淘氣的玩法出現在第三方的依賴包中,絕對有一批程式設計師會瘋掉(請不要嘗試這麼惡劣的玩法,後果很嚴重)。

慶幸的是Java 9對此進行了限制。可以在相應的module中編寫module-info.java檔案,限制了使用反射來訪問成員等,按照需要聲明後,程式碼只能訪問欄位、方法和其他用反射能訪問的資訊,只有當類在相同的模組中,或者模組打開了包用於反射方式訪問。詳細內容可參考一下文章:
在 Java 9 裡對 IntegerCache 進行修改?
感謝Lydia和飛鳥的寶貴建議和辛苦校對。
最後跟大家分享一個java中Integer值比較不注意的問題:

先來看一個程式碼片段:

public static void main(String[] args) {
Integer a1 = Integer.valueOf(60); //danielinbiti
Integer b1 = 60;
System.out.println("1:="+(a1 == b1));


Integer a2 = 60;
Integer b2 = 60;
System.out.println("2:="+(a2 == b2));


Integer a3 = new Integer(60);
Integer b3 = 60;
System.out.println("3:="+(a3 == b3));

Integer a4 = 129;
Integer b4 = 129;
System.out.println("4:="+(a4 == b4));
}

這段程式碼的比較結果,如果沒有執行不知道各位心中的答案都是什麼。

要知道這個答案,就涉及到Java緩衝區和堆的問題。

java中Integer型別對於-128-127之間的數是緩衝區取的,所以用等號比較是一致的。但對於不在這區間的數字是在堆中new出來的。所以地址空間不一樣,也就不相等。

Integer b3=60 ,這是一個裝箱過程也就是Integer b3=Integer.valueOf(60)

所以,以後碰到Integer比較值是否相等需要用intValue()

對於Double沒有緩衝區。

答案

1:=true
2:=true
3:=false
4:=false