請(qǐng)?zhí)峁┮粋€(gè)對(duì)i的聲明,將下面的循環(huán)轉(zhuǎn)變?yōu)橐粋€(gè)無限循環(huán):
while (i != 0) {
i >>>= 1;
}
回想一下,>>>=是對(duì)應(yīng)于無符號(hào)右移操作符的賦值操作符。0被從左移入到由移位操作而空出來的位上,即使被移位的負(fù)數(shù)也是如此。
這個(gè)循環(huán)比前面三個(gè)循環(huán)要稍微復(fù)雜一點(diǎn),因?yàn)槠溲h(huán)體非空。在其循環(huán)題中,i的值由它右移一位之后的值所替代。為了使移位合法,i必須是一個(gè)整數(shù)類型(byte、char、short、int或long)。無符號(hào)右移操作符把0從左邊移入,因此看起來這個(gè)循環(huán)執(zhí)行迭代的次數(shù)與的整數(shù)類型所占據(jù)的位數(shù)相同,即64次。如果你在循環(huán)的前面放置如下的聲明,那么這確實(shí)就是將要發(fā)生的事情:
long i = -1; // -1L has all 64 bits set
你怎樣才能將它轉(zhuǎn)變?yōu)橐粋€(gè)無限循環(huán)呢?解決本謎題的關(guān)鍵在于>>>=是一個(gè)復(fù)合賦值操作符。(復(fù)合賦值操作符包括*=、/=、%=、+=、-=、<<=、>>=、>>>=、&=、^=和|=。)有關(guān)混合操作符的一個(gè)不幸的事實(shí)是,它們可能會(huì)自動(dòng)地執(zhí)行窄化原始類型轉(zhuǎn)換[JLS 15.26.2],這種轉(zhuǎn)換把一種數(shù)字類型轉(zhuǎn)換成了另一種更缺乏表示能力的類型。窄化原始類型轉(zhuǎn)換可能會(huì)丟失級(jí)數(shù)的信息,或者是數(shù)值的精度[JLS 5.1.3]。
讓我們更具體一些,假設(shè)你在循環(huán)的前面放置了下面的聲明:
short i = -1;
因?yàn)閕的初始值((short)0xffff)是非0的,所以循環(huán)體會(huì)被執(zhí)行。在執(zhí)行移位操作時(shí),第一步是將i提升為int類型。所有算數(shù)操作都會(huì)對(duì)short、byte和char類型的操作數(shù)執(zhí)行這樣的提升。這種提升是一個(gè)拓寬原始類型轉(zhuǎn)換,因此沒有任何信息會(huì)丟失。這種提升執(zhí)行的是符號(hào)擴(kuò)展,因此所產(chǎn)生的int數(shù)值是0xffffffff。然后,這個(gè)數(shù)值右移1位,但不使用符號(hào)擴(kuò)展,因此產(chǎn)生了int數(shù)值0x7fffffff。最后,這個(gè)數(shù)值被存回到i中。為了將int數(shù)值存入short變量,Java執(zhí)行的是可怕的窄化原始類型轉(zhuǎn)換,它直接將高16位截掉。這樣就只剩下(short)oxffff了,我們又回到了開始處。循環(huán)的第二次以及后續(xù)的迭代行為都是一樣的,因此循環(huán)將永遠(yuǎn)不會(huì)終止。
如果你將i聲明為一個(gè)short或byte變量,并且初始化為任何負(fù)數(shù),那么這種行為也會(huì)發(fā)生。如果你聲明i為一個(gè)char,那么你將無法得到無限循環(huán),因?yàn)閏har是無符號(hào)的,所以發(fā)生在移位之前的拓寬原始類型轉(zhuǎn)換不會(huì)執(zhí)行符號(hào)擴(kuò)展。
總之,不要在short、byte或char類型的變量之上使用復(fù)合賦值操作符。因?yàn)檫@樣的表達(dá)式執(zhí)行的是混合類型算術(shù)運(yùn)算,它容易造成混亂。更糟的是,它們執(zhí)行將隱式地執(zhí)行會(huì)丟失信息的窄化轉(zhuǎn)型,其結(jié)果是災(zāi)難性的。
對(duì)語言設(shè)計(jì)者的教訓(xùn)是語言不應(yīng)該自動(dòng)地執(zhí)行窄化轉(zhuǎn)換。還有一點(diǎn)值得好好爭(zhēng)論的是,Java是否應(yīng)該禁止在short、byte和char變量上使用復(fù)合賦值操作符。
while (i != 0) {
i >>>= 1;
}
回想一下,>>>=是對(duì)應(yīng)于無符號(hào)右移操作符的賦值操作符。0被從左移入到由移位操作而空出來的位上,即使被移位的負(fù)數(shù)也是如此。
這個(gè)循環(huán)比前面三個(gè)循環(huán)要稍微復(fù)雜一點(diǎn),因?yàn)槠溲h(huán)體非空。在其循環(huán)題中,i的值由它右移一位之后的值所替代。為了使移位合法,i必須是一個(gè)整數(shù)類型(byte、char、short、int或long)。無符號(hào)右移操作符把0從左邊移入,因此看起來這個(gè)循環(huán)執(zhí)行迭代的次數(shù)與的整數(shù)類型所占據(jù)的位數(shù)相同,即64次。如果你在循環(huán)的前面放置如下的聲明,那么這確實(shí)就是將要發(fā)生的事情:
long i = -1; // -1L has all 64 bits set
你怎樣才能將它轉(zhuǎn)變?yōu)橐粋€(gè)無限循環(huán)呢?解決本謎題的關(guān)鍵在于>>>=是一個(gè)復(fù)合賦值操作符。(復(fù)合賦值操作符包括*=、/=、%=、+=、-=、<<=、>>=、>>>=、&=、^=和|=。)有關(guān)混合操作符的一個(gè)不幸的事實(shí)是,它們可能會(huì)自動(dòng)地執(zhí)行窄化原始類型轉(zhuǎn)換[JLS 15.26.2],這種轉(zhuǎn)換把一種數(shù)字類型轉(zhuǎn)換成了另一種更缺乏表示能力的類型。窄化原始類型轉(zhuǎn)換可能會(huì)丟失級(jí)數(shù)的信息,或者是數(shù)值的精度[JLS 5.1.3]。
讓我們更具體一些,假設(shè)你在循環(huán)的前面放置了下面的聲明:
short i = -1;
因?yàn)閕的初始值((short)0xffff)是非0的,所以循環(huán)體會(huì)被執(zhí)行。在執(zhí)行移位操作時(shí),第一步是將i提升為int類型。所有算數(shù)操作都會(huì)對(duì)short、byte和char類型的操作數(shù)執(zhí)行這樣的提升。這種提升是一個(gè)拓寬原始類型轉(zhuǎn)換,因此沒有任何信息會(huì)丟失。這種提升執(zhí)行的是符號(hào)擴(kuò)展,因此所產(chǎn)生的int數(shù)值是0xffffffff。然后,這個(gè)數(shù)值右移1位,但不使用符號(hào)擴(kuò)展,因此產(chǎn)生了int數(shù)值0x7fffffff。最后,這個(gè)數(shù)值被存回到i中。為了將int數(shù)值存入short變量,Java執(zhí)行的是可怕的窄化原始類型轉(zhuǎn)換,它直接將高16位截掉。這樣就只剩下(short)oxffff了,我們又回到了開始處。循環(huán)的第二次以及后續(xù)的迭代行為都是一樣的,因此循環(huán)將永遠(yuǎn)不會(huì)終止。
如果你將i聲明為一個(gè)short或byte變量,并且初始化為任何負(fù)數(shù),那么這種行為也會(huì)發(fā)生。如果你聲明i為一個(gè)char,那么你將無法得到無限循環(huán),因?yàn)閏har是無符號(hào)的,所以發(fā)生在移位之前的拓寬原始類型轉(zhuǎn)換不會(huì)執(zhí)行符號(hào)擴(kuò)展。
總之,不要在short、byte或char類型的變量之上使用復(fù)合賦值操作符。因?yàn)檫@樣的表達(dá)式執(zhí)行的是混合類型算術(shù)運(yùn)算,它容易造成混亂。更糟的是,它們執(zhí)行將隱式地執(zhí)行會(huì)丟失信息的窄化轉(zhuǎn)型,其結(jié)果是災(zāi)難性的。
對(duì)語言設(shè)計(jì)者的教訓(xùn)是語言不應(yīng)該自動(dòng)地執(zhí)行窄化轉(zhuǎn)換。還有一點(diǎn)值得好好爭(zhēng)論的是,Java是否應(yīng)該禁止在short、byte和char變量上使用復(fù)合賦值操作符。