你真的瞭解String和StringBuilder嗎

前言

面試的時候可能常常會問這樣一個問題:請說一下String、StringBuilder、StringBuffer的區別,可能不少人會說String若是經過+(這是加好,後面同理)來拼接字符串時,會建立不少臨時變量,性能比較低(網上不少帖子也是這麼寫的),可是,真的是這樣的嗎?java

字符串的拼接

那麼String經過+來拼接字符串時,到底有沒有建立臨時變量呢?其實,這個問題很簡單,只須要經過javap反編譯生成的class文件,看看class文件中String所作的操做就能夠了。下面咱們就以《java編程思想》中字符串章節的例子來說解。面試

首先咱們來看下面這段代碼:編程

public class Test {
    public static void main(String[] args){
        String mango = "mango";
        String s = "abc" + mango + "def" + 47;
        System.out.println(s);
    }
}
複製代碼

這段代碼是比較典型的經過+來拼接字符串的代碼,接下來咱們經過javac Test.java來編譯這段代碼,而後經過javap -c Test.class反編譯生成的Test.class文件。剔除掉一些無關的部分,主要展現了main()中代碼的字節碼,因而有了如下的字節碼。你會發現很是有意思的東西發生了。segmentfault

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #2 // String mango
         2: astore_1
         3: new           #3 // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5 // String abc
        12: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: aload_1
        16: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        19: ldc           #7 // String def
        21: invokevirtual #6 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        24: bipush        47
        26: invokevirtual #8 // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        29: invokevirtual #9 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        32: astore_2
        33: getstatic     #10 // Field java/lang/System.out:Ljava/io/PrintStream;
        36: aload_2
        37: invokevirtual #11 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        40: return
}
複製代碼

這裏涉及到彙編語言,讀者能夠網上搜索字節碼指令表會有不少,我這裏提供一個,讀者能夠對着表來理解每個指令的含義,這裏我不詳細展開。每一個指令的後面可能會有////後面的內容表示指令碼操做的對象。細心的讀者必定會發現:編譯器自動引入了java.lang.StringBuilder(其中java前面的L表示引用類型,想要詳細瞭解的讀者能夠看一下《深刻理解Java虛擬機》中類文件結構那一章)。雖然咱們在源代碼中沒有使用StringBuilder,可是編譯器卻自做主張的使用了它,由於它更高效。數組

看上面的字節碼你會發現,編譯器建立StringBuilder對象以後,對+號相連的每個字符串使用append()方法來拼接,總計調用了四次,最後調用toString()方法生成結果。(注:讀者感興趣的話能夠用StringBuilder來替換上面的代碼,經過javac javap從新編譯,而後你會發現main()方法中生成的字節碼是同樣的)。bash

結論

經過上面的例子咱們發現,當咱們經過+來拼接字符串時,編譯器會自動替咱們優化成StringBuilder來拼接,並不會形成網上所說的建立臨時變量,速度變慢這些缺點。(注:因爲StringBuidler是在jdk5.0以後引入的,因此jdk5.0以前是經過StringBuffer來拼接的,感興趣的讀者能夠自行驗證)。app

延伸

如今,咱們確定會很開心,既然編譯器都替咱們優化了,那咱們是否是能夠隨意使用String了呢(想一想都開心)。哈哈,不要高興得太早,由於有時候編譯器的優化可能並非你想要的結果。讓咱們來看下面這段代碼:jvm

下面這段程序採用兩種方式生成一個String:方法一使用多個String對象;方法二代碼中使用了StringBuidler。性能

public class Test {
    public String testString(String[] fields) {
        String result = "";
        for (int i = 0; i < fields.length; i++) {
            result += fields[i];
        }
        return result;
    }

    public String testStringBuilder(String[] fields){
        StringBuilder result = new StringBuilder();
        for (int i = 0; i<fields.length; i++){
            result.append(fields[i]);
        }
        return result.toString();
    }
}
複製代碼

上面代碼中的兩個方法執行相似,都是傳入字符串數組,而後經過for循環將數組字符串拼接起來,區別是第一個方法使用String來拼接,第二個方法使用StringBuilder來拼接。而後咱們仍是經過javap來反編譯這段代碼,剔除無關部分,會看到兩個方法的字節碼。優化

首先是testString()方法的字節碼:

public java.lang.String testString(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: ldc           #2 // String
         2: astore_2
         3: iconst_0
         4: istore_3
         5: iload_3
         6: aload_1
         7: arraylength
         8: if_icmpge     38
        11: new           #3 // class java/lang/StringBuilder
        14: dup
        15: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
        18: aload_2
        19: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        22: aload_1
        23: iload_3
        24: aaload
        25: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        31: astore_2
        32: iinc          3, 1
        35: goto          5
        38: aload_2
        39: areturn
複製代碼

這裏讀者看一下第8行中的if_icmpge,對照字節碼指令表會發現這個指令就是for循環中比較i值等於某個值時進入循環,後面的38表示在38行跳出循環,這裏的循環體是第8行到第35行。第35行的意思是:返回循環體的起始點(第5行)。而後咱們看循環體(第8行到第35行)中第11行,是一個new指令,這個太熟悉了,就是建立對象。可是它竟然是在循環體內部,這就意味着每循環一次,就要建立一個新的StringBuilder對象。這顯然不能接受。

那咱們再看一下testStringBuilder()的字節碼:

public java.lang.String testStringBuilder(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)Ljava/lang/String;
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=4, args_size=2
         0: new           #3 // class java/lang/StringBuilder
         3: dup
         4: invokespecial #4 // Method java/lang/StringBuilder."<init>":()V
         7: astore_2
         8: iconst_0
         9: istore_3
        10: iload_3
        11: aload_1
        12: arraylength
        13: if_icmpge     30
        16: aload_2
        17: aload_1
        18: iload_3
        19: aaload
        20: invokevirtual #5 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        23: pop
        24: iinc          3, 1
        27: goto          10
        30: aload_2
        31: invokevirtual #6 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        34: areturn
複製代碼

咱們能夠看到,不只循環體部分的代碼更簡短、更簡單了,並且new只在剛開始調用了一次,說明只生成了一個StringBuilder對象。

延伸結論

因此,當你爲一個類編寫toString()方法時,若是字符串操做比較簡單,那就能夠信賴編譯器,它會爲你合理的構造最終的字符串結果。可是,若是你要在toString()方法中使用循環,那麼你就須要本身建立一個StringBuilder對象。固然,當你拿不定主意的時候,那麼你隨時能夠經過javap來分析你的程序。

留一個問題:枚舉你們應該都知道,可是你知道它在jvm中究竟是怎麼執行的嗎?(思路:其實要想知道它的原理很簡單,你一樣能夠寫一段枚舉代碼,而後經過javap反編譯這段代碼,你會有一種豁然開朗的感受。)

參考文獻

-《java編程思想》

相關文章
相關標籤/搜索