基本數據類型及其包裝類(一)


我們都說,Java 是一門面向對象型程序設計語言,但是它設計出來的「基本數據類型」仿佛又打破了這一點,所以,只能說 Java 是非 100% 純度的面向對象程序設計語言。

但是,為什么 Sun 公司一直沒有刪除「基本數據類型」,而是為它增設了具有面向對象設計思想的「包裝類型」呢?

想必是有道理的,那么本文就試着分析一下「基本數據類型」存在的意義以及具有哪些優勢點,還有「包裝類」的具體實現細節。

基本類型 VS 對象類型

Java 中預定義了八種基本數據類型,包括:byte,int,long,double,float,boolean,char,short。基本類型與對象類型最大的不同點在於,基本類型基於數值,對象類型基於引用

image

基本類型的變量在棧的局部變量表中直接存儲的具體數值,而對象類型的變量則存儲的堆中引用。

顯然,相對於基本類型的變量來說,對象類型的變量需要占用更多的內存空間

上面說到,基本類型基於數值,所以基本類型是沒有類而言的,是不存在類的概念的,也就是說,變量只能存儲數值,而不具備操作數據的方法。對象類型則截然不同,變量實際上是某個類的實例,可以擁有屬性方法等信息,不再單一的存儲數值,可以提供各種各樣對數值的操作方法,但代價就是犧牲一些性能並占用更多的內存空間。

之所以 Java 里沒有一刀切了基本類型,就是看在基本類型占用內存空間相對較小,在計算上具有高於對象類型的性能優勢,當然缺點也是不言而喻的。

所以一般都是結合兩者在不同的場合下適時切換,那么 Java 中提供了哪些「包裝類型」來彌補「基本類型」不具備面向對象思想的劣勢呢?

image

可以看到,除了 int 和 char 兩者的包裝類名變化有些大以外,其余六種基本類型對應的包裝類名,都是大寫了首字母而已。

下面我們以 int 和 Integer 為例,通過源碼簡單看看包裝類具體是如何實現的。

int 與 Integer

首先需要明確一點的是,既然 Integer 是 int 的包裝類型,那么必然 Integer 也能像 int 一樣存儲整型數值。

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

Integer 類的內部定義了一個私有字段 value,專門用於保存一個整型數值,整個包裝類就是圍繞着這個 value 封裝了各種不同操作的方法。

而接着我們看看如何構建一個包裝類實例:

public Integer(int value) {
    this.value = value;
}
public Integer(String s) throws NumberFormatException {
    this.value = parseInt(s, 10);
}

Integer 類中提供兩種構造器給我們構建和初始化一個 Integer 類實例。第一種比較直接,允許你直接傳入一個整型數值對 value 進行初始化。第二種間接一點,允許你傳入一個數字的字符串,Integer 內部會嘗試着將字符串向整型數值進行轉換,如果成功則初始化 value,否則將拋出一個異常。

所以我們可以通過以下代碼將一個 int 類型變量轉換成一個 Integer 的包裝類實例:

int age = 22;
Integer iAge = new Integer(age);

接着,我們知道使用 System.out.println 方法打印一個 Integer 實例時,虛擬機會使用 Integer 實例的 toString 方法的返回值作為打印方法的參數。

那么 Integer 內部是如何實現將一個數值轉換為一個整型數值的呢?可能這個問題大家很少想過,因為這樣的細小的問題基本都被封裝的很好了,我們一般的開發並不需要過多的關心,但是如果讓你來寫,你能准確的寫出來嗎?

public String toString() {
    return toString(value);
}

首先,默認無參的 toString 方法會調用內部有參的另一個 toString 方法。

public static String toString(int i) {
    if (i == Integer.MIN_VALUE)
        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);
}

如果你的值等於 Integer.MIN_VALUE,那么直接返回預定義好的字符串即可,否則將會通過一個方法 stringSize 確定當前傳入的整數 i 是一個幾位的整數,也就是它需要使用幾個字符進行表示,該方法的具體細節我們等會說,這是一個實現很優雅的算法。

確定了 size,於是可以創建字符數組,並通過 getChars 方法完成數值向字符串的轉換,並最后構建一個字符串對象返回。

我們先看看這個 stringSize 方法的具體實現是怎樣的:

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;
}

這段代碼的實現於我用文字來描述可能不是那么清晰,我舉個例子,你就能很快明白了。

例如:x 等於 85,那么比 x 大並且最接近 x 的 sizeTable 元素是 99(兩位數中最大的數值),索引為 1,於是我們得到 x 是一個兩位數(1+1)。

仔細想一想,還是很好理解的,sizeTable 中的每個元素都是同等位數數字下最大的數值,99 是兩位數中最大的,999 是三位數中最大的,等等。那么當 x 最接近某個索引的元素時,即說明 x 的位數和該元素是一樣的,然后計算該元素的位數即可。

接着我們看看核心的 getChars 方法是如何實現的

static void getChars(int i, int index, char[] buf) {
    int q, r;
    int charPos = index;
    char sign = 0;
    if (i < 0) {
        sign = '-';
        i = -i;
    }
    while (i >= 65536) {
        q = i / 100;
    // really: r = i - (q * 100);
        r = i - ((q << 6) + (q << 5) + (q << 2));
        i = q;
        buf [--charPos] = DigitOnes[r];
        buf [--charPos] = DigitTens[r];
    }

    for (;;) {
        q = (i * 52429) >>> (16+3);
        r = i - ((q << 3) + (q << 1));  // r = i-(q*10) ...
        buf [--charPos] = digits [r];
        i = q;
        if (i == 0) break;
    }
    if (sign != 0) {
        buf [--charPos] = sign;
    }
}

