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