Think of Java :13 - 字符串

13.字符串

13.1 String 簡介

String 類是Java中很是基礎和重要的類,提供了構造和管理字符串的基本邏輯,它是典型的 Immutable 類,被聲明成爲 final class,全部的屬性也是 fianl 的。因爲它的不可變性,相似拼接、裁剪字符串等動做,都會產生新的 String 對象,所以字符串相關性能的效率對應用性能有明顯影響。java

13.2 String 對象的建立

經過字符串常量的方式

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

String()構造函數的方式

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

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

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

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

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

13.3 String經常使用方法

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // String類使用final做爲一個不可變類,實現了序列化接口、比較接口、字符序列接口
    // CharSequence 字符序列接口 經常使用實現類有String StringBuilder StringBuffer
    public int length() { return value.length;}  
    
    public char charAt(int index) {
       //...
        return value[index];
    }
    
    public String toString();
    
    // 拆分字符序列,包左不包右
    public CharSequence subSequence(int beginIndex, int endIndex) {
        return this.substring(beginIndex, endIndex);
    }

    // replace 和 replaceAll 二者都是能夠所有替換 replaceAll 支持正則表達式
    public String replace(CharSequence target, CharSequence replacement) {
        return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
                this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
    }    
    
    public String replaceAll(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }
    
    // equals是Object的方法,contenEquals能夠判斷字符序列內容是否相等
    public boolean equals(Object anObject) {
            //...
        }
    public boolean contentEquals(CharSequence cs) {
            //...
        }

    // 是否包含指定 字符序列
    public boolean contains(CharSequence s) {      
           return indexOf(s.toString()) > -1;
    }

    /*
     * 在調用」ab」.intern()方法的老是會返回常量池中」ab」的引用
     * 該方法會首先檢查字符串池中是否有」ab」這個字符串,若是存在則返回該字符串的引用,
     * 不然就將這個字符串添加到常量池中,然會返回該字符串的引用。
     */
    public native String intern();  
    
    // 返回指定索引的字符
    public char charAt(int index) {    
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
    }

    //判斷字符串中是否包含 指定字符,如包含返回索引,不然返回-1
    public int indexOf(String str) {
        //...
    }
    
    // 將輸入值轉爲字符串
    public static String valueOf(int i) {   
        return Integer.toString(i);
    }
    
    // 按regex分割成String數組,limit默認爲0,是分紅幾塊
    public String[] split(String regex, [int limit])  

    // 分割字符串包左不包右 
    public String substring(int beginIndex, [int endIndex]) {}  
    
    // 向當前字符串對象末尾追加str
    public String concat(String str) {}     

    // 變成字符數組 
    public char[] toCharArray() {}       

    // 字符串反轉
    StringBuilder public AbstractStringBuilder reverse() {}

靈活的字符串的分割

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

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

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

13.4 String 爲何設計爲不可變類

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

13.5 String的緩存

經過對常見應用的堆轉儲(Dump Heap),分析對象組成,會發現25%的對象是字符串,其中有半數是重複的,若是能避免建立重複的字符串,那麼能夠有效下降內存消耗和對象建立開銷,這即是 字符串常量池 存在的意義。優化

編譯期間,編譯器就會將代碼中用到的全部字符串彙總到一塊兒,保存在字節碼文件中某個位置,這一部分就是字符串常量池了,以上是靜態地生成常量池的過程。在程序運行期間,字符串也能夠被動態的添加至池中。ui

String 在 Java 6 之後提供了 intern()方法,目的是提示JVM把對應的字符串緩存起來,以備重複使用。在咱們對字符串對象調用 intern()方法的時候,若是常量池中已經有緩存的字符串,則會返回緩存的實例引用,不然先將其緩存起來,再返回實例引用。這樣就能夠避免建立重複的對象,幫助咱們節約很多的空間。

Intern 是一種顯示的排重機制,但它也有必定的反作用,一個是由於常量池的實現機制,相似於HashTable的實現方式,存儲的數據量越大,遍歷的時間複雜度就會增長,背離了intern的初衷;另外一個是寫起來不方便,並且在開發時,很難統計字符串的重複狀況。所以,咱們要結合場景使用。

Oracle JDK 8u20以後,推出了一個新特性,在G1 GC 下的字符串排重,它是經過將相同數據的字符串指向同一份數據來作到的,是JVM底層的改變,不須要Java類庫作什麼修改。

通常狀況下JVM 會將 相似 "abc"這樣的文本字符串,或者字符串常量之類的緩存起來。

在Java 6及之前的版本中,HotSpot 虛擬機經過用永久代來實現了JVM中方法區的概念(其餘虛擬機實現可沒有永久代的概念),這個空間是有限的,基本不會被FullGC之外的垃圾收集照顧到,所以,容易發生OOM。

Java 7中將字符串常量從永久代移出,放置在堆中,這樣就極大的避免了永久帶佔滿的問題。

JDK 8中永久代被 MetaSpace (元數據區)代替了,這個區域並不在JVM中,而是屬於本地內存,所以能夠隨着物理機的內存增加而增加。元空間存儲類的元信息,靜態變量和常量池等併入堆中。

