String,StringBuffer和StringBuilder

類的繼承關係

StringStringBufferStringBuilder 都實現了 CharSequence 接口。下面是類關係圖:

這裏寫圖片描述

數據結構

它們內部都是用一個char類型的數組實現,雖然它們都與字符串相關,但是其處理機制不同

1.String是不可改變的量,創建後就不能再修改了。可以看到String源碼中對char數組的定義:

    private final char[] value;

注意:

  • 這裏看到了char數組被聲明爲了final但是並不是因爲被聲明爲了final,char數組就不可變了。確實char數組的地址值不可變,但是char數組存儲的元素是可以改變的。然而SUN公司的工程師,在後面所有String的方法裏很小心的沒有去動char數組裏的元素,沒有暴露內部成員字段。private final char[] value這一句裏,private的私有訪問權限的作用都比final大。而且設計師還很小心地把整個String設成final禁止繼承,避免被其他人繼承後破壞。所以String是不可變的關鍵都在底層的實現,而不是一個final。考驗的是工程師構造數據類型,封裝數據的功力。

  • 如果我們選用可變的StringBuffer或者StringBuilder作爲HashMap和HashSet的鍵值,就很有可能出現線程安全問題。


2.StringBuilderStringBuffer都繼承自AbstractStringBuilder,我們看到AbstractStringBuilder源碼中對char數組的定義:

    char[] value;

如果進一步查看AbstractStringBuilder的源碼,就能發現其中的數組在進行append()操作時會進行擴容,所以它是一個可擴容的動態數組

線程安全

String既然是不可變的,那麼肯定線程安全。

StringBuilderStringBuffer的區別呢?兩個類中的方法基本一樣,只是StringBuffer中的所有方法都加上了synchronized關鍵字。
所以StringBuffer是線程安全的,StringBuilder是線程不安全的。

使用效率

對String對象進行 + 操作,實際上,會創建一個臨時的StringBuilder對象進行拼接操作,用StringBuilder的append()方法拼接完畢,再調用toString()方法返回一個新的String對象,然後拿原來的索引指向這個新產生的對象。當然頻繁地創建對象就會比較影響系統的性能。

StringBuilderStringBuffer都是調用append()對內部的數組進行擴容。因爲StringBuffer的方法都加了鎖,所以會比StringBuilder慢。

所以一般情況下使用效率從高到低StringBuilder > StringBuffer > String

知識補充

在 JAVA 語言中有8中基本類型和一種比較特殊的類型String。這些類型爲了使他們在運行過程中速度更快,更節省內存,都提供了一種常量池的概念。常量池就類似一個JAVA系統級別提供的緩存

8種基本類型的常量池都是系統協調的,String類型的常量池比較特殊。它的主要使用方法有兩種:

  • 直接使用雙引號聲明出來的String對象會直接存儲在常量池中。

  • 如果不是用雙引號聲明的String對象,可以使用String提供的intern方法。intern 方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。

面試題

第一題

String s1 = “abc”;
String s2 = “abc”;
System.out.println(s1 == s2);

結果是true。由於字符串是常量(內存中創建對象後不能修改), 而且字符串在程序中經常使用,所以Java對其提供了緩衝區。緩衝區內的字符串會被共享。使用雙引號的形式定義字符串常量就是存儲在緩衝區中的。使用”abc”時會先在緩衝區中查找是否存在此字符串,沒有就創建一個,有則直接使用。第一次使用”abc”時會在緩衝區中創建,第二次則是直接引用之前創建好的了。

第二題
String s1 = new String(“abc”);創建了幾個對象?
創建了一個或者兩個 ,首先new String(“abc”)肯定會在內存中創建一個對象,它是參照常量”abc”進行創建對象,所以首先會看緩衝區中是否存在常量”abc”,如果不存在,要先在緩衝區中創建一個常量”abc”,否則不用創建,所以答案爲1個或者2個。

第三題

String s1 = new String(“abc”);
String s2 = new String(“abc”);
System.out.println(s1 == s2);

結果是false。new String()就是在堆內存中創建一個新的對象,然後把常量池中的”abc”的副本放到了堆內存中新的對象中,並且分配新的地址值,和常量池中的不一樣 。

第四題

String s1 = “abc”;
String s2 = “a”;
String s3 = “bc”;
String s4 = s2 + s3;
System.out.println(s1 == s4);

結果是false。Java中字符串的相加其內部是使用StringBuilder類的append()方法和toString()方法來實現的,而StringBuilder類toString()方法返回的字符串是通過new String()創建的。

第五題

String s1 = “abc”;
String s2 = “a” + “bc”;
System.out.println(s1 == s2);

結果是true。其實這裏的s2並沒有進行字符串相加, 兩個雙引號形式的字符串常量相加,在編譯階段直接會被轉爲一個字符串“abc”。

第六題

String str = “abc”;
str.substring(3);
str.concat(“123″);
System.out.println(str);   

結果是”abc”。由於字符串是常量(內存中創建對象後不能修改),該類中所有方法都不會改變字符串的值。如果希望使用一個可變的字符串,可以使用StringBuilder或StringBuffer類。

下面兩道題就很有意思了。

第七題

    String s = new String("1");
    s.intern();
    String s2 = "1";
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    s3.intern();
    String s4 = "11";
    System.out.println(s3 == s4);

JDK6:false,false;
JDK7:false,true。

第八題

    String s = new String("1");
    String s2 = "1";
    s.intern();
    System.out.println(s == s2);

    String s3 = new String("1") + new String("1");
    String s4 = "11";
    s3.intern();
    System.out.println(s3 == s4);

JDK6:false,false;
JDK7:false,false。

出現如此現象的原因,是因爲JDK6中的常量池在方法區中,而從JDK7開始常量池在堆中

在第七題中,看s3和s4字符串。String s3 = new String("1") + new String("1");,這句代碼中現在生成了2最終個對象,是字符串常量池中的“1”JAVA Heap 中的 s3引用指向的對象。中間還有2個匿名的new String(“1”)我們不去討論它們。此時s3引用對象內容是”11”,但此時常量池中是沒有 “11”對象的

接下來s3.intern();這一句代碼,是將 s3中的“11”字符串放入 String 常量池中,因爲此時常量池中不存在“11”字符串,如果是在JDK6中會在常量池中生成一個 “11” 的對象,關鍵點是 JDK7 中常量池不在 Perm 區域了,這塊做了調整。常量池中不需要再存儲一份對象了,可以直接存儲堆中的引用,這份引用指向 s3 引用的對象,也就是說引用地址是相同的

詳解請參見深入解析String#intern

參考:
1.String類的面試題
2.java面試題中常見的關於String類問題總結
3.在java中String類爲什麼要設計成final?
4.深入解析String#intern

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章