天天看點

Java StringBuffer StringBuilder類源碼解析

StringBuffer

StringBuffer是線程安全的字元動态序列,像String但是可以修改,在任何時點他都含有字元的特定序列,但是序列的長度和内容可以通過調用某些方法來修改。

StringBuffer對于多線程是安全的,在必要的方法上都加了synchronized。核心方法是append和insert,他們通過重載可以接受任何類型的資料。将資料轉換為String然後擴充或者插入到StringBuffer中。append将字元添加到末尾,insert是添加到某個指定的位置。舉個例子,z是一個StringBuffer,目前内容為"start",此時調用z.append("le")則内容變為"startle",若調用的是z.insert(4, "le")則内容變為"starlet"。sb是一個StringBuffer,sb.append(x)和sb.insert(sb.length(), x)是等效的。

當有一個包含源序列的操作發生時,隻有StringBuffer同步操作,不會發生在源上。

由于StringBuffer被設計為線程安全類,是以在通過一個被多個線程共享的源序列構造和append insert操作時,調用的程式必須確定在這些操作期間源序列沒有發生變化。這個可以通過調用者在操作期間加鎖來保證,或者通過使用一個不可變的源序列,或者不使用線程共享的源序列。

除非另外說明,對建構或者其他方法傳入一個null參數會引起抛出NullPointerException錯誤。

JDK5中,補充了StringBuffer的單線程版本StringBuilder,StringBuilder應該優先使用,他有同樣的操作但是沒有synchronized是以速度更快。

内部變量與構造函數

從類的定義中可以看出StringBuffer繼承了AbstractStringBuilder,下面會介紹到複用了AbstractStringBuilder的内部變量與函數

public final class StringBuffer
    extends AbstractStringBuilder
    implements java.io.Serializable, CharSequence           

StringBuffer自身有一個内部變量toStringCache,這是上一個toString傳回值的高速緩存,一旦StringBuffer被修改就會清空,作用是在調用toString的時如果沒有變更可以快速傳回結果不用重新構造字元串

private transient char[] toStringCache;           

觀察StringBuffer的構造函數,可以看到他們都是基于super(capacity)這個方法來展開的,也就是AbstractStringBuilder的構造函數

//構造一個初始大小為16的StringBuffer
    public StringBuffer() {
        super(16);
    }
    //構造指定初始容量大小
    public StringBuffer(int capacity) {
        super(capacity);
    }
    //構造一個StringBuffer,初始内容為str,初始大小為16+str的長度
    public StringBuffer(String str) {
        super(str.length() + 16);
        append(str);
    }
    //構造一個StringBuffer内容和CharSequence一緻,初始容量為16+CharSequence.length,如果CharSequence的長度為0,則傳回一個空的buffer容量為16
    public StringBuffer(CharSequence seq) {
        this(seq.length() + 16);
        append(seq);
    }           

下面的内部變量和構造函數來自AbstractStringBuilder,可以看到他的構造方法主要是新配置設定了一個給定大小的數組

char[] value;//存儲字元

    int count;//字元個數

    AbstractStringBuilder(int capacity) {
        value = new char[capacity];//配置設定一個大小為capacity的字元數組給value
    }           

下面兩個方法是對容量和字元長度的查詢,隻做查詢而不會做出修改

public synchronized int length() {
        return count;//傳回字元個數
    }

    public synchronized int capacity() {
        return value.length;//傳回容量大小也就是數組大小
    }           

而ensureCapacity是會修改數組大小的,他會確定value數組的大小不小于minimumCapacity,如果容量小于該大小,會配置設定一個新的數組并将原本的字元複制到新數組中,新數組大小是目前容量*2+2和minimumCapacity中的較大值,minimumCapacity有大小限制,超過一定的值會記憶體溢出

public synchronized void ensureCapacity(int minimumCapacity) {
        super.ensureCapacity(minimumCapacity);//確定value數組的大小不小于minimumCapacity
    }
