Java SE基礎鞏固(二):String類

String使用頻率很是高,不管是在大型仍是小型的應用程序都會大量的使用String類。因此,理解並以高性能的方式使用String是很是重要的。java

String類提供了不少功能豐富的API,例如substring(),indexOf(),lastIndexOf()等等。String是不可變類,它沒有提供任何訪問內部狀態的方法,即便是substring()這樣的看起來是要修改字符串方法也不會真正的修改實例,而是會建立一個新的String對象並返回。String也不能被繼承,繼承雖然提升了靈活性,但同時也可能會破壞父類的邏輯(由於重寫機制),這樣會使得String類很是危險,故String的設計者將其設計成不可繼承是有很充分的理由的。api

下面咱們從源碼開始慢慢分析上面講到的特性。數組

1 String類中豐富的API

String類有不少各類功能的API,沒法一一細說,我就挑選了indexOf()方法來詳細討論討論。安全

public int indexOf(int ch) {
    return indexOf(ch, 0);
}

public int indexOf(int ch, int fromIndex) {
    final int max = value.length;
    if (fromIndex < 0) {
        fromIndex = 0;
    } else if (fromIndex >= max) {
        // Note: fromIndex might be near -1>>>1.
        return -1;
    }

    if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
        // handle most cases here (ch is a BMP code point or a
        // negative value (invalid code point))
        final char[] value = this.value;
        for (int i = fromIndex; i < max; i++) {
            if (value[i] == ch) {
                return i;
            }
        }
        return -1;
    } else {
        return indexOfSupplementary(ch, fromIndex);
    }
}
複製代碼

indexOf()除了列出來的兩種,還有5種重載形式,不過不是很經常使用,比較經常使用就是這倆。只接受一個參數的就很少說了,他沒有多餘的邏輯,直接就調用了indexOf(int ch, int fromIndex)。indexOf(int ch, int fromIndex)方法的第一個參數是要查找的字符,是int類型,關於該參數,JDK文檔是這樣描述的:網絡

a character (Unicode code point).app

即它是一個Unicode編碼的字符(看JDK裏註釋文檔是瞭解一個方法的最快速的方式),有計算機基礎知識的朋友應該不難理解爲何使用整形數值來代替字符,雖然是int類型,但實際上咱們使用的時候徹底能夠直接傳遞char類型,以下所示:ide

String s = "hello";
int i = s.indexOf('h',0);
複製代碼

第二個參數fromIndex即從哪一個下標開始查找,若是調用的是隻有一個參數ch的API,那麼fromIndex的值默認就是0,即從字符串開頭查找。性能

接下來就是if-else結構,這裏就是對fromIndex作驗證,若是fromIndex小於0,即令其等於0,若是fromIndex大於max,那麼就直接返回-1,即表示沒有找到。ui

接下來的代碼纔是indexOf的核心:this

final char[] value = this.value;
for (int i = fromIndex; i < max; i++) {
    if (value[i] == ch) {
        return i;
    }
}
return -1;
複製代碼

其實也很簡單,就是遍歷value數組,而後一個一個比較,若是找到就直接返回,遍歷完以後還沒找到,就返回-1,表示沒有找到。

其餘的方法就很少說了,具體使用建議看看JDK API文檔,或者使用IDE,直接在IDE裏看註釋文檔。(IDEA的話可使用Ctrl/cmd + 左鍵單擊進入方法)。

2 String的不可變性

String是不可變的,但爲何要設計成不可變的呢?主要有如下幾點考慮:

  1. 便於實現常量池。常量池裏的字符串常量若是可變的話,會致使不少安全問題。
  2. 網絡安全。網絡鏈接的參數每每都是以字符串的形式出現,若是字符串可變,那就意味着參數可能被篡改,這顯然不是咱們想看到的。
  3. 線程安全。不可變類確定是線程安全的,不存在多個線程修改共享變量的狀況(由於字符串沒法修改)。
  4. 加快字符串處理速度。由於字符串是不可變的,因此hashcode只須要在對象建立的時候計算一次便可,例如將String做爲Map的Key。

