計算機程式的思維邏輯 (27) - 剖析包裝類 (中) - 解析晦澀的二進位制操作

swiftma發表於2016-09-30

本系列文章經補充和完善,已修訂整理成書《Java程式設計的邏輯》(馬俊昌著),由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買:京東自營連結

計算機程式的思維邏輯 (27) - 剖析包裝類 (中) - 解析晦澀的二進位制操作

本節繼續探討包裝類,主要介紹Integer類,下節介紹Character類,Long與Integer類似,就不再單獨介紹了,其他類基本已經介紹完了,不再贅述。

一個簡單的Integer還有什麼要介紹的呢?它有一些二進位制操作,我們來看一下,另外,我們也分析一下它的valueOf實現。

為什麼要關心實現程式碼呢?大部分情況下,確實不用關心,我們會用它就可以了,我們主要是為了學習,尤其是其中的二進位制操作,二進位制是計算機的基礎,但程式碼往往晦澀難懂,我們希望對其有一個更為清晰深刻的理解。

我們先來看按位翻轉。

位翻轉

用法

Integer有兩個靜態方法,可以按位進行翻轉:

public static int reverse(int i)
public static int reverseBytes(int i)
複製程式碼

位翻轉就是將int當做二進位制,左邊的位與右邊的位進行互換,reverse是按位進行互換,reverseBytes是按byte進行互換。我們來看個例子:

int a = 0x12345678;
System.out.println(Integer.toBinaryString(a));

int r = Integer.reverse(a);
System.out.println(Integer.toBinaryString(r));

int rb = Integer.reverseBytes(a);
System.out.println(Integer.toHexString(rb));
複製程式碼

a是整數,用十六進位制賦值,首先輸出其二進位制字串,接著輸出reverse後的二進位制,最後輸出reverseBytes的十六進位制,輸出為:

10010001101000101011001111000
11110011010100010110001001000
78563412
複製程式碼

reverseBytes是按位元組翻轉,78是十六進位制表示的一個位元組,12也是,所以結果78563412是比較容易理解的。

二進位制翻轉初看是不對的,這是因為輸出不是32位,輸出時忽略了前面的0,我們補齊32位再看:

00010010001101000101011001111000
00011110011010100010110001001000
複製程式碼

這次結果就對了。

這兩個方法是怎麼實現的呢?

reverseBytes