13.6 字符串的拼接

字符串的拼接是對字符串操做使用最頻繁的操做之一,所以編譯器會對其進行優化。

字面量的直接拼接

// 字面量(文本字符串)直接拼接
public void test0() {
    String str = "111" + "222";
}

    // 反編譯後:
  public void test0();
    Code:
       0: ldc           #2    // String 111222   將int,float或String型常量值從常量池中推送至棧頂
       2: astore_1
       3: return

前面提到JVM會將相似 「abc」 這樣的文本字符串緩存起來,代碼中 「111」+「222」,編譯器能夠直接判斷出結果是 「111222」,因此會將 「111222」 這個字符字面量存放到常量池中,還須要注意的是,常量池中不會緩存 「111」 和 「222」。

public void test1() {
    final  String str1 = "111";
    final  String str2 = "222";
    String str = str1 + str2;
}

字符串常量也會被JVM在編譯期緩存起來,上面的代碼在反編譯後會和 test0()的反編譯結果相同。

字符串變量的拼接

// 字符串變量的拼接
public void test1() {
    String str1 = "111";
    String str2 = "222";
    String str = str1 + str2;
}

  public void test1();
    Code:
       0: ldc           #3                  // String 111
       2: astore_1
       3: ldc           #4                  // String 222
       5: astore_2
       6: new           #5                  // class java/lang/StringBuilder
       9: dup
      10: invokespecial #6                  // Method java/lang/StringBuilder."<init>":()V
      13: aload_1
      14: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      17: aload_2
      18: invokevirtual #7                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
      21: invokevirtual #8                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
      24: astore_3
      25: return

在JDK 8中,使用 「 + 」 進行的字符串變量拼接操做會自動被 Java編譯器轉換爲 StringBuilder 操做,下面的代碼進行編譯後會生成和上面同樣的字節碼指令,但寫法比較繁瑣。

public void test2() {
    String str1 = "111";
    String str2 = "222";
    String str=new StringBuilder().append(str1).append(str2).toString();
}

因爲這個特性的存在,咱們要儘可能不要在循環中使用字符串拼接,由於這種作法會致使建立大量的StringBuilder對象,從而容易引發OOM。

String str = "str";
for(int i=0; i<1000; i++) {
      str = str + i;
}
//編譯器優化後
String str = "str";
for(int i=0; i<1000; i++) {
    str = (new StringBuilder(String.valueOf(str))).append(i).toString();
}

因此咱們在作字符串拼接時,若是不涉及到線程安全的狀況下,咱們顯示的使用 StringBuilder 進行拼接,提高系統性能,若是涉及到線程安全的話,可使用 StringBuffer 來進行字符串拼接。

StringBuilder str = new StringBuilder("str");
for (int i = 0; i < 1000; i++) {
    str = str.append(i);
}
在JDK 9中爲了更加統一字符串的操做優化,提供了 StringConcatFactory。

13.7 String 的自身演化

16d5eac18d375060-1584499478807.jpg

Java6 以及之前版本

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

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

Java7 版本開始到 Java8 版本

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

Java9 版本開始

將 char[] 數組改成了 byte[] 數組,並增長一個標識編碼的屬性 coder。coder 屬性默認有 0 和 1 兩個值, 0 表明Latin-1(單字節編碼),1 表明 UTF-16 編碼。在計算字符串長度或者調用 indexOf() 方法時,會用到這個屬性。

Java中char 是兩個字節大小,但咱們平時使用的26個字母和數字均可以用一個byte來表示,只有在存中文字符的時候纔會用到兩個字節,所以爲了節約空間,將字符串的實現由以前的兩個字節,改成一個字節,咱們能夠明顯感覺到緊湊字符串帶來的優點,即更小的內存佔用,更快的傳輸和操做速度。

13.8 StringBuffer & StringBuilder

StringBuffer 是爲了解決上面提到的拼接產生太多中間對象的問題而提供的一個類,它是Java1.5中新增的,咱們能夠用appened 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置,StringBuffer 本質是一個線程安全的可修改字符序列,初始容量爲16,它在保證線程安全的同時也帶來了額外的性能開銷,除非有線程安全的須要,否則仍是推薦使用StringBuilder,這二者都繼承自 AbstractStringBuilder,在重寫父類方法時,只是簡單的調用了父類對應方法,區別在於StringBuffer 會在方法上加上 synchronized 來保證線程的安全。

StirngBuileder動態擴容

構建時初始的字符數組長度爲16,每次在拼接字符串的時候,都會修改底層的字符數組,不過在正式修改以前,先會肯定當前字符數組的容量是否足夠,不夠的話會進行擴容,擴容以後還要進行數組拷貝。這一過程會產生多重開銷,所以若是咱們能肯定拼接會發生很是屢次且容量大體可預計,那麼就能夠指定合適的初始大小,避免屢次擴容帶來的性能開銷。

Reference
《Java核心技術36講》

相關文章
相關標籤/搜索