Java拾遺:014 - 二進制、進制轉換及位運算

二進制

二進制是計算機中普遍採用的一種數制,由0和1組成,進位規則爲「逢二進一」,如:0001表示十進制中的1,0010表示十進制中的2。二進制擁有大量非學有用的特性,詳情參考:百度百科:二進制數html

在有符號整數中,最高位表示符號位,0表示正數、1表示負數。java

在Java中整型(int)由4個字節(32位)表示,因此一個整型數字(以1024爲例)用二進制表示爲:程序員

# 下面是一個32位的二進制數,等價於十進制的1024
00000000 00000000 00000100 00000000
# 其換算過程爲(0乘以任何數都爲0),這裏^表示次冪(非按位異或)
0 * 2 ^ 0 + 0 * 2 ^ 1 + 0 * 2 ^ 2 + ... + 1 * 2 ^ 10 == 2 ^ 10 == 1024

原碼、反碼、補碼

二進制運算涉及到原碼、反碼、補碼,其中正數的原碼、反碼、補碼都是同樣的,而負數則不一樣。負數的反碼爲符號位不變,其它位取反(0變成1,1變成0),補碼則爲反碼加一。0與正數同樣,反碼、補碼都是其原碼0。 而在計算機中,正數由原碼(也能夠理解爲補碼,補碼原碼是同樣的)而負數由補碼來形式來表示。 計算機中負數使用補碼錶示的緣由多是因爲有符號整數表示範圍形成成的。一個無符號字節表示整數範圍爲:[0, 255]恰好256個整數,而有符號的整數範圍則是:[-128, 127],若是是無符號整數能夠用[00000000, 11111111]來表示這個範圍,而在有符號的整數只能用[11111111, 01111111]但這隻能表示到[-127, 127],而沒法表示-128。 能夠推導一下,若是是-127,它的反碼爲:10000000,而它的補碼則爲:10000001,而比這個補碼小一位的數字恰好是:10000000,也就變相表示了-128,但其對應的原碼在計算機中沒法表示,因此計算機中採用補碼錶示負數。 上述推斷過程來自:http://blog.chinaunix.net/uid-26495963-id-3074603.html編程

二進制運算

二進制實現加法運算,好比在十進制中1 + 2 == 3,而用二進制計算爲:性能

00000000 00000000 00000000 00000001
+
00000000 00000000 00000000 00000010
=
00000000 00000000 00000000 00000011 (十進制爲3)

二進制減法,實際是將減法轉換爲正數與負數相加,實際還是加法運算,如在十進制中1 - 2 == -1,用二進制計算爲:測試

# 這裏須要先找到1的補碼,由於計算項中有負數(計算機中負數使用補碼錶示):1 + (-2)
00000000 00000000 00000000 00000001
+ 
11111111 11111111 11111111 11111110  (原碼:10000000 00000000 00000000 00000010,反碼:11111111 11111111 11111111 11111101)
==
11111111 11111111 11111111 11111111
# 將計算結果(補碼)轉換爲反碼:11111111 11111111 11111111 11111110,
# 再轉換爲原碼:10000000 00000000 00000000 00000001,
# 即十進制中的:-1

進制轉換

所謂進制就是人們用來計數的方法,好比十進制,逢十進一;二進制,逢二進一,經常使用的還有八進制、十六進制。優化

進制轉換本質

「數制」只是一套符號系統來表示指稱「量」的多少,不管「數制」怎麼變或者使用任意一種「數制」,其所表示「量」還是一致的,這一點是進制轉換的基礎。任意一種進制的基本表示方法能夠以以下形式表示(負進制不予考慮):ui

n位的R進制中的數位排列是這樣的:R^n-1 R^n-2 ... R^2 R^1 R^0

以十進制爲例:1024 == 1 * 10^3 + 0 * 10^2 + 2 * 10^1 + 4 * 10^0 == 1000 + 0 + 20 + 4 而二進制則是一個道理:0000 1101 == 1 * 2^3 + 1 * 2^2 + 0 * 2^1 + 1 * 2^0 == 8 + 4 + 0 + 1 == 13.net

