全方位深入理解Java包裝類

本文轉載自個人掘金博客:https://juejin.im/post/5ec52a3b51882542f010a898

前言

這篇文章主要從使用角度,源碼角度以及JVM內存位置等角度深入解析Java的基本數值包裝類。

1. 包裝類

1.1 包裝類的定義:

Java中每一種基本類型都會對應一個唯一的包裝類(位於java.lang.*package中),基本類型與其包裝類都可以通過包裝類中的靜態或者成員方法進行轉換。每種基本類型及其包裝類的對應關係如下表所示。

    | 基本數據類型 | 包裝類    |
    | ------------ | --------- |
    | byte         | Byte      |
    | short        | Short     |
    | int          | Integer   |
    | long         | Long      |
    | float        | Float     |
    | double       | Double    |
    | char         | Character |
    | boolean      | Boolean   |
  1. 需要明確的是這些包裝類都是一個java類,所以這些類的對象實例都是在堆上分配
  2. 所有的包裝類都是final修飾的,也就是它們都是無法被繼承和重寫的。
  3. 包裝類實例化Integer i = new Integer(1);有連個兩個部分,首先在堆上實例化new Integer(1)這個對象實例,然後變量i作爲堆上該對象實例的地址或者引用被保存。

1.2 與基礎數值類型的區別

  1. 作爲類變量或是類成員變量的默認值不同,如Integer默認值爲null,int的默認值爲0。
  2. 在JVM的內存位置不同,包裝類的對象實例在堆上分配,基本數值類型則有如下3種情況:
    • 方法中時,存放在虛擬機的棧幀中的局部變量表中;
    • 作爲類的成員變量時,存放在中;
    • 作爲類的靜態變量/常量時,存放在方法區中。
  3. 包裝類型可用於泛型,而基本類型不可以。因爲泛型在編譯時會進行類型擦除,最後只保留原始類型,而原始類型只能是 Object 類及其子類——基本類型是個特例。
  4. 包裝類必須實例化之後才能使用(java 1.5會自動的進行裝箱),基礎數據類型則不用。
  5. 包裝類型的變量作爲一個指針(引用),指針佔用的內存在64位虛擬機上8個字節,如果默認開啓指針壓縮是4個字節-XX:CompressedOops 。實例化對象的內存則需要根據包裝類型指針壓縮情況以及padding的情況來計算。基本數值類型的變量所佔內存就是其內存大小。

2. 包裝類的使用

包裝類的使用我理解大致分爲兩個方面:

2.1 包裝類提供的靜態函數

這裏以Integer包裝類爲例,大致展示下其提供的各種非常有用的API:

1. String toString(int i, int radix),將一個十進制整數轉化爲radix進制的字符串
2. String toHexString(int i),將一個十進制整數轉化爲16進制的字符串
3. String toOctalString(int i),將一個十進制整數轉化爲8進制的字符串
4. String toBinaryString(int i),將一個十進制整數轉化爲2進制的字符串
5. Integer valueOf(String s, int radix)/int parseInt(String s, int radix),讀取一個radix進制字符串
6. Integer valueOf(String s)/int parseInt(String s),讀取一個radix進制字符串
7. int compareTo(Integer anotherInteger),this與另一個Integer進行比較
8. int compare(int x, int y),兩個int進行比較
9. int lowestOneBit(int i),返回i最低位的1的位數

這裏僅展示了部分博主覺得常用的API,還有一些其他有用API值得讀者自己去查看源碼。

2.2 包裝類的實例化使用

我們將根據下面一段demo代碼的運行結果來介紹包裝類使用過程中的注意事項:

    @Test
    public void testCase01() {
        Integer intOne1 = 1;
        Integer intOne2 = 1;
        Integer intOne3 = new Integer(1);

        Integer two = 2;
        Integer three = 3;

        Integer intNum1 = 321;
        Integer intNum2 = 321;
        int intNum3 = 321;

        Long longTwo = 2l;
        long ltwo = 2l;

        // case1: == 比較包裝類實例化對象地址,IntegerCache緩存Integer.valueOf()入口的 -128 ~ 127
        System.out.println(intOne1 == intOne2);  // true

        // case2:
        System.out.println(intOne1 == intOne3);  // false

        // case3: 超出Integer常量池緩存範圍,地址不相同
        System.out.println(intNum1 == intNum2);  // false

        // case4:
        System.out.println(intOne1.equals(intOne3));  // true

        // case5: a.equals(b),1. 保證b是和a一樣的包裝類 2.比較數值
        System.out.println(intOne1.equals(intOne2));  // true

        // case6: == 一邊有表達式或者基本類型時,會先進行拆箱,然後比較具體數值
        System.out.println(three == (intOne1 + two));  // true

        // case7:
        System.out.println(intNum1 == intNum3);  // true

        // case8:
        System.out.println(intNum1.equals(intNum2));  // true

        // case9: 自動裝箱
        System.out.println(intNum1.equals(intNum3));  // true

        // case10: 類型不一致
        System.out.println(two.equals(longTwo));  // false

        // case11: 自動裝箱,類型不一致
        System.out.println(two.equals(ltwo));  // false

        // case12: 自動拆箱,然後比較具體數值
        System.out.println(longTwo == intOne1 + intOne1);  // true

        // case13: 自動裝箱,類型不一致
        System.out.println(longTwo.equals(intOne1 + intOne1));  // false

        // case14: 自動裝箱,類型一致
        System.out.println(two.equals(intOne1 + intOne1));  // true

        // case15: 容量小的類型可自動(隱式)轉換爲容量大的數據類型,byte,short,char → int → long → float → double
        System.out.println(2 == 2L);  // true
        System.out.println(2 == 2d);  // true
        System.out.println((short)2 == 2f);  // true,(int) 2 先強轉成(short)2,然後在自動轉成float
    }

