深度分析:面試騰訊,阿里面試官都喜歡問的String源碼,看完你學會了嗎?

前言

最近花了兩天時間,整理了一下String的源碼。這個整理並不全面可是也涵蓋了大部分Spring源碼中的方法。後續若是有時間還會將剩餘的未整理的方法更新到這篇文章中。方便之後的複習和麪試使用。若是文章中有地方有問題還請指出。java

簡述

字符串普遍應用 在 Java 編程中,在 Java 中字符串屬於對象,Java 提供了String 類來建立和操做字符串。字符串緩衝區支持可變字符串。由於String對象是不可變的,所以能夠共享它們。面試

String類表明字符串,Java程序中的全部字符串字面值如"abc"都是這個類的實例對象。String 類是不可改變的,因此你一旦建立了 String 對象,那它的值就沒法改變了。若是須要對字符串作不少修改,那麼應該選擇使用StringBuilder或者StringBuffer。正則表達式

最簡單的建立字符串的方式:String qc = "qiu chan"編譯器會使用該值建立一個 對象。咱們也可使用關鍵字New建立String對象。
String類型的常量池比較特殊。它的主要使用方法有兩種:
直接使用雙引號聲明出來的String對象會直接存儲在常量池中。
若是不是用雙引號聲明的String對象,可使用String提供的intern方法。intern 方法會從字符串常量池中查詢當前字符串是否存在,若不存在就會將當前字符串放入常量池中。編程

繼承/實現關係

public final class String
    implements java.io.Serializable, Comparable<String>, CharSequence {
    // 省略
}

String是final修飾的不可以被繼承和修改。設計模式

源碼

String的底層使用的是char數組用於存儲。

private final char value[];

緩存字符串的哈希碼默認值爲0

private int hash;

無參數構造函數

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

解析:初始化一個新建立的String對象,使其表明一個空字符序列。 注意,因爲String是不可變的,因此不須要使用這個構造函數。數組

參數爲字符串的構造函數

public String(String original) {
    this.value = original.value;
    this.hash = original.hash;
}

解析:初始化一個新建立的String對象,使其表明與參數相同的字符序列。換句話說,新建立的字符串是參數字符串的副本。除非須要參數字符串的顯式拷貝,不然不須要使用這個構造函數,由於String是不可變的。緩存

參數爲char數組的構造函數

public String(char value[]) {
        this.value = Arrays.copyOf(value, value.length);
}

解析:分配一個新的String,使其表明當前字符數組參數中包含的字符序列。使用Arrays.copyOf方法進行字符數組的內容被複制。字符數組的後續修改不會影響新建立的字符串。安全

參數爲char數組而且帶有偏移量的構造方法

// value[]:做爲字符源的數組,offset:偏移量、下標從0開始而且包括offset,count:從數組中取到的元素的個數。
public String(char value[], int offset, int count) {
  // 若是偏移量小於0拋出IndexOutOfBoundsException異常
    if (offset < 0) {
        throw new StringIndexOutOfBoundsException(offset);
    }
  // 判斷要取的元素的個數是否小於等於0
    if (count <= 0) {
    // 要取的元素的個數小於0,拋出IndexOutOfBoundsException異常
        if (count < 0) {
            throw new StringIndexOutOfBoundsException(count);
        }
    // 在要取的元素的個數等於0的狀況下,判斷偏移量是否小於等於數組的長度
        if (offset <= value.length) {
      // 偏移量小於等於數組的長度,返回一個空字符串數組的形式
            this.value = "".value;
            return;
        }
    }
    // 若是偏移量的值大於數組的長度減去取元素的個數拋出IndexOutOfBoundsException異常
    if (offset > value.length - count) {
        throw new StringIndexOutOfBoundsException(offset + count);
    }
  // 複製元素
    this.value = Arrays.copyOfRange(value, offset, offset+count);
}

解析:分配一個新的Sting,來源於給定的char數組中的字符。offset參數是子數組中第一個字符的索引,count參數指定子數組的長度。子數組被被複制之後,對字符數組的修改不會影響新建立的字符串。多線程

參數爲StringBuffer的構造方法

