雖然印象中記得StringBuffer是線程安全,因此性能比StringBuilder慢一丟丟,可是實話說對於它們3個的瞭解仍是很淺,本文咱們就深刻♂一些,完全搞明白這三兄貴。html
首先咱們要清楚一個知識:String是不可變的。java
這是啥意思呢,就是一個String對象,它所存儲的具體字符串值,是不可修改的。String本質上也是一個類,它裏面有不少屬性和方法,而存儲的字符串值在它裏面也只是一個char數組的屬性而已,可是這個屬性卻被final修飾了,不可更改,因此這個屬性只會隨着String對象的建立而初始化一次。也就是一個String對象,它存儲的字符串是固定死的,直到這個對象被回收也不會更改。數組
一句話:一旦你經過new或其餘手段建立了一個String對象,那麼它存儲的字符串值就是固定的,不會再改變了。緩存
話雖如此,咱們偶爾仍是能夠看到字符串拼接操做:安全
String str = "a";
str = str + "b";
複製代碼
看起來str對象的值由【a】改變成了【ab】,實際上它已經不是那個「它」了,第一行的str和第二行的str指向的已經不是同一個String對象了。markdown
詳細且廢話點說,就是第一行時,變量str指向了一個String對象,它的值是「a」。而第二行str+"b"中,新new了一個String對象,而且它的值是"ab",同時str從新指向了這個對象。而本來的值爲"a"的對象,仍是存在的,只是如今已經沒有變量指向它了。多線程
驗證:併發
驗證方法也很簡單,查看第一行和第二行的str指向的內存地址就能夠了,因爲String已經重寫了hashCode()方法,因此咱們能夠經過System.identityHashCode(object)
獲取它的原始hashCode,這個hash值就是根據內存地址獲取的,若是是同一個對象,天然取出來的值也是同樣的。app
代碼:less
package com.lzh.array;
public class Test1 {
public static void main(String[] args) {
String str = "a";
System.out.println("字符串 a 的String對象hash值:"+System.identityHashCode(str));
str = str + "b";
System.out.println("字符串 ab 的String對象hash值:"+System.identityHashCode(str));
String str1 = new String("a");
System.out.println("雖然是字符串a,可是是new出來的對象,因此hash值爲:"+System.identityHashCode(str1));
}
}
複製代碼
結果是
字符串 a 的String對象hash值:1265094477 字符串 ab 的String對象hash值:2125039532 雖然是字符串a,可是是new出來的對象,因此hash值爲:312714112
其實,經過上面的知識,咱們就知道爲何須要StringBuffer和StringBuilder了,正是由於String是不可變的。
若是咱們須要頻繁的操做同一個字符串,那必然會建立不少String對象,而後不停的讓變量指向新的String對象。可是實際上咱們須要用的就只有一個對象,那麼就會產生很大的資源浪費,若是你更改了10次字符串,那就會建立10次String對象,效率低不說,浪費的內存空間更多。
若是代碼裏這樣的操做多一些或來幾十個循環,估計就麻煩了,一會兒就可能建立了成百上千個無用的String對象。
因此java必須有一個可變長的字符串類,這就是StringBuffer和StringBuilder的做用,它們均可以更改自身所存儲的字符串值,當須要對字符串頻繁操做時,咱們就能夠用它們代替String對象了。不用擔憂轉換問題,它們存儲字符串的方式和String是相同的,都是char數組,只是沒有加final修飾,而且也都重寫了toString方法。
這時可能咱們會有一個疑問,爲何最開始要把String設計成不可變的呢?若是它一開始就是可變的,那不就沒這麼多事了嗎?
這裏咱們就說一下String類是不可變的好處:
java中,String的使用能夠說是最多的,並且不少是做爲常量反覆使用。像基本類型Integer、Long這些,也都設置了各自的常量池(一般是-128~127),覆蓋一些經常使用的數字範圍,目的就是避免建立大量無心義的對象。String做爲使用最多的對象,也天然得設置一個常量池。
而String的常量池因爲不能預判用戶常常會使用哪些字符串,因此不能像Integer同樣初始化一個範圍。因此String的常量池是這樣實現的:在聲明一個String時,它會進入常量池中找這個字符串,若是沒有,就直接new一個String對象,同時將這個對象投入到常量池。那麼若是後面又有其餘地方用到了這個字符串,就會直接使用第一次new出來的對象。
這就是String常量池的原理,但若是String是可變長的,那就實現不了常量池了。若是常量池中的String能夠被任意改動它實際存儲的值,那仍是常量池嗎?因此說個題外話,Integer那些包裝類,也是不可變的。
其實①就是爲了提高使用性能而建立的常量池,可是還有一些其餘方面的性能問題,例如HashMap等容器,它們的Key大可能是String,固然HashMap已經利用hashcode進行性能上的優化了,可是若是對象的hashcode不能保持穩定不變,也會形成很大的性能浪費。
若是String是可變的,那麼每次你修改String對象,它的hashcode都不得不從新計算一次,反覆計算新的hashcode就已經夠麻煩了,更麻煩的是若是你把已經加入到Map裏的一個數據的key改重複了,那同一個Map就有兩個key相同的數據了,爲了不這點又不知道要作多少設計和限制。
一旦容易發生變化,就很容易引發各類各樣的問題。
若是String隨隨便便就能夠把它的值改了,那涉及到線程的地方確定又是個大麻煩,要作到線程安全,又是一大筆性能開銷(怎麼又是性能,看來性能真的很重要)。
不只是線程安全,其餘地方例如在寫代碼的時候,不當心將String的value操做變化了,可是卻沒發現,也是一種風險。
能夠說官方只是選擇了最快和最安全的方式表達字符串,而且將這種方式鎖定設爲了默認選擇。但若是咱們想用可變的字符串,官方也爲咱們留了一扇門:StringBuffer和StringBuilder
說好的一扇門呢,這怎麼有兩扇?
別擔憂,兩個門各有各的特點,先讓咱們搞清楚兩個門的區別:
StringBuffer是線程安全的(可是也由於這點,犧牲了一些性能),StringBuilder不是線程安全的(因此效率比前者高)。
好了,沒了,結束。
。。
。。
。。
呃,的確就只是這個區別而已。
若是隻是想知道它們兩個的「區別」,那到這裏爲止就結束了,不過大家可能想了解一下這兩個類的其餘知識,我就繼續講解一下好了。
在前面,我說了它們的區別就只是線程是否安全,以及因爲這個區別產生的性能效率區別。
的確沒有其餘區別,包括怎麼使用,怎麼初始化,都是同樣的。
相信看到這裏,你們就猜到它們這麼類似的緣由了,由於它們實現了同一個抽象類:AbstractStringBuilder。這個抽象類的描述是:可變的字符序列。簡單粗暴的說明了它的特色,可變的字符串。
關於這個抽象類,咱們後面詳細說說,先說個一個小知識:StringBuffer的誕生比AbstractStringBuilder更早。
這是很正常的,StringBuffer從JDK1.0開始就存在了,它是線程安全的,可是也所以犧牲了一些性能。在JDK1.5的時候,線程不安全可是效率更高的StringBuilder就和它們的抽象類AbstractStringBuilder一塊兒誕生了。這個時候StringBuffer也被迫繼承了這個抽象類。
因此AbstractStringBuilder其實就是對可變長字符串專門提取出來的抽象類,也是對這一律唸的描述。
接口:
關於AbstractStringBuilder,它定義了一些可變字符串的屬性和方法實現,同時它還實現了兩個接口:
Appendable和CharSequence
abstract class AbstractStringBuilder implements Appendable, CharSequence {
複製代碼
Appendable(翻譯:可追加)接口也是一同推出的接口,內容很簡單,就是3個append的方法,append方法用過的人應該懂,就是StringBuffer和StringBuilder進行字符串拼接的方法
package java.lang;
import java.io.IOException;
public interface Appendable {
//容許append拼接實現了CharSequence接口的類
Appendable append(CharSequence csq) throws IOException;
//容許append拼接實現了CharSequence接口的類,並指定要拼接的字符串範圍,只取其開始到結束位置的字符
Appendable append(CharSequence csq, int start, int end) throws IOException;
//容許append拼接基本類型char字符
Appendable append(char c) throws IOException;
}
複製代碼
而其中眼熟的CharSequence(翻譯:字符序列)接口,就是說明實現了它的類是一個字符序列。固然最多見的實現類就是String,StringBuffer和StringBuilder了。因爲這兩個接口的組合使用,才讓咱們能夠進行字符串的拼接,甚至能夠跨類進行拼接(StringBuffer拼接StringBuilder對象),只要這個類實現了CharSequence接口便可。
而CharSequence接口,也定義了一些字符序列的方法,例如最經常使用的length()獲取字符串長度,charAt(index)獲取單個字符,subSequence(start,end)截取字符串,這三個方法是實現類必須實現的。
這兩個接口咱們大體明白了,簡單總結一下:Appendable是關於拼接字符串的接口,而實現了CharSequence接口則是代表自身也是屬於字符串類型的類。
屬性:
//字符數組,即存儲字符串的值,不過和Spring不一樣的是它沒有用final修飾,因此能夠修改,聽說JDK9以後,採用的就是byte[]了。
char[] value;
//字符數組的長度,length()方法其實就是直接返回這個值。
int count;
//字符串的最大值,實際上這個值是直接從數組的最大長度直接取的,畢竟字符串的值也是數組,數組能有多長,字符串就有多長
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
複製代碼
其中比較關鍵的,固然就是value屬性了,本質上它和String是同樣的,只是Spring的value屬性被private final修飾了,才致使它是不可變的。而沒有任何修飾的value,就能夠修改了,這個value也就是可變長字符串的核心屬性了,因此定義在了父類的抽象類中,子類StringBuffer和StringBuilder是沒有這個屬性的。
方法:
原本想要不把方法都講解一下,可是看了一下里面的方法數。。?抱歉,是我不知天高地厚了。數量仍是有億點多的,包括經常使用的對字符串進行操做的方法(畢竟是可變長的),獲取字符串的方法。還有一些是針對字符數組的操做(即value屬性),由於java中數組是定長的,顯然咱們不可能每次都初始化一個最大長度的字符數組,而是應該隨着字符數量的增多,對數組進行擴容。最後還有一些是兼容String的方法,像indexOf,substring這些String裏有的方法。
因此裏面的方法,這裏就先不講了,仍是重點關注一下StringBuffer和StringBuilder類吧,結合它們會順帶帶出來一些AbstractStringBuilder中的方法。
前面也說過了,其實它們最大的區別就是:是否線程安全。而且因爲這個緣由致使了線程不安全的StringBuilder能夠有更快的效率。這裏咱們看看源碼,經過源碼查看一下二者的區別。
固然前提你得知道synchronized關鍵字是啥和它的做用,若是不知道還要繼續看的話,就先記住它的功能是線程保護。加了它修飾的方法,同一時間只能有一個線程執行(因此會下降性能)。
首先它們兩的繼承結構固然是同樣的,都繼承了父類AbstractStringBuilder,這意味着它們是可變長的。而後都實現了CharSequence接口,同時也實現了Serializable接口,代表本身支持序列化。
StringBuffer:
public final class StringBuffer extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
}
StringBuilder:
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence {
}
複製代碼
先聲明,這塊內容意義不是很大,可是能夠漲漲知識,可選擇跳過。
/** * A cache of the last value returned by toString. Cleared * whenever the StringBuffer is modified. */
private transient char[] toStringCache;
複製代碼
toStringCache屬性是StringBuffer特有的屬性,註釋的意思大概是:toString返回的最後一次緩存值,會隨着StringBuffer的修改而清空。
因此這個字段也就是個緩存,而且是專門用於toString方法的緩存,另外若是StringBuffer的value值進行了任何修改,它都會被直接設爲null。
既然它是爲toString服務的,那麼咱們就看看兩個類的toString方法區別:
StringBuffer的toString方法:
@Override
public synchronized String toString() {
if (toStringCache == null) {
toStringCache = Arrays.copyOfRange(value, 0, count);
}
return new String(toStringCache, true);
}
複製代碼
StringBuilder的toString方法:
@Override
public String toString() {
// Create a copy, don't share the array
return new String(value, 0, count);
}
複製代碼
toStringCache的做用和理解:
能夠看到,它的確是爲toString方法服務的一個屬性。當使用者調用toString方法時,邏輯會先判斷它是否爲null。若是爲null(說明被改過),就拷貝當前的value數組值做爲一個新數組存給toStringCache,而後將它new直接傳遞給String的value數組。這裏的重點是它直接傳遞了過去,這說明這個數組在這個時刻,StringBuffer的toStringCache和String的value指向的是同一個數組。也就是隻要一個地方改了,另外一個地方也會自動改變。
那這不就不符合String的不可變性了嗎?答案是不會,首先String的value值是不可能改的,由於final修飾了,惟一可能變化的就是StringBuffer,可是StringBuffer只要字符串有任何改動,toStringCache屬性都會當即設爲null,也就是和以前的char數組撇開關係。
因此這裏惟一提升了效率的地方,就是new String,因爲直接將數組值傳遞了過去,固然一行就搞定啦。
String的構造方法源碼:
String(char[] value, boolean share) {
// assert share : "unshared not supported";
this.value = value;
}
複製代碼
但是StringBuillder卻用的不是這個構造方法,由於StringBuillder沒有toStringCache屬性,爲了不它發生上面所說的,違反String不可變性的問題,因此它調用的String構造方法是從新複製了一個數組,加上一堆有的沒的邏輯判斷,天然就相對慢一點。
toStringCache的意義:
雖然看起來高大上,但有時候不要就覺得它是100%實用的。上面咱們所說的優化,隱藏了一個大前提,就是須要連續調用未更改的StringBuffer的toString方法。畢竟若是你修改了StringBuffer,那麼toStringCache就會被設爲null,那一樣也要徹底複製value的char數組給它,只是這個複製操做從String的構造方法挪到了StringBuffer的toString中。因此這種狀況下,性能徹底不見得會有啥區別,沒準兒還更慢。
可是連續調用未修改的StringBuffer的toString方法,更是極其罕見的操做,若是真有人會這樣寫代碼,那我就有點好奇他想幹啥了。。
因此我我的以爲它的實用性並不大,這也是我開頭所說的,意義不大,可是能夠漲漲姿式。網上有網友說,有些代碼可能從JDK1.0開始就存在了,像這種可能優點並不大的代碼,不多會發生改動,有必定的缺陷是正常的。有時候改起來的成本和影響,遠超過它自己存在所形成的負面影響。
因此沒準,它也是個有一點點冗餘的小功能,只是不方便修改代碼而已?固然我也只能畫個問號,由於我也不知道。。
構造方法二者是徹底一致的,二者都有4個構造方法,且這4個代碼都是同樣的。因此這裏不會過多的說明。
無參構造方法:
public StringBuffer() {
super(16);
}
複製代碼
能夠看到調用了父類的構造方法,並默認傳了數字16。
父類AbstractStringBuilder的構造方法:
/** * Creates an AbstractStringBuilder of the specified capacity. */
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
複製代碼
因此,這個16的意義就是初始化了一個長度爲16的char數組設爲值。前面已經瞭解過了,數組的最大長度是Integer.MAX_VALUE - 8,可是顯然不可能每次都初始化這麼大的,這裏咱們能夠理解了。默認狀況下,StringBuffer和StringBuilder是先建立一個長度爲16的字符數組。
固然,若是是有參的初始化,它也是往字符串的長度再延長一個16,再進行初始化。
public StringBuffer(String str) {
super(str.length() + 16);
append(str);
}
複製代碼
二者的append方法原理是相似的,雖然有一些細微差異,可是因爲數量太多了,StringBuffer有14個append方法,StringBuilder有13個,因此只節選兩個簡單講解。
StringBuffer的append方法:
@Override
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
@Override
public synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
複製代碼
StringBuilder的append方法:
@Override
public StringBuilder append(Object obj) {
return append(String.valueOf(obj));
}
@Override
public StringBuilder append(String str) {
super.append(str);
return this;
}
複製代碼
首先synchronized關鍵字,便是StringBuffer對多線程操做的預防,嗯,雖然以前覺得線程安全很高深,可是好像也就是靠這個關鍵字作到的。而後toStringCache屬性設爲null的理由前面也說明的很清楚了。最後一個細微的小區別,我以爲多是synchronized的影響,不過自己也是無關痛癢的變化。
它們都選擇調用了父類的append方法,說明核心的內容仍是在AbstractStringBuilder父類抽象類中的,雖然咱們目的是想了解它們的區別,可是這個時候很適合插入一些知識,因此咱們簡單的看看append方法的源碼學習一下。
AbstractStringBuilder的其中一個append方法:
public AbstractStringBuilder append(String str) {
//校驗要append的字符是否爲空
if (str == null)
//若是爲空,調用appendNull方法,會添加null四個字符到尾部,並不會拋出異常哦
return appendNull();
//獲取要添加的字符串長度
int len = str.length();
//確保數組容量夠用,不夠用的話會進行擴容
ensureCapacityInternal(count + len);
//經過String的getChars方法,將String對象中的char數組複製到StringBuffer的char數組尾部。
str.getChars(0, len, value, count);
count += len;
return this;
}
複製代碼
就不詳解方法內部了,對每一個函數的操做都註釋了。值得關注的是ensureCapacityInternal
,它是檢查當前數組容量的方法,固然裏面還嵌套了好幾個不一樣方法,目的只有一個:檢查當前數組的長度是否足夠,若是不夠則擴容。
其中 str.getChars(0, len, value, count);
內部實際上使用了System.arraycopy,這是System提供了的一個native靜態方法,專門用於拷貝數組的。
關於StringBuffer和StringBuilder的擴容:
經過查看源碼,並不難理解,前面咱們已經知道它通常狀況下的初始化大小爲16(能夠本身指定這個大小初始化)。當使用append方法添加字符時,就會檢查其容量是否足夠,不足時首先會擴容至當前數組長度*2+2,乘2好理解,就是翻倍當前的長度。至於加2,聽說是由於拼接字符串一般末尾都會有個多餘的字符。
固然有時候一次加的字符串太長,翻倍+2也不足以裝下它,這時候就會直接將長度設置爲添加的字符串加上本來字符串的長度,也就是剛恰好裝的下的程度。
要查看StringBuffer和StringBuilder的字符容量,能夠用capacity方法,它會返回char數組的長度,而length方法實際返回的是存儲的字符數量。
其餘還有很多區別,可是就不細講了,由於這些區別有個共同點,就是這些方法只有StringBuffer有,而StringBuilder沒有,可是二者的對象均可以使用這些方法。
沒錯,就是父類的方法,StringBuffer額外重寫了好幾個父類的方法,可是卻沒有做多少 改動,幾乎全都加了synchronized
,有些方法還會在第一行加上一個toStringCache = null;
,因此目的只是兼容它的線程安全,因此沒有什麼必要進行比較。
看了這麼多,相信你們最關心的就是這個了,先說結論:通常用StringBuilder,除非可能有線程問題。
StringBuffer對線程安全的處理比較簡單粗暴,就是爲大部分方法都上個synchronized
,無論你是加是減仍是查,不少方法都直接用synchronized
修飾,天然能夠保證線程安全。可是效率可想而知。。。比較低下。
並且咱們通常也不常須要在多線程的狀況下操做StringBuffer或StringBuilder。
就算是多線程,還要要求不能是高併發的,由於StringBuffer是直接用synchronized
的,很容易堵塞。因此有些時候會選擇用StringBuilder搭配其餘手段解決高併發狀況下的線程問題(本身在外部加鎖之類的)。
因此無論怎麼看,StringBuilder都用的比StringBuffer多,固然除非你是低併發下的多線程操做。
感受都講到這個地步了,不貼幾個方法好像也過不去了,如下會列一些StringBuffer和StringBuilder的經常使用方法。
固然經過了源碼分析,咱們都知道這大部分方法都是從它們的爸爸:AbstractStringBuilder抽象類父類中來的。
將指定的字符串追加到此字符序列,同時有各類各樣的重載方法。
將字符序列翻轉,就是123翻轉變成了321這樣。
刪除字符序列中指定位置的子字符串
將字符串插入此字符序列的指定位置,有不少格式的重載方法。
使用給定 String 中的字符,替換此字符序列中指定位置的字符。
返回當前字符數組的容量(即char[]的容量)。
返回第一次出現的指定子字符串在該字符串中的索引下標。
返回指定子字符串最後一次出如今字符串中的索引。
截取指定位置的字符串,而後返回爲一個新的String對象。
經過這9節內容,我想若是認真看完了,應該學到的不只只有StringBuffer和StringBuilder的區別而已。
StringBuffer雖然是從JDK1.0就開始出現的,可是目前來看,最經常使用的應該是JDK1.5出的StringBuilder。
(StringBuffer:爲何會變成這樣呢?明明。。明明是我先的來的。。)
這兩個類我剛開始在用的時候不太容易分得清哪一個是線程安全的,當時是用Buffer這個單詞記憶的,Buffer有緩衝的意思,由於是處理多線程的類,因此須要緩衝。。。。我大概就是這樣記的,雖然聽起來有點不太靠譜。。
參考資料:
JDK源碼之AbstractStringBuilder類分析:
[十三]基礎數據類型之AbstractStringBuilder: