JDK原始碼閱讀-String類

七印miss發表於2019-01-13

概述

String類無疑是日常開發中的使用最頻繁的類之一。其被final修飾:

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence 
複製程式碼

因此可知String類是一個不可變類,且實現了Serializable,ComparableCharSequence介面。

String類類圖

hash

hash閾來用快取字串的hash code,預設為0。

/** Cache the hash code for the string */
private int hash; // Default to 0
複製程式碼

serialVersionUID

String類實現了Serializable介面,因此支援序列化和反序列化。serialVersionUID欄位則是用來在執行時判斷類版本的一致性。

value[]

String類用一個char陣列來儲存真實字元序列,就好比Integer中的欄位value表示Integer物件的實際值。並且這個陣列還被final修飾了,這正是我們所熟知的:String的內容一旦被初始化後,其值就不能再被修改了。當然,通過反射還是可以修改String內容的,不過沒啥意義。

/**The value is usesed for character storage.*/
private final char value[];
複製程式碼

建構函式

在jdk8中有14中不同的String類建構函式,下面記錄一些常用的。

public String()

很明顯,String類顯示的宣告瞭一個空建構函式。

public String() {
    this.value = "".value;
}
複製程式碼

即使是空的建構函式,但還是會建立一個String物件,因此,下面這種字串的宣告方式,不僅雞肋而且浪費恐怖,不提倡:

String str = new String();
str = "realString";
複製程式碼

public Sting(String)

入參為一個字串的建構函式。其直接將入參字串的value域和hash域直接賦值給目標String。且String類是不可變類,因此不會對原有的入參字串產生副作用。

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}
複製程式碼

在實際開發中,我們應該直接採用字面量賦值的方式來初始化String物件(記為方法一),而不是通過上述這個建構函式new一個String物件(記為方法二)。這是因為:

  • 方法一構造一個新的字串時,編譯器會優先從常量池中(堆中的一塊專門區域)查詢是否已經存在這個字串字面量。如果存在則直接返回該物件地址,否則構造一個新物件並儲存一份在常量池中。這實現了一種類似快取的機制,可以避免不必要的開銷。
  • 方法二new一個String物件時,即使常量池在存在該物件的字面量,但還是會要求編譯器在堆中開闢一個新的記憶體空間來儲存新new出來的物件。
public static void main(String[] args){
    String a = "string";
    String b = "string";
    String c = new String("string");
    String d = new String("string");
    
    System.out.println("a==b: " +(a == b));
    System.out.println("a==c: " +(a == c));
    System.out.println("c==d: " +(c == d));
}

---------------output------------
a==b: true // a和b都是常量池中同一個String物件
a==c: false // a是常量池中的物件,但c是堆記憶體中新申請的一塊空間
c==d: false // 雖然在常量池中存在字面量"string",但c和d都是堆記憶體中新建立的獨立物件
複製程式碼

public String(char[])

入參是一個char陣列,構造器呼叫Arrays.copyOf()方法將入參的字元序列依次複製成一個新的String物件,且不會對入參有副作用。

public String(char value[]) {
    this.value = Arrays.copyof(value, vlaue.length);
}
複製程式碼

方法

equals(Object)方法

該方法重寫了Object的equals方法,用來判斷兩個字串是否包含相同的字元序列,最大時間複雜度為O(n),其中n字串長度。

public boolean equals(Object anObject) {
    // 1. 引用同一物件,判定true
    if (this == anObject) {
        return true;
    }
    
    // 2. 入參不是String型別,判定false
    if (anObject instancof String) {
        String anotherString = (String) anObject;
        int n = value.lenth;
        // 3. 長度不匹配,判定false
        if (n == anotherString.value.length) {
            char v1[] = value; // 寫成char[] v1 = value,更規範點
            char v2[] = anotherString.value;
            int i = 0;
            // 4. 逐字元比較,如不等,判定false
            while (n-- != 0){
                if (v1[i] != v2[i]) {
                    return false;
                }
                i++;
            }
        }
        // 5. 否則,判斷true
        return true;
    }
    return false;
}
複製程式碼

compareTo(String)方法

該方法實現了Comparable介面的compareTo方法,用來按字典順序比較兩個字串。該比較是基於字串中各個字元的Unicode值。

字典序

因為該方法比較兩個字串的大小,採用的是字典序,因此有必要了解一下字典序。
字典序定義:如果這兩個字串不同,那麼它們要麼在某個索引處的字元不同(該索引對二者均為有效索引),要麼長度不同,或者同時具備這兩種情況。如果它們在一個或多個索引位置上的字元不同,假設 k 是這類索引的最小值;則在位置k上具有較小值的那個字串(使用 < 運算子確定),其字典順序在其他字串之前。在這種情況下,compareTo返回這兩個字串在位置k處兩個char值得差,即值:

this.charAt(k) - anotherString.charAt(k);
複製程式碼

如果沒有字元不同的索引位置,則較短字串的字典順序在較長字串之前。在這種情況下,compareTo返回這兩個字串長度的差,即值:

this.length() - anotherString.length();
複製程式碼

入參

anotherString: String,要比較的字串

返回值

  • 正數:按字典序,呼叫者位於入參字串之後
  • 負數:按字典序,呼叫者位於入參字串之前
  • 0:兩者相等,此時若呼叫equals方法肯定是返回true

原始碼分析

public int compareTo(String anotherString) {
    int len1 = value.length;
    int len2 = anotherString.value.length;
    int lim = min(len1, len2); // 取兩字串長度的最小值
    char v1[] = value;
    char v2[] = anotherString.value;
    
    int k = 0;
    while (k < lim) { // 只遍歷min(len1, len2)次
        char c1 = v1[k];
        char c2 = v2[k];
        if (c1 != c2) { // 某個索引處字元不同,則返回字元的差值
            return c1 - c2;
        }
        k++;
    }
    // 如果前個字元都相同,則返回長度差
    return len1 - len2;
}
複製程式碼

這個方法寫的很巧妙。從初始索引開始,如果兩個物件能比較字元的地方都比較完了還相等,就直接返回長度差。此時如果兩個字串長度相等,則返回的剛好就是0,巧妙地判斷了返回值的三種情況。且是的最大時間複雜度為T=O(n),其中n為兩個字串的長度最小值。
可能有人好奇,為什麼沒有在方法體中看到對入參為null的防護?這是因為Comparable介面中已經定了,當入參為null時,compareTo方法會直接丟擲NullPointException異常

startsWith方法

startsWith(才發現原來中間還有個s,程式碼補全的鍋!)用來判斷字串是否以指定的字首開始。其包含兩種過載方法:

startsWith(String, int)

入參
  • String prefix: 字首
  • int toffset: 在此字串中開始查詢的位置
返回值
  • true: 字元序列是此物件從索引 toffset 處開始的子字串字首
  • false: toffset為負或大於此String物件的長度或字元序列不是此物件從索引 toffset 處開始的子字串字首
原始碼分析
public boolean startsWith(String prefix, int toffset) {
    char ta[] = value; // 變數名為啥用ta,難道是target?
    int to = toffset;
    char pa[] = prefix.value;
    int po = 0;
    int pc = prefix.value.length;
    // Note: toffset might be near -1>>>1.
    //如果起始地址小於0或者大於自身物件長度,返回false
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
    // 從尾開始比較
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
            return false;
        }
    }
    return true;
}
複製程式碼

startsWith(String)

這個方法直接呼叫了startsWith(String, 0):

public boolean startsWith(String prefix) {
    return startsWith(prefix, 0);
}
複製程式碼

endsWith(String)

相比startsWith,endsWith只有一種方法簽名。而且,endsWith的實現方式很巧妙,直接藉助了startsWith(String, int)方法:

public boolean endsWith(String suffix) {
    // 將判斷是否存在指定字尾轉換為去判斷是否是指定字首
    return startsWith(suffix, value.length - suffix.value.length); 
    }
複製程式碼

replace方法

replace包含兩種過載形式,一種是替換字串中指定字元,另一種則是替換指定字元序列。

replace(char, char)

直接上原始碼,就是逐字元校驗,討巧之處在於先定位到舊值出現的位置,才去new一個char陣列儲存目標字串。

public String replace(char oldChar, char newChar) {
    if (oldChar != newChar) {
                int len = value.length;
        int i = -1;
        char[] val = value; /* avoid getfield opcode */

        // 找到oldChar最開始出現的位置
        while (++i < len) {
            if (val[i] == oldChar) {
                break;
            }
        }
        // 從那個位置開始依次遍歷,用新值代替出現的舊值
        if (i < len) {
            char buf[] = new char[len];
            for (int j = 0; j < i; j++) { // 直接for迴圈拷貝
                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;
}
複製程式碼

待續...updated 0115

相關文章