程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> JAVA綜合教程 >> 計算機程序的思維邏輯 (27),思維27

計算機程序的思維邏輯 (27),思維27

編輯:JAVA綜合教程

計算機程序的思維邏輯 (27),思維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的代碼:

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,這本書中,相關內容如下圖所示:

可以看出,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>>>24,i<<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編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。

-----------

更多好評原創文章

計算機程序的思維邏輯 (1) - 數據和變量

計算機程序的思維邏輯 (5) - 小數計算為什麼會出錯?

計算機程序的思維邏輯 (6) - 如何從亂碼中恢復 (上)?

計算機程序的思維邏輯 (8) - char的真正含義

計算機程序的思維邏輯 (12) - 函數調用的基本原理

計算機程序的思維邏輯 (17) - 繼承實現的基本原理

計算機程序的思維邏輯 (18) - 為什麼說繼承是把雙刃劍

計算機程序的思維邏輯 (19) - 接口的本質

計算機程序的思維邏輯 (20) - 為什麼要有抽象類?

計算機程序的思維邏輯 (21) - 內部類的本質

計算機程序的思維邏輯 (23) - 枚舉的本質

計算機程序的思維邏輯 (24) - 異常 (上)

計算機程序的思維邏輯 (25) - 異常 (下)

計算機程序的思維邏輯 (26) - 剖析包裝類 (上)

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved