從JDK原始碼看StringBuilder

超人汪小建發表於2018-05-25

概況

在 Java 中處理字串時經常會使用 String 類,實際上 String 物件的值是一個常量,一旦建立後不能被改變。正是因為其不可變,所以也無法進行修改操作,只有不斷地 new 出新的 String 物件。

為此 Java 引入了可變字串變數 StringBuilder 類,它不是執行緒安全的,只用在單執行緒場景下。

繼承結構

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

類定義

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

StringBuilder 類被宣告為 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() {
        省略程式碼。。
    }
}
複製程式碼

主要屬性

byte[] value;
byte coder;
int count;
複製程式碼
  • value 該陣列用於儲存字串值。
  • coder 表示該字串物件所用的編碼器。
  • count 表示該字串物件中已使用的字元數。

構造方法

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

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

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

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

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

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

主要方法

append方法

有多個append方法,都只是傳入的引數不同而已,下面挑幾個典型的深入看看,其他都是類似的處理。


如果傳入 String 型別引數則呼叫父類的append方法將字串物件新增到 StringBuilder 的 byte 陣列中,然後返回 this。append 的邏輯為:

  • String 物件為 null的話則在 StringBuilder 的 byte 陣列中新增n u l l四個字元。
  • 通過ensureCapacityInternal方法確保有足夠的空間,如果沒有則需要重新開闢空間。
  • 通過putStringAt方法將字串物件裡面的 byte 陣列複製到 StringBuilder 的 byte 陣列中,使用了System.arraycopy進行復制。
  • count 為已使用的字元數,將其加上覆制的字串長度。
  • 返回 this。
public StringBuilder append(String str) {
        super.append(str);
        return this;
    }
    
public AbstractStringBuilder append(String str) {
        if (str == null) {
            return appendNull();
        }
        int len = str.length();
        ensureCapacityInternal(count + len);
        putStringAt(count, str);
        count += len;
        return this;
    }

複製程式碼

ensureCapacityInternal方法邏輯:

  • 首先獲取現有的容量大小。
  • 如果需要的容量大於現有容量,則需要擴充容量,並且將原來的陣列複製過來。
  • newCapacity方法用於確定新容量大小,將現有容量大小擴大一倍再加上2,如果還是不夠大則直接等於需要的容量大小,另外,如果新容量大小為負則容量設定為MAX_ARRAY_SIZE,它的大小等於Integer.MAX_VALUE - 8
private void ensureCapacityInternal(int minimumCapacity) {
        int oldCapacity = value.length >> coder;
        if (minimumCapacity - oldCapacity > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity) << coder);
        }
    }

