05、String、StringBuffer、StringBuilder區別

目錄

現在來總結一下String、StringBuffer、StringBuilder三者的區別

1、String

(1) String的創建機理

(2) String的特性

2 、StringBuffer/StringBuilder

3、 應用場景

字符串設計和實現考量

字符串緩存

String 自身的演化


現在來總結一下String、StringBuffer、StringBuilder三者的區別

 

1、String
 

String:底層利用字符數組(char[ ])保存字符串常量它是典型的 Immutable 類,被聲明成爲 final class,所有屬性也都是 final 的。也由於它的不可變性,類似拼接、裁剪字符串等動作,都會產生新的 String 對象。由於字符串操作的普遍性,所以相關操作的效率往往對應用性能有明顯影響。String中的對象是不可變的,也就可以理解爲常量,顯然線程安全。

 

(1) String的創建機理

由於String在Java世界中使用過於頻繁,Java爲了避免在一個系統中產生大量的String對象,引入了字符串常量池。其運行機制是:創建一個字符串時,首先檢查池中是否有值相同的字符串對象,如果有則不需要創建,直接從池中剛查找到的對象引用;如果沒有則新建字符串對象,返回對象引用,並且將新創建的對象放入池中。但是,通過new方法創建的String對象是不檢查字符串池的,而是直接在堆區或棧區創建一個新的對象,也不會把對象放入池中。上述原則只適用於通過直接給String對象引用賦值的情況。

舉例:

   String str1 = "123";                          //通過直接量賦值方式,放入字符串常量池
              String str2 = new String(“123”);       //通過new方式賦值方式,不放入字符串常量池

注意:String提供了inter()方法。調用該方法時,如果常量池中包括了一個等於此String對象的字符串(由equals方法確定),則返回池中的字符串。否則,將此String對象添加到池中,並且返回此池中對象的引用。

 

(2) String的特性

  •  不可變。是指String對象一旦生成,則不能再對它進行改變。不可變的主要作用在於當一個對象需要被多線程共享,並且訪問頻繁時,可以省略同步和鎖等待的時間,從而大幅度提高系統性能。不可變模式是一個可以提高多線程程序的性能,降低多線程程序複雜度的設計模式。
  • 針對常量池的優化。當2個String對象擁有相同的值時,他們只引用常量池中的同一個拷貝。當同一個字符串反覆出現時,這個技術可以大幅度節省內存空間。
     

2 、StringBuffer/StringBuilder

StringBuffer和StringBuilder都實現了AbstractStringBuilder抽象類,擁有幾乎一致對外提供的調用接口;我們可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。

其底層在內存中的存儲方式與String相同,都是以一個有序的字符序列(char類型的數組,char[ ])進行存儲,不同點是StringBuffer/StringBuilder對象的值是可以改變的,並且值改變以後,對象引用不會發生改變;

兩者對象在構造過程中,首先按照默認大小申請一個字符數組,由於會不斷加入新數據,當超過默認大小後,會創建一個更大的數組,並將原先的數組內容複製過來,再丟棄舊的數組。因此,對於較大對象的擴容會涉及大量的內存複製操作,如果能夠預先評估大小,可提升性能。

唯一需要注意的是:StringBuffer是線程安全的,但是StringBuilder是線程不安全的。可參看Java標準類庫的源代碼,StringBuffer類中方法定義前面都會有synchronize關鍵字。爲此,StringBuffer的性能要遠低於StringBuilder
 

3、 應用場景

在字符串內容不經常發生變化的業務場景優先使用String類。例如:常量聲明、少量的字符串拼接操作等。如果有大量的字符串內容拼接,避免使用String與String之間的“+”操作,因爲這樣會產生大量無用的中間對象,耗費空間且執行效率低下(新建對象、回收對象花費大量時間)。

在頻繁進行字符串的運算(如拼接、替換、刪除等),並且運行在多線程環境下,建議使用StringBuffer,例如XML解析、HTTP參數解析與封裝。

在頻繁進行字符串的運算(如拼接、替換、刪除等),並且運行在單線程環境下,建議使用StringBuilder,例如SQL語句拼裝、JSON封裝等。

 

字符串設計和實現考量