別看這個方法的代碼不多,但是卻要求你有一定的二進制位運算基礎。首先需要明確幾個形參所代表的含義,i 就是我們待轉換成字符串的整型數值,index 是該數字的位數,buf 數組是轉換后的字符存儲的容器,用於存儲結果。

首先,如果 i 是一個負數,那么變量 sign 的值給它賦為「-」,標識它是一個負數,並將它取正,畢竟正數更方便我們操作。

接着是一個循環,只要 i 大於 65536(2^16),就一直執行循環體。

q = i / 100;

// q * (2^6 + 2^5 + 2^2) = q * 100

r = i - ((q << 6) + (q << 5) + (q << 2));

q 得到的是 i 去掉個位和十位后的值,而 r 得到的就是丟失的十位和個位,舉個例子:如果 i 等於 12345,那么 q 等於 123,r 等於 45 。

最后重置 i 的值以便進入下一次循環,並通過下面兩條語句完成個位和十位的存儲。

buf [--charPos] = DigitOnes[r];

buf [--charPos] = DigitTens[r];

image

這兩條賦值語句也很有意思,由於 r 必然是一個兩位數,所以無論怎樣 r 不會超過 100 。例如:r 等於 56,那么 DigitOnes[r] 將得到 6,DigitTens[r] 將得到 5 。

這段代碼的設計還是很巧妙的,那么通過這個循環,大於 65536 的位數都被倒序存儲進 buf 數組中了。

接着的一個 for 循環完成就是對小於 65536 的位部分的存儲。

q = (i * 52429) >>> (16+3);

r = i - ((q << 3) + (q << 1));

因為 2^ 19 等於 524288,所以 (i * 52429) >>> (16+3) 等效於 i * 52429 / 524288 約等於 i * 0.1000003814697 ,而 q 是整型數值,所以最終 q 的值其實就等於 i / 10 。

可能為了效率才將簡單的除以十的操作搞這么復雜的吧,最終 q 存儲的是 i 去掉個位后的數值,r 存儲的是丟失的個位。

例如:i 等於 1234,那么 q 等於 123,r 等於 4 。

於是可以通過類似的思想一位一位的存儲:

buf [--charPos] = digits [r];

image

而最后,判斷 sign 標志位以決定輸出該字符串的時候是否需要帶上符號「-」以表示該數值的正負性。

總結一下整個 toString 方法,核心點就兩件事,一是確定該數值的位數,即需要用幾個字符進行表述,二是根據數值轉換成字符串。第一步很簡單,不用多說,第二步針對 value 值的大小分步驟進行,大於 65536 的數值采取每次兩位的速度存儲,小於 65536 的數值位采取一位的速度存儲。

Integer 類中還有一類方法,valueOf,這是一個很重要的方法,jdk 1.5 以后實現的自動拆裝箱就是基於它的,具體的我們后面說,先看這個方法的實現。

該方法用到一個 IntegerCache 緩存機制,so,我們先看看這個緩存機制在 Integer 中的實現情況:

image

自 jdk 1.5 以后,sun 加入了這個緩存類用於復用指定范圍內的 Integer 實例,把內容相同的對象緩存起來,減少內存開銷。默認可以緩存數值在 [-128,127] 之間的實例,當然你可以通過啟動虛擬機參數 -XX:AutoBoxCacheMax 指定緩存區間的最大數值。

而程序的第一步就是讀取虛擬機啟動參數判斷程序啟動時是否指定了可緩存的最大數值,如果 integerCacheHighPropValue 為 null,那么說明並沒有顯式指定,於是使用 127 作為可緩存的最高限定。

否則,根據參數進行一些計算,如果設定的參數小於 127,那么將取 127 作為緩存的最高限定值。理論上,我們可以緩存最大到 Integer.MAX_VALUE ,但是實際上是做不到的,因為 Integer[] 數組可定義的最大長度就是 Integer.MAX_VALUE,而我們還有 127 個負數待緩存,顯然數組容量是不夠的。

所以 IntegerCache 其實最大能緩存到 Integer.MAX_VALUE - 129,一旦是設定的參數大於這個值,將默認取用這個值作為最高緩存限定。

所以,最終 IntegerCache 能夠緩存的數值區間介於 [low,high] 之間。

然后我們看 valueOf 這個方法是如何使用 IntegerCache 的:

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 中返回直接引用,只是它這里的取值方式有點意思。

cache[0] = -128(128 + -128)   cache[1] = -127(128 + -127)
cache[2] = -126(128 + -126)   cache[3] = -125(128 + -125)
......
cache[128 + i] = i;

因為 cache 是從 -128 開始緩存的,而索引又是從 0 開始的,所以任意一個數值距離 -128 的差值就是該值緩存在 cache 中的索引。

所以,一旦 i 位於我們緩存的值區間,那么將直接從緩存池中返回直接引用,否則將會實際創建一個 Integer 實例返回。

我們這里分析了三到四個方法的源碼實現,其實 Integer 類中還有很多工具性的方法,限於篇幅我們不能一一敘述,大家可以自行學習一下。

有關於包裝類型和基本類型之間的關系想必大家已經稍有了解了,還有一些有關自動拆裝箱以及一些經典的面試題放在下篇文章。


文章中的所有代碼、圖片、文件都雲存儲在我的 GitHub 上:

(https://github.com/SingleYam/overview_java)

歡迎關注微信公眾號:撲在代碼上的高爾基,所有文章都將同步在公眾號上。

image


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM