原文鏈接:http://blog.csdn.net/hj7jay/article/details/52770174
字符串,就是一系列字符的集合。
Java裏面提供了String,StringBuffer和StringBuilder三個類來封裝字符串,其中StringBuilder類是到jdk 1.5才新增的。字符串操作可以說是幾乎每門編程語言中所必不可少的,你真的理解其內幕嗎?
下面讓我們開始探祕之旅吧!
1、既然都是用來封裝字符串的,那爲什麼還要3個類來封裝呢?
2、它們三者之間到底有何區別?
3、它們三者之間的使用場景分別是什麼?
4、它們三者之間從內存角度來看又是怎麼來實現的呢?
5、它們三者之間的性能效率是怎麼排列的?
下面就讓我們逐一破解這幾個謎團吧!
一、概述
字符串是由若干個字符線性排列組成的,所以我們可以把字符串當作數組(Array)來看待。衆所周知,數組就是內存裏線性排列的有序地址空間塊。既然是線性排列,有序的一組地址塊,那麼在分配數組空間時就必須指定空間大小也就是數組大小,這也是大多數編程語言裏規定在定義數組時要指定數組大小的原因( 某些編程語言中可變數組的實現另當別論 )。換言之, 數組就分爲可變數組和不可變數組。可變數組能夠動態插入和刪除,而不可變數組一旦分配好空間後則不能進行動態插入或刪除操作。
二、從實際應用可能的場景中分析String,StringBuilder,StringBuffer產生的背景
在實際應用當中我們可能會對字符串經常做如下幾種操作:插入,刪除,修改,拼接,截取,查到,替換……其中,“插入”和“刪除”操作就涉及到對原字符串的長度 進行修改( 其實,“拼接”和“截取”也分爲可以理解爲插入和刪除操作 )。
然而,在jdk 1.0中就出現的 String類封裝的字符串是不可變的 ,即不能在原字符串上進行 插入 和 刪除 操作,String的API是通過新建臨時變量的方式來實現字符串的插入和刪除操作。因爲是新建臨時變量,所以 當你調用String類的API對字符串進行插入和刪除操作時原來的字符串是不會有任何改變的,返回給你的是一個新的字符串對象 。關於這一點,我們將在後面通過分析源碼的方式來證實!既然String類封裝的是不可變數組,那麼對應的就應該有一個類來封裝可變數組。沒錯! StringBuffer類封裝的就是可變數組,並且還是線程安全的。 所以,在非多線程環境下效率相對較低。正如大家所想的一樣,JDK裏也提供了非多線程環境下使用的可變字符串的封裝類,它就是在jdk 1.5裏面才姍姍來遲的StringBuilder類, StringBuilder類是非線程安全的可變字符串封裝類 ,也是我們今天要討論的成員之一。
到此,我們基本上就已經回答了在文章開始處提出的第1,2,3個問題。接下來,我們再從源碼的角度來更深入討論。
三、從源碼角度進一步探討其內部實現方式
1、String類的關鍵源碼分析如下:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** The value is used for character storage. */ private final char value[];//final類型char數組//省略其他代碼………… }
從上述的代碼片段中我們可以看到,String類在類開始處就定義了一個final 類型的char數組value。也就是說通過 String類定義的字符串中的所有字符都是存儲在這個final 類型的char數組中的。
下面我們來看一下String類對字符串的截取操作,關鍵源碼如下:
public String substring(int beginIndex) { if (beginIndex < 0) { throw new StringIndexOutOfBoundsException(beginIndex); } int subLen = value.length - beginIndex; if (subLen < 0) { throw new StringIndexOutOfBoundsException(subLen); } //當對原來的字符串進行截取的時候(beginIndex >0),返回的結果是新建的對象 return (beginIndex == 0) ? this : new String(value, beginIndex, subLen); }
當我們對字符串從第beginIndex(beginIndex >0) 個字符開始進行截取時,返回的結果是重新new出來的對象。所以,在對String類型的字符串進行大量“插入”和“刪除”操作時會產生大量的臨時變量。
2、StringBuffer和StringBuilder類關鍵源碼分析:
在進行這兩個類的源碼分析前,我們先來分析下一個抽象類AbstractStringBuilder,因爲,StringBuffer和StringBuilder都繼承自這個抽象類,即AbstractStringBuilder類是StringBuffer和StringBuilder的共同父類。AbstractStringBuilder類的關鍵代碼片段如下:
abstract class AbstractStringBuilder implements Appendable, CharSequence { /** * The value is used for character storage. */ char[] value;//一個char類型的數組,非final類型,這一點與String類不同 /** * This no-arg constructor is necessary for serialization of subclasses. */ AbstractStringBuilder() { } /** * Creates an AbstractStringBuilder of the specified capacity. */ AbstractStringBuilder(int capacity) { value = new char[capacity];//構建了長度爲capacity大小的數組 }//其他代碼省略………… }
StringBuffer類的關鍵代碼如下:
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence{ /** * Constructs a string buffer with no characters in it and an * initial capacity of 16 characters. */ public StringBuffer() { super(16);//創建一個默認大小爲16的char型數組 } /** * Constructs a string buffer with no characters in it and * the specified initial capacity. * * @param capacity the initial capacity. * @exception NegativeArraySizeException if the {@code capacity} * argument is less than {@code 0}. */ public StringBuffer(int capacity) { super(capacity);//自定義創建大小爲capacity的char型數組 }//省略其他代碼…………
StringBuilder類的構造函數與StringBuffer類的構造函數實現方式相同,此處就不貼代碼了。
下面來看看StringBuilder類的append方法和insert方法的代碼,因StringBuilder和StringBuffer的方法實現基本上一致,不同的是StringBuffer類的方法前多了個synchronized關鍵字,即StringBuffer是線程安全的。所以接下來我們就只分析StringBuilder類的代碼了。StringBuilder類的append方法,insert方法都是Override 父類AbstractStringBuilder的方法,所以我們直接來分析AbstractStringBuilder類的相關方法。
public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); //調用下面的ensureCapacityInternal方法 ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this; }private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) //調用下面的expandCapacity方法實現“擴容”特性 expandCapacity(minimumCapacity); } /** * This implements the expansion semantics of ensureCapacity with no * size check or synchronization. */ void expandCapacity(int minimumCapacity) { //“擴展”的數組長度是按“擴展”前數組長度的2倍再加上2 byte的規則來擴展 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返回的新的char[]對象,從而達到“擴容”的特性 value = Arrays.copyOf(value, newCapacity); }
從上述代碼分析得出,StringBuilder和StringBuffer的append方法“擴容”特性本質上是通過調用Arrays類的copyOf方法來實現的。接下來我們順藤摸瓜,再分析下Arrays.copyOf(value, newCapacity)這個方法吧。代碼如下:
public static char[] copyOf(char[] original, int newLength) { //創建長度爲newLength的char數組,也就是“擴容”後的char 數組,並作爲返回值 char[] copy = new char[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy;//返回“擴容”後的數組變量 }
其中,insert方法也是調用了expandCapacity方法來實現“擴容”特性的,此處就不在贅述了。
接下來,分析下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) { //調用native方法arraycopy對value數組進行復制操作,然後重新賦值count變量達到“刪除”特性 System.arraycopy(value, start+len, value, start, count-end); count -= len; } return this; }
從源碼可以看出delete方法的“刪除”特性是調用native方法arraycopy對value數組進行復制操作,然後重新賦值count變量實現的
最後,來看下substring方法,源碼如下 :
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); //根據start,end參數創建String對象並返回 return new String(value, start, end - start); }
四、總結:
1、String類型的字符串對象是不可變的,一旦String對象創建後,包含在這個對象中的字符系列是不可以改變的,直到這個對象被銷燬。
2、StringBuilder和StringBuffer類型的字符串是可變的,不同的是StringBuffer類型的是線程安全的,而StringBuilder不是線程安全的
3、如果是多線程環境下涉及到共享變量的插入和刪除操作,StringBuffer則是首選。如果是非多線程操作並且有大量的字符串拼接,插入,刪除操作則StringBuilder是首選。畢竟String類是通過創建臨時變量來實現字符串拼接的,耗內存還效率不高,怎麼說StringBuilder是通過JNI方式實現終極操作的。
4、StringBuilder和StringBuffer的“可變”特性總結如下:
(1)append,insert,delete方法最根本上都是調用System.arraycopy()這個方法來達到目的
(2)substring(int, int)方法是通過重新new String(value, start, end - start)的方式來達到目的。因此,在執行substring操作時,StringBuilder和String基本上沒什麼區別。