public String(StringBuffer buffer) {
  // 這裏對StringBuffer進行了加鎖,而後再進行拷貝操做。這裏對其進行加鎖正是爲了保證在多線程環境下只能有一個線程去操做StringBuffer對象。
    synchronized(buffer) {
        this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
    }
}

解析:分配一個新的字符串,該字符串包含當前字符串緩衝區參數中包含的字符序列。Arrays.copyOf方法進行字符串緩衝區中內容的複製。這裏對StringBuffer進行了加鎖,而後再進行拷貝操做。這裏對其進行加鎖正是爲了保證在多線程環境下只能有一個線程去操做StringBuffer對象。app

參數爲StringBuilder的構造方法

public String(StringBuilder builder) {
    this.value = Arrays.copyOf(builder.getValue(), builder.length());
}

解析:參數是StringBuilder,這個是線程不安全的,可是性能相對於StringBuffer有很大的提高,源碼的註釋中說經過toString方法從字符串構建器中獲取字符串可能會運行得更快,一般是首選。

length方法

public boolean isEmpty() {
    // 底層的char數組的長度是否爲0進行判斷
        return value.length == 0;
}

//舉例
@Test
public void test_string_isEmpty(){
    System.out.println(" ".isEmpty());// false
  System.out.println("".isEmpty());// true
}

解析:返回此字符串的長度。查看源碼發現,這個value是一個char數組,本質獲取的是字符串對應的char數組的長度。

isEmpty方法

public boolean isEmpty() {
    // 底層的char數組的長度是否爲0進行判斷
        return value.length == 0;
}

//舉例
@Test
public void test_string_isEmpty(){
    System.out.println(" ".isEmpty());// false
  System.out.println("".isEmpty());// true
}

解析:判斷給定的字符串是否爲空,底層實現是根據char數組的長度是否爲0進行判斷。

charAt方法

public char charAt(int index) {
  // 給定的索引小於0或者給定的索引大於這個字符串對應的char數組的長度拋出角標越界異常
    if ((index < 0) || (index >= value.length)) {
        throw new StringIndexOutOfBoundsException(index);
    }
  // 獲取當前的指定位置的char字符
    return value[index];
}

解析:根據給定的索引獲取當前的指定位置的char字符。若是給定的索引否小於0,或者給定的索引是大於這個字符串對應的char數組的長度拋出角標越界異常。index是從0開始到length-1結束。序列的第一個char值在索引0處,下一個在索引1處,依此類推,與數組索引同樣。

getChars方法

// srcBegin:要複製的字符串中第一個字符的索引【包含】。srcEnd:要複製的字符串中最後一個字符以後的索引【不包含】。dst[]:目標數組。dstBegin:目標數組中的起始偏移量。
public void getChars(int srcBegin, int srcEnd, char dst[], int dstBegin) {
  // 校驗起始索引小於0拋出角標越界異常
    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);
}

// 案例
@Test
public void test_string_codePointAt(){
  // 原始字符串
    String h = "ahelloworld";
  // 目標char數組
    char[] data = new char[4];
  // 執行拷貝
    h.getChars(2, 6, data, 0);
    System.out.println(data);
}

解析:將字符串中的字符複製到目標字符數組中。索引包含srcBegin,不包含srcEnd。

equals方法

// anObject:與此String進行比較的對象。
public boolean equals(Object anObject) {
  // 引用相同直接返回true
    if (this == anObject) {
        return true;
    }
    // 判斷給定的對象是不是String類型的
    if (anObject instanceof String) {
    // 給定的對象是字符串類型的轉換爲字符串類型
        String anotherString = (String)anObject;
    // 獲取當前字符串的長度
        int n = value.length;
    // 判斷給定字符串的長度是否等於當前字符串的長度
        if (n == anotherString.value.length) {
      // v1[]表明當前字符串對應的char數組
            char v1[] = value;
      // v2[]表明給定的字符串對應的char數組
            char v2[] = anotherString.value;
      // 遍歷原始char數組,而且與給定的字符串對應的數組進行比較
            int i = 0;
            while (n-- != 0) {
                if (v1[i] != v2[i])
          // 任意一個位置上不相等返回false
                    return false;
                i++;
            }
      // 都相等返回true
            return true;
        }
    }
  // 不是String類型,或者長度不一致返回false
    return false;
}

解析:這個方法重寫了Object中的equals方法。方法中的將此字符串與指定對象進行比較。接下來附贈一個手寫的String字符串equals方法。

手寫equals方法

private boolean mineEquals(String srcObject, Object anObject){
  // 比較引用是否相同
    if (srcObject == anObject){
        return true;
    }
  // 引用不相同比較內容
    if (anObject instanceof String){
        String ans = (String) anObject;
        char[] srcChar = srcObject.toCharArray();
        char[] anChar = ans.toCharArray();
        int n = srcChar.length;
        if (n == anChar.length){
            int i = 0;
            while (n-- != 0){
                if (srcChar[i] != anChar[i])
                    return false;
                i++;
            }
            return true;
        }
    }
    return false;
}

// 測試咱們本身寫的equals方法
    @Test
    public void test_string_mine(){
        String s = new String("aaa");
    // 走的是引用的比較
        System.out.println(s.equals(s));// true 
        boolean b = mineEquals(s, s);
        System.out.println(b);// true
    }

equalsIgnoreCase方法

public boolean equalsIgnoreCase(String anotherString) {
  // 引用相同返回true。引用不相同進行長度、各個位置上的char是否相同
    return (this == anotherString) ? true
            : (anotherString != null)
            && (anotherString.value.length == value.length)
            && regionMatches(true, 0, anotherString, 0, value.length);
}

解析:將此字符串與另外一個字符串進行比較,而忽略大小寫注意事項。regionMatches方法的源碼頗有趣的,源碼裏面有一個while循環,先進行未忽略大小的判斷,而後進行忽略大小的判斷,在忽略大小的判斷中,先進行的是大寫的轉換進行比較,可是可能會失敗【這種字體Georgian alphabet】。因此在大寫轉換之後的比較失敗,進行一次小寫的轉換比較。

startsWith方法

// 判斷是否以指定的前綴開頭
public boolean startsWith(String prefix) {
  // 0表明從開頭進行尋找
  return startsWith(prefix, 0);
}

endsWith方法

// 判斷是否以指定的前綴結尾
public boolean endsWith(String suffix) {
  // 從【value.length - suffix.value.length】開始尋找,這個方法調用的仍是startsWith方法
  return startsWith(suffix, value.length - suffix.value.length);
}

startsWith和endsWith最終的實現方法

// prefix: 測試此字符串是否以指定的前綴開頭。toffset: 從哪裏開始尋找這個字符串。
public boolean startsWith(String prefix, int toffset) {
  // 原始的字符串對應的char[]
    char ta[] = value;
  // 開始尋找的位置
    int to = toffset;
  // 獲取指定的字符串對應的char[]
    char pa[] = prefix.value;
    int po = 0;
  // 獲取指定的字符串對應的char[]長度
    int pc = prefix.value.length;
    // 開始尋找的位置小於0,或者起始位置大於要查找的長度【value.length - pc】返回false。
    if ((toffset < 0) || (toffset > value.length - pc)) {
        return false;
    }
  // 比較給定的字符串的char[]裏的每一個元素是否跟原始的字符串對應的char數組的元素相同
    while (--pc >= 0) {
        if (ta[to++] != pa[po++]) {
      // 有一個char不相同返回false
            return false;
        }
    }
  // 相同返回true
    return true;
}

substring方法

