Notes 20180311 : String第三講_深刻了解String

  不少前輩我可能對於個人這節文章很困惑,以爲String這個東西還有什麼須要特別瞭解的嗎?其實否則,String是一個使用十分頻繁的工具類,不可避免地咱們也會遇到一些陷阱,深刻了解String對於咱們避免陷阱,甚至優化操做是頗有必要的。本節咱們主要講解"碼點與代碼單元"、「不可變的String」、「無心識的遞歸」、「重載+」。html

1.碼點與代碼單元

  Java字符串是由字符序列組成的。而前面咱們介紹char數據類型的時候也講到過,char數據類型是一個採用UTF-16編碼表示Unicode碼點的代碼單元大多數的經常使用Unicode字符使用一個代碼單元就能夠表示,而輔助字符須要一對代碼單元表示。更多Unicode的內容能夠參見Knowledge Point 20180305 Java程序員詳述編碼Unicodejava

1.1 字符串「長度」

  String中提供了一個方法length(),該方法將返回採用UTF-16編碼表示的給定字符串所須要的代碼單元數量。注意是代碼單元數量,而不是字符串的長度(咱們一般所理解的字符串長度是字符串中字符個數,這裏獲得的並非這種結果);除了length()外,String還提供了另外一個關於長度的方法codePointCount(int beginIndex, int endIndex),該方法返回此 String指定文本範圍內的Unicode代碼點數。在這裏咱們要搞清楚代碼單元和代碼點數的區別,代碼點:是指一個編碼表中的某個字符對應的代碼值,也就是Unicode編碼表中每一個字符對應的數值代碼單元是指表示一個代碼點所需的最小單位,在Java中使用的是char數據類型,一個char表示一個代碼單元,這也就是爲何下面的代碼會編譯報錯,𝕆是一個輔助字符,須要用兩個代碼單元表示,因此這裏不能使用char類型來表示。程序員

//        char ch = '𝕆';會提示無效的字符,由於char只能表示基本的

看下面的例子:編程

String greeting = "Hello";
System.out.println("字符串greeting的代碼單元長度:" + greeting.length());//字符串greeting的代碼單元長度:5
System.out.println("字符串greeting的碼點數量:" + greeting.codePointCount(0, greeting.length()));//字符串greeting的碼點數量:5

  上面的代碼並無什麼晦澀難懂的地方,咱們使用的都是經常使用的Unicode字符,它們使用一個代碼單元(2個字節)就能夠表示,因此字符的碼點數量和代碼單元的數量是一致的,可是咱們不要忘了Unicode中是存在輔助字符的,輔助字符是一個字符佔用兩個代碼單元,下面咱們來看另外一個例子:數組

String str = "𝕆 is the set of octonions";
System.out.println("字符串str的代碼單元長度"+ str.length());//字符串str的代碼單元長度26
System.out.println("字符串str的碼點數量:" + str.codePointCount(0, str.length()));//字符串str的碼點數量:25
System.out.println("str獲取指定代碼單元的碼點:" + str.codePointAt(1));//56646

   經過這段代碼,咱們很容易就看出了兩個方法的區別了,length()返回String中的代碼單元,底層是經過字符數組的長度來獲取。而codePointCount()返回了代碼點數量,底層是Character的一個方法。因爲使用了一個輔助字符,因此明顯的代碼單元是比代碼點數量多1的。在最後一句咱們獲取索引1處的碼點,獲得的也並不是是「空格」,空格的Unicode是32,因此這裏返回的並非一個空格,而是輔助字符的第二個代碼單元。安全

1.2 String中對於碼點的操做方法

  String中給咱們提供了不少用於操做碼點的方法,咱們在上一節中已經認識了,這節咱們詳細羅列一下:數據結構

  1. int      codePointAt(int index)  返回指定索引處的字符(Unicode代碼點)。  IndexOutOfBoundsException
  2. int        codePointBefore(int index)  返回指定索引以前的字符(Unicode代碼點)。  IndexOutOfBoundsException
  3. int        codePointCount(int beginIndex, int endIndex)  返回此 String指定文本範圍內的Unicode代碼點數IndexOutOfBoundsException
  4. int        offsetByCodePoints(int index, int codePointOffset)   返回此 String內的指數,與 index codePointOffset代碼點。IndexOutOfBoundsException

  上面幾個方法都存在索引越界的異常【底層是數組,因此存在這種隱患,在操做時應該注意參數越界的狀況】,這裏全部的參數是代碼單元。前面三個方法咱們已經認識過,這裏就只講解一下第四個方法:「這個函數的第二個參數是以第一個參數爲標準後移的代碼單元(注意是代碼單元,不是代碼點)的數量。返回該代碼點在字符串中的代碼單元索引。」app

