Java 字符串拼接效率分析及最佳實踐

本文來源於問題 Java字符串鏈接最佳實踐?java

  1. java鏈接字符串有多種方式,好比+操做符,StringBuilder.append方法,這些方法各有什麼優劣(能夠適當說明各類方式的實現細節)?
  2. 按照高效的原則,那麼java中字符串鏈接的最佳實踐是什麼?
  3. 有關字符串處理,都有哪些其餘的最佳實踐?

廢話很少說,直接開始, 環境以下:segmentfault

JDK版本: 1.8.0_65
CPU: i7 4790
內存: 16G

直接使用+拼接

看下面的代碼:數組

@Test
    public void test() {
        String str1 = "abc";
        String str2 = "def";
        logger.debug(str1 + str2);
    }

在上面的代碼中,咱們使用加號來鏈接四個字符串,這種字符串拼接的方式優勢很明顯: 代碼簡單直觀,可是對比StringBuilderStringBuffer大部分狀況下比後者都低,這裏說是大部分狀況下,咱們用javap工具對上面代碼生成的字節碼進行反編譯看看在編譯器對這段代碼作了什麼。安全

public void test();
    Code:
       0: ldc           #5                  // String abc
       2: astore_1
       3: ldc           #6                  // String def
       5: astore_2
       6: aload_0
       7: getfield      #4                  // Field logger:Lorg/slf4j/Logger;
      10: new           #7                  // class java/lang/StringBuilder
      13: dup
      14: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
      17: aload_1
      18: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: aload_2
      22: invokevirtual #9                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      25: invokevirtual #10                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      28: invokeinterface #11,  2           // InterfaceMethod org/slf4j/Logger.debug:(Ljava/lang/String;)V
      33: return

從反編譯的結果來看,實際上對字符串使用+操做符進行拼接,編譯器會在編譯階段把代碼優化成使用StringBuilder類,並調用append方法進行字符串拼接,最後調用toString方法,這樣看來是否能夠認爲在通常狀況下其實直接使用+,反正編譯器也會幫我優化爲使用StringBuilderapp

StringBuilder源碼分析

答案天然是不能夠的,緣由就在於StringBuilder這個類它內部作了些什麼時。
咱們看一看StringBuilder類的構造器ide

public StringBuilder() {
        super(16);
    }

    public StringBuilder(int capacity) {
        super(capacity);
    }

    public StringBuilder(String str) {
        super(str.length() + 16);
        append(str);
    }

    public StringBuilder(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }

StringBuilder提供了4個默認的構造器, 除了無參構造函數外,還提供了另外3個重載版本,而內部都調用父類的super(int capacity)構造方法,它的父類是AbstractStringBuilder,構造方法以下:函數

AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }

能夠看到實際上StringBuilder內部使用的是char數組來存儲數據(String、StringBuffer也是),這裏capacity的值指定了數組的大小。結合StringBuilder的無參構造函數,能夠知道默認的大小是16個字符。
也就是說若是待拼接的字符串總長度不小於16的字符的話,那麼其實直接拼接和咱們手動寫StringBuilder區別不大,可是咱們本身構造StringBuilder類能夠指定數組的大小,避免分配過多的內存。工具

如今咱們再看看StringBuilder.append方法內部作了什麼事:源碼分析

@Override
    public StringBuilder append(String str) {
        super.append(str);
        return this;
    }

直接調用的父類的append方法性能

public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;
        return this;
    }

在這個方法內部調用了ensureCapacityInternal方法,當拼接後的字符串總大小大於內部數組value的大小時,就必須先擴容才能拼接,擴容的代碼以下:

void expandCapacity(int minimumCapacity) {
        int newCapacity = value.length * 2 + 2;
        if (newCapacity - minimumCapacity < 0)
            newCapacity = minimumCapacity;
        if (newCapacity < 0) {
            if (minimumCapacity < 0) // overflow
                throw new OutOfMemoryError();
            newCapacity = Integer.MAX_VALUE;
        }
        value = Arrays.copyOf(value, newCapacity);
    }