爲了實現修改字符序列的目的,StringBuffer 和 StringBuilder 底層都是利用可修改的 char[ ](JDK 9 以後是 byte)數組,二者都繼承了 AbstractStringBuilder,裏面包含了基本操作,區別僅在於最終的方法是否加了 synchronized

另外,這個內部數組應該創建成多大的呢?如果太小,拼接的時候可能要重新創建足夠大的數組;如果太大,又會浪費空間。目前的實現是,構建時初始字符串長度加 16(這意味着,如果沒有構建對象時輸入最初的字符串,那麼初始值就是 16)。我們如果確定拼接會發生非常多次,而且大概是可預計的,那麼就可以指定合適的大小,避免很多次擴容的開銷。擴容會產生多重開銷,因爲要拋棄原有數組,創建新的(可以簡單認爲是倍數)數組,還要進行 arraycopy

在 JDK 8 中,String字符串拼接操作會自動被 javac 轉換爲 StringBuilder 操作,而在 JDK 9 裏面則是因爲 Java 9 爲了更加統一字符串操作優化,提供了 StringConcatFactory,作爲一個統一的入口。

 

字符串緩存

我們粗略統計過,把常見應用進行堆轉儲(Dump Heap),然後分析對象組成,會發現平均 25% 的對象是字符串,並且其中約半數是重複的。如果能避免創建重複字符串,可以有效降低內存消耗和對象創建開銷。

String 在 Java 6 以後提供了 intern() 方法,目的是提示 JVM 把相應字符串緩存起來,以備重複使用。在我們創建字符串對象並調用 intern() 方法的時候,如果已經有緩存的字符串,就會返回緩存裏的實例,否則將其緩存起來。一般來說,JVM 會將所有的類似“abc”這樣的文本字符串,或者字符串常量之類緩存起來。

看起來很不錯是吧?但實際情況估計會讓你大跌眼鏡。一般使用 Java 6 這種歷史版本,並不推薦大量使用 intern(),爲什麼呢?魔鬼存在於細節中,被緩存的字符串是存在所謂 PermGen 裏的,也就是臭名昭著的“永久代”,這個空間是很有限的,也基本不會被 FullGC 之外的垃圾收集照顧到。所以,如果使用不當,OOM (Out Of Memory)就會光顧。

在後續版本中,這個緩存被放置在堆中,這樣就極大避免了永久代佔滿的問題,甚至永久代在 JDK 8 中被 MetaSpace(元數據區)替代了。而且,默認緩存大小也在不斷地擴大中,從最初的 1009,到 jdk 7 以後被修改爲 60013。你可以使用下面的參數直接打印具體數字,可以拿自己的 JDK 立刻試驗一下。

-XX:+PrintStringTableStatistics

你也可以使用下面的 JVM 參數手動調整大小,但是絕大部分情況下並不需要調整,除非你確定它的大小已經影響了操作效率。

-XX:StringTableSize=N

Intern() 是一種顯式地排重機制,但是它也有一定的副作用,因爲需要開發者寫代碼時明確調用,一是不方便,每一個都顯式調用是非常麻煩的;另外就是我們很難保證效率,應用開發階段很難清楚地預計字符串的重複情況,有人認爲這是一種污染代碼的實踐。

注意這個功能目前是默認關閉的,你需要使用下面參數開啓,並且記得指定使用 G1 GC:

-XX:+UseStringDeduplication

在運行時,字符串的一些基礎操作會直接利用 JVM 內部的 Intrinsic機制,往往運行的就是特殊優化的本地代碼,而根本就不是 Java 代碼生成的字節碼。

 

String 自身的演化

Java 的String字符串,在歷史版本中,它是使用  char [ ] 數組來存數據的,這樣非常直接。但是 Java 中的 char 是兩個 bytes 大小,拉丁語系語言的字符,根本就不需要太寬的 char,這樣無區別的實現就造成了一定的浪費。

在 Java 9 中,將數據存儲方式從 char 數組,改變爲一個 byte 數組加上一個標識編碼的所謂 coder,並且將相關字符串操作類都進行了修改。另外,所有相關的 Intrinsic 之類也都進行了重寫,以保證沒有任何性能損失。

發佈了96 篇原創文章 · 獲贊 19 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章