一.引言
很多人覺得C/C++難,Java則相對簡單,其中有一個原因就是,C/C++處理字元串那真的是會讓很多人頭疼,比如在C/C++中對字元串的初始化定義為:
char str[] = "java";
char *str = "java";
char str[]={'j','a','v','a','\0'};
一看到數組、指針,就讓很多人犯愁了。而又例如字元串的拼接,在C/C++中是通過strcat(str1,str2)實作的,但是使用這個方法,必須得清楚知道str1擁有足夠的空間容納str2,否則會造成不能完整将str2拼接到str1上。總之,挺麻煩的,不是?而Java則對字元串相關的處理方法進行了很進階的封裝,Java使用者也能很輕松地對字元串進行一系列操作,相比于C/C++,簡直是如魚得水。
當然,本人在此并不是比較C/C++和Java誰好誰不好。本篇文章主要講講Java中涉及到字元串的String類和StringBuilder類。
二.String類
1.String類的定義
如下代碼所示:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
//其他成員變量和方法
}
第一,可以注意到final修飾符,說明String類不能被繼承。
第二,成員變量char value[]用于存儲字元串中的每一個字元。
2.String對象的隻讀特性
String對象是不可變的,具有隻讀特性。
這句話看似無關痛癢,其實在實際的工程項目中,這一特性對性能必然有很大的影響,隻是在大多數的開發過程中,我們并不在意。
那如何說明String對象的隻讀特性呢?又如何說明這一特性對性能的影響呢?我們One by one的回答。
2.1證明隻讀特性
在《Thinking in Java》第13章《字元串》中,作者舉例說明:
package String;
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);
String qq = upcase(q);
System.out.println(qq);
System.out.println(q);
}
}
輸出:
howdy
HOWDY
howdy
作者的解釋:當把q傳給upcase()方法時,實際傳遞的是引用的一個拷貝。其實,每當把String對象作為方法的參數時,都會複制一份引用,而該引用所指的對象其實一直待在單一的實體位置上,從未動過。回到upcase()的定義,傳入其中的引用有了名字s,隻有upcase()運作的時候,局部引用s才存在。一旦upcase()運作結束,s就消失了。當然了,upcase()的傳回值,其實隻是最終結果的引用。這足以說明,upcase()傳回的引用已經指向了一個新的引用,而原本的q則還在原地。
個人認為這個例子并不能完整地說明String對象的隻讀特性。我的例子如下:
public static void main(String[] args) {
String s = "abc";
String t = "JAVA";
System.out.println(s);
System.out.println(t);
String ss = s.toUpperCase();
String tt = t.toUpperCase();
System.out.println(ss);
System.out.println(s);
System.out.println(tt);
System.out.println(t);
System.out.println(ss == s);
System.out.println(tt == t);
}
輸出:
abc
JAVA
ABC
abc
JAVA
JAVA
false
true
大家都知道在Java中“==”比較的是兩個對象的記憶體位址,從上面的例子可以看出來,如果原String對象的值未被修改,則傳回的就是原來的對象,如果原對象被修改了,就會傳回一個新的String對象。可參考String類中所有修改String值的方法,比如toUpperCase()方法中大緻結構就是:
if(不需要修改) return this; //傳回本身
else return new String(修改後的值);//傳回一個新的String對象
2.2隻讀特性的影響
如各種資料可見,最好的例子是字元串拼接。
最常用的方式就是重載“+”和StringBuilder.append()方法。可能一般情況下,大多數Java開發人員都喜歡用“+”,
因為最簡單,最友善,比如:
public static void main(String[] args) {
String a = "喜歡";
String b = "我" + a + "Java" + ;
}
由于String對象的不可變性,那麼上面的代碼會執行多次“+”:“我”和a相連,産生一個新的String對象,然後再和”Java”相連,再産生一個新的String對象,以此類推。實際開發過程中,一定會遇到拼接多個String對象的時候,那如此可見,這樣一行代碼,就會産生很多的String類型的中間變量。那對性能的影響展現在何處?這裡就要提到Java對象在記憶體中的真正大小=對象頭+執行個體資料+對齊填充(可參考http://www.cnblogs.com/zhanjindong/p/3757767.html)。我們在多線程開發中經常使用synchronized給對象加鎖,那一個對象的鎖狀态在哪裡?就在對象頭裡。32位HotSpot虛拟機中,對象頭的結構如下:

圖檔來源:http://blog.csdn.net/zhoufanyang_china/article/details/54601311。
也意味着,每建立一個String中間變量,都會占用一定記憶體,比我們想象的還要多。如果可以避免産生這麼多的中間變量,豈不是更好?
2.3編譯器的優化
首先我們編譯一下上面的代碼,然後再反編碼看一下編譯器都幹嘛了:
從這段反編譯後的位元組碼中可以看出,編譯器自動引入了StringBuilder類,因為StringBuilder更高效(為何更高效,請看第三部分)。編譯器建立了一個StringBuilder對象用于構造最終的String,然後調用了四次append()方法。也意味着上面的代碼等價于:
如此看來,編譯器會自動優化性能,那我們便可以随意使用重載“+”用于字元串拼接嗎?非也,例如:
//String“+”:循環拼接字元串
public static String connectStr(String[] str) {
String result = "";
for (String s : str) {
result += s;
}
return result;
}
//StringBuilder:循環拼接字元串
public static String connectStrBuilder(String[] str) {
StringBuilder stringBuilder = new StringBuilder();
for (String s : str) {
stringBuilder.append(s);
}
return stringBuilder.toString();
}
同樣通過反編譯看看編譯器都幹嘛了:
首先是重載“+”:
StringBuilder.append():
由此可見,編譯器的确對重載“+”方法進行了優化,但是在循環中使用重載“+”,每一次循環都會都産生一個StringBuilder類型的中間變量。OMG,我們不是一直在盡力避免産生不必要的中間變量嗎?而使用StringBuilder的append()方法,則簡單多了,因為隻會在循環之前産生一個StringBuilder對象用于構造最終的String,在循環中,隻需要調用append()方法即可。是以一般在處理字元串拼接時,為了性能達到最優,推薦使用StringBuilder的append()方法,然後再通過toString()方法将結果轉為String類型,同時StringBuilder也提供了其他一些方法。
三.StringBuilder類
前面已經提到在對字元串的某些處理上,StringBuilder類相比于String類更加高效,比較常見的就是通過append()方法進行字元串拼接,那麼為何StringBuilder會更加高效呢?我們來分析一下StringBuilder的定義和append()方法。
//StringBuilder.java
public final class StringBuilder
extends AbstractStringBuilder
implements java.io.Serializable, CharSequence
{
public StringBuilder() {
super();//調用父類的構造函數,并且參數為16,這個參數的意義是初始容量
}
public StringBuilder(int capacity) {//指定初始容量
super(capacity);
//其他構造函數等等
}
@Override
public StringBuilder append(String str) {
super.append(str);//調用父類AbstractStringBuilder的append()方法
return this;//傳回的是本身
}
}
再看看AbstractStringBuilder的定義和append()方法
//AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];//初始化value數組
}
//append()方法
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);//擴容
str.getChars(, len, value, count);//
count += len;
return this;
}
private void ensureCapacityInternal(int minimumCapacity) {
// overflow-conscious code
//如果需要的最小容量(minimumCapacity = 原來的字元串長度count+需要拼接的字元串長度)已經超過了總的容量(數組value的長度),則進行擴容
if (minimumCapacity - value.length > ) {
value = Arrays.copyOf(value,
newCapacity(minimumCapacity));
}
}
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << ) + ;//原來的容量進行翻倍!
if (newCapacity - minCapacity < ) {
newCapacity = minCapacity;//如果翻倍之後的容量依然小于需要的最小容量,則将數組的容量大小設定成需要的最小容量。
}
return (newCapacity <= || MAX_ARRAY_SIZE - newCapacity < )
? hugeCapacity(minCapacity)
: newCapacity;
}
是以,StringBuilder的拼接過程如下,用預設的初始容量16舉例:
總結起來就是:
(1)StringBuilder初始化時,既可以指定初始化容量(如果你已經大概知道最終的字元串大小,那這樣就可以省去擴容過程),也可以按照預設的初始化容量進行初始化。
(2)無論拼接多少次、是否會循環,隻會生成一個StringBuilder對象(當然如果你把StringBuilder s = new StringBuilder()這樣的語句寫在了循環内,那就另說,而且這樣寫,也不符合一般情況下的邏輯)。
文章内容參考了《Thinking in java》和幾篇網上的資料,已在文章給對外連結接。由于水準有限,文中出現錯誤與不妥之處在所難免,懇請讀者批評指正。