// 返回一個字符串,該字符串是該字符串的子字符串。beginIndex開始截取的索引【包含】。
public String substring(int beginIndex) {
  // 校驗指定的索引,小於0拋出角標越界
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
  // 子字符串的長度
    int subLen = value.length - beginIndex;
  // 子字符串的長度小於0拋出角標越界
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
  // 開始位置爲0,返回當前字符串,不爲0,建立一個新的子字符串對象並返回
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

解析:返回一個字符串,該字符串是該字符串的子字符串。子字符串以指定索引處的字符開頭【包含】,而且擴展到該字符串的末尾。

substring方法

// 返回一個字符串,該字符串是該字符串的子字符串。beginIndex開始截取的索引【包含】。
public String substring(int beginIndex) {
  // 校驗指定的索引,小於0拋出角標越界
    if (beginIndex < 0) {
        throw new StringIndexOutOfBoundsException(beginIndex);
    }
  // 子字符串的長度
    int subLen = value.length - beginIndex;
  // 子字符串的長度小於0拋出角標越界
    if (subLen < 0) {
        throw new StringIndexOutOfBoundsException(subLen);
    }
  // 開始位置爲0,返回當前字符串,不爲0,建立一個新的子字符串對象並返回
    return (beginIndex == 0) ? this : new String(value, beginIndex, subLen);
}

解析:返回一個字符串,該字符串是該字符串的子字符串。子字符串從指定的beginIndex開始【包含】,而且擴展到索引endIndex-1處的字符【不包含】。

concat方法

public String concat(String str) {
  // 獲取給定的字符串的長度
    int otherLen = str.length();
  // 長度爲0,直接返回當前的字符串
    if (otherLen == 0) {
        return this;
    }
  // 獲取當前字符串的長度
    int len = value.length;
  // 構建一個新的長度爲len + otherLen的字符數組,而且將原始的數據放到這個數組
    char buf[] = Arrays.copyOf(value, len + otherLen);
  // 這個底層調用是System.arraycopy這個方法的處理是使用c語言寫的
    str.getChars(buf, len);
    return new String(buf, true);
}

將指定的字符串鏈接到該字符串的末尾。字符串拼接。

format方法

// 使用指定的格式字符串和參數返回格式化的字符串。
public static String format(String format, Object... args) {
    return new Formatter().format(format, args).toString();
}

// 案例,這裏是使用%s替換後面的如"-a-"
@Test
public void test_start(){
    System.out.println(String.format("ha %s hh %s a %s h", "-a-", "-b-", "-c-"));
}

trim方法

public String trim() {
  // 指定字符串的長度
    int len = value.length;
  // 定義一個開始位置的索引0
    int st = 0;
  // 定義一個char[] val,用於避免使用getfiled操做碼,這個能夠寫段代碼反編譯一下看看
    char[] val = value;
  // 對於字符串的開頭進行去除空格,並記錄這個索引
    while ((st < len) && (val[st] <= ' ')) {
        st++;
    }
  // 對於字符串的尾部進行去除空格,也記錄這個索引,這個索引就是去除尾部空格後的索引
    while ((st < len) && (val[len - 1] <= ' ')) {
        len--;
    }
  // 根據上面記錄的長度判斷是否要截取字符串
    return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}

返回一個字符串,其值就是這個字符串,並去掉任何首部和尾部的空白。

join方法

// 返回一個新的String,該字符串由給定的分隔符和要鏈接的元素組成。delimiter:分隔每一個元素的分隔符。elements:鏈接在一塊兒的元素。
public static String join(CharSequence delimiter, CharSequence... elements) {
  // delimiter和elements爲空拋出空指針異常,null會被攔截,""不會被攔截
    Objects.requireNonNull(delimiter);
    Objects.requireNonNull(elements);
    // 
    StringJoiner joiner = new StringJoiner(delimiter);
  // 遍歷給定的要拼接的元素,拼接的元素容許爲null
    for (CharSequence cs: elements) {
    // 執行拼接方法
        joiner.add(cs);
    }
    return joiner.toString();
}

// 拼接方法
public StringJoiner add(CharSequence newElement) {
  // prepareBuilder()方法首次調用會建立StringBuilder對象,後面再調用會執行拼接分隔符
    prepareBuilder().append(newElement);
    return this;
}

// 未進行拼接建立StringBuilder對象,已經拼接之後value != null執行拼接分隔符
private StringBuilder prepareBuilder() {
  // 判斷拼接的value是否爲空
    if (value != null) {
    // 不爲空執行拼接分隔符
        value.append(delimiter);
    } else {
    // 最開始使用拼接的時候,調用這個方法建立一個空的StringBuilder對象,只調一次
        value = new StringBuilder().append(prefix);
    }
    return value;
}

// 上面是調用的這個拼接元素方法
@Override
public StringBuilder append(CharSequence s) {
  // 這裏啥都沒處理,調用的是父類的append方法,設計模式爲建造者模式
    super.append(s);
    return this;
}

// 上面的prepareBuilder方法是拼接分隔符,這個方法是將分隔符和給定的元素拼接的方法
@Override
public AbstractStringBuilder append(CharSequence s) {
  // 如下3個判斷根據類型和是否爲空進行區別拼接
    if (s == null)
        return appendNull();
    if (s instanceof String)
        return this.append((String)s);
    if (s instanceof AbstractStringBuilder)
        return this.append((AbstractStringBuilder)s);
  // 拼接
    return this.append(s, 0, s.length());
}

將給定的字符串以給定的分割符分割並返回分隔後的字符串。

replace方法

// target:要被替換的目標字符串。 replacement:替換的字符串
public String replace(CharSequence target, CharSequence replacement) {
    return Pattern.compile(target.toString(), Pattern.LITERAL).matcher(
            this).replaceAll(Matcher.quoteReplacement(replacement.toString()));
}

解析:用指定的字符串替換這個字符串中與之匹配的每一個子字符串。替換從字符串的開頭到結尾,例如,在字符串 "aaa "中用 "b "替換 "aa "將致使 "ba "而不是 「ab」。

replaceAll方法

// regex:這個支持正則表達式,也能夠是要被替換的目標字符串。
public String replaceAll(String regex, String replacement) {
    return Pattern.compile(regex).matcher(this).replaceAll(replacement);
}

問題:replace和replaceAll方法的區別是啥?
replaceAll支持正則表達式。

針對char的replace方法

// oldChar:要被替換的字符,newChar:替換的字符
public String replace(char oldChar, char newChar) {
  // oldChar不等於newChar
    if (oldChar != newChar) {
    // 當前字符串的長度
        int len = value.length;
    // 這個用於下面的while循環裏的條件比較,val[i]中的i是從0開始的
        int i = -1;
    // 定義一個char[] val,用於避免使用getfiled操做碼,這個能夠寫段代碼反編譯一下看看
        char[] val = value; /* avoid getfield opcode */
    // 這個用於記錄這個i的值,而且判斷是否有要替換的,這個循環有利於性能的提高
        while (++i < len) {
      // val[i]中的i是從0開始的
            if (val[i] == oldChar) {
        // 有要替換的直接跳出循環
                break;
            }
        }
    // 上面的while循環中若是有要替換的i確定小於len,若是沒有下面這個判斷就不會執行
        if (i < len) {
      // 能進到這個循環確定是有要替換的,建立一個長度爲len的char數組
            char buf[] = new char[len];
      // 上面的i是記錄第一個能夠替換的char的索引,下面這個循環是將這個i索引前的不須要被替換的填充到buf[]數組中
            for (int j = 0; j < i; j++) {
        // 填充buf[]數組
                buf[j] = val[j];
            }
      // 從能夠替換的索引i開始將剩餘的字符一個一個填充到 buf[]中
            while (i < len) {
        // 獲取要被替換的字符
                char c = val[i];
        // 判斷這個字符是否真的須要替換,c == oldChar成立就替換,不然不替換
                buf[i] = (c == oldChar) ? newChar : c;
                i++;
            }
      // 返回替換後的字符串
            return new String(buf, true);
        }
    }
  // oldChar等於newChar直接返回當前字符串
    return this;
}

案例

@Test
public void test_matches(){
    String a = "adddfdefe";
    System.out.println(a.replace('d', 'b'));// abbbfbefe
}

仿寫replace方法參數針對char

仿寫

// 和源碼給的惟一不一樣的是參數傳遞,其餘的都和源碼同樣,本身寫一遍能夠加深記憶和借鑑編程思
public String replace(String source, char oldChar, char newChar) {
  char[] value = source.toCharArray();
    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);
        }
    }
    return new String(value);
}

intern方法

public native String intern();

這是一個native方法。調用String#intern方法時,若是池中已經包含一個由equals方法肯定的等於此String對象的字符串,則返回來自池的字符串。不然,將此String對象添加到池中,並返回這個String的引用。

相關文章
相關標籤/搜索