概況
在 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
e
和f
a
l
s
e
新增到 StringBuilder 物件的位元組陣列中。 - 如果為 UTF16 編碼,則按照編碼格式將
t
r
u
e
和f
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);
}
複製程式碼
-------------推薦閱讀------------
跟我交流,向我提問:
公眾號的選單已分為“讀書總結”、“分散式”、“機器學習”、“深度學習”、“NLP”、“Java深度”、“Java併發核心”、“JDK原始碼”、“Tomcat核心”等,可能有一款適合你的胃口。
歡迎關注: