天天看點

深入了解Java String類

一、String類

想要了解一個類,最好的辦法就是看這個類的實作源代碼,來看一下String類的源碼:

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

    /** The offset is the first index of the storage that is used. */
    private final int offset;

    /** The count is the number of characters in the String. */
    private final int count;

    /** 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 = -L;
}
           

從上面可以看出幾點:

1)String類是final類,也即意味着String類不能被繼承,并且它的成員方法都預設為final方法。在Java中,被final修飾的類是不允許被繼承的,并且該類中的成員方法都預設為final方法。

2)上面列舉出了String類中所有的成員屬性,從上面可以看出String類其實是通過char數組來儲存字元串的。

下面再繼續看String類的一些方法實作:

public String substring(int beginIndex, int endIndex) {
    if (beginIndex < ) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
    if (endIndex > count) {
        throw new StringIndexOutOfBoundsException(endIndex);
    }
    if (beginIndex > endIndex) {
        throw new StringIndexOutOfBoundsException(endIndex - beginIndex);
    }
    return ((beginIndex == ) && (endIndex == count)) ? this :
        new String(offset + beginIndex, endIndex - beginIndex, value);
}

public String concat(String str) {
    int otherLen = str.length();
    if (otherLen == ) {
        return this;
    }
    char buf[] = new char[count + otherLen];
    getChars(, count, buf, );
    str.getChars(, otherLen, buf, count);
    return new String(, count + otherLen, buf);
}

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
        int len = count;
        int i = -;
        char[] val = value; /* avoid getfield opcode */
        int off = offset;   /* avoid getfield opcode */

        while (++i < len) {
        if (val[off + i] == oldChar) {
            break;
        }
        }
        if (i < len) {
        char buf[] = new char[len];
        for (int j =  ; j < i ; j++) {
            buf[j] = val[off+j];
        }
        while (i < len) {
            char c = val[off + i];
            buf[i] = (c == oldChar) ? newChar : c;
            i++;
        }
        return new String(, len, buf);
        }
    }
    return this;
}
           

從上面的三個方法可以看出,無論是sub操、concat還是replace操作都不是在原有的字元串上進行的,而是重新生成了一個新的字元串對象。也就是說進行這些操作後,最原始的字元串并沒有被改變。

在這裡要永遠記住一點:“String對象一旦被建立就是固定不變的了,對String對象的任何改變都不影響到原對象,相關的任何change操作都會生成新的對象”。

二、字元串常量池

我們知道字元串的配置設定和其他對象配置設定一樣,是需要消耗高昂的時間和空間的,而且字元串我們使用的非常多。JVM為了提高性能和減少記憶體的開銷,在執行個體化字元串的時候進行了一些優化:使用字元串常量池。每當我們建立字元串常量時,JVM會首先檢查字元串常量池,如果該字元串已經存在常量池中,那麼就直接傳回常量池中的執行個體引用。如果字元串不存在常量池中,就會執行個體化該字元串并且将其放到常量池中。由于String字元串的不可變性我們可以十分肯定常量池中一定不存在兩個相同的字元串(這點對了解上面至關重要)。

Java中的常量池,實際上分為兩種形态:靜态常量池和運作時常量池。

所謂靜态常量池,即*.class檔案中的常量池,class檔案中的常量池不僅僅包含字元串(數字)字面量,還包含類、方法的資訊,占用class檔案絕大部分空間。

而運作時常量池,則是jvm虛拟機在完成類裝載操作後,将class檔案中的常量池載入到記憶體中,并儲存在方法區中,我們常說的常量池,就是指方法區中的運作時常量池。

來看下面的程式:

String a = “xhy”;

String b = “xhy”;

a、b和字面上的xhy都是指向JVM字元串常量池中的”xhy”對象,他們指向同一個對象。

String c = new String(“xhy”);

new關鍵字一定會産生一個對象xhy(注意這個xhy和上面的xhy不同),同時這個對象是存儲在堆中。是以上面應該産生了兩個對象:儲存在棧中的c和儲存堆中xhy。但是在Java中根本就不存在兩個完全一模一樣的字元串對象。故堆中的xhy應該是引用字元串常量池中xhy。是以c、xhy、池xhy的關系應該是:c—>xhy—>池xhy。整個關系如下:

