我們先要記住三者的特徵:
String 字串常數
StringBuffer 字串變數(執行緒安全)
StringBuilder 字串變數(非執行緒安全)
一、定義
查看API 、StringBuffer、StringBuilder都實作了CharSequence接口,雖然它們都與字串相關,但是其處理機制不同。
String:是不可改變的量,也就是創建後就不能在修改了。
StringBuffer:是一個可變字串序列,它與String一樣,在記憶體中保存的都是一個有序的字串序列(char類型的陣列),不同點是StringBuffer物件的值都是可變的。
StringBuilder:與StringBuffer類別基本上相同,都是可變字元換字串序列,不同點是StringBuffer是執行緒安全的,StringBuilder是執行緒不安全的。 在效能方面,由於String類別的操作是產生新的String對象,而StringBuilder和StringBuffer只是一個字元陣列的擴容而已,所以String類別的操作要遠慢於StringBuffer和StringBuilder。
二、使用場景
使用String類別的場景:在字串不常變化的場景中可以使用String類,例如常數的宣告、少量的變數運算。
使用StringBuffer類別的場景:在頻繁進行字串運算(如拼接、替換、刪除等),並且運行在多執行緒環境中,則可以考慮使用StringBuffer,例如XML解析、HTTP參數解析和封裝。
使用StringBuilder類別的場景:在頻繁進行字串運算(如拼接、替換、和刪除等),並且運行在單一執行緒的環境中,則可以考慮使用StringBuilder,如SQL語句的拼裝、JSON封裝等。
三、分析
簡要的說, String 類型和StringBuffer 類型的主要性能區別其實在於String 是不可變的對象, 因此在每次對String 類型進行改變的時候其實都等同於生成了一個新的String 對象,然後將指標指向新的String 物件。所以經常改變內容的字串最好不要用String ,因為每次生成物件都會對系統效能產生影響,特別當記憶體中無引用物件多了以後, JVM 的GC 就會開始工作,那速度是一定會相當慢的。
而如果是使用 StringBuffer 類別則結果就不一樣了,每次結果都會對 StringBuffer 物件本身進行操作,而不是產生新的對象,再改變物件參考。所以在一般情況下我們推薦使用 StringBuffer ,特別是字串物件經常改變的情況下。而在某些特別情況下, String 物件的字串拼接其實是被JVM 解釋成了StringBuffer 物件的拼接,所以這些時候String 物件的速度不會比StringBuffer 物件慢,而特別是以下的字串物件生成中, String 效率是遠要比StringBuffer 快的:
String S1 = “This is only a" + “ simple" + “ test"; StringBuffer Sb = new StringBuilder(“This is only a").append(“ simple").append(“ test");
你會很驚訝的發現,生成String S1 物件的速度簡直太快了,而這個時候StringBuffer 居然速度上根本都不會佔優勢。其實這是 JVM 的把戲,在 JVM 眼裡,這個
String S1 = “This is only a" + “ simple" + “test";
其實就是:
String S1 = “This is only a simple test";
但大家這裡要注意的是,如果你的字串是來自另外的String 物件的話,速度就沒那麼快了,譬如:
String S2 = "This is only a"; String S3 = "simple"; String S4 = "test"; String S1 = S2 +S3 + S4;
這時候JVM 會規規矩矩的按照原來的方式去做。
四、深入JVM的優化處理
真的會有上面的性能代價麼,字符串拼接這麼常用,沒有特殊的處理優化麼,答案是有的,這個優化進行在JVM編譯.java到bytecode時。
一個Java程式如果想要運作起來,需要經過兩個時期,編譯時和執行時。在編譯時,Java JVM(Compiler)將java檔轉換成字節碼。在執行時,Java虛擬機器(JVM)會執行編譯時產生的字節碼。透過這樣兩個時期,Java做到了所謂的一個編譯,處處運作。
我們實驗一下編譯期做了哪些最佳化,我們製造一段可能會出現效能代價的程式碼。
public class Concatenation { public static void main(String[] args) { String userName = "Andy"; String age = "24"; String job = "Developer"; String info = userName + age + job; System.out.println(info); } }
對Concatenation.java編譯一下。得到Concatenation.class
javac Concatenation.java
然後我們使用javap反編譯一下編譯出來的Concatenation.class檔案。 javap -c Concatenation。如果沒有找到javap指令,請考慮將javap所在目錄加入環境變數或使用javap的完整路徑。
17:22:04-androidyue~/workspace_adt/strings/src$ javap -c Concatenation Compiled from "Concatenation.java" public class Concatenation { public Concatenation(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // String Andy 2: astore_1 3: ldc #3 // String 24 5: astore_2 6: ldc #4 // String Developer 8: astore_3 9: new #5 // class java/lang/StringBuilder 12: dup 13: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 16: aload_1 17: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 20: aload_2 21: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 24: aload_3 25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 28: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 31: astore 4 33: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 36: aload 4 38: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 41: return }
其中,ldc,astore等為java字節碼的指令,類似組譯指令。後面的註解使用了Java相關的內容進行了說明。 我們可以看到上面有很多StringBuilder,但是我們在Java程式碼裡並沒有顯示地調用,這就是JavaJVM所做的最佳化,當JavaJVM遇到字串拼接的時候,會建立一個StringBuilder對象,後面的拼接,實際上是呼叫StringBuilder物件的append方法。這樣就不會出現我們上面擔心的問題了。
五、仅靠JVM优化?
既然JVM帮我们做了优化,是不是仅仅依靠JVM的优化就够了呢,当然不是。
下面我们看一段未优化性能较低的代码
public void implicitUseStringBuilder(String[] values) { String result = ""; for (int i = 0 ; i < values.length; i ++) { result += values[i]; } System.out.println(result); }
使用javac编译,使用javap查看
public void implicitUseStringBuilder(java.lang.String[]); Code: 0: ldc #11 // String 2: astore_2 3: iconst_0 4: istore_3 5: iload_3 6: aload_1 7: arraylength 8: if_icmpge 38 11: new #5 // class java/lang/StringBuilder 14: dup 15: invokespecial #6 // Method java/lang/StringBuilder."<init>":()V 18: aload_2 19: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 22: aload_1 23: iload_3 24: aaload 25: invokevirtual #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 28: invokevirtual #8 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; 31: astore_2 32: iinc 3, 1 35: goto 5 38: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream; 41: aload_2 42: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 45: return
其中8: if_icmpge 38 和35: goto 5构成了一个循环。8: if_icmpge 38的意思是如果JVM操作数栈的整数对比大于等于(i < values.length的相反结果)成立,则跳到第38行(System.out)。35: goto 5则表示直接跳到第5行。
但是这里面有一个很重要的就是StringBuilder对象创建发生在循环之间,也就是意味着有多少次循环会创建多少个StringBuilder对象,这样明显不好。赤裸裸地低水平代码啊。
稍微优化一下,瞬间提升逼格。
public void explicitUseStringBuider(String[] values) { StringBuilder result = new StringBuilder(); for (int i = 0; i < values.length; i ++) { result.append(values[i]); } }
对应的编译后的信息
public void explicitUseStringBuider(java.lang.String[]); Code: 0: new #5 // class java/lang/StringBuilder 3: dup 4: invokespecial #6 // 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 #7 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; 23: pop 24: iinc 3, 1 27: goto 10 30: return
从上面可以看出,13: if_icmpge 30和27: goto 10构成了一个loop循环,而0: new #5位于循环之外,所以不会多次创建StringBuilder.
总的来说,我们在循环体中需要尽量避免隐式或者显式创建StringBuilder. 所以那些了解代码如何编译,内部如何执行的人,写的代码档次都比较高。
六、结论
在大部分情况下 StringBuffer > String
Java.lang.StringBuffer是线程安全的可变字符序列。一个类似于 String 的字符串缓冲区,但不能修改。虽然在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。在程序中可将字符串缓冲区安全地用于多线程。而且在必要时可以对这些方法进行同步,因此任意特定实例上的所有操作就好像是以串行顺序发生的,该顺序与所涉及的每个线程进行的方法调用顺序一致。
StringBuffer 上的主要操作是 append 和 insert 方法,可重载这些方法,以接受任意类型的数据。每个方法都能有效地将给定的数据转换成字符串,然后将该字符串的字符追加或插入到字符串缓冲区中。append 方法始终将这些字符添加到缓冲区的末端;而 insert 方法则在指定的点添加字符。
例如,如果 z 引用一个当前内容是“start”的字符串缓冲区对象,则此方法调用 z.append(“le”) 会使字符串缓冲区包含“startle”(累加);而 z.insert(4, “le”) 将更改字符串缓冲区,使之包含“starlet”。
在大部分情况下 StringBuilder > StringBuffer
java.lang.StringBuilder一个可变的字符序列是JAVA 5.0新增的。此类提供一个与 StringBuffer 兼容的 API,但不保证同步,所以使用场景是单线程。该类被设计用作 StringBuffer 的一个简易替换,用在字符串缓冲区被单个线程使用的时候(这种情况很普遍)。如果可能,建议优先采用该类,因为在大多数实现中,它比 StringBuffer 要快。两者的使用方法基本相同。
更多详细分析Java中String、StringBuffer、StringBuilder类的性能相关文章请关注PHP中文网!