啃碎String源碼

前言

最近打算開始來讀一下JDK的部分源碼,此次先從咱們平時用的最多的String類(JDK1.8)開始,本文主要會對如下幾個方法的源碼進行分析:java

  • equals
  • hashCode
  • equalsIgnoreCase
  • indexOf
  • startsWith
  • concat
  • substring
  • split
  • trim
  • compareTo

若是有不對的地方請多多指教,那麼開始進入正文。git

源碼剖析

首先看下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

能夠看到,value[]是存儲String的內容的,即當使用String str = "abc";的時候,本質上,"abc"是存儲在一個char類型的數組中的。性能

hash是String實例化的hashcode的一個緩存。由於String常常被用於比較,好比在HashMap中。若是每次進行比較都從新計算hashcode的值的話,那無疑是比較麻煩的,而保存一個hashcode的緩存無疑能優化這樣的操做。優化

image

注意:這邊有一個須要注意的點就是能夠看到value數組是用final修飾的,也就是說不能再去指向其它的數組,可是數組的內容是能夠改變的,之因此說String不可變是由於其提供的API(好比replace等方法)都會給咱們返回一個新的String對象,而且咱們沒法去改變數組的內容,這纔是它不可變的緣由。ui

equals

equals() 方法用於判斷 Number 對象與方法的參數進是否相等

String類重寫了父類Object的equals方法,來看看源碼實現:

image.png

  1. 首先會判斷兩個對象是否指向同一個地址,若是是的話則是同一個對象,直接返回true
  2. 接着會使用instanceof判斷目標對象是不是String類型或其子類的實例,若是不是的話則返回false
  3. 接着會比較兩個String對象的char數組長度是否一致,若是不一致則返回false
  4. 最後迭代依次比較兩個char數組是否相等

hashCode

hashCode() 方法用於返回字符串的哈希碼

Hash算法就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。在Java中,全部的對象都有一個int hashCode()方法,用於返回hash碼。

根據官方文檔的定義:Object.hashCode() 函數用於這個函數用於將一個對象轉換爲其十六進制的地址。根據定義,若是2個對象相同,則其hash碼也應該相同。若是重寫了 equals() 方法,則原 hashCode() 方法也一併失效,因此也必需重寫 hashCode() 方法。

image.png

按照上面源碼舉例說明:

String msg = "abcd"; 
System.out.println(msg.hashCode());

此時value = {'a','b','c','d'}  所以for循環會執行4次

第一次:h = 31*0 + a = 97 
第二次:h = 31*97 + b = 3105 
第三次:h = 31*3105 + c = 96354 
第四次:h = 31*96354 + d = 2987074 

由以上代碼計算能夠算出 msg 的hashcode = 2987074

image.png

在源碼的hashcode的註釋中還提供了一個多項式計算方式:

s[0] 31^(n-1) + s[1]31^(n-2) + ... + s[n-1]

另外,咱們能夠看到,計算中使用了31這個質數做爲權進行計算。能夠儘量保證數據分佈更分散

在《Effective Java》中有說起:

之因此選擇31,是由於它是一個奇素數。若是乘數是偶數,而且乘法溢出的話,信息就會丟失,由於與2相乘等價於移位運算。使用素數的好處並不明顯,可是習慣上都使用素數來計算散列結果。31有個很好的特性。即用移位和減法來代替乘法,能夠獲得更好的性能:31 * i == (i << 5) - i。現代的VM能夠自動完成這種優化。
/** Cache the hash code for the string */
private int hash; // Default to 0

並且如上面所示,當計算完以後會用一個變量hash把哈希值保存起來,下一次再獲取的時候就不用換從新計算了,正是由於String的不可變性保證了hash值的惟一。

equalsIgnoreCase

equalsIgnoreCase() 方法用於將字符串與指定的對象比較,不考慮大小寫

接下來來看看源碼實現:

image.png

來看看核心方法

image.png

相信看了上圖的介紹就能看懂了,這裏就很少說了。

indexOf

查找指定字符或字符串在字符串中第一次出現地方的索引,未找到的狀況返回 -1
String str = "wugui";
System.out.println(str.indexOf("g"));
輸出結果:2
public int indexOf(String str) {
   return indexOf(str, 0);
}

public int indexOf(String str, int fromIndex) {
   return indexOf(value, 0, value.length,str.value, 0, str.value.length, fromIndex);
}

接下來是咱們的核心方法,先看下各個參數的介紹

/*
 * @param   source       被搜索的字符
 * @param   sourceOffset 原字符串偏移量
 * @param   sourceCount  原字符串大小
 * @param   target       要搜索的字符
 * @param   targetOffset 目標字符串偏移量
 * @param   targetCount  目標字符串大小
 * @param   fromIndex    開始搜索的位置
*/
static int indexOf(char[] source, int sourceOffset, int sourceCount,
        char[] target, int targetOffset, int targetCount,
        int fromIndex) {
   ......
}

下面是代碼的邏輯步驟

image.png

indexOf的源碼裏面我認爲邊界條件是寫的比較好的

咱們這裏假設

String str = "wugui";
str.indexOf("ug");

在上圖第2步,計算出max做爲下面循環的邊界條件

//找到第一個匹配的字符索引
if (source[i] != first) {
   while (++i <= max && source[i] != first);
}

咱們計算出 max=3,也就是說咱們在使用迭代搜索第一個字符的時候只須要遍歷到索引爲3的位置,就能夠了,由於索引第4位也就是最後一位 'i',就是匹配到了第一個字符也是無心義的,由於咱們要搜索的目標自字符是2位字符,同第5步計算出end做爲邊界條件也是一樣的道理。

有了indexOf方法以後,那有些方法就能夠借用它來實現了,好比contains方法,源碼以下:

public boolean contains(CharSequence s) {
   return indexOf(s.toString()) > -1;
}

只須要調用根據indexOf的返回值來判斷是否包含目標字符串就能夠了。

startsWith

startsWith() 方法用於檢查字符串是不是以指定子字符串開頭,若是是則返回 True,不然返回 False
String str = "wugui";
System.out.println(str.startsWith("wu"));
輸出結果:true
public boolean startsWith(String prefix) {
    return startsWith(prefix, 0);
}

public boolean startsWith(String prefix, int toffset) {
    ......
}

image.png

既然有了startsWith方法,那麼endsWith就很容易實現了,以下:

image.png

只要修改一下參數,設置偏移量就能夠了。

concat

用於將指定的字符串參數鏈接到字符串上
String str1 = "wu";
String str2 = "gui";
System.out.println(str1.concat(str2));
輸出結果:wugui

image.png

能夠看到是使用了Arrays.copyOf方法來生成新數組

char buf[] = Arrays.copyOf(value, len + otherLen);

咱們來看看其實現:

image.png

能夠看到主要使用system.arraycopy方法,點進去看一下實現:

image.png

若是看不到的話咱們這裏舉個例子:

好比 :咱們有一個數組數據

byte[] srcBytes =  new byte[]{2,4,0,0,0,0,0,10,15,50};//原數組
byte[] destBytes = new byte[5]; //目標數組

咱們使用System.arraycopy進行復制

System.arrayCopy(srcBytes,0,destBytes ,0,5)

上面這段代碼就是 : 建立一個一維空數組,數組的總長度爲 12位,而後將srcBytes源數組中 從0位 到 第5位之間的數值 copy 到 destBytes目標數組中,在目標數組的第0位開始放置,
那麼這行代碼的運行效果應該是 2,4,0,0,0,

調用完Arrays.copy返回新數組方法後,會調用str.getChars(buf, len)來拼接字符串,咱們看下其實現:

image.png

能夠看到其實也是調用了System.arraycopy來實現,這裏再也不細說。

最後一步就是把新數組賦值給value

return new String(buf, true);

image.png

substring

提取字符串中介於兩個指定下標之間的字符
String str = "wugui";
System.out.println(str.substring(1, 3));//包括索引1不包括索引3
輸出結果:ug

image.png

來看看 new String(value, beginIndex, subLen) 的實現

image.png

看看Arrays.copyOfRange是如何實現的:

image.png

能夠看到其實仍是使用的System.arraycopy來實現,上面已經介紹過了,這裏再也不細說。

split

根據匹配給定的正則表達式來拆分字符串

先來看看用法:

public String[] split(String regex, int limit)

第一個參數regex表示正則表達式,第二個參數limit是分割的子字符串個數

String str = "a:b:c:d";
String[] split = str.split(":");

當沒有傳limit參數默認調用的是split(String regex, 0)

上面的輸出爲:[a, b, c, d]

若是把limit參數換成2那麼輸出結果變成:[a, b:c:d],能夠看出limit意味着分割後的子字符串個數。

看看整個源碼:

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;
        //若是regex只有一位,且不爲列出的特殊字符; 
        //若是regex有兩位,第一位爲轉義字符且第二位不是數字或字母 
        //第三個是和編碼有關,就是不屬於utf-16之間的字符
        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);
    }

接下來咱們一步步來分析:

image.png

能夠看到有三個條件:

  1. 若是regex只有一位,且不爲列出的特殊字符
  2. 若是regex有兩位,第一位爲轉義字符且第二位不是數字或字母
  3. 第三個是和編碼有關,就是不屬於utf-16之間的字符

只有知足上面三個條件才能進入下一步:

第一次分割時,使用offnextoff指向每次分割的起始位置,next指向分隔符的下標,完成一次分割後更新off的值,當list的大小等於limit-1時,直接添加剩下子字符串,具體看下源碼:

image.png

最後就是對子字符串進行處理:

image.png

我的以爲這部分源碼仍是比較難的,有興趣的同窗能夠再去研究一下。

trim

刪除字符串的頭尾空白符
String str = "  wugui         ";
System.out.println(str.trim());
輸出:wugui

image.png

這部分仍是比較簡單的,這裏再也不細說。

compareTo

比較兩個字符
String a = "a";
String b = "b";
System.out.println(a.compareTo(b));
輸出:-1

看看源碼:

image.png

總結

有關String的源碼暫時分析到這裏,其它的源碼感興趣的小夥伴能夠按本身去研究一下,接下來可能會得寫幾篇文章來介紹一下Java中的包裝類,敬請期待!

image

相關文章
相關標籤/搜索