Java String 對象,你真的瞭解了嗎?

String 對象的實現

String對象是 Java 中使用最頻繁的對象之一,因此 Java 公司也在不斷的對String對象的實現進行優化,以便提高String對象的性能,看下面這張圖,一塊兒瞭解一下String對象的優化過程。java

1. 在 Java6 以及以前的版本中

String對象是對 char 數組進行了封裝實現的對象,主要有四個成員變量: char 數組、偏移量 offset、字符數量 count、哈希值 hash。程序員

String對象是經過 offset 和 count 兩個屬性來定位 char[] 數組,獲取字符串。這麼作能夠高效、快速地共享數組對象,同時節省內存空間,但這種方式頗有可能會致使內存泄漏。正則表達式

2. 從 Java7 版本開始到 Java8 版本

從 Java7 版本開始,Java 對String類作了一些改變。String類中再也不有 offset 和 count 兩個變量了。這樣的好處是String對象佔用的內存稍微少了些,同時 String.substring 方法也再也不共享 char[],從而解決了使用該方法可能致使的內存泄漏問題。數組

3. 從 Java9 版本開始

將 char[] 數組改成了 byte[] 數組,爲何須要這樣作呢?咱們知道 char 是兩個字節,若是用來存一個字節的字符有點浪費,爲了節約空間,Java 公司就改爲了一個字節的byte來存儲字符串。這樣在存儲一個字節的字符是就避免了浪費。緩存

在 Java9 維護了一個新的屬性 coder,它是編碼格式的標識,在計算字符串長度或者調用 indexOf() 函數時,須要根據這個字段,判斷如何計算字符串長度。coder 屬性默認有 0 和 1 兩個值, 0 表明Latin-1(單字節編碼),1 表明 UTF-16 編碼。若是 String判斷字符串只包含了 Latin-1,則 coder 屬性值爲 0 ,反之則爲 1。安全

String 對象的建立方式

一、經過字符串常量的方式

String str= "pingtouge"的形式,使用這種形式建立字符串時, JVM 會在字符串常量池中先檢查是否存在該對象,若是存在,返回該對象的引用地址,若是不存在,則在字符串常量池中建立該字符串對象而且返回引用。使用這種方式建立的好處是:避免了相同值的字符串重複建立,節約了內存微信

二、String()構造函數的方式

String str = new String("pingtouge")的形式,使用這種方式建立字符串對象過程就比較複雜,分紅兩個階段,首先在編譯時,字符串pingtouge會被加入到常量結構中,類加載時候就會在常量池中建立該字符串。而後就是在調用new()時,JVM 將會調用String的構造函數,同時引用常量池中的pingtouge字符串, 在堆內存中建立一個String對象而且返回堆中的引用地址。app

瞭解了String對象兩種建立方式,咱們來分析一下下面這段代碼,加深咱們對這兩種方式的理解,下面這段代碼片中,str是否等於str1呢?函數

String str = "pingtouge";
  String str1 = new String("pingtouge");
  system.out.println(str==str1)
複製代碼

咱們逐一來分析這幾行代碼,首先從String str = "pingtouge"開始,這裏使用了字符串常量的方式建立字符串對象,在建立pingtouge字符串對象時,JVM會去常量池中查找是否存在該字符串,這裏的答案確定是沒有的,因此JVM將會在常量池中建立該字符串對象而且返回對象的地址引用,因此str指向的是pingtouge字符串對象在常量池中的地址引用。性能

而後是String str1 = new String("pingtouge")這行代碼,這裏使用的是構造函數的方式建立字符串對象,根據咱們上面對構造函數方式建立字符串對象的理解,str1獲得的應該是堆中pingtouge字符串的引用地址。因爲str指向的是pingtouge字符串對象在常量池中的地址引用而str1指向的是堆中pingtouge字符串的引用地址,因此str確定不等於str1

String 對象的不可變性

從咱們知道String對象的那一刻起,我想你們都知道了String對象是不可變的。那它不可變是怎麼作到的呢?Java 這麼作能帶來哪些好處?咱們一塊兒來簡單的探討一下,先來看看String 對象的一段源碼:

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0

    /** use serialVersionUID from JDK 1.0.2 for interoperability */
    private static final long serialVersionUID = -6849794470754667710L;
    }
複製代碼

從這段源碼中能夠看出,String類用了 final 修飾符,咱們知道當一個類被 final 修飾時,代表這個類不能被繼承,因此String類不能被繼承。這是String不可變的第一點

再往下看,用來存儲字符串的char value[]數組被privatefinal修飾,咱們知道對於一個被final的基本數據類型的變量,則其數值一旦在初始化以後便不能更改。這是String不可變的第二點。

Java 公司爲何要將String設置成不可變的,主要從如下三方面考慮:

  • 一、保證 String 對象的安全性。假設 String 對象是可變的,那麼 String 對象將可能被惡意修改。
  • 二、保證 hash 屬性值不會頻繁變動,確保了惟一性,使得相似 HashMap 容器才能實現相應的 key-value 緩存功能。
  • 三、能夠實現字符串常量池

String 對象的優化

字符串是咱們經常使用的Java類型之一,因此對字符串的操做也是避免不了的,在對字符串的操做過程當中,若是使用不當,性能會天差地別。那麼在字符串的操做過程當中,有哪些地方須要咱們注意呢?

優雅的拼接字符串

