Java編程的邏輯 (30) - 剖析StringBuilder


本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營鏈接http://item.jd.com/12299018.html


上節介紹了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方法

StringBuilder還有兩個構造方法,分別接受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);//增補字符:冬
sb.append("b");
sb.reverse();
System.out.println(sb.toString()); 

即使內含增補字符"冬",輸出也是正確的,為:

b冬a 

長度方法

StringBuilder中有一些與長度有關的方法

確保字符數組長度不小於給定值

public void ensureCapacity(int minimumCapacity)

返回字符數組的長度

public int capacity() 

返回數組實際使用的長度

public int length()

注意capacity()方法與length()方法的的區別,capacity返回的是value數組的長度,length返回的是實際使用的字符個數,是count實例變量的值。

直接修改長度

public void setLength(int newLength) 

代碼為:

public void setLength(int newLength) {
    if (newLength < 0)
        throw new StringIndexOutOfBoundsException(newLength);
    ensureCapacityInternal(newLength);

    if (count < newLength) {
        for (; count < newLength; count++)
            value[count] = '\0';
    } else {
        count = newLength;
    }
}

count設為newLength,如果原count小於newLength,則多出來的字符設置默認值為'\0'。

縮減使用的空間

 public void trimToSize()

代碼為:

public void trimToSize() {
    if (count < value.length) {
        value = Arrays.copyOf(value, count);
    }
}

減少value占用的空間,新建了一個剛好夠用的空間。

與String類似的方法

StringBuilder中也有一些與String類似的方法,如:

查找子字符串

public int indexOf(String str)
public int indexOf(String str, int fromIndex)
public int lastIndexOf(String str)
public int lastIndexOf(String str, int fromIndex) 

取子字符串

public String substring(int start)
public String substring(int start, int end)
public CharSequence subSequence(int start, int end)

獲取其中的字符或Code Point

public char charAt(int index)
public int codePointAt(int index)
public int codePointBefore(int index)
public int codePointCount(int beginIndex, int endIndex)
public void getChars(int srcBegin, int srcEnd, char[] dst, int dstBegin)

以上這些方法與String中的基本一樣,本節就不再贅述了。

String的+和+=運算符

Java中,String可以直接使用+和+=運算符,這是Java編譯器提供的支持,背后,Java編譯器會生成StringBuilder,+和+=操作會轉換為append。比如說,如下代碼:

String hello = "hello";
hello+=",world";
System.out.println(hello);

背后,Java編譯器會轉換為:

StringBuilder hello = new StringBuilder("hello");
hello.append(",world");
System.out.println(hello.toString());

既然直接使用+和+=就相當於使用StringBuilder和append,那還有什么必要直接使用StringBuilder呢?在簡單的情況下,確實沒必要。不過,在稍微復雜的情況下,Java編譯器沒有那么智能,它可能會生成很多StringBuilder,尤其是在有循環的情況下,比如說,如下代碼:

String hello = "hello";
for(int i=0;i<3;i++){
    hello+=",world";    
}
System.out.println(hello);

Java編譯器轉換后的代碼大概如下所示:

String hello = "hello";
for(int i=0;i<3;i++){
    StringBuilder sb = new StringBuilder(hello);
    sb.append(",world");
    hello = sb.toString();
}
System.out.println(hello);

在循環內部,每一次+=操作,都會生成一個StringBuilder。

所以,結論是,對於簡單的情況,可以直接使用String的+和+=,對於復雜的情況,尤其是有循環的時候,應該直接使用StringBuilder。

小結

本節介紹了StringBuilder,介紹了其用法,實現原理,數組長度擴展策略,以及String的+和+=操作符的實現原理。

字符串操作是計算機程序中最常見的操作,理解了String和StringBuilder的用法及實現原理,我們就對字符串操作建立了一個堅實的基礎。

上節和本節,我們都提到了一個類Arrays,它包括很多數組相關的方法,數組操作也是非常常見的操作,讓我們下節來詳細討論。

----------------

未完待續,查看最新文章,敬請關注微信公眾號“老馬說編程”(掃描下方二維碼),從入門到高級,深入淺出,老馬和你一起探索Java編程及計算機技術的本質。用心寫作,原創文章,保留所有版權。

-----------

相關好評原創文章

(6)  如何從亂碼中恢復 (上)?

(7)  如何從亂碼中恢復 (下)?

(8)  char的真正含義

(28) 剖析包裝類 - Character

(29) 剖析String

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM