一,從根本上認識java.lang.String類和String池
首先,我建議先看看String類的源碼實現,這是從本質上認識String類的根本出發點.從中可以看到:
1,String類是final的,不可被繼承.public final class String.
2,String類是的本質是字符數組char[], 並且其值不可改變.private final char value[];
然後打開String類的API文檔,可以發現:
3,String類對象有個特殊的創建的方式,就是直接指定比如String x = "abc","abc"就表示一個字符串對象.而x是"abc"對象的地址,也叫做"abc"對象的引用.
4,String對象可以通過"+"串聯.串聯後會生成新的字符串.也可以通過concat()來串聯,這個後面會講述. 5,創建字符串的方式很多,歸納起來有三類:
其一,使用new關鍵字創建字符串,比如String s1 = new String("abc");
其二,直接指定.比如String s2 = "abc";
其三,使用串聯生成新的字符串.比如String s3 = "ab" + "c";
6,Java運行時會維護一個String Pool(String池),JavaDoc翻譯很模糊"字符串緩沖區".String池用來存放運行時中產生的各種字符串,並且池中的字符串的內容不重復.而一般對象不存在這個緩沖池,並且創建的對象僅僅存在於方法的堆棧區.下面是個系統內存示意圖:
二,String對象的創建
String對象的創建也很講究,關鍵是要明白其原理.
原理1:當使用任何方式來創建一個字符串對象s時,Java運行時(運行中JVM)會拿著這個X在String池中找是否存在內容相同的字符串對象,如果不存在,則在池中創建一個字符串s,否則,不在池中添加.
原理2:Java中,只要使用new關鍵字來創建對象,則一定會(在堆區或棧區)創建一個新的對象.
原理3:使用直接指定或者使用純字符串串聯來創建String對象,則僅僅會檢查維護String池中的字符串,池中沒有就在池中創建一個,有則罷了!但絕不會在堆棧區再去創建該String對象.
原理4:使用包含變量的表達式來創建String對象,則不僅會檢查維護String池,而且還會在堆棧區創建一個String對象.
另外,String的intern()方法是一個本地方法,定義為public native String intern(); intern()方法的價值在於讓開發者能將注意力集中到String池上.當調用 intern 方法時,如果池已經包含一個等於此 String 對象的字符串(該對象由 equals(Object) 方法確定),則返回池中的字符串.否則,將此 String 對象添加到池中,並且返回此 String 對象的引用.
最後,有幾點問題請大家注意:String a; 與String a=null在作為類變量時候是等價的,在局部變量則不同.null表示一個空引用,String a=null意思是在棧中聲明了a,但是這個a沒有指向任何地址.此時我們注意到String a 在棧中聲明了a,但是也沒有指向任何地址,但是java的語法檢查如果在局部變量中,String a;是不能直接使用的,String a=null中的這個a可以直接使用.
三,經常會問到的問題
1,"abc"與new String("abc");
String s = new String("abc");創建了幾個String Object?(如這裡創建了多少對象? 和一道小小的面試題 )
這個問題比較簡單,涉及的知識點包括:
引用變量與對象的區別;
字符串文字"abc"是一個String對象;
文字池[pool of literal strings]和堆[heap]中的字符串對象.
引用變量與對象:除了一些早期的Java書籍和現在的垃圾書籍,人們都可以從中比較清楚地學習到兩者的區別.A aa;語句聲明一個類A的引用變量aa[我常常稱之為句柄],而對象一般通過new創建.所以題目中s僅僅是一個引用變量,它不是對象.[ref 句柄,引用與對象]
Java中所有的字符串文字[字符串常量]都是一個String的對象.有人[特別是C程序員]在一些場合喜歡把字符串"當作/看成"字符數組,這也沒有辦法,因為字符串與字符數組存在一些內在的聯系.事實上,它與字符數組是兩種完全不同的對象.
System.out.println("Hello".length());
char[] cc={'H','i'};
System.out.println(cc.length);
字符串對象的創建:由於字符串對象的大量使用[它是一個對象,一般而言對象總是在heap分配內存],Java中為了節省內存空間和運行時間[如比較字符串時,==比equals()快],在編譯階段就把所有的字符串文字放到一個文字池[pool of literal strings]中,而運行時文字池成為常量池的一部分.文字池的好處,就是該池中所有相同的字符串常量被合並,只占用一個空間.我們知道,對兩個引用變量,使用==判斷它們的值[引用]是否相等,即指向同一個對象:
String s1 = "abc" ;
String s2 = "abc" ;
if( s1 == s2 ) System.out.println("s1,s2 refer to the same object");
else System.out.println("trouble");
這裡的輸出顯示,兩個字符串文字保存為一個對象.就是說,上面的代碼只在pool中創建了一個String對象.
現在看String s = new String("abc");語句,這裡"abc"本身就是pool中的一個對象,而在運行時執行new String()時,將pool中的對象復制一份放到heap中,並且把heap中的這個對象的引用交給s持有.ok,這條語句就創建了2個String對象.
String s1 = new String("abc") ;
String s2 = new String("abc") ;
if( s1 == s2 ){ //不會執行的語句}
這時用==判斷就可知,雖然兩個對象的"內容"相同[equals()判斷],但兩個引用變量所持有的引用不同,
BTW:上面的代碼創建了幾個String Object? [三個,pool中一個,heap中2個.]
[Java2 認證考試學習指南 (第4版)( 英文版)p197-199有圖解.]
2,字符串的+運算和字符串轉換
字符串轉換和串接是很基礎的內容,因此我以為這個問題簡直就是送分題.事實上,我自己就答錯了.
String str = new String("jf"); // jf是接分
str = 1+2+str+3+4;
一共創建了多少String的對象?[我開始的答案:5個.jf,new,3jf,3jf3,3jf34]
首先看JLS的有關論述:
一,字符串轉換的環境[JLS 5.4 String Conversion]
字符串轉換環境僅僅指使用雙元的+運算符的情況,其中一個操作數是一個String對象.在這一特定情形下,另一操作數轉換成String,表達式的結果是這兩個String的串接.
二,串接運算符[JLS 15.18.1 String Concatenation Operator + ]
如果一個操作數/表達式是String類型,則另一個操作數在運行時轉換成一個String對象,並兩者串接.此時,任何類型都可以轉換成String.[這裡,我漏掉了"3"和"4"]
如果是基本數據類型,則如同首先轉換成其包裝類對象,如int x視為轉換成Integer(x).
現在就全部統一到引用類型向String的轉換了.這種轉換如同[as if]調用該對象的無參數toString方法.[如果是null則轉換成"null"].因為toString方法在Object中定義,故所有的類都有該方法,而且Boolean, Character, Integer, Long, Float, Double, and String改寫了該方法.
關於+是串接還是加法,由操作數決定.1+2+str+3+4 就很容易知道是"3jf34".[BTW :在JLS的15.18.1.3中舉的一個jocular little example,真的很無趣.]
下面的例子測試了改寫toString方法的情況..
class A
{ int i = 10;
public static void main(String []args)
{ String str = new String("jf");
str += new A();
System.out.print(str);
}
public String toString(){ return " a.i ="+i+"\n"; }
}
三,字符串轉換的優化
按照上述說法,str = 1+2+str+3+4;語句似乎應該就應該生成5個String對象:
1+2 =3,then 3→Integer(3)→"3" in pool? [假設如此]
"3"+str(in heap) = "3jf" (in heap)
"3jf" +3 ,first 3→Integer(3)→"3" in pool? [則不創建] then "3jf3"
"3jf3"+4 create "4" in pool
then "3jf34"
這裡我並不清楚3,4轉換成字符串後是否在池中,所以上述結果仍然是猜測.
為了減少創建中間過渡性的字符串對象,提高反復進行串接運算時的性能,a Java compiler可以使用StringBuffer或者類似的技術,或者把轉換與串接合並成一步.例如:對於 a + b + c ,Java編譯器就可以將它視為[as if]
new StringBuffer().append(a).append(b).append(c).toString();
注意,對於基本類型和引用類型,在append(a)過程中仍然要先將參數轉換,從這個觀點看,str = 1+2+str+3+4;創建的字符串可能是"3","4"和"3jf34"[以及一個StringBuffer對象].
現在我仍然不知道怎麼回答str = 1+2+str+3+4;創建了多少String的對象,.或許,這個問題不需要過於研究,至少SCJP不會考它.
3,這又不同:str = "3"+"jf"+"3"+"4";
如果是一個完全由字符串文字組成的表達式,則在編譯時,已經被優化而不會在運行時創建中間字符串.測試代碼如下:
String str1 ="3jf34";
String str2 ="3"+"jf"+"3"+"4";
if(str1 == str2) { System.out.println("str1 == str2"); }
else { System.out.println("think again"); }
if(str2.equals(str1)) System.out.println("yet str2.equals(str1)");
可見,str1與str2指向同一個對象,這個對象在pool中.所有遵循Java Language Spec的編譯器都必須在編譯時對constant expressions 進行簡化.JLS規定:Strings computed by constant expressions (y15.28) are computed at compile time and then treated as if they were literals.
對於String str2 ="3"+"jf"+"3"+"4";我們說僅僅創建一個對象.注意,"創建多少對象"的討論是說運行時創建多少對象.
BTW:編譯時優化
String x = "aaa " + "bbb ";
if (false) { x = x + "ccc "; }
x += "ddd ";
等價於: String x = "aaa bbb "; x = x + "ddd ";
//這個地方我自己進行了編譯,不過和他的結論不一樣,好像當用x+="ddd"的時候和直接的x="aaa"+"bbb"+"ddd" 不同,但是具體為什麼我也不清楚,正在研究中...
4,不變類
String對象是不可改變的(immutable).有人對str = 1+2+str+3+4;語句提出疑問,怎麼str的內容可以改變?其實仍然是因為不清楚:引用變量與對象的區別.str僅僅是引用變量,它的值??它持有的引用可以改變.你不停地創建新對象,我就不斷地改變指向.[參考TIJ的Read-only classes.]
不變類的關鍵是,對於對象的所有操作都不可能改變原來的對象[只要需要,就返回一個改變了的新對象].這就保證了對象不可改變.為什麼要將一個類設計成不變類?有一個OOD設計的原則:Law of Demeter.其廣義解讀是:使用不變類.只要有可能,類應當設計為不變類.