1. 程式人生 > 程式設計 >Java中的String、StringBuffer和StringBuilder

Java中的String、StringBuffer和StringBuilder

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

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最大的特點就是 不可變,這是它的優點,因為不可變意味著使用簡單,沒有執行緒安全的問題。 但這也是它的缺點,因為每次變更都會生成一個新的字串,明顯太浪費空間了。

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了。

  1. 如果是常量字串,用String。
  2. 多執行緒環境下經常變動的字串用StringBuffer。
  3. 單執行緒經常變動的字串用StringBuilder。

彩蛋

  我們來看個比較底層的東西,是關於jvm對String優化的,現在有如下程式碼。

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版權宣告:本文為博主原創文章,轉載請註明出處。 部落格地址:xindoo.blog.csdn.net/