短除法實現進制轉換

關於進制轉換一種十分常見的方法是「短除法」,按進制整除取餘,將每次餘數倒排即爲轉換後的進製表示結果,如:十進制整數17轉換爲二進制unix

# 當整除結果爲0時計算結束
17 / 2 == 8,餘1
8 / 2 == 4,餘0
4 / 2 == 2,餘0
2 / 2 == 1, 餘0
1 / 2 == 0,餘1
# 餘數倒排即爲(前面補0):00010001
# 00010001 == 1 * 2^4 + 1 == 16 + 1 == 17

下面是一段使用Java實現的進制轉換代碼

// 以十進制數字 1024 爲例,測試進制間轉換
        int number = 1024;

        // 使用短除法轉換爲二進制,短除法轉換時除數爲進制值,這裏是二進制,因此是2
        // 當整數被除盡後,餘數倒排序即爲轉換後的二進制數字,由於要倒排,這裏採用棧結構存儲餘數
        Stack<Integer> stack = new Stack<>();
        int m = number;
        while (m != 0) {
            // 整除後的餘數
            stack.push(m % 2);
            // 整除後的結果
            m /= 2;
        }
        // 將數據取出,組成二進制序列
        // 10000000000
        while (!stack.isEmpty()) {
            System.out.print(stack.pop());
        }
        System.out.println();
        // 測試結果是否正確(後面的項全是0,因此省略了)
        assertEquals(number, 1 * (int) (Math.pow(2, 10)));

十進制轉換爲八進制、十六進制的方法是同樣的,但須要注意的是十六進制中[10-16]由英文字母[a-f]表示,因此自定義實現進制轉換時須要特殊處理。而其它進制間轉換一般先轉換爲十進制,再轉換爲目標進制,因此轉換過程是一致的。

Java進制轉換API

在Java中,JDK默認提供了一系列進制轉換方法

// Integer提供的進制轉換方法
        // 十進制轉換二進制
        assertEquals("10000000000", Integer.toBinaryString(1024));
        // 十進制轉換八進制
        assertEquals("2000", Integer.toOctalString(1024));
        // 十進制轉換十六進制
        assertEquals("400", Integer.toHexString(1024));
        assertEquals("40c", Integer.toHexString(1036));

        // 其它進制轉換爲十進制
        assertEquals(1024, Integer.parseInt("10000000000", 2));
        assertEquals(1024, Integer.parseInt("2000", 8));
        assertEquals(1024, Integer.parseInt("400", 16));

        // 字符串格式化使用佔位符進行進制轉換
        // 1024, 2000, 400,沒有二進制格式化方法
        System.out.println(String.format("%d, %o, %x", 1024, 1024, 1024, 1024));
        // 1024, 02000, 0x400
        System.out.println(String.format("%d, %#o, %#x", 1024, 1024, 1024, 1024));

位運算及位運算符

程序中全部數據最終都是以二進制存儲的,而位運算則是針對二進制直接進行計算的一種方法,一般性能會很是高(目前CPU對加法作了優化,能夠達到二進制運算的性能,但乘法仍差一些)。 Java中提供了一組位運算符,分別是:按位與&、按位或|、按位異或^、按位取反~、左移<<、右移>>以及無符號右移>>>,下面分別演示其用法及效果。

按位與 &

僅當兩個操做數都是1時,輸出結果才爲1,不然爲0

// 比較數二進制長度不等時前面補0對齊
        // 12 -> 1100
        //  5 -> 0101
        // 12 & 5 = 0100 0 4
        assertEquals(4, 12 & 5);
        // 使用二進制寫法測試
        assertEquals(0b0100, 0b1100 & 0b0101);

按位或 |

僅當兩個操做數都是0時,輸出結果才爲0,不然爲1

assertEquals(13, 12 | 5);
        assertEquals(0b1101, 0b1100 | 0b0101);

按位異或 ^

僅當兩個操做數不一樣時,輸出結果才爲1,不然爲0

assertEquals(9, 12 ^ 5);
        assertEquals(0b1001, 0b1100 ^ 0b0101);