2.2.1 自動裝箱和自動拆箱

把基本類型轉換成包裝類型的過程叫做裝箱(boxing)。反之,把包裝類型轉換成基本類型的過程叫做拆箱(unboxing)。Java SE5 以後提供了自動裝箱與自動拆箱的功能。

Integer integerVal  = 10;  // 自動裝箱
int intVal = integerVal;     // 自動拆箱

上面這段代碼使用 jd-gui反編譯後的結果如下所示:

Integer integerVal = Integer.valueOf(10);
int intVal = integerVal.intValue();

對於Integer類型的包裝類:

  1. 自動裝箱是通過 Integer.valueOf() 完成的。
  2. 自動拆箱是通過 Integer.intValue() 完成的。

2.2.2 "=="運算符的使用

  1. “==”比較的是包裝類實例化變量的地址。

  2. Integer包裝類內置IntegerCache

    • 默認會緩存 -128 ~ 127之間的Integer包裝類的實例對象。

    • 通過Integer.valueOf()的入口才會訪問到這些緩存的對象實例。

    • 自動裝箱會訪問到,自己手動new Integer(1)不會訪問到。

  3. 如果"=="運算符的一邊有表達式或者基本數據類型,會先進行拆箱,然後比較具體數值。

2.2.3 equals方法的使用

Integer.equals方法的源碼:

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }
  1. 包裝類的equals方法首先會進行傳入變量類型判斷。
  2. 在類型相同的情況下才會進行具體數值的比較。
  3. 如果equals的入參是一個基本類型,會先進行裝箱。

2.2.4 基本數據類型轉換

整型,字符型,浮點型的數據在混合運算中相互轉換,轉換時遵循以下原則:

  1. 容量小的類型可自動轉換(隱式轉換)爲容量大的數據類型;byte,short,char → int → long → float → double。byte,short,char之間不會相互轉換,他們在計算時首先會轉換爲int類型。boolean 類型是不可以轉換爲其他基本數據類型。
  2. 容量大的類型轉容量小的數據類型需要強制轉化(顯示轉化)。

3. 包裝類的Cache

通過java.lang.*package的源碼閱讀可以發現:

  1. Boolean,Character ,Byte,Short,Integer,Long等基本類型的包裝類具有緩存,Float,Double等基本類型的包裝類則沒有緩存。
  2. 通過**包裝類Class.valueOf()**的入口(即自動裝箱的方法)纔會獲取到這些緩存的對象實例,其他如 new Integer(1)的方式則是重新在堆上實例化一個Integer的對象實例。
  3. 不同包裝類的緩存範圍不完全相同,其中只有Integer的緩存上限可以配置。

3.1 Boolean類型的緩存

Boolean類型的緩存就直接是2個靜態常量,緩存的範圍就是:true, false

boolean: 1位,範圍 true or false

    public static final Boolean TRUE = new Boolean(true);

    public static final Boolean FALSE = new Boolean(false);
    
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }

3.2 Character類型的緩存

Character類型的緩存是通過靜態內部類CharacterCache實現,緩存的範圍是: [0, 127],即標準ASCII碼錶的範圍。 char:16位,2個字節,範圍[0, 2^16 - 1],[\u0000, \uffff]。

    private static class CharacterCache {
        private CharacterCache(){}

        static final Character cache[] = new Character[127 + 1];

        static {
            for (int i = 0; i < cache.length; i++)
                cache[i] = new Character((char)i);
        }
    }
    
    public static Character valueOf(char c) {
        if (c <= 127) { // must cache
            return CharacterCache.cache[(int)c];
        }
        return new Character(c);
    }

3.3 Byte類型的緩存

Byte類型的緩存是通過靜態內部類ByteCache實現,緩存的範圍是: [-128, 127]。

byte: 8位,1個字節,範圍[-2^7, 2^7 - 1]即 [-128, 127],所以Byte緩存的範圍就是它本身能表示的範圍。

    private static class ByteCache {
        private ByteCache(){}

        static final Byte cache[] = new Byte[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Byte((byte)(i - 128));
        }
    }
    
    public static Byte valueOf(byte b) {
        final int offset = 128;
        return ByteCache.cache[(int)b + offset];
    }

3.4 Short類型的緩存

