Java-- String源碼分析

本篇博文基於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 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字符串的,抓住這一點,其實很多設計方法,方法的實現方式就顯而易見了。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章