Java中的String、StringBuffer和StringBuilder

  做爲做爲一個已經入了門的java程序猿,確定對Java中的String、StringBuffer和StringBuilder都略有耳聞了,尤爲是String 確定是常常用的。但確定你有一點很好奇,爲何java中有三個關於字符串的類?一個不夠嗎!先回答這個問題,黑格爾曾經說過——存在必合理,單純一個String確實是不夠的,因此要引入StringBuffer。再後來引入StringBuilder是另外一個故事了,後面會詳細講到。
  要了解爲何,咱們就得先來看下這三者各自都有什麼樣的特色,有什麼樣的異同,對其知根知底以後,一切謎團都會被解開。java

String

  點開String的源碼,能夠發現String被定義爲final類型,意味着它不能被繼承,再仔細看其提供的方法,沒有一個能對原始字符串作任何操做的,有幾個開啓了貌似是操做原字符串的,好比replaceFirst replaceAll,點進去一看,實際上是從新生成了一個新的字符串,對原始內容沒有作任何修改。
  是的,從實現的角度來看,它是不可變的,全部String的變動其實都會生成一個新的字符串,比String str = "abcdefghijklmnopqrstuvwxy"; str = str + "z"; 以後新生成的a-z並不包含原來的a-y,原來的a-y已經變成垃圾了。簡單歸納,只要是兩個不一樣的字符,確定都是兩個徹底不一樣不相關的對象,即使其中一個是從另外一個subString出來的,兩個也沒有任何關係。 若是是兩個相同的字符串,狀況比較複雜,多是同一份也可能不是。若是在JVM中使用G1gc,並且開啓-XX:+UseStringDeduplication ,JVM會對字符串的存儲作優化,因此若是你的服務中有大量相同字符串,建議開啓這個參數。
  Java做爲一個非純面向對象的語言,除了提供分裝對象外,也提供了一些原始類型(好比:int long double char),String的使用竟然能夠像用原始類型同樣不須要new,直接String str = "a"這樣聲明,我以爲String更像是面向對象和非面向對象結合的一個產物。
  String最大的特色就是 __ 不可變__,這是它的優勢,由於不可變意味着使用簡單,沒有線程安全的問題。 但這也是它的缺點,由於每次變動都會生成一個新的字符串,明顯太浪費空間了。git

StringBuffer

  我以爲StringBuffer是徹底由於String的缺點而生的。咱們平常使用String的過程當中,確定常常會用到字符串追加的狀況,按String的實現,沒次追加即使只是一個字符,都是生成一個徹底不一樣的對象,若是此次操做很頻繁不少的話會大幅提升內存的消耗,而且增長gc的壓力。對於這種問題,StringBuffer是如何解決的呢?咱們直接從源碼上來看。
在這裏插入圖片描述
  但看StringBuffer裏,幾乎全部的方法都會調super父類,其實它全部的實現都是在AbstractStringBuilder裏的。鑑於咱們對其最長用的方法是append,因此咱們就從append入手,其實append也是StringBuffer比較核心的功能。數組

/**
     * The value is used for character storage.
     */
    char[] value;
    
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }
    
    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;
    }
    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));
        }
    }
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }

  原來是StringBuffer父類AbstractStringBuilder有個char數組value,用來存放字符串全部的字符,StringBuffer默認初始大小是16。StringBuffer在每次append的時候,若是value的容量不夠,就會申請一個容量比當前所需大一倍的字符數組,而後把舊的數據拷貝進去。這種一次性擴容一倍的方式,在咱們以前HashMap源碼淺析中已經看到過了。一次性多申請內存,雖然看起來會有大段的內存空閒,但其實能夠減小String append時頻繁建立新字符串的問題。
  因此記住,若是你代碼中對String頻繁操做,千萬不用用String而是選擇用StringBuffer或者咱們下面要講的StringBuilder。還有一個優化點,若是你能提早知道你字符串最大的長度,建議你在建立StringBuffer時指定其capacity,避免在append時執行ensureCapacityInternal,從而提高性能。
  對於StringBuffer還有一個點沒提到,注意看它源碼的全部方法,除構造函數外,全部的方法都被__synchronized__修飾,意味着它是有個線程安全的類,全部操做查詢方法都會被加同步,可是若是咱們只是單線程呢,想用StringBuffer的優點,但又以爲加同步太多餘,太影響性能。這個時候就輪到StringBuilder上場了。安全

StringBuilder

在這裏插入圖片描述
  StringBuilder從類圖上看和StringBuffer徹底沒有任何區別,再打開它的源碼,和StringBuffer同樣幾乎啥邏輯都沒有,全是調調super父類AbstractStringBuilder,它和StringBuffer最大的區別就是全部方法沒有用synchronized修復,它不是一個線程安全的類,但也意味着它沒有同步,在單線程狀況下性能會優於StringBuffer。多線程

總結

  看完上面內容,我以爲你應該知道上面時候用String、何時用StringBuffer、何時用StringBuilder了。app

  1. 若是是常量字符串,用String。
  2. 多線程環境下常常變更的字符串用StringBuffer。
  3. 單線程常常變更的字符串用StringBuilder。

彩蛋

  咱們來看個比較底層的東西,是關於jvm對String優化的,如今有以下代碼。jvm

public class StringTest {
    public static void main(String[] args) {
        String str = "abc";
        str = str + "d";
        str = str + "e";
    }
}

  咱們用javac StringTest.java編譯成class文件,而後用 javap -c StringTest 生成字節碼,內容以下函數

public class StringTest {
  public StringTest();
    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 abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: return
}
➜  java git:(master) ✗ javap -c StringTest
Compiled from "StringTest.java"
public class StringTest {
  public StringTest();
    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 abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: return
}
➜  java git:(master) ✗ javac StringTest.java
➜  java git:(master) ✗ javap -c StringTest  
Compiled from "StringTest.java"
public class StringTest {
  public StringTest();
    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 abc
       2: astore_1
       3: new           #3                  // class java/lang/StringBuilder
       6: dup
       7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      10: aload_1
      11: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      14: ldc           #6                  // String d
      16: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      19: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      22: astore_1
      23: new           #3                  // class java/lang/StringBuilder
      26: dup
      27: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
      30: aload_1
      31: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      34: ldc           #8                  // String e
      36: invokevirtual #5                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      39: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      42: astore_1
      43: return
}

  其實能夠看出,java底層實現字符串+的時候實際上是用StringBuilder的append()來實現的,若是有字符串的連續+,jvm用StringBuilder append也能夠實現優化。性能

備註:源碼來自JDK11優化

本文由博客一文多發平臺 OpenWrite 發佈!
相關文章
相關標籤/搜索