//下面的代碼來自父類
    //確定容量不小于最小值,如果目前容量小于參數值,配置設定一個新的更大的内部數組,他的大小是minimumCapacity和舊容量舊容量*2+2中的較大值。如果minimumCapacity是負數,什麼也不做直接傳回。
    public void ensureCapacity(int minimumCapacity) {
        if (minimumCapacity > 0)
            ensureCapacityInternal(minimumCapacity);
    }

    private void ensureCapacityInternal(int minimumCapacity) {
        // overflow-conscious code
        if (minimumCapacity - value.length > 0) {
            value = Arrays.copyOf(value,
                    newCapacity(minimumCapacity));//根據minimumCapacity配置設定一個新的數組,并将原來的字元複制到新的數組中
        }
    }
    //傳回不小于minCapacity的大小,如果目前大小*2+2足夠的話就取該值。不會傳回超過MAX_ARRAY_SIZE的大小,除非minCapacity超過該值
    private int newCapacity(int minCapacity) {
        // overflow-conscious code
        int newCapacity = (value.length << 1) + 2;
        if (newCapacity - minCapacity < 0) {
            newCapacity = minCapacity;
        }//新的大小為舊大小*2+2與minCapacity中的較大值
        return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
            ? hugeCapacity(minCapacity)
            : newCapacity;
    }
    //如果minCapacity在MAX_ARRAY_SIZE到Integer.MAX_VALUE之間的話傳回minCapacity,超過Integer.MAX_VALUE抛出OutOfMemoryError
    private int hugeCapacity(int minCapacity) {
        if (Integer.MAX_VALUE - minCapacity < 0) { // 記憶體溢出
            throw new OutOfMemoryError();
        }
        return (minCapacity > MAX_ARRAY_SIZE)
            ? minCapacity : MAX_ARRAY_SIZE;
    }           

trimToSize在value中存在沒有存儲的空間時,會重新配置設定一個大小和字元個數相等的數組将字元複制過去,提高空間使用率,會改變capacity()的值

public synchronized void trimToSize() {
        super.trimToSize();//新配置設定一個數組僅保留與字元個數相等的大小,将字元複制過去
    }
    //嘗試減少用于存儲字元串的空間。如果緩沖區比儲存目前字元串所需的空間更大,會變更大小提高空間使用率。這個方法可能會改變capacity()的傳回值
    public void trimToSize() {
        if (count < value.length) {
            value = Arrays.copyOf(value, count);//新配置設定一個大小為字元個數的數組,将現有的字元複制過去
        }
    }           

setLength在newLength小于等于目前數組大小時直接傳回,大于時新配置設定一個大小為newLength和目前容量*2+2的較大值的新數組,并複制字元,然後将數組中的剩餘位置填充上'0',count設為newLength

public synchronized void setLength(int newLength) {
        toStringCache = null;//清空上一次toString的緩存
        super.setLength(newLength);
    }

    public void setLength(int newLength) {
        if (newLength < 0)
            throw new StringIndexOutOfBoundsException(newLength);
        ensureCapacityInternal(newLength);//newLength小于等于目前數組大小的話直接傳回,否則配置設定一個大小為newLength和目前容量*2+2的較大值的新數組,并複制字元

        if (count < newLength) {
            Arrays.fill(value, count, newLength, '\0');//字元個數小于newLength時,用'\0'填充剩餘的位置
        }

        count = newLength;//count設為newLength
    }           

charAt傳回指定位置的字元,會檢查index傳回是否大于等于0且小于count

public synchronized char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }   

    public char charAt(int index) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        return value[index];
    }           

codePointAt是傳回index位置的代碼點,代碼點這個東西之前在String裡講過,這裡再貼一次:字元資料類型是一個采用UTF-16編碼表示Unicode代碼點的代碼單元。大多數的常用Unicode字元使用一個代碼單元就可以表示,而輔助字元需要一對代碼單元表示。而length傳回的是UTF-16下的代碼單元的數量,而codePointCount傳回的是代碼點的數量。對于大部分人工輸入的字元,這兩者是相等的,會出現length比codePointCount長的通常是某些數學或者機器符号,需要兩個代碼單元來表示一個代碼點 。codePointBefore傳回index前一個位置的代碼點,codePointCount則是統計指定序列段中的代碼點數量