String str2 = "𝕆is the set 𝕆is the set of octonions of octonions";
System.out.println(str2.offsetByCodePoints(7, 7));//15     以第7個代碼點爲標準後移7個代碼點後是i,在字符串中的代碼單元位置爲15
System.out.println(str2.codePointAt(15));//105
String str3 = "i";
System.out.println(str3.codePointAt(0));//10

  看完上面的,咱們再來看一下另外兩個方法:dom

System.out.println(str2.codePointAt(0));//120134
        System.out.println(str2.codePointAt(1));//56646
        System.out.println(str2.codePointBefore(2));//120134
        System.out.println(str2.codePointBefore(1));//55349

  codePointAt(int index)該方法會返回該代碼單元的碼點數,可是該方法會向後尋找,可是不能向前尋找,因此在操做輔助字符的時候,咱們發現若是查詢的是輔助字符的第一個代碼單元,那麼返回的是該輔助字符的碼點數,這是由於該方法向後尋找和第二個代碼單元合併成了一個完整的輔助字符。但若是查看的輔助字符的第二個代碼單元,那麼就只能返回第二個代碼單元的碼點數了。String應對該方法,也提供了一個向前查詢的方法codePointBefore該方法會查詢給定代碼單元前的碼點數可是若是給定代碼單元是普通字符,那麼無論該代碼單元前面是普通字符仍是輔助字符,均可以完整顯示該碼點數。若是給定代碼單元是輔助字符且是輔助字符的第二個代碼單元,那麼就只會返回該輔助字符的第一個代碼單元了。ide

1.3 String關於碼點的練習操做

1.3.1 獲取碼點數組和代碼單元數組

  給定一個字符串,將該字符串返回一個由碼點數構成的int數組和代碼單元構成的int數組:

    @Test
    public void test1(){
        String str1 = "𝕆is the set 𝕆is";
        System.out.println(Arrays.toString(codePoint(str1)));
    }
    /**
     * 碼點數組
     * @param str
     */
    public int[] codePoint(String str){
        int[] arr = new int[str.codePointCount(0, str.length())];
        int cp = 0;
        int j = 0;
        for (int i = 0; i < str.length();) {
            cp = str.codePointAt(i);
            if(Character.isSupplementaryCodePoint(cp)){
                arr[j] = cp;
                i += 2;
            }else{
                arr[j] = cp;
          i++; }
j++; } return arr; }

  上面咱們看到使用到了Character的一個靜態方法isSupplementaryCodePoint(int index),該方法的做用「肯定指定字符(Unicode代碼點)是否在 supplementary character範圍內,即檢查該碼點是不是輔助字符的碼點」,

代碼分析:

  咱們首先要建立一個數組來存放字符串中的碼點,這個數組的長度和字符串的碼點數量一致;定義兩個變量做爲碼點數和數組角標,遍歷字符串,判斷每一個代碼單元是不是輔助字符,若是是輔助字符,那麼就要往前進兩位,不然往前進一位;同時將該碼點存入數組中,數組角標進1.

  上面咱們使用的前進的方法來操做的,天然也是能夠後退查詢的,下面咱們改寫上面的代碼:

    /**
     * 碼點數組
     * @param str
     */
    public int[] codePoint(String str){
        int[] arr = new int[str.codePointCount(0, str.length())];
        int cp = 0;
        int j = arr.length-1;
        for (int i = str.length(); i > 0; ) {
            i--;
            if(Character.isSurrogate(str.charAt(i)))
                i--;
                cp = str.codePointAt(i);
                arr[j] = cp;
                j--;
            }
            
        return arr;
    }

 

  這裏很容易就看出來這是經過後退來操做的(--),在這個操做中又使用了Character的一個靜態方法isSurrogate(char ch),該方法用來判斷碼點是否屬於輔助字符,從最後一個代碼單元開始循環,咱們知道代碼單元是從0開始的,因此在開始判斷前應該是長度先-1,不然會出現越界異常,判斷該碼點是否屬於輔助字符,若是屬於,那麼向後退1,獲取該輔助字符的碼點,將其放入數組,同時數組索引減1,由於我是讓數組索引和字符串中相應字符對應對弈從後開始填充數組。若是不是輔助字符,那麼此時獲取該代碼單元,而不用再向前退1.

  上面是獲取碼點的數組,下面看一下獲取代碼單元的數組,這比起上面就簡單了不少:

    /**
     * 代碼單元數組
     */
    public int[] codeUnit(String str){
        int[] arr = new int[str.length()];
        int cp = 0;
        int j = 0;
        for (int i = 0; i < arr.length; i++) {
            arr[j] = str.codePointAt(i);
            j++;
        }
        return arr;
    }