深入了解Java String類

通過上面的圖我們可以非常清晰的認識他們之間的關系。是以我們修改記憶體中的值,他變化的是所有。

總結:雖然a、b、c、xhy是不同的對象,但是從String的内部結構我們是可以了解上面的。String c = new String(“xhy”);雖然c的内容是建立在堆中,但是他的内部value還是指向JVM常量池的xhy的value,它構造xhy時所用的參數依然是xhy字元串常量。

下面再來看幾個例子:

例子1:

/**
 * 采用字面值的方式指派
 */
public void test1(){
    String str1="aaa";
    String str2="aaa";
    System.out.println("===========test1============");
    System.out.println(str1==str2);//true 可以看出str1跟str2是指向同一個對象 
}
           

執行上述代碼,結果為:true。

分析:當執行String str1=”aaa”時,JVM首先會去字元串池中查找是否存在”aaa”這個對象,如果不存在,則在字元串池中建立”aaa”這個對象,然後将池中”aaa”這個對象的引用位址傳回給字元串常量str1,這樣str1會指向池中”aaa”這個字元串對象;如果存在,則不建立任何對象,直接将池中”aaa”這個對象的位址傳回,賦給字元串常量。當建立字元串對象str2時,字元串池中已經存在”aaa”這個對象,直接把對象”aaa”的引用位址傳回給str2,這樣str2指向了池中”aaa”這個對象,也就是說str1和str2指向了同一個對象,是以語句System.out.println(str1 == str2)輸出:true。

例子2:

/**
 * 采用new關鍵字建立一個字元串對象
 */
public void test2(){
    String str3=new String("aaa");
    String str4=new String("aaa");
    System.out.println("===========test2============");
    System.out.println(str3==str4);//false 可以看出用new的方式是生成不同的對象 
}
           

執行上述代碼,結果為:false。

分析: 采用new關鍵字建立一個字元串對象時,JVM首先在字元串池中查找有沒有”aaa”這個字元串對象,如果有,則不在池中再去建立”aaa”這個對象了,直接在堆中建立一個”aaa”字元串對象,然後将堆中的這個”aaa”對象的位址傳回賦給引用str3,這樣,str3就指向了堆中建立的這個”aaa”字元串對象;如果沒有,則首先在字元串池中建立一個”aaa”字元串對象,然後再在堆中建立一個”aaa”字元串對象,然後将堆中這個”aaa”字元串對象的位址傳回賦給str3引用,這樣,str3指向了堆中建立的這個”aaa”字元串對象。當執行String str4=new String(“aaa”)時, 因為采用new關鍵字建立對象時,每次new出來的都是一個新的對象,也即是說引用str3和str4指向的是兩個不同的對象,是以語句System.out.println(str3 == str4)輸出:false。

例子3:

/**
 * 編譯期确定
 */
public void test3(){
    String s0="helloworld";
    String s1="helloworld";
    String s2="hello"+"world";
    System.out.println("===========test3============");
    System.out.println(s0==s1); //true 可以看出s0跟s1是指向同一個對象 
    System.out.println(s0==s2); //true 可以看出s0跟s2是指向同一個對象 
}
           

執行上述代碼,結果為:true、true。

分析:因為例子中的s0和s1中的”helloworld”都是字元串常量,它們在編譯期就被确定了,是以s0==s1為true;而”hello”和”world”也都是字元串常量,當一個字元串由多個字元串常量連接配接而成時,它自己肯定也是字元串常量,是以s2也同樣在編譯期就被解析為一個字元串常量,是以s2也是常量池中”helloworld”的一個引用。是以我們得出s0==s1==s2。

例子4:

/**
 * 編譯期無法确定
 */
public void test4(){
    String s0="helloworld"; 
    String s1=new String("helloworld"); 
    String s2="hello" + new String("world"); 
    System.out.println("===========test4============");
    System.out.println( s0==s1 ); //false  
    System.out.println( s0==s2 ); //false 
    System.out.println( s1==s2 ); //false
}
           