那String是如何實現不可變的呢?實現不可變至少須要下面幾個步驟:

  1. 將類聲明成final,這樣他就不可被繼承了。(繼承頗有可能會改變類的行爲)
  2. 將全部可變的狀態聲明成final和私有的。
  3. 保證沒有任何相似setter可修改狀態的方法。
  4. 在getter方法中返回的狀態應該返回其拷貝,而不是其自己。
  5. 若是不得不提供一些修改狀態的方法,那麼這些方法返回值應該是一個新的對象,不能直接在源對象上修改。

咱們來看看String是如何實現的。

從源碼中咱們能夠看到,String在類上是有fianl修飾,這樣整個類的全部方法就都是final方法,不可被繼承重寫。

public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    /** The value is used for character storage. */
    private final char value[];
    ......
}
複製代碼

同時,咱們能夠看到除了hash以外,其餘的成員變量都是final修飾的。以下所示:

public static final Comparator<String> CASE_INSENSITIVE_ORDER
                                         = new CaseInsensitiveComparator();

private int hash; // Default to 0
private static final long serialVersionUID = -6849794470754667710L;
private final char value[];
private static final ObjectStreamField[] serialPersistentFields =
        new ObjectStreamField[0];
複製代碼

咱們發現,這和咱們最開始講的第二個原則有些不一樣。但不要緊,那只是一個比較強硬的規則,只要能證實即便不將其設置成final,也不會有問題便可。那CASE_INSENSITIVE_ORDER爲何是public的呢?其實這個成員變量不能算是String的內部狀態,只能是算是一個常量,即便被外界訪問了,也不會有太大影響。

再來看看String有沒有提供setter方法,我瀏覽了一下其API文檔,沒有發現任何狀態的setter方法。接下來看看getter方法,發現有個getBytes()方法,該訪問會訪問到String的內部狀態value數組,並將其編碼成字節數組,以下所示:

public byte[] getBytes() {
    return StringCoding.encode(value, 0, value.length);
}

//StringCoding.encode方法
static byte[] encode(char[] ca, int off, int len) {
    String csn = Charset.defaultCharset().name();
    try {
        // use charset name encode() variant which provides caching.
        return encode(csn, ca, off, len);
    } catch (UnsupportedEncodingException x) {
        warnUnsupportedCharset(csn);
    }
    try {
        return encode("ISO-8859-1", ca, off, len);
    } catch (UnsupportedEncodingException x) {
        // If this code is hit during VM initialization, MessageUtils is
        // the only way we will be able to get any kind of error message.
        MessageUtils.err("ISO-8859-1 charset not available: "
                         + x.toString());
        // If we can not find ISO-8859-1 (a required encoding) then things
        // are seriously wrong with the installation.
        System.exit(1);
        return null;
    }
}
複製代碼

encode方法並無改變value數組的內容,只是獲取了內容,並根據內容按照必定的編碼方式編碼並將結果存入byte數組中,最後返回。從這裏能夠看出,這個方法沒有對String內部狀態作修改。

最後看看有沒有一些可能修改String的API,大體瀏覽了一下API 文檔,發現有不少方法均可能修改String,例如substring,replace等。但實際上,這些方法最終沒有修改String對象的值,而是根據原String的內容生成了新的String對象,以下substring方法所示:

public String substring(int beginIndex) {
        if (beginIndex < 0) {
            throw new StringIndexOutOfBoundsException(beginIndex);
        }
        int subLen = value.length - beginIndex;
        if (subLen < 0) {
            throw new StringIndexOutOfBoundsException(subLen);
        }
        return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
    }
複製代碼

關鍵看看最後的返回語句,若是beginIndex爲0,那麼就不須要從新建立對象了,直接返回自己便可,若是不爲0,那就建立一個新的String對象,而不是將對象自己進行修改。replace等方法也是同樣的,就再也不贅述了。

