天天看點

Java基礎——字元串之String類和StringBuilder類一.引言二.String類三.StringBuilder類

一.引言

很多人覺得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虛拟機中,對象頭的結構如下:

Java基礎——字元串之String類和StringBuilder類一.引言二.String類三.StringBuilder類

圖檔來源:http://blog.csdn.net/zhoufanyang_china/article/details/54601311。

也意味着,每建立一個String中間變量,都會占用一定記憶體,比我們想象的還要多。如果可以避免産生這麼多的中間變量,豈不是更好?

2.3編譯器的優化

首先我們編譯一下上面的代碼,然後再反編碼看一下編譯器都幹嘛了:

Java基礎——字元串之String類和StringBuilder類一.引言二.String類三.StringBuilder類

從這段反編譯後的位元組碼中可以看出,編譯器自動引入了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();
}
           

同樣通過反編譯看看編譯器都幹嘛了:

首先是重載“+”:

Java基礎——字元串之String類和StringBuilder類一.引言二.String類三.StringBuilder類

StringBuilder.append():

Java基礎——字元串之String類和StringBuilder類一.引言二.String類三.StringBuilder類

由此可見,編譯器的确對重載“+”方法進行了優化,但是在循環中使用重載“+”,每一次循環都會都産生一個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舉例:

Java基礎——字元串之String類和StringBuilder類一.引言二.String類三.StringBuilder類

總結起來就是:

(1)StringBuilder初始化時,既可以指定初始化容量(如果你已經大概知道最終的字元串大小,那這樣就可以省去擴容過程),也可以按照預設的初始化容量進行初始化。

(2)無論拼接多少次、是否會循環,隻會生成一個StringBuilder對象(當然如果你把StringBuilder s = new StringBuilder()這樣的語句寫在了循環内,那就另說,而且這樣寫,也不符合一般情況下的邏輯)。

文章内容參考了《Thinking in java》和幾篇網上的資料,已在文章給對外連結接。由于水準有限,文中出現錯誤與不妥之處在所難免,懇請讀者批評指正。