執行上述代碼,結果為:false、false、false。

分析:用new String() 建立的字元串不是常量,不能在編譯期就确定,是以new String() 建立的字元串不放入常量池中,它們有自己的位址空間。

s0還是常量池中”helloworld”的引用,s1因為無法在編譯期确定,是以是運作時建立的新對象”helloworld”的引用,s2因為有後半部分new String(”world”)是以也無法在編譯期确定,是以也是一個新建立對象”helloworld”的引用。

例子5:

/**
 * 繼續-編譯期無法确定
 */
public void test5(){
    String str1="abc";   
    String str2="def";   
    String str3=str1+str2;
    System.out.println("===========test5============");
    System.out.println(str3=="abcdef"); //false
}
           

執行上述代碼,結果為:false。

分析:因為str3指向堆中的”abcdef”對象,而”abcdef”是字元串池中的對象,是以結果為false。JVM對String str=”abc”對象放在常量池中是在編譯時做的,而String str3=str1+str2是在運作時刻才能知道的。new對象也是在運作時才做的。而這段代碼總共建立了5個對象,字元串池中兩個、堆中三個。+運算符會在堆中建立來兩個String對象,這兩個對象的值分别是”abc”和”def”,也就是說從字元串池中複制這兩個值,然後在堆中建立兩個對象,然後再建立對象str3,然後将”abcdef”的堆位址賦給str3。

步驟:

1)棧中開辟一塊中間存放引用str1,str1指向池中String常量”abc”。

2)棧中開辟一塊中間存放引用str2,str2指向池中String常量”def”。

3)棧中開辟一塊中間存放引用str3。

4)str1 + str2通過StringBuilder的最後一步toString()方法還原一個新的String對象”abcdef”,是以堆中開辟一塊空間存放此對象。

5)引用str3指向堆中(str1 + str2)所還原的新String對象。

6)str3指向的對象在堆中,而常量”abcdef”在池中,輸出為false。

例子6:

/**
 * 編譯期優化
 */
public void test6(){
    String s0 = "a1"; 
    String s1 = "a" + ; 
    System.out.println("===========test6============");
    System.out.println((s0 == s1)); //result = true  
    String s2 = "atrue"; 
    String s3= "a" + "true"; 
    System.out.println((s2 == s3)); //result = true  
    String s4 = "a3.4"; 
    String s5 = "a" + ; 
    System.out.println((s4 == s5)); //result = true
}
           

執行上述代碼,結果為:true、true、true。

分析:在程式編譯期,JVM就将常量字元串的”+”連接配接優化為連接配接後的值,拿”a” + 1來說,經編譯器優化後在class中就已經是a1。在編譯期其字元串常量的值就确定下來,故上面程式最終的結果都為true。

例子7:

/**
 * 編譯期無法确定
 */
public void test7(){
    String s0 = "ab"; 
    String s1 = "b"; 
    String s2 = "a" + s1; 
    System.out.println("===========test7============");
    System.out.println((s0 == s2)); //result = false
}
           

執行上述代碼,結果為:false。

分析:JVM對于字元串引用,由于在字元串的”+”連接配接中,有字元串引用存在,而引用的值在程式編譯期是無法确定的,即”a” + s1無法被編譯器優化,隻有在程式運作期來動态配置設定并将連接配接後的新位址賦給s2。是以上面程式的結果也就為false。

例子8:

/**
 * 比較字元串常量的“+”和字元串引用的“+”的差別
 */
public void test8(){
    String test="javalanguagespecification";
    String str="java";
    String str1="language";
    String str2="specification";
    System.out.println("===========test8============");
    System.out.println(test == "java" + "language" + "specification");
    System.out.println(test == str + str1 + str2);
}
           

執行上述代碼,結果為:true、false。

分析:為什麼出現上面的結果呢?這是因為,字元串字面量拼接操作是在Java編譯器編譯期間就執行了,也就是說編譯器編譯時,直接把”java”、”language”和”specification”這三個字面量進行”+”操作得到一個”javalanguagespecification” 常量,并且直接将這個常量放入字元串池中,這樣做實際上是一種優化,将3個字面量合成一個,避免了建立多餘的字元串對象。而字元串引用的”+”運算是在Java運作期間執行的,即str + str2 + str3在程式執行期間才會進行計算,它會在堆記憶體中重新建立一個拼接後的字元串對象。總結來說就是:字面量”+”拼接是在編譯期間進行的,拼接後的字元串存放在字元串池中;而字元串引用的”+”拼接運算實在運作時進行的,新建立的字元串存放在堆中。

對于直接相加字元串,效率很高,因為在編譯器便确定了它的值,也就是說形如”I”+”love”+”java”; 的字元串相加,在編譯期間便被優化成了”Ilovejava”。對于間接相加(即包含字元串引用),形如s1+s2+s3; 效率要比直接相加低,因為在編譯器不會對引用變量進行優化。

例子9:

/**
 * 編譯期确定
 */
public void test9(){
    String s0 = "ab"; 
    final String s1 = "b"; 
    String s2 = "a" + s1;  
    System.out.println("===========test9============");
    System.out.println((s0 == s2)); //result = true
}
           

執行上述代碼,結果為:true。

分析:和例子7中唯一不同的是s1字元串加了final修飾,對于final修飾的變量,它在編譯時被解析為常量值的一個本地拷貝存儲到自己的常量池中或嵌入到它的位元組碼流中。是以此時的”a” + s1和”a” + “b”效果是一樣的。故上面程式的結果為true。

例子10:

/**
 * 編譯期無法确定
 */
public void test10(){
    String s0 = "ab"; 
    final String s1 = getS1(); 
    String s2 = "a" + s1; 
    System.out.println("===========test10============");
    System.out.println((s0 == s2)); //result = false 

}

private static String getS1() {  
    return "b";   
}
           

執行上述代碼,結果為:false。

分析:這裡面雖然将s1用final修飾了,但是由于其指派是通過方法調用傳回的,那麼它的值隻能在運作期間确定,是以s0和s2指向的不是同一個對象,故上面程式的結果為false。

三、總結

1.String類初始化後是不可變的(immutable)

String使用private final char value[]來實作字元串的存儲,也就是說String對象建立之後,就不能再修改此對象中存儲的字元串内容,就是因為如此,才說String類型是不可變的(immutable)。程式員不能對已有的不可變對象進行修改。我們自己也可以建立不可變對象,隻要在接口中不提供修改資料的方法就可以。

然而,String類對象确實有編輯字元串的功能,比如replace()。這些編輯功能是通過建立一個新的對象來實作的,而不是對原有對象進行修改。比如:

s = s.replace(“World”, “Universe”);

上面對s.replace()的調用将建立一個新的字元串”Hello Universe!”,并傳回該對象的引用。通過指派,引用s将指向該新的字元串。如果沒有其他引用指向原有字元串”Hello World!”,原字元串對象将被垃圾回收。

深入了解Java String類

2.引用變量與對象

A aa;

這個語句聲明一個類A的引用變量aa[我們常常稱之為句柄],而對象一般通過new建立。是以aa僅僅是一個引用變量,它不是對象。

3.建立字元串的方式

建立字元串的方式歸納起來有兩類:

(1)使用”“引号建立字元串;

(2)使用new關鍵字建立字元串。

結合上面例子,總結如下:

(1)單獨使用”“引号建立的字元串都是常量,編譯期就已經确定存儲到String Pool中;

(2)使用new String(“”)建立的對象會存儲到heap中,是運作期新建立的;

new建立字元串時首先檢視池中是否有相同值的字元串,如果有,則拷貝一份到堆中,然後傳回堆中的位址;如果池中沒有,則在堆中建立一份,然後傳回堆中的位址(注意,此時不需要從堆中複制到池中,否則,将使得堆中的字元串永遠是池中的子集,導緻浪費池的空間)!

(3)使用隻包含常量的字元串連接配接符如”aa” + “aa”建立的也是常量,編譯期就能确定,已經确定存儲到String Pool中;

