這是一道老生常談的問題了,字符串是不只是 Java 中很是重要的一個對象,它在其餘語言中也存在。好比 C++、Visual Basic、C# 等。字符串使用 String 來表示,字符串一旦被建立出來就不會被修改,當你想修改 StringBuffer 或者是 StringBuilder,出於效率的考量,雖然 String 能夠經過 + 來建立多個對象達到字符串拼接的效果,可是這種拼接的效率相比 StringBuffer 和 StringBuilder,那就是愛莫能助了。本篇文章咱們一塊兒來深刻了解一下這三個對象。java
String 表示的就是 Java 中的字符串,咱們平常開發用到的使用 ""
雙引號包圍的數都是字符串的實例。String 類實際上是經過 char 數組來保存字符串的。下面是一個典型的字符串的聲明正則表達式
String s = "abc";
上面你建立了一個名爲 abc
的字符串。api
字符串是恆定的,一旦建立出來就不會被修改,怎麼理解這句話?咱們能夠看下 String 源碼的聲明數組
告訴我你看到了什麼?String 對象是由final
修飾的,一旦使用 final 修飾的類不能被繼承、方法不能被重寫、屬性不能被修改。並且 String 不僅只有類是 final 的,它其中的方法也是由 final 修飾的,換句話說,Sring 類就是一個典型的 Immutable
類。也因爲 String 的不可變性,相似字符串拼接、字符串截取等操做都會產生新的 String 對象。緩存
因此請你告訴我下面安全
String s1 = "aaa"; String s2 = "bbb" + "ccc"; String s3 = s1 + "bbb"; String s4 = new String("aaa");
分別建立了幾個對象?多線程
javap -c
看一下反彙編代碼public class com.sendmessage.api.StringDemo { public com.sendmessage.api.StringDemo(); Code: 0: aload_0 1: invokespecial #1 // 執行對象的初始化方法 4: return public static void main(java.lang.String[]); Code: 0: ldc #2 // 將 String aaa 執行入棧操做 2: astore_1 # pop出棧引用值,將其(引用)賦值給局部變量表中的變量 s1 3: ldc #3 // String bbbccc 5: astore_2 6: return }
編譯器作了優化 String s2 = "bbb" + "ccc"
會直接被優化爲 bbbccc
。也就是直接建立了一個 bbbccc 對象。app
javap 是 jdk 自帶的
反彙編
工具。它的做用就是根據 class 字節碼文件,反彙編出當前類對應的 code 區(彙編指令)、本地變量表、異常表和代碼行偏移量映射表、常量池等等信息。工具javap -c 就是對代碼進行反彙編操做。源碼分析
咱們能夠看到,s3 執行 + 操做會建立一個 StringBuilder
對象而後執行初始化。執行 + 號至關因而執行 new StringBuilder.append()
操做。因此
String s3 = s1 + "bbb"; == String s3 = new StringBuilder().append(s1).append("bbb").toString(); // Stringbuilder.toString() 方法也會建立一個 String public String toString() { // Create a copy, don't share the array return new String(value, 0, count); }
因此 s3 執行完成後,至關於建立了 3 個對象。
說完了 String 對象,咱們再來講一下 StringBuilder 和 StringBuffer 對象。
上面的 String 對象居然和 StringBuilder 產生了千絲萬縷的聯繫。不得不說 StringBuilder 是一個牛逼的對象。String 對象底層是使用了 StringBuilder 對象的 append 方法進行字符串拼接的,不禁得對 StringBuilder 心生敬意。
不禁得咱們想要真正認識一下這個 StringBuilder 大佬,可是在認識大佬前,還有一個大 boss 就是 StringBuffer 對象,這也是你不得不跨越的鴻溝。
StringBuffer 對象
表明一個可變的字符串序列,當一個 StringBuffer 被建立之後,經過 StringBuffer 的一系列方法能夠實現字符串的拼接、截取等操做。一旦經過 StringBuffer 生成了最終想要的字符串後,就能夠調用其 toString
方法來生成一個新的字符串。例如
StringBuffer b = new StringBuffer("111"); b.append("222"); System.out.println(b);
咱們上面提到 +
操做符鏈接兩個字符串,會自動執行 toString()
方法。那你猜 StringBuffer.append 方法會自動調用嗎?直接看一下反彙編代碼不就完了麼?
上圖左邊是手動調用 toString 方法的代碼,右圖是沒有調用 toString 方法的代碼,能夠看到,toString() 方法不像 +
同樣自動被調用。
StringBuffer 是線程安全的,咱們能夠經過它的源碼能夠看出
StringBuffer 在字符串拼接上面直接使用 synchronized
關鍵字加鎖,從而保證了線程安全性。
最後來認識大佬了,StringBuilder 實際上是和 StringBuffer 幾乎同樣,只不過 StringBuilder 是非線程安全
的。而且,爲何 + 號操做符使用 StringBuilder 做爲拼接條件而不是使用 StringBuffer 呢?我猜想緣由是加鎖是一個比較耗時的操做,而加鎖會影響性能,因此 String 底層使用 StringBuilder 做爲字符串拼接。
咱們上面說到,使用 +
鏈接符時,JVM 會隱式建立 StringBuilder 對象,這種方式在大部分狀況下並不會形成效率的損失,不過在進行大量循環拼接字符串時則須要注意。以下這段代碼
String s = "aaaa"; for (int i = 0; i < 100000; i++) { s += "bbb"; }
這是一段很普通的代碼,只不過對字符串 s 進行了 + 操做,咱們經過反編譯代碼來看一下。
// 通過反編譯後 String s = "aaa"; for(int i = 0; i < 10000; i++) { s = (new StringBuilder()).append(s).append("bbb").toString(); }
你能看出來須要注意的地方了嗎?在每次進行循環時,都會建立一個 StringBuilder
對象,每次都會把一個新的字符串元素 bbb
拼接到 aaa
的後面,因此,執行幾回後的結果以下
每次都會建立一個 StringBuilder ,並把引用賦給 StringBuilder 對象,所以每一個 StringBuilder 對象都是強引用
, 這樣在建立完畢後,內存中就會多了不少 StringBuilder 的無用對象。瞭解更多關於引用的知識,請看
https://mp.weixin.qq.com/s/ZflBpn2TBzTNv_-G-zZxNg
這樣因爲大量 StringBuilder 建立在堆內存中,確定會形成效率的損失,因此在這種狀況下建議在循環體外建立一個 StringBuilder 對象調用 append()
方法手動拼接。
例如
StringBuilder builder = new StringBuilder("aaa"); for (int i = 0; i < 10000; i++) { builder.append("bbb"); } builder.toString();
這段代碼中,只會建立一個 builder 對象,每次循環都會使用這個 builder 對象進行拼接,所以提升了拼接效率。
咱們前面說過,String 類是典型的 Immutable
不可變類實現,保證了線程安全性,全部對 String 字符串的修改都會構造出一個新的 String 對象,因爲 String 的不可變性,不可變對象在拷貝時不須要額外的複製數據。
String 在 JDK1.6 以後提供了 intern()
方法,intern 方法是一個 native
方法,它底層由 C/C++ 實現,intern 方法的目的就是爲了把字符串緩存起來,在 JDK1.6 中卻不推薦使用 intern 方法,由於 JDK1.6 把方法區放到了永久代(Java 堆的一部分),永久代的空間是有限的,除了 Fullgc
外,其餘收集並不會釋放永久代的存儲空間。JDK1.7 將字符串常量池移到了堆內存
中,
下面咱們來看一段代碼,來認識一下 intern
方法
public static void main(String[] args) { String a = new String("ab"); String b = new String("ab"); String c = "ab"; String d = "a"; String e = new String("b"); String f = d + e; System.out.println(a.intern() == b); System.out.println(a.intern() == b.intern()); System.out.println(a.intern() == c); System.out.println(a.intern() == f); }
上述的執行結果是什麼呢?咱們先把答案貼出來,以防心急的同窗想急於看到結果,他們的答案是
false
true
true
false
和你預想的同樣嗎?爲何會這樣呢?咱們先來看一下 intern 方法的官方解釋
這裏你須要知道 JVM 的內存模型
虛擬機棧
: Java 虛擬機棧是線程私有的數據區,Java 虛擬機棧的生命週期與線程相同,虛擬機棧也是局部變量的存儲位置。方法在執行過程當中,會在虛擬機棧種建立一個 棧幀(stack frame)
。本地方法棧
: 本地方法棧也是線程私有的數據區,本地方法棧存儲的區域主要是 Java 中使用 native
關鍵字修飾的方法所存儲的區域程序計數器
:程序計數器也是線程私有的數據區,這部分區域用於存儲線程的指令地址,用於判斷線程的分支、循環、跳轉、異常、線程切換和恢復等功能,這些都經過程序計數器來完成。方法區
:方法區是各個線程共享的內存區域,它用於存儲虛擬機加載的 類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。堆
: 堆是線程共享的數據區,堆是 JVM 中最大的一塊存儲區域,全部的對象實例都會分配在堆上運行時常量池
:運行時常量池又被稱爲 Runtime Constant Pool
,這塊區域是方法區的一部分,它的名字很是有意思,它並不要求常量必定只有在編譯期才能產生,也就是並不是編譯期間將常量放在常量池中,運行期間也能夠將新的常量放入常量池中,String 的 intern 方法就是一個典型的例子。在 JDK 1.6 及以前的版本中,常量池是分配在方法區中永久代(Parmanent Generation)
內的,而永久代和 Java 堆是兩個徹底分開的區域。若是字符串常量池中已經包含一個等於此 String 對象的字符串,則返回常量池中這個字符串的 String 對象;不然,將此 String 對象包含的字符串添加到常量池中,而且返回此 String 對象的引用。
一些人把方法區稱爲永久代,這種說法不許確,僅僅是 Hotspot 虛擬機設計團隊選擇使用永久代來實現方法區而已。
從JDK 1.7開始去永久代
,字符串常量池已經被轉移至 Java 堆中,開發人員也對 intern 方法作了一些修改。由於字符串常量池和 new 的對象都存於 Java 堆中,爲了優化性能和減小內存開銷,當調用 intern 方法時,若是常量池中已經存在該字符串,則返回池中字符串;不然直接存儲堆中的引用,也就是字符串常量池中存儲的是指向堆裏的對象。
因此咱們對上面的結論進行分析
String a = new String("ab"); String b = new String("ab"); System.out.println(a.intern() == b);
輸出什麼? false,爲何呢?畫一張圖你就明白了(圖畫的有些問題,棧應該是後入先出,因此 b 應該在 a 上面,不過不影響效果)
a.intern 返回的是常量池中的 ab,而 b 是直接返回的是堆中的 ab。地址不同,確定輸出 false
因此第二個
System.out.println(a.intern() == b.intern());
也就沒問題了吧,它們都返回的是字符串常量池中的 ab,地址相同,因此輸出 true
而後來看第三個
System.out.println(a.intern() == c);
圖示以下
a 不會變,由於常量池中已經有了 ab ,因此 c 不會再建立一個 ab 字符串,這是編譯器作的優化,爲了提升效率。
下面來看最後一個
System.out.println(a.intern() == f);
首先來看一下 String 類在繼承樹的什麼位置、實現了什麼接口、父類是誰,這是源碼分析的幾大重要因素。
String 沒有繼承任何接口,不過實現了三個接口,分別是 **Serializable、Comparable、CharSequence **接口
重要屬性
字符串是什麼,字!符!串! 你品,你細品。你會發現它就是一連串字符組成的串。
也就是說
String str = "abc"; // === char data[] = {'a', 'b', 'c'}; String str = new String(data);
原來這麼回事啊!
因此,String 中有一個用於存儲字符的 char 數組 value[]
,這個數組存儲了每一個字符。另一個就是 hash 屬性,它用於緩存字符串的哈希碼。由於 String 常常被用於比較,好比在 HashMap 中。若是每次進行比較都從新計算其 hashcode 的值的話,那無疑是比較麻煩的,而保存一個 hashcode 的緩存無疑能優化這樣的操做。
String 能夠經過許多途徑建立,也能夠根據 Stringbuffer 和 StringBuilder 進行建立。
畢竟咱們本篇文章探討的不是源碼分析的文章,因此涉及到的源碼不會不少。
除此以外,String 還提供了一些其餘方法
charAt
:返回指定位置上字符的值
getChars
: 複製 String 中的字符到指定的數組
equals
: 用於判斷 String 對象的值是否相等
indexOf
: 用於檢索字符串
substring
: 對字符串進行截取
concat
: 用於字符串拼接,效率高於 +
replace
:用於字符串替換
match
:正則表達式的字符串匹配
contains
: 是否包含指定字符序列
split
: 字符串分割
join
: 字符串拼接
trim
: 去掉多餘空格
toCharArray
: 把 String 對象轉換爲字符數組
valueOf
: 把對象轉換爲字符串
StringBuilder 類表示一個可變的字符序列,咱們知道,StringBuilder 是非線程安全的容器,通常適用於單線程
場景中的字符串拼接操做,下面咱們就來從源碼角度看一下 StringBuilder
首先咱們來看一下 StringBuilder 的定義
public final class StringBuilder extends AbstractStringBuilder implements java.io.Serializable, CharSequence {...}
StringBuilder 被 final 修飾,表示 StringBuilder 是不可被繼承的,StringBuilder 類繼承於 AbstractStringBuilder類。實際上,AbstractStringBuilder 類具體實現了可變字符序列的一系列操做,好比:append()、insert()、delete()、replace()、charAt() 方法等。
StringBuilder 實現了 2 個接口
StringBuilder 使用 AbstractStringBuilder 類中的兩個變量做爲元素
char[] value; // 存儲字符數組 int count; // 字符串使用的計數
StringBuffer 也是繼承於 AbstractStringBuilder ,使用 value 和 count 分別表示存儲的字符數組和字符串使用的計數,StringBuffer 與 StringBuilder 最大的區別就是 StringBuffer 能夠在多線程場景下使用,StringBuffer 內部有大部分方法都加了 synchronized
鎖。在單線程場景下效率比較低,由於有鎖的開銷。
我相信這個問題不少同窗都沒有注意到吧,其實 StringBuilder 和 StringBuffer 存在擴容問題,先從 StringBuilder 開始看起
首先先注意一下 StringBuilder 的初始容量
public StringBuilder() { super(16); }
StringBuilder 的初始容量是 16,固然也能夠指定 StringBuilder 的初始容量。
在調用 append 拼接字符串,會調用 AbstractStringBuilder 中的 append 方法
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; }
上面代碼中有一個 ensureCapacityInternal
方法,這個就是擴容方法,咱們跟進去看一下
private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); } }
這個方法會進行判斷,minimumCapacity 就是字符長度 + 要拼接的字符串長度,若是拼接後的字符串要比當前字符長度大的話,會進行數據的複製,真正擴容的方法是在 newCapacity
中
private int newCapacity(int minCapacity) { // overflow-conscious code int newCapacity = (value.length << 1) + 2; if (newCapacity - minCapacity < 0) { newCapacity = minCapacity; } return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) ? hugeCapacity(minCapacity) : newCapacity; }
擴容後的字符串長度會是原字符串長度增長一倍 + 2,若是擴容後的長度還比拼接後的字符串長度小的話,那就直接擴容到它須要的長度 newCapacity = minCapacity,而後再進行數組的拷貝。
本篇文章主要描述了 String 、StringBuilder 和 StringBuffer 的主要特性,String、StringBuilder 和 StringBuffer 的底層構造是怎樣的,以及 String 常量池的優化、StringBuilder 和 StringBuffer 的擴容特性等。
若是有錯誤的地方,還請大佬們提出寶貴意見。