JAVA循環(huán)謎題27:變幻莫測的i值

字號:

與謎題26中的程序一樣,下面的程序也包含了一個記錄在終止前有多少次迭代的循環(huán)。與那個程序不同的是,這個程序使用的是左移操作符(<<)。你的任務(wù)照舊是要指出這個程序?qū)⒋蛴∈裁?。?dāng)你閱讀這個程序時,請記住 Java 使用的是基于2的補碼的二進制算術(shù)運算,因此-1在任何有符號的整數(shù)類型中(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數(shù)值(0xffffffff)。左移操作符將0移入到由移位所空出的右邊的最低位,因此表達式(-1 << i)將i最右邊的位設(shè)置為0,并保持其余的32 - i位為1。很明顯,這個循環(huán)將完成32次迭代,因為-1 << i對任何小于32的i來說都不等于0。你可能期望終止條件測試在i等于32時返回false,從而使程序打印32,但是它打印的并不是32。實際上,它不會打印任何東西,而是進入了一個無限循環(huán)。
    問題在于(-1 << 32)等于-1而不是0,因為移位操作符之使用其右操作數(shù)的低5位作為移位長度?;蛘呤堑?位,如果其左操作數(shù)是一個long類數(shù)值[JLS 15.19]。
    這條規(guī)則作用于全部的三個移位操作符:<<、>>和>>>。移位長度總是介于0到31之間,如果左操作數(shù)是long類型的,則介于0到63之間。這個長度是對32取余的,如果左操作數(shù)是long類型的,則對64取余。如果試圖對一個int數(shù)值移位32位,或者是對一個long數(shù)值移位64位,都只能返回這個數(shù)值自身的值。沒有任何移位長度可以讓一個int數(shù)值丟棄其所有的32位,或者是讓一個long數(shù)值丟棄其所有的64位。
    幸運的是,有一個非常容易的方式能夠訂正該問題。我們不是讓-1重復(fù)地移位不同的移位長度,而是將前一次移位操作的結(jié)果保存起來,并且讓它在每一次迭代時都向左再移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);
     }
    }
    這個訂正過的程序說明了一條普遍的原則:如果可能的話,移位長度應(yīng)該是常量。如果移位長度緊盯著你不放,那么你讓其值超過31,或者如果左操作數(shù)是long類型的,讓其值超過63的可能性就會大大降低。當(dāng)然,你并不可能總是可以使用常量的移位長度。當(dāng)你必須使用一個非常量的移位長度時,請確保你的程序可以應(yīng)付這種容易產(chǎn)生問題的情況,或者壓根就不會碰到這種情況。
    前面提到的移位操作符的行為還有另外一個令人震驚的結(jié)果。很多程序員都希望具有負(fù)的移位長度的右移操作符可以起到左移操作符的作用,反之亦然。但是情況并非如此。右移操作符總是起到右移的作用,而左移操作符也總是起到左移的作用。負(fù)的移位長度通過只保留低5位而剔除其他位的方式被轉(zhuǎn)換成了正的移位長度——如果左操作數(shù)是long類型的,則保留低6位。因此,如果要將一個int數(shù)值左移,其移位長度為-1,那么移位的效果是它被左移了31位。
    總之,移位長度是對32取余的,或者如果左操作數(shù)是long類型的,則對64取余。因此,使用任何移位操作符和移位長度,都不可能將一個數(shù)值的所有位全部移走。同時,我們也不可能用右移操作符來執(zhí)行左移操作,反之亦然。如果可能的話,請使用常量的移位長度,如果移位長度不能設(shè)為常量,那么就要千萬當(dāng)心。
    語言設(shè)計者可能應(yīng)該考慮將移位長度限制在從0到以位為單位的類型尺寸的范圍內(nèi),并且修改移位長度為類型尺寸時的語義,讓其返回0。盡管這可以避免在本謎題中所展示的混亂情況,但是它可能會帶來負(fù)面的執(zhí)行結(jié)果,因為Java的移位操作符的語義正是許多處理器上的移位指令的語義。