String源碼分析
- 一.概述
- 二.源碼解析
- 1.重要的全局變量
- 2.常用的構造方法
- 3. length方法
- 4.isEmpty方法
- 5. charAt方法
- 6.getBytes方法
- 7.equels方法
- 8. compareTo方法
- 9. compareToIgnoreCase方法
- 10. regionMatches方法
- 11. equalsIgnoreCase方法
- 12. startsWith方法
- 13. endsWith方法
- 14. hashCode方法
- 15. indexOf方法
- 16. substring方法
- 17. replace方法
- 18.contains方法
- 19.split方法
- 20. trim方法
- 21. toCharArray方法
- 22. valueOf方法
一.概述
String類是Java中的用於創建和處理字符串的類。String類對象一旦創建,不能修改,即不可變。
String.java中的相關代碼:
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
…
}
1.String類被final關鍵字修飾,因此不可以被繼承。
2.實現了java.io.Serializable接口,可以進行序列化。
3.實現Comparable接口,可以進行字符串的比較。
4.實現了CharSequence接口,可以將字符串當成一個一個的字符處理。
二.源碼解析
1.重要的全局變量
String.java中的相關代碼:
private final char value[]; // 用於保存字符串
private int hash; // 字符串的哈希值,默認爲0
private static final long serialVersionUID = -6849794470754667710L; // 序列化UID
// 用於比較兩個字符串誰包含的範圍更大,比較時忽略字符大小寫
public static final Comparator<String> CASE_INSENSITIVE_ORDER
= new CaseInsensitiveComparator();
2.常用的構造方法
1)無參數
String.java中的相關代碼:
public String() {
this.value = "".value;
}
無參數時默認的字符串爲””。
2)參數爲String
String.java中的相關代碼:
public String(String original) {
this.value = original.value;
this.hash = original.hash;
}
保存參數的值和參數的哈希值的引用,因爲original是不可變的,所以不需要再創建一個和original一模一樣的新對象。
3)參數爲char[]
String.java中的相關代碼:
public String(char value[]) {
this.value = Arrays.copyOf(value, value.length);
}
根據數組的內容創建一個新對象,保存新創建的數組對象,因爲字符數組的內容是可變的,所以需要創建新的對象。
4)參數爲byte[]
String.java中的相關代碼:
public String(byte bytes[]) {
this(bytes, 0, bytes.length);
}
調用了重載的構造方法,從0開始,長度爲字節數組的長度。
String.java中的相關代碼:
public String(byte bytes[], int offset, int length) {
// 對邊界進行檢查,詳解在a)處
checkBounds(bytes, offset, length);
// 對字節數組進行解碼,並保存結果
this.value = StringCoding.decode(bytes, offset, length);
}
a)checkBounds方法
private static void checkBounds(byte[] bytes, int offset, int length) {
// 長度必須大於零
if (length < 0)
throw new StringIndexOutOfBoundsException(length);
// 起始位置必須大於零
if (offset < 0)
throw new StringIndexOutOfBoundsException(offset);
// 指定的長度必須小於數組的總長度
// 採用減法的方式,可以防止當offset+length大於最大值-1>>>1而報錯
if (offset > bytes.length - length)
throw new StringIndexOutOfBoundsException(offset + length);
}
5)參數爲StringBuffer
String.java中的相關代碼:
public String(StringBuffer buffer) {
// 同步鎖,鎖對象爲buffer
synchronized(buffer) {
this.value = Arrays.copyOf(buffer.getValue(), buffer.length());
}
}
根據buffer中保存的字符串的值創建一個新對象並保存新對象,因爲StringBuffer中字符串長度可變。因爲StringBuffer需要保證線程安全,所以加鎖。
6)參數爲StringBuilder
String.java中的相關代碼:
public String(StringBuilder builder) {
this.value = Arrays.copyOf(builder.getValue(), builder.length());
}
根據builder中保存的字符串的值創建一個新對象並保存新對象,因爲StringBuilder中字符串長度可變。因爲StringBuilder不需要保證線程安全,所以不加鎖。
3. length方法
獲取字符串長度。
String.java中的相關代碼:
public int length() {
return value.length;
}
返回字符數組的長度。
4.isEmpty方法
判斷字符串是否爲空。
String.java中的相關代碼:
public boolean isEmpty() {
return value.length == 0;
}
通過比較字符數組長度進行判斷。
5. charAt方法
獲取字符串中指定位置的字符。
String.java中的相關代碼:
public char charAt(int index) {
// 若指定的位置超過了數組的邊界,則拋出異常
if ((index < 0) || (index >= value.length)) {
throw new StringIndexOutOfBoundsException(index);
}
// 滿足條件,返回字符數組中指定位置的字符
return value[index];
}
6.getBytes方法
指定字符集,獲取字符串對應的字節數組。
String.java中的相關代碼:
public byte[] getBytes(Charset charset) {
// 若指定的字符集爲空,則拋出異常
if (charset == null) throw new NullPointerException();
// 滿足條件,根據字符集將字符數組編碼成字節數組並返回
return StringCoding.encode(charset, value, 0, value.length);
}
7.equels方法
比較兩個String對象是否相等。
String.java中的相關代碼:
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) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
// 循環 比較每一位的字符是否相等
while (n-- != 0) {
// 若發現有不相等的字符,則返回false
if (v1[i] != v2[i])
return false;
i++;
}
// 若全部相等,則返回true;
return true;
}
}
// 待比較的對象不是String類型,返回false
return false;
}
8. compareTo方法
比較兩個字符串誰包含的字符範圍更大
String.java中的相關代碼:
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++;
}
// 若兩個字符串長度不相等,且最小長度內的按位比較,兩者都相等
// 則返回兩個字符串的長度之差 eg: abcdef和abc。
return len1 - len2;
}
9. compareToIgnoreCase方法
在忽略字符大小寫的情況下,比較兩個字符串誰包含的字符範圍更大
String.java中的相關代碼:
public int compareToIgnoreCase(String str) {
return CASE_INSENSITIVE_ORDER.compare(this, str);
}
調用了全局變量CASE_INSENSITIVE_ORDER的compare方法。
CASE_INSENSITIVE_ORDER是一個CaseInsensitiveComparator類型的對象。
CaseInsensitiveComparator是String的靜態內部類。
String.java中的相關代碼:
private static class CaseInsensitiveComparator
implements Comparator<String>, java.io.Serializable {
// 用於序列化
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++) {
// 獲取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) {
// 返回兩個字符的差值
return c1 - c2;
}
}
}
}
// 若兩個字符串長度不相等,且最小長度內的按位比較,兩者都相等
// 則返回兩個字符串的長度之差 eg: abcdef和abc。
return n1 - n2;
}
// 返回全局變量,用於替換反序列化的對象
private Object readResolve() { return CASE_INSENSITIVE_ORDER; }
}
爲什麼兩個字符大寫形式比較不相等,還要比較兩個字符的小寫形式?
官方說法是因爲格魯吉亞字母的存儲規則和英文字母不同。
10. regionMatches方法
判斷兩個從不同起始位置開始的字符串,在一定的長度內的值是否相等
String.java中的相關代碼:
public boolean regionMatches(boolean ignoreCase, int toffset,
String other, int ooffset, int len) {
// 第一個字符串的字符數組形式
char ta[] = value;
// 第一個字符串的起始位置
int to = toffset;
// 第二個字符串的字符數組形式
char pa[] = other.value;
// 第二個字符串的起始位置
int po = ooffset;
// 若不滿足邊界條件,則返回false
// 採用減法形式比較,因爲相加後的結果可能超過最大值-1>>>1,導致程序崩潰
if ((ooffset < 0) || (toffset < 0)
|| (toffset > (long)value.length - len)
|| (ooffset > (long)other.value.length - len)) {
return false;
}
// 循環比較
while (len-- > 0) {
// 獲取對應位置字符
char c1 = ta[to++];
char c2 = pa[po++];
// 若相等,則跳過本次循環
if (c1 == c2) {
continue;
}
// 若允許忽略字符的大小寫形式
if (ignoreCase) {
// 將二者變成大寫形式
char u1 = Character.toUpperCase(c1);
char u2 = Character.toUpperCase(c2);
// 若相等,則跳過本次循環
if (u1 == u2) {
continue;
}
// 若二者的大寫形式相等,則跳過本次循環
if (Character.toLowerCase(u1) == Character.toLowerCase(u2)) {
continue;
}
}
// 若還是不相等,則返回false
return false;
}
// 最後返回true
return true;
}
11. equalsIgnoreCase方法
忽略大小寫的情況下比較兩個字符串是否相等
String.java中的相關代碼:
public boolean equalsIgnoreCase(String anotherString) {
return (this == anotherString) ? true
: (anotherString != null)
&& (anotherString.value.length == value.length)
&& regionMatches(true, 0, anotherString, 0, value.length);
}
若兩個字符串爲同一個對象,則返回true。
否則需要同時滿足三個條件:1)另一個字符串不爲空。2)兩個字符串長度相等。3)忽略大小寫時,兩個字符串對應位置的字符相等。
12. startsWith方法
判斷一個字符串從頭開始的連續長度內的字符是否和指定字符串的內容相等。
String.java中的相關代碼:
public boolean startsWith(String prefix) {
return startsWith(prefix, 0);
}
調用了重載的方法。
判斷一個字符串從指定位置開始的連續長度內的字符是否和指定的字符串中的字符匹配。
String.java中的相關代碼:
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;
// 若不滿足邊界條件,則返回false
if ((toffset < 0) || (toffset > value.length - pc)) {
return false;
}
// 循環比較
while (--pc >= 0) {
// 若不相等,則返回false
if (ta[to++] != pa[po++]) {
return false;
}
}
// 全部相等,返回true
return true;
}
13. endsWith方法
判斷一個字符串從末尾向前的連續長度內的字符是否和指定字符串的內容相等。
String.java中的相關代碼:
public boolean endsWith(String suffix) {
// 用兩個字符串的長度做差,得出起始位置
return startsWith(suffix, value.length - suffix.value.length);
}
14. hashCode方法
求一個字符串的哈希值。
String.java中的相關代碼:
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;
}
計算公式爲s[0]*31^(n-1) + s[1]*31^(n-2) + … + s[n-1],n爲字符串的長度。
15. indexOf方法
獲取字符串中指定的字符首次出現的位置。
String.java中的相關代碼:
public int indexOf(int ch) {
return indexOf(ch, 0);
}
調用了重載方法。
獲取字符串中指定起始位置開始,指定的字符首次出現的位置。
String.java中的相關代碼:
public int indexOf(int ch, int fromIndex) {
// 獲取字符串長度
final int max = value.length;
// 若起始位置小於0
if (fromIndex < 0) {
// 則默認起始位置爲0
fromIndex = 0;
// 或起始位置超過字符串的長度
} else if (fromIndex >= max) {
// 返回-1,說明指定的字符在字符串中沒有出現
return -1;
}
// 若字符ch爲BMP字符,即佔用空間爲兩個字節
// 詳解在1)處
if (ch < Character.MIN_SUPPLEMENTARY_CODE_POINT) {
// 字符串的字符數組
final char[] value = this.value;
// 從起始位置開始循環
for (int i = fromIndex; i < max; i++) {
// 若有相等的字符出現,則返回出現的位置
if (value[i] == ch) {
return i;
}
}
// // 返回-1,說明指定的字符在字符串中沒有出現
return -1;
} else { //若ch爲增補字符集中的字符。
// 詳解在2)處
return indexOfSupplementary(ch, fromIndex);
}
}
1)UTF-16編碼方式下,爲每16bit表示一個字符,一共可以表示65536種字符,這種佔用兩個字節的字符稱爲BMP字符。但是Unicode編碼方式爲了表示更多的字符,同時還要用16bit表示,於是引入了Surrogates,即從UTF-16的65536種字符中選擇2048種,讓這些字符兩個爲一組來表示超出65536的字符。進一步將這2048種字符分成High Surrogates和Low Surrogates兩部分,每部分1024種字符,一共可以表示1048576種字符。
UTF-16編碼下的High Surrogates和Low Surrogates通過一定的規則計算,可以得到16bit的Unicode編碼下的字符。
2)indexOfSupplementary方法
String.java中的相關代碼:
private int indexOfSupplementary(int ch, int fromIndex) {
if (Character.isValidCodePoint(ch)) {
final char[] value = this.value;
// 獲取High Surrogate
final char hi = Character.highSurrogate(ch);
// 獲取Low Surrogates
final char lo = Character.lowSurrogate(ch);
// 獲取字符串長度
final int max = value.length - 1;
// 循環比較
for (int i = fromIndex; i < max; i++) {
// 若High Surrogates和Low Surrogates都相等,則返回對應位置
if (value[i] == hi && value[i + 1] == lo) {
return i;
}
}
}
// 返回-1,說明指定的字符在字符串中沒有出現
return -1;
}
16. substring方法
從指定的起始位置開始,對字符串進行截取。
String.java中的相關代碼:
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);
}
調用String的構造方法,將字符數組中指定位置和長度的字符變成字符串
String.java中的相關代碼:
public String(char value[], int offset, int count) {
// 若起始位置小於0
if (offset < 0) {
throw new StringIndexOutOfBoundsException(offset);
}
// 若指定長度小於等於0
if (count <= 0) {
// 若指定長度小於0
if (count < 0) {
throw new StringIndexOutOfBoundsException(count);
}
// 若起始位置小於等於字符數組的長度,同時指定長度爲0
if (offset <= value.length) {
// 字符串爲””
this.value = "".value;
// 返回
return;
}
}
// 若終止位置超出了字符數組的長度
if (offset > value.length - count) {
throw new StringIndexOutOfBoundsException(offset + count);
}
// 對字符數組指定範圍的字符進行復制,並保存
this.value = Arrays.copyOfRange(value, offset, offset+count);
}
17. replace方法
將字符串中的某一種字符替換成另一種字符
String.java中的相關代碼:
public String replace(char oldChar, char newChar) {
// 若要替換的字符和替換後的字符不是相同的字符
if (oldChar != newChar) {
// 獲取字符串長度
int len = value.length;
// 用於記錄首個需要替換字符的位置
int i = -1;
// 獲取字符串對應的字符數組
char[] val = value;
// 遍歷查找字符串中是否有需要替換的字符
// 同時記錄首個需要替換的字符的位置
while (++i < len) {
// 若i位置的字符爲需要替換的字符
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];
// 若i位置字符爲待替換的字符,則替換
buf[i] = (c == oldChar) ? newChar : c;
i++;
}
// 根據新創建的字符數組的內容,創建新的字符串,返回
return new String(buf, true);
}
}
// 若要替換的字符和替換後的字符一樣,則不需要替換,直接返回
return this;
}
18.contains方法
判斷一個字符串是否含有某個字符
String.java中的相關代碼:
public boolean contains(CharSequence s) {
// 若字符串中字符s第一次出現的位置大於-1,則包含字符s
return indexOf(s.toString()) > -1;
}
19.split方法
根據給出指定的字符或正則表達式規則,將字符串拆分成數組
String.java中的相關代碼:
public String[] split(String regex) {
return split(regex, 0);
}
調用了重載方法。
String.java中的相關代碼:
public String[] split(String regex, int limit) {
// 拆分的界限,即以ch爲界限進行拆分
char ch = 0;
// 對ch賦值,同時若滿足以下條件
// 詳解在1)處
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;
// ch字符出現的位置
int next = 0;
// 表示對拆分結果的數量是否有限制
boolean limited = limit > 0;
// 用於保存拆分後的結果
ArrayList<String> list = new ArrayList<>();
// 循環,從off位置開始,查找字符ch首次出現的位置,並賦值給next
while ((next = indexOf(ch, off)) != -1) {
// 如果對結果數量沒有限制
// 或者有限制的情況下,已經拆分出來的結果數量小於限制數量減1
// 減1是因爲如果對結果數量有限制,
// 最後一次需要把剩餘的字符全部添加到list,不需拆分,該過程在else處
if (!limited || list.size() < limit - 1) {
// 對字符串截取,並保存
list.add(substring(off, next));
// 設置下一次查找的起始位置
off = next + 1;
} else {
// 若對結果數量有限制,同時已經拆分出來的結果數量等於限制數量減1
// 即本次爲有限制情況下的最後一次
// 對字符串截取,從off開始截取到最後,保存
list.add(substring(off, value.length));
// 設置下一次查找的起始位置
off = value.length;
// 跳出循環
break;
}
}
// 若起始位置爲0,說明沒有進行上面的循環
// 說明字符串中不包含用於劃分界限字符
if (off == 0)
// 返回值爲自身的新對象
return new String[]{this};
// 若沒有限制結果數量,或拆分出的字符串的數量小於限制要求
// 這裏用於添加在沒有限制的情況下的最後一個拆分出的字符串,
// 因爲上面循環在最後一次時,next=-1跳出循環,沒有添加到list中
// 同時,若存在限制,且限制的數量大於等於實際可以拆分出的結果數量
// 也需要在這裏添加最後一個拆分出的字符串
if (!limited || list.size() < limit)
// 截取,添加
list.add(substring(off, value.length));
// 獲取拆分結果的數量
int resultSize = list.size();
// 若limit爲0,說明沒有限制
if (limit == 0) {
// 從後向前剔除末尾長度爲0的字符串
// 直到末尾出現第一個長度不爲0的字符串,跳出循環
// 因爲當界限字符連續出現在末尾,會產生長度爲0的字符串
// 這些字符串沒有意義,需要剔除
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
// 減一,記錄有效結果的數量
resultSize--;
}
}
// 創建字符串數組
String[] result = new String[resultSize];
// 根據有效結果的數量對list截取,複製結果到字符串數組,返回
return list.subList(0, resultSize).toArray(result);
}
// 若regex爲正則表達式,則交給Pattern類處理
return Pattern.compile(regex).split(this, limit);
}
1)滿足條件
若regex長度爲1,則對ch賦值,同時ch不能爲 .、$、|、(、)、[、{、^、?、*、+、\這些字符,因爲這些字符是正則表達式中的內容。\爲\的轉義字符。
若regex長度爲2,則regex的第一個字符必須爲\,ch的值爲regex第二個字符。同時ch只能爲0-9、a-z、A-Z之間的字符,代表regex轉義字符。
最後,無論regex長度爲1還是2,ch必須爲UTF-16字符集中除了用於構成Unicode中擴展字符集的其他字符。
regex其它情況當作正則表達式處理。
20. trim方法
去除字符串前面和後面的空格,字符串中間的空格不做處理。
String.java中的相關代碼:
public String trim() {
// 獲取字符串長度
int len = value.length;
// 設置起始位置
int st = 0;
// 獲取字符串的字符數組
char[] val = value;
// ASCII碼錶小於空格的符號都是一些分隔符,控制符等。
// 當小於等於空格,且長度滿足要求
while ((st < len) && (val[st] <= ' ')) {
// 起始位置自增,跳過這些符號
st++;
}
// 同理
while ((st < len) && (val[len - 1] <= ' ')) {
// 從後向前,跳過這些符號
len--;
}
// 若起始位置大於0或終止位置小於字符串長度
// 則截取並返回,否則返回自身
return ((st > 0) || (len < value.length)) ? substring(st, len) : this;
}
21. toCharArray方法
將字符串轉換爲字符數組。
String.java中的相關代碼:
public char[] toCharArray() {
// 創建新字節數組
char result[] = new char[value.length];
// 複製字符串的字節數據到新數組
System.arraycopy(value, 0, result, 0, value.length);
// 返回新數組
return result;
}
這裏複製數組內容不使用Arrays的copyOf方法,因爲String類加載完成時,Arrays類還沒有被加載,這時候調用,會拋出異常。
22. valueOf方法
1)參數爲Object
將Object類型的對象轉化爲String類型
String.java中的相關代碼:
public static String valueOf(Object obj) {
// 若不爲空,調用toString方法並返回,否則返回null
return (obj == null) ? "null" : obj.toString();
}
2)參數爲char[]
將char[]類型的對象轉化爲String類型
String.java中的相關代碼:
public static String valueOf(char data[]) {
// 直接創建新String對象並返回
return new String(data);
}
3)參數爲boolean
將boolean類型的對象轉化爲String類型
String.java中的相關代碼:
public static String valueOf(boolean b) {
// 若爲true,則返回“true”,否則返回“false”
return b ? "true" : "false";
}
4)參數爲int
將int類型的對象轉化爲String類型
String.java中的相關代碼:
public static String valueOf(int i) {
// 調用int的包裝類Integer的toString方法
return Integer.toString(i);
}
參數爲float、double、long時同理,都是通過調用其包裝類的靜態方法toString實現。