String的特殊之處
String是Java編程中很常見的一個類,這個類的實例是不可變的(immutable ).為了提高效率,JVM內部對其操作進行了一些特殊處理,本文就旨在于幫助大家辨析這些特殊的地方.
在進入正文之前,你需要澄清這些概念:
1) 堆與棧
2) 相同與相等,==與equals
3) =的真實意義.
棧與堆
1. 棧(stack)與堆(heap)都是Java用來在內存中存放數據的地方。與C++不同,Java自動管理棧和堆,程序員不能直接地設置棧或堆。每個函數都有自己的棧,而一個程序只有一個堆.
2. 棧的優勢是,存取速度比堆要快,僅次于直接位于CPU中的寄存器。但缺點是,存在棧中的數據大小與生存期必須是確定的,缺乏靈活性。另外,棧數據可以共享,詳見第3點。堆的優勢是可以動態地分配內存大小,生存期也不必事先告訴編譯器,Java的垃圾收集器會自動收走這些不再使用的數據。但缺點是,由于要在運行時動態分配內存,存取速度較慢。 3. Java中的數據類型有兩種。 一種是基本類型(primitive types), 共有8種,即int, short, long, byte, float, double, boolean, char(注意,并沒有string的基本類型)。這種類型的定義是通過諸如int a = 3; long b = 255L;的形式來定義的,稱為自動變量。值得注意的是,自動變量存的是字面值,不是類的實例,即不是類的引用,這里并沒有類的存在。如int a = 3; 這里的a是一個指向int類型的引用,指向3這個字面值。這些字面值的數據,由于大小可知,生存期可知(這些字面值固定定義在某個程序塊里面,程序塊退出后,字段值就消失了),出于追求速度的原因,就存在于棧中。 另外,棧有一個很重要的特殊性,就是存在棧中的數據可以共享。假設我們同時定義 int a = 3; int b = 3; 編譯器先處理int a = 3;首先它會在棧中創建一個變量為a的引用,然后查找有沒有字面值為3的地址,沒找到,就開辟一個存放3這個字面值的地址,然后將a指向3的地址。接著處理int b = 3;在創建完b的引用變量后,由于在棧中已經有3這個字面值,便將b直接指向3的地址。這樣,就出現了a與b同時均指向3的情況。 特別注意的是,這種字面值的引用與類對象的引用不同。假定兩個類對象的引用同時指向一個對象,如果一個對象引用變量修改了這個對象的內部狀態,那么另一個對象引用變量也即刻反映出這個變化。相反,通過字面值的引用來修改其值,不會導致另一個指向此字面值的引用的值也跟著改變的情況。如上例,我們定義完a與 b的值后,再令a=4;那么,b不會等于4,還是等于3。在編譯器內部,遇到a=4;時,它就會重新搜索棧中是否有4的字面值,如果沒有,重新開辟地址存放4的值;如果已經有了,則直接將a指向這個地址。因此a值的改變不會影響到b的值。 另一種是包裝類數據,如Integer, String, Double等將相應的基本數據類型包裝起來的類。這些類數據全部存在于堆中,Java用new()語句來顯示地告訴編譯器,在運行時才根據需要動態創建,因此比較靈活,但缺點是要占用更多的時間。
相同與相等,==與equals
在Java中,相同指的是兩個變量指向的地址相同,地址相同的變量自然值相同;而相等是指兩個變量值相等,地址可以不同.
相同的比較使用==,而相等的比較使用equals.
對于字符串變量的值比較來說,我們一定要使用equals而不是==.
=的真實意義
=即賦值操作,這里沒有問題,關鍵是這個值有時是真正的值,有的是地址,具體來說會根據等號右邊的部分而變化.
如果是基本類型(八種),則賦值傳遞的是確定的值,即把右邊變量的值傳遞給左邊的變量.
如果是類類型,則賦值傳遞的是變量的地址,即把等號左邊的變量地址指向等號右邊的變量地址.
指出下列代碼的輸出
String andy="andy";
String bill="andy";
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
String str=“andy”的機制分析
上頁代碼的輸出是andy和bill地址相同.
當通過String str=“andy”;的方式定義一個字符串時,JVM先在棧中尋找是否有值為“andy”的字符串,如果有則將str指向棧中原有字符串的地址;如果沒有則創建一個,再將str的地址指向它. String andy=“andy”這句代碼走的是第二步,而String bill=“andy”走的是第一步,因此andy和bill指向了同一地址,故而andy==bill,andy和bill地址相等,所以輸出是andy和bill地址相同.
這樣做能節省空間—少創建一個字符串;也能節省時間—定向總比創建要省時.
指出下列代碼的輸出
String andy="andy";
String bill="andy";
bill="bill";
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
輸出及解釋
上頁代碼的輸出是:andy和bill地址不同
當執行bill=“bill”一句時,外界看來好像是給bill變換了一個新值bill,但JVM的內部操作是把棧變量bill的地址重新指向了棧中一塊值為bill的新地址,這是因為字符串的值是不可變的,要換值(賦值操作)只有將變量地址重新轉向. 這樣andy和bill的地址在執行bill=“bill”一句后就不一樣了,因此輸出是andy和bill地址不同.
指出下列代碼的輸出
String andy=new String("andy");
String bill=new String("andy");
// 地址比較
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比較
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
輸出及機制分析
andy和bill地址不同
andy和bill值相等
我們知道new操作新建出來的變量一定處于堆中,字符串也是一樣.
只要是用new()來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即每個字符串都有自己的值,自然地址就不會相同.因此輸出了andy和bill地址不同.
equals操作比較的是值而不是地址,地址不同的變量值可能相同,因此輸出了andy和bill值相等.
指出下列代碼的輸出
String andy=new String("andy");
String bill=new String(andy);
// 地址比較
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比較
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
輸出
andy和bill地址不同
andy和bill值相等
道理仍和第八頁相同.只要是用new()來新建對象的,都會在堆中創建,而且其字符串是單獨存值的,即每個字符串都有自己的值,自然地址就不會相同.
指出下列代碼的輸出
String andy="andy";
String bill=new String(“Bill");
bill=andy;
// 地址比較
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比較
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
輸出及解析
andy和bill地址相同
andy和bill值相等
String bill=new String(“Bill”)一句在棧中創建變量bill,指向堆中創建的”Bill”,這時andy和bill地址和值都不相同;而執行bill=andy;一句后,棧中變量bill的地址就指向了andy,這時bill和andy的地址和值都相同了.而堆中的”Bill”則沒有指向它的指針,此后這塊內存將等待被垃圾收集.
指出下列代碼的輸出
String andy="andy";
String bill=new String("bill");
andy=bill;
// 地址比較
if(andy==bill){
System.out.println("andy和bill地址相同");
}
else{
System.out.println("andy和bill地址不同");
}
// 值比較
if(andy.equals(bill)){
System.out.println("andy和bill值相等");
}
else{
System.out.println("andy和bill值不等");
}
輸出
andy和bill地址相同
andy和bill值相等
道理同第十二頁
結論
使用諸如String str = “abc”;的語句在棧中創建字符串時時,str指向的字符串不一定會被創建!唯一可以肯定的是,引用str本身被創建了。至于這個引用到底是否指向了一個新的對象,必須根據上下文來考慮,如果棧中已有這個字符串則str指向它,否則創建一個再指向新創建出來的字符串. 清醒地認識到這一點對排除程序中難以發現的bug是很有幫助的。
使用String str = “abc”;的方式,可以在一定程度上提高程序的運行速度,因為JVM會自動根據棧中數據的實際情況來決定是否有必要創建新對象。而對于String str = new String(“abc”);的代碼,則一概在堆中創建新對象,而不管其字符串值是否相等,是否有必要創建新對象,從而加重了程序的負擔。
如果使用new()來新建字符串的,都會在堆中創建字符串,而且其字符串是單獨存值的,即每個字符串都有自己的值,且其地址絕不會相同
當比較包裝類里面的數值是否相等時,用equals()方法;當測試兩個包裝類的引用是否指向同一個對象時,用==。
由于String類的immutable性質,當String變量需要經常變換其值如SQL語句拼接,HTML文本輸出時,應該考慮使用StringBuffer類,以提高程序效率。