來看reverseBytes的程式碼:(您在掘金APP中可能看到亂碼,掘金bug,您可以在手機瀏覽器中開啟,或者使用掘金PC版,或者關注我的公眾號"老馬說程式設計"

public static int reverseBytes(int i) {
    return ((i >>> 24)           ) |
           ((i >>   8) &   0xFF00) |
           ((i <<   8) & 0xFF0000) |
           ((i << 24));
}
複製程式碼

以引數i等於0x12345678為例,我們來分析執行過程:

i>>>24 無符號右移,最高位元組挪到最低位,結果是 0x00000012。

(i>>8) & 0xFF00,左邊第二個位元組挪到右邊第二個,i>>8結果是 0x00123456,再進行 & 0xFF00,保留的是右邊第二個位元組,結果是0x00003400。

(i << 8) & 0xFF0000,右邊第二個位元組挪到左邊第二個,i<<8結果是0x34567800,再進行 & 0xFF0000,保留的是右邊第三個位元組,結果是0x00560000。

i<<24,結果是0x78000000,最右位元組挪到最左邊。

這四個結果再進行或操作|,結果就是0x78563412,這樣,通過左移、右移、與和或操作,就達到了位元組翻轉的目的。

reverse

我們再來看reverse的程式碼:

public static int reverse(int i) {
    // HD, Figure 7-1
    i = (i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
    i = (i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
    i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
    i = (i << 24) | ((i & 0xff00) << 8) |
        ((i >>> 8) & 0xff00) | (i >>> 24);
    return i;
}
複製程式碼

這段程式碼雖然很短,但非常晦澀,到底是什麼意思呢?

程式碼第一行是一個註釋, "HD, Figure 7-1",這是什麼意思呢?HD表示的是一本書,書名為Hacker's Delight,HD是它的縮寫,Figure 7-1是書中的圖7-1,這本書中,相關內容如下圖所示:

計算機程式的思維邏輯 (27) - 剖析包裝類 (中) - 解析晦澀的二進位制操作

可以看出,Integer中reverse的程式碼就是拷貝了這本書中圖7-1的程式碼,這個程式碼的解釋在圖中也說明了,我們翻譯一下。

高效實現位翻轉的基本思路,首先交換相鄰的單一位,然後以兩位為一組,再交換相鄰的位,接著是四位一組交換、然後是八位、十六位,十六位之後就完成了。這個思路不僅適用於二進位制,十進位制也是適用的,為便於理解,我們看個十進位制的例子,比如對數字12345678進行翻轉,

第一輪,相鄰單一數字進行互換,結果為:

21 43 65 87

第二輪,以兩個數字為一組交換相鄰的,結果為:

43 21 87 65

第三輪,以四個數字為一組交換相鄰的,結果為:

8765 4321

翻轉完成。

對十進位制而言,這個效率並不高,但對於二進位制,卻是高效的,因為二進位制可以在一條指令中交換多個相鄰位

這行程式碼就是對相鄰單一位進行互換:

x = (x & 0x55555555) <<  1 | (x & 0xAAAAAAAA) >>>  1;
複製程式碼

5的二進位制是0101,0x55555555的二進位制表示是:

01010101010101010101010101010101
複製程式碼

x & 0x55555555就是取x的奇數位。

A的二進位制是1010,0xAAAAAAAA的二進位制表示是:

10101010101010101010101010101010
複製程式碼

x & 0xAAAAAAAA就是取x的偶數位。

(x & 0x55555555) <<  1 | (x & 0xAAAAAAAA) >>>  1;
複製程式碼

表示的就是x的奇數位向左移,偶數位向右移,然後通過|合併,達到相鄰位互換的目的。這段程式碼可以有個小的優化,只使用一個常量0x55555555,後半部分先移位再進行與操作,變為:

(i & 0x55555555) << 1 | (i >>> 1) & 0x55555555;
複製程式碼

同理,如下程式碼就是以兩位為一組,對相鄰位進行互換:

i = (i & 0x33333333) << 2 | (i & 0xCCCCCCCC)>>>2;
複製程式碼

3的二進位制是0011,0x33333333的二進位制表示是:

00110011001100110011001100110011 
複製程式碼

x & 0x33333333就是取x以兩位為一組的低半部分。

C的二進位制是1100,0xCCCCCCCC的二進位制表示是:

11001100110011001100110011001100
複製程式碼

x & 0xCCCCCCCC就是取x以兩位為一組的高半部分。

(i & 0x33333333) << 2 | (i & 0xCCCCCCCC)>>>2;
複製程式碼

表示的就是x以兩位為一組,低半部分向高位移,高半部分向低位移,然後通過|合併,達到交換的目的。同樣,可以去掉常量0xCCCCCCCC,程式碼可以優化為:

(i & 0x33333333) << 2 | (i >>> 2) & 0x33333333;
複製程式碼

同理,下面程式碼就是以四位為一組,進行交換。

i = (i & 0x0f0f0f0f) << 4 | (i >>> 4) & 0x0f0f0f0f;
複製程式碼

到以八位為單位交換時,就是位元組翻轉了,可以寫為如下更直接的形式,程式碼和reverseBytes基本完全一樣。

i = (i << 24) | ((i & 0xff00) << 8) |
    ((i >>> 8) & 0xff00) | (i >>> 24);
複製程式碼

reverse程式碼為什麼要寫的這麼晦澀呢?或者說不能用更容易理解的方式寫嗎?比如說,實現翻轉,一種常見的思路是,第一個和最後一個交換,第二個和倒數第二個交換,直到中間兩個交換完成。如果資料不是二進位制位,這個思路是好的,但對於二進位制位,這個效率比較低。

CPU指令並不能高效的操作單個位,它操作的最小資料單位一般是32位(32位機器),另外,CPU可以高效的實現移位和邏輯運算,但加減乘除則比較慢。

reverse是在充分利用CPU的這些特性,並行高效的進行相鄰位的交換,也可以通過其他更容易理解的方式實現相同功能,但很難比這個程式碼更高效。

迴圈移位

用法

Integer有兩個靜態方法可以進行迴圈移位:

public static int rotateLeft(int i, int distance)
public static int rotateRight(int i, int distance) 
複製程式碼

rotateLeft是迴圈左移,rotateRight是迴圈右移,distance是移動的位數,所謂迴圈移位,是相對於普通的移位而言的,普通移位,比如左移2位,原來的最高兩位就沒有了,右邊會補0,而如果是迴圈左移兩位,則原來的最高兩位會移到最右邊,就像一個左右相接的環一樣。我們來看個例子:

int a = 0x12345678;
int b = Integer.rotateLeft(a, 8);
System.out.println(Integer.toHexString(b));

int c = Integer.rotateRight(a, 8);
System.out.println(Integer.toHexString(c))
複製程式碼

b是a迴圈左移8位的結果,c是a迴圈右移8位的結果,所以輸出為:

34567812
78123456
複製程式碼

實現程式碼

這兩個函式的實現程式碼為:

public static int rotateLeft(int i, int distance) {
    return (i << distance) | (i >>> -distance);
}
public static int rotateRight(int i, int distance) {
    return (i >>> distance) | (i << -distance);
}
複製程式碼

這兩個函式中令人費解的是負數,如果distance是8,那 i>>>-8是什麼意思呢?其實,實際的移位個數不是後面的直接數字,而是直接數字的最低5位的值,或者說是直接數字 & 0x1f的結果。之所以這樣,是因為5位最大表示31,移位超過31位對int整數是無效的。

理解了移動負數位的含義,我們就比較容易上面這段程式碼了,比如說,-8的二進位制表示是:

11111111111111111111111111111000
複製程式碼

其最低5位是11000,十進位制就是24,所以i>>>-8就是i>>>24i<<8 | i>>>24就是迴圈左移8位。

上面程式碼中,i>>>-distance就是 i>>>(32-distance)i<<-distance就是i<<(32-distance)

按位查詢、計數

Integer中還有其他一些位操作,包括:

public static int signum(int i)
複製程式碼

檢視符號位,正數返回1,負數返回-1,0返回0

public static int lowestOneBit(int i)
複製程式碼

找從右邊數第一個1的位置,該位保持不變,其他位設為0,返回這個整數。比如對於3,二進位制為11,二進位制結果是01,十進位制就是1,對於20,二進位制是10100,結果就是00100,十進位制就是4。

public static int highestOneBit(int i) 
複製程式碼

找從左邊數第一個1的位置,該位保持不變,其他位設為0,返回這個整數。

public static int bitCount(int i)  
複製程式碼

找二進位制表示中1的個數。比如20,二進位制是10100,1的個數是2。

public static int numberOfLeadingZeros(int i)
複製程式碼

左邊開頭連續為0的個數。比如20,二進位制是10100,左邊有27個0。

public static int numberOfTrailingZeros(int i)
複製程式碼

右邊結尾連續為0的個數。比如20,二進位制是10100,右邊有兩個0。

關於其實現程式碼,都有註釋指向Hacker's Delight這本書的相關章節,本文就不再贅述了。

valueOf的實現

上節我們提到,建立包裝類物件時,可以使用靜態的valueOf方法,也可以直接使用new,但建議使用valueOf,為什麼呢?我們來看valueOf的程式碼:

public static Integer valueOf(int i) {
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(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;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            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);
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);
    }

    private IntegerCache() {}
}
複製程式碼

IntegerCache表示Integer快取,其中的cache變數是一個靜態Integer陣列,在靜態初始化程式碼塊中被初始化,預設情況下,儲存了從-128到127,共256個整數對應的Integer物件。

在valueOf程式碼中,如果數值位於被快取的範圍,即預設-128到127,則直接從IntegerCache中獲取已預先建立的Integer物件,只有不在快取範圍時,才通過new建立物件。

通過共享常用物件,可以節省記憶體空間,由於Integer是不可變的,所以快取的物件可以安全的被共享。Boolean/Byte/Short/Long/Character都有類似的實現。這種共享常用物件的思路,是一種常見的設計思路,在<設計模式>這本著作中,它被賦予了一個名字,叫享元模式,英文叫Flyweight,即共享的輕量級元素。

小結

本節介紹了Integer中的一些位操作,位操作程式碼比較晦澀,但效能比較高,我們詳細解釋了其中的一些程式碼,如果希望有更多的瞭解,可以根據註釋,檢視Hacker's Delight這本書。我們同時介紹了valueOf的實現,介紹了享元模式。

下一節,讓我們來探討Character。


未完待續,檢視最新文章,敬請關注微信公眾號“老馬說程式設計”(掃描下方二維碼),深入淺出,老馬和你一起探索Java程式設計及計算機技術的本質。用心原創,保留所有版權。

計算機程式的思維邏輯 (27) - 剖析包裝類 (中) - 解析晦澀的二進位制操作

相關文章