大資料Java基礎——移位運算的真實剖析 (一)
拋磚引玉:
Java 中定義了 3 種移位運算子,分別是左移運算子“<<”、右移運算子“>>”和無符號右移運算子“>>>”,對於移位運算,移位運算兩邊的運算元要求為整型,即 byte、short、char、 int 和 long型別,或者通過拆箱轉換後為整型。當運算元的型別為 byte、short 或 char 型別時, 會自動提升為 int 型別,運算的結果也為 int型別。對於移位運算,人們對其“誤會”實在太深了……
超過自身位數的移位
我們知道,int 型別佔用 4 位元組,32 位,而 long 型別佔用 8 位元組,64 位。那麼,如果將 int 型別(long 型別)移動超過 31位(63 位)便失去了意義,因為用通俗的話來說,就是“全移 走了”。不過幸運的是,當左側運算元為 int 型別(byte、char 與 short型別自動提升為 int 型別) 或 long 型別時,如果右側運算元大於 31 或 63,系統做了相關處理。
是怎麼處理的呢?普遍都是這樣認為的:如果左側運算元為 int 型別(包括提升後為 int 類 型),會對右側運算元進行除數為 32 的求餘運算,如果左側運算元為long 型別,會對右側操作 數進行除數為64的求餘運算,例如:
int i = 20;
int j = 30;
i = i << 3;
j = j >> 70;
結果會先進行求餘運算:
3 % 32 //結果為3
70 % 64 //結果為6
因此,實際右側的運算元是 3 與 6,而不是 3 與 70。
90%的 Java 程式設計師都持上述觀點,然而,遺憾的是,這個想法是不正確的。
需要注意的是:右側的運算元可以是任意的整型數值,只要該值沒有超過型別變數的取值 範圍就可以。那麼,當右側運算元為負值時,例如:
int i= 28;
i = 28 << -4;
按照上面的觀點,首先對 32 求餘,即:
-4 % 32
結果還是−4,現在問題來了,向左移動−4 位,該怎樣移動?
實際上,當左側運算元為 int 型別時(包括提升後為 int 型別),右側運算元只有低 5位是 有效的(低 5 位的範圍為 0~31) ,也就是說可以看作右側運算元會先與掩碼 0x1f 做與運算(&) ,然後左側運算元再移動相應的位數。類似地,當左側運算元為 long 型別時,右側運算元只有低 6 位是有效的,可以看作右側運算元先與掩碼 0x3f做與運算,然後再移動相應的位數。例如, 假設有如下的賦值運算:
int i = 5 << -10;
−10 的補碼為: 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 0 1 1 0
取其低 5 位,結果為: 1 0 1 1 0 這個值就是 22,也就是相當於:
int i = 5 << 22;
因此,不要把移位運算右側的運算元與求餘運算聯絡在一起,那是不正確的。
移位運算與乘除運算
由於資料採用二進位制來表示,因此就會普遍存在這樣的想法:左移一位就相當於乘以 2, 而右移一位就相當於除以 2,這種想法正確嗎?下面的程式將給予驗證。
【例 】移位與乘除
1. package chapter2;
2.
3. public class ShiftOperation {
4. public static void main(String[] args) {
5. testShift(10); //正偶數
6. testShift(-10); //負偶數
7. testShift(0); //0
8. testShift(9); //正奇數
9. testShift(-9); //負奇數
10. }
11.
12. public static void testShift(int value) {
13. int multiply = value * 2;
14. int divide = value / 2;
15. int leftShift = value << 1;
16. int rightShift = value >> 1;
17. System.out.println(value + "*2=" + multiply);
18. System.out.println(value + "/2=" + divide);
19. System.out.println(value + "<<1=" + leftShift);
20. System.out.println(value + ">>1=" + rightShift);
21. System.out.print("測試結果:" + value + "*2="+ value + "<<1"); 22. if (multiply == leftShift) {
23. System.out.println("通過!");
24. } else {
25. System.out.println("不通過!");
26. }
27. System.out.print("測試結果:" + value + "/2="+ value + ">>1"); 28. if (divide == rightShift) {
29. System.out.println("通過!");
30. } else {
31. System.out.println("不通過!");
32. }
33. }
34.
本程式選取了幾個數值,然後分別計算該數值乘以 2、除以 2、左移一位與右移一位的值(第 13~16 行),接下來比較乘以 2 與左移一位(第22~26 行)、除以 2 與右移一位(第 28~32 行) 是否相等。程式執行結果如下:
10*2=20
10/2=5
10<<1=20
10>>1=5
測試結果:10*2=10<<1通過!
測試結果:10/2=10>>1通過!
-10*2=-20
-10/2=-5
-10<<1=-20
-10>>1=-5
測試結果:-10*2=-10<<1通過!
測試結果:-10/2=-10>>1通過!
0*2=0
0/2=0
0<<1=0
0>>1=0
測試結果:0*2=0<<1通過!
測試結果:0/2=0>>1通過!
9*2=18
9/2=4
9<<1=18
9>>1=4
測試結果:9*2=9<<1通過!
測試結果:9/2=9>>1通過!
-9*2=-18
-9/2=-4
-9<<1=-18
-9>>1=-5
測試結果:-9*2=-9<<1通過!
測試結果:-9/2=-9>>1不通過!
輸出結果除了最後一行以外,其餘都是相等的。那麼最後一行有什麼特別嗎? 這要從相除的舍入模式說起。在 Java中,當兩個運算元都是整型的時候,結果也是整型的 (假設沒有發生 ArithmeticException 異常)。如果不能整除,則結果是向 0 舍入的,也就是說,
向靠近 0 的方向取值。例如在本程式中,表示式:
9 / 2
的值為 4.5,介於 4 與 5 之間,由於 4 更靠近 0,所以向 0 舍入,結果為 4,這相當於向下舍入, 而表示式:
-9 / 2
的值為−4.5,介於−4 與−5 之間,向 0 舍入為−4,這相當於向上舍入。而對於移位運算來說,表示式:
9 >> 1
的值為 4,相當於向下舍入,而表示式:
-9 >> 1
的值為−5,還是相當於向下舍入,因此,不同的數值出現了。由於相乘運算與整除的時候不涉及如上舍入模式的問題,所以,值是相等的。但是,一旦不能整除,就會涉及舍入模式,這樣, 結果就會存在差異了。表 2-4 給出了具體的情況。
所以,乘以 2n與左移 n 位的值是相等的,如果可以整除,除以 2n與右移 n 位的值也是相等 的。如果不能整除,當被除數為正數時,除以 2n與右移n 位的值相等,當被除數為負數時,除 以 2n與右移 n 位的值則是不相等的。
注意 以上所指的右移,如無特殊說明,均指有符號右移(>>)。