字符串的拼接是對字符串操做使用最頻繁的操做之一,因爲咱們知道String對象的不可變性,因此咱們在作拼接時儘量少的使用+進行字符串拼接或者說潛意識裏認爲不能使用+進行字符串拼接,認爲使用+進行字符串拼接會產生許多無用的對象。事實真的是這樣嗎?咱們來作一個實驗。咱們使用+來拼接下面這段字符串。

String str8 = "ping" +"tou"+"ge";
複製代碼

一塊兒來分析一下這段代碼會產生多少個對象?若是按照咱們理解的意思來分析的話,首先會建立ping對象,而後建立pingtou對象,最後纔會建立pingtouge對象,一共建立了三個對象。真的是這樣嗎?其實不是這樣的,Java 公司怕咱們程序員手誤,因此對編譯器進行了優化,上面的這段字符串拼接會被咱們的編譯器優化,優化成一個String str8 = "pingtouge";對象。除了對常量字符串拼接作了優化之外,對於使用+號動態拼接字符串,編譯器也作了相應的優化,以便提高String的性能,例以下面這段代碼:

String str = "pingtouge";

for(int i=0; i<1000; i++) {
      str = str + i;
}

複製代碼

編譯器會幫咱們優化成這樣

String str = "pingtouge";

for(int i=0; i<1000; i++) {
        	  str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

複製代碼

能夠看出 Java 公司對這一塊進行了很多的優化,防止因爲程序員不當心致使String性能急速降低,儘管 Java 公司在編譯器這一塊作了相應的優化,可是咱們仍是能看出 Java 公司優化的不足之處,在動態拼接字符串時,雖然使用了 StringBuilder 進行字符串拼接,可是每次循環都會生成一個新的 StringBuilder 實例,一樣也會下降系統的性能。

因此咱們在作字符串拼接時,咱們須要從代碼的層面進行優化,在動態的拼接字符串時,若是不涉及到線程安全的狀況下,咱們顯示的使用 StringBuilder 進行拼接,提高系統性能,若是涉及到線程安全的話,咱們使用 StringBuffer 來進行字符串拼接

巧妙的使用 intern() 方法

* <p>
     * When the intern method is invoked, if the pool already contains a
     * string equal to this {@code String} object as determined by
     * the {@link #equals(Object)} method, then the string from the pool is
     * returned. Otherwise, this {@code String} object is added to the
     * pool and a reference to this {@code String} object is returned.
     * <p>
     public native String intern();
複製代碼

這是 intern() 函數的官方註釋說明,大概意思就是 intern 函數用來返回常量池中的某字符串,若是常量池中已經存在該字符串,則直接返回常量池中該對象的引用。不然,在常量池中加入該對象,而後 返回引用。

有一位Twitter工程師在QCon全球軟件開發大會上分享了一個他們對 String對象優化的案例,他們利用String.intern()方法將之前須要20G內存存儲優化到只須要幾百兆內存。這足以體現String.intern()的威力,咱們一塊兒來看一個例子,簡單的瞭解一下String.intern()的用法。

public static void main(String[] args) {
        String str = new String("pingtouge");
        String str1 = new String("pingtouge");
        System.out.println("未使用intern()方法:"+(str==str1));
        System.out.println("未使用intern()方法,str:"+str);
        System.out.println("未使用intern()方法,str1:"+str1);

        String str2= new String("pingtouge").intern();
        String str3 = new String("pingtouge").intern();
        System.out.println("使用intern()方法:"+(str2==str3));
        System.out.println("使用intern()方法,str2:"+str2);
        System.out.println("使用intern()方法,str3:"+str3);

    }
複製代碼

從結果中能夠看出,未使用 String.intern()方法時,構造相同值的字符串對象返回不一樣的對象引用地址,使用 String.intern()方法後,構造相同值的字符串對象時,返回相同的對象引用地址。這能幫咱們節約很多空間

String.intern()方法雖然好,可是咱們要結合場景使用,不能亂用,由於常量池的實現是相似於一個HashTable的實現方式,HashTable 存儲的數據越大,遍歷的時間複雜度就會增長。若是數據過大,會增長整個字符串常量池的負擔。

靈活的字符串的分割

字符串的分割是字符串操做的經常使用操做之一,對於字符串的分割,大部分人使用的都是 Split() 方法,Split() 方法大多數狀況下使用的是正則表達式,這種分割方式自己沒有什麼問題,可是因爲正則表達式的性能是很是不穩定的,使用不恰當會引發回溯問題,極可能致使 CPU 居高不下。在如下兩種狀況下 Split() 方法不會使用正則表達式:

  • 傳入的參數長度爲1,且不包含「.$|()[{^?*+\」regex元字符的狀況下,不會使用正則表達式
  • 傳入的參數長度爲2,第一個字符是反斜槓,而且第二個字符不是ASCII數字或ASCII字母的狀況下,不會使用正則表達式

因此咱們在字符串分割時,應該慎重使用 Split() 方法,首先考慮使用 String.indexOf() 方法進行字符串分割,若是 String.indexOf() 沒法知足分割要求,再使用 Split() 方法,使用 Split() 方法分割字符串時,須要注意回溯問題。

文章不足之處,望你們多多指點,共同窗習,共同進步

參考資料

  • Java性能調優實戰 劉超

最後

打個小廣告,歡迎掃碼關注微信公衆號:「平頭哥的技術博文」,一塊兒進步吧。

平頭哥的技術博文
相關文章
相關標籤/搜索