Java核心技術----String,StringBuffer,StringBuilder詳解

1.概述

(1)String 字符串常量 (線程安全)
 Immutable 類,被聲明成爲 final class,所有屬性也都是 final 的,是不可變的對象, 因此在每次對 String 類型進行改變的時候其實都等同於生成了一個新的 String 對象,然後將指針指向新的 String 對象,所以經常改變內容的字符串最好不要用 String ,因爲每次生成對象都會對系統性能產生影響,特別當內存中無引用對象多了以後, JVM 的 GC 就會開始工作,那速度是一定會相當慢的。

(2)StringBuffer 字符串變量 (線程安全)
 爲解決上面提到String拼接產生太多中間對象的問題而提供的一個類,它是 Java 1.5 中新增的,我們可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。每次結果都會對 StringBuffer 對象本身進行操作,而不是生成新的對象,再改變對象引用。StringBuffer 本質是一個線程安全的可修改字符序列,它保證了線程安全,也隨之帶來了額外的性能開銷,所以除非有線程安全的需要,不然還是推薦使用它的後繼者,也就是 StringBuilder。

(3)StringBuilder 字符串變量 (非線程安全)
 此類提供一個與 StringBuffer 兼容的 API,但不保證同步。該類被設計用作 StringBuffer 的一個簡易替換,用在字符串緩衝區被單個線程使用的時候(這種情況很普遍)。如果可能,建議優先採用該類,因爲在大多數實現中,它比 StringBuffer 要快。兩者的方法基本相同。

一般情況下,拼接字符串時效率:StringBuilder > StringBuffer > String

應用場景:

  • 在字符串內容不經常發生變化的業務場景優先使用String類. 例如:常量聲明、少量的字符串拼接操作等。如果有大量的字符串內容拼接,避免使用String與String之間的“+”操作,因爲這樣會產生大量無用的中間對象,耗費空間且執行效率低下(新建對象、回收對象花費大量時間)。
  • 在頻繁進行字符串的運算(如拼接、替換、刪除等),並且運行在單線程環境下,建議使用StringBuilder,例如SQL語句拼裝、JSON封裝等。在頻繁進行字符串的運算(如拼接、替換、刪除等),並且運行在多線程環境下,建議使用StringBuffer,例如XML解析、HTTP參數解析與封裝。
  • 在頻繁進行字符串的運算(如拼接、替換、刪除等),並且運行在單線程環境下,建議使用StringBuilder,例如SQL語句拼裝、JSON封裝等。

2.深入分析

(1)String:

  • String類中使用字符數組保存字符串,如下就是,因爲有“final”修飾符,所以可以知道string對象是不可變的.
private final char value[];
  • String中的對象是不可變的,也就可以理解爲常量,原生的保證了基礎線程安全,因爲你無法對它內部數據進行任何修改,這種便利甚至體現在拷貝構造函數中,由於不可變,Immutable 對象在拷貝時不需要額外複製數據。

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

(2)StringBuffer:

  • StringBuffer 和 StringBuilder 底層都是利用可修改的(char,JDK 9 以後是 byte)數組,二者都繼承了AbstractStringBuilder,定義了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
  • StringBuffer對方法加了同步鎖或者對調用的方法加了同步鎖,所以是線程安全的。看如下源碼:
public synchronized StringBuffer append(String string) {
        append0(string);
        return this;
}
  • 關於內部數組的創建,目前的實現是構建時初始字符串長度加 16(這意味着,如果沒有構建對象時輸入最初的字符串,那麼初始值就是 16)。我們如果確定拼接會發生非常多次,而且大概是可預計的,那麼就可以指定合適的大小,避免很多次擴容的開銷。擴容會產生多重開銷,因爲要拋棄原有數組,創建新的(可以簡單認爲是倍數)數組,還要進行 arraycopy。

  • 在 JDK 8 中,字符串拼接操作會自動被 javac 轉換爲 StringBuilder 操作,而在 JDK 9 裏面則是因爲 Java 9 爲了更加統一字符串操作優化,提供了 StringConcatFactory,作爲一個統一的入口。javac 自動生成的代碼,雖然未必是最優化的,但普通場景也足夠了,你可以酌情選擇。

    StringBuilder儘管在拼接字符串時效率高於StringBuffer,但是最終還是好轉成String類型的,而在大字符串拼接的情況下StringBuffer的toString()要比StringBuilder的toString()執行速度將近快3.5倍。

    項目中,如果字符串不是非常的大併發量不是非常的高,StringBuffer或者StringBuilder並不是項目中的性能瓶頸,真沒必要太糾結與選擇哪個的問題,在此前提下,爲了一個很好的編碼規範,建議大家確定線程安全的情況下選擇StingBuilder,線程不安全的情況下還是用StringBuffer。

