請觀察下述代碼:
//: Stringer.java public class Stringer { static String upcase(String s) { return s.toUpperCase(); } public static void main(String[] args) { String q = new String("howdy"); System.out.println(q); // howdy String qq = upcase(q); System.out.println(qq); // HOWDY System.out.println(q); // howdy } } ///:~
q傳遞進入upcase()時,它實際是q的句柄的一個副本。該句柄連接的對象實際只在一個統一的物理位置處。句柄四處傳遞的時候,它的句柄會得到復制。
若觀察對upcase()的定義,會發現傳遞進入的句柄有一個名字s,而且該名字只有在upcase()執行期間才會存在。upcase()完成後,本地句柄s便會消失,而upcase()返回結果——還是原來那個字串,只是所有字符都變成了大寫。當然,它返回的實際是結果的一個句柄。但它返回的句柄最終是為一個新對象的,同時原來的q並未發生變化。所有這些是如何發生的呢?
1. 隱式常數
若使用下述語句:
String s = "asdf";
String x = Stringer.upcase(s);
那麼真的希望upcase()方法改變自變量或者參數嗎?我們通常是不願意的,因為作為提供給方法的一種信息,自變量一般是拿給代碼的讀者看的,而不是讓他們修改。這是一個相當重要的保證,因為它使代碼更易編寫和理解。
為了在C++中實現這一保證,需要一個特殊關鍵字的幫助:const。利用這個關鍵字,程序員可以保證一個句柄(C++叫“指針”或者“引用”)不會被用來修改原始的對象。但這樣一來,C++程序員需要用心記住在所有地方都使用const。這顯然易使人混淆,也不容易記住。
2. 覆蓋"+"和StringBuffer
利用前面提到的技術,String類的對象被設計成“不可變”。若查閱聯機文檔中關於String類的內容(本章稍後還要總結它),就會發現類中能夠修改String的每個方法實際都創建和返回了一個嶄新的String對象,新對象裡包含了修改過的信息——原來的String是原封未動的。因此,Java裡沒有與C++的const對應的特性可用來讓編譯器支持對象的不可變能力。若想獲得這一能力,可以自行設置,就象String那樣。
由於String對象是不可變的,所以能夠根據情況對一個特定的String進行多次別名處理。因為它是只讀的,所以一個句柄不可能會改變一些會影響其他句柄的東西。因此,只讀對象可以很好地解決別名問題。
通過修改產生對象的一個嶄新版本,似乎可以解決修改對象時的所有問題,就象String那樣。但對某些操作來講,這種方法的效率並不高。一個典型的例子便是為String對象覆蓋的運算符“+”。“覆蓋”意味著在與一個特定的類使用時,它的含義已發生了變化(用於String的“+”和“+=”是Java中能被覆蓋的唯一運算符,Java不允許程序員覆蓋其他任何運算符——注釋④)。
④:C++允許程序員隨意覆蓋運算符。由於這通常是一個復雜的過程(參見《Thinking in C++》,Prentice-Hall於1995年出版),所以Java的設計者認定它是一種“糟糕”的特性,決定不在Java中采用。但具有諷剌意味的是,運算符的覆蓋在Java中要比在C++中容易得多。
針對String對象使用時,“+”允許我們將不同的字串連接起來:
String s = "abc" + foo + "def" + Integer.toString(47);
可以想象出它“可能”是如何工作的:字串"abc"可以有一個方法append(),它新建了一個字串,其中包含"abc"以及foo的內容;這個新字串然後再創建另一個新字串,在其中添加"def";以此類推。
這一設想是行得通的,但它要求創建大量字串對象。盡管最終的目的只是獲得包含了所有內容的一個新字串,但中間卻要用到大量字串對象,而且要不斷地進行垃圾收集。我懷疑Java的設計者是否先試過種方法(這是軟件開發的一個教訓——除非自己試試代碼,並讓某些東西運行起來,否則不可能真正了解系統)。我還懷疑他們是否早就發現這樣做獲得的性能是不能接受的。
解決的方法是象前面介紹的那樣制作一個可變的同志類。對字串來說,這個同志類叫作StringBuffer,編譯器可以自動創建一個StringBuffer,以便計算特定的表達式,特別是面向String對象應用覆蓋過的運算符+和+=時。下面這個例子可以解決這個問題:
//: ImmutableStrings.java // Demonstrating StringBuffer public class ImmutableStrings { public static void main(String[] args) { String foo = "foo"; String s = "abc" + foo + "def" + Integer.toString(47); System.out.println(s); // The "equivalent" using StringBuffer: StringBuffer sb = new StringBuffer("abc"); // Creates String! sb.append(foo); sb.append("def"); // Creates String! sb.append(Integer.toString(47)); System.out.println(sb); } } ///:~
創建字串s時,編譯器做的工作大致等價於後面使用sb的代碼——創建一個StringBuffer,並用append()將新字符直接加入StringBuffer對象(而不是每次都產生新對象)。盡管這樣做更有效,但不值得每次都創建象"abc"和"def"這樣的引號字串,編譯器會把它們都轉換成String對象。所以盡管StringBuffer提供了更高的效率,但會產生比我們希望的多得多的對象。