(4)使用包含變量的字元串連接配接符如”aa” + s1建立的對象是運作期才建立的,存儲在heap中;

4.使用String不一定建立對象

在執行到雙引号包含字元串的語句時,如String a = “123”,JVM會先到常量池裡查找,如果有的話傳回常量池裡的這個執行個體的引用,否則的話建立一個新執行個體并置入常量池裡。是以,當我們在使用諸如String str = “abc”;的格式定義對象時,總是想當然地認為,建立了String類的對象str。擔心陷阱!對象可能并沒有被建立!而可能隻是指向一個先前已經建立的對象。隻有通過new()方法才能保證每次都建立一個新的對象。

5.使用new String,一定建立對象

在執行String a = new String(“123”)的時候,首先走常量池的路線取到一個執行個體的引用,然後在堆上建立一個新的String執行個體,走以下構造函數給value屬性指派,然後把執行個體引用指派給a:

public String(String original) {
    int size = original.count;
    char[] originalValue = original.value;
    char[] v;
      if (originalValue.length > size) {
         // The array representing the String is bigger than the new
         // String itself.  Perhaps this constructor is being called
         // in order to trim the baggage, so make a copy of the array.
            int off = original.offset;
            v = Arrays.copyOfRange(originalValue, off, off+size);
     } else {
         // The array representing the String is the same
         // size as the String, so no point in making a copy.
        v = originalValue;
     }
    this.offset = ;
    this.count = size;
    this.value = v;
    }
           

從中我們可以看到,雖然是新建立了一個String的執行個體,但是value是等于常量池中的執行個體的value,即是說沒有new一個新的字元數組來存放”123”。

6.關于String.intern()

intern方法使用:一個初始為空的字元串池,它由類String獨自維護。當調用 intern方法時,如果池已經包含一個等于此String對象的字元串(用equals(oject)方法确定),則傳回池中的字元串。否則,将此String對象添加到池中,并傳回此String對象的引用。

它遵循以下規則:對于任意兩個字元串 s 和 t,當且僅當 s.equals(t) 為 true 時,s.intern() == t.intern() 才為 true。

String.intern();

再補充介紹一點:存在于.class檔案中的常量池,在運作期間被jvm裝載,并且可以擴充。String的intern()方法就是擴充常量池的一個方法;當一個String執行個體str調用intern()方法時,java查找常量池中是否有相同unicode的字元串常量,如果有,則傳回其引用,如果沒有,則在常量池中增加一個unicode等于str的字元串并傳回它的引用。

/**
 * 關于String.intern()
 */
public void test11(){
    String s0 = "kvill"; 
    String s1 = new String("kvill"); 
    String s2 = new String("kvill"); 
    System.out.println("===========test11============");
    System.out.println( s0 == s1 ); //false
    System.out.println( "**********" ); 
    s1.intern(); //雖然執行了s1.intern(),但它的傳回值沒有賦給s1
    s2 = s2.intern(); //把常量池中"kvill"的引用賦給s2 
    System.out.println( s0 == s1); //flase
    System.out.println( s0 == s1.intern() ); //true//說明s1.intern()傳回的是常量池中"kvill"的引用
    System.out.println( s0 == s2 ); //true
}
           

運作結果:false、false、true、true。

7.關于equals和==

(1)對于==,如果作用于基本資料類型的變量(byte,short,char,int,long,float,double,boolean ),則直接比較其存儲的”值”是否相等;如果作用于引用類型的變量(String),則比較的是所指向的對象的位址(即是否指向同一個對象)。

(2)equals方法是基類Object中的方法,是以對于所有的繼承于Object的類都會有該方法。在Object類中,equals方法是用來比較兩個對象的引用是否相等,即是否指向同一個對象。

(3)對于equals方法,注意:equals方法不能作用于基本資料類型的變量。如果沒有對equals方法進行重寫,則比較的是引用類型的變量所指向的對象的位址;而String類對equals方法進行了重寫,用來比較指向的字元串對象所存儲的字元串是否相等。其他的一些類諸如Double,Date,Integer等,都對equals方法進行了重寫用來比較指向的對象所存儲的内容是否相等。

