溫故知新(二)深入認識Java中的字串

2020-09-19 12:02:48

上篇文章我們深入分析了String的記憶體和它的一些特性。本篇文章我們深入的來分析一下與String相關的另外兩個類,它們分別是StringBuilder和StringBuffer。這兩個類與String有什麼關係呢?首先我們看下下邊這張類圖:

從圖中可以看出StringBuilder和StringBuffer都繼承了AbstractStringBuilder,而AbstractStringBuilder與String實現了共同的介面CharSequence。

我們知道,字串是由一系列字元組成的,String的內部就是基於char陣列(jdk9之後基於byte陣列)實現的,而陣列通常是一塊連續的記憶體區域,在陣列初始化的時候就需要指定陣列的大小。上一篇文章中我們已經知道String是不可變的,因為它內部的陣列被宣告為了final,同時,String的字元拼接、插入、刪除等操作均是通過範例化新的物件實現的。而今天要認識的StringBuilder和StringBuffer與String相比就具有了動態性。接下來就讓我們一起來認識下這兩個類。

一、StringBuilder

在StringBuilder的父類別AbstractStringBuilder 中可以看到如下程式碼:

abstract class AbstractStringBuilder implements Appendable, CharSequence {    /**
     * The value is used for character storage.
     */
    char[] value;    /**
     * The count is the number of characters used.
     */
    int count;
}複製程式碼

StringBuilder與String一樣都是基於char陣列實現的,不同的是StringBuilder沒有final修飾,這就意味著StringBuilder是可以被動態改變的。接下來看下StringBuilder無參構造方法,程式碼如下:

 /**
     * Constructs a string builder with no characters in it and an
     * initial capacity of 16 characters.
     */
    public StringBuilder() {        super(16);
    }複製程式碼

在這個方法中呼叫了父類別的構造方法,到AbstractStringBuilder 中看到其構造方法如下:

    /**
     * Creates an AbstractStringBuilder of the specified capacity.
     */
    AbstractStringBuilder(int capacity) {
        value = new char[capacity];
    }複製程式碼

AbstractStringBuilder的構造方法內部初始化了一個容量為capacity的陣列。也就是說StringBuilder預設初始化了一個容量為16的char[]陣列。StringBuilder中除了無參構造外還提供了多個構造方法,原始碼如下:

 /**
     * Constructs a string builder with no characters in it and an
     * initial capacity specified by the {@code capacity} argument.
     *
     * @param      capacity  the initial capacity.
     * @throws     NegativeArraySizeException  if the {@code capacity}
     *               argument is less than {@code 0}.
     */
    public StringBuilder(int capacity) {        super(capacity);
    }    /**
     * Constructs a string builder initialized to the contents of the
     * specified string. The initial capacity of the string builder is
     * {@code 16} plus the length of the string argument.
     *
     * @param   str   the initial contents of the buffer.
     */
    public StringBuilder(String str) {        super(str.length() + 16);
        append(str);
    }    /**
     * Constructs a string builder that contains the same characters
     * as the specified {@code CharSequence}. The initial capacity of
     * the string builder is {@code 16} plus the length of the
     * {@code CharSequence} argument.
     *
     * @param      seq   the sequence to copy.
     */
    public StringBuilder(CharSequence seq) {        this(seq.length() + 16);
        append(seq);
    }複製程式碼

這段程式碼的第一個方法初始化一個指定容量大小的StringBuilder。另外兩個構造方法分別可以傳入String和CharSequence來初始化StringBuilder,這兩個構造方法的容量均會在傳入字串長度的基礎上在加上16。

1.StringBuilder的append操作與擴容

上篇文章已經知道通過StringBuilder的append方法可以進行高效的字串拼接,append方法是如何實現的呢?這裡以append(String)為例,可以看到StringBuilder的append呼叫了父類別的append方法,其實不止append,StringBuilder類中操作字串的方法幾乎都是通過父類別來實現的。append方法原始碼如下:

    // StringBuilder
    @Override
    public StringBuilder append(String str) {        super.append(str);        return this;
    }    
  // AbstractStringBuilder
  public AbstractStringBuilder append(String str) {        if (str == null)            return appendNull();        int len = str.length();
        ensureCapacityInternal(count + len);
        str.getChars(0, len, value, count);
        count += len;        return this;
    }複製程式碼

在append方法的第一行首先進行了null檢查,等於null的時候呼叫了appendNull方法。其原始碼如下:

private AbstractStringBuilder appendNull() {        int c = count;
        ensureCapacityInternal(c + 4);        final char[] value = this.value;
        value[c++] = 'n';
        value[c++] = 'u';
        value[c++] = 'l';
        value[c++] = 'l';
        count = c;        return this;
    }複製程式碼

appendNull方法中首先呼叫了ensureCapacityInternal來確保字串陣列容量充值,關於ensureCapacityInternal這個方法下邊再詳細分析。接下來可以看到把"null"的字元新增到了char[]陣列value中。

上文我們提到,StringBuilder內部陣列的預設容量是16,因此,在進行字串拼接的時候需要先確保char[]陣列有足夠的容量。因此,在appendNull方法以及append方法中都呼叫了ensureCapacityInternal方法來檢查char[]陣列是否有足夠的容量,如果容量不足則會對陣列進行擴容,ensureCapacityInternal原始碼如下:

private void ensureCapacityInternal(int minimumCapacity) {        // overflow-conscious code
        if (minimumCapacity - value.length > 0)
            expandCapacity(minimumCapacity);
    }複製程式碼

這裡判讀如果拼接後的字串長度大於字串陣列的長度則會呼叫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);
    }複製程式碼

expandCapacity的邏輯也很簡單,首先通過原陣列的長度乘2並加2後計算得到擴容後的陣列長度。接下來判斷了newCapacity如果小於minimumCapacity,則將minimumCapacity值賦值給了newCapacity。這裡因為呼叫expandCapacity方法的不止一個地方,所以加這句程式碼確保安全。

而接下來的一句程式碼就很有趣了,newCapacity 和minimumCapacity 還有可能小於0嗎?當minimumCapacity小於0的時候竟然還丟擲了一個OutOfMemoryError異常。其實,這裡小於0是因為越界了。我們知道在計算機中儲存的都是二進位制,乘2相當於向左移了一位。以byte為例,一個byte有8bit,在有符號數中最左邊的一個bit位是符號位,正數的符號位為0,負數為1。那麼一個byte可以表示的大小範圍為[-128~127],而如果一個數位大於127時則會出現越界,即最左邊的符號位會被左邊第二位的1頂替,就出現了負數的情況。當然,並不是byte而是int,但是原理是一樣的。

另外在這個方法的最後一句通過Arrays.copyOf進行了一個陣列拷貝,其實Arrays.copyOf在上篇文章中就有見到過,在這裡不妨來分析一下這個方法,看原始碼:

 public static char[] copyOf(char[] original, int newLength) {        char[] copy = new char[newLength];
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));        return copy;
    }複製程式碼

咦?copyOf方法中竟然也去範例化了一個物件!!那不會影響效能嗎?莫慌,看一下這裡僅僅是範例化了一個newLength長度的空陣列,對於陣列的初始化其實僅僅是指標的移動而已,浪費的效能可謂微乎其微。接著這裡通過System.arraycopy的native方法將原陣列複製到了新的陣列中。

2.StringBuilder的subString()方法toString()方法

StringBuilder中其實沒有subString方法,subString的實現是在StringBuilder的父類別AbstractStringBuilder中的。它的程式碼非常簡單,原始碼如下:

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);
    }複製程式碼

在進行了合法判斷之後,substring直接範例化了一個String物件並返回。這裡和String的subString實現其實並沒有多大差別。 而StringBuilder的toString方法的實現其實更簡單,原始碼如下:

 @Override
    public String toString() {        // Create a copy, don't share the array
        return new String(value, 0, count);
    }複製程式碼

這裡直接範例化了一個String物件並將StringBuilder中的value傳入,我們來看下String(value, 0, count)這個構造方法:

    public String(char value[], int offset, int count) {        if (offset < 0) {            throw new StringIndexOutOfBoundsException(offset);
        }        if (count < 0) {            throw new StringIndexOutOfBoundsException(count);
        }        // Note: offset or count might be near -1>>>1.
        if (offset > value.length - count) {            throw new StringIndexOutOfBoundsException(offset + count);
        }        this.value = Arrays.copyOfRange(value, offset, offset+count);
    }複製程式碼

可以看到,在String的這個構造方法中又通過Arrays.copyOfRange方法進行了陣列拷貝,Arrays.copyOfRange的原始碼如下:

   public static char[] copyOfRange(char[] original, int from, int to) {        int newLength = to - from;        if (newLength < 0)            throw new IllegalArgumentException(from + " > " + to);        char[] copy = new char[newLength];
        System.arraycopy(original, from, copy, 0,
                         Math.min(original.length - from, newLength));        return copy;
    }複製程式碼

Arrays.copyOfRange與Arrays.copyOf類似,內部都是重新範例化了一個char[]陣列,所以String構造方法中的this.value與傳入進來的value不是同一個物件。意味著StringBuilder在每次呼叫toString的時候生成的String物件內部的char[]陣列並不是同一個!這裡立一個Falg

3.StringBuilder的其它方法

StringBuilder除了提供了append方法、subString方法以及toString方法外還提供了還提供了插入(insert)、刪除(delete、deleteCharAt)、替換(replace)、查詢(indexOf)以及反轉(reverse)等一些列的字串操作的方法。但由於實現都非常簡單,這裡就不再贅述了。

二、StringBuffer

在第一節已經知道,StringBuilder的方法幾乎都是在它的父類別AbstractStringBuilder中實現的。而StringBuffer同樣繼承了AbstractStringBuilder,這就意味著StringBuffer的功能其實跟StringBuilder並無太大差別。我們通過StringBuffer幾個方法來看

     /**
     * A cache of the last value returned by toString. Cleared
     * whenever the StringBuffer is modified.
     */
    private transient char[] toStringCache;    @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;        super.append(str);        return this;
    }    /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     * @since      1.2
     */
    @Override
    public synchronized StringBuffer delete(int start, int end) {
        toStringCache = null;        super.delete(start, end);        return this;
    }  /**
     * @throws StringIndexOutOfBoundsException {@inheritDoc}
     * @since      1.2
     */
    @Override
    public synchronized StringBuffer insert(int index, char[] str, int offset,                                            int len)
    {
        toStringCache = null;        super.insert(index, str, offset, len);        return this;
    }@Override
    public synchronized String substring(int start) {        return substring(start, count);
    }    
// ...複製程式碼

可以看到在StringBuffer的方法上都加上了synchronized關鍵字,也就是說StringBuffer的所有操作都是執行緒安全的。所以,在多執行緒操作字串的情況下應該首選StringBuffer。 另外,我們注意到在StringBuffer的方法中比StringBuilder多了一個toStringCache的成員變數 ,從原始碼中看到toStringCache是一個char[]陣列。它的註釋是這樣描述的:

toString返回的最後一個值的快取,當StringBuffer被修改的時候該值都會被清除。

我們再觀察一下StringBuffer中的方法,發現只要是操作過操作過StringBuffer中char[]陣列的方法,toStringCache都被置空了!而沒有操作過字元陣列的方法則沒有對其做置空操作。另外,註釋中還提到了 toString方法,那我們不妨來看一看StringBuffer中的 toString,原始碼如下:

   @Override
    public synchronized String toString() {        if (toStringCache == null) {
            toStringCache = Arrays.copyOfRange(value, 0, count);
        }        return new String(toStringCache, true);
    }複製程式碼

這個方法中首先判斷當toStringCache 為null時會通過 Arrays.copyOfRange方法對其進行賦值,Arrays.copyOfRange方法上邊已經分析過了,他會重新範例化一個char[]陣列,並將原陣列賦值到新陣列中。這樣做有什麼影響呢?細細思考一下不難發現在不修改StringBuffer的前提下,多次呼叫StringBuffer的toString方法,生成的String物件都共用了同一個字元陣列--toStringCache。這裡是StringBuffer和StringBuilder的一點區別。至於StringBuffer中為什麼這麼做其實並沒有很明確的原因,可以參考StackOverRun 《Why StringBuffer has a toStringCache while StringBuilder not?》中的一個回答:

1.因為StringBuffer已經保證了執行緒安全,所以更容易實現快取(StringBuilder執行緒不安全的情況下需要不斷同步toStringCache) 2.可能是歷史原因

三、 總結

本篇文章到此就結束了。《深入理解Java中的字串》通過兩篇文章深入的分析了String、StringBuilder與StringBuffer三個字串相關類。這塊內容其實非常簡單,只要花一點時間去讀一下原始碼就很容易理解。當然,如果你沒看過此部分原始碼相信這篇文章能夠幫助到你。不管怎樣,相信大家通過閱讀本文還是能有一些收穫。解了這些知識後可以幫助我們在開發中對字串的選用做出更好的選擇。同時,這塊內容也是面試常客,相信大家讀完本文去應對面試官的問題也會綽綽有餘。

想了解更多程式設計學習,敬請關注欄目!

以上就是溫故知新(二)深入認識Java中的字串的詳細內容,更多請關注TW511.COM其它相關文章!