全文大約【6000】字,不說廢話,只講可以讓你學到技術、明白原理的純干貨!本文帶有豐富的案例及配圖視頻,讓你更好地理解和運用文中的技術概念,并可以給你帶來具有足夠啟迪的思考......
一. 可變字符串
1.簡介
在Java中,我們除了可以通過String類創(chuàng)建和處理字符串之外,還可以使用StringBuffer和StringBuilder類來處理字符串。其中,String類定義的字符串內容不可變,所以String屬于不可變字符串。而StringBuffer和StringBuilder定義的字符串內容可變,這兩者屬于可變字符串,并且StringBuffer和StringBuilder,對字符串的處理效率比String類更高。
2.使用場景
有的小伙伴可能還是不太理解,字符串的使用并不是很難,咱們直接使用String來操作就可以了,為什么還要搞出來StringBuffer和StringBuilder這兩個類?這不是找麻煩嗎?其實這都是有原因的!
從底層原理來分析,String構建的字符串對象,其內容理論上是不能被改變的。一旦定義了String對象就無法再改變其內容,但很多時候我們還是需要改變字符串的內容的,所以String類就存在一定的短板。
另外從應用層面來分析,String字符串的執(zhí)行效率其實是比較低的。舉個例子,就比如常見的字符串拼接,很多人喜歡使用“+號”來拼接String字符串。其實如果是操作少量的字符串,使用String還湊活,一旦同時操作的字符串過多,String的效率就極低了。小編之前曾做過一個關于10萬個字符串拼接的實驗。同等條件下,利用“+”號進行拼接所需要的時間是29382毫秒,利用StringBuffer所需要的時間只有4毫秒,而StringBuilder所用的時間更是只需2毫秒,這效率真是天差地別!
另外我們還可以通過下面這個稍微簡單點的案例,來看看Java底層是如何處理字符串拼接的。
String str = "Hello" + "World";
System.out.println("str=" + str);
相信很多朋友都會用 “+”號 來進行字符串拼接,因為覺得該方式簡單方便,畢竟 一 “+” 了事。那么利用 “+”號來拼接字符串是最好的方案嗎?肯定不是的!如果我們使用JAD反編譯工具對上述Java字節(jié)碼進行反編譯,你會發(fā)現不一樣的結果,上述案例反編譯后得到的JAD文件內容如下所示:
import java.io.PrintStream;
public class StringTest13
{
public StringTest13()
{
}
public static void main(String args[])
{
String s = "HelloWorld";
System.out.println((new StringBuilder()).append("str=").append(s).toString());
}
}
從反編譯出來的JAD文件中我們可以看出,Java在編譯的時候會把 “+”號操作符替換成StringBuilder的append()方法。也就是說,“+”號操作符在拼接字符串的時候只是一種形式,讓開發(fā)者使用起來比較簡便,代碼看起來比較簡潔,但底層使用的還是StringBuilder操作。
既然 “+”號 的底層還是利用StringBuilder的append()方法操作,那么我們?yōu)槭裁床恢苯邮褂肧tringBuilder呢?你說對吧?而且當我們需要操作大量的字符串時,更不推薦使用String,比如:
String str = "";
for (int i = 0; i < 10000; i++) {
str = str + "," + i;
}
上面這段代碼,雖然可以實現字符串的拼接,但是在該循環(huán)中,每次循環(huán)都會創(chuàng)建一個新的字符串對象,然后扔掉舊的字符串。如果是10000次循環(huán),就會執(zhí)行10000次這樣的操作。而這些操作中的絕大部分字符串對象都是臨時對象,最終都會被扔掉不用,這就會嚴重地浪費內存,并會嚴重影響GC垃圾回收的效率。
為了能提高拼接字符串的效率,Java給我們提供了StringBuffer和StringBuilder,它們都是可變對象,可以預分配緩沖區(qū)。當我們往StringBuffer或StringBuilder中新增字符時,不會創(chuàng)建新的臨時對象,可以極大地節(jié)省了內存。可以說,好處多多。
那么接下來小編就帶領各位來學習StringBuffer、StringBuilder的用法吧。
二. StringBuffer
1.簡介
StringBuffer是一種可變的字符串類,即在創(chuàng)建StringBuffer對象后,我們還可以隨意修改字符串的內容。每個StringBuffer的類對象都能夠存儲指定容量的字符串,如果字符串的長度超過了StringBuffer對象的容量空間,則該對象的容量會自動擴大。
另外我們在使用StringBuffer類時,比如每次調用toString()方法,都會直接使用緩存區(qū)的toStringCache 值來構造一個字符串,這每次都是對StringBuffer對象本身進行操作,而不會重新生成一個新對象。所以如果我們需要對大量字符串的內容進行修改,小編推薦大家使用StringBuffer。
2.基本特性
StringBuffer作為一個可變字符串類,具有如下特性:
● 具有線程安全性:StringBuffer中的公開方法都由synchronized關鍵字修飾,保證了線程同步;
● 帶有緩沖區(qū):StringBuffer每次調用toString()方法時,都會直接使用緩存區(qū)的toStringCache值來構造一個字符串;
● 內容可變性:StringBuffer中帶有字符串緩沖區(qū),我們可以通過數組的復制來實現內容的修改;
● 自帶擴容機制:StringBuffer可以初始化容量,也可以指定容量,當字符串長度超過了指定的容量后,可以通過擴容機制實現長度的變更;
● 內容類型多樣性:StringBuffer中可以存儲多種不同類型的數據。
了解了StringBuffer的基本特性之后,請大家跟著小編來學習一下StringBuffer的基本用法吧。
3.基本用法
3.1 常用API方法
StringBuffer作為一個字符串操作類,它有以下幾個需要我們掌握的常用API方法,如下所示:
3.2 基本案例
知道了這些常用的API方法后,我們再通過一個案例來看看這些方法到底是怎么用的。
public class Demo01 {
public static void main(String[] args) {
//創(chuàng)建StringBuffer對象
StringBuffer sb = new StringBuffer("跟一一哥,");
//在字符串后面追加新的字符串
sb.append("學Java!");
System.out.println(sb);
//刪除指定位置上的字符串,從指定的下標開始和結束,下標從0開始
sb.delete(2, 4);
System.out.println(sb);//"一哥"
//在指定下標位置上添加指定的字符串
sb.insert(2, "123");
System.out.println(sb);//跟一123,學Java!
//將字符串翻轉
sb.reverse();
System.out.println(sb);//!avaJ學,321一跟
//將StringBuffer轉換成String類型
String s = sb.toString();
System.out.println(s);
}
}
3.3 append()用法
在以上幾個方法中,小編再重點給大家說一下append()追加方法。該方法的作用是追加內容到當前StringBuffer對象的末尾,類似于字符串的連接。調用該方法以后,StringBuffer對象的內容也會發(fā)生改變。使用該方法進行字符串的連接,會比String更加節(jié)約內存。我們可以利用append()方法進行動態(tài)內容的追加,比如進行數據庫SQL語句的拼接:
public class Demo02 {
public static void main(String[] args) {
StringBuffer sb = new StringBuffer();
String user = "yyg";
String pwd = "123";
//實現SQL語句的拼接
sb.append("select * from userInfo where username=")
.append(user)
.append(" and pwd=")
.append(pwd);
System.out.println("sql="+sb.toString());
}
}
StringBuffer的用法其實很簡單,和String差不多,大家簡單掌握即可。
三. StringBuilder
1.簡介
要想實現可變字符串的操作,其實還有另一個StringBuilder類,該類是在Java 5中被提出的。它和 StringBuffer的基本用法幾乎是完全一樣的,關于StringBuilder的用法,小編不會講解太多。
但StringBuilder和StringBuffer最大的不同在于,StringBuilder的各個方法都不是線程安全的(不能同步訪問),在多線程時可能存在線程安全問題,但StringBuilder的執(zhí)行效率卻比StringBuffer快的多。
實際上大多數情況下,我們都是在單線程下進行字符串的操作,所以使用StringBuilder并不會產生線程安全問題。所以針對大多數的單線程情況,小編還是建議大家使用StringBuilder,而不是StringBuffer,除非你們的項目對線程安全有著明確的高要求。
2.特性
StringBuilder作為可變字符串操作類,具有如下特性:
● StringBuilder是線程不安全的,但執(zhí)行效率更快;
● 適用于單線程環(huán)境下,在字符緩沖區(qū)進行大量操作的情況。
3.基本用法
StringBuilder的API方法和基本用法與StringBuffer一樣,此處略過。
四. 擴容機制(重點)
擴容機制應該是本篇文章中的一個重難點,所以小編要結合源碼,單獨列出一節(jié)給大家仔細分析一下。
在常規(guī)的用法上面,StringBuffer和StringBuilder基本沒有什么差別。兩者的主要區(qū)別在于StringBuffer是線程安全的,但效率低,StringBuilder是線程不安全的,但效率高。不過在擴容機制上,StringBuffer和StringBuilder是一樣的。所以在這里,小編就以StringBuffer為例,只給大家分析一個類即可。
1.繼承關系
首先我們可以追蹤一下StringBuffer的源碼,看看它繼承自哪個父類。
從上圖可以看出,StringBuffer和StringBuilder其實都是繼承自AbstractStringBuilder,所以StringBuffer與StringBuilder這兩者可以說是“親兄弟”的關系,它們倆有一個共同的抽象父類AbstractStringBuilder,如下所示:
2.AbstractStringBuilder抽象父類
小編在之前給大家講解抽象類時就跟大家說過,抽象類可以將多個子類個性化的實現,通過抽象方法交由子類來實現;而多個子類共性的方法,可以放在父類中實現。StringBuffer和StringBuilder的共同父類AbstractStringBuilder就是一個抽象類,在這個父類中把StringBuffer和StringBuilder的一些共同內容進行了定義。比如在該類中,就定義了一個定長的字節(jié)數組來保存字符串,后面當我們利用append()方法不斷地追加字符串時,如果該字符串的長度超過了這個數組的長度,就會利用數組復制的方式給該數組進行擴容。
3.容量設置
另外小編在前面給大家講解StringBuffer的API方法時,也給大家說過StringBuffer有3個構造方法。而無論是哪個構造方法都可以設置存儲容量,即使是默認的構造方法也會有值為16的存儲容量,如下圖所示:
4.擴容過程(核心)
4.1 StringBuffer#append()方法
雖然StringBuffer有默認的容量設置,也有自定義的容量設置,但在實際開發(fā)過程中,容量還是有可能不夠用。這時就會根據追加的字符串長度進行動態(tài)擴容,那么這個擴容過程到底是怎么樣的呢?其實StringBuffer的擴容需要利用append()方法作為入口,我們先來看看append()方法的源碼,如下所示:
4.2 AbstractStringBuilder#append()方法
在StringBuffer的append()方法中,你會發(fā)現實際上真正的實現是通過super關鍵字,在調用父類的append()方法,所以我們繼續(xù)往下追蹤,此時進入到AbstractStringBuilder類中的append()方法中,如下圖所示:
此時我們看到了一個ensureCapacityInternal()方法,從字面意思來理解,該方法是用于確保內部容量。傳遞給該方法的個參數是count+len,也就是 原有字符串的長度+新追加的字符串長度,即append后字符串的總長度。
4.3 ensureCapacityInternal()方法
那么ensureCapacityInternal()接受了新字符串的總長度之后會發(fā)生什么變化呢?我們必須進入到ensureCapacityInternal()方法的內部來探究一番,源碼如下:
在該方法中,我們首先看到了一個二進制位的右移運算。value.length是字符數組的長度,結合coder參數進行右移運算,得到字符串的原有容量。這里的coder參數是一種編碼方式,如果字符串中沒有中文,默認是采用Latin1編碼,如果有中文則會采用UTF-16編碼。因為UTF-16編碼中文時需要兩個字節(jié),也就是說,只要字符串中含有中文,value字節(jié)數組中是每兩位對應一個字符。
然后會判斷新追加的字符串長度是否超過了value字節(jié)數組的長度,如果新字符串的長度大于value字節(jié)數組的長度,則說明需要給該字節(jié)數組進行擴容。接著就會利用用Arrays.copyOf()方法,將當前數組的值拷貝給newCapacity()個長度的新數組,最后再重新賦值給value字節(jié)數組。在擴容的過程中,主要是利用數組復制的方法來實現!
4.4 newCapacity()方法
其實講到現在,關于StringBuffer的擴容,基本原理小編已經給大家講清楚了,但我們還可以繼續(xù)深入看看newCapacity()這個方法的實現過程與返回值,它與數組擴容密切相關。
該方法的大致作用就是,獲取value數組的原有長度和待追加的新字符串長度,利用ArraysSupport.newLength()方法計算出擴容后新數組的長度length,并最終返回該length。如果length的值等于Integer的最大值,說明我們傳遞過來的字符串太長了,就會直接觸發(fā)一個內存溢出的異常。
4.5 newLength()方法
而ArraysSupport.newLength()方法的內部實現,主要是利用Math.max()方法實現的,如下所示:
4.6 小結(重點)
至此,小編就把StringBuffer的擴容過程給大家分析完畢了,最后,小編再給大家把這個擴容的核心思路總結一下,StringBuffer擴容機制的基本規(guī)則如下:
● 如果一次追加的字符長度超過了當前設置的容量,則會按照 當前容量2+2 進行擴容;
● 如果一次追加的長度不僅超過了初始容量,而且按照 當前容量2+2 擴容一次還不夠,其容量會直接擴容到與所添加字符串長度相等的長度;
● 之后如果還要再追加新的字符內容,依然會按照 當前容量*2+2 進行擴容。
5. 驗證案例
最后為了驗證上述結論是否正確,小編再給大家設計如下案例,供大家思考驗證。
public class Demo03 {
// 擴容機制
public static void main(String[] args) {
//無參構造方法,初始容量默認為16
StringBuffer sb = new StringBuffer();
//使用StringBuffer的capacity()方法查看其當前容量
System.out.println("默認初始化容量capacity=" + sb.capacity() + ",默認長度length=" + sb.length());
//一次追加20個字符,因為超過了初始容量,因此會擴容16*2+2=34
sb.append("11111111112222222222");
System.out.println("擴容一次的capacity()=" + sb.capacity() + ",擴容一次后的length=" + sb.length());
StringBuffer sb02 = new StringBuffer();
//再次添加50個字符,不僅超過了初始容量16,而且按照 當前容量*2+2 進行擴容(34)后,依然存儲不下,
//則直接將容量擴容到新追加的字符串長度50
sb02.append("11111111112222222222333333333344444444445555555555");
System.out.println("再次擴容后的capacity="+sb02.capacity()+",再次擴容后的長度length():"+sb02.length());
}
}
從上述實驗的執(zhí)行結果中,你會發(fā)現StringBuffer與StringBuilder就是按照上述規(guī)則進行擴容的。
五. 結語
至此,我們就把字符串相關的內容都學習完了,接下來小編就把今天的重點內容給大家總結一下,尤其是String、StringBuffer與StringBuilder的區(qū)別有哪些。
1.相同點
String、StringBuffer、StringBuilder三者共同之處,它們都是final類,不允許被繼承,這樣設計主要是從性能和安全性上考慮的。
2.不同點
String、StringBuffer、StringBuilder這三個類之間的區(qū)別主要體現在3個方面,即 運行速度、線程安全、功能、可變性 這4個方面。
在運行速度方面:三者之間的執(zhí)行速度由快到慢為:StringBuilder > StringBuffer > String
在線程安全方面:StringBuilder是線程不安全的,而StringBuffer是線程安全的。
如果一個StringBuffer對象在字符串緩沖區(qū)被多個線程使用,StringBuffer中很多方法都帶有synchronized關鍵字,可以保證線程是安全的。但StringBuilder的方法中則沒有該關鍵字,所以不能保證線程安全,有可能在進行線程并發(fā)操作時產生一些異常。所以如果要進行多線程環(huán)境下的操作,考慮使用StringBuffer;在單線程環(huán)境下,建議使用速度StringBuilder。
在功能方面:String實現了三個接口,即Serializable、Comparable、CarSequence;
StringBuilder和StringBuffer實現了兩個接口,Serializable、CharSequence,相比之下String的實例可以通過compareTo方法進行比較,其他兩個不可以。
在可變性方面:String字符串是不可變的,StringBuilder與StringBuffer是可變的。
3.最后總結一下
String:適用于少量字符串操作的情況;
StringBuilder:適用于單線程環(huán)境下,在字符緩沖區(qū)進行大量操作的情況;
StringBuffer:適用多線程環(huán)境下,在字符緩沖區(qū)進行大量操作的情況;
使用場景:當修改字符串的操作比較多時,可以使用StringBuilder或StringBuffer;在要求線程安全的情況下用StringBuffer,在不要求線程安全的情況下用StringBuilder。