按位取反 ~

這是一個單目運算符,用於將所有數字取反:0爲1,1爲0。將一個二進制數按位取反,等效於-1減去該數,如:~12 == -13

// 注意,上面都是簡寫的形式,前面的0未寫出來,但取反時要寫全,不然結果是錯的,int類型是32位
        // 下面目標數字實際是:0000000000000000000000000001100,按位取反:11111111111111111111111111110011
        // 注意二進制左邊第一位是0,表示無符號整數(爲1表示負數,即有符號整數),要使用Integer.parseUnsignedInt()方法轉換
        assertEquals(0b11111111111111111111111111110011, ~0b1100);
        // ~12 == -1 - 12 == -13
        assertEquals(-13, ~12);
        // ~-12 == -1 - (-12) == -1 + 12 == 11
        assertEquals(11, ~-12);

左移 <<

把一個數的所有位都向左移動指定位數。其實際效果爲:左移n位就是乘以2的n次方

// 12 -> 1100:1100 << 0 == 1100 == 12 == 12 * 2 ^ 0
        assertEquals(24, 12 << 1);
        // 12 -> 1100:1100 << 1 == 11000 == 24 == 12 * 2 ^ 1
        assertEquals(24, 12 << 1);
        // 12 -> 1100:1100 << 2 == 110000 == 48 == 12 * 2 ^ 2
        assertEquals(48, 12 << 2);
        // 12 -> 1100:1100 << 3 == 1100000 == 96 == 12 * 2 ^ 3
        assertEquals(96, 12 << 3);

右移 >>

把一個數的所有位都向右移動指定位。其實際效果爲:右移n位就是除以2的n次方結果取整

// 1100 >> 1 == 0110 == 6
        assertEquals(6, 12 >> 1);
        // 1100 >> 2 == 0011 == 3
        assertEquals(3, 12 >> 2);
        // 1100 >> 3 == 0001 == 1
        assertEquals(1, 12 >> 3);
        // 1100 >> 4 == 0000 == 0
        assertEquals(0, 12 >> 4);

        // 100000 >> 5 == 000001 == 1
        assertEquals(1, 32 >> 5);
        assertEquals(2, 32 >> 4);
        assertEquals(4, 32 >> 3);
        assertEquals(8, 32 >> 2);
        assertEquals(16, 32 >> 1);
        assertEquals(32, 32 >> 0);

無符號右移 >>>

與右移相似,可是符號位仍用原值填充

// 00000000000000000000000000001100 >>> 1 == 00000000000000000000000000000110
        // => 110 == 6
        assertEquals(12, 12 >>> 0);
        assertEquals(6, 12 >>> 1);
        assertEquals(3, 12 >>> 2);
        assertEquals(1, 12 >>> 3);
        assertEquals(0, 12 >>> 4);
        assertEquals(0, 12 >>> 5);

        // 11111111111111111111111111110100 >>> 1 == 01111111111111111111111111111010
        assertEquals(-12, -12 >>> 0);
        assertEquals(2147483642, -12 >>> 1);
        assertEquals(1073741821, -12 >>> 2);
        assertEquals(536870910, -12 >>> 3);


        // 標誌位使用原標誌位填充,因此仍是1
        // 11111111111111111111111111110100 >> 1 == 11111111111111111111111111111010
        assertEquals(-6, -12 >> 1);
        // 10111111111111111111111111110100 >> 1 == 11011111111111111111111111111010
        assertEquals(-536870918, -1073741836 >> 1);
        assertEquals(-1073741836, 0b10111111111111111111111111110100);
        assertEquals(-536870918, 0b11011111111111111111111111111010);

結語

二進制及進制轉換做爲編程的基礎知識,每一個技術人員都應該掌握並熟練應用,尤爲是位運算能極大提高程序性能,高級程序員更應精通此道(筆者實際也不擅長,因此這裏屬於補課)。 本篇內容本來是放後面再寫的,但在研究HashMap源碼過程當中發現大量使用了位運算,因此不得不提早整理出來。

相關文章
相關標籤/搜索