Java-- String源碼分析

  版權聲明:本文爲博主原創文章,未經博主容許不得轉載html

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

  首先,將一個類分爲幾個部分,分別是類定義(繼承,實現接口等),全局變量,方法,內部類等等,再分別對這幾個部分進行說明,這樣到最後類的全貌也就比較直觀了。git

   一:實現接口。算法

  

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的緩存無疑能優化這樣的操做。ui

  最後,這個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

相關文章
相關標籤/搜索