文章已收錄Github精選,歡迎Star: https://github.com/yehongzhi/learningSummary
String類
在Java中String類的使用的頻率可謂相當高。它是Java語言中的核心類,在java.lang包下,主要用于字元串的比較、查找、拼接等等操作。如果要深入了解一個類,最好的方法就是看看源碼:
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
/** Cache the hash code for the string */
private int hash; // Default to 0
//...
}
從源碼中,可以看出以下幾點:
- String類被final關鍵字修飾,表示String類不能被繼承,并且它的成員方法都預設為final方法。
- String類實作了Serializable、CharSequence、 Comparable接口。
- String類的值是通過char數組存儲的,并且char數組被private和final修飾,字元串一旦建立就不能再修改。
下面通過幾個問題不斷加深對String類的了解。
問題一
上面說字元串一旦建立就不能再修改,String類提供的replace()方法不就可以替換修改字元串的内容嗎?
實際上replace()方法并沒有對原字元串進行修改,而是建立了一個新的字元串傳回,看看源碼就知道了。
public String replace(char oldChar, char newChar) {
if (oldChar != newChar) {
int len = value.length;
int i = -1;
char[] val = value; /* avoid getfield opcode */
while (++i < len) {
if (val[i] == oldChar) {
break;
}
}
if (i < len) {
char buf[] = new char[len];
for (int j = 0; j < i; j++) {
buf[j] = val[j];
}
while (i < len) {
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
//建立一個新的字元串傳回
return new String(buf, true);
}
}
return this;
}
其他方法也是一樣,無論是sub、concat還是replace操作都不是在原有的字元串上進行的,而是重新生成了一個新的字元串對象。
問題二
為什麼要使用final關鍵字修飾String類?
首先要講final修飾類的作用,被final修飾的類不能被繼承,類中的所有成員方法都會被隐式地指定為final方法。也就是不能擁有子類,成員方法也不能被重寫。
回到問題,String類被final修飾主要基于安全性和效率兩點考慮。
- 安全性
因為字元串是不可變的,是以是多線程安全的,同一個字元串執行個體可以被多個線程共享。這樣便不用因為線程安全問題而使用同步。字元串自己便是線程安全的。
String被許多的Java類(庫)用來當做參數,比如網絡連接配接位址URL,檔案路徑path,還有反射機制所需要的String參數等,假若String不是固定不變的,将會引起各種安全隐患。
- 效率
字元串不變性保證了hash碼的唯一性,是以可以放心的進行緩存,這也是一種性能優化手段,意味着不必每次都取計算新的哈希碼。
隻有當字元串是不可變的,字元串池才有可能實作,字元串常量池是java堆記憶體中一個特殊的存儲區域,當建立一個String對象,假如此字元串值已經存在于常量池中,則不會建立一個新的對象,而是引用已經存在的對象。
字元串常量池
字元串的配置設定和其他對象配置設定一樣,是需要消耗高昂的時間和空間的,而且字元串我們使用的非常多。JVM為了提高性能和減少記憶體的開銷,是以在執行個體化字元串的時候使用字元串常量池進行優化。
池化思想其實在Java中并不少見,字元串常量池也是類似的思想,當建立字元串時,JVM會首先檢查字元串常量池,如果該字元串已經存在常量池中,那麼就直接傳回常量池中的執行個體引用。如果字元串不存在常量池中,就會執行個體化該字元串并且将其放到常量池中。
我們可以寫個簡單的例子證明:
public static void main(String[] args) throws Exception {
String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2);//true
}

還有一個面試中經常問的,new String(“abc”)建立了幾個對象?
這可能就是想考你對字元串常量池的了解,我一般回答是一個或者兩個對象。
如果之前"abc"字元串沒有使用過,毫無疑問是建立兩個對象,堆中建立了一個String對象,字元串常量池建立了一個,一共兩個。
如果之前已經使用過了"abc"字元串,則不會再在字元串常量池建立對象,而是從字元串常量緩沖區中擷取,隻會在堆中建立一個String對象。
String s1 = "abc";
String s2 = new String("abc");
//s2這行代碼,隻會建立一個對象
字元串拼接
字元串的拼接在Java中是很常見的操作,但是拼接字元串并不是簡簡單單地使用"+"号即可,還有一些要注意的點,否則會造成效率低下。
比如下面這段代碼:
public static void main(String[] args) throws Exception {
String s = "";
for (int i = 0; i < 10; i++) {
s+=i;
}
System.out.println(s);//0123456789
}
在循環内使用+=拼接字元串會有什麼問題呢?我們反編譯一下看看就知道了。
其實反編譯後,我們可以看到String類使用"+="拼接的底層其實是使用StringBuilder,先初始化一個StringBuilder對象,然後使用append()方法拼接,最後使用toString()方法得到結果。
問題在于如果在循環體内使用+=拼接,會建立很多臨時的StringBuilder對象,拼接後再調用toString()賦給原String對象。這會生成大量臨時對象,嚴重影響性能。
是以在循環體内進行字元串拼接時,建議使用StringBuilder或者StringBuffer類,例子如下:
public static void main(String[] args) throws Exception {
StringBuilder s = new StringBuilder();
for (int i = 0; i < 10; i++) {
s.append(i);
}
System.out.println(s.toString());//0123456789
}
StringBuilder和StringBuffer的差別在于,StringBuffer的方法都被sync關鍵字修飾,是以是線程安全的,而StringBuilder則是線程不安全的(效率高)。
總結
回顧一下,本文介紹了String類的不可變的特點,還有字元串常量池的作用,最後簡單地從JVM編譯的層面對字元串拼接提出一點建議。所謂溫故而知新,即使是一些很基礎很常見的類,如果深入去探索的話,也會有一番收獲。
這篇文章就講到這裡了,感謝大家的閱讀,希望看完大家能有所收獲!
我是一個努力讓大家記住的程式員。我們下期再見!!!
能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!