Short類型的緩存是通過靜態內部類ShortCache實現,緩存的範圍是: [-128, 127]。

short: 16位,2個字節,範圍[-2^15, 2^15 - 1]

    private static class ShortCache {
        private ShortCache(){}

        static final Short cache[] = new Short[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Short((short)(i - 128));
        }
    }

    public static Short valueOf(short s) {
        final int offset = 128;
        int sAsInt = s;
        if (sAsInt >= -128 && sAsInt <= 127) { // must cache
            return ShortCache.cache[sAsInt + offset];
        }
        return new Short(s);
    }

3.5 Integer類型的緩存

Integer類型的緩存是通過靜態內部類IntegerCache實現,默認緩存的範圍是: [-128, high(默認爲127)]。其中緩存的上限high可以通過JVM的-XX:AutoBoxCacheMax=<size>命令配置。

int: 32位,4個字節,範圍[-2^31, 2^31 - 1]

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

3.6 Long類型的緩存

Long類型的緩存是通過靜態內部類LongCache實現,緩存的範圍是: [-128, 127]。 long: 64位,8個字節,範圍[-2^63, 2^63 - 1]

    private static class LongCache {
        private LongCache(){}

        static final Long cache[] = new Long[-(-128) + 127 + 1];

        static {
            for(int i = 0; i < cache.length; i++)
                cache[i] = new Long(i - 128);
        }
    }
    
    public static Long valueOf(long l) {
        final int offset = 128;
        if (l >= -128 && l <= 127) { // will cache
            return LongCache.cache[(int)l + offset];
        }
        return new Long(l);
    }    

4. 包裝類的內存佈局

4.1 普通Java對象的內存佈局

首先了解下Java對象的內存佈局。一個Java對象在內存中可以分爲三部分:

  1. 對象頭:普通Java類型的對象頭,分爲兩個部分:
    • Mark Word: 存儲對象自身的運行時數據,如hashcode、gc分代年齡 ,鎖記錄等。
    • Klass Word: 存儲對象的類型指針,該指針指向方法區它的類元數據,JVM通過這個指針確定對象是哪個類的實例 。
  2. 實例數據:即Java的成員字段,包括基本類型和對象引用。
  3. 對齊填充:只用作佔位對齊字節,不一定存在。 一個對象的內存佈局示意如下:
|------------------------|-----------------|---------|
|       Object Header    |  Instance Data  | Padding |
|-----------|------------|-----------------|---------|
| Mark Word | Klass Word | field1|filed2|  | Padding |
|-----------|------------|-----------------|---------|

4.2 Java包裝類的內存佈局

這裏通過jol工具來分析對象所佔的內存,其Maven依賴如下:

    <dependency>
        <groupId>org.openjdk.jol</groupId>
        <artifactId>jol-core</artifactId>
        <version>0.9</version>
    </dependency>

使用方式如下:

    @Test
    public void testCase01() {
        System.out.println(ClassLayout.parseInstance(new Integer(1)).toPrintable());
    }

這裏的進行研究測試64位JVM虛擬機,JDK8。默認開啓-XX:CompressedOops選項。

這裏以Integer包裝爲例進行分析

4.2.1 開啓指針壓縮

上面代碼輸出結果爲:

java.lang.Integer object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           3e 22 00 f8 (00111110 00100010 00000000 11111000) (-134208962)
     12     4    int Integer.value                             1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

還原其內存佈局,可以發現包裝類的對象實例所佔內存爲16個字節,在加上包裝類引用變量指針的4個字節,一共20個字節,比int的4個字節的內存多5倍。

|----------------------------------------|----------------|
|               Object Header            |  Instance Data |
|-------------------|--------------------|----------------|
| Mark Word(8 byte) | Klass Word(4 byte) |  value(4 byte) |
|-------------------|--------------------|----------------|

4.2.2 關閉指針壓縮

VM options配置-XX:-UseCompressedOops 上面代碼輸出結果爲:

java.lang.Integer object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           60 96 3c 25 (01100000 10010110 00111100 00100101) (624727648)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
     16     4    int Integer.value                             1
     20     4        (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

還原其內存佈局,可以發現包裝類的對象實例所佔內存爲24個字節,在加上包裝類引用變量指針的8個字節,一共32個字節,比int的4個字節的內存多8倍。

|----------------------------------------|----------------|-----------------|
|                Object Header           |  Instance Data |      Padding    |
|-------------------|--------------------|----------------|-----------------|
| Mark Word(8 byte) | Klass Word(8 byte) |  value(4 byte) | Padding(4 byte) |
|-------------------|--------------------|----------------|-----------------|

參考與感謝

老哥們,覺得總結的有點用的,點個讚唄~

  1. https://juejin.im/post/5d8ff563f265da5bb252de76
  2. https://juejin.im/post/5b624f4d518825068302aee9
  3. https://juejin.im/post/5d628c50e51d4561c75f2822
  4. https://blog.csdn.net/xialei199023/article/details/63251295
  5. https://cloud.tencent.com/developer/article/1097119
  6. https://www.jianshu.com/p/ad505f9163b2

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章