【Java】String、StringBuilder和StringBuffer

utopia 發表於 2021-05-02

【String】

首先,從String類的定義入手,可以看到String類是由final修飾,即不可變的,一旦建立出來就不可修改,因此首先明確,字串的拼接、擷取等操作都會產生新的字串物件。

 【Java】String、StringBuilder和StringBuffer

觀察以下幾種建立Stirng的語句

1 String s1 = "hello ";
2 String s2 = "world";
3 String s3 = s1 + s2;
4 String s4 = "hello " + "world";
5 String s5 = new String("hello world");

1> 對於s1,直接通過字串常量建立,會先查詢常量池中是否有 hello 這個字串,有的話不建立,沒有的話新建,因此會在常量池中建立一個物件。

2> 對於s2,同上,在常量池中建立一個物件。

3> 對於s3,字串拼接操作,檢視反彙編程式碼,字串拼接實際上是new StringBuilder(),然後執行其append()方法,最後執行toString,過程中建立了3個物件。

【Java】String、StringBuilder和StringBuffer

4> 對於s4,直接通過常量字串拼接,會被優化為建立 hello world,在常量池中建立物件。

【Java】String、StringBuilder和StringBuffer

5> 對於s5,new操作一定會在堆中建立物件,然後查詢常量池中是否有 hello world 這個字串,沒有就在常量池中建立一個。

對於s3、s4、s5,字串的內容都是hello world,但對於s3和s5,引用的都是堆中建立的字串物件,s4引用的則直接是常量池中的物件,因此它們三個的引用都是不同的,如下

【Java】String、StringBuilder和StringBuffer

intern

通過上面的驗證,知道直接通過常量定義的String物件是位於常量池中,而通過new顯式建立或者拼接隱式建立的String物件是位於堆中。

除了常用的操作字串方法,JDK還提供了 inter() 方法,該方法的官方說明如下,作用是當方法呼叫時,如果常量池中已經包含一個和這個String相等(內容相同,即equals方法)的String,就會返回常量池中的物件,否則,這個物件會被新增到常量池,然後返回她的引用。簡單來說其作用是把字串快取到常量池中。

【Java】String、StringBuilder和StringBuffer

通過幾個例子來驗證intern的作用

【Java】String、StringBuilder和StringBuffer

【StringBuilder和StringBuffer】

StringBuilder和StringBuffer都表示可變的字串序列,可以通過其提供的一序列方法實現字串的拼接、擷取,觀察原始碼會發現兩個類的核心程式碼基本一致,二者都繼承自抽象類AbstractStringBuilder。

  • 二者的共同點是均提供了字串的拼接、擷取等操作,無需每次變動都建立新的物件,一次建立,最後只需通過toString生成最終的字串物件即可。

  • 二者的區別在於在字串拼接等操作上StringBuffer使用synchronized關鍵字修飾方法,多執行緒情況下保證了執行緒安全,當然相比StringBuilder,也降低了效能。

通過原始碼來看動態拼接字串邏輯,StringBuilder和StringBuffer都是呼叫父類的append方法實現,因此二者實現邏輯是一致的,其中count是用來記錄value陣列已經使用的長度,即在現有的value陣列後面拼接上新的str字串。因此在StringBuilder中,程式碼中的 count+=len 不是原子操作,多執行緒操作的情況下,例如當前count為5,可能出現多個執行緒拿到的count都是5,分別執行累加操作後再賦給count,得到的count只是6,此時便出現了執行緒不安全的情況。

【Java】String、StringBuilder和StringBuffer

附:雖然上面提到,通過 + 號拼接String物件時,java也會隱式地建立StringBuilder物件進行拼接,通常情況下並不會造成效能效率損失,但在需要大量迴圈拼接字串時,使用+拼接,會在每次迴圈時都建立一個StringBuilder物件,迴圈結束後,記憶體中會多出許多無用的StringBuilder物件,因此這種情況,建議在迴圈體外建立StringBuilder物件,迴圈呼叫其append()方法拼接,如此只需建立一個StringBuilder物件。

【Java】String、StringBuilder和StringBuffer

擴容

首先注意二者的初始容量都是16,當然也可以通過引數指定初始容量

1 public StringBuilder() {
2         super(16);
3     }
4 public StringBuffer() {
5         super(16);
6     }

在字串拼接時,都會呼叫父類AbstractStringBuilder的append方法,方法原始碼如下

 1 public AbstractStringBuilder append(String str) {
 2         if (str == null) {
 3             return appendNull();
 4         }
 5         int len = str.length();
 6         ensureCapacityInternal(count + len);
 7         putStringAt(count, str);
 8         count += len;
 9         return this;
10     }

其中的 ensureCapacityInternal 方法,就是在拼接前進行擴容

1 private void ensureCapacityInternal(int minimumCapacity) {
2         // overflow-conscious code
3         int oldCapacity = value.length >> coder;
4         if (minimumCapacity - oldCapacity > 0) {
5             value = Arrays.copyOf(value,
6                     newCapacity(minimumCapacity) << coder);
7         }
8     }

其中的minimumCapacity就是擴容後的字串長度,超過目前容量的話,呼叫newCapacity方法執行擴容,可以看出每次擴容後的容量是原容量的2倍加2,如果仍不夠,就直接擴容到需要的長度。

 1 private int newCapacity(int minCapacity) {
 2         // overflow-conscious code
 3         int oldCapacity = value.length >> coder;
 4         int newCapacity = (oldCapacity << 1) + 2;
 5         if (newCapacity - minCapacity < 0) {
 6             newCapacity = minCapacity;
 7         }
 8         int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;
 9         return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
10             ? hugeCapacity(minCapacity)
11             : newCapacity;
12     }