不同的程式語言,會有一些相類似的語法或元素,例如程式語言都會有if、for、while
之類語法,也大多有字元、數值、字串之類的元素,然而各種程式語言解決的問題不同,也因此在這些類似語法或元素中,各程式語言會有細微、重要且不容忽視的特性,在學習程式語言時,不得不慎。
以Java的字串來說,就有一些必須注意的特性:
- 字串常量與字串池
- 不可變動(Immutable)字串
字串常量與字串池
來看個程式片段,你覺得以下會顯示true
或false
?
char[] name = {'J', 'u', 's', 't', 'i', 'n'};
String name1 = new String(name);
String name2 = new String(name);
System.out.println(name1 == name2);
希望現在的你有足夠的能力與自信回答出false
的答案,因為name1
與name2
分別參考至建構出來的String
物件,那麼底下這個程式碼呢?
String name1 = "Justin";
String name2 = "Justin";
System.out.println(name1 == name2);
很意外地,答案會是true
!這代表了name1
與name2
是參考到同一物件囉?答案是對的!在Java中為了效率考量,以""
包括的字串,只要內容相同(序列、大小寫相同),無論在程式碼中出現幾次,JVM都只會建立一個String
實例,並在字串池(String pool)中維護。在上面這個程式片段的第一行,JVM會建立一個String
實例放在字串池中,並給name1
參考,而第二行則是讓name2
直接參考至字串池中的String
實例,如下圖所示:
用""寫下的字串稱為字串常量(String literal),既然你用
"Justin"
寫死了字串內容,基於節省記憶體考量,自然就不用為這些字串常量分別建立String
實例。來看個實務上不會如此撰寫,但認證上很常考的問題:String name1 = "Justin";
String name2 = "Justin";
String name3 = new String("Justin");
String name4 = new String("Justin");
System.out.println(name1 == name2);
System.out.println(name1 == name3);
System.out.println(name3 == name4);
這個片段會分別顯示
true、false、false
的結果,因為"Justin"
會建立String
實例並在字串池中維護,所以name1
與name2
參考的是同一個物件,而new
一定是建立新物件,所以name3
與name4
分別參考至新建的String
實例。以圖來表示的話就可以知道為何會顯示true、false、false
的結果:先前一直強調,如果你想比較物件實質內容是否相同,不要使用
==
,要使用equals()
。同樣地,如果想比較字串實際字元內容是否相同,不要使用==
,要使用equals()
。以下程式片段執行結果都是顯示true
:String name1 = "Justin";
String name2 = "Justin";
String name3 = new String("Justin");
String name4 = new String("Justin");
System.out.println(name1.equals(name2));
System.out.println(name1.equals(name3));
System.out.println(name3.equals(name4));
不可變動字串
在Java中,字串物件一旦建立,就無法更動物件中任何內容,物件上沒有任何方法可以更動字串內容。那麼使用
+
串接字串是怎麼達到的?例如:String name1 = "Java";
String name2 = name1 + "World";
System.out.println(name2);
上面這個程式片段會顯示JavaWorld,由於無法更動字串物件內容,所以絕不是在
name1
參考的字串物件之後附加World內容。可以試著反組譯這段程式,結果會發現:String s = "Java";
String s1 = (new StringBuilder()).append(s).append("World").toString();
System.out.println(s1);
如果使用
+
串接字串,會變成建立java.lang.StringBuilder
物件,使用其append()
方法來進行+左右兩邊字串附加,最後再轉換為toString()
傳回。簡單來說,使用
+
串接字串會產生新的String
實例,這並不是告訴你,不要使用+
串接字串,畢竟+
串接字串很方便,這只是在告訴你,不要將+
用在重複性的串接場合,像是迴圈中或遞迴時使用+
串接字串,這會因為頻繁產生新物件,造成效能上的負擔。舉個例子來說,如果使用程式顯示下圖的結果,你會怎麼寫呢?
這是個很有趣的題目,以下列出幾個我看過的寫法。首先有人這麼寫:
for(int i = 1; i < 101; i++) {
System.out.print(i);
if(i != 100) {
System.out.print('+');
}
}
這可以達到題目要求,不過有沒有效能上可以改進的空間?其實可以改成這樣:
for(int i = 1; i < 100; i++) {
System.out.printf("%d+", i);
}
System.out.println(100);
程式變簡潔了,而且可以少一個
if
判斷,不過就這個小程式而言,少個if
判斷是節省不了多少時間,事實上,你可以減少輸出次數,因為在for
迴圈中呼叫了99次System.out.printf()
,相較於記憶體中的運算,標準輸出速度是慢得多了,有的人知道可以使用+
串接字串,所以會這麼寫:String text = "";
for(int i = 1; i < 100; i++) {
text = text + i + '+';
}
System.out.println(text + 100);
這個程式片段中,在
for
迴圈中沒有進行輸出,這確實改善了不少效能,不過在Java中使用+
串接會產生新字串物件,這個程式片段for
迴圈中有頻繁產生新物件的問題,正如先前對+
串接片段反組譯中所看到的,你可以改用StringBuilder
來改善:package cc.openhome;
public class OneTo100 {
public static void main(String[] args) {
StringBuilder builder = new StringBuilder();
for (int i = 1; i < 100; i++) {
builder.append(i).append('+');
}
System.out.println(builder.append(100).toString());
}
}
StringBulder
每次append()
呼叫過後,都會傳回原有的StringBuilder
物件,方便你進行下一次的操作。這個程式片段只產生了一個StringBuilder
物件,只進行一次輸出,效能上對比最初看到的程式片段好得多。java.lang.StringBulder
是JDK5之後新增的類別,在這版本之前,是使用java.lang.StringBuffer
類別,StringBuilder
與StringBuffer
具有相同操作介面,在單機非多執行緒(Multithread)情況下,使用StringBuilder
會有較好的效率,因為StringBuilder
不處理同步(Synchronized)問題;StringBuffer
則會處理同步問題,在多執行緒環境下建議改用StringBuffer
,讓物件自行管理同步問題。之後還會介紹何為多執行緒。再來看個很無聊但認證會考的題目,請問以下會顯示
true
或false
? String text1 = "Ja" + "va";
String text2 = "Java";
System.out.println(text1 == text2);
有的人會這麼說:因為用了
+
串接字串,所以產生新字串,所以text1 == text2
應該是false
吧!如果你這麼認為,那就上當了!答案是true
!反組譯之後就知道為什麼了:String s = "Java";
String s1 = "Java";
System.out.println(s == s1);
編譯器是這麼認為的:既然你寫死了
"Ja" + "va"
,那你要的不就是"Java"
嗎?根據以上反組譯之後的程式碼,顯示true
的結果就不足為奇了。