拆箱裝箱都不知道?(從源碼帶你理解包裝類)

衆所周知,java 是一門面向對象的高級語言。但是 java 中的基本類型不能作爲對象使用,爲了解決對象的調用問題,爲每個基本類型創造了對應的包裝類型。
先來看一道包裝類的題目吧

int a = 10;
Integer b = 10;
System.out.println(a == b);        ①
System.out.println(b.equals(a));   ②
Integer c = 100;
Integer d = 100;
System.out.println(c == d);        ③
System.out.println(c.equals(d));   ④
Integer e = new Integer(100);
Integer f = new Integer(100);
System.out.println(e == f);        ⑤
System.out.println(e.equals(f));   ⑥
Integer g = 1000;
Integer h = 1000;
System.out.println(g == h);        ⑦
System.out.println(g.equals(h));

在看知識點前先看一下習題,看看是否存在不瞭解的點,以便對症下藥
這些題考查的都是 java 中很基礎的一類概念
包括包裝類與基本類型的轉換,包括自動拆箱裝箱
以及 == 和 equals 的區別

給上答案
①true ②true ③true ④true ⑤false ⑥true ⑦false ⑧true
以下示範都以Integer爲例,其他包裝類型與之無明顯差異,大家看會了 Integer 其他包裝類也就都能理解了

爲什麼需要包裝類

  • 基本類的值不屬於對象,因此基本類型只存在一個值,沒有各種屬性,也沒有各種方法可以調用。
  • 很多場景需要使用對象,比如 java 中的集合,所以爲了能使基本類型的值也能存入集合中,或者用於其他類中,則需要把它們包裝成對象。
  • 每個非基本類都繼承自基類 Object ,所以天生神力,自帶一身方法。但是基本類型沒有方法可以調用,用包裝類可以繼承 Object 方法,也可以添加自身方法。
  • 非基本類型變量不賦值默認爲 null,但基本類型沒有 null 值,比如 int 默認爲 0,Boolean 默認爲 false。
  • 在某些場合中,我們可能會希望使用到 null 值,表示某些意義,那基本類型則就做不到了。

包裝類與基本類型的轉換 (包括自動裝拆箱)

JDK自從1.5之後就引入了自動裝拆箱的功能,這大大簡化了開發人員的代碼複雜度,使得對基本類型的代碼書寫效率大大提高。
但是這也帶來了一個問題,很多小白搞不明白基本類型和包裝類型,使很多開發人員對此形成誤解。

首先,最基本的,用 new 來構造一個 Integer 對象
再用 Integer 的 intValue() 方法返回它的int值

Integer a = new Integer(1);
int b = a.intValue();

但在 java 中一般不提倡 new 一個 Integer 包裝類對象
用 Integer.valueOf() 靜態方法來獲取一個 Integer 對象

a = Integer.valueOf(5);
Integer a = 5; // 也可以寫成

而這個方法即是 int --> Integer 的自動裝箱方法 (即上面兩行代碼是等同的)
同理自動拆箱

Integer a = 5;
// int b = a.intValue();
int b = a; // 這行代碼與上面一行代碼等同

比如之前題目中的①和② 就有許多裝拆箱的知識點

int a = 10;
Integer b = 10; // 自動裝箱
// 將b自動拆箱與a比較
System.out.println(a == b);
// 將a自動裝箱成爲對象才能作爲equals中的參數
System.out.println(b.equals(a));

但是在自動裝箱需要注意一個細節,也就是之前題目中的③④⑦⑧。
在 -128 ~ 127 的數中,也就是 byte 的取值範圍,自動裝箱時,會從常量池中去取Integer對象,因而包裝出的 Integer 對象是同一個對象,不在這個範圍內的數字,包裝出的 Integer 對象則都是 new 出來的新對象。

Integer c = 100;  // 從常量池中取出Integer對象
Integer h = 1000; // 相當於用new創建出一個新對象

因此在之前題目中值爲 100 的 Integer 對象 == 爲 true,而值爲 1000 的Integer對象則爲 false。

基本類型和包裝類型與String的轉換

由於存在自動裝拆箱的功能,所以對於 String 與數字轉換時,可以將其轉換爲基本類型 int,也可以轉換爲包裝類 Integer,通過自動裝拆箱即可以正確賦值給變量
String 和 Integer 之間的轉換

// String --> Integer
Integer a = new Integer("12345");
// Integer --> String
String s = a.toString();

String 和 int 之間的轉換

// String --> int
int a = Integer.parseInt("12345");
// String --> Integer --> int
a = new Integer("12345");
// int --> Integer --> String
String s = a + ""; //自動裝箱調用toString()
s = (Integer.valueOf(a)).toString();

包裝類基礎

基本類 包裝類 繼承 基本類類信息 包裝類對應類信息
byte Byte Number byte.class Byte.TYPE
short Short Number short.class Short.TYPE
int Integer Number int.class Integer.TYPE
long Long Number long.class Long.TYPE
float Float Number float.class Float.TYPE
double Double Number double.class Double.TYPE
char Character Object char.class Character.TYPE
boolean Boolean Object boolean.class Boolean.TYPE

一些關鍵源碼(解釋寫在註釋之中)

首先是 Integer 的範圍,與 int 相同,都爲 -2的31次方~2的31次方-1。
其中0x表示16進制,但是仔細一看,會發現最小值比最大值大一,這是因爲在二進制中負數是用補碼錶示的,最高位是符號位,0是正,1位負,所以正數最大值沒有問題,而負數最小值則根據首位1推出爲負數。
對於補碼不細講,只需記住
正數的補碼 = 原碼
負數的補碼 = 原碼符號位不變 + 數值位按位取反後+1
或者 = 原碼符號位不變 + 數值位從右邊數第一個1及其右邊的0保持不變,左邊安位取反

@Native public static final int   MIN_VALUE = 0x80000000;
@Native public static final int   MAX_VALUE = 0x7fffffff;

構造方法很簡單

public Integer(int value) {
    this.value = value;
} // 給指定值創建對象
public Integer(String s) throws NumberFormatException {
    this.value = parseInt(s, 10);
} // 通過字符串解析出值創建對象

裝箱方法

public static Integer valueOf(int i) {
    // 如果值在low和high之間 low爲-128 high一般默認爲127
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        // 則直接返回常量池中的對象
        return IntegerCache.cache[i + (-IntegerCache.low)];
    // 否則用new新建一個Integer對象返回
    return new Integer(i);
}

在源碼中我們發現了一個靜態嵌套類IntegerCache,它擁有兩個 final 值 low 和 high,代表緩存的 Integer 的對象的範圍,這樣在裝箱時可以直接獲取,而不用再創建對象。

private static class IntegerCache {
    static final int low = -128;  // 緩存最小值
    static final int high;        // 緩存最大值
    // low到high的Integer對象都被保存在這裏 裝箱時取
    static final Integer cache[];
    // 根據類加載機制 以下方法塊在類初始化時執行
    // 會率先將low到high的Integer保存在cache數組中
    static {
        int h = 127;
        /* 此處省略一些無關代碼  */
        high = h;
        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }
    // 私有構造方法 保證類無法創建對象
    // 因爲該類僅作爲事先緩存Integer對象
    private IntegerCache() {}
}

再看toString()方法

public static String toString(int i) {
    if (i == Integer.MIN_VALUE) //爲最小值則直接return
        return "-2147483648";
    // 計算數字的長度
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    // 根據長度創建字符數組
    char[] buf = new char[size];
    // 不斷獲取字符放入數組中
    getChars(i, size, buf);
    // 根據字符數組返回新字符串
    return new String(buf, true);
}

裏面的 stringSize 方法很有趣,很多情況一般會用循環除10來計算長度,不衆所周知,JDK源碼是非常講究效率的,它直接保存了在 MAX 範圍內的所有每個長度最大值,直接依次比較,十分高效。

final static int [] sizeTable = { 9, 99, 999, 9999, 
    99999, 999999, 9999999, 
    99999999, 999999999, Integer.MAX_VALUE };
static int stringSize(int x) {
    for (int i=0; ; i++)
        if (x <= sizeTable[i])
            return i+1;
}

總之對於我們來說,java 中的每一個基礎類我們都應把它學習透徹,理解它的執行原理,儘量對源碼中的關鍵方法都能掌握,這樣在我們的編程生涯中,才能做到遊刃有餘,不犯一些低級錯誤。
同時,jdk 的源碼都是十分優秀,高效的,在 jdk 版本不斷迭代的過程中,很多關鍵類的代碼都會進行優化和調整,確保程序的高效執行。

文章到這裏就結束啦,我還是大學生哦,喜歡學習的小夥伴可以評論交流,或者加關注,一起學習更輕鬆。
素質三連。。。

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