字串特性


不同的程式語言,會有一些相類似的語法或元素,例如程式語言都會有if、for、while之類語法,也大多有字元、數值、字串之類的元素,然而各種程式語言解決的問題不同,也因此在這些類似語法或元素中,各程式語言會有細微、重要且不容忽視的特性,在學習程式語言時,不得不慎。

以Java的字串來說,就有一些必須注意的特性:

  • 字串常量與字串池
  • 不可變動(Immutable)字串
字串常量與字串池

來看個程式片段,你覺得以下會顯示truefalse

char[] name = {'J', 'u', 's', 't', 'i', 'n'};
String name1 = new String(name);
String name2 = new String(name);
System.out.println(name1 == name2);

希望現在的你有足夠的能力與自信回答出false的答案,因為name1name2分別參考至建構出來的String物件,那麼底下這個程式碼呢?

String name1 = "Justin";
String name2 = "Justin";
System.out.println(name1 == name2);

很意外地,答案會是true!這代表了name1name2是參考到同一物件囉?答案是對的!在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實例並在字串池中維護,所以name1name2參考的是同一個物件,而new一定是建立新物件,所以name3name4分別參考至新建的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實例,這並不是告訴你,不要使用+串接字串,畢竟+串接字串很方便,這只是在告訴你,不要將+用在重複性的串接場合,像是迴圈中或遞迴時使用+串接字串,這會因為頻繁產生新物件,造成效能上的負擔。

舉個例子來說,如果使用程式顯示下圖的結果,你會怎麼寫呢?

顯示到1+2+...+100


這是個很有趣的題目,以下列出幾個我看過的寫法。首先有人這麼寫:

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類別,StringBuilderStringBuffer具有相同操作介面,在單機非多執行緒(Multithread)情況下,使用StringBuilder會有較好的效率,因為StringBuilder不處理同步(Synchronized)問題;StringBuffer則會處理同步問題,在多執行緒環境下建議改用StringBuffer,讓物件自行管理同步問題。之後還會介紹何為多執行緒。

再來看個很無聊但認證會考的題目,請問以下會顯示truefalse

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的結果就不足為奇了。