public synchronized int codePointAt(int index) {
        return super.codePointAt(index);
    }

    public int codePointAt(int index) {
        if ((index < 0) || (index >= count)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return Character.codePointAtImpl(value, index, count);
    }

    public synchronized int codePointBefore(int index) {
        return super.codePointBefore(index);
    }

    public int codePointBefore(int index) {
        int i = index - 1;
        if ((i < 0) || (i >= count)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return Character.codePointBeforeImpl(value, index, 0);
    }

    public synchronized int codePointCount(int beginIndex, int endIndex) {
        return super.codePointCount(beginIndex, endIndex);//統計從beginIndex到endIndex之間的代碼點數量
    }

    public int codePointCount(int beginIndex, int endIndex) {
        if (beginIndex < 0 || endIndex > count || beginIndex > endIndex) {
            throw new IndexOutOfBoundsException();
        }
        return Character.codePointCountImpl(value, beginIndex, endIndex-beginIndex);
    }           

offsetByCodePoints這個方法單看注釋翻譯比較難了解:傳回從index到codePointOffset的代碼點偏移index,每個不成對的代理(兩個代碼單元表示一個代碼點時稱為兩個代理)在範圍内被記為一個代碼點。實際上可以了解為,如果不存在兩個代碼單元表示一個代碼點的情況,傳回的結果就是index+codePointOffset;如果存在那種特殊代碼點,則index的變化量會偏移特殊代碼點的個數,例如有3個特殊代碼點,則傳回值為index+codePointOffset+3(codePointOffset>0)或者index+codePointOffset-3(codePointOffset<0)

public synchronized int offsetByCodePoints(int index, int codePointOffset) {
        return super.offsetByCodePoints(index, codePointOffset);
    }

    public int offsetByCodePoints(int index, int codePointOffset) {
        if (index < 0 || index > count) {
            throw new IndexOutOfBoundsException();
        }
        return Character.offsetByCodePointsImpl(value, 0, count,
                                                index, codePointOffset);
    }           

getChars會再檢查參數範圍後,複制指定位置的字元串到指定的位置

public synchronized void getChars(int srcBegin, int srcEnd, char[] dst,
                                      int dstBegin)
    {
        super.getChars(srcBegin, srcEnd, dst, dstBegin);//複制value從srcBegin到srcEnd的内容到dst從dstBegin開始的位置
    }           

setCharAt修改指定位置的字元

public synchronized void setCharAt(int index, char ch) {
        if ((index < 0) || (index >= count))
            throw new StringIndexOutOfBoundsException(index);
        toStringCache = null;//清空toString緩存
        value[index] = ch;//修改對應位置的字元
    }           

核心函數之一的append有衆多的重載,篇幅原因就不全貼了。append需要注意一點,直接在參數裡輸入null是會報錯的,但是以對象指派null的方式傳入是可行的,相當于添加"null"。對于傳入的非字元串對象,統一調用toString方法轉換為字元串;數值對象的話通過包裝類的方法轉為字元串。

public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

    public AbstractStringBuilder append(String str) {
        if (str == null)
            return appendNull();
        int len = str.length();
        ensureCapacityInternal(count + len);//確定數組容量足夠大
        str.getChars(0, len, value, count);//将str從頭到尾複制到value中從count開始的位置,實作拼接
        count += len;//增加字元數量
        return this;
    }

    private AbstractStringBuilder appendNull() {
        int c = count;
        ensureCapacityInternal(c + 4);
        final char[] value = this.value;
        value[c++] = 'n';//null當做"null"來進行擴充
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;
        return this;
    }           

delete删除包括start在内到end之前的字元,end開始部分保留,通過複制保留部分到start的位置來實作

public synchronized StringBuffer delete(int start, int end) {
        toStringCache = null;//清除toString緩存
        super.delete(start, end);//删除從start到end-1位置的元素
        return this;
    }

    public AbstractStringBuilder delete(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            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);//将start+len開始的長度為count-end的部分,複制到start開始的位置
            count -= len;//修改count值
        }
        return this;
    }           

deleteCharAt隻删除單個字元,也是通過複制來實作

public synchronized StringBuffer deleteCharAt(int index) {
        toStringCache = null;
        super.deleteCharAt(index);//将index後一位開始的内容複制到index的位置
        return this;
    }           

replace操作會移除start到end-1的内容,将str插入到start開始的位置,實作的話會先把value中的後面那段複制到他最終所處的位置,中間留出一段空間供str複制進去

public synchronized StringBuffer replace(int start, int end, String str) {
        toStringCache = null;
        super.replace(start, end, str);//移除start到end-1的内容,将str插入到start開始的位置
        return this;
    }           

substring和subSequence方法截取子串,substring可以不輸入end參數截取到末尾,方法都是基于父類的同一個函數來傳回一個新的String