StringBuilder在擴容時把容量增大到當前容量的兩倍+2,這是很可怕的,若是在構造的時候沒有指定容量,那麼頗有可能在擴容以後佔用了浪費大量的內存空間。其次擴容後還調用了Arrays.copyOf方法,這個方法把擴容前的數據複製到擴容後的空間內,這樣作的緣由是:StringBuilder內部使用char數組存放數據,java的數組是不可擴容的,因此只能從新申請一片內存空間,並把已有的數據複製到新的空間去,這裏它最終調用了System.arraycopy方法來複制,這是一個native方法,底層直接操做內存,因此比咱們用循環來複制要塊的多,即使如此,大量申請內存空間和複製數據帶來的影響也不可忽視。

使用+拼接和使用StringBuilder比較

@Test
    public void test() {
        String str = "";
        for (int i = 0; i < 10000; i++) {
            str += "asjdkla";
        }
    }

上面這段代碼通過優化後至關於:

@Test
    public void test() {
        String str = null;
        for (int i = 0; i < 10000; i++) {
            str = new StringBuilder().append(str).append("asjdkla").toString();
        }
    }

一眼就能看出建立了太多的StringBuilder對象,並且在每次循環事後str愈來愈大,致使每次申請的內存空間愈來愈大,而且當str長度大於16時,每次都要擴容兩次!而實際上toString方法在建立String對象時,調用了Arrays.copyOfRange方法來複制數據,此時至關於每執行一次,擴容了兩次,複製了3次數據,這樣的代價是至關高的。

public void test() {
        StringBuilder sb = new StringBuilder("asjdkla".length() * 10000);
        for (int i = 0; i < 10000; i++) {
            sb.append("asjdkla");
        }
        String str = sb.toString();
    }

這段代碼的執行時間在個人機器上都是0ms(小於1ms)和1ms,而上面那段代碼則大約在380ms!效率的差距至關明顯。

一樣是上面的代碼,將循環次數調整爲1000000時,在個人機器上,有指定capacity時耗時大約20ms,沒有指定capacity時耗時大約29ms,這個差距雖然和直接使用+操做符有了很大的提高(且循環次數增大了100倍),可是它依舊會觸發屢次擴容和複製。

將上面的代碼改爲使用StringBuffer,在個人機器上,耗時大約爲33ms,這是由於StringBuffer在大部分方法上都加上了synchronized關鍵字來保證線程安全,執行效率有必定程度上的下降。

使用String.concat拼接

如今再看這段代碼:

@Test
    public void test() {
        String str = "";
        for (int i = 0; i < 10000; i++) {
            str.concat("asjdkla");
        }
    }

這段代碼使用了String.concat方法,在個人機器上,執行時間大約爲130ms,雖然直接相加要好的多,可是比起使用StringBuilder還要太多了,彷佛沒什麼用。其實並非,在不少時候,咱們只須要鏈接兩個字符串,而不是多個字符串的拼接,這個時候使用String.concat方法比StringBuilder要簡潔且效率要高。

public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

上面這段是String.concat的源碼,在這個方法中,調用了一次Arrays.copyOf,而且指定了len + otherLen,至關於分配了一次內存空間,並分別從str1和str2各複製一次數據。而若是使用StringBuilder並指定capacity,至關於分配一次內存空間,並分別從str1和str2各複製一次數據,最後由於調用了toString方法,又複製了一次數據。

結論

如今根據上面的分析和測試能夠知道:

  1. Java中字符串拼接不要直接使用+拼接。
  2. 使用StringBuilder或者StringBuffer時,儘量準確地估算capacity,並在構造時指定,避免內存浪費和頻繁的擴容及複製。
  3. 在沒有線程安全問題時使用StringBuilder, 不然使用StringBuffer
  4. 兩個字符串拼接直接調用String.concat性能最好。

關於String的其餘最佳實踐

  1. equals時老是把能肯定不爲空的變量寫在左邊,如使用"".equals(str)判斷空串,避免空指針異常。
  2. 第二點是用來排擠第一點的.. 使用str != null && str.length() == 0來判斷空串,效率比第一點高。
  3. 在須要把其餘對象轉換爲字符串對象時,使用String.valueOf(obj)而不是直接調用obj.toString()方法,由於前者已經對空值進行檢測了,不會拋出空指針異常。
  4. 使用String.format()方法對字符串進行格式化輸出。
  5. 在JDK 7及以上版本,能夠在switch結構中使用字符串了,因此對於較多的比較,使用switch代替if-else
  6. 我暫時想的起來的就這麼幾個了.. 請你們幫忙補充補充...
相關文章
相關標籤/搜索