/**
 * 關于equals和==
 */
public void test12(){
    String s1="hello";
    String s2="hello";
    String s3=new String("hello");
    System.out.println("===========test12============");
    System.out.println( s1 == s2); //true,表示s1和s2指向同一對象,它們都指向常量池中的"hello"對象
    //flase,表示s1和s3的位址不同,即它們分别指向的是不同的對象,s1指向常量池中的位址,s3指向堆中的位址
    System.out.println( s1 == s3); 
    System.out.println( s1.equals(s3)); //true,表示s1和s3所指向對象的内容相同
}
           

8.String相關的+:

String中的 + 常用于字元串的連接配接。看下面一個簡單的例

public void test13(){

String a = “aa”;

String b = “bb”;

String c = “xx” + “yy ” + a + “zz” + “mm” + b;

System.out.println(“===========test13============”);

System.out.println(c);

}

編譯運作後,主要位元組碼部分如下:

public static main([Ljava/lang/String;)V
   L0
    LINENUMBER  L0
    LDC "aa"
    ASTORE 
   L1
    LINENUMBER  L1
    LDC "bb"
    ASTORE 
   L2
    LINENUMBER  L2
    NEW java/lang/StringBuilder
    DUP
    LDC "xxyy "
    INVOKESPECIAL java/lang/StringBuilder.<init> (Ljava/lang/String;)V
    ALOAD 
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "zz"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    LDC "mm"
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    ALOAD 
    INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
    ASTORE 
   L3
    LINENUMBER  L3
    GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
    ALOAD 
    INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
   L4
    LINENUMBER  L4
    RETURN
   L5
    LOCALVARIABLE args [Ljava/lang/String; L0 L5 
    LOCALVARIABLE a Ljava/lang/String; L1 L5 
    LOCALVARIABLE b Ljava/lang/String; L2 L5 
    LOCALVARIABLE c Ljava/lang/String; L3 L5 
    MAXSTACK = 
    MAXLOCALS = 
}
           

顯然,通過位元組碼我們可以得出如下幾點結論:

(1).String中使用 + 字元串連接配接符進行字元串連接配接時,連接配接操作最開始時如果都是字元串常量,編譯後将盡可能多的直接将字元串常量連接配接起來,形成新的字元串常量參與後續連接配接(通過反編譯工具jd-gui也可以友善的直接看出);

(2).接下來的字元串連接配接是從左向右依次進行,對于不同的字元串,首先以最左邊的字元串為參數建立StringBuilder對象,然後依次對右邊進行append操作,最後将StringBuilder對象通過toString()方法轉換成String對象(注意:中間的多個字元串常量不會自動拼接)。

也就是說String c = “xx” + “yy ” + a + “zz” + “mm” + b; 實質上的實作過程是: String c = new StringBuilder(“xxyy “).append(a).append(“zz”).append(“mm”).append(b).toString();

由此得出結論:當使用+進行多個字元串連接配接時,實際上是産生了一個StringBuilder對象和一個String對象。

9.String的不可變性導緻字元串變量使用+号的代價:

String s = “a” + “b” + “c”;

String s1 = “a”;

String s2 = “b”;

String s3 = “c”;

String s4 = s1 + s2 + s3;

分析:變量s的建立等價于 String s = “abc”; 由上面例子可知編譯器進行了優化,這裡隻建立了一個對象。由上面的例子也可以知道s4不能在編譯期進行優化,其對象建立相當于:

StringBuilder temp = new StringBuilder();

temp.append(a).append(b).append(c);

String s = temp.toString();

由上面的分析結果,可就不難推斷出String 采用連接配接運算符(+)效率低下原因分析,形如這樣的代碼:

public class Test {
    public static void main(String args[]) {
        String s = null;
        for(int i = ; i < ; i++) {
            s += "a";
        }
    }
}
           

每做一次 + 就産生個StringBuilder對象,然後append後就扔掉。下次循環再到達時重新産生個StringBuilder對象,然後 append 字元串,如此循環直至結束。 如果我們直接采用 StringBuilder 對象進行 append 的話,我們可以節省 N - 1 次建立和銷毀對象的時間。是以對于在循環中要進行字元串連接配接的應用,一般都是用StringBuffer或StringBulider對象來進行append操作。

10.String、StringBuffer、StringBuilder的差別

(1)可變與不可變:String是不可變字元串對象,StringBuilder和StringBuffer是可變字元串對象(其内部的字元數組長度可變)。

(2)是否多線程安全:String中的對象是不可變的,也就可以了解為常量,顯然線程安全。StringBuffer 與 StringBuilder 中的方法和功能完全是等價的,隻是StringBuffer 中的方法大都采用了synchronized 關鍵字進行修飾,是以是線程安全的,而 StringBuilder 沒有這個修飾,可以被認為是非線程安全的。

(3)String、StringBuilder、StringBuffer三者的執行效率:

StringBuilder > StringBuffer > String 當然這個是相對的,不一定在所有情況下都是這樣。比如String str = “hello”+ “world”的效率就比 StringBuilder st = new StringBuilder().append(“hello”).append(“world”)要高。是以,這三個類是各有利弊,應當根據不同的情況來進行選擇使用:

當字元串相加操作或者改動較少的情況下,建議使用 String str=”hello”這種形式;

當字元串相加操作較多的情況下,建議使用StringBuilder,如果采用了多線程,則使用StringBuffer。

11.String中的final用法和了解

final StringBuffer a = new StringBuffer(“111”);

final StringBuffer b = new StringBuffer(“222”);

a=b;//此句編譯不通過

final StringBuffer a = new StringBuffer(“111”);

a.append(“222”);//編譯通過

可見,final隻對引用的”值”(即記憶體位址)有效,它迫使引用隻能指向初始指向的那個對象,改變它的指向會導緻編譯期錯誤。至于它所指向的對象的變化,final是不負責的。

12.關于String str = new String(“abc”)建立了多少個對象?

這個問題在很多書籍上都有說到比如《Java程式員面試寶典》,包括很多國内大公司筆試面試題都會遇到,大部分網上流傳的以及一些面試書籍上都說是2個對象,這種說法是片面的。

首先必須弄清楚建立對象的含義,建立是什麼時候建立的?這段代碼在運作期間會建立2個對象麼?毫無疑問不可能,用javap -c反編譯即可得到JVM執行的位元組碼内容:

很顯然,new隻調用了一次,也就是說隻建立了一個對象。而這道題目讓人混淆的地方就是這裡,這段代碼在運作期間确實隻建立了一個對象,即在堆上建立了”abc”對象。而為什麼大家都在說是2個對象呢,這裡面要澄清一個概念,該段代碼執行過程和類的加載過程是有差別的。在類加載的過程中,确實在運作時常量池中建立了一個”abc”對象,而在代碼執行過程中确實隻建立了一個String對象。

是以,這個問題如果換成 String str = new String(“abc”)涉及到幾個String對象?合理的解釋是2個。

個人覺得在面試的時候如果遇到這個問題,可以向面試官詢問清楚”是這段代碼執行過程中建立了多少個對象還是涉及到多少個對象“再根據具體的來進行回答。

13.字元串池的優缺點:

字元串池的優點就是避免了相同内容的字元串的建立,節省了記憶體,省去了建立相同字元串的時間,同時提升了性能;另一方面,字元串池的缺點就是犧牲了JVM在常量池中周遊對象所需要的時間,不過其時間成本相比而言比較低。

四、綜合執行個體

package com.spring.test;

public class StringTest {
    public static void main(String[] args) {  
        /** 
         * 情景一:字元串池 
          * JAVA虛拟機(JVM)中存在着一個字元串池,其中儲存着很多String對象; 
         * 并且可以被共享使用,是以它提高了效率。 
         * 由于String類是final的,它的值一經建立就不可改變。 
         * 字元串池由String類維護,我們可以調用intern()方法來通路字元串池。  
         */  
        String s1 = "abc";     
        //↑ 在字元串池建立了一個對象  
        String s2 = "abc";     
        //↑ 字元串pool已經存在對象“abc”(共享),是以建立0個對象,累計建立一個對象  
        System.out.println("s1 == s2 : "+(s1==s2));    
        //↑ true 指向同一個對象,  
        System.out.println("s1.equals(s2) : " + (s1.equals(s2)));    
        //↑ true  值相等  
        //↑------------------------------------------------------over  
        /** 
         * 情景二:關于new String("") 
         *  
         */  
        String s3 = new String("abc");  
        //↑ 建立了兩個對象,一個存放在字元串池中,一個存在與堆區中;  
        //↑ 還有一個對象引用s3存放在棧中  
        String s4 = new String("abc");  
        //↑ 字元串池中已經存在“abc”對象,是以隻在堆中建立了一個對象  
        System.out.println("s3 == s4 : "+(s3==s4));  
        //↑false   s3和s4棧區的位址不同,指向堆區的不同位址;  
        System.out.println("s3.equals(s4) : "+(s3.equals(s4)));  
        //↑true  s3和s4的值相同  
        System.out.println("s1 == s3 : "+(s1==s3));  
        //↑false 存放的地區多不同,一個棧區,一個堆區  
        System.out.println("s1.equals(s3) : "+(s1.equals(s3)));  
        //↑true  值相同  
        //↑------------------------------------------------------over  
        /** 
         * 情景三:  
         * 由于常量的值在編譯的時候就被确定(優化)了。 
         * 在這裡,"ab"和"cd"都是常量,是以變量str3的值在編譯時就可以确定。 
         * 這行代碼編譯後的效果等同于: String str3 = "abcd"; 
         */  
        String str1 = "ab" + "cd";  //1個對象  
        String str11 = "abcd";   
        System.out.println("str1 = str11 : "+ (str1 == str11));  
        //↑------------------------------------------------------over  
        /** 
         * 情景四:  
         * 局部變量str2,str3存儲的是存儲兩個拘留字元串對象(intern字元串對象)的位址。 
         *  
         * 第三行代碼原理(str2+str3): 
         * 運作期JVM首先會在堆中建立一個StringBuilder類, 
         * 同時用str2指向的拘留字元串對象完成初始化, 
         * 然後調用append方法完成對str3所指向的拘留字元串的合并, 
         * 接着調用StringBuilder的toString()方法在堆中建立一個String對象, 
         * 最後将剛生成的String對象的堆位址存放在局部變量str3中。 
         *  
         * 而str5存儲的是字元串池中"abcd"所對應的拘留字元串對象的位址。 
         * str4與str5位址當然不一樣了。 
         *  
         * 記憶體中實際上有五個字元串對象: 
         *       三個拘留字元串對象、一個String對象和一個StringBuilder對象。 
         */  
        String str2 = "ab";  //1個對象  
        String str3 = "cd";  //1個對象                                         
        String str4 = str2+str3;                                        
        String str5 = "abcd";    
        System.out.println("str4 = str5 : " + (str4==str5)); // false  
        //↑------------------------------------------------------over  
        /** 
         * 情景五: 
         *  JAVA編譯器對string + 基本類型/常量 是當成常量表達式直接求值來優化的。 
         *  運作期的兩個string相加,會産生新的對象的,存儲在堆(heap)中 
         */  
        String str6 = "b";  
        String str7 = "a" + str6;  
        String str67 = "ab";  
        System.out.println("str7 = str67 : "+ (str7 == str67));  
        //↑str6為變量,在運作期才會被解析。  
        final String str8 = "b";  
        String str9 = "a" + str8;  
        String str89 = "ab";  
        System.out.println("str9 = str89 : "+ (str9 == str89));  
        //↑str8為常量變量,編譯期會被優化  
        //↑------------------------------------------------------over  
    }
}
           

運作結果:

s1 == s2 : true

s1.equals(s2) : true

s3 == s4 : false

s3.equals(s4) : true

s1 == s3 : false

s1.equals(s3) : true

str1 = str11 : true

str4 = str5 : false

str7 = str67 : false

str9 = str89 : true