(3)StringBuilder:

  • AbstractStringBuilder是StringBuilder與StringBuffer的公共父類,定義了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。
  • StringBuilder並沒有對方法進行加同步鎖,所以是非線程安全的。

(4)字符串緩存

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

    • String提供了inter()方法。調用該方法時,如果常量池中包括了一個等於此String對象的字符串(由equals方法確定),則返回池中的字符串。否則,將此String對象添加到池中,並且返回此池中對象的引用。
  • 使用 Java 6 這種歷史版本,並不推薦大量使用 intern,爲什麼呢?魔鬼存在於細節中,被緩存的字符串是存在所謂 PermGen 裏的,也就是臭名昭著的“永久代”,這個空間是很有限的,也基本不會被 FullGC 之外的垃圾收集照顧到。所以,如果使用不當,OOM 就會光顧在後續版本中,這個緩存被放置在堆中,這樣就極大避免了永久代佔滿的問題,甚至永久代在 JDK 8 中被 MetaSpace(元數據區)替代了。而且,默認緩存大小也在不斷地擴大中,從最初的 1009,到 7u40 以後被修改爲 60013。

  • Intern 是一種顯式地排重機制,但是它也有一定的副作用,因爲需要開發者寫代碼時明確調用,一是不方便,每一個都顯式調用是非常麻煩的;另外就是我們很難保證效率,應用開發階段很難清楚地預計字符串的重複情況,有人認爲這是一種污染代碼的實踐。幸好在 Oracle JDK 8u20 之後,推出了一個新的特性,也就是 G1 GC 下的字符串排重。它是通過將相同數據的字符串指向同一份數據來做到的,是 JVM 底層的改變,並不需要 Java 類庫做什麼修改。注意這個功能目前是默認關閉的,你需要使用下面參數開啓,並且記得指定使用 G1 GC:

-XX:+UseStringDeduplication
  • 在運行時,字符串的一些基礎操作會直接利用 JVM 內部的 Intrinsic 機制,往往運行的就是特殊優化的本地代碼,而根本就不是 Java 代碼生成的字節碼。Intrinsic 可以簡單理解爲,是一種利用 native 方式 hard-coded 的邏輯,算是一種特別的內聯,很多優化還是需要直接使用特定的 CPU 指令,具體可以看相關源碼,搜索“string”以查找相關 Intrinsic 定義。當然,你也可以在啓動實驗應用時,使用下面參數,瞭解 intrinsic 發生的狀態。
-XX:+PrintCompilation -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
    // 樣例輸出片段    
        180    3       3       java.lang.String::charAt (25 bytes)  
                                  @ 1   java.lang.String::isLatin1 (19 bytes)   
                                  ...  
                                  @ 7 java.lang.StringUTF16::getChar (60 bytes) intrinsic

(5)String自身演化

  • Java 的字符串,在歷史版本中,它是使用 char 數組來存數據的,這樣非常直接。但是 Java 中的 char 是兩個 bytes 大小,拉丁語系語言的字符,根本就不需要太寬的 char,這樣無區別的實現就造成了一定的浪費。密度是編程語言平臺永恆的話題,因爲歸根結底絕大部分任務是要來操作數據的。

  • 在 Java 9 中,引入了 Compact Strings 的設計,對字符串進行了大刀闊斧的改進。將數據存儲方式從 char 數組,改變爲一個 byte 數組加上一個標識編碼的所謂 coder,並且將相關字符串操作類都進行了修改。另外,所有相關的 Intrinsic 之類也都進行了重寫,以保證沒有任何性能損失。雖然底層實現發生了這麼大的改變,但是 Java 字符串的行爲並沒有任何大的變化,所以這個特性對於絕大部分應用來說是透明的,絕大部分情況不需要修改已有代碼。

  • 當然,在極端情況下,字符串也出現了一些能力退化,比如最大字符串的大小。你可以思考下,原來 char 數組的實現,字符串的最大長度就是數組本身的長度限制,但是替換成 byte 數組,同樣數組長度下,存儲能力是退化了一倍的!還好這是存在於理論中的極限,還沒有發現現實應用受此影響。

  • 在通用的性能測試和產品實驗中,我們能非常明顯地看到緊湊字符串帶來的優勢,即更小的內存佔用、更快的操作速度。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章