上節介紹了String,提到如果字符串修改操作比較頻繁,應該采用StringBuilder和StringBuffer類,這兩個類的方法基本是完全一樣的,它們的實現代碼也幾乎一樣,唯一的不同就在於,StringBuffer是線程安全的,而StringBuilder不是。
線程以及線程安全的概念,我們在後續章節再詳細介紹。這裡需要知道的就是,線程安全是有成本的,影響性能,而字符串對象及操作,大部分情況下,沒有線程安全的問題,適合使用StringBuilder。所以,本節就只討論StringBuilder。
StringBuilder的基本用法也是很簡單的,我們來看下。
基本用法
創建StringBuilder
StringBuilder sb = new StringBuilder();
添加字符串,通過append方法
sb.append("老馬說編程"); sb.append(",探索編程本質");
獲取構建後的字符串,通過toString方法
System.out.println(sb.toString());
輸出為:
老馬說編程,探索編程本質
大部分情況,使用就這麼簡單,通過new新建StringBuilder,通過append添加字符串,然後通過toString獲取構建完成的字符串。
StringBuilder是怎麼實現的呢?
基本實現原理
內部組成和構造方法
與String類似,StringBuilder類也封裝了一個字符數組,定義如下:
char[] value;
與String不同,它不是final的,可以修改。另外,與String不同,字符數組中不一定所有位置都已經被使用,它有一個實例變量,表示數組中已經使用的字符個數,定義如下:
int count;
StringBuilder繼承自AbstractStringBuilder,它的默認構造方法是:
public StringBuilder() { super(16); }
調用父類的構造方法,父類對應的構造方法是:
AbstractStringBuilder(int capacity) { value = new char[capacity]; }
也就是說,new StringBuilder()這句代碼,內部會創建一個長度為16的字符數組,count的默認值為0。
append的實現
來看append的代碼:
public AbstractStringBuilder append(String str) { if (str == null) str = "null"; int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }
append會直接拷貝字符到內部的字符數組中,如果字符數組長度不夠,會進行擴展,實際使用的長度用count體現。具體來說,ensureCapacityInternal(count+len)會確保數組的長度足以容納新添加的字符,str.getChars會拷貝新添加的字符到字符數組中,count+=len會增加實際使用的長度。
ensureCapacityInternal的代碼如下:
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) expandCapacity(minimumCapacity); }
如果字符數組的長度小於需要的長度,則調用expandCapacity進行擴展,expandCapacity的代碼是:
void expandCapacity(int minimumCapacity) { int newCapacity = value.length * 2 + 2; if (newCapacity - minimumCapacity < 0) newCapacity = minimumCapacity; if (newCapacity < 0) { if (minimumCapacity < 0) // overflow throw new OutOfMemoryError(); newCapacity = Integer.MAX_VALUE; } value = Arrays.copyOf(value, newCapacity); }
擴展的邏輯是,分配一個足夠長度的新數組,然後將原內容拷貝到這個新數組中,最後讓內部的字符數組指向這個新數組,這個邏輯主要靠下面這句代碼實現:
value = Arrays.copyOf(value, newCapacity);
下節我們討論Arrays類,本節就不介紹了,我們主要看下newCapacity是怎麼算出來的。
參數minimumCapacity表示需要的最小長度,需要多少分配多少不就行了嗎?不行,因為那就跟String一樣了,每append一次,都會進行一次內存分配,效率低下。這裡的擴展策略,是跟當前長度相關的,當前長度乘以2,再加上2,如果這個長度不夠最小需要的長度,才用minimumCapacity。
比如說,默認長度為16,長度不夠時,會先擴展到16*2+2即34,然後擴展到34*2+2即70,然後是70*2+2即142,這是一種指數擴展策略。為什麼要加2?大概是因為在原長度為0時也可以一樣工作吧。
為什麼要這麼擴展呢?這是一種折中策略,一方面要減少內存分配的次數,另一方面也要避免空間浪費。在不知道最終需要多長的情況下,指數擴展是一種常見的策略,廣泛應用於各種內存分配相關的計算機程序中。
那如果預先就知道大概需要多長呢?可以調用StringBuilder的另外一個構造方法:
public StringBuilder(int capacity)
toString實現
字符串構建完後,我們來看toString代碼:
public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
基於內部數組新建了一個String,注意,這個String構造方法不會直接用value數組,而會新建一個,以保證String的不可變性。
更多構造方法和append方法
String還有兩個構造方法,分別接受String和CharSequence參數,它們的代碼分別如下:
public StringBuilder(String str) { super(str.length() + 16); append(str); } public StringBuilder(CharSequence seq) { this(seq.length() + 16); append(seq); }
邏輯也很簡單,額外多分配16個字符的空間,然後調用append將參數字符添加進來。
append有多種重載形式,可以接受各種類型的參數,將它們轉換為字符,添加進來,這些重載方法有:
public StringBuilder append(boolean b) public StringBuilder append(char c) public StringBuilder append(double d) public StringBuilder append(float f) public StringBuilder append(int i) public StringBuilder append(long lng) public StringBuilder append(char[] str) public StringBuilder append(char[] str, int offset, int len) public StringBuilder append(Object obj) public StringBuilder append(StringBuffer sb) public StringBuilder append(CharSequence s) public StringBuilder append(CharSequence s, int start, int end)
具體實現比較直接,就不贅述了。
還有一個append方法,可以添加一個Code Point:
public StringBuilder appendCodePoint(int codePoint)
如果codePoint為BMP字符,則添加一個char,否則添加兩個char。如果不清楚Code Point的概念,請參見剖析包裝類 (下)。
其他修改方法
除了append, StringBuilder還有一些其他修改方法,我們來看下。
插入
public StringBuilder insert(int offset, String str)
在指定索引offset處插入字符串str,原來的字符後移,offset為0表示在開頭插,為length()表示在結尾插,比如說:
StringBuilder sb = new StringBuilder(); sb.append("老馬說編程"); sb.insert(0, "關注"); sb.insert(sb.length(), "老馬和你一起探索編程本質"); sb.insert(7, ","); System.out.println(sb.toString());
輸出為
關注老馬說編程,老馬和你一起探索編程本質
來看下insert的實現代碼:
public AbstractStringBuilder insert(int offset, String str) { if ((offset < 0) || (offset > length())) throw new StringIndexOutOfBoundsException(offset); if (str == null) str = "null"; int len = str.length(); ensureCapacityInternal(count + len); System.arraycopy(value, offset, value, offset + len, count - offset); str.getChars(value, offset); count += len; return this; }
這個實現思路是,在確保有足夠長度後,首先將原數組中offset開始的內容向後挪動n個位置,n為待插入字符串的長度,然後將待插入字符串拷貝進offset位置。
挪動位置調用了System.arraycopy方法,這是個比較常用的方法,它的聲明如下:
public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
將數組src中srcPos開始的length個元素拷貝到數組dest中destPos處。這個方法有個優點,即使src和dest是同一個數組,它也可以正確的處理,比如說,看下面代碼:
int[] arr = new int[]{1,2,3,4}; System.arraycopy(arr, 1, arr, 0, 3); System.out.println(arr[0]+","+arr[1]+","+arr[2]);
這裡,src和dest都是arr,srcPos為1,destPos為0,length為3,表示將第二個元素開始的三個元素移到開頭,所以輸出為:
2,3,4
arraycopy的聲明有個修飾符native,表示它的實現是通過Java本地接口實現的,Java本地接口是Java提供的一種技術,用於在Java中調用非Java語言實現的代碼,實際上,arraycopy是用C++語言實現的。為什麼要用C++語言實現呢?因為這個功能非常常用,而C++的實現效率要遠高於Java。
其他插入方法
與append類似,insert也有很多重載的方法,如下列舉一二
public StringBuilder insert(int offset, double d) public StringBuilder insert(int offset, Object obj)
刪除
刪除指定范圍內的字符
public StringBuilder delete(int start, int end)
其實現代碼為:
public AbstractStringBuilder delete(int start, int end) { if (start < 0) throw new StringIndexOutOfBoundsException(start); if (end > count) end = count; if (start > end) throw new StringIndexOutOfBoundsException(); int len = end - start; if (len > 0) { System.arraycopy(value, start+len, value, start, count-end); count -= len; } return this; }
也是通過System.arraycopy實現的,System.arraycopy被大量應用於StringBuilder的內部實現中,後文就不再贅述了。
刪除一個字符
public StringBuilder deleteCharAt(int index)
替換
public StringBuilder replace(int start, int end, String str)
如
StringBuilder sb = new StringBuilder(); sb.append("老馬說編程"); sb.replace(3, 5, "Java"); System.out.println(sb.toString());
程序輸出為:
老馬說Java
替換一個字符
public void setCharAt(int index, char ch)
翻轉字符串
public StringBuilder reverse()
這個方法不只是簡單的翻轉數組中的char,對於增補字符,簡單翻轉後字符就無效了,這個方法能保證其字符依然有效,這是通過單獨檢查增補字符,進行二次翻轉實現的。比如說:
StringBuilder sb = new StringBuilder(); sb.append("a"); sb.appendCodePoint(0x2F81A);//增補字符: