從JDK原始碼看StringBuffer

超人汪小建發表於2018-06-03

概況

Java 中處理字串時經常使用的 String 是一個常量,一旦建立後不能被改變。為了提供可修改的操作,引入了 StringBuilder 類,可看前面的文章《從JDK原始碼看StringBuilder》。但它不是執行緒安全的,只用在單執行緒場景下。所以引入了執行緒安全的 StringBuffer 類,用於多執行緒場景。

總的來說主要是通過在必要的方法上加 synchronized 來實現執行緒安全。

三種字串類關係

這裡寫圖片描述

繼承結構

--java.lang.Object
  --java.lang.AbstractStringBuilder
    --java.lang.StringBuffer
複製程式碼

類定義

public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence
複製程式碼

StringBuffer 類被宣告為 final,說明它不能再被繼承。同時它繼承了 AbstractStringBuilder 類,並實現了 Serializable 和 CharSequence 兩個介面。

其中 Serializable 介面表明其可以序列化。

CharSequence 介面用來實現獲取字元序列的相關資訊,介面定義如下:

  • length()獲取字元序列長度。
  • charAt(int index)獲取某個索引對應字元。
  • subSequence(int start, int end)獲取指定範圍子字串。
  • toString()轉成字串物件。
  • chars()用於獲取字元序列的字元的 int 型別值的流,該介面提供了預設的實現。
  • codePoints()用於獲取字元序列的程式碼點的 int 型別的值的流,提供了預設的實現。
public interface CharSequence {

    int length();

    char charAt(int index);

    CharSequence subSequence(int start, int end);

    public String toString();

    public default IntStream chars() {
        省略程式碼。。
    }

    public default IntStream codePoints() {
        省略程式碼。。
    }
}
複製程式碼

主要屬性

private transient String toStringCache;
byte[] value;
byte coder;
int count;
複製程式碼
  • toStringCache 用於快取呼叫toString方法生成的 String 物件,避免每次都要根據編碼生成 String 物件。
  • value 該陣列用於儲存字串值。
  • coder 表示該字串物件所用的編碼器。
  • count 表示該字串物件中已使用的字元數。

構造方法

有若干種構造方法,可以指定容量大小引數,如果沒有指定則構造方法預設建立容量為16的字串物件。如果 COMPACT_STRINGS 為 true,即使用緊湊佈局則使用 LATIN1 編碼(ISO-8859-1編碼),則開闢長度為16的 byte 陣列。而如果是 UTF16 編碼則開闢長度為32的 byte 陣列。

public StringBuffer() {
        super(16);
    }
    
AbstractStringBuilder(int capacity) {
        if (COMPACT_STRINGS) {
            value = new byte[capacity];
            coder = LATIN1;
        } else {
            value = StringUTF16.newBytesFor(capacity);
            coder = UTF16;
        }
    }
    
public StringBuffer(int capacity) {
        super(capacity);
    }
複製程式碼

如果建構函式傳入的引數為 String 型別,則會開闢長度為str.length() + 16的 byte 陣列,並通過append方法將字串物件新增到 byte 陣列中。

public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }
複製程式碼

類似地,傳入引數為 CharSequence 型別時也做相同處理。

public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }
複製程式碼

主要方法

為了實現執行緒安全,其實最簡單也可能是最沒效率的方法就是通過對某些方法進行同步,以此允許併發操作。所以 StringBuffer 和 StringBuilder 其實實現邏輯幾乎都一樣,並且抽象到 AbstractStringBuilder 抽象類中來實現,只是 StringBuffer 將一些必要的方法進行同步處理了。

StringBuffer 中大多數方法都只是加了 synchronized。

比如下面該方法加了同步來保證計數的準確性。此外還包含很多其他方法,比如codePointCountcapacityensureCapacitycodePointAtcodePointBeforecharAtgetCharssetCharAtsubstringsubSequenceindexOflastIndexOfgetBytes

@Override
public synchronized int length() {
        return count;
    }
    
@Override
public synchronized void setLength(int newLength) {
        toStringCache = null;
        super.setLength(newLength);
    }

複製程式碼

trimToSize方法

該方法用於將該 StringBuffer 物件的容量壓縮到與字串長度大小相等。重寫了該方法,主要是新增了同步,保證了陣列複製過程的準確性。

@Override
public synchronized void trimToSize() {
        super.trimToSize();
    }

public void trimToSize() {
        int length = count << coder;
        if (length < value.length) {
            value = Arrays.copyOf(value, length);
        }
    }
複製程式碼

append方法

有多個append方法,都只是傳入的引數不同而已,同樣是使用了 synchronized,另外它還會清理快取 toStringCache,這是因為 append 後的字串的值已經變了,所以需要重置快取。重置快取的方法還包括:appendCodePointdeletedeleteCharAtreplaceinsertreverse

@Override
public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
複製程式碼

toString方法

使用同步操作,先判斷快取是否為空,如果為空則先根據編碼(Latin1 或 UTF16)建立對應編碼佔位的 String 物件,然後建立新 String 物件並返回。

@Override
public synchronized String toString() {
        if (toStringCache == null) {
            return toStringCache =
                    isLatin1() ? StringLatin1.newString(value, 0, count)
                               : StringUTF16.newString(value, 0, count);
        }
        return new String(toStringCache);
    }
複製程式碼

writeObject方法

該方法是序列化方法,分別將 value、count、shared 欄位的值寫入。

private synchronized void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        java.io.ObjectOutputStream.PutField fields = s.putFields();
        char[] val = new char[capacity()];
        if (isLatin1()) {
            StringLatin1.getChars(value, 0, count, val, 0);
        } else {
            StringUTF16.getChars(value, 0, count, val, 0);
        }
        fields.put("value", val);
        fields.put("count", count);
        fields.put("shared", false);
        s.writeFields();
    }
複製程式碼

readObject方法

該方法是反序列方法,分別讀取 value 和 count,並且初始化物件內的位元組陣列和編碼標識。

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        java.io.ObjectInputStream.GetField fields = s.readFields();
        char[] val = (char[])fields.get("value", null);
        initBytes(val, 0, val.length);
        count = fields.get("count", 0);
    }
    
void initBytes(char[] value, int off, int len) {
        if (String.COMPACT_STRINGS) {
            this.value = StringUTF16.compress(value, off, len);
            if (this.value != null) {
                this.coder = LATIN1;
                return;
            }
        }
        this.coder = UTF16;
        this.value = StringUTF16.toBytes(value, off, len);
    }
複製程式碼

-------------推薦閱讀------------

我的2017文章彙總——機器學習篇

我的2017文章彙總——Java及中介軟體

我的2017文章彙總——深度學習篇

我的2017文章彙總——JDK原始碼篇

我的2017文章彙總——自然語言處理篇

我的2017文章彙總——Java併發篇


跟我交流,向我提問:

這裡寫圖片描述

公眾號的選單已分為“讀書總結”、“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。

為什麼寫《Tomcat核心設計剖析》

歡迎關注:

這裡寫圖片描述

相關文章