目錄
1 String不可變性
- String類被宣告為 final,因此它不可被繼承。
- 內部使用char陣列儲存資料,該陣列被宣告為final,這意味著value陣列初始化之後就不能再指向其它陣列。
- String內部沒有改變value陣列的方法
- String類中所有修改String值的方法,如果內容沒有改變,則返回原來的String物件引用,如果改變了,建立了一個全新的String物件,包含修改後的字串內容,最初的String物件沒有任何改變。(目的:節約儲存空間、避免額外的開銷)
//String的類宣告以及value欄位程式碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
private final char value[]; //字元陣列儲存String的內容
/** Cache the hash code for the string */
private int hash; // Default to 0
}
不可變的驗證分析:
public class Immutable {
public static String upcase(String s) {
return s.toUpperCase();
}
public static void main(String[] args) {
String q = "howdy";
System.out.println(q); // howdy
String qq = upcase(q);
System.out.println(qq); // HOWDY
System.out.println(q); // howdy
}
} /* 輸出:
howdy
HOWDY
howdy
*///:~
- 當把q傳給upcase0方法時,實際傳遞的是引用的一個複製。
- upcase0方法中,傳入引用s,只有upcase0執行的時候,區域性引用s才存在。一旦upcase0執行結束,s就消失。upcaseO的返回值是最終結果的引用。
- 綜上,upcase()返回的引用已經指向了一個新的物件,而原本的q則還在原地。
延伸結論:
String物件作為方法的引數時,都會複製一份引用,引數傳遞是引用的複製
2 不可變的好處
1. 可以快取 hash 值
String的hash值經常被使用,例如String用做HashMap的key。不可變的特性可以使得hash值也不可變,因此只需要進行一次計算。
2. String Pool 的需要
如果一個String物件已經被建立過了,那麼就會從 String Pool 中取得引用。只有String是不可變的,才可能使用 String Pool。
3. 執行緒安全
String不可變性天生具備執行緒安全,可以在多個執行緒中安全地使用。
3 String+和StringBuilder效率差異
String使用“+”表示字串拼接
先說結論:
- “+”操作,javac編譯器會自動最佳化為StringBuilder.append() 呼叫。
- StringBuilder要比“+”操作高效
- 涉及迴圈追加的,手動建立StringBuilder物件操作比“+”操作編譯器最佳化,更高效
驗證:
public class StringBuilderTest {
public static void main(String[] args) {
String s1 = "ABC";
String s2 = "123";
String result = s1+s2;
System.out.println(result);
}
}
編譯並檢視位元組碼:javap -verbose StringBuilderTest.class
執行過程:
呼叫了2次append()方法,最後呼叫StringBuilder.toString()返回最終結果
為什麼StringBuilder要比+高效?
- +操作,按照:每次追加都建立新的String物件,把字元加入value陣列中。這裡產生一次物件建立操作,以及對應的垃圾回收
- StringBuilder的底層陣列value也是用到了char[],但它沒有宣告為final,故它可變,所以追加內容時不用建立新的陣列,而是直接修改value
- StringBuilder比+省去String物件建立以及垃圾回收的開銷,因此效率更高。
原始碼追溯:
//StringBuilder.append()
@Override
public StringBuilder append(char c) {
super.append(c);
return this;
}
// 父類 AbstractStringBuilder.append()
@Override
public AbstractStringBuilder append(char c) {
ensureCapacityInternal(count + 1);
value[count++] = c;
return this;
}
//AbstractStringBuilder value 欄位
abstract class AbstractStringBuilder implements Appendable, CharSequence {
//The value is used for character storage.
char[] value; // 沒有宣告為final,因此value可變
}
手動實現StringBuilder物件操作比編譯器自行最佳化,更高效:
- 透過位元組碼分析可知(我這裡省去了,可以自己實現驗證):迴圈部分的程式碼更簡短、更簡單,而且它只生成了一個StringBuilder物件。
- 顯式地建立StringBuilder還允許你預先為其指定大小。如果你已經知道最終的字串大概有多長,那預先指定StringBuilder的大小可以避免多次重新分配緩衝。
當你為一個類編寫toString方法時,如果字串操作比較簡單,那就可以信賴編譯
器,它會為你合理地構造最終的字串結果。但是,如果你要在toString0方法中使用迴圈,那
麼最好自己建立一個StringBuilder物件來實現。
4 String, StringBuffer and StringBuilder
可變性
- String 不可變
- StringBuffer和StringBuilder可變
執行緒安全
- String不可變,因此是執行緒安全的
- StringBuilder 不是執行緒安全的
- StringBuffer 是執行緒安全的,內部使用synchronized進行同步
效率
- 如果要操作少量的資料用String
- 單執行緒環境且字串緩衝區涉及大量資料 StringBuilder
- 多執行緒環境且字串緩衝區涉及大量資料 StringBuffer
5 String與JVM記憶體管理
一、引入字串常量池
- Javac編譯後,位元組碼檔案中有一塊區域:常量池,儲存了包括類中宣告的字串常量值等字面量
- 執行時,JVM開闢實際記憶體空間:字串常量值寫入了字串常量池,屬於方法區的一部分。
案例1:
String s1 = "win";
String s2 = "win";
System.out.println(s1==s2);
//輸出結果:true
//引用 s1 s2 的值等於win在字串常量池的地址值
結論:
引用 s1 s2 的值等於win在字串常量池的地址值
分析位元組碼的執行過程:
案例2:
public class StringPool {
public static void main(String[] args) {
String s3 = new String("win");
String s4 = new String("win");
System.out.println(s3==s4);//false
}
}
結論:
透過new運算子建立的字串物件不指向字串池中的任何物件。
位元組碼分析:
綜上:
public class StringPool {
public static void main(String[] args) {
String s1 = "win";
String s2 = "win";
String s3 = new String("win");
String s4 = new String("win");
System.out.println(s1==s2);//true
System.out.println(s1==s3);//false
System.out.println(s3==s4);//false
}
}
6 String api方法
從這個表中可以看出,當需要改變字串的內容時,String類的方法都會返回一個新的String物件。同時,如果內容沒有發生改變,String的方法只是返回指向原物件的引用而已。這可以節約儲存空間以及避免額外的開銷。