可見,String類基本上是知足上面提到的5個步驟的,只有少部分代碼沒有遵循,例如hash不是final修飾的,但不要緊,只要能保證沒有任何外部途徑能修改hash值便可。

3 「+」號重載

咱們常常會用「+」號來拼接兩個字符串,例如:

String s1 = "hello,";
String s2 = "world";
String res = s1 + s2;
System.out.println(res);
複製代碼

在上一節中,咱們說到了String是不可變的,但爲何這裏看起來就好像String「可變」了呢?其實這裏並無違反String的不可變性,只是編譯器和咱們玩了個小「把戲」。咱們來看看生成的字節碼是怎樣的吧,先使用javac將其編譯,而後使用javap -verbose xxx.class 命令來展現字節碼,結果以下所示(省略了其餘內容):

......
Code:
      stack=2, locals=4, args_size=1
         0: ldc           #2                  // String hello,
         2: astore_1
         3: ldc           #3                  // String world
         5: astore_2
         6: new           #4                  // class java/lang/StringBuilder
         9: dup
        10: invokespecial #5                  // Method java/lang/StringBuilder."<init>":()V
        13: aload_1
        14: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        17: aload_2
        18: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        21: invokevirtual #7                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: astore_3
        25: getstatic     #8                  // Field java/lang/System.out:Ljava/io/PrintStream;
        28: aload_3
        29: invokevirtual #9                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        32: return
 ....
複製代碼

咱們能夠看到,「+」號被編譯器翻譯成了StringBuilder.append()方法(看序號14和18的指令),換句話說,「+」號被重載了,最終拼接完畢後,調用StringBuilder.toString()方法返回新的拼接好的String對象(看序號21的指令)。

因此,String沒有被修改,而是調用了StringBuilder.append()方法來輔助拼接原字符串,並生成新的字符串。

運算符重載在C++裏是容許的,可是Java並不容許。到底是出於什麼緣由,我不是很清楚,但我認爲運算符重載是致使C++複雜的一個緣由。

4 Integer.toString()和String.valueOf()的關係

咱們直接來看源碼(不懂的時候直接看源碼是最好的方式,比去網上查更加快速、直觀):

//String.valueOf(int)
public static String valueOf(int i) {
    return Integer.toString(i);
}

//Integer.toString(int i)
public static String toString(int i) {
    if (i == Integer.MIN_VALUE)
        return "-2147483648";
    int size = (i < 0) ? stringSize(-i) + 1 : stringSize(i);
    char[] buf = new char[size];
    getChars(i, size, buf);
    return new String(buf, true);
}
複製代碼

很明顯,String.valueOf(int)方法直接調用了Integre.toString()方法,以此來返回一個用字符串表示的整形數字。而在Integer.toString()方法裏,最後返回的是一個新的String對象。

String.valueOf()還有其餘重載形式,和 valueOf(int)的方式很是類似,例如valueOf(long)會調用Long.toString(),valueOf(double)會調用Double.toString(),但Boolean就不須要了,直接返回「true」或者「false」便可,具體差異仍是建議看看源碼。

5 Java9和Java8 String類的區別

Java9對String作了不小的改變,最根本的區別是再也不使用char類型數組來存儲字符了,而是改用byte類型的數組。char類型佔用空間是2個字節,byte佔用的是1個字節,這樣就節省了不少空間。其餘的修改都是基於這個改變而改變的,具體改變建議網上搜索一下,這是我看到的一篇文章:JAVA9 String新特性,說說你不知道的東西

6 小結

String類是很是重要的,Java程序的方方面面都會用到。本文簡單的講了一下String.indexOf()方法、「+」號重載,String的不可變性等,還順帶着講了一丟丟閱讀源碼的方式,我的以爲這纔是最重要的,由於咱們不可能把JDK裏全部的類及其API記下來,但只要掌握了看源碼、文檔的方法,就隨時都能快速的瞭解該類及其API的功能和使用方法,因此掌握方法很是重要!!!

相關文章
相關標籤/搜索