類的繼承關係
String、StringBuffer、StringBuilder 都實現了 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.StringBuilder與StringBuffer都繼承自AbstractStringBuilder,我們看到AbstractStringBuilder源碼中對char數組的定義:
char[] value;
如果進一步查看AbstractStringBuilder的源碼,就能發現其中的數組在進行append()操作時會進行擴容,所以它是一個可擴容的動態數組。
線程安全
String既然是不可變的,那麼肯定線程安全。
而StringBuilder與StringBuffer的區別呢?兩個類中的方法基本一樣,只是StringBuffer中的所有方法都加上了synchronized關鍵字。
所以StringBuffer是線程安全的,StringBuilder是線程不安全的。
使用效率
對String對象進行 + 操作,實際上,會創建一個臨時的StringBuilder對象進行拼接操作,用StringBuilder的append()方法拼接完畢,再調用toString()方法返回一個新的String對象,然後拿原來的索引指向這個新產生的對象。當然頻繁地創建對象就會比較影響系統的性能。
而StringBuilder和StringBuffer都是調用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