Java-- String原始碼分析

zzzzMing發表於2018-03-10

  版權宣告:本文為博主原創文章,未經博主允許不得轉載

  本篇博文基於java8,主要探討java中的String原始碼。

  首先,將一個類分為幾個部分,分別是類定義(繼承,實現介面等),全域性變數,方法,內部類等等,再分別對這幾個部分進行說明,這樣到最後類的全貌也就比較直觀了。

   一:實現介面。

  

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {

 

  • java.io.Serializable

    這個序列化介面沒有任何方法和域,僅用於標識序列化的語意。

  • Comparable<String>

    這個介面只有一個compareTo(T 0)介面,用於對兩個例項化物件比較大小。

  • CharSequence

    這個介面是一個只讀的字元序列。包括length(), charAt(int index), subSequence(int start, int end)這幾個API介面,值得一提的是,StringBuffer和StringBuild也是實現了改介面。

  二:主要變數。

  

    /** The value is used for character storage. */
    private final char value[];

    /** Cache the hash code for the string */
    private int hash; // Default to 0
  public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
 

  可以看到,value[]是儲存String的內容的,即當使用String str = "abc";的時候,本質上,"abc"是儲存在一個char型別的陣列中的。

  而hash是String例項化的hashcode的一個快取。因為String經常被用於比較,比如在HashMap中。如果每次進行比較都重新計算hashcode的值的話,那無疑是比較麻煩的,而儲存一個hashcode的快取無疑能優化這樣的操作。

  最後,這個CASE_INSENSITIVE_ORDER在下面內部類中會說到,其根本就是持有一個靜態內部類,用於忽略大小寫得比較兩個字串。

  三:內部類。

 再String只有一個內部類,那就是 

    private static class CaseInsensitiveComparator
            implements Comparator<String>, java.io.Serializable {
        // use serialVersionUID from JDK 1.2.2 for interoperability
        private static final long serialVersionUID = 8575799808933029326L;

        public int compare(String s1, String s2) {
            int n1 = s1.length();
            int n2 = s2.length();
            int min = Math.min(n1, n2);
            for (int i = 0; i < min; i++) {
                char c1 = s1.charAt(i);
                char c2 = s2.charAt(i);
                if (c1 != c2) {
                    c1 = Character.toUpperCase(c1);
                    c2 = Character.toUpperCase(c2);
                    if (c1 != c2) {
                        c1 = Character.toLowerCase(c1);
                        c2 = Character.toLowerCase(c2);
                        if (c1 != c2) {
                            // No overflow because of numeric promotion
                            return c1 - c2;
                        }
                    }
                }
            }
            return n1 - n2;
        }

        /** Replaces the de-serialized object. */
        private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
    }

 這裡有一個疑惑,在String中已經有了一個compareTo的方法,為什麼還要有一個CaseInsensitiveComparator的內部靜態類呢?

其實這一切都是為了程式碼複用。

首先看一下這個類就會發現,其實這個比較和compareTo方法也是有差別的,這個方法在比較時是忽略大小寫的。

而且這是一個單例,可以簡單得用它來比較兩個String,因為String類提供一個變數:CASE_INSENSITIVE_ORDER 來持有這個內部類,這樣當要比較兩個String時可以通過這個變數來呼叫。

其次,可以看到String類中提供的compareToIgnoreCase方法其實就是呼叫這個內部類裡面的方法實現的。這就是程式碼複用的一個例子。

 

  四:方法。

  首先是一系列的初始化方法。

    public String() {
        this.value = "".value;
    }

  String支援多種初始化方法,包括接收String,char[],byte[],StringBuffer等多種引數型別的初始化方法。但本質上,其實就是將接收到的引數傳遞給全域性變數value[]。

  

    public int length() {
        return value.length;
    }

    public boolean isEmpty() {
        return value.length == 0;
    }
    
    public char charAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return value[index];
    }

  知道了String其實內部是通過char[]實現的,那麼就不難發現length(),isEmpty(),charAt()這些方法其實就是在內部呼叫陣列的方法。

  

   
  //返回指定索引的程式碼點
  public int codePointAt(int index) {
        if ((index < 0) || (index >= value.length)) {
            throw new StringIndexOutOfBoundsException(index);
        }
        return Character.codePointAtImpl(value, index, value.length);
    }
  //返回指定索引前一個程式碼點
public int codePointBefore(int index) { int i = index - 1; if ((i < 0) || (i >= value.length)) { throw new StringIndexOutOfBoundsException(index); } return Character.codePointBeforeImpl(value, index, 0); }
  //返回指定起始到結束段內字元個數
public int codePointCount(int beginIndex, int endIndex) { if (beginIndex < 0 || endIndex > value.length || beginIndex > endIndex) { throw new IndexOutOfBoundsException(); } return Character.codePointCountImpl(value, beginIndex, endIndex - beginIndex); }
  //返回指定索引加上codepointOffset後得到的索引值
public int offsetByCodePoints(int index, int codePointOffset) { if (index < 0 || index > value.length) { throw new IndexOutOfBoundsException(); } return Character.offsetByCodePointsImpl(value, 0, value.length, index, codePointOffset); }

 這幾個函式用得比較少,並且可以看到其本質上都是用Character這個類的一些靜態方法來實現。這些功能在平常並不經常使用,個人認為,如果使用的話那應該是在對未知字串進行處理,且重點在異常處理上。

 這裡說明一下,16 位unicode編碼的所有 65,536 個字元並不能完全表示全世界所有正在使用或曾經使用的字元。於是,Unicode 標準已擴充套件到包含多達 1,112,064 個字元。那些超出原來的16 位限制的字元被稱作增補字元。Java的char型別是固定16bits的。程式碼點在U+0000 — U+FFFF之內到是可以用一個char完整的表示出一個字元。但程式碼點在U+FFFF之外的,一個char無論如何無法表示一個完整字元。這樣用char型別來獲取字串中的那些程式碼點在U+FFFF之外的字元就會出現問題。

增補字元是程式碼點在 U+10000 至 U+10FFFF 範圍之間的字元,也就是那些使用原始的 Unicode 的 16 位設計無法表示的字元。從 U+0000 至 U+FFFF 之間的字符集有時候被稱為基本多語言面 (BMP UBasic Multilingual Plane )。因此,每一個 Unicode 字元要麼屬於 BMP,要麼屬於增補字元。

  //將字串複製到dst陣列中,複製到dst陣列中的起始位置可以指定。值得注意的是,該方法並沒有檢測複製到dst陣列後是否越界。
    void getChars(char dst[], int dstBegin) {
        System.arraycopy(value, 0, dst, dstBegin, value.length);
    }

    public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        System.arraycopy(value, srcBegin, dst, dstBegin, srcEnd - srcBegin);
    }

可以看到,這個兩個過載方法本質上都是呼叫System.arraycopy()這個函式,包括在jdk很多其他原始碼中都是這樣,比如ThreadPoolExcuter,看似有很多個過載,其實本質上都是呼叫同樣的一個函式,只是會給你不同的預設初始值。

 

    //獲取當前字串的二進位制
    public void getBytes(int srcBegin, int srcEnd, byte dst[], int dstBegin) {
        if (srcBegin < 0) {
            throw new StringIndexOutOfBoundsException(srcBegin);
        }
        if (srcEnd > value.length) {
            throw new StringIndexOutOfBoundsException(srcEnd);
        }
        if (srcBegin > srcEnd) {
            throw new StringIndexOutOfBoundsException(srcEnd - srcBegin);
        }
        Objects.requireNonNull(dst);

        int j = dstBegin;
        int n = srcEnd;
        int i = srcBegin;
        char[] val = value;   /* avoid getfield opcode */

        while (i < n) {
            dst[j++] = (byte)val[i++];
        }
    }
    public byte[] getBytes(String charsetName)
            throws UnsupportedEncodingException {
        if (charsetName == null) throw new NullPointerException();
        return StringCoding.encode(charsetName, value, 0, value.length);
    }
  
  public byte[] getBytes() {
  return StringCoding.encode(value, 0, value.length);
  }
 

將String字串轉成二進位制的幾種方式,可以指定byte陣列,也能讓其返回一個byte陣列。本質上,其實都是呼叫了StringCoding.encode()這個靜態方法。

 

    public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }
public int hashCode() {
        int h = hash;
        if (h == 0 && value.length > 0) {
            char val[] = value;

            for (int i = 0; i < value.length; i++) {
                h = 31 * h + val[i];
            }
            hash = h;
        }
        return h;
    }

 hashCode()和equals()兩個方法比較重要且有所關係就放一起了,equals()是string能成為廣泛用於Map[key,value]中key的關鍵所在。

此外除equals()外,還有隻比較內容的contentEquals();

    public boolean contentEquals(CharSequence cs) {
        // Argument is a StringBuffer, StringBuilder
        if (cs instanceof AbstractStringBuilder) {
            if (cs instanceof StringBuffer) {
                synchronized(cs) {
                   return nonSyncContentEquals((AbstractStringBuilder)cs);
                }
            } else {
                return nonSyncContentEquals((AbstractStringBuilder)cs);
            }
        }
        // Argument is a String
        if (cs instanceof String) {
            return equals(cs);
        }
        // Argument is a generic CharSequence
        char v1[] = value;
        int n = v1.length;
        if (n != cs.length()) {
            return false;
        }
        for (int i = 0; i < n; i++) {
            if (v1[i] != cs.charAt(i)) {
                return false;
            }
        }
        return true;
    }

