JAVA字符謎題13:不勞而獲

字號(hào):

下面的程序?qū)⒋蛴∫粋€(gè)單詞,其第一個(gè)字母是由一個(gè)隨機(jī)數(shù)生成器來選擇的。請(qǐng)描述該程序的行為:
    import java.util.Random;
    public class Rhymes {
     private static Random rnd = new Random();
     public static void main(String[] args) {
     StringBuffer word = null;
     switch(rnd.nextInt(2)) {
     case 1: word = new StringBuffer(’P’);
     case 2: word = new StringBuffer(’G’);
     default: word = new StringBuffer(’M’);
     }
     word.append(’a’);
     word.append(’i’);
     word.append(’n’);
     System.out.println(word);
     }
    }
    乍一看,這個(gè)程序可能會(huì)在一次又一次的運(yùn)行中,以相等的概率打印出Pain,Gain或 Main??雌饋碓摮绦驎?huì)根據(jù)隨機(jī)數(shù)生成器所選取的值來選擇單詞的第一個(gè)字母:0選M,1選P,2選G。謎題的題目也許已經(jīng)給你提供了線索,它實(shí)際上既不會(huì)打印Pain,也不會(huì)打印Gain。也許更令人吃驚的是,它也不會(huì)打印Main,并且它的行為不會(huì)在一次又一次的運(yùn)行中發(fā)生變化,它總是在打印ain。
    有三個(gè)bug湊到一起引發(fā)了這種行為。你完全沒有發(fā)現(xiàn)它們嗎?第一個(gè)bug是所選取的隨機(jī)數(shù)使得switch語句只能到達(dá)其三種情況中的兩種。Random.nextInt(int)的規(guī)范描述道:“返回一個(gè)偽隨機(jī)的、均等地分布在從0(包括)到指定的數(shù)值(不包括)之間的一個(gè)int數(shù)值”[Java-API]。這意味著表達(dá)式rnd.nextInt(2)可能的取值只有0和1,Switch語句將永遠(yuǎn)也到不了case 2分支,這表示程序?qū)⒂肋h(yuǎn)不會(huì)打印Gain。nextInt的參數(shù)應(yīng)該是3而不是2。
    這是一個(gè)相當(dāng)常見的問題源,被熟知為“柵欄柱錯(cuò)誤(fencepost error)”。這個(gè)名字來源于對(duì)下面這個(gè)問題最常見的但卻是錯(cuò)誤的答案,如果你要建造一個(gè)100英尺長的柵欄,其柵欄柱間隔為10英尺,那么你需要多少根柵欄柱呢?11根或9根都是正確答案,這取決于是否要在柵欄的兩端樹立柵欄柱,但是10根卻是錯(cuò)誤的。要當(dāng)心柵欄柱錯(cuò)誤,每當(dāng)你在處理長度、范圍或模數(shù)的時(shí)候,都要仔細(xì)確定其端點(diǎn)是否應(yīng)該被包括在內(nèi),并且要確保你的代碼的行為要與其相對(duì)應(yīng)。
    第二個(gè)bug是在不同的情況(case)中沒有任何break語句。不論switch表達(dá)式為何值,該程序都將執(zhí)行其相對(duì)應(yīng)的case以及所有后續(xù)的case[JLS 14.11]。因此,盡管每一個(gè)case都對(duì)變量word賦了一個(gè)值,但是總是最后一個(gè)賦值勝出,覆蓋了前面的賦值。最后一個(gè)賦值將總是最后一種情況(default),即new StringBuffer{’M’}。這表明該程序?qū)⒖偸谴蛴ain,而從來不打印Pain或Gain。
    在switch的各種情況中缺少break語句是非常常見的錯(cuò)誤。從5.0版本起,javac提供了-Xlint:fallthrough標(biāo)志,當(dāng)你忘記在一個(gè)case與下一個(gè)case之間添加break語句是,它可以生成警告信息。不要從一個(gè)非空的case向下進(jìn)入了另一個(gè)case。這是一種拙劣的風(fēng)格,因?yàn)樗⒉怀S?,因此?huì)誤導(dǎo)讀者。十次中有九次它都會(huì)包含錯(cuò)誤。如果Java不是模仿C建模的,那么它倒是有可能不需要break。對(duì)語言設(shè)計(jì)者的教訓(xùn)是:應(yīng)該考慮提供一個(gè)結(jié)構(gòu)化的switch語句。
    最后一個(gè),也是最微妙的一個(gè)bug是表達(dá)式new StringBuffer(’M’)可能沒有做哪些你希望它做的事情。你可能對(duì)StringBuffer(char)構(gòu)造器并不熟悉,這很容易解釋:它壓根就不存在。StringBuffer有一個(gè)無參數(shù)的構(gòu)造器,一個(gè)接受一個(gè)String作為字符串緩沖區(qū)初始內(nèi)容的構(gòu)造器,以及一個(gè)接受一個(gè)int作為緩沖區(qū)初始容量的構(gòu)造器。在本例中,編譯器會(huì)選擇接受int的構(gòu)造器,通過拓寬原始類型轉(zhuǎn)換把字符數(shù)值’M’轉(zhuǎn)換為一個(gè)int數(shù)值77[JLS 5.1.2]。換句話說,new StringBuffer(’M’)返回的是一個(gè)具有初始容量77的空的字符串緩沖區(qū)。該程序余下的部分將字符a、i和n添加到了這個(gè)空字符串緩沖區(qū)中,并打印出該字符串緩沖區(qū)那總是ain的內(nèi)容。
    為了避免這類問題,不管在什么時(shí)候,都要盡可能使用熟悉的慣用法和API。如果你必須使用不熟悉的API,那么請(qǐng)仔細(xì)閱讀其文檔。在本例中,程序應(yīng)該使用常用的接受一個(gè)String的StringBuffer構(gòu)造器。
    下面是該程序訂正了這三個(gè)bug之后的正確版本,它將以均等的概率打印Pain、Gain和Main:
    import java.util.Random;
    public class Rhymes1 {
     private static Random rnd = new Random();
     public static void main(String[] args) {
     StringBuffer word = null;
     switch(rnd.nextInt(3)) {
     case 1:
     word = new StringBuffer("P");
     break;
     case 2:
     word = new StringBuffer("G");
     break;
     default:
     word = new StringBuffer("M");
     break;
     }
     word.append(’a’);
     word.append(’i’);
     word.append(’n’);
     System.out.println(word);
     }
    }
    盡管這個(gè)程序訂正了所有的bug,它還是顯得過于冗長了。下面是一個(gè)更優(yōu)雅的版本:
    import java.util.Random;
    public class Rhymes2 {
     private static Random rnd = new Random();
     public static void main(String[] args) {
     System.out.println("PGM".charAt(rnd.nextInt(3)) + "ain");
     }
    }
    下面是一個(gè)更好的版本。盡管它稍微長了一點(diǎn),但是它更加通用。它不依賴于所有可能的輸出只是在它們的第一個(gè)字符上有所不同的這個(gè)事實(shí):
    import java.util.Random;
    public class Rhymes3 {
     public static void main(String[] args) {
     String a[] = {"Main","Pain","Gain"};
     System.out.println(randomElement(a));
     }
     private static Random rnd = new Random();
     private static String randomElement(String[] a){
     return a[rnd.nextInt(a.length)];
     }
    }
    總結(jié)一下:首先,要當(dāng)心柵欄柱錯(cuò)誤。其次,牢記在 switch 語句的每一個(gè) case 中都放置一條 break 語句。第三,要使用常用的慣用法和 API,并且當(dāng)你在離開老路子的時(shí)候,一定要參考相關(guān)的文檔。第四,一個(gè) char 不是一個(gè) String,而是更像一個(gè) int。最后,要提防各種詭異的謎題。