1.3.2 碼點和字符串的轉換

   若是給出一個字符串,你怎麼將字符串中的某個碼點轉換爲Unicode中的對應數呢?給定一個碼點數,怎麼轉換爲字符串呢?

    /**
     * 碼點-->碼點數
     */
    @Test
    public void numCode(){
        String str1 = "𝕆is the set 𝕆is";
        System.out.println("\\U+" + Integer.toHexString(str1.codePointAt(0)));
    }
    /**
     * 根據給定Unicode-->String
     */
    @Test
    public void numString(){
        String str1 = "\\U+1d546\\U+1d546";
        String[] arr = str1.split("\\\\U\\+");
        System.out.println(Arrays.toString(arr));
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            if(!arr[i] .equals("")){
                int code = Integer.parseInt(arr[i], 16);
//                sb.append((char)code);  強轉會形成輔助字符的丟失
                char[] ch = Character.toChars(code);
                sb.append(ch);
            }
            
        }
        System.out.println(sb.toString());
    }

 

1.4 總結

  String是一種基本的引用數據類型,也是咱們使用很頻繁的一種引用數據。底層是經過字符數組來實現的,String的長度取決於字符數組的長度,而字符數組的長度在於代碼單元的數量,代碼單元和碼點是大相徑庭的概念。咱們在操做String的時候,經過索引查找到的其實就是相應的代碼單元,並非咱們認爲的"字符",因此要注意,一旦String中含有輔助字符的時候,咱們要切切當心.。

2.不可變的String

  本文轉載https://www.zhihu.com/question/31345592 @胖胖

  不可變的String,初聽之下好像是說字符串不能夠改變,實際上這種說法,並沒錯,不過這裏我想說的是爲何String要不可變,String是怎麼實現不可變的,什麼是不可變,下面咱們一一探討一下:

  觀察String的源代碼,咱們發現,String是一個被final修飾的類,以下:

public final class String
  private final char value[];
  。。。。。。。。。。
public native String intern(); }

  那麼這是什麼意思呢?String爲何要用final修飾呢?目的何在呢?下面咱們來了解一下:

  咱們知道final修飾的類不能被繼承,而沒有子類的類,天然不存在重寫方法的風險。JDK中有一些類在設計之處,Java程序員爲了保護類不被破壞,就將其修飾爲final,拒絕由於繼承而形成的惡意對類的方法形成的破壞。這是對String不可變最基礎的解釋了。

2.1 什麼是不可變

  String不可變很簡單,以下圖,給一個已有字符串「abcd」第二次賦值爲"abcdel",不是在原內存地址上修改數據,而是從新指向一個新地址,新對象。這是String不可變最直觀的的一種理解和解釋了,咱們經過一段代碼就能夠看出來:

/**
     * 字符串是不可變的
     */
    @Test
    public void fun1(){
        String str1 = "abcd";
        String str2 = str1;
        System.out.println(str1 == str2);//true
        str1 = "abcdel";
        System.out.println(str1 == str2);//false
    }

  咱們發現,當str1改變後,str2並無隨着改變,這是由於什麼呢?經過一幅圖來看一下:

  經過這種直接賦值字符串內容生成的字符串對象,會首先去字符串常量池中尋找是否有這個字符串,若是有那麼直接返回該字符串地址,若是沒有,那麼先在字符串常量池中建立該字符串,而後返回該字符串地址;上面在建立str1時就是後一種狀況,而在將str1賦值給str2時,是將該字符串常量池中的地址返回給str2的,當str1改變時,因爲String是不可變的,因此是從新建立了一個字符串「abcdel」,並將該字符串地址返回給str1,因此此時str1指向了abcdel,可是原有字符串上面還有指針指向它,就是str2,因此也不會被垃圾回收,咱們在進行地址判斷時,也出現了false的狀況。可是若是咱們經過另外一種方式來建立字符串會是什麼狀況呢?

/**
     * 字符串不可變
     */
    @Test
    public void fun2(){
        String str1 = new String("abcd");
        String str2 = str1;
        System.out.println(str1 == str2);//true
        str1 = "abcdel";
        System.out.println(str1 == str2);//false
    }

  此時這種狀況出現的結果和上面是相同的,那麼內存中的結構也相同嗎?不是的,這種狀況雖然結果和上面相同,可是內存結構卻差異很大,下面再畫副圖看一下:

  這幅圖看起來和上面的很類似,惟一不一樣的在於咱們建立String使用了new,所以而帶來的變化是在堆中建立了一個str1對象,str1和str2都指向這個對象,咱們更改str1後,str1指向了字符串常量池中的「abcdel」,而str2的指向內有改變,因此咱們看到的結果就如同上面所示了。經過兩幅圖咱們知道了String改變時是不會在原有內容上改變的,而是重新建立了一個字符串對象,那麼這種不可變是怎麼實現的呢?

2.2 String爲何不可變

  在前面咱們貼出過String的一些源碼,咱們放到這裏再看一下:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

    private final char value[];//String本質是一個char數組,並且是經過final修飾的.

    private int hash; 
    public String() {
        this.value = "".value;
    }

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

    public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
    }
}

   首先String類是用final關鍵字修飾,這說明String不可繼承。繼續查看發現,String類的核心字段value是個char[],並且是用final修飾的。final修飾的字段建立後就不可改變。可能認爲咱們講到這裏就完了,其實否則。雖然value是不可變的,但也僅限於這個引用地址不會再發生變化。這並不可否定Array數組是可變的事實。Array的數據結構看下圖:

  也就是說Array變量只是stack上的一個引用,數組的本體結構在heap堆。String類裏的value用final修飾,只是說stack裏的這個叫value的引用地址不可變。沒有說堆裏array自己數據不可變。看下面的示例:

@org.junit.Test
    public void fun1(){
        final int[] value = {1,2,3};
        int[] another = {4,5,6};
//        value = another;這裏會提示final不可改變
    }

  value用final修飾,編譯器不容許咱們將value指向堆中另外一個地址。但若是我直接對數組元素進行動手,那麼狀況就又不一樣了;

    @org.junit.Test
    public void fun1(){
        final int[] value = {1,2,3};
        System.out.println(Arrays.toString(value));//[1, 2, 3]
        value[2] = 100;
        System.out.println(Arrays.toString(value));//[1, 2, 100]
    }

  或者咱們使用更粗暴的反射修改也是能夠的:

    @org.junit.Test
    public void fun1(){
        final int[] value = {1,2,3};
        System.out.println(Arrays.toString(value));//[1, 2, 3]//        value[2] = 100;
        Array.set(value, 2, 101);
        System.out.println(Arrays.toString(value));//[1, 2, 101]
    }

   因此說String是不可變的,關鍵是由於SUN的工程師在設計該基本工具類時,在後面全部String的方法裏很當心的沒有去動Array裏的元素,沒有暴露內部成員字段(value是private的)。private final char value[]這一句中,真正構成不可變的除了final外,還有更重要的一點就是private,private的私有訪問權限的做用比final還要重要。並且設計師還很當心地把整個String設計成final禁止繼承,避免被其餘人繼承後破壞。因此String不可變的關鍵在於底層的實現,而並不是單單是一個final。考研的是工程師構造數據類型,封裝數據的能力。

2.3 不可變有什麼用

   上面咱們瞭解了什麼是不可變,也瞭解了不可變是如何實現的,那麼不可變在開發中有什麼做用呢?也就是優點何在呢?最簡單的優勢就是爲了安全,看下面這個場景:

package cn.charsequence.string.can_not_change;

import java.lang.reflect.Array;
import java.util.Arrays;

public class Test {
    //不可變的String
    public static String appendStr(String s){
        s += "bbb";
        return s;
    }
    //可變的StringBuilder
    public static StringBuilder appendSb(StringBuilder sb){
        return sb.append("bbb");
    }
    
    public static void main(String[] args) {
        String s = new String("aaa");
        String ns = Test.appendStr(s);
        System.out.println("String aaa >>> " + s.toString());//String aaa >>> aaa
        StringBuilder sb = new StringBuilder("aaa");
        StringBuilder nsb = Test.appendSb(sb);
        System.out.println("StringBuiler >>> " + sb.toString());//StringBuiler >>> aaabbb
    }
}

  若是開發中不當心像上面的例子裏,直接在傳進來的參數上加「bbb」,由於Java對象參數傳的是引用,因此可變的StringBuiler參數就被改變了。能夠看到變量sb在Test.appendSb(sb)操做以後,就變成了"aaabbb"。有的時候這可能不是咱們的本意。因此String不可變的安全性就體現出來了。再看下面這個HashSet用StringBuilder作元素的場景,問題就更嚴重了,並且更隱蔽。

public static void main(String[] args) {
        HashSet<StringBuilder> hs = new HashSet<>();
        StringBuilder sb1 = new StringBuilder("aaa");
        StringBuilder sb2 = new StringBuilder("aaabbb");
        hs.add(sb1);
        hs.add(sb2);
        StringBuilder sb3 = sb1;
        sb3.append("bbb");
        System.out.println(hs);//[aaabbb, aaabbb]
    }

  StringBuilder型變量sb1和sb2分別指向了堆內的字面量「aaa」和"aaabbb"。把他們都插入一個HashSet。到這一步沒問題。但若是後面我把變量sb3也指向sb1的地址,再改變sb3的值,由於StringBuilder沒有不可變性的保護,sb3直接在原先「aaa」的地址上改。致使sb1的值也變了。這時候,HashSet上就出現了兩個相等的鍵值「aaabbb」。破壞了HashSet鍵值的惟一性。因此千萬不要用可變類型作HashMap和HashSet鍵值。

  上面咱們說了String不可變的安全性,當有多個引用指向同一個內存地址時,不可變保證了安全性。除了安全性外,String的不可變也體如今了高性能上面。咱們知道Java內存結構模型中提供了字符串常量池,咱們經過直接賦值的方式建立字符串對象時,會先去字符串常量池中查找該字符串是否存在,若是存在直接返回該字符串地址,若是不存在則先建立後返回地址,以下面:

String one = "someString";
        String two = "someString";

  上面one和two指向同一個字符串對象,這樣能夠在大量使用字符串的狀況下,能夠節省內存空間,提升效率。但之因此能實現這個特性,String的不可變性是最基本的一個必要條件。要是內存中字符串內容可以改來改去,這麼作就徹底沒有意義了。

  總結:String的不可變性提升了複用性,節省空間,保證了開發安全,提升了程序效率。

2.4 不可變提升效率的補充解讀

  乍一看可能會以爲小編我是否是腦子進水了,怎麼上邊剛驗證了String的不可變安全、高效,在這裏又疑惑是否提升效率。其實我在這裏想要說的是「有些時候看起來好像修改一個代碼單元要比建立一個新字符串更加簡潔。答案是也對,也不對。的確,經過拼接「Hel」和「p!」來建立一個新字符串的效率確實不高。可是,不可變字符串卻有一個優勢:使字符串共享。」

  設計之初,Java的設計者認爲共享帶來的高效率遠遠賽過於提取、拼接字符串所帶來的低效率。查看程序發現:不多須要修改字符串,而是每每須要對字符串進行比較(固然,也有例外狀況,未來自文件或鍵盤的單個字符或較短的字符串聚集成字符串。爲此Java提供了緩衝字符串用來操做)。因此應該站在不同的角度來看不可變的高效率,在合適的地方,採用合適的操做。

3.無心識的遞歸

  無心識的遞歸是在讀《Java編程思想》時遇到的一個知識點,以爲是有必要了解的,下面咱們來認識一下:

  Java中的每一個類從根本上都是繼承自Object,標準容器類天然也不例外.所以容器類都有toString()方法,而且覆寫了該方法,使得它生成的String結果可以表達容器自身,以及容器所包含的對象.例如ArrayList.toString(),它會遍歷ArrayList中包含的全部對象,調用每一個元素上的toString()方法.但若是你但願toString()打印出對象的內存地址,也許你會考慮使用this關鍵字:

package cn.charsequence.string.can_not_change;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class InfiniteRecursion {
    
    /**
     * 重寫toString方法
     */
    @Override
    public String toString() {
        // TODO Auto-generated method stub
        return " InfiniteRecursion address: " + this + "\n";
    }
    public static void main(String[] args) {
        List<InfiniteRecursion> list = new ArrayList<InfiniteRecursion>();
        for (int i = 0; i < 10; i++) {
            list.add(new InfiniteRecursion());
        }
        System.out.println(list);
    }
}

  當你建立了InfiniteRecursion對象,並將其打印出來的時候,你會獲得一串很是長的異常.若是你將該InfiniteRecursion對象存入一個ArrayList中,而後打印該ArrayList,你也會獲得一樣的異常.其實,當以下代碼運行時:

return " InfiniteRecursion address: " + this + "\n";

  這裏發生了自動類型轉換.由InfiniteRecursion類型轉換成String類型.由於編譯器看到一個String對象後面跟着一個」+」,而再後面的對象不是String,因而編譯器試着將this轉換成一個String.它怎麼轉換呢,正是經過this上的toString()方法,因而就發生了遞歸調用.

  若是你真的想要打印出對象的內存地址,應該調用Object.toString()方法,這纔是負責此任務的方法,因此你不應使用this,而是應該調用super.toString()方法.改變上面toString方法代碼:

/**
     * 重寫toString方法
     */
    @Override
    public String toString() {
        // TODO Auto-generated method stub
//        return " InfiniteRecursion address: " + this + "\n";
        return " InfiniteRecursion address: " + super.toString() + "\n";
    }

4. 重載「+」與StringBuilder 

  String對象是不可變的,你能夠給一個String對象加任意多的別名.改變String時會建立一個新的String,原String並不會發生變化,因此指向它的任何引用都不可能改變原有的值,所以,也就不會對其餘的引用有什麼影響(例如兩個別名指向同一個引用,一個別名有了改變這個引用的操做,那麼不可變性就保證了另外一個別名引用的安全).

  不可變性會帶來必定的效率問題.爲String對象重載的」+」操做符就是一個例子.重載的意思是,一個操做符在應用於特定的類時,被賦予了特殊的意義(用於String的」+」與」+=」是Java中僅有的兩個重載過的操做符,而Java並不容許程序員重載任何操做符).+在數學中用來兩個數的相加,在字符串中用來鏈接String:

package cn.string.two;

public class Concatenation {
    public static void main(String[] args) {
        String mango = "mango";
        String s = "abc" + mango + "def" + 47;
        System.out.println(s);
    }
}

  能夠想象一下,這段代碼多是這樣工做的:String可能有一個append()方法,它會生成一個新的String對象,以包含」abc」與mango鏈接後的字符串.而後,該對象再與」def」相連,生成另外一個新的String對象,依次類推.這種工做方式固然也行得通,可是爲了生成最終的String,此方式會產生一大堆須要垃圾回收的中間對象.我猜測,Java設計師一開始就是這麼作的(這也是軟件設計中的一個教訓:除非你用代碼將系統實現,並讓它動起來,不然你沒法真正瞭解它會有什麼問題),而後他們發現其性能至關糟糕.想看看以上代碼究竟是如何工做的嗎,能夠用JDK自帶的工具javap來反編譯以上代碼.命令以下:

這裏的-c標誌表示將生成JVM字節碼.我剔除掉了不感興趣的部分,而後做了一點點修改,因而有了如下的字節碼:

  若是有彙編語言的經驗,以上代碼必定看着眼熟,其中的dup與invokevirtural語句至關於Java虛擬機上的彙編語句.即便你徹底不瞭解彙編語言也無需擔憂,須要注意的重點是:編譯器自動引入了java.lang.StringBuilder類.雖然咱們在源代碼中並無使用StringBuilder類,可是編譯器卻自做主張地使用了它,由於它更高效.

  在這個例子中,編譯器建立了一個StringBuilder對象,用以構造最終的String,併爲每一個字符串調用一次StringBuilder的append()方法,總計四次.最後調用toString()生成結果,並存在s(使用的命令爲astore_2)

  如今,也許你會以爲能夠隨意使用String對象,反正編譯器會爲你自動地優化性能.但是在這以前,讓咱們更深刻地看看編譯器能爲咱們優化到什麼程度.下面的程序採用兩種方式生成一個String:方法一使用了多個String對象,方法二在代碼中使用了StringBuilder.

package cn.stringPractise.Commonoperation;
public class WhitherStringBuilder {
    public static void main(String[] args) {
        String[] str = {"長安古道馬遲遲","高柳亂蟬嘶","夕陽島外","秋風原上","目斷四天垂",
                "歸雲一去無蹤影","何處是前期","狎興生疏","酒徒蕭索","不似去年時。"};
        System.out.println(implicit(str));
        System.out.println(explicit(str));
    }
    public static String implicit(String[] fields){
        String result = "";
        for (int i = 0; i < fields.length; i++) {
            result += fields[i];
        }
        return result;
    }
    public static String explicit(String[] fields){
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < fields.length; i++) {
            sb.append(fields[i]);
        }
        return sb.toString();
    }
}
public static void main(String[] args) {
        String[] str = {"長安古道馬遲遲","高柳亂蟬嘶","夕陽島外","秋風原上","目斷四天垂",
                "歸雲一去無蹤影","何處是前期","狎興生疏","酒徒蕭索","不似去年時。"};
        String[] str1 = new String[20000];
        for (int i = 0; i < 20000; i++) {
            str1[i] = Integer.toString(i);
        }
        long start = System.currentTimeMillis();
//        System.out.println(implicit(str1));
        implicit(str1);
        long end = System.currentTimeMillis();
        System.out.println(end-start);
        start = System.currentTimeMillis();
        explicit(str1);
//        System.out.println(explicit(str1));
        end = System.currentTimeMillis();
        System.out.println(end-start);
    }

如今運行javap -c WitherStringBuilder,能夠看到兩個方法對應的(簡化過的)字節碼.首先是implicit()方法:

 

  注意從第8行到第35行構成了一個循環體.第8行:對堆棧中的操做數進行」大於或等於的整數比較運算」,循環結束時跳到第38行.第35行:返回循環體的起始點(第5行).要注意的重點是:StringBuilder是在循環體內構成的,這意味着每通過一次循環,就會建立一個新的StringBuilder對象.

  下面是explicit()方法對應的字節碼:

  能夠看到,不只循環部分的代碼更簡短、更簡單,並且它只生成了一個StringBuilder對象。顯式的建立StringBuilder還容許你預先爲其指定大小.若是你已經知道最終的字符串大概有多長,那預先指定StringBuilder的大小能夠避免多長從新分配緩衝.

  所以,當你爲一個類編寫toString()方法時,若是字符串操做比較簡單,那就能夠信賴編譯器,它會爲你合理地構造最終的字符串結果.可是,若是你要在toString()方法中使用循環,那麼最好本身建立一個StringBuilder對象,用它來構造最終的結果.參考一下示例:

package cn.stringPractise.Commonoperation;
import java.util.Random;

public class UsingStringBuilder {
    public static Random rand = new Random(47);
    @Override
    public String toString() {
        StringBuilder builder = new StringBuilder("[");
        for (int i = 0; i < 25; i++) {
            builder.append(rand.nextInt(100));
            builder.append(",");
        }
        builder.delete(builder.length()/2, builder.length());
        builder.append("]");
        return builder.toString();
    }
    
    public static void main(String[] args) {
        UsingStringBuilder usingStringBuilder = new UsingStringBuilder();
        System.out.println(usingStringBuilder);
    }
}

    public void println(Object x) {
        String s = String.valueOf(x);
        synchronized (this) {
            print(s);
            newLine();
        }
    }
public static String valueOf(Object obj) {
        return (obj == null) ? "null" : obj.toString();
    }

  最終的結果是用append()語句一點點拼接起來的.若是你想走捷徑,例如append(a+」:」+c),那編譯器就會掉入陷阱,從而爲你另外建立一個StringBuilder對象處理括號內的字符串操做.若是拿不許該用哪一種方式,隨時能夠用javap來分析你的程序.

  StringBuilder提供了豐富而全面的方法,包括insert()、repleace()、substring()甚至reverse(),可是最經常使用的仍是append()和toString().還有delete()方法,上面的例子中咱們用它刪除最後一個逗號與空格,以便添加右括號.

  StringBuilder是Java SE5引入的,在這以前Java用的是StringBuffer.後者是線程安全的,所以開銷也會大些,因此在Java SE5/6中,字符串操做應該還會更快一點.關於緩衝字符串咱們在介紹完String後會統一再詳細介紹。

4.1 重載「+」流程簡略

  兩個字段在進行「+」操做時,那麼到底是怎麼操做呢?咱們本身書寫一段代碼,debug能夠看到,在操做時會首先調用String,valueOf()方法,若是該字段是null,那麼返回null,不然調用toString方法,將其轉變爲String,進行StringBuilder。append()操做。

相關文章
相關標籤/搜索