標籤:自身 操作 常量 千萬 語言 重複 語義 應該 使用
與謎題26中的程式一樣,下面的程式也包含了一個記錄在終止前有多少次迭代的迴圈。與那個程式不同的是,這個程式使用的是左移操作符(<<)。你的任務照舊是要指出這個程式將列印什麼。當你閱讀這個程式時,請記住 Java 使用的是基於2的補碼的二進位算術運算,因此-1在任何有符號的整數類型中(byte、short、int或long)的表示都是所有的位被置位:
public class Shifty { public static void main(String[] args) { int i = 0; while (-1 << i != 0) i++; System.out.println(i); }}
常量-1是所有32位都被置位的int數值(0xffffffff)。左移操作符將0移入到由移位所空出的右邊的最低位,因此運算式(-1 << i)將i最右邊的位設定為0,並保持其餘的32 - i位為1。很明顯,這個迴圈將完成32次迭代,因為-1 << i對任何小於32的i來說都不等於0。你可能期望終止條件測試在i等於32時返回false,從而使程式列印32,但是它列印的並不是32。實際上,它不會列印任何東西,而是進入了一個無限迴圈。
問題在於(-1 << 32)等於-1而不是0,因為移位操作符之使用其右運算元的低5位作為移位長度。或者是低6位,如果其左運算元是一個long類數值[JLS 15.19]。
這條規則作用於全部的三個移位操作符:<<、>>和>>>。移位長度總是介於0到31之間,如果左運算元是long類型的,則介於0到63之間。這個長度是對32取餘的,如果左運算元是long類型的,則對64取餘。如果試圖對一個int數值移位32位,或者是對一個long數值移位64位,都只能返回這個數值自身的值。沒有任何移位長度可以讓一個int數值丟棄其所有的32位,或者是讓一個long數值丟棄其所有的64位。
幸運的是,有一個非常容易的方式能夠訂正該問題。我們不是讓-1重複地移位不同的移位長度,而是將前一次移位操作的結果儲存起來,並且讓它在每一次迭代時都向左再移1位。下面這個版本的程式就可以列印出我們所期望的32:
public class Shifty { public static void main(String[] args) { int distance = 0; for (int val = -1; val != 0; val <<= 1) distance++; System.out.println(distance); }}
這個訂正過的程式說明了一條普遍的原則:如果可能的話,移位長度應該是常量。如果移位長度緊盯著你不放,那麼你讓其值超過31,或者如果左運算元是long類型的,讓其值超過63的可能性就會大大降低。當然,你並不可能總是可以使用常量的移位長度。當你必須使用一個非常量的移位長度時,請確保你的程式可以應付這種容易產生問題的情況,或者壓根就不會碰到這種情況。
前面提到的移位操作符的行為還有另外一個令人震驚的結果。很多程式員都希望具有負的移位長度的右移操作符可以起到左移操作符的作用,反之亦然。但是情況並非如此。右移操作符總是起到右移的作用,而左移操作符也總是起到左移的作用。負的移位長度通過只保留低5位而剔除其他位的方式被轉換成了正的移位長度——如果左運算元是long類型的,則保留低6位。因此,如果要將一個int數值左移,其移位長度為-1,那麼移位的效果是它被左移了31位。
總之,移位長度是對32取餘的,或者如果左運算元是long類型的,則對64取餘。因此,使用任何移位操作符和移位長度,都不可能將一個數值的所有位全部移走。同時,我們也不可能用右移操作符來執行左移操作,反之亦然。如果可能的話,請使用常量的移位長度,如果移位長度不能設為常量,那麼就要千萬當心。
語言設計者可能應該考慮將移位長度限制在從0到以位為單位的類型尺寸的範圍內,並且修改移位長度為類型尺寸時的語義,讓其返回0。儘管這可以避免在本謎題中所展示的混亂情況,但是它可能會帶來負面的執行結果,因為Java的移位操作符的語義正是許多處理器上的移位指令的語義。
謎題27:變幻莫測的i值