private int newCapacity(int minCapacity) {
        int oldCapacity = value.length >> coder;
        int newCapacity = (oldCapacity << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }
        int SAFE_BOUND = MAX_ARRAY_SIZE >> coder;
        return (newCapacity <= 0 || SAFE_BOUND - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }
複製程式碼

putStringAt的邏輯:

  • String 物件的編碼和 StringBuilder 物件的編碼不相同,則先執行inflate方法轉換成 UTF16 編碼。
  • 如果 StringBuilder 物件不是 Latin1 編碼則不執行轉換。
  • 通過StringUTF16.newBytesFor擴充空間,因為UTF16編碼的佔位是 Latin1 編碼的兩倍。
  • 通過StringLatin1.inflate將原來的值拷貝到擴充後的空間中。
  • 通過str.getBytes將 String 物件的值拷貝到 StringBuilder 物件中。
private final void putStringAt(int index, String str) {
        if (getCoder() != str.coder()) {
            inflate();
        }
        str.getBytes(value, index, coder);
    }

private void inflate() {
        if (!isLatin1()) {
            return;
        }
        byte[] buf = StringUTF16.newBytesFor(value.length);
        StringLatin1.inflate(value, 0, buf, 0, count);
        this.value = buf;
        this.coder = UTF16;
    }
複製程式碼

傳入的引數為 CharSequence 型別時,他會分幾種情況處理,如果為空則新增n u l l字元。另外還會根據物件例項化自 String 型別或 AbstractStringBuilder 型別呼叫對應的append方法。

public StringBuilder append(CharSequence s) {
        super.append(s);
        return this;
    }
    
public AbstractStringBuilder append(CharSequence s) {
        if (s == null) {
            return appendNull();
        }
        if (s instanceof String) {
            return this.append((String)s);
        }
        if (s instanceof AbstractStringBuilder) {
            return this.append((AbstractStringBuilder)s);
        }
        return this.append(s, 0, s.length());
    }
複製程式碼

傳入的引數為 char 陣列型別時,邏輯如下:

  • 通過ensureCapacityInternal方法確保足夠容量。
  • append 過程中根據不同編碼做不同處理。
  • 如果是 Latin1 編碼,從偏移量開始將一個個字元賦值到 StringBuilder 物件的位元組陣列中,這個過程中會檢測每個字元是否可以使用 Latin1 編碼來解碼,可以的話則直接將 char 轉成 byte 並進行賦值操作。否則為 UTF16 編碼,此時先通過inflate()擴充套件空間,然後再通過StringUTF16.putCharsSB將所有剩下的字串以 UTF16 編碼儲存到 StringBuilder 物件中。
  • 如果是 UTF16 編碼,則直接通過StringUTF16.putCharsSB將 char 陣列新增到 StringBuilder 物件中。
  • 修改 count 屬性,即已使用的位元組數。
public StringBuilder append(char[] str) {
        super.append(str);
        return this;
    }

public AbstractStringBuilder append(char[] str) {
        int len = str.length;
        ensureCapacityInternal(count + len);
        appendChars(str, 0, len);
        return this;
    }

private final void appendChars(char[] s, int off, int end) {
        int count = this.count;
        if (isLatin1()) {
            byte[] val = this.value;
            for (int i = off, j = count; i < end; i++) {
                char c = s[i];
                if (StringLatin1.canEncode(c)) {
                    val[j++] = (byte)c;
                } else {
                    this.count = count = j;
                    inflate();
                    StringUTF16.putCharsSB(this.value, j, s, i, end);
                    this.count = count + end - i;
                    return;
                }
            }
        } else {
            StringUTF16.putCharsSB(this.value, count, s, off, end);
        }
        this.count = count + end - off;
    }
複製程式碼

傳入的引數為 boolean 型別時,邏輯如下:

  • 通過ensureCapacityInternal確定容量足夠大,true 和 false 的長度分別為4和5。
  • 如果為 Latin1 編碼,按條件將t r u ef a l s e新增到 StringBuilder 物件的位元組陣列中。
  • 如果為 UTF16 編碼,則按照編碼格式將 t r u ef a l s e新增到 StringBuilder 物件的位元組陣列中。
public StringBuilder append(boolean b) {
        super.append(b);
        return this;
    }

public AbstractStringBuilder append(boolean b) {
        ensureCapacityInternal(count + (b ? 4 : 5));
        int count = this.count;
        byte[] val = this.value;
        if (isLatin1()) {
            if (b) {
                val[count++] = 't';
                val[count++] = 'r';
                val[count++] = 'u';
                val[count++] = 'e';
            } else {
                val[count++] = 'f';
                val[count++] = 'a';
                val[count++] = 'l';
                val[count++] = 's';
                val[count++] = 'e';
            }
        } else {
            if (b) {
                count = StringUTF16.putCharsAt(val, count, 't', 'r', 'u', 'e');
            } else {
                count = StringUTF16.putCharsAt(val, count, 'f', 'a', 'l', 's', 'e');
            }
        }
        this.count = count;
        return this;
    }
複製程式碼

如果傳入的引數為 int 或 long 型別,則處理的大致邏輯都為先計算整數一共多少位數,然後再一個個放到 StringBuilder 物件的位元組陣列中。比如“789”,長度為3,對於 Latin1 編碼則佔3個位元組,而 UTF16 編碼佔6個位元組。

public StringBuilder append(int i) {
        super.append(i);
        return this;
    }

public StringBuilder append(long lng) {
        super.append(lng);
        return this;
    }
複製程式碼

如果傳入的引數為 float 或 double 型別,則處理的大致邏輯都為先計算浮點數一共多少位數,然後再一個個放到 StringBuilder 物件的位元組陣列中。比如“789.01”,長度為6,注意點也佔空間,對於 Latin1 編碼則佔6個位元組,而 UTF16 編碼佔12個位元組。

public StringBuilder append(float f) {
        super.append(f);
        return this;
    }

public StringBuilder append(double d) {
        super.append(d);
        return this;
    }
複製程式碼

appendCodePoint方法

該方法用於往 StringBuilder 物件中新增程式碼點。程式碼點是 unicode 編碼給字元分配的唯一整數,unicode 有17個程式碼平面,其中的基本多語言平面(Basic Multilingual Plane,BMP)包含了主要常見的字元,其餘平面叫做補充平面。

所以這裡先通過Character.isBmpCodePoint判斷是否屬於 BMP 平面,如果屬於該平面,此時只需要2個位元組,則直接轉成 char 型別並新增到 StringBuilder 物件。如果超出 BMP 平面,此時需要4個位元組,分別用來儲存 High-surrogate 和 Low-surrogate,通過Character.toChars完成獲取對應4個位元組並新增到 StringBuilder 物件中。

public StringBuilder appendCodePoint(int codePoint) {
        super.appendCodePoint(codePoint);
        return this;
    }
    
public AbstractStringBuilder appendCodePoint(int codePoint) {
        if (Character.isBmpCodePoint(codePoint)) {
            return append((char)codePoint);
        }
        return append(Character.toChars(codePoint));
    }


複製程式碼

delete方法

該方法用於將指定範圍的字元刪掉,邏輯為:

  • end 不能大於已使用字元數 count,大於的話則令其等於 count。
  • 通過checkRangeSIOOBE檢查範圍合法性。
  • 通過shift方法實現刪除操作,其通過System.arraycopy來實現,即把 end 後面的字串複製到 start 位置,即相當於將中間的字元刪掉。
  • 修改已使用字元數 count 值。
  • 返回 this。
public StringBuilder delete(int start, int end) {
        super.delete(start, end);
        return this;
    }
    
public AbstractStringBuilder delete(int start, int end) {
        int count = this.count;
        if (end > count) {
            end = count;
        }
        checkRangeSIOOBE(start, end, count);
        int len = end - start;
        if (len > 0) {
            shift(end, -len);
            this.count = count - len;
        }
        return this;
    }

private void shift(int offset, int n) {
        System.arraycopy(value, offset << coder,
                         value, (offset + n) << coder, (count - offset) << coder);
    }
複製程式碼

deleteCharAt方法

刪除指定索引字元,與 delete 方法實現一樣,通過shift方法實現刪除,修改 count 值。

public StringBuilder deleteCharAt(int index) {
        super.deleteCharAt(index);
        return this;
    }

public AbstractStringBuilder deleteCharAt(int index) {
        checkIndex(index, count);
        shift(index + 1, -1);
        count--;
        return this;
    }
複製程式碼

replace方法

該方法用於將指定範圍的字元替換成指定字串。邏輯如下:

  • end 不能大於已使用字元數 count,大於的話則令其等於 count。
  • 通過checkRangeSIOOBE檢查範圍合法性。
  • 計算新 count。
  • 通過shift方法把 end 後面的字串複製到 end + (newCount - count) 位置。
  • 更新 count。
  • 通過putStringAt將字串放到 start 後,直接覆蓋掉後面的若干字元即可。
public StringBuilder replace(int start, int end, String str) {
        super.replace(start, end, str);
        return this;
    }
    
public AbstractStringBuilder replace(int start, int end, String str) {
        int count = this.count;
        if (end > count) {
            end = count;
        }
        checkRangeSIOOBE(start, end, count);
        int len = str.length();
        int newCount = count + len - (end - start);
        ensureCapacityInternal(newCount);
        shift(end, newCount - count);
        this.count = newCount;
        putStringAt(start, str);
        return this;
    }
複製程式碼

insert方法

該方法用於向 StringBuilder 物件中插入字元。根據傳入的引數型別有若干個 insert 方法,操作都相似,深入看重點一個。

當傳入的引數為 String 型別時,邏輯為:

  • 通過checkOffset檢查偏移量的合法性。
  • 如果字串為空,則將null字串賦值給它。
  • 通過ensureCapacityInternal確保足夠的容量。
  • 通過shift方法把 offset 後面的字串複製到 offset+len 位置。
  • 更新 count。
  • 將 str 放到 offset 位置,完成插入操作。
  • 返回 this。
public StringBuilder insert(int offset, String str) {
        super.insert(offset, str);
        return this;
    }
    
public AbstractStringBuilder insert(int offset, String str) {
        checkOffset(offset, count);
        if (str == null) {
            str = "null";
        }
        int len = str.length();
        ensureCapacityInternal(count + len);
        shift(offset, len);
        count += len;
        putStringAt(offset, str);
        return this;
    }
複製程式碼

除此之外,還可能插入 boolean 型別、object 型別、char 型別、char 陣列型別、float 型別、double 型別、long 型別、int 型別和 CharSequence 型別。幾乎都是先轉成 String 型別再插入。

indexOf方法

該方法用於查詢指定字串的索引值,可以從頭開始查詢,也可以指定起始位置。

可以看到它間接呼叫了 String 類的indexOf方法,核心邏輯是如果是 Latin1 編碼則通過StringLatin1.indexOf查詢,而如果是 UTF16 編碼則通過StringUTF16.indexOf查詢。如果要查詢的字串編碼和 StringBuilder 物件的編碼不相同,則通過StringUTF16.indexOfLatin1查詢。

public int indexOf(String str) {
        return super.indexOf(str);
    }
    
public int indexOf(String str, int fromIndex) {
        return super.indexOf(str, fromIndex);
    }
    
public int indexOf(String str, int fromIndex) {
        return String.indexOf(value, coder, count, str, fromIndex);
    }
    
static int indexOf(byte[] src, byte srcCoder, int srcCount,
                       String tgtStr, int fromIndex) {
        byte[] tgt    = tgtStr.value;
        byte tgtCoder = tgtStr.coder();
        int tgtCount  = tgtStr.length();

        if (fromIndex >= srcCount) {
            return (tgtCount == 0 ? srcCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (tgtCount == 0) {
            return fromIndex;
        }
        if (tgtCount > srcCount) {
            return -1;
        }
        if (srcCoder == tgtCoder) {
            return srcCoder == LATIN1
                ? StringLatin1.indexOf(src, srcCount, tgt, tgtCount, fromIndex)
                : StringUTF16.indexOf(src, srcCount, tgt, tgtCount, fromIndex);
        }
        if (srcCoder == LATIN1) {   
            return -1;
        }
        return StringUTF16.indexOfLatin1(src, srcCount, tgt, tgtCount, fromIndex);
    }
複製程式碼

Latin1 編碼的StringLatin1.indexOf的主要邏輯為:先確定要查詢的字串的第一個位元組 first,然後在 value 陣列中遍歷尋找等於 first 的位元組,一旦找到等於第一個位元組的元素,則比較剩下的字串是否相等,如果所有都相等則查詢到指定的位元組陣列,返回該索引值,否則返回-1。

public static int indexOf(byte[] value, int valueCount, byte[] str, int strCount, int fromIndex) {
        byte first = str[0];
        int max = (valueCount - strCount);
        for (int i = fromIndex; i <= max; i++) {
            if (value[i] != first) {
                while (++i <= max && value[i] != first);
            }
            if (i <= max) {
                int j = i + 1;
                int end = j + strCount - 1;
                for (int k = 1; j < end && value[j] == str[k]; j++, k++);
                if (j == end) {
                    return i;
                }
            }
        }
        return -1;
    }
複製程式碼

UTF16 編碼的StringUTF16.indexOf邏輯與 Latin1 編碼類似,只不過是需要兩個位元組合到一起(即比較 char 型別)進行比較。

另外如果源字串的編碼為 UTF16,而查詢的字串編碼為 Latin1 編碼, ,則通過StringUTF16.indexOfLatin1來查詢,查詢邏輯也是類似,只不過需要把一個位元組的 Latin1 編碼轉成兩個位元組的 UTF16 編碼後再比較。

lastIndexOf方法

該方法用於從尾部開始反向查詢指定字串的索引值,可以從最末尾開始查詢,也可以指定末尾位置。它的實現邏輯跟indexOf差不多,只是反過來查詢,這裡不再贅述。

public int lastIndexOf(String str) {
        return super.lastIndexOf(str);
    }

public int lastIndexOf(String str, int fromIndex) {
        return super.lastIndexOf(str, fromIndex);
    }
複製程式碼

reverse方法

該方法用於將字串反轉,實現邏輯如下,其實就是做一個反轉操作,遍歷整個 StringBuilder 物件的陣列,實現反轉。其中分為 LATIN1 編碼和 UTF16 編碼做不同處理。

public StringBuilder reverse() {
        super.reverse();
        return this;
    }
    
public AbstractStringBuilder reverse() {
        byte[] val = this.value;
        int count = this.count;
        int coder = this.coder;
        int n = count - 1;
        if (COMPACT_STRINGS && coder == LATIN1) {
            for (int j = (n-1) >> 1; j >= 0; j--) {
                int k = n - j;
                byte cj = val[j];
                val[j] = val[k];
                val[k] = cj;
            }
        } else {
            StringUTF16.reverse(val, count);
        }
        return this;
    }
複製程式碼

toString方法

該方法用於返回 String 物件,根據不同的編碼分別 new 出 String 物件。其中 UTF16 編碼會嘗試壓縮成 LATIN1 編碼,失敗的話則以 UTF16 編碼生成 String 物件。

public String toString() {
        return isLatin1() ? StringLatin1.newString(value, 0, count)
                          : StringUTF16.newString(value, 0, count);
    }
    
public static String newString(byte[] val, int index, int len) {
        return new String(Arrays.copyOfRange(val, index, index + len),
                          LATIN1);
    }
    
public static String newString(byte[] val, int index, int len) {
        if (String.COMPACT_STRINGS) {
            byte[] buf = compress(val, index, len);
            if (buf != null) {
                return new String(buf, LATIN1);
            }
        }
        int last = index + len;
        return new String(Arrays.copyOfRange(val, index << 1, last << 1), UTF16);
    }
複製程式碼

writeObject方法

該方法是序列化方法,先按預設機制將物件寫入,然後再將 count 和 char 陣列寫入。

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        s.defaultWriteObject();
        s.writeInt(count);
        char[] val = new char[capacity()];
        if (isLatin1()) {
            StringLatin1.getChars(value, 0, count, val, 0);
        } else {
            StringUTF16.getChars(value, 0, count, val, 0);
        }
        s.writeObject(val);
    }
複製程式碼

readObject方法

該方法是反序列方法,先按預設機制讀取物件,再讀取 count 和 char 陣列,最後再初始化物件內的位元組陣列和編碼標識。

private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        count = s.readInt();
        char[] val = (char[]) s.readObject();
        initBytes(val, 0, val.length);
    }
    
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核心設計剖析》

歡迎關注:

這裡寫圖片描述

相關文章