首頁 > Java > java教程 > 詳細分析Java中String、StringBuffer、StringBuilder類別的效能

詳細分析Java中String、StringBuffer、StringBuilder類別的效能

高洛峰
發布: 2017-01-22 11:25:12
原創
1436 人瀏覽過

我們先要記住三者的特徵:

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中文网!

來源:php.cn
本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn
熱門教學
更多>
最新下載
更多>
網站特效
網站源碼
網站素材
前端模板