參考:
Bitwise and Bit Shift Operators
《Java 編程思想 第3章 操作符》
今天學習 Java BitSet
類時,發現對於位運算符和移位運算符的操作有些陌生,所以重新複習一下
主要內容:
- 位操作淺析
- 位運算符
- 移位運算符
- 優先級
- 問題解析
- 取值範圍
位操作淺析
Java
可在整數類型(integral type
)數據上進行位(bit
)操作
整數類型:
- 字節型(
byte
,8
位) - 短整型(
short
,16
位) - 整型(
int
,32
位) - 長整型(
long
,64
位)
原碼,反碼和補碼
參考:
首先原碼,反碼和補碼都是基於二進制數進行的
對於正數而言,其原碼,反碼和補碼一致(無符號數就是正數)
在 Java
中的整數類型都是有符號數,即其最高位爲符號位(正數爲 0
,負數爲 1
)
默認情況下,二進制數就是原碼錶示,所以將十進制數 15
轉換爲二進制原碼就是(假定爲 8
位整數)
00001111
將十進制數 -15
轉換爲二進制原碼就是
10001111
原碼和反碼相互轉換規則:負數保留符號位不變,其它位按位取反
將十進制 -15
轉換爲二進制反碼就是
11110000
原碼和補碼相互轉換規則:負數符號位不變,其餘位求反再加 1
將十進制 -15
轉換爲二進制補碼就是
11110001
加減運算
在計算機中,使用補碼保存整數類型數據(因爲補碼格式有利於計算機進行移位運算)
加減運算時,補碼直接相加即可(符號位參與運算)
進制轉換
Java
不能直接表示二進制整數,但可以表示成八進制(以數字 0
開頭),十進制(沒有前置)和十六進制(以數字 0
和 字符 x
開頭),默認情況使用十進制計算
比如對於整數 15
來說,其八進制表示爲 017
,十六進制表示爲 0xf
通常情況下使用 十六進制 來表示 二進制
一個整型數據佔 4
個字節,共 32
位,那麼對於整型數 a = 15
來說,其二進制表示如下
// 前面共 28 個 0
0000...0001111
轉換爲十六進制,就是 0x0000000f
位運算符
Java
提供了 4
種位運算符
- 位與運算符(
bitwise and operator
):&
- 位或運算符(
bitwise inclusive or operator
):|
- 位異或運算符(
bitwise exclusive or operator
):^
- 位取反運算符(
bitwise invert operator
):~
這些運算符是在二進制補碼上進行操作
測試程序如下:
public static void main(String[] args) {
byte a = 15;
byte b = -15;
System.out.println(a & b);
System.out.println(a | b);
System.out.println(a ^ b);
System.out.println(~a);
System.out.println(~b);
}
一個字節數佔 8
位,將 a
,b
轉換爲二進制:
a = 0000 1111
b = 1111 0001
Note:計算機使用補碼錶示
位與運算符:僅當兩個操作數同一下標的值均爲 1
時,結果才爲 1
a & b = 0000 1111 & 1111 0001 = 0000 0001(補) = 0000 0001(原) = 1
位或運算符:只要兩個操作數同一下標的值有一個爲 1
時,結果就爲 1
a | b = 0000 1111 & 1111 0001 = 1111 1111(補) = 1000 0001(原) = -1
位異或運算符:只有兩個操作數同意下標的值不相等時,結果才爲 1
a ^ b = 0000 1111 ^ 1111 0001 = 1111 1110(補) = 1000 0010(原) = -2
位取反運算符:按位取反每一位
~a = ~0000 1111 = 1111 0000(補) = 1001 0000(原) = -16
~b = ~1111 0001 = 0000 1110(補) = 0000 1110(原) = 14
Note 1:byte
或者 short
類型數值進行位運算後,返回的是 int
類型數值(沒有找到資料說明在位運算之前是否已經進行了轉換,不過先將 a
,b
轉換爲 int
類型二進制再進行計算的結果和上面一致)
Note 2:位運算符的操作不排除符號位
移位運算符
Java
提供了 3
種移位運算符
- 左移運算符(
left shift operator
):<<
- 右移運算符(
right shift operator
):>>
- 無符號右移運算符(
unsigned right shift operator
):>>>
示例程序如下:
public static void main(String[] args) {
System.out.println("正數移位");
compute(15);
System.out.println("負數移位");
compute(-15);
}
public static void compute(int a) {
println(a << 3);
println(a << -61);
println(a << 35);
println(a >> 3);
println(a >> -61);
println(a >> 35);
println(a >>> 3);
println(a >>> -61);
println(a >>> 35);
}
public static void println(int n) {
System.out.println(n);
}
對於移位運算符而言,左側操作數表示要移動的二進制數,右側操作數表示要移動的位數
進行移位操作時,需要注意以下幾點:
對於
byte
或者short
類型數值,進行移位操作時,會先轉換爲int
類型,然後進行移位(如果是long
類型,則不變)對於右側操作數而言,在進行移位之前,先轉換爲二進制數(補碼)。如果左側數是
int
類型,則取右側操作數最右端5
位數值進行移動;如果是long
類型數值,則取右側操作數最右端6
位數值進行移動
左移運算符:數值位向左移動指定位數
15 << 3 = 0x0000000f << 3 = 0x00000078(補,原) = 120
15 << -61 = 0x0000000f << 0xffffffc3(左側是 int 類型,取右側 5 位) = 0x0000000f << 3 = 0x00000078(補,原) = 120
15 << 35 = 0x0000000f << 0x00000023(左側是 int 類型,取右側 5 位) = 0x0000000f << 3 = 0x00000078(補,原) = 120
-15 << 3 = 0xfffffff1 << 3 = 0xffffff88(補) = 0x80000078(原) = -120
-15 << -61 = 0xfffffff1 << 0xffffffc3(左側是 int 類型,取右側 5 位) = 0xfffffff1 << 3 = 0xffffff88(補) = 0x80000078(原) = -120
-15 << 35 = 0xfffffff1 << 0x00000023(左側是 int 類型,取右側 5 位) = 0xfffffff1 << 3 = 0xffffff88(補) = 0x80000078(原) = -120
右移運算符:數字位向右移動指定位數(如果左操作數是正數,高位補 0
;如果是負數,高位補 1
)
15 >> 3 = 0x0000000f >> 3 = 0x00000001 = 1
-15 >> 3 = 0xfffffff1 >> 3 = 0xfffffffe(補) = 0x80000002(原) = -2
無符號右移運算符:功能和右移運算符一樣,不過無論正負,高位均補 0
15 >>> 3 = 0x0000000f >>> 3 = 0x00000001 = 1
-15 >> 3 = 0xfffffff1 >>> 3 = 0x1ffffffe(補,原) = 2^29 - 2 = 536870910
Note 1:移位運算時,從符號位開始操作
Note 2:由結果可知,左移一位相當於乘以2,右移一位相當於除以 2
優先級
參考:運算符優先級
Java
運算符優先級如下圖所示:
由圖中可知,位運算符和移位運算符的優先級從左到右如下:
~,<<,>>,>>>,&,^,|
問題解析
之前學習類 BitSet
時遇到了很多的位運算,但是有一些操作沒搞明白,下面是我總結的一些問題和解答
問題一:移動位數超過其精度如何解決
- 解答:在進行移位操作之前,先將右側數值轉換成二進制(其實在計算機內部就是以二進制補碼保存的)。如果左側操作數爲
int
類型數值,那麼取右側操作數的最右端5
位進行移位;或者左側操作數是long
類型數值,取右側操作數的最右端6
位進行移位
- 解答:在進行移位操作之前,先將右側數值轉換成二進制(其實在計算機內部就是以二進制補碼保存的)。如果左側操作數爲
問題二:移動位數爲負如何解決
- 解答:和問題一的解答一樣,取右側操作數的最右端
5/6
位進行移位
- 解答:和問題一的解答一樣,取右側操作數的最右端
問題三:如何解決符號位的問題
- 解答:計算機以二進制補碼形式保存整型數值。
- 無論是加減 / 位運算 / 移位運算,符號位均參與其中
- 解答:計算機以二進制補碼形式保存整型數值。
問題四:已知起始下標
fromIndex
和 終止下標toIndex
,如何在位集中設定這一段連續區間爲true
解答:以
int
類型爲例,位集長度爲32
位,假設fromIndex = 3
,toIndex = 10
,那麼示例程序如下:public static final int WORD_MASK = 0xffffffff; public static void main(String[] args) { int fromIndex = 3; int toIndex = 10; int firstWordMask = WORD_MASK << fromIndex; int lastWordMask = WORD_MASK >>> -toIndex; int res = (firstWordMask & lastWordMask); System.out.println(Integer.toBinaryString(firstWordMask)); System.out.println(Integer.toBinaryString(lastWordMask)); System.out.println(Integer.toBinaryString(res)); }
要設定位集中連續區間位值爲
true
,可以設定一個輔助常量WORD_MASK
,保證每個位均爲true
定義起始下標
fromIndex = 3
,結束下標toIndex = 10
計算
firstWordMask
,使得WORD_MASK
向左移動fromIndex
個位置,低位補0
,結果使得區間[0-fromIndex)
的位值爲0
計算
lastWordMask
,使得WORD_MASK
向右移動n
個位置,高位補0
,結果使得區間(toIndex-32]
的位值爲0
最後進行位與操作,得到區間
[fromIndex-toIndex]
的位值爲true
Note:對於
int
值而言,設a = 3
,則取-3
的後5
位就是(32-3)=29
;若是long
值,取-3
的後6
位就是(64-3)=61
取值範圍
字節類型佔 8
位,其中最高位爲符號位,所以其取值範圍爲 [-2^7-1,2^7-1] = [-127,127]
,其中 0
有兩種表示方式
00000000 或者 10000000
將 10000000
當作 -128
,則 字節類型的取值爲 [-128,127]
public static final byte MIN_VALUE = -128;
public static final byte MAX_VALUE = 127;
示例程序如下:
public static void main(String[] args) {
System.out.println(Integer.toBinaryString(Byte.toUnsignedInt(Byte.MAX_VALUE)));
System.out.println(Integer.toBinaryString(Byte.toUnsignedInt(Byte.MIN_VALUE)));
}
同理,短整型的取值範圍爲 [-2^15,2^15-1]
,整型的取值範圍爲 [-2^31,2^31-1]
,長整型的取值範圍爲 [-2^63,2^63-1]
// Short.java
public static final short MIN_VALUE = -32768;
public static final short MAX_VALUE = 32767;
// Integer.java
@Native public static final int MIN_VALUE = 0x80000000;
@Native public static final int MAX_VALUE = 0x7fffffff;
// Long.java
@Native public static final long MIN_VALUE = 0x8000000000000000L;
@Native public static final long MAX_VALUE = 0x7fffffffffffffffL;
示例程序如下:
public static void main(String[] args) {
System.out.println(Integer.toBinaryString(Short.toUnsignedInt(Short.MAX_VALUE)));
System.out.println(Integer.toBinaryString(Short.toUnsignedInt(Short.MIN_VALUE)));
System.out.println(Integer.toBinaryString(Integer.MAX_VALUE));
System.out.println(Integer.toBinaryString(Integer.MIN_VALUE));
System.out.println(Long.toBinaryString(Long.MAX_VALUE));
System.out.println(Long.toBinaryString(Long.MIN_VALUE));
}