public String substring(int start, int end) {
        if (start < 0)
            throw new StringIndexOutOfBoundsException(start);
        if (end > count)
            throw new StringIndexOutOfBoundsException(end);
        if (start > end)
            throw new StringIndexOutOfBoundsException(end - start);
        return new String(value, start, end - start);
    }           

insert方法同樣是重載衆多,但是主要參數隻有在value中插入的位置、插入的對象、插入對象從哪裡開始截取、截取長度是多少,後兩個可以不輸入那麼就是整個對象進行插入。會清空toStringCache

public synchronized StringBuffer insert(int index, char[] str, int offset,
                                            int len)
    {
        toStringCache = null;
        super.insert(index, str, offset, len);
        return this;
    }

    public AbstractStringBuilder insert(int index, char[] str, int offset,
                                        int len)
    {
        if ((index < 0) || (index > length()))
            throw new StringIndexOutOfBoundsException(index);
        if ((offset < 0) || (len < 0) || (offset > str.length - len))
            throw new StringIndexOutOfBoundsException(
                "offset " + offset + ", len " + len + ", str.length "
                + str.length);
        ensureCapacityInternal(count + len);//確定空間足夠,不足時擴充為目前容量*2+2和count+len的較大值
        System.arraycopy(value, index, value, index + len, count - index);//将index開始的内容複制到index+len的位置,空出留給str的空間
        System.arraycopy(str, offset, value, index, len);//str複制到留出的空間中
        count += len;//count增加str的長度
        return this;
    }           

indexOf和lastIndexOf兩個方法分别是從頭開始向後尋找第一個完全相等的字元串和從尾部開始從頭尋找第一個,可以指定開始尋找的位置,直接調用了String的同名方法

public synchronized int indexOf(String str, int fromIndex) {
        return super.indexOf(str, fromIndex);//調用了String.indexOf
    }

    public synchronized int lastIndexOf(String str, int fromIndex) {
        return super.lastIndexOf(str, fromIndex);//調用了String.lastIndexOf
    }           

reverse這個方法會逆序字元串内容,從中心開始做軸對稱的交換

public synchronized StringBuffer reverse() {
        toStringCache = null;
        super.reverse();//以中心為軸,從中間點開始做軸對稱位置的字元複制交換
        return this;
    }           

toString有緩存直接傳回,否則建立一個數組複制value裡的有效字元。所有會導緻value中内容變化的方法都會清空緩存,還有setLength無論是否導緻長度變化并填充了'0'都會清空

public synchronized String toString() {
        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);//緩存無效時,建立一個新的數組将value中的有效字元複制進去
        }
        return new String(toStringCache, true);//緩存有效時直接傳回,緩存中的字元串是被共享的
    }           

StringBuilder

JDK1.5加入,同樣繼承了AbstractStringBuilder,實作了java.io.Serializable, CharSequence接口。

StringBuilder是沒有toStringCache的,是以他的toString函數必定是複制産生一個新的String,猜測是出于StringBuilder預設是用于單線程環境,不需要進行共享操作,是以也就沒有了cache

public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }           

StringBuilder在單線程情況下由于沒有了同步鎖性能更好,推薦優先使用。他的實作和StringBuffer除了上面提到的cache和同步的問題外幾乎沒有差別,另外一個有差別的地方是序列化部分。

先看StringBuilder的序列化函數,非常簡單,除了預設對象外隻有count和value的讀寫

private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        s.defaultWriteObject();
        s.writeInt(count);
        s.writeObject(value);
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        count = s.readInt();
        value = (char[]) s.readObject();
    }           

而StringBuffer就不同了,用了ObjectStreamField來聲明序列化的字段,至于這兩個序列化的方式到底有什麼差別,以後能更新到IO流的時候再說吧

private static final java.io.ObjectStreamField[] serialPersistentFields =
    {
        new java.io.ObjectStreamField("value", char[].class),
        new java.io.ObjectStreamField("count", Integer.TYPE),
        new java.io.ObjectStreamField("shared", Boolean.TYPE),
    };

    private synchronized void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {
        java.io.ObjectOutputStream.PutField fields = s.putFields();
        fields.put("value", value);
        fields.put("count", count);
        fields.put("shared", false);
        s.writeFields();
    }

    private void readObject(java.io.ObjectInputStream s)
        throws java.io.IOException, ClassNotFoundException {
        java.io.ObjectInputStream.GetField fields = s.readFields();
        value = (char[])fields.get("value", null);
        count = fields.get("count", 0);
    }