1. 程式人生 > >大資料Java基礎——移位運算的真實剖析 (一)

大資料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 位的值則是不相等的。

  注意 以上所指的右移,如無特殊說明,均指有符號右移(>>)。