謎題 12:ABC
這個謎題要問的是一個悅耳的問題,下面的程序將打印什麼呢?
public class ABC{
public static void main(String[] args){
String letters = "ABC";
char[] numbers = {'1', '2', '3'};
System.out.println(letters + " easy as " + numbers);
}
}
可能大家希望這個程序打印出 ABC easy as 123。遺憾的是,它沒有。如果你運
行它,就會發現它打印的是諸如 ABC easy as [C@16f0472 之類的東西。爲什麼
這個輸出會如此醜陋?
儘管 char 是一個整數類型,但是許多類庫都對其進行了特殊處理,因爲 char
數值通常表示的是字符而不是整數。例如,將一個 char 數值傳遞給 println 方
法會打印出一個 Unicode 字符而不是它的數字代碼。字符數組受到了相同的特殊
處理:println 的 char[]重載版本會打印出數組所包含的所有字符,而
String.valueOf和StringBuffer.append的char[]重載版本的行爲也是類似的。
然而,字符串連接操作符在這些方法中沒有被定義。該操作符被定義爲先對它的
兩個操作數執行字符串轉換,然後將產生的兩個字符串連接到一起。對包括數組
在內的對象引用的字符串轉換定義如下[JLS 15.18.1.1]:
如果引用爲 null,它將被轉換成字符串"null"。否則,該轉換的執行就像是不
用任何參數調用該引用對象的 toString 方法一樣;但是如果調用 toString 方法
的結果是 null,那麼就用字符串"null"來代替。
那麼,在一個非空 char 數組上面調用 toString 方法會產生什麼樣的行爲呢?數
組是從 Object 那裏繼承的 toString 方法[JLS 10.7],規範中描述到:“返回一
個字符串,它包含了該對象所屬類的名字,'@'符號,以及表示對象散列碼的一
個無符號十六進制整數”[Java-API]。有關 Class.getName 的規範描述到:在
char[]類型的類對象上調用該方法的結果爲字符串"[C"。將它們連接到一起就形
成了在我們的程序中打印出來的那個醜陋的字符串。
有兩種方法可以訂正這個程序。你可以在調用字符串連接操作之前,顯式地將一
個數組轉換成一個字符串:
System.out.println(letters + " easy as " +
String.valueOf(numbers));
或者,你可以將 System.out.println 調用分解爲兩個調用,以利用 println 的
char[]重載版本:
System.out.print(letters + " easy as ");
System.out.println(numbers);
請注意,這些訂正只有在你調用了 valueOf 和 println 方法正確的重載版本的情
況下,才能正常運行。換句話說,它們嚴格依賴於數組引用的編譯期類型。
下面的程序說明了這種依賴性。看起來它像是所描述的第二種訂正方式的具體實
現,但是它產生的輸出卻與最初的程序所產生的輸出一樣醜陋,因爲它調用的是
println 的 Object 重載版本,而不是 char[]重載版本。
class ABC2{
public static void main(String[] args){
String letters = "ABC";
Object numbers = new char[] { '1', '2', '3' };
System.out.print(letters + " easy as ");
System.out.println(numbers);
}
}
總之,char 數組不是字符串。要想將一個 char 數組轉換成一個字符串,就要調
用 String.valueOf(char[])方法。某些類庫中的方法提供了對 char 數組的類似
字符串的支持,通常是提供一個 Object 版本的重載方法和一個 char[]版本的重
載方法,而之後後者才能產生我們想要的行爲。
對語言設計者的教訓是:char[]類型可能應該覆寫 toString 方法,使其返回數
組中包含的字符。更一般地講,數組類型可能都應該覆寫 toString 方法,使其
返回數組內容的一個字符串表示。
謎題 13:畜牧場
George Orwell 的《畜牧場(Animal Farm)》一書的讀者可能還記得老上校的
宣言:“所有的動物都是平等的。”下面的 Java 程序試圖要測試這項宣言。那
麼,它將打印出什麼呢?
public class AnimalFarm{
public static void main(String[] args){
final String pig = "length: 10";
final String dog = "length: " + pig.length();
System.out. println("Animals are equal: "
+ pig == dog);
}
}
對該程序的表面分析可能會認爲它應該打印出 Animal are equal: true。畢竟,
pig和dog都是final的string類型變量,它們都被初始化爲字符序列“length:
10”。換句話說,被 pig 和 dog 引用的字符串是且永遠是彼此相等的。然而,==
操作符測試的是這兩個對象引用是否正好引用到了相同的對象上。在本例中,它
們並非引用到了相同的對象上。
你可能知道 String 類型的編譯期常量是內存限定的。換句話說,任何兩個 String
類型的常量表達式,如果標明的是相同的字符序列,那麼它們就用相同的對象引
用來表示。如果用常量表達式來初始化 pig 和 dog,那麼它們確實會指向相同的
對象,但是 dog 並不是用常量表達式初始化的。既然語言已經對在常量表達式中
允許出現的操作作出了限制,而方法調用又不在其中,那麼,這個程序就應該打
印 Animal are equal: false,對嗎?
嗯,實際上不對。如果你運行該程序,你就會發現它打印的只是 false,並沒有
其它的任何東西。它沒有打印 Animal are equal: 。它怎麼會不打印這個字符
串字面常量呢?畢竟打印它纔是正確的呀!謎題 11 的解謎方案包含了一條暗示:
+ 操作符,不論是用作加法還是字符串連接操作,它都比 == 操作符的優先級高。
因此,println 方法的參數是按照下面的方式計算的:
System.out.println(("Animals are equal: " + pig) == dog);
這個布爾表達式的值當然是 false,它正是該程序的所打印的輸出。
有一個肯定能夠避免此類窘境的方法:在使用字符串連接操作符時,總是將非平
凡的操作數用括號括起來。更一般地講,當你不能確定你是否需要括號時,應該
選擇穩妥地做法,將它們括起來。如果你在 println 語句中像下面這樣把比較部
分括起來,它將產生所期望的輸出 Animals are equal: false :
System.out.println("Animals are equal: " + (pig == dog));
可以論證,該程序仍然有問題。
如果可以的話,你的代碼不應該依賴於字符串常量的內存限定機制。內存限定機
制只是設計用來減少虛擬機內存佔有量的,它並不是作爲程序員可以使用的一種
工具而設計的。就像這個謎題所展示的,哪一個表達式會產生字符串常量並非總
是很顯而易見。
更糟的是,如果你的代碼依賴於內存限定機制實現操作的正確性,那麼你就必須
仔細地瞭解哪些域和參數必定是內存限定的。編譯器不會幫你去檢查這些不變
量,因爲內存限定的和不限定的字符串使用相同的類型(String)來表示的。這
些因在內存中限定字符串失敗而導致的 bug 是非常難以探測到的。
在比較對象引用時,你應該優先使用 equals 方法而不是 == 操作符,除非你需
要比較的是對象的標識而不是對象的值。通過把這個教訓應用到我們的程序中,
我們給出了下面的 println 語句,這纔是它應該具有的模樣。很明顯,在用這種
方式訂正了該程序之後,它將打印出 true:
System.out.println("Animals are equal: " + pig.equals(dog));
這個謎題對語言設計者來說有兩個教訓。
?6?1 字符串連接的優先級不應該和加法一樣。這意味着重載 + 操作符來執行 字符串連接是有問題的,就像在謎題 11 中提到的一樣。 ?6?1 還有就是,對於不可修改的類型,例如 String,其引用的等價性比值的 等價性更加讓人感到迷惑。也許 == 操作符在被應用於不可修改的類型時 應該執行值比較。要實現這一點,一種方法是將 == 操作符作爲 equals 方法的簡便寫法,並提供一個單獨的類似於 System.identityHashCode 的方法來執行引用標識的比較。