這個主要是用來比較String和StringBuffer或者StringBuild的內容是否一樣。可以看到傳入引數是CharSequence ,這也說明了StringBuffer和StringBuild同樣是實現了CharSequence。原始碼中先判斷引數是從哪一個類例項化來的,再根據不同的情況採用不同的方案,不過其實大體都是採用上面那個for迴圈的方式來進行判斷兩字串是否內容相同。

    public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }

這個就是String對Comparable介面中方法的實現了。其核心就是那個while迴圈,通過從第一個開始比較每一個字元,當遇到第一個較小的字元時,判定該字串小。

但還有一種是在較小長度的字元粗每個字元都和另一個字串的每個字元相等,那麼字串長度較大的較大。

public int compareToIgnoreCase(String str) {
        return CASE_INSENSITIVE_ORDER.compare(this, str);
    }

這個也是比較字串大小,規則和上面那個比較方法基本相同,差別在於這個方法忽略大小寫。可以看到這是通過一個String 內部一個static的內部類實現的,那麼為什麼還要特地寫一個內部類呢,這樣其實就是為了程式碼複用,這樣在其他情況下也可以使用這個static內部類。

    public boolean regionMatches(int toffset, String other, int ooffset,
            int len) {
        char ta[] = value;
        int to = toffset;
        char pa[] = other.value;
        int po = ooffset;
        // Note: toffset, ooffset, or len might be near -1>>>1.
        if ((ooffset < 0) || (toffset < 0)
                || (toffset > (long)value.length - len)
                || (ooffset > (long)other.value.length - len)) {
            return false;
        }
        while (len-- > 0) {
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        return true;
    }

比較該字串和其他一個字串從分別指定地點開始的n個字元是否相等。看程式碼可知道,其原理還是通過一個while去迴圈對應的比較區域進行判斷,但在比較之前會做判定,判定給定引數是否越界。

 

    public boolean startsWith(String prefix, int toffset) {
        char ta[] = value;
        int to = toffset;
        char pa[] = prefix.value;
        int po = 0;
        int pc = prefix.value.length;
        // Note: toffset might be near -1>>>1.
        if ((toffset < 0) || (toffset > value.length - pc)) {
            return false;
        }
        while (--pc >= 0) {
            if (ta[to++] != pa[po++]) {
                return false;
            }
        }
        return true;
    }

 判斷當前字串是否以某一段其他字串開始的,和其他字串比較方法一樣,其實就是通過一個while來迴圈比較。

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);
        }
    }

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

可以看到這裡在if中有一句

ch < Character.MIN_SUPPLEMENTARY_CODE_POINT
而在Character中看到
public static final int MIN_SUPPLEMENTARY_CODE_POINT = 0x010000;
這表明在java中char儲存的值通常都是比ox010000小的,就是BMP型別的字元。
而當比這個值大的時候,就是增補字元了,那麼會呼叫Character先判斷是否是有效的字元,再進一步處理。
    public int lastIndexOf(int ch, int fromIndex) {
        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;
            int i = Math.min(fromIndex, value.length - 1);
            for (; i >= 0; i--) {
                if (value[i] == ch) {
                    return i;
                }
            }
            return -1;
        } else {
            return lastIndexOfSupplementary(ch, fromIndex);
        }
    }

和indexOf基本一致,只是順序反過來。

 

    static int indexOf(char[] source, int sourceOffset, int sourceCount,
            char[] target, int targetOffset, int targetCount,
            int fromIndex) {
        if (fromIndex >= sourceCount) {
            return (targetCount == 0 ? sourceCount : -1);
        }
        if (fromIndex < 0) {
            fromIndex = 0;
        }
        if (targetCount == 0) {
            return fromIndex;
        }

        char first = target[targetOffset];
        int max = sourceOffset + (sourceCount - targetCount);

        for (int i = sourceOffset + fromIndex; i <= max; i++) {
            /* Look for first character. */
            if (source[i] != first) {
                while (++i <= max && source[i] != first);
            }

            /* Found first character, now look at the rest of v2 */
            if (i <= max) {
                int j = i + 1;
                int end = j + targetCount - 1;
                for (int k = targetOffset + 1; j < end && source[j]
                        == target[k]; j++, k++);

                if (j == end) {
                    /* Found whole string. */
                    return i - sourceOffset;
                }
            }
        }
        return -1;
    }

這個是上面indexOf的一個過載,主要是實現找到某個子串在當前字串的起始位置,若沒找到,則返回-1。

大致說下這裡的實現思路:先是進行一系列的初始判定,比如子串長度不能大於當前字串。然後在當前字串中找到子串的第一個字元的位置 i ,從這個位置開始,和子串每一個字元比較。若完全匹配,則返回結果,如果在這個過程中,某個字元不匹配,則從 i+1 的位置開始繼續尋找子串第一個字元的位置,後繼續比較。

    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);
    }

這個方法可以返回字串中一個子串,看最後一行可以發現,其實就是指定頭尾,然後構造一個新的字串。

    public String concat(String str) {
        int otherLen = str.length();
        if (otherLen == 0) {
            return this;
        }
        int len = value.length;
        char buf[] = Arrays.copyOf(value, len + otherLen);
        str.getChars(buf, len);
        return new String(buf, true);
    }

concat的作用是將str拼接到當前字串後面,通過程式碼也可以看出其實就是建一個新的字串。

    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;
    }

替換操作,主要是將原來字串中的oldChar全部替換成newChar。看這裡實現,主要是先找到第一個所要替換的字串的位置 i ,將i之前的字元直接複製到一個新char陣列。然後從 i 開始再對每一個字元進行判斷是不是所要替換的字元。

    public boolean matches(String regex) {
        return Pattern.matches(regex, this);
    }

    public String replaceFirst(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceFirst(replacement);
    }

    public String replaceAll(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }

    public String replace(CharSequence target, CharSequence replacement) {
        return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
                this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
    }

這幾個方法都是使用了正則的方式來進行處理的。包括最後一個雖然引數不用提供正則規則,但內部其實也是使用了Pattern類的正則操作。

public String[] split(String regex, int limit) {
        /* fastpath if the regex is a
         (1)one-char String and this character is not one of the
            RegEx's meta characters ".$|()[{^?*+\\", or
         (2)two-char String and the first char is the backslash and
            the second is not the ascii digit or ascii letter.
         */
        char ch = 0;
        if (((regex.value.length == 1 &&
             ".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1) ||
             (regex.length() == 2 &&
              regex.charAt(0) == '\\' &&
              (((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
              ((ch-'a')|('z'-ch)) < 0 &&
              ((ch-'A')|('Z'-ch)) < 0)) &&
            (ch < Character.MIN_HIGH_SURROGATE ||
             ch > Character.MAX_LOW_SURROGATE))
        {
            int off = 0;
            int next = 0;
            boolean limited = limit > 0;
            ArrayList<String> list = new ArrayList<>();
            while ((next = indexOf(ch, off)) != -1) {
                if (!limited || list.size() < limit - 1) {
                    list.add(substring(off, next));
                    off = next + 1;
                } else {    // last one
                    //assert (list.size() == limit - 1);
                    list.add(substring(off, value.length));
                    off = value.length;
                    break;
                }
            }
            // If no match was found, return this
            if (off == 0)
                return new String[]{this};

            // Add remaining segment
            if (!limited || list.size() < limit)
                list.add(substring(off, value.length));

            // Construct result
            int resultSize = list.size();
            if (limit == 0) {
                while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
                    resultSize--;
                }
            }
            String[] result = new String[resultSize];
            return list.subList(0, resultSize).toArray(result);
        }
        return Pattern.compile(regex).split(this, limit);
    }

 這個方法看起來比較複雜,但其實我們一般都不會用到那一大串的內容,一般我們用到最後那一句return Pattern.compile(regex).split(this, limit); 即同樣是使用Pattern的正則方式去解析並拆分成字串陣列。

那麼進到那些複雜的程式碼裡面需要什麼條件呢,看那個if:

1. 如果regex只有一位,且不為列出的特殊字元;

2.如regex有兩位,第一位為轉義字元且第二位不是數字或字母,“|”表示或,即只要ch小於0或者大於9任一成立,小於a或者大於z任一成立,小於A或大於Z任一成立

3.第三個是不屬於utf-16之間的字元

其中的關係為( (1 || 2) && 3 ),光看第三點就知道這是為了應對特殊情況的。其實也就是使用一個ArrayList<String>存放每一段找到分割點的字串,不斷迴圈。

    public String trim() {
        int len = value.length;
        int st = 0;
        char[] val = value;    /* avoid getfield opcode */

        while ((st < len) && (val[st] <= ' ')) {
            st++;
        }
        while ((st < len) && (val[len - 1] <= ' ')) {
            len--;
        }
        return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
    }

這個函式平時用的應該比較多,刪除字串前後的空格,原理是通過找出前後第一個不是空格的字串,返回原字串的該子串。

 

總結:

在String中,其實最底層的實現就是通過一個final char value[] 來儲存String字串的,抓住這一點,其實很多設計方法,方法的實現方式就顯而易見了。

 

---
推薦閱讀:
大資料儲存的進化史 --從 RAID 到 Hdfs
貝葉斯分類演算法例項 --根據姓名推測男女
從分治演算法到 MapReduce

相關文章