Java 原始碼出發徹底搞懂String與StringBuffer和StringBuilder的區別

艾陽丶發表於2017-05-03

導讀


         在Java中資料型別分為基本資料型別與引用資料型別。其中String屬於引用型別,也是最常見的一種型別。但是我們對於String瞭解多少呢?String物件的記憶體地址?如何建立String物件?併發影響?等等。

關於Java的String記憶體儲存位置及原始碼解析文章推薦閱讀:

Java Final修飾符儲存位置,為什麼String是不可變的?

Android必須知道的Java記憶體結構及堆疊區別


一、String

探究String類原始碼,JDK1.7中String類的主要成員變數就兩個:

public final class String    
    implements java.io.Serializable, Comparable<String>, CharSequence {    
    /** The value is used for character storage. */    
    private final char value[];    
    
    /** Cache the hash code for the string */    
    private int hash; // Default to 0 
    由以上的程式碼可以看出, 在Java中String類其實就是對char value[](字元陣列)的封裝。在JDK7中,有一個value變數,也就是value中的所有字元都是屬於String這個物件的。另外,還有一個hash成員變數,是該String物件引用地址的雜湊值的快取。在Java中,陣列也是物件,所以value也只是一個引用,它指向一個真正的陣列物件。
即,執行了String s1 = “ABCabc”這句程式碼之後,應該是這樣的:
String s1 = char value[] ='A' 'B' 'C' 'a' 'b' 'c'   

    由於原始碼中這個 value 變數是加了final 修飾符的。 也就是說在String類內部,一旦value這個值初始化了, 就不能被改變。所以可以認為String物件是不可變的了。即,String的例項一旦生成就不會再改變了。如果執行 String str=”kv”+”ill”+” “+”ans”; 就有四個字串常量,最終由於String的不可變導致通過“+”產生了很多不必要的臨時變數且不是執行緒安全的,這種情況下使用StringBuffer更好。 因為從JDK 1.5開始,帶有字串變數的連線操作 +,JVM內部採用的是StringBuilder來實現的,而之前這個操作是採用StringBuffer實現的。所以,String的+操作實際是通過StringBuilderr的append方法進行操作,然後又通過toString()操作重新賦值的。

   此外,使用String不一定建立物件。因為在 java中對 String 物件有特殊對待,在堆區域給分成了兩塊,一塊是 String constant pool(常量池),另一塊用於儲存普通物件及字串物件。比如:

String a ="123";
String b ="123"; //值“123”在常量池中已有例項物件,可直接引用
String c = new String("123456");//首先值“123456”在常量池中建立一個物件例項,然後new String在堆中又建立一個由c指向引用物件地址。
a == b == true ;//值在常量池的物件例項都是“123”,即引用地址相同

因為JVM會先到常量池中查詢有沒有“123”這個物件例項,發現沒有“123”,然後會建立新的物件例項置入常量池中。所以,變數b會直接在拿到常量池中的例項引用。

    但是,使用new String,一定建立物件。在執行String a = new String(“123456”)的時候,首先到常量池查詢例項的引用(若沒有,則建立一個”123456”物件),然後再通過new關鍵字建立一個新的String例項,實際上建立了兩個String物件。 


二、StringBuffer

    通過上面的分析,當字串資料進行拼接時候,為了避免產生很多不必要的臨時變數,提高效率,需要使用StringBuffer或StringBuilder。其中,StringBuffer 是執行緒安全可變字元序列,適用於多執行緒場景。在任意時間點上它都包含某種特定的字元序列,但通過某些方法呼叫可以改變該序列的長度和內容,可將字串緩衝區安全地用於多個執行緒。因為使用StringBuffer類時,每次都會對 StringBuffer 物件本身進行操作,所以不會生成新的物件並改變物件引用。為了搞清楚原理還得從原始碼從發。

StringBuffer.class 原始碼:

public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence  
{  
 public StringBuffer() {  
     super(16);//定義一個長度為16的陣列  
  }  
   
  public StringBuffer(int capacity) {  
      super(capacity);  
   }  
  
  
   public StringBuffer(String str) {  
      super(str.length() + 16);  
      append(str); //字串拼接
   }   
} 

首先,StringBuffer類跟String類一樣定義成final形式,為了保證變數初始化後的引用物件不可以重新賦值,主要是為了“效率”和“安全性”的考慮,但是物件例項的成員變數的值是可變的,這一點與String完全不同,後面原始碼會講到。其次,StringBuffer實現的介面Serializable的作用就是為了序列化,就不多說了。

繼續看, append(str) 方法原始碼如何實現字串拼接:

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

這個方法對於詮釋StringBuffer的特性來說是相當的重要了。首先 append 用的修飾符是 synchronized,說明在操作時是執行緒安全的,而這一點 StringBuilder 就沒有。其次,從 return this; 可以看出不管執行多少次的append(String)方法,都會對 StringBuffer 物件本身進行操作,不會像String的字串拼接那樣new String()建立新的物件。最後,從super.append(str);可以看出在StringBuffer裡直接呼叫父類的append方法,對於該方法的具體程式碼是在父類中實現的。

接下來,就來看看繼承的父類 AbstractStringBuilder.class 和實現的介面 CharSequence.class。

介面 CharSequence.class 原始碼:

public interface CharSequence {  
  
    int length();  
  
    char charAt(int index);  
  
    CharSequence subSequence(int start, int end);  
  
    public String toString();  
  
}  

 AbstractStringBuilder.class 原始碼:  

abstract class AbstractStringBuilder implements Appendable, CharSequence {  
    /** 
     * 與String類一樣,定義了一個char型別的陣列儲存值 ,但是沒有加final,說明值可變。
     */  
    char value[];  
  
    int count;  
  
    AbstractStringBuilder() {  
    }  
   
    AbstractStringBuilder(int capacity) {  
        value = new char[capacity];  
    }  

    public synchronized String toString() { //同步,執行緒安全
        return new String(value, 0, count);//生成一個新的String物件  
   } 

    public AbstractStringBuilder append(String str) {  //append方法實現
        if (str == null) str = "null";  //非null判斷
            int len = str.length();  
           
        if (len == 0) return this;  //非空判斷
          
        int newCount = count + len;  
          
        if (newCount > value.length)  
            expandCapacity(newCount);     //這一步主要是陣列擴容  
          
        str.getChars(0, len, value, count);  //這一步得到新陣列
        count = newCount;  
        return this;  
    } 
....其他方法省略...}

首先,父類加了Abstract修飾符說明是抽象類,關於抽象類與介面的Java知識請移步學習。從 append()方法實現中可以看出,對str做了非空判斷,然後走一個陣列擴容的方法 expandCapacity(new count); 和得到新陣列的 str.getChars() 方法。

expandCapacity():


   void expandCapacity(int minimumCapacity) {  
     int newCapacity = (value.length + 1) * 2;//首先定義一個是原容量的2倍大小的值  
        if (newCapacity < 0) {  
            newCapacity = Integer.MAX_VALUE;  
        } else if (minimumCapacity > newCapacity) {//這一步主要是判斷,取最大的值做新的陣列容量大小
        newCapacity = minimumCapacity;  
      }  
        value = Arrays.copyOf(value, newCapacity);//最後進行擴容  
    }

str.getChars():

public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {  
        if (srcBegin < 0) {  //陣列越界異常
            throw new StringIndexOutOfBoundsException(srcBegin);  
        }  
        if (srcEnd > count) { 
            throw new StringIndexOutOfBoundsException(srcEnd);  
        }  
        if (srcBegin > srcEnd) {  
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);  
        }
       //前面三個判斷主要是為了安全驗證。這一步才是重點:將在原來的陣列上追加新陣列 
        System.arraycopy(value, offset + srcBegin, dst, dstBegin,srcEnd - srcBegin);  
} 
    StringBuffer主要操作有 append() 和 insert() 方法,可過載這些方法,以接受任意型別的資料。每個方法都能有效地將給定的資料轉換成字串,然後將該字串的字元追加或插入到字串緩衝區中。append 方法始終將這些字元新增到緩衝區的末端;而 insert 方法則在指定的點新增字元。

 例如:

String s = “start”;

//StringBuffer z = new StringBuffer(“start”);

StringBuffer z = new StringBuffer(s); // String轉換為StringBuffer

z.append("le");//startle

z.insert(6,"t");//插入內容t 。startlet
    如果 z 引用一個當前內容是“start”的字串緩衝區物件,則此方法呼叫 z.append(“le”) 會使字串緩衝區包含“startle”,而 z.insert(6, “t”) 將更改字串緩衝區,使之包含“starlet”。


三、StringBuilder

    StringBuilder是一個執行緒不安全的字元序列,是JDK5.0新增被設計用作 StringBuffer 的一個簡易替換,用在字串緩衝區被單個執行緒使用的時候,速度較StringBuffer要更快。StringBuffer和StringBuilder都是繼承自AbstractStringBuilder的。

AbstractStringBuilder原理:

    AbstractStringBuilder中採用一個char陣列來儲存需要append的字串,char陣列有一個初始大小,當append的字串長度超過當前char陣列容量時,則對char陣列進行動態擴充套件,也即重新申請一段更大的記憶體空間,然後將當前char陣列拷貝到新的位置,因為重新分配記憶體並拷貝的開銷比較大,所以每次重新申請記憶體空間都是採用申請大於當前需要的記憶體空間的方式。

    為什麼說StringBuilder的效率最高呢?從前面的原始碼分析中可以看出,因為StringBuffer的append()方法都是被synchronized修飾了,所以它執行緒安全,但是效率自然就降低,僅此而已。

 public synchronized StringBuffer append(Object paramObject)
  {
    super.append(String.valueOf(paramObject));
    return this;
  }

append()對比:    

 public StringBuilder append(char paramChar)
  {
    super.append(paramChar);
    return this;
  }
在操作用法上與StringBuffer基本相似。

    

四、總結

  • 如果要操作少量的資料,用String;
  • 單執行緒操作大量資料,用StringBuilder;
  • 多執行緒操作大量資料,用StringBuffer;
  • 不要使用String類的”+”來進行頻繁的拼接,因為效能很差,應該使用StringBuffer或StringBuilder類;
  • 為了效能更好,構造StringBuffer或StringBuilder時應指定它們的容量,預設構造的容量為16個字元;
  • StringBuilder最好在方法內部來完成字串拼接,因為是執行緒不安全的,所以用完以後可以丟棄。而StringBuffer